@zenera/cli 1.1.21 → 1.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +40 -3
  2. package/dist/archive.d.ts +139 -0
  3. package/dist/archive.js +376 -0
  4. package/dist/banner.d.ts +33 -24
  5. package/dist/banner.js +25 -6
  6. package/dist/commands/check.js +1 -0
  7. package/dist/commands/export.d.ts +3 -0
  8. package/dist/commands/export.js +255 -0
  9. package/dist/commands/import.d.ts +3 -0
  10. package/dist/commands/import.js +173 -0
  11. package/dist/commands/index.d.ts +5 -1
  12. package/dist/commands/index.js +13 -1
  13. package/dist/commands/init.js +1 -0
  14. package/dist/commands/inspect.js +1 -0
  15. package/dist/commands/key.js +1 -0
  16. package/dist/commands/list.js +1 -0
  17. package/dist/commands/memory.js +118 -10
  18. package/dist/commands/open.js +1 -0
  19. package/dist/commands/run.js +3 -0
  20. package/dist/commands/sandbox.js +1 -0
  21. package/dist/commands/version.js +1 -0
  22. package/dist/lib.d.ts +2 -1
  23. package/dist/lib.js +2 -1
  24. package/dist/meta.d.ts +1 -1
  25. package/dist/meta.js +5 -28
  26. package/dist/resolve.js +19 -12
  27. package/dist/sandbox.js +1 -0
  28. package/dist/scaffold.d.ts +8 -0
  29. package/dist/scaffold.js +7 -2
  30. package/dist/term.d.ts +2 -0
  31. package/dist/term.js +2 -0
  32. package/dist/tui/markdown.d.ts +16 -0
  33. package/dist/tui/markdown.js +259 -0
  34. package/dist/tui/theme.js +1 -1
  35. package/dist/tui/wrap.d.ts +1 -1
  36. package/dist/validate.js +150 -4
  37. package/package.json +12 -6
  38. package/templates/editor/.github/copilot-instructions.md +62 -33
  39. package/templates/editor/.github/prompts/spec-sync-project.prompt.md +56 -3
  40. package/templates/editor/.github/skills/zen-cli/SKILL.md +21 -16
  41. package/templates/editor/.github/skills/zen-cli/references/check.md +6 -0
  42. package/templates/editor/.github/skills/zen-cli/references/export.md +107 -0
  43. package/templates/editor/.github/skills/zen-cli/references/memory.md +51 -13
  44. package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
  45. package/templates/editor/.github/skills/zen-cli/references/rag.md +43 -3
  46. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +30 -2
  47. package/templates/editor/.github/skills/zen-instructions/SKILL.md +232 -0
  48. package/templates/editor/.github/skills/zen-memory/SKILL.md +2 -1
  49. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +684 -0
  50. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +56 -23
  51. package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +55 -28
  52. package/templates/editor/.github/skills/zen-review/SKILL.md +17 -1
  53. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +257 -0
  54. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +16 -3
  55. package/templates/editor/.vscode/agents.schema.json +8 -3
  56. package/templates/project/SPECIFICATION.md.tmpl +4 -1
  57. package/templates/project/agents/files-instructions.md +86 -0
  58. package/templates/project/agents/fork-instructions.md +17 -0
  59. package/templates/project/agents/memory-instructions.md +37 -0
  60. package/templates/project/agents.yaml.tmpl +3 -3
  61. package/templates/project/gitignore +12 -1
  62. package/templates/project/scripts/_setup.sh +68 -11
@@ -19,17 +19,19 @@ hits for `limit` are not an answer; the eight lines around one of them are.
19
19
  ## The commands
20
20
 
21
21
  ```
22
- zen rag docs <index|search|list|grep|show|stats> [path...]
22
+ zen rag docs <index|restore|ready|search|list|grep|show|stats> [path...]
23
23
  ```
24
24
 
25
- | Command | Answers | Embedder? | Typical |
26
- | -------- | ------------------------------------------ | --------- | ------- |
27
- | `index` | builds the thing | yes | minutes |
28
- | `search` | _where does this corpus talk about X?_ | **yes** | seconds |
29
- | `list` | _what documents/headings/tables are here?_ | no | instant |
30
- | `grep` | _does the string X appear anywhere?_ | no | instant |
31
- | `show` | _print this section, verbatim_ | no | instant |
32
- | `stats` | _what is in this index?_ | no | instant |
25
+ | Command | Answers | Embedder? | Typical |
26
+ | --------- | ------------------------------------------ | --------- | ------- |
27
+ | `index` | builds the thing | yes | minutes |
28
+ | `restore` | _put the vectors back_ | **yes** | minutes |
29
+ | `ready` | _can this index answer at all?_ | no | instant |
30
+ | `search` | _where does this corpus talk about X?_ | **yes** | seconds |
31
+ | `list` | _what documents/headings/tables are here?_ | no | instant |
32
+ | `grep` | _does the string X appear anywhere?_ | no | instant |
33
+ | `show` | _print this section, verbatim_ | no | instant |
34
+ | `stats` | _what is in this index?_ | no | instant |
33
35
 
34
36
  Only `search` ranks, and only `search` costs a network round trip - it embeds
