@zenera/cli 1.1.0 → 1.1.2

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.
@@ -133,11 +133,15 @@ my-project/
133
133
  │ │ └── adjuster.md
134
134
  │ └── skills/
135
135
  │ ├── house_style/
136
- │ │ ├── SKILL.md folder skill
137
- │ │ └── examples.md sibling files become `resources`
136
+ │ │ ├── SKILL.md the instructions
137
+ │ │ └── examples.md its own files, reachable at /skills/house_style
138
138
  │ ├── water_damage/
139
139
  │ │ └── SKILL.md
140
- │ └── shipping_delays.md flat skill (frontmatter + body)
140
+ │ └── refund_policy/
141
+ │ ├── SKILL.md
142
+ │ └── scripts/calculate.py run at /skills/refund_policy/scripts — §3.4.1
143
+ ├── assets/ reference material, read-only at /assets
144
+ ├── sandbox/Dockerfile the image commands run in — §3.7
141
145
  └── sessions/ run state, memory, whatever the agent wrote
142
146
  ```
143
147
 
@@ -240,6 +244,7 @@ model: fast # fallback for agents that do not pin their own
240
244
  embeddings: {} # named vectorisers — §3.1.1
241
245
  embedding: small # the one `AgentProject.embedder()` returns when asked for no name
242
246
  skills: agents/skills # one directory, or a list
247
+ assets: assets # reference material, read-only at /assets — §3.11
243
248
 
244
249
  agents: # the only required key; at least one entry
245
250
  - name: intake
@@ -465,18 +470,24 @@ yet.
465
470
 
466
471
  ### 3.4 Skills
467
472
 
468
- A skill is curated, reusable instruction content — plus optional tools — loaded
469
- **on demand** instead of permanently occupying the system prompt. Two layouts,
470
- discovered in the same scan:
473
+ A skill is curated, reusable instruction content — plus the files and tools it
474
+ needs — loaded **on demand** instead of permanently occupying the system prompt.
475
+ One layout, and only one:
471
476
 
472
477
  ```
473
- agents/skills/refund_policy.md flat: frontmatter + body
474
- agents/skills/refund_policy/SKILL.md folder: sibling files become `resources`
478
+ agents/skills/refund_policy/SKILL.md the instructions
479
+ agents/skills/refund_policy/rates.csv whatever they refer to
475
480
  ```
476
481
 
482
+ The folder name is the skill name. A bare `agents/skills/refund_policy.md` is
483
+ **not a skill location**: `zen check` reports it as `skill.flat`, and the fix is
484
+ to move it to `refund_policy/SKILL.md`. A skill with nowhere to put a table, an
485
+ example or a script can only ever be prose, which is the half of the idea that
486
+ does not need a file.
487
+
477
488
  Frontmatter is a deliberately small subset of YAML — `key: value`, plus `[a, b]`
478
489
  flow lists for `tags` and `tools`. **Every key is optional**: `name` defaults to
479
- the file/folder name, `description` to the first non-empty line of the body.
490
+ the folder name, `description` to the first non-empty line of the body.
480
491
 
481
492
  ```markdown
482
493
  ---
@@ -510,8 +521,58 @@ skill is needed_, not as a title:
510
521
  provider from turn 0 (the schema never changes) but **refuse to execute** until
511
522
  the skill is active. This is how a tool can be gated without breaking the cache.
512
523
 
