Contain outputs under an output root (zwill_work/)#70
Conversation
The research-agent run flung report files, bundles, and exports across the working directory. Guidance in a doc is a weak guardrail; make the system set the location instead. Add resolve_output_path(): relative --path/--out and CWD-relative defaults (e.g. <survey>_report/, artifacts/...) rebase under an output root, default zwill_work/ beside .zwill/. Override with the ZWILL_OUT env var or `zwill init --output-dir` (persisted in config.json). Absolute paths are written verbatim (an escape hatch) with a one-line warning that they left the root. When a path is rebased its parent dir is created, so callers that assumed a CWD-relative parent still have a writable location. Deliberately NOT rebased, to avoid breaking read-back chains and misplacing state: - managed state stays in .zwill/; - intermediate EDSL plumbing read back by a later command — edsl-export/edsl-run --path (job/results) and --job-path — stays at CWD so `edsl-run --job <file>` still finds it; - explicit directory args (--output-dir, --workdir, --artifacts-dir) that hold job plumbing or whole project dirs stay at CWD; - --input-path reads are never rebased. Rebased flags are the deliverable file outputs: path, out, report-html/json/csv, results-path, prompt-path, context-path, markdown-path, json-path, option-path, zip-path. Enabled by the earlier --input-path rename, which made every --path/--out unambiguously an output. Adds tests/test_output_root.py and documents the convention in the agent workflow guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR introduces a centralized output root (
Confidence Score: 3/5The core output-root machinery and the bulk of the command-module updates are correct, but the three workflow commands now look for the workflow config file under zwill_work/ instead of the current directory, making them immediately non-functional for the common case of a CWD-relative workflow file. The rebase logic in cli.py is well-designed and the test coverage is solid. The majority of the 15 changed modules apply resolve_output_path correctly. However, workflow_commands.py applies it to a positional argument that is a workflow configuration file the command reads via load_workflow_file(path), not a file it writes. Running zwill workflow run my_workflow.yaml will fail with a file-not-found because it searches zwill_work/my_workflow.yaml instead. zwill/workflow_commands.py needs the resolve_output_path calls removed from cmd_workflow_explain, cmd_workflow_dry_run, and cmd_workflow_run — all three use path to READ an existing workflow file, not to write an output. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["User supplies --path / --out\n(or command default)"] --> B{Is path None?}
B -->|yes| Z[return None]
B -->|no| C{Is path under .zwill/?}
C -->|yes| D[Return as-is\nmanaged state]
C -->|no| E{Is path absolute?}
E -->|yes, inside root| F[Return as-is]
E -->|yes, outside root| G[Warn once to stderr\nReturn as-is]
E -->|no| H{Already under output_root?}
H -->|yes| I[Return as-is\nno double-nest]
H -->|no| J[Rebase: root / path]
J -->|create_parents=True| K[mkdir parents\nthen return rebased path]
J -->|create_parents=False| L[Return rebased path\nno mkdir side-effect]
M["output_root() resolution"] --> N{ZWILL_OUT env var set?}
N -->|yes| O[Use ZWILL_OUT]
N -->|no| P{.zwill/config.json\nhas output_dir?}
P -->|yes| Q[Use configured output_dir]
P -->|no| R[Use default: zwill_work/]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["User supplies --path / --out\n(or command default)"] --> B{Is path None?}
B -->|yes| Z[return None]
B -->|no| C{Is path under .zwill/?}
C -->|yes| D[Return as-is\nmanaged state]
C -->|no| E{Is path absolute?}
E -->|yes, inside root| F[Return as-is]
E -->|yes, outside root| G[Warn once to stderr\nReturn as-is]
E -->|no| H{Already under output_root?}
H -->|yes| I[Return as-is\nno double-nest]
H -->|no| J[Rebase: root / path]
J -->|create_parents=True| K[mkdir parents\nthen return rebased path]
J -->|create_parents=False| L[Return rebased path\nno mkdir side-effect]
M["output_root() resolution"] --> N{ZWILL_OUT env var set?}
N -->|yes| O[Use ZWILL_OUT]
N -->|no| P{.zwill/config.json\nhas output_dir?}
P -->|yes| Q[Use configured output_dir]
P -->|no| R[Use default: zwill_work/]
|
| @@ -137,7 +137,7 @@ def cmd_workflow_dry_run(args: argparse.Namespace) -> dict[str, Any]: | |||
|
|
|||
|
|
|||
| def cmd_workflow_run(args: argparse.Namespace) -> dict[str, Any]: | |||
| path = Path(args.path) | |||
| path = resolve_output_path(args.path) | |||
| config = load_workflow_file(path) | |||
| values = workflow_vars(config, args.var) | |||
| steps = rendered_workflow_steps(config, values) | |||
There was a problem hiding this comment.
Workflow
path is an input, not an output — rebasing it breaks all workflow commands
The path positional argument for workflow explain, workflow dry-run, and workflow run points to an existing workflow YAML/JSON configuration file that is only ever READ (via load_workflow_file(path)). Applying resolve_output_path rebases it under zwill_work/, so zwill workflow run my_workflow.yaml now looks for zwill_work/my_workflow.yaml rather than ./my_workflow.yaml, causing a file-not-found error for any workflow file at a CWD-relative path.
The parser confirms this is a positional input: p.add_argument("path", help="Path to a workflow .json/.yaml/.yml file."). The PR's own design rule states inputs are never rebased; this is exactly the kind of path the --input-path rename in #69 was meant to disambiguate — but workflow's positional path was not renamed and is now incorrectly treated as an output.
What
Reports, bundles, executive summaries, and exported CSVs now land under a single output root (default
zwill_work/, beside.zwill/) instead of sprawling across the working directory — the file-sprawl the research-agent run hit. A doc convention is a soft guardrail; this makes the system set the location.resolve_output_path():--path/--outand CWD-relative defaults (<survey>_report/,artifacts/...) → rebased under the rootZWILL_OUTenv >.zwill/config.jsonoutput_dir(set viazwill init --output-dir) > defaultzwill_work/Deliberately not rebased
To avoid breaking read-back chains and misplacing state (found empirically — the example scripts'
edsl-export --path job.json→edsl-run --job job.jsonchain broke when the write rebased but the read didn't):.zwill/edsl-export/edsl-run--path(job/results) and--job-path— stays at CWD--output-dir,--workdir,--artifacts-dir) that hold job plumbing or whole project dirs stay at CWD--input-pathreads are never rebasedRebased (deliverable file outputs):
path, out, report-html/json/csv, results-path, prompt-path, context-path, markdown-path, json-path, option-path, zip-path.This is enabled by the earlier
--input-pathrename (#69), which made every--path/--outunambiguously an output.Behavior
Testing
pytest— 248 passed (addstests/test_output_root.py: rebase, absolute-passthrough-with-single-warning,.zwillnever rebased, no double-nesting, env override,init --output-dirpersistence).ruffclean.🤖 Generated with Claude Code