35
37
  the query before it can compare anything. The other four read `outline.json`
@@ -80,6 +82,35 @@ the answer.)
80
82
  `manifest.json` records **which embedder made the vectors**, so a search with a
81
83
  different model is refused rather than answered with noise.
82
84
 
85
+ ### What survives a clone
86
+
87
+ `lance/` is binary, large, and rebuildable, so a project commits the index and
88
+ git-ignores the vectors. What a fresh checkout holds is therefore an index that
89
+ **looks built and cannot answer**: the manifest is there, `sources/` is there,
90
+ and the first search fails deep in the store with `holds no searchable table`.
91
+
92
+ Two commands close that gap, and neither needs the original documents:
93
+
94
+ ```sh
95
+ zen rag docs ready --dir assets/docs-db # exit 0 searchable, exit 3 not
96
+ zen rag docs restore --dir assets/docs-db # re-embed from sources/
97
+ ```
98
+
99
+ `ready` answers with its **exit code**, so a setup step branches on it without
100
+ parsing anything; `--json` gives the same answer as `{ ready, state, embedding }`
101
+ for a caller that wants the detail. `restore` reads the documents back out of
102
+ `sources/`, re-embeds them with the model the manifest names, and renames the
103
+ result into place - so a failed restore leaves the old index untouched, at the
104
+ cost of room for both while it runs.
105
+
106
+ > **Never test for a built index with `[ -f .../manifest.json ]`.** The manifest
107
+ > is committed and the vectors are not, so that test is true on exactly the
108
+ > machine where it is wrong. `zen rag docs ready` is the test.
109
+
110
+ `restore` is also how an index **changes embedder**: `--embedding <ref>`
111
+ re-embeds everything and rewrites the manifest, so later searches ask for the
112
+ new model. Same corpus, same names, same chunk ids - only the vectors are new.
113
+
83
114
  ### How a document is cut up
84
115
 
85
116
  Chunking follows the markdown rather than a character count: a paragraph, a
@@ -582,17 +613,19 @@ sections down. Cite the document name and the line numbers.
582
613
 
583
614
  ## When it goes wrong
584
615
 
585
- | Symptom | Cause |
586
- | ------------------------------------------------ | ------------------------------------------------------------------------------------- |
587
- | "not installed" | `@zenera/rag` is not resolvable - install it, or use the two-`--package` npx form |
588
- | `nothing to index` | No `.md`/`.markdown`/`.txt`/`.text` under those paths, or they are all hidden |
589
- | A document you expected is missing | Hidden directory, `node_modules`, an unlisted extension, or over 16 MB - check stderr |
590
- | Refused for a different embedding | The index records the model that built it; re-index or pass the right `--embedding` |
591
- | No manifest / not an index | A build that did not finish. `manifest.json` is written last on purpose |
592
- | A build is refused | `.lock` - another build is running. It is taken over when the process is gone |
593
- | `provider "openai": no api key` | `zen key ls` - the keyring, or a real environment variable |
594
- | A usage error before any credential is asked for | Deliberate: everything about the invocation is checked first, so a typo is a typo |
595
- | `no document matched --file` | The pattern is matched against the document **name**; `list files` prints them |
596
- | Nothing matched | Exit 0 with an empty answer. Try fewer words, or `--mode text` for an exact string |
597
- | Answers quoting text that is no longer there | Nothing watches the documents. Re-index after they change |
598
- | A search took ten seconds, using no CPU | One embedding round trip, not the index. `list`/`grep`/`show` make none |
616
+ | Symptom | Cause |
617
+ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
618
+ | "not installed" | `@zenera/rag` is not resolvable - install it, or use the two-`--package` npx form |
619
+ | `nothing to index` | No `.md`/`.markdown`/`.txt`/`.text` under those paths, or they are all hidden |
620
+ | A document you expected is missing | Hidden directory, `node_modules`, an unlisted extension, or over 16 MB - check stderr |
621
+ | Refused for a different embedding | The index records the model that built it; pass the right `--embedding`, or move it with `restore --embedding` |
622
+ | No manifest / not an index | A build that did not finish. `manifest.json` is written last on purpose |
623
+ | `holds no searchable table` | A manifest with no vectors - a clone, since `lance/` is git-ignored. `restore` it |
624
+ | A step reported `skipped` but nothing can search | The step tested for `manifest.json` instead of asking `ready` |
625
+ | A build is refused | `.lock` - another build is running. It is taken over when the process is gone |
626
+ | `provider "openai": no api key` | `zen key ls` - the keyring, or a real environment variable |
627
+ | A usage error before any credential is asked for | Deliberate: everything about the invocation is checked first, so a typo is a typo |
628
+ | `no document matched --file` | The pattern is matched against the document **name**; `list files` prints them |
629
+ | Nothing matched | Exit 0 with an empty answer. Try fewer words, or `--mode text` for an exact string |
630
+ | Answers quoting text that is no longer there | Nothing watches the documents. Re-index after they change |
631
+ | A search took ten seconds, using no CPU | One embedding round trip, not the index. `list`/`grep`/`show` make none |
@@ -18,18 +18,20 @@ word "password" appears in forty places that are not the one you want.
18
18
  ## The commands