513
- Use a folder skill when the content needs companions — a CSV rate table, an
514
- example letter, a JSON schema. Siblings become `resources` the model can read.
524
+ ### 3.4.1 Skills that ship files and scripts
525
+
526
+ The whole catalog is mounted **read-only at `/skills`** before anything is
527
+ loaded, so a skill's own files sit at `/skills/<name>/...`. The runtime says so
528
+ in the activation, once, when the skill loads:
529
+
530
+ > This skill's files are at /skills/refund_policy, read-only. Paths written in
531
+ > it are relative to that directory.
532
+
533
+ That line is what turns a script in a skill from decoration into something the
534
+ agent can run:
535
+
536
+ ```
537
+ agents/skills/refund_policy/
538
+ ├── SKILL.md
539
+ ├── rates.csv
540
+ └── scripts/calculate.py
541
+ ```
542
+
543
+ ```markdown
544
+ Run `python /skills/refund_policy/scripts/calculate.py <order-id>` and use the
545
+ figure it prints. Read `/skills/refund_policy/rates.csv` if you need the band.
546
+ Do not compute the amount yourself.
547
+ ```
548
+
549
+ **Reach for a script whenever the answer is arithmetic, a lookup or a fixed
550
+ transformation.** It is the cheapest tool there is: no schema in the prefix, no
551
+ vendor, versioned beside the prose that calls it, and its output is a fact
552
+ rather than a guess (§1.4).
553
+
554
+ Rules that follow from the mount:
555
+
556
+ - The agent needs `sandbox:*` to run the script and `read_file` to open a data
557
+ file. Grant them, or the skill's own instructions cannot be followed (§3.5).
558
+ - **The interpreter must already be in the sandbox image.** A skill whose
559
+ script needs `pandas` is a `sandbox/Dockerfile` line that was never written —
560
+ not a first line that runs `pip install` on every turn (§3.7).
561
+ - **`/skills` is read-only.** A script that produces a file must take an output
562
+ path and write it under `/workspace`.
563
+ - **Write the absolute `/skills/<name>/...` path** in the skill body. The text is
564
+ loaded into a prompt, not executed from its directory, and the working
565
+ directory is `/workspace`.
566
+ - Say what to do when the script fails, as with any other instruction (§4.2).
567
+ - With several `skills:` directories, one catalog is the whole of `/skills` and
568
+ several take `/skills/<folder>` each — check the rendered path before writing
569
+ it into the body.
570
+ - The mount is created before anything is loaded, so `allow:` limits what an
571
+ agent can **load**, not what it can **read**. Do not put anything in the
572
+ catalog that some agents must not see.
573
+
574
+ `zen check` lists what each skill folder ships, so a script that was never
575
+ committed shows up as a skill with no files.
515
576
 
516
577
  ### 3.5 Tools
517
578
 
@@ -635,6 +696,7 @@ sandbox:
635
696
  | Field | Default | Meaning |
636
697
  | --------- | --------------------------------------------- | -------------------------------------------- |
637
698
  | `image` | `docker.io/library/python:3.14-slim-bookworm` | The base image commands run in |
699
+ | `build` | none | A Dockerfile to build instead |
638
700
  | `cpus` | the host's | Fractional cores |
639
701
  | `memory` | the host's | MiB |
640
702
  | `network` | `bridge` | `bridge` / `none` / `host` |
@@ -648,6 +710,30 @@ sandbox:
648
710
  repository — and anything credential-shaped (`KEY`, `TOKEN`, `SECRET`,
649
711
  `PASSWORD`, `CREDENTIAL`) is refused at load.
650
712
 
713
+ **`build:`, when no published image fits.** A project that needs two runtimes,
714
+ or a pinned toolchain, names a Dockerfile instead of an image — `zen init`
715
+ writes one at `sandbox/Dockerfile` with Python and Node in it:
716
+
717
+ ```yaml
718
+ sandbox:
719
+ persist: true
720
+ build:
721
+ dockerfile: sandbox/Dockerfile
722
+ # context: . # what the build may COPY from; the Dockerfile's folder by default
723
+ ```
724
+
725
+ `image:` and `build:` cannot both be set — a Dockerfile names its own base in
726
+ its `FROM` line. The tag is a hash of the Dockerfile and its context, so
727
+ editing it produces a new image and, like any other change here, a new
728
+ container. `zen check` builds it and runs a command in it, so a Dockerfile that
729
+ does not build fails the check rather than the next run.
730
+
731
+ **Put what the project always needs in the image, not in a prompt.** This is the
732
+ real use of `build:`: a `RUN apt-get install ripgrep` in the Dockerfile is
733
+ installed once, for everyone, forever. The same instruction written into a
734
+ prompt is executed on every run, by an agent that is root in a filesystem that
735
+ is about to be thrown away.
736
+
651
737
  Agents share one container, because they share the workspace and a hand-off is
652
738
  meant to be continuous. An agent that needs something else says so and gets its
653
739
  own, with its block merged over the top-level one:
