You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat: NOUS_CAMPAIGN_PARENT env var relocates campaign artifacts outside target repo (#239)
Closes#239.
Today's friction: campaign work_dirs default to `<target_repo>/.nous/<run_id>/`,
where they live as **untracked files** in the target repo's working tree.
This causes:
- `git stash -u` silently captures multi-GB of campaign output (recently
caused 4.7 GB of ea-control-stack JSONs to be stashed; recovery required
digging through `stash@{0}^3` and unstaging 60+ files)
- `git status` clutter from thousands of untracked entries
- `git add .` and `git checkout <commit> -- <path>` silently stage
campaign content
- PR review noise
Fix:
1. Add `NOUS_CAMPAIGN_PARENT` env var. When set, work_dir lives at
`$NOUS_CAMPAIGN_PARENT/<run_id>/`, fully outside the target.
2. Add `work_dir` field to state.json — per-campaign source of truth
for location, robust to env var changes between runs.
3. Worktrees are NOT affected — they still live at
`<target>/.nous-experiments/<run>/<arm>/` per #133, because they
ARE code FOR the target repo and must share its git history.
Changes:
- New `orchestrator/work_dir_resolver.py` (73 LOC): single source of
truth for "where would this run_id live, given today's environment?"
- `orchestrator/iteration.py:setup_work_dir` uses the resolver and
records the resolved absolute path in state.json's new `work_dir`
field.
- `orchestrator/cli.py:resolve_work_dir` and `_cmd_run` use the same
resolver — keeps all three call sites consistent (avoids the silent
"work_dir mismatch" trap from #184).
- `orchestrator/templates/state.json` adds `work_dir: null` (set on
setup).
- `orchestrator/schemas/state.schema.json` extends to permit `work_dir`.
- `orchestrator/create_campaign.py:_TEMPLATE` documents the env var
for new campaigns.
- `README.md` adds a recommended-setup section.
- `tests/test_work_dir_resolver.py`: 12 behavioral tests covering both
env-var-set and env-var-unset paths, state.json record robustness,
expanduser, idempotency. No live LLM calls (per CLAUDE.md).
Backward-compat: when `NOUS_CAMPAIGN_PARENT` is unset, behavior is
byte-identical to today's legacy `<repo>/.nous/<run_id>/`. All 1,195
existing tests still pass.
Out of scope (per-user / per-campaign one-time work):
- Migrating existing campaigns: `mv <target>/.nous/<name> $NOUS_CAMPAIGN_PARENT/<name>`
- Updating per-campaign internal scripts to self-locate via __file__
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address PR review: harden NOUS_CAMPAIGN_PARENT resolver + close in-progress detection trap (#239)
Comprehensive fixup addressing all 12 issues identified by review agents.
CRITICAL fixes:
- C1 / silent-failure-hunter C3: `_cmd_run` now uses
`find_existing_work_dir` to check BOTH legacy and env-var locations
for in-progress detection. Fixes the regression where toggling
NOUS_CAMPAIGN_PARENT between runs could silently allow a parallel
run that corrupts shared worktrees.
- C2 / silent-failure-hunter C1: empty/whitespace `NOUS_CAMPAIGN_PARENT`
now raises ValueError instead of silently falling through to legacy.
Bash `export NOUS_CAMPAIGN_PARENT=$UNSET` is a common typo that the
user explicitly tried to opt out of.
- C3 / pr-test-analyzer gap #2: `tests/conftest.py` autouse fixture
now strips `NOUS_CAMPAIGN_PARENT` so existing tests don't fail
for contributors who follow README's recommendation to export it.
- C4 / comment-analyzer #1: README "byte-identical" claim corrected;
scope tightened to "the resolved work_dir is unchanged".
IMPORTANT fixes:
- I1 / pr-test-analyzer gap #1: 4 new CLI tests cover yaml + bare-run-id
with env-var, including the test that would have caught C1.
- I2 / comment-analyzer #3 / silent-failure-hunter C4: README adds
migration command (`mv <repo>/.nous/<run> $NOUS_CAMPAIGN_PARENT/<run>`).
`_cmd_run` emits a migration hint when a campaign is found at the
legacy path while env var is set.
- I3 / silent-failure-hunter I5: state.json's `work_dir` field is now
read by `find_existing_work_dir` — prefers the recorded path when
it points to an existing campaign elsewhere (handles post-creation
`mv`).
- I4 / comment-analyzer #10: `cli.resolve_work_dir` legacy branch
delegates to the resolver via `find_existing_work_dir` instead of
reimplementing path logic.
- I5 / silent-failure-hunter I1: `repo_path` validated for existence
in resolver; raises FileNotFoundError on typos.
- I6 / code-reviewer #2: explicit `Path(...)` wrap restored at CLI
boundary for type robustness.
- I7 / pr-test-analyzer gap #3: schema validator regression test
added; state.json shape verified post-setup.
- I8 / silent-failure-hunter D1: collision detection. `setup_work_dir`
refuses to clobber a same-named campaign at the env-var path that
records a different `repo_path`. Adds `repo_path` field to state.json
schema.
Suggestion fixes:
- comment-analyzer #2: Resolution rules consolidated in
`work_dir_resolver.py:RESOLUTION RULES` marker comment; other
docstrings reference it. Reduces drift across 5 places.
- comment-analyzer #5: schema description tightened (drop
implementation detail; mark fields as machine-local).
- comment-analyzer #7: `_TEMPLATE` bullet structure restructured for
unambiguous conditional reading.
- comment-analyzer #4: cross-machine portability story documented
in schema descriptions (machine-local with caveat).
- comment-analyzer #8: README line 166 (post-#239) updated to
reflect the conditional default.
- silent-failure-hunter I2: setup_work_dir wraps mkdir with OSError
surfacing env-var context.
- silent-failure-hunter I4: `_find_repo_root` failure message now
mentions `NOUS_CAMPAIGN_PARENT` as an alternative.
- pr-test-analyzer suggestions: edge-case tests added (whitespace
env var, trailing slash, env-var-change scenario, corrupt state.json,
recorded-path-wins, recorded-path-nonexistent-falls-back).
Schema additions:
- `state.json` gains `work_dir` and `repo_path` fields (both
nullable strings; absolute paths recorded at setup_work_dir).
- `state.schema.json` extends with both fields and machine-local
caveats in descriptions.
Test count:
- Before: 12 tests (work_dir_resolver only)
- After: 32 tests covering resolver + setup + CLI + schema validation
- Full suite: 1,215 pass, 2 skip (was 1,195/2/0 — 20 new tests, 0 regressions).
Backward compatibility preserved:
- `NOUS_CAMPAIGN_PARENT` unset → resolved path is unchanged
- Pre-#239 campaigns at `<repo>/.nous/<run>/` continue to be found
by `find_existing_work_dir` even when env var is set (migration
grace).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: README.md
+25-1Lines changed: 25 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -79,6 +79,30 @@ export OPENAI_BASE_URL=https://your-litellm-proxy.example.com # or any OpenAI-c
79
79
80
80
If you're using Anthropic directly via a LiteLLM proxy, point both vars at the proxy. If these aren't set, gate summaries and report generation are skipped (non-fatal). The campaign still runs — you just won't get LLM-generated summaries at the gates or a final report.
81
81
82
+
**Recommended: relocate campaign artifacts outside the target repo (#239).**
83
+
84
+
By default, Nous creates each campaign's working directory at `<target_repo>/.nous/<run_id>/`. That puts campaign output (state, ledger, principles, JSON results, findings) inside the target repo's working tree as untracked files — which means `git stash -u` silently captures them, `git status` shows thousands of unrelated entries, and `git add .` accidentally stages campaign content. To avoid this, set:
When `NOUS_CAMPAIGN_PARENT` is set, campaign artifacts live at `$NOUS_CAMPAIGN_PARENT/<run_id>/` — wholly outside the target repo. Code worktrees (per-arm BLIS branches, #133) continue to live at `<target>/.nous-experiments/<run_id>/<arm>/` because they ARE code FOR the target. The target repo's working tree stays clean. Backward-compat: when the env var is unset, the resolved `work_dir` is unchanged — `<repo_path>/.nous/<run_id>/`, exactly as today.
92
+
93
+
The resolved absolute `work_dir` and `repo_path` are recorded in each campaign's `state.json` so the campaign location and target survive env-var changes between runs. `nous resume` and `nous status` look up campaigns at both the env-var location AND the legacy location, so existing pre-#239 campaigns continue to work without immediate migration.
94
+
95
+
**Migrating existing campaigns** to the new location is a one-time operation per campaign:
# 3. Continue using the campaign as before. Nous will find it at the new location.
102
+
```
103
+
104
+
state.json's recorded `work_dir` will be stale until the campaign's next setup; that's fine — `find_existing_work_dir` checks the actual directory existence, not just state.json's recorded path.
105
+
82
106
### 1. Install Nous
83
107
84
108
```bash
@@ -139,7 +163,7 @@ target_system:
139
163
repo_path: /path/to/your/repo
140
164
```
141
165
142
-
When `repo_path` is set, the campaign directory is created inside the target repo at `.nous/<run_id>/`. All artifacts live there.
166
+
When `repo_path` is set, the campaign directory is created at `$NOUS_CAMPAIGN_PARENT/<run_id>/` if you've set that env var (recommended — see [Environment setup](#environment-setup) above), or otherwise at the legacy `<target_repo>/.nous/<run_id>/`. All artifacts live there.
143
167
144
168
To discover the full schema (required vs optional fields, descriptions
Copy file name to clipboardExpand all lines: orchestrator/schemas/state.schema.json
+8Lines changed: 8 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -37,6 +37,14 @@
37
37
"config_ref": {
38
38
"type": ["string", "null"],
39
39
"description": "Path to the campaign configuration file (campaign.yaml). Null before setup."
40
+
},
41
+
"work_dir": {
42
+
"type": ["string", "null"],
43
+
"description": "Absolute filesystem path to the campaign's work_dir, recorded at setup_work_dir (#239). Per-campaign source of truth for location, robust to NOUS_CAMPAIGN_PARENT changes between runs. **Machine-local**: tools that travel state.json across machines must validate Path(work_dir).exists() before trusting it (or rederive from the local environment). Null before setup."
44
+
},
45
+
"repo_path": {
46
+
"type": ["string", "null"],
47
+
"description": "Absolute path to the target system's repo, recorded at setup_work_dir (#239). Used for collision detection (refusing to clobber a same-named campaign that targets a different repo when NOUS_CAMPAIGN_PARENT is set) and to identify which target a campaign belongs to. **Machine-local**, same caveats as work_dir. Null before setup or when the campaign was authored without a target_system.repo_path."
0 commit comments