19
19
 
20
20
  ```
21
- zen rag schema <index|search|list|grep|trace|show|stats> [spec...]
21
+ zen rag schema <index|restore|ready|search|list|grep|trace|show|stats> [spec...]
22
22
  ```
23
23
 
24
- | Command | Answers | Embedder? | Typical |
25
- | -------- | -------------------------------------- | --------- | ------- |
26
- | `index` | builds the thing | yes | minutes |
27
- | `search` | _what is this API's way to do X?_ | **yes** | seconds |
28
- | `list` | _what methods/types/fields are there?_ | no | instant |
29
- | `grep` | _does the string X appear anywhere?_ | no | instant |
30
- | `trace` | _which call can reach this field?_ | no | instant |
31
- | `show` | _print exactly these things_ | no | instant |
32
- | `stats` | _what is in this index?_ | no | instant |
24
+ | Command | Answers | Embedder? | Typical |
25
+ | --------- | -------------------------------------- | --------- | ------- |
26
+ | `index` | builds the thing | yes | minutes |
27
+ | `restore` | _put the vectors back_ | **yes** | minutes |
28
+ | `ready` | _can this index answer at all?_ | no | instant |
29
+ | `search` | _what is this API's way to do X?_ | **yes** | seconds |
30
+ | `list` | _what methods/types/fields are there?_ | no | instant |
31
+ | `grep` | _does the string X appear anywhere?_ | no | instant |
32
+ | `trace` | _which call can reach this field?_ | no | instant |
33
+ | `show` | _print exactly these things_ | no | instant |
34
+ | `stats` | _what is in this index?_ | no | instant |
33
35
 
34
36
  Only `search` ranks, and only `search` costs a network round trip - it embeds
35
37
  the query before it can compare anything. The other five read `graph.json` off
@@ -108,6 +110,29 @@ schema-db/
108
110
  `manifest.json` records **which embedder made the vectors**, so a search with a
109
111
  different model is refused rather than answered with noise.
110
112
 
113
+ ### What survives a clone
114
+
115
+ `lance/` is binary, large, and rebuildable, so a project commits the index and
116
+ git-ignores the vectors. What a fresh checkout holds is therefore an index that
117
+ **looks built and cannot answer**: the manifest is there, the graph is there,
118
+ and the first search fails with `holds no searchable table`.
119
+
120
+ ```sh
121
+ zen rag schema ready --dir assets/schema-db # exit 0 searchable, exit 3 not
122
+ zen rag schema restore --dir assets/schema-db # re-embed from sources/
123
+ ```
124
+
125
+ `ready` answers with its **exit code**, so a setup step branches on it without
126
+ parsing anything. `restore` rebuilds from the bundled copies in `sources/` and
127
+ renames the result into place, so a failure leaves the old index untouched -
128
+ which means an index built with `--no-sources` cannot be restored at all, and
129
+ says so. `--embedding <ref>` is how an index moves to another model: everything
130
+ is re-embedded and the manifest rewritten.
131
+
132
+ > **Never test for a built index with `[ -f .../manifest.json ]`.** The manifest
133
+ > is committed and the vectors are not, so that test is true on exactly the
134
+ > machine where it is wrong. `zen rag schema ready` is the test.
135
+
111
136
  ## Installing
112
137
 
113
138
  `zen rag` ships in `@zenera/rag`, a sibling of the CLI. It has no binary of its
@@ -134,13 +159,13 @@ have not installed globally.
134
159
  zen rag schema index <spec...> [--embedding <ref>] [-o <dir>] [--batch <n>]
135
160
  ```
136
161
 
137
- | Flag | Default | Meaning |
138
- | ------------------- | ------------- | ------------------------------------------------------------ |
139
- | `--embedding <ref>` | - | Which embedder makes the vectors. Omit it to see the choices |
140
- | `-o`, `--out <dir>` | `./schema-db` | Where the index goes; `$ZEN_SCHEMA_DB` if that is set |
141
- | `--batch <n>` | `96` | Texts per embedding request, and how often progress prints |
142
- | `--no-sources` | - | Keep no copy of the documents; `show --source` rebuilds them |
143
- | `--quiet` | - | No narration |
162
+ | Flag | Default | Meaning |
163
+ | ------------------- | ------------- | ---------------------------------------------------------------------------------------------- |
164
+ | `--embedding <ref>` | - | Which embedder makes the vectors. Omit it to see the choices |
165
+ | `-o`, `--out <dir>` | `./schema-db` | Where the index goes; `$ZEN_SCHEMA_DB` if that is set |
166
+ | `--batch <n>` | `96` | Texts per embedding request, and how often progress prints |
167
+ | `--no-sources` | - | Keep no copy of the documents; `show --source` rebuilds them, and `restore` becomes impossible |
168
+ | `--quiet` | - | No narration |
144
169
 
145
170
  ```sh
146
171
  zen rag schema index openapi.yaml --embedding openai:text-embedding-3-small
@@ -647,14 +672,16 @@ Worked: the invoice total is `Invoice.amount_due` (minor units), returned by
647
672
 