@@ -682,15 +768,16 @@ sandbox:
682
768
  ```
683
769
 
684
770
  With it, the container is _stopped_ rather than removed, and the next run of
685
- that session starts the same one back up with everything still installed. The
686
- cost is containers that outlive their sessions `zen sandbox status` lists them
687
- and `zen sandbox clean` removes them.
771
+ that session starts the same one back up with everything still installed. A
772
+ container is per session rather than per project, so they accumulate: `zen
773
+ sandbox status` lists them, `zen sandbox disk` totals what they and the project
774
+ directories cost, and `zen sandbox clean` removes them.
688
775
 
689
776
  Changing any field renames the container, so bumping the image gets a fresh one
690
777
  rather than an old one quietly persisting with the wrong contents. That is also
691
778
  the one sharp edge of `persist: true`: a config change abandons the old
692
779
  container with whatever was installed in it, so a long-lived setup still belongs
693
- in `image:` rather than in an accumulated rootfs.
780
+ in `image:` — or in `sandbox/Dockerfile` — rather than in an accumulated rootfs.
694
781
 
695
782
  Granting the group is what makes the project need Podman: `zen run` checks the
696
783
  engine before the first turn and exits `5` with an install command if it is
@@ -792,7 +879,7 @@ zen run "what changed?" one shot; stdout is the answer
792
879
  zen run --session <id> continue a session
793
880
  zen run --workspace ./repo what the agent may read and write
794
881
  zen run --model careful override the default model for this run
795
- zen run --image <ref> override the sandbox image for this run
882
+ zen run --image <ref> override the sandbox image for this run, ignoring build:
796
883
  zen run --read-only withhold every tool that can write
797
884
  ```
798
885
 
@@ -816,6 +903,22 @@ EXA_API_KEY=...
816
903
  Never commit. Never inline a key into `agents.yaml` — use `${VAR}`. Never print a
817
904
  key in a log line, a test fixture, or a chat message.
818
905
 
906
+ ### 3.11 `assets/`
907
+
908
+ `assets/` next to `agents.yaml` — or `assets: <path>` — is reference material
909
+ every agent can read and none can write. It is mounted at `/assets`: the file
910
+ tools read, list and search it, `run_command` sees it bind-mounted read-only,
911
+ and every tool that would change it refuses.
912
+
913
+ Put a handbook, a specification, a schema or a style guide there — what agents
914
+ consult while working, as opposed to the workspace, which is the work. Do not
915
+ point `assets:` at the project root: that hands every agent the `sessions/`
916
+ directory, every transcript of every run, including the one reading it.
917
+ `zen check` warns when it would.
918
+
919
+ It is project-wide by design. An agent that may see only part of the material
920
+ is a different project, not a different key.
921
+
819
922
  ---
820
923
 
821
924
  ## 4. Writing prompts
@@ -1468,10 +1571,13 @@ Before finishing any change here:
1468
1571
 
1469
1572
  **Skills**
1470
1573
 
1574
+ - [ ] Every skill is `<name>/SKILL.md` — no bare `<name>.md` in the catalog
1471
1575
  - [ ] Every skill has a `description` that says _when it is needed_
1472
1576
  - [ ] `preload` is reserved for content the model would never decline
1473
1577
  - [ ] `preload` entries also appear in `allow` where `allow` is used
1474
1578
  - [ ] Catalog >~30 entries → `discovery: search`
1579
+ - [ ] A skill that ships a script writes its `/skills/<name>/...` path, the agent
1580
+ holds `sandbox:*`, and the sandbox image already has the interpreter
1475
1581
 
1476
1582
  **Tools**
1477
1583
 
@@ -1481,7 +1587,7 @@ Before finishing any change here:
1481
1587
  - [ ] `sandbox:*` is granted only where a shell is actually needed
1482
1588
  - [ ] `sandbox.persist: true`, unless a throwaway rootfs is wanted on purpose
1483
1589
  - [ ] The `sandbox:` image carries what the work needs, rather than the prompt
1484
- installing it every run
1590
+ installing it every run — add it to `sandbox/Dockerfile` if `build:` is used
1485
1591
  - [ ] `sandbox.env` lists names only, and nothing credential-shaped
1486
1592
  - [ ] `exa:*` is granted only where the live web is actually needed, and the
1487
1593
  prompt says when to trust it over what the model already believes
@@ -1507,31 +1613,33 @@ Before finishing any change here:
1507
1613
 
1508
1614
  ## 10. Where to change what
1509
1615
 