648
673
  ## When it goes wrong
649
674
 
650
- | Symptom | Cause |
651
- | ------------------------------------------------ | ----------------------------------------------------------------------------------- |
652
- | "not installed" | `@zenera/rag` is not resolvable - install it, or use the two-`--package` npx form |
653
- | Refused for a different embedding | The index records the model that built it; re-index or pass the right `--embedding` |
654
- | No manifest / not an index | A build that did not finish. `manifest.json` is written last on purpose |
655
- | `provider "openai": no api key` | `zen key ls` - the keyring, or a real environment variable |
656
- | A usage error before any credential is asked for | Deliberate: everything about the invocation is checked first, so a typo is a typo |
657
- | Nothing matched | Exit 0 with an empty answer. Try fewer words, or `--all` instead of a narrow field |
658
- | Answers about the wrong version of the API | Nothing watches the document. Re-index after it changes |
659
- | A search took ten seconds, using no CPU | One embedding round trip, not the index. `list`/`grep` make none |
660
- | `search <word> <word>` gave ranked nonsense | Bare words after `search` are the QUERY, not a subcommand. You meant `list`/`grep` |
675
+ | Symptom | Cause |
676
+ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
677
+ | "not installed" | `@zenera/rag` is not resolvable - install it, or use the two-`--package` npx form |
678
+ | Refused for a different embedding | The index records the model that built it; pass the right `--embedding`, or move it with `restore --embedding` |
679
+ | No manifest / not an index | A build that did not finish. `manifest.json` is written last on purpose |
680
+ | `holds no searchable table` | A manifest with no vectors - a clone, since `lance/` is git-ignored. `restore` it |
681
+ | A step reported `skipped` but nothing can search | The step tested for `manifest.json` instead of asking `ready` |
682
+ | `provider "openai": no api key` | `zen key ls` - the keyring, or a real environment variable |
683
+ | A usage error before any credential is asked for | Deliberate: everything about the invocation is checked first, so a typo is a typo |
684
+ | Nothing matched | Exit 0 with an empty answer. Try fewer words, or `--all` instead of a narrow field |
685
+ | Answers about the wrong version of the API | Nothing watches the document. Re-index after it changes |
686
+ | A search took ten seconds, using no CPU | One embedding round trip, not the index. `list`/`grep` make none |
687
+ | `search <word> <word>` gave ranked nonsense | Bare words after `search` are the QUERY, not a subcommand. You meant `list`/`grep` |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: zen-review
3
- description: Checking a project before committing - the four mechanical checks `/project-review` makes before any judgement (`zen check`, the §2.5 path sweep, the memory house-rules copy, the spec-sync record), what each one owns, what their exit codes mean, and what deliberately has no check because no tool can make it. Load before running `/project-review`, before saying a project is ready, and whenever a review finding needs verifying rather than restating.
3
+ description: Checking a project before committing - the four mechanical checks `/project-review` makes before any judgement (`zen check`, the §2.5 path sweep, the memory house-rules copy, the spec-sync record), what each one owns, what their exit codes mean, which findings hand off to `zen-instructions`, and what deliberately has no check because no tool can make it. Load before running `/project-review`, before saying a project is ready, and whenever a review finding needs verifying rather than restating.
4
4
  ---
5
5
 
6
6
  # Reviewing a project
@@ -92,6 +92,22 @@ cause and the least believable finding.
92
92
  `agents/memory-policy-instructions.md`, which filename order puts directly after
93
93
  the copy - see the `zen-memory` skill.
94
94
 
95
+ ## Findings that are not yours to fix in place
96
+
97
+ `zen check` reports `rules.stale`, `files.uninstructed`, `memory.uninstructed`,
98
+ `fork.uninstructed`, `rules.requires.unknown` or `rules.unreached` against a
99
+ file under `agents/`, and the obvious repair - open it and edit it - is wrong for
100
+ four of those files. `tools-instructions.md`, `files-instructions.md`,
101
+ `memory-instructions.md` and `fork-instructions.md` are `zen`'s copies:
102
+ `zen check --fix` replaces them, so an edit made inside one is gone at the next
103
+ fix, `init` or `open`.
104
+
105
+ **Load `zen-instructions` before acting on any of those findings**, and before
106
+ judging a house rule at all. It owns which documents belong to whom, where a
107
+ project's own rules on the same subject go instead, what `requires:` does, and
108
+ what filename order decides. A review that fixes one of ours in place has
109
+ undone itself by the next `zen open` and left nothing behind to say so.
110
+
95
111
  ## What has no check, on purpose
96
112
 
97
113
  Not because it would be hard, but because the answer would be wrong:
@@ -0,0 +1,257 @@
1
+ ---
2
+ name: zen-sandbox
3
+ description: How the container sandbox works, when and how to configure it in `agents.yaml` (`sandbox:` and `agents[].sandbox`), the four `sandbox:*` tools, mount layout (/workspace, $HOME, /tmp, /run/zenera/keys), lifecycle and persistence (`persist: true`), credential forwarding by name, resource ceilings (cpus, memory, pids), standard vs strict hardening profiles, path rules, and debugging container issues with `zen sandbox`. Refers to `zen-cli` (`.github/skills/zen-cli/references/sandbox.md`).
4
+ ---
5
+
6
+ # The Container Sandbox
7
+
8
+ A shell, and a boundary around it. The boundary is a Linux container: the
9
+ agent's commands run against an image the project chose, with the session's
10
+ workspace bind-mounted at `/workspace` and nothing else of the host machine in
11
+ reach.
12
+
13
+ The container is the boundary rather than any parsing or filtering of what the
14
+ model wrote. On the host side, every process is spawned with `shell: false` and
15
+ arguments assembled as an array; command text travels exclusively down **stdin**
16
+ into `/bin/sh -s` inside the container. There is no host shell to inject into,
17
+ and no argv length to overflow.
18
+
19
+ The container engine is **Podman** (rootless by default).
20
+
21
+ For the CLI subcommands (`zen sandbox status`, `up`, `pull`, `clean`, `disk`)
22
+ and command-line options, see the [zen-cli skill](../zen-cli/SKILL.md) and its
23
+ dedicated reference document [sandbox.md](../zen-cli/references/sandbox.md).
24
+
25
+ ---
26
+
27
+ ## 1. The Sandbox Tools (`sandbox:*`)
28
+
29
+ When an agent is granted sandbox tools in `agents.yaml`, it receives up to four
30
+ tools from the `sandbox` group:
31
+
32
+ | Tool | Type | Description |
33
+ | ------------------------ | ---------- | --------------------------------------------------------------------- |
34
+ | `run_command` | foreground | Runs a shell command under `/bin/sh`, waits for exit, captures output |
35
+ | `run_command_background` | background | Starts a detached command (server, watcher) and returns a `job_id` |
36
+ | `read_command_output` | inspection | Reads a window of log lines from a background job and checks status |
37
+ | `stop_command` | control | Signals and terminates a background job process group |
38
+
39
+ ### `run_command`
40
+
41
+ - **Parameters:**
42
+ - `command` (string, required): the shell command string, interpreted by `/bin/sh` inside the container.
43
+ - `cwd` (string, optional): working directory relative to `/workspace`. Defaults to `/workspace`.
44
+ - `timeout` (integer, optional): seconds before SIGTERM/SIGKILL. Defaults to 120 s; capped at 3600 s.
45
+ - **Output:** returns `exit_code`, `stdout`, `stderr`, `duration_ms`. Combined output is capped at 64 KiB (`truncated: true` when exceeded).
46
+
47
+ ### `run_command_background`
48
+
49
+ - **Parameters:** `command` (string, required), `cwd` (string, optional).
50
+ - **Execution:** written to `/tmp/zenera-jobs/<job_id>.sh` and started in a separate session via `setsid`. Output and exit status stream to `/tmp/zenera-jobs/<job_id>.log` and `<job_id>.exit`.
51
+ - **Returns:** `{ job_id, started: true }`.
52
+
53
+ ### `read_command_output`
54
+
55
+ - **Parameters:** `job_id` (string, required), `start_line` (integer, optional, 1-based).
56
+ - **Returns:** `{ job_id, running: boolean, exit_code?: number, output: string, start_line, end_line, lines }`. Displays up to 400 lines per call.
57
+
58
+ ### `stop_command`
59
+
60
+ - **Parameters:** `job_id` (string, required).
61
+ - **Behaviour:** sends SIGTERM to the job's entire process group (`-$pid`), waits 1 s grace period, then sends SIGKILL.
62
+
63
+ ---
64
+
65
+ ## 2. Tool Selection in `agents.yaml`
66
+
67
+ Agents select tools through `tools:` using exact names, group wildcards, or subtraction:
68
+
69
+ ```yaml
70
+ agents:
71
+ - name: developer
72
+ tools: [files:*, sandbox:*] # all file and shell tools
73
+
74
+ - name: reviewer
75
+ tools: [files:*, sandbox:run_command] # foreground only, no background servers
76
+
77
+ - name: tester
78
+ tools: [sandbox:*, -stop_command] # sandbox tools without stop_command
79
+
80
+ - name: analyst
81
+ tools: [files:*] # no shell tools; container is never started
82
+ ```
83
+
84
+ An agent with no `sandbox:*` tools never triggers an image pull, container creation, or VM startup.
85
+
86
+ ---
87
+
88
+ ## 3. Filesystem and Mount Layout
89
+
90
+ Inside the container, paths are isolated to designated mount points:
91
+
92
+ | Path inside container | Host Source | Mode | Lifecycle / Purpose |
93
+ | --------------------- | ------------------------- | ----------- | ------------------------------------------------------ |
94
+ | `/workspace` | Session workspace | read-write* | The work itself. Changes survive session completion. |
95
+ | `/home/agent` | `.data/sandbox/home` | read-write | User home (`$HOME`). Persists across sessions. |
96
+ | `/tmp` | In-memory / host tmpfs | read-write | Ephemeral scratch; holds `/tmp/zenera-jobs`. |
97
+ | `/memory` | Session memory directory | read-only | Generated memory files (when `memory:` is enabled). |
98
+ | `/assets` | Project assets directory | read-only | Reference assets declared in `SPECIFICATION.md`. |
99
+ | `/skills` | Project skills directory | read-only | Custom skill files available to agents. |
100
+ | `/run/zenera/keys` | Keychain / temp directory | read-only | Forwarded file credentials (e.g. Vertex service keys). |
101
+
102
+ _\*Mounted read-only if the session or project was opened in read-only mode._
103
+
104
+ ### Path Rules
105
+
106
+ - All relative paths and `cwd` arguments resolve against `/workspace`.
107
+ - Lexical escapes outside `/workspace` (`../../etc`, absolute `/etc`, null bytes `\0`) are **strictly rejected** by the runtime and return an actionable error to the model.
108
+ - Path resolution inside the container never resolves against host symlinks.
109
+
110
+ ---
111
+
112
+ ## 4. Lifecycle, Container Naming, and Persistence
113
+
114
+ ### Lazy Initialization
115
+
116
+ Containers are started on-demand. If an agent never invokes a `sandbox:*` tool, no image is pulled and no container runs. Concurrent tool calls in the same turn await a single startup promise without racing.
117
+
118
+ ### Deterministic Container Naming
119
+
120
+ Container names follow the pattern:
121
+
122
+ ```
123
+ zn-<key>-<digest>
124
+ ```
125
+
126
+ - `<key>` is derived from the session identifier or project key.
127
+ - `<digest>` is the first 10 hex characters of the SHA-256 hash of the configuration:
128
+ `[root, image, hashCpus, hashMemory, network, workdir, user, readOnly, mounts, env, secrets, hardening]`
129
+
130
+ ### The Persistence Rule (`persist: true` vs `persist: false`)
131
+
132
+ - **`persist: false` (ephemeral):** when the session finishes, the container is deleted (`podman rm --force --volumes`). Changes made to `/workspace` and `/home/agent` survive on the host; changes made to the root filesystem (such as `apt-get` or root `pip install`) are discarded.
133
+ - **`persist: true` (recommended for development):** the container is stopped (`podman stop --time 2`) rather than removed. System packages and global caches survive between runs.
134
+ - **Caveat:** because the container name depends on its configuration, **any edit to `sandbox:` in `agents.yaml` generates a new container name**, abandoning the persisted container rootfs. When dependencies must be permanent across config changes, bake them into the image using `sandbox.build`.
135
+
136
+ ---
137
+
138
+ ## 5. Configuring `sandbox:` in `agents.yaml`
139
+
140
+ Configured at the top level and optionally overridden per agent under `agents[].sandbox`:
141
+
142
+ ```yaml
143
+ sandbox:
144
+ persist: true
145
+ image: docker.io/library/python:3.14-slim-bookworm
146
+ cpus: 4
147
+ memory: 4096
148
+ network: bridge
149
+ timeout: 120
150
+ hardening: standard
151
+ env: [HTTPS_PROXY, NO_PROXY]
152
+ keys: true
153
+
154
+ agents:
155
+ - name: builder
156
+ # Inherits base sandbox configuration
157
+
158
+ - name: isolated-runner
159
+ sandbox:
160
+ hardening: strict
161
+ network: none
162
+ ```
163
+
164
+ ### Configuration Fields
165
+
166
+ | Field | Type | Default | Meaning |
167
+ | ----------- | ---------------------- | --------------------------------------------- | ------------------------------------------------------- |
168
+ | `image` | string | `docker.io/library/python:3.14-slim-bookworm` | Base image to run. Mutually exclusive with `build`. |
169
+ | `build` | object | none | `{ dockerfile, context? }`. Builds a local Dockerfile. |
170
+ | `cpus` | number | `4` (`2` in `strict`) | Fractional CPU limit (`--cpus`). `0` for unconstrained. |
171
+ | `memory` | integer (MiB) | `4096` (`2048` in `strict`) | Memory limit (`--memory`). `0` for unconstrained. |
172
+ | `network` | `bridge`/`none`/`host` | `bridge` (`none` in `strict`) | Network connectivity mode. |
173
+ | `workdir` | absolute path | `/workspace` | Working directory and workspace mount destination. |
174
+ | `timeout` | integer (seconds) | `120` | Default per-command execution timeout. |
175
+ | `hardening` | `standard`/`strict` | `standard` | Security posture (see §6). |
176
+ | `user` | string | image user | `uid`, `name`, or `uid:gid` inside the container. |
177
+ | `persist` | boolean | `false` | Keep container filesystem between runs. |
178
+ | `env` | string[] | `[]` | Host environment variables to pass by name. |
179
+ | `keys` | boolean | `true` | Forward model credentials to the container. |
180
+
181
+ ---
182
+
183
+ ## 6. Hardening Postures: `standard` vs `strict`
184
+
185
+ Every container runs with baseline safety controls:
186
+
187
+ - `--security-opt no-new-privileges` prevents setuid escalation.
188
+ - `--init` provides a dedicated init process (catatonit) to reap zombie background processes.
189
+ - `--pids-limit 1024` halts fork bombs.
190
+ - `--restart no` prevents runaway resurrection.
191
+ - Never `--privileged`.
192
+ - Podman's default seccomp filter is always enabled.
193
+
194
+ ### Comparison
195
+
196
+ | Security Dimension | `standard` (default) | `strict` |
197
+ | -------------------- | ------------------------------------------- | ------------------------------------------------- |
198
+ | **Intended Use** | General development, testing, builds | Executing unreviewed or untrusted code |
199
+ | **Kernel Caps** | Default set kept (allows `apt`, `pip`) | `--cap-drop ALL` (all capabilities dropped) |
200
+ | **Root Filesystem** | Writable | `--read-only` (immutable rootfs) |
201
+ | **Temporary Space** | Standard `/tmp` | `--tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m` |
202
+ | **User Namespace** | Rootless default (uid 0 inside = host user) | `--userns keep-id` (host uid inside, owns mounts) |
203
+ | **Default Network** | `bridge` | `none` |
204
+ | **Default Ceilings** | `cpus: 4`, `memory: 4096` | `cpus: 2`, `memory: 2048` |
205
+ | **Package Installs** | Allowed (`apt-get`, `pip`, `npm`) | Refused (must pre-bake into Dockerfile) |
206
+ | **Workspace** | `/workspace` is read-write | `/workspace` is read-write |
207
+
208
+ ### Key Strict Mode Nuances
209
+
210
+ 1. **Pre-baked dependencies:** because `/` is read-only and caps are dropped, dynamic package installations fail. Dependencies must be defined in `sandbox/Dockerfile` using `sandbox.build`.
211
+ 2. **`keep-id` user mapping:** under rootless Podman, `--userns keep-id` maps your host UID to the container UID. Files written to `/workspace` remain owned by you on the host, and the container user cannot access root-only files inside the image. If an explicit `user:` is specified in config, `keep-id` is omitted.
212
+ 3. **`noexec` on `/tmp`:** prevents running downloaded binaries directly from scratch space. Interpreted scripts (`/bin/sh /tmp/script.sh` or `python /tmp/test.py`) remain functional because the interpreter executable lives in the read-only root.
213
+
214
+ ---
215
+
216
+ ## 7. Credentials and Secret Forwarding
217
+
218
+ 1. **Forwarded by Name, Never by Value:**
219
+ Host environment variables and credentials are passed using Podman's `--env NAME` syntax (without an `=value`). Podman pulls values from its own environment at execution time. Secrets never appear on command-line arguments, in process lists (`ps`), or in `podman inspect`.
220
+ 2. **Env Name Blacklist:**
221
+ The `env:` array in `agents.yaml` strictly refuses variables named `KEY`, `TOKEN`, `SECRET`, `PASSWORD`, `PASSWD`, or `CREDENTIAL` to prevent accidental credential leakage into uninspected scripts.
222
+ 3. **File-Shaped Credentials:**
223
+ Key files (such as Google Vertex service account keys) are mounted read-only to `/run/zenera/keys/` and their environment variables (e.g. `GOOGLE_APPLICATION_CREDENTIALS`) are automatically rewritten to point to the in-container path.
224
+ 4. **Disabling Forwarding:**
225
+ Set `keys: false` in `agents.yaml` or launch runs with `zen run --no-keys` to prevent any model API keys or service credentials from reaching the container.
226
+
227
+ ---
228
+
229
+ ## 8. Multi-Agent Sandboxes (`SandboxPool`)
230
+
231
+ - Two agents with identical sandbox configurations share the **same container**.
232
+ - Two agents with different images or configurations receive separate containers.
233
+ - The `sandbox:*` tool definitions remain uniform across the session; when an agent calls a tool, the pool routes the call to that specific agent's container at invocation time.
234
+
235
+ ---
236
+
237
+ ## 9. Diagnostics and Troubleshooting (`zen sandbox`)
238
+
239
+ Use the `zen sandbox` commands via terminal to inspect and manage container state:
240
+
241
+ ```sh
242
+ zen sandbox status # Check podman version, machine status, and active containers
243
+ zen sandbox up # Start VM (macOS) and pull or build the required image
244
+ zen sandbox pull # Re-pull base image or force build of sandbox/Dockerfile
245
+ zen sandbox clean # Force remove all zn-* containers created by this project
246
+ zen sandbox disk # Inspect disk space consumed by images, containers, and volumes
247
+ ```
248
+
249
+ ### Exit Codes and Common Issues
250
+
251
+ - **Exit Code 5:** Container engine failed or image build failed.
252
+ - On macOS: ensure Podman machine is running (`zen sandbox up` or `podman machine start`).
253
+ - On Linux: verify rootless subuid/subgid mapping is configured (`/etc/subuid`).
254
+ - **Permission Denied in `/workspace`:**
255
+ - In `strict` mode without `user:`, rootless Podman uses `keep-id` to preserve host UID. If using custom `user:`, verify host directory permissions allow that UID.
256
+ - **Disk Space Pressure:**
257
+ - Run `zen sandbox clean` to prune stopped session containers.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: zen-spec-sync
3
- description: Running `/spec-sync-project` as an incremental pass - the committed `.spec-sync` baseline of what the last pass applied, how to scope a pass from the specification and feedback diffs and from what moved in the project itself, the four checks a narrow pass may never skip, the iteration history format, and why the baseline is written last. Load before every `/spec-sync-project`, including the first one on a project that has no baseline yet.
3
+ description: Running `/spec-sync-project` as an incremental pass - the committed `.spec-sync` baseline of what the last pass applied, how to scope a pass from the specification and feedback diffs and from what moved in the project itself, the five checks a narrow pass may never skip, which skills a pass must load before editing (`zen-instructions` for any house rule it touches), the iteration history format, and why the baseline is written last. Load before every `/spec-sync-project`, including the first one on a project that has no baseline yet.
4
4
  ---
5
5
 
6
6
  # Syncing a project with its specification, one change at a time
@@ -103,7 +103,7 @@ editing anything, and report it in the chat as the pass's scope.
103
103
  covers it, and you say which line, or it does not, and it is an **extra**
104
104
  reported under `➕`.
105
105
 
106
- ## The four checks a narrow pass may never skip
106
+ ## The five checks a narrow pass may never skip
107
107
 
108
108
  However small the diff:
109
109
 
@@ -111,7 +111,12 @@ However small the diff:
111
111
  last.
112
112
  2. **`scripts/_setup.sh` has run to completion in this session, and re-runs
113
113
  clean.** A one-line specification change can still break a setup step, and a
114
- pass that did not run it does not know.
114
+ pass that did not run it does not know. Then `scripts/_setup.sh --check` must
115
+ report every step `skipped`: it makes each step's own readiness test without
116
+ doing the work, which is what tells a real `skipped` from a step that only
117
+ looked done. A step that tests for a committed `manifest.json` skips on a
118
+ fresh clone whose git-ignored `lance/` vectors are missing, and the project
119
+ cannot search.
115
120
  3. **§9 of the copilot instructions, worked as a checklist**, group by group.
116
121
  4. **The capability obligations in §0.1.** For every capability the project has
117
122
  turned on, load that capability's skill and re-check what it requires - in