1510
- | Symptom | Change this |
1511
- | ------------------------------------------ | ---------------------------------------------------------- |
1512
- | Wrong tone, wrong format, wrong length | `INSTRUCTIONS.md` (all agents) or the agent prompt |
1513
- | Says something forbidden | `INSTRUCTIONS.md` prohibition, stated specifically |
1514
- | Ignores a rule that only applies sometimes | Move the rule into a skill with a sharp description |
1515
- | Never loads the skill it should | The skill's `description`; or `preload` it |
1516
- | Loads too much, answers slowly | `allow:`, `maxIndexEntries:`, or `discovery: search` |
1517
- | Invents a number | A skill holding the figure, or a command that computes it |
1518
- | Rewrites a whole file to change one line | A prompt line preferring `apply_patch` — §3.6 |
1519
- | Edits files it should only be reading | Subtract the mutating tools, or `zen run --read-only` |
1520
- | Cannot run the build or the tests | Grant `sandbox:*`; pick an `image:` that has the toolchain |
1521
- | Installs the same packages on every run | `sandbox.persist: true`, or set `sandbox.image` §3.7 |
1522
- | Answers from stale knowledge of the world | Grant `web_search` + `web_read`, and say when — §3.8 |
1523
- | Cites a page it only saw the excerpt of | A prompt line: `web_read` before quoting — §3.8 |
1524
- | Every web call refuses | No Exa key: `zen key add exa` `zen check` warns — §3.8 |
1525
- | Answers instead of routing | Router prompt prohibition; check `handoffs:` |
1526
- | Routes to the wrong specialist | The target agents' `description:` fields |
1527
- | Loses a detail after a handoff | Say it in the handoff; check the collapse policy |
1528
- | Works through N independent items serially | `fork:` on that agent, and a prompt line — §6.4 |
1529
- | Forks when the steps actually depend | Prompt line: branches cannot see each other |
1530
- | Slow and expensive on trivial cases | Demote that agent's model tier / reasoning effort |
1531
- | Fails only on genuinely hard cases | Promote that agent's tier, or split the hard path out |
1532
- | Shows no reasoning while it works | Turn summaries on for that model §7.6 |
1533
- | Forgets across conversations | Continue the session rather than starting a new one |
1534
- | Breaks at load with a named path | Read the message it names the exact key |
1616
+ | Symptom | Change this |
1617
+ | ------------------------------------------ | ------------------------------------------------------------------- |
1618
+ | Wrong tone, wrong format, wrong length | `INSTRUCTIONS.md` (all agents) or the agent prompt |
1619
+ | Says something forbidden | `INSTRUCTIONS.md` prohibition, stated specifically |
1620
+ | Ignores a rule that only applies sometimes | Move the rule into a skill with a sharp description |
1621
+ | Never loads the skill it should | The skill's `description`; or `preload` it |
1622
+ | Loads too much, answers slowly | `allow:`, `maxIndexEntries:`, or `discovery: search` |
1623
+ | Invents a number | A skill holding the figure, or a script that computes it — §3.4.1 |
1624
+ | A skill in the catalog is never offered | It is a bare `<name>.md`; move it to `<name>/SKILL.md` — §3.4 |
1625
+ | Cannot find a file its own skill names | Absolute `/skills/<name>/…` path, and `sandbox:*` §3.4.1 |
1626
+ | Rewrites a whole file to change one line | A prompt line preferring `apply_patch` §3.6 |
1627
+ | Edits files it should only be reading | Subtract the mutating tools, or `zen run --read-only` |
1628
+ | Cannot run the build or the tests | Grant `sandbox:*`; add the toolchain to `sandbox/Dockerfile` |
1629
+ | Installs the same packages on every run | Put them in `sandbox/Dockerfile`, or `sandbox.persist: true` — §3.7 |
1630
+ | Answers from stale knowledge of the world | Grant `web_search` + `web_read`, and say when — §3.8 |
1631
+ | Cites a page it only saw the excerpt of | A prompt line: `web_read` before quoting — §3.8 |
1632
+ | Every web call refuses | No Exa key: `zen key add exa` — `zen check` warns — §3.8 |
1633
+ | Answers instead of routing | Router prompt prohibition; check `handoffs:` |
1634
+ | Routes to the wrong specialist | The target agents' `description:` fields |
1635
+ | Loses a detail after a handoff | Say it in the handoff; check the collapse policy |
1636
+ | Works through N independent items serially | `fork:` on that agent, and a prompt line — §6.4 |
1637
+ | Forks when the steps actually depend | Prompt line: branches cannot see each other |
1638
+ | Slow and expensive on trivial cases | Demote that agent's model tier / reasoning effort |
1639
+ | Fails only on genuinely hard cases | Promote that agent's tier, or split the hard path out |
1640
+ | Shows no reasoning while it works | Turn summaries on for that model §7.6 |
1641
+ | Forgets across conversations | Continue the session rather than starting a new one |
1642
+ | Breaks at load with a named path | Read the message — it names the exact key |
1535
1643
 