@@ -119,6 +124,14 @@ However small the diff:
119
124
  `.github/skills/zen-memory/references/memory-instructions.md`. `zen init` and
120
125
  `zen open` rewrite the reference, so this drifts without anybody editing the
121
126
  project.
127
+ 5. **`zen-instructions` is loaded before any house rule is touched.** A pass
128
+ whose scope reaches `agents/*instructions.md` - and input 4 puts it there
129
+ whenever one of them moved - has to know that `tools-instructions.md`,
130
+ `files-instructions.md`, `memory-instructions.md` and `fork-instructions.md`
131
+ are `zen`'s and are replaced by `zen check --fix`. A pass that writes a
132
+ requirement into one of them has implemented nothing: the next `open` reverts
133
+ it, the history entry claims it, and the baseline records it as applied.
134
+ Specification-driven rules go in a topic file of the project's own beside it.
122
135
 
123
136
  A narrow diff narrows reading and editing. It does not narrow verification.
124
137
 
@@ -527,6 +527,11 @@
527
527
  "type": "integer",
528
528
  "exclusiveMinimum": 0
529
529
  },
530
+ "hardening": {
531
+ "description": "How much of the host the container may be. `standard` keeps the root filesystem writable and capabilities intact so package managers work. `strict` drops every capability, mounts the root filesystem read-only, restricts /tmp to a 64 MiB noexec tmpfs, and defaults network to none.",
532
+ "type": "string",
533
+ "enum": ["standard", "strict"]
534
+ },
530
535
  "user": {
531
536
  "description": "uid, name or `uid:gid`. Unset means the image's own user.",
532
537
  "type": "string",
@@ -572,12 +577,12 @@
572
577
  "$ref": "#/$defs/modelRef"
573
578
  },
574
579
  "tools": {
575
- "description": "Tool names and selectors: `workspace:*` is every file tool, `sandbox:*` the shell, `exa:*` web search and page reading. Subtract from a group with a leading `-`.",
580
+ "description": "Tool names and selectors: `files:*` (alias `workspace:*`) is every file tool, `sandbox:*` the shell, `exa:*` web search and page reading. Subtract from a group with a leading `-`.",
576
581
  "type": "array",
577
582
  "items": { "type": "string", "minLength": 1 },
578
583
  "examples": [
579
- ["workspace:*", "sandbox:*"],
580
- ["workspace:*", "-delete_file"]
584
+ ["files:*", "sandbox:*"],
585
+ ["files:*", "-delete_file"]
581
586
  ]
582
587
  },
583
588
  "handoffs": {
@@ -26,7 +26,10 @@ something in it — and report what was done in enough detail to be checked.
26
26
  an index built, a document fetched, a file generated.
27
27
  - `scripts/_setup.sh` is the one command that does it, and `scripts/<name>.sh`
28
28
  are the steps it runs in order. A step is safe to run twice and reports
29
- `skipped` the second time.
29
+ `skipped` the second time — testing for something that survives a clone, not
30
+ for a file `.gitignore` excludes.
31
+ - `scripts/_setup.sh --check` asks the same questions and does none of the work,
32
+ so what is missing can be read without paying to find out.
30
33
  - Currently: no steps. Nothing has to be prepared before a run.
31
34
 
32
35
  ## Agents