1536
1644
  ---
1537
1645
 
@@ -1542,6 +1650,10 @@ Before finishing any change here:
1542
1650
  prompt style. Collapse into one with a catalog.
1543
1651
  - **Facts in prompts.** A fee schedule inside `INSTRUCTIONS.md`. It cannot be
1544
1652
  versioned, cannot be shared, and is paid for on every call.
1653
+ - **The bare skill file.** `agents/skills/<name>.md` instead of a folder. It can
1654
+ never grow the table or script the next revision of the rule will want — §3.4.
1655
+ - **Arithmetic in prose.** A skill that walks the model through a calculation it
1656
+ will get wrong, in a folder that could have held the script — §3.4.1.
1545
1657
  - **Politeness padding.** "Please try your best to be helpful." Costs tokens,
1546
1658
  changes nothing.
1547
1659
  - **Commented implementation notes.** `agents.yaml` explaining how skill
@@ -10,9 +10,10 @@ change without the prompt changing.
10
10
 
11
11
  Ask what the skill must say and when it applies, if I have not already said.
12
12
 
13
- 1. Choose the layout. A flat `agents/skills/<name>.md` for text alone; a folder
14
- `agents/skills/<name>/SKILL.md` when it needs companions a rate table, an
15
- example letter, a schema. Siblings of `SKILL.md` become readable resources.
13
+ 1. Create `agents/skills/<name>/SKILL.md`. That is the only skill layout there
14
+ is — a bare `agents/skills/<name>.md` fails `zen check`and the folder is
15
+ what lets the skill carry companions later: a rate table, an example letter,
16
+ a schema, a script.
16
17
  2. Write the `description`. This is the routing key and the only thing the model
17
18
  sees before deciding to load the skill, so write it as **the condition under
18
19
  which the skill is needed**, not as a title. `Water policy` is a title;
@@ -23,12 +24,18 @@ Ask what the skill must say and when it applies, if I have not already said.
23
24
  wording, the boundaries of the rule, and what to do when the case falls
24
25
  outside it. Put the facts here rather than in a prompt — that is the point of
25
26
  the file.
26
- 4. Declare `tools:` in the frontmatter only for tools that must not run until
27
+ 4. Put anything the rule computes or looks up in a file beside `SKILL.md` and
28
+ name it from the body by its absolute path — `python
29
+ /skills/<name>/scripts/calculate.py <id>`, not `scripts/calculate.py`. The
30
+ folder is mounted read-only at `/skills/<name>/`, so the script writes
31
+ nothing there; the agent needs `sandbox:*` to run it, and the interpreter has
32
+ to be in the `sandbox:` image already.
33
+ 5. Declare `tools:` in the frontmatter only for tools that must not run until
27
34
  this skill is active. They are advertised from turn 0 and refuse to execute
28
35
  while the skill is dormant, which is how gating happens without breaking the
29
36
  prompt cache.
30
- 5. Leave `name` out unless it must differ from the file or folder name, and
31
- leave `version`/`tags` out unless something uses them.
37
+ 6. Leave `name` out unless it must differ from the folder name, and leave
38
+ `version`/`tags` out unless something uses them.
32
39
 
33
40
  Do not `preload:` it unless every case genuinely needs it — a preloaded skill is
34
41
  a longer prompt, paid for on every call. Bind it under `agents[].skills.allow`
@@ -0,0 +1,19 @@
1
+ # The container `run_command` runs in. Built by `zen run` and `zen check`, and
2
+ # named by the `sandbox: build:` block in agents.yaml.
3
+ #
4
+ # Python and Node both, because an agent asked to look at a repository does not
5
+ # get to choose which one it finds. Add what this project always needs here
6
+ # rather than letting an agent install it at run time: a root `pip install` or
7
+ # `apt-get` lands in the container's own filesystem, which is thrown away.
8
+
9
+ FROM node:24-bookworm-slim AS node
10
+ FROM python:3.14-slim-bookworm
11
+
12
+ # Copy Node.js runtime and global modules from official Node image
13
+ COPY --from=node /usr/local/bin/node /usr/local/bin/node
14
+ COPY --from=node /usr/local/include/node /usr/local/include/node
15
+ COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
16
+
17
+ # Symlink npm and npx executables
18
+ RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
19
+ && ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx