@zenera/cli 1.1.15 → 1.1.17
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.
- package/README.md +201 -129
- package/dist/banner.d.ts +5 -1
- package/dist/banner.js +2 -2
- package/dist/catalog.js +19 -20
- package/dist/command.d.ts +5 -0
- package/dist/commands/check.js +5 -1
- package/dist/commands/init.js +41 -6
- package/dist/commands/run.js +4 -0
- package/dist/engine.js +6 -0
- package/dist/env.d.ts +20 -0
- package/dist/env.js +65 -0
- package/dist/keys.d.ts +2 -0
- package/dist/keys.js +2 -0
- package/dist/main.js +10 -6
- package/dist/sandbox.d.ts +9 -0
- package/dist/sandbox.js +23 -3
- package/dist/scaffold.d.ts +6 -0
- package/dist/scaffold.js +56 -15
- package/dist/validate.js +57 -15
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +501 -301
- package/templates/editor/.github/prompts/new-agent.prompt.md +6 -6
- package/templates/editor/.github/prompts/new-skill.prompt.md +7 -7
- package/templates/editor/.github/prompts/review-project.prompt.md +54 -15
- package/templates/editor/.github/prompts/sync-with-spec.prompt.md +229 -42
- package/templates/editor/.github/prompts/update-spec-from-feedback.prompt.md +142 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +62 -6
- package/templates/editor/.github/skills/zen-cli/references/check.md +9 -6
- package/templates/editor/.github/skills/zen-cli/references/faker.md +14 -14
- package/templates/editor/.github/skills/zen-cli/references/frame.md +11 -9
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +3 -3
- package/templates/editor/.github/skills/zen-cli/references/keys.md +9 -9
- package/templates/editor/.github/skills/zen-cli/references/memory.md +11 -11
- package/templates/editor/.github/skills/zen-cli/references/models.md +8 -8
- package/templates/editor/.github/skills/zen-cli/references/projects.md +23 -6
- package/templates/editor/.github/skills/zen-cli/references/rag.md +191 -56
- package/templates/editor/.github/skills/zen-cli/references/run.md +3 -3
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +14 -8
- package/templates/editor/.github/skills/zen-memory/SKILL.md +144 -72
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +115 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +71 -48
- package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +67 -67
- package/templates/editor/.github/skills/zen-review/SKILL.md +123 -0
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +119 -0
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +104 -0
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +197 -0
- package/templates/editor/.github/skills/zen-spec-sync/scripts/snapshot.sh +200 -0
- package/templates/editor/.vscode/settings.json +46 -3
- package/templates/parts/memory-terms.yaml.tmpl +16 -0
- package/templates/parts/memory.yaml.tmpl +12 -0
- package/templates/project/SPECIFICATION.md.tmpl +7 -0
- package/templates/project/{INSTRUCTIONS.md → agents/instructions.md} +1 -1
- package/templates/project/agents/memory-instructions.md +278 -0
- package/templates/project/agents.yaml.tmpl +9 -0
- package/templates/project/assets/README.md +1 -1
- package/templates/project/env +22 -0
- package/templates/project/sandbox/Dockerfile.tmpl +3 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Every path-shaped token a running agent would be told to open, minus the ones
|
|
4
|
+
# that exist. The §2.5 sweep, as a script rather than a command to retype.
|
|
5
|
+
#
|
|
6
|
+
# check-paths.sh each candidate, with the file and line it is on
|
|
7
|
+
# check-paths.sh raw the tokens alone, unfiltered — the sweep itself
|
|
8
|
+
#
|
|
9
|
+
# A running agent sees four paths: /workspace, /assets, /skills and /memory.
|
|
10
|
+
# Everything else printed here is either noise this script has not learned yet
|
|
11
|
+
# or an instruction to open a file that is not there. There is no third case,
|
|
12
|
+
# which is why this exits non-zero whenever it prints one: the list is meant to
|
|
13
|
+
# be emptied or explained, not skimmed.
|
|
14
|
+
#
|
|
15
|
+
# Run it from anywhere; it finds the project root from its own location.
|
|
16
|
+
|
|
17
|
+
set -eu
|
|
18
|
+
cd "$(dirname "$0")/../../../.."
|
|
19
|
+
|
|
20
|
+
# What a path looks like in prose. §2.5 of the copilot instructions describes
|
|
21
|
+
# the sweep and sends you here for it, so this is the only copy of the pattern.
|
|
22
|
+
# Every segment after the first is taken too: matched one segment at a time,
|
|
23
|
+
# `/skills/house_style/examples.md` comes back as a mount that resolves *and* a
|
|
24
|
+
# stray `/examples.md`, and that fragment is noise on every run of every project.
|
|
25
|
+
TOKEN='[~.]?/?[A-Za-z0-9_.-]*(/[A-Za-z0-9_.*-]+)+'
|
|
26
|
+
|
|
27
|
+
# The four mounts, and only those: a token under one of them is a path that
|
|
28
|
+
# resolves.
|
|
29
|
+
MOUNTS='^/(workspace|assets|skills|memory)(/|$)'
|
|
30
|
+
|
|
31
|
+
# English that happens to contain a slash, and the placeholder paths of a
|
|
32
|
+
# worked example. Add to this rather than teaching a reviewer to skip the same
|
|
33
|
+
# line every run.
|
|
34
|
+
NOISE='^(and/or|he/she|his/her|input/output|read/write|w/o|n/a|N/A|24/7|km/h)$|^(path|foo)/'
|
|
35
|
+
|
|
36
|
+
# A url, stripped before tokenising rather than filtered after it — the token
|
|
37
|
+
# regex reads `https://acme.dev/docs` as `//acme.dev` *and* `/docs`, and the
|
|
38
|
+
# second half is indistinguishable from a real absolute path once it is alone.
|
|
39
|
+
strip_urls() {
|
|
40
|
+
sed -E 's#[a-zA-Z][a-zA-Z0-9+.-]*://[^][ )"'"'"'`>]*##g' "$1"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# The three trees §2.5 names, prose only: a skill's own script is code the
|
|
44
|
+
# sandbox runs, not text rendered into a prompt, and its paths are its own.
|
|
45
|
+
#
|
|
46
|
+
# `agents/memory-instructions.md` is left out on purpose. It is a copy of the
|
|
47
|
+
# reference, full of worked examples, so it fails this sweep by design and
|
|
48
|
+
# every run of it — and it has a check of its own that a sweep cannot make:
|
|
49
|
+
# .github/skills/zen-memory/scripts/check-instructions.sh
|
|
50
|
+
files() {
|
|
51
|
+
{
|
|
52
|
+
if [ -d agents/prompts ]; then find agents/prompts -type f -name '*.md'; fi
|
|
53
|
+
if [ -d agents/skills ]; then find agents/skills -type f -name '*.md'; fi
|
|
54
|
+
for file in agents/*instructions.md; do
|
|
55
|
+
if [ -f "$file" ]; then echo "$file"; fi
|
|
56
|
+
done
|
|
57
|
+
} | grep -v '^agents/memory-instructions\.md$' | sort -u
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Every token in those files, urls removed first, and a sentence's full stop
|
|
61
|
+
# trimmed off the end — no path ends in a dot, and `/workspace.` would
|
|
62
|
+
# otherwise read as a mount that does not exist.
|
|
63
|
+
sweep() {
|
|
64
|
+
while IFS= read -r file; do
|
|
65
|
+
if [ -n "$file" ]; then
|
|
66
|
+
strip_urls "$file"
|
|
67
|
+
fi
|
|
68
|
+
done <<EOF
|
|
69
|
+
$(files)
|
|
70
|
+
EOF
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
tokens() {
|
|
74
|
+
sweep | grep -Eoh "$TOKEN" | sed 's/\.*$//' | sort -u
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
candidates() {
|
|
78
|
+
tokens | grep -Ev "$MOUNTS" | grep -Ev "$NOISE" || true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Where a token came from, so a finding names a file and a line rather than
|
|
82
|
+
# sending someone back to grep for it.
|
|
83
|
+
where() {
|
|
84
|
+
while IFS= read -r file; do
|
|
85
|
+
if [ -n "$file" ]; then
|
|
86
|
+
grep -nF -- "$1" "$file" | sed "s|^| $file:|" | cut -d: -f1,2
|
|
87
|
+
fi
|
|
88
|
+
done <<EOF
|
|
89
|
+
$(files)
|
|
90
|
+
EOF
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
report() {
|
|
94
|
+
found=0
|
|
95
|
+
while IFS= read -r token; do
|
|
96
|
+
if [ -n "$token" ]; then
|
|
97
|
+
found=$((found + 1))
|
|
98
|
+
echo "path: $token"
|
|
99
|
+
where "$token"
|
|
100
|
+
fi
|
|
101
|
+
done <<EOF
|
|
102
|
+
$(candidates)
|
|
103
|
+
EOF
|
|
104
|
+
if [ "$found" -eq 0 ]; then
|
|
105
|
+
echo "ok: every path in the three trees is under a mount"
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
echo "candidates: $found to explain or fix"
|
|
109
|
+
return 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case "${1:-report}" in
|
|
113
|
+
report) report ;;
|
|
114
|
+
raw) tokens ;;
|
|
115
|
+
*)
|
|
116
|
+
echo "usage: check-paths.sh [report|raw]" >&2
|
|
117
|
+
exit 2
|
|
118
|
+
;;
|
|
119
|
+
esac
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Every check a review can make without judgement, in the order it makes them.
|
|
4
|
+
#
|
|
5
|
+
# review.sh zen check, then paths, memory, and the spec-sync record
|
|
6
|
+
# review.sh fast the same, with `zen check --no-models --no-sandbox`
|
|
7
|
+
#
|
|
8
|
+
# What it prints is findings. What it cannot see — whether an agent has one job,
|
|
9
|
+
# whether a description is a condition, whether a fact belongs in a skill — is
|
|
10
|
+
# the rest of /review-project, and the reason that prompt still exists.
|
|
11
|
+
#
|
|
12
|
+
# Every step runs even when an earlier one fails: a review that stops at the
|
|
13
|
+
# first problem is one somebody has to run four times. The exit code is 1 if
|
|
14
|
+
# anything wants attention, 0 if nothing does.
|
|
15
|
+
#
|
|
16
|
+
# Run it from anywhere; it finds the project root from its own location.
|
|
17
|
+
|
|
18
|
+
set -u
|
|
19
|
+
cd "$(dirname "$0")/../../../.."
|
|
20
|
+
|
|
21
|
+
SKILLS=.github/skills
|
|
22
|
+
CHECK_FLAGS=""
|
|
23
|
+
|
|
24
|
+
case "${1:-full}" in
|
|
25
|
+
full) ;;
|
|
26
|
+
fast) CHECK_FLAGS="--no-models --no-sandbox" ;;
|
|
27
|
+
*)
|
|
28
|
+
echo "usage: review.sh [full|fast]" >&2
|
|
29
|
+
exit 2
|
|
30
|
+
;;
|
|
31
|
+
esac
|
|
32
|
+
|
|
33
|
+
SUMMARY=""
|
|
34
|
+
WORST=0
|
|
35
|
+
|
|
36
|
+
# Records one step's verdict for the summary, and remembers the worst of them.
|
|
37
|
+
verdict() {
|
|
38
|
+
SUMMARY="$SUMMARY$1: $2
|
|
39
|
+
"
|
|
40
|
+
if [ "$3" -ne 0 ]; then
|
|
41
|
+
WORST=1
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
step() {
|
|
46
|
+
printf '\n--- %s ---\n' "$1"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
step 'zen check'
|
|
50
|
+
if command -v zen >/dev/null 2>&1; then
|
|
51
|
+
# Unquoted on purpose: empty means no flags rather than one empty argument.
|
|
52
|
+
# shellcheck disable=SC2086
|
|
53
|
+
zen check $CHECK_FLAGS
|
|
54
|
+
rc=$?
|
|
55
|
+
if [ "$rc" -eq 0 ]; then
|
|
56
|
+
verdict 'zen check' 'ok' 0
|
|
57
|
+
else
|
|
58
|
+
verdict 'zen check' "exit $rc — nothing below matters until this passes" 1
|
|
59
|
+
fi
|
|
60
|
+
else
|
|
61
|
+
echo "zen is not on PATH — install it, or run the other three alone"
|
|
62
|
+
verdict 'zen check' 'not run' 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
step 'paths'
|
|
66
|
+
"$SKILLS/zen-review/scripts/check-paths.sh"
|
|
67
|
+
rc=$?
|
|
68
|
+
if [ "$rc" -eq 0 ]; then
|
|
69
|
+
verdict 'paths' 'ok' 0
|
|
70
|
+
else
|
|
71
|
+
verdict 'paths' 'candidates to explain or fix' 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
step 'memory'
|
|
75
|
+
"$SKILLS/zen-memory/scripts/check-instructions.sh"
|
|
76
|
+
rc=$?
|
|
77
|
+
if [ "$rc" -eq 0 ]; then
|
|
78
|
+
verdict 'memory' 'ok' 0
|
|
79
|
+
else
|
|
80
|
+
verdict 'memory' 'the house rules are missing or stale' 1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
step 'spec-sync'
|
|
84
|
+
if [ -f SPECIFICATION.md ]; then
|
|
85
|
+
# `status` reports a tampered baseline rather than failing on one, so the
|
|
86
|
+
# verdict has to read what it said.
|
|
87
|
+
state="$("$SKILLS/zen-spec-sync/scripts/snapshot.sh" status)"
|
|
88
|
+
rc=$?
|
|
89
|
+
printf '%s\n' "$state"
|
|
90
|
+
if [ "$rc" -ne 0 ]; then
|
|
91
|
+
verdict 'spec-sync' "exit $rc" 1
|
|
92
|
+
elif printf '%s' "$state" | grep -q 'mode: tampered'; then
|
|
93
|
+
verdict 'spec-sync' 'the baseline does not match its own manifest' 1
|
|
94
|
+
else
|
|
95
|
+
verdict 'spec-sync' "$(printf '%s' "$state" | sed -n 's/^mode: /mode /p')" 0
|
|
96
|
+
fi
|
|
97
|
+
else
|
|
98
|
+
echo "no SPECIFICATION.md — this project keeps no spec-sync record"
|
|
99
|
+
verdict 'spec-sync' 'not applicable' 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
step 'summary'
|
|
103
|
+
printf '%s' "$SUMMARY"
|
|
104
|
+
exit "$WORST"
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zen-spec-sync
|
|
3
|
+
description: Running `/sync-with-spec` 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 `/sync-with-spec`, including the first one on a project that has no baseline yet.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Syncing a project with its specification, one change at a time
|
|
7
|
+
|
|
8
|
+
`SPECIFICATION.md` is the intent and the files are the implementation, so
|
|
9
|
+
bringing the two together is a pass over the whole project. Done from nothing
|
|
10
|
+
every time, that pass re-reads every prompt, re-derives every decision the last
|
|
11
|
+
one already made, and re-raises questions someone has already answered.
|
|
12
|
+
|
|
13
|
+
A pass is worth repeating cheaply, so it keeps a record of **what it applied**.
|
|
14
|
+
The next pass diffs the specification against that record and works the
|
|
15
|
+
difference. What it must not do is confuse a narrow diff with a narrow
|
|
16
|
+
obligation: the record scopes what you _read and edit_, never what you _verify_.
|
|
17
|
+
|
|
18
|
+
## What this owns
|
|
19
|
+
|
|
20
|
+
Everything is committed. `.spec-sync/` is a record, not scratch - `.tmp/` is
|
|
21
|
+
scratch.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
.spec-sync/
|
|
25
|
+
├── baseline/
|
|
26
|
+
│ ├── SPECIFICATION.md the spec the last completed pass applied
|
|
27
|
+
│ ├── SPECIFICATION-FEEDBACK.md the questions it had open, if any
|
|
28
|
+
│ ├── agents.yaml the wiring it left behind
|
|
29
|
+
│ ├── agents/ and every instruction, prompt and skill
|
|
30
|
+
│ ├── applied.txt the history entry this baseline belongs to
|
|
31
|
+
│ └── manifest.txt sha256 of every file above
|
|
32
|
+
└── history/
|
|
33
|
+
└── 2026-09-13T104500Z.md one file per pass, never edited after
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The baseline mirrors the paths it records, so `baseline/agents/skills/x/SKILL.md`
|
|
37
|
+
is that skill as the last completed pass left it. Both sides are there on
|
|
38
|
+
purpose: the specification and the feedback say what was **asked for**, the
|
|
39
|
+
wiring and the `agents/` tree say what was **built**, and a pass has to know
|
|
40
|
+
which of the two moved.
|
|
41
|
+
|
|
42
|
+
The baseline is **not a copy of the current specification**. It is the
|
|
43
|
+
specification as the last completed pass left it, which is what makes the
|
|
44
|
+
difference between the two mean "work nobody has done yet".
|
|
45
|
+
|
|
46
|
+
`zen init` creates `baseline/` and `history/` empty, and git does not carry an
|
|
47
|
+
empty directory, so **the presence of `.spec-sync/` proves nothing**. The
|
|
48
|
+
manifest is the signal: no `manifest.txt` means no pass has ever completed.
|
|
49
|
+
|
|
50
|
+
## Deciding the size of the pass
|
|
51
|
+
|
|
52
|
+
Run this first, from the project root:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
.github/skills/zen-spec-sync/scripts/snapshot.sh status
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
It prints one `mode:` line, the `applied:` stamp the baseline belongs to, and a
|
|
59
|
+
`changed:` line per file that differs - specification, feedback, `agents.yaml`
|
|
60
|
+
or anything under `agents/`:
|
|
61
|
+
|
|
62
|
+
| Mode | Means | Do |
|
|
63
|
+
| ------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
64
|
+
| `full` | no manifest - no pass has ever completed | the whole prompt as written, then write the first baseline |
|
|
65
|
+
| `incremental` | a baseline is there and the files differ | scope from the four inputs below |
|
|
66
|
+
| `tampered` | a baseline file does not match its own sha | treat as `full`, and say in the chat which file and that the baseline was not trusted |
|
|
67
|
+
|
|
68
|
+
`tampered` is either a baseline edited by hand or a pass that died between
|
|
69
|
+
copying the files and writing the manifest. Both mean the record is not
|
|
70
|
+
evidence. Do not rewrite `manifest.txt` to make the message go away - that
|
|
71
|
+
asserts a pass happened that did not.
|
|
72
|
+
|
|
73
|
+
`snapshot.sh diff` then prints the unified diffs, and is where the pass
|
|
74
|
+
actually starts.
|
|
75
|
+
|
|
76
|
+
## The four scope inputs
|
|
77
|
+
|
|
78
|
+
An incremental pass is scoped by the union of these. Write the union down before
|
|
79
|
+
editing anything, and report it in the chat as the pass's scope.
|
|
80
|
+
|
|
81
|
+
1. **The `SPECIFICATION.md` diff.** The primary input: added, changed and
|
|
82
|
+
deleted requirements. A deleted requirement is work too - what implemented it
|
|
83
|
+
is now an **extra**, to be reported under `➕` rather than quietly deleted.
|
|
84
|
+
2. **The `SPECIFICATION-FEEDBACK.md` diff.** The file is a questionnaire the
|
|
85
|
+
human answers by ticking one answer's `[ ]` box or deleting the answers they
|
|
86
|
+
do not want. A question that has moved under `✅ Answered` was folded into the
|
|
87
|
+
specification by `/update-spec-from-feedback`, and its **Still to implement**
|
|
88
|
+
line is the work. A question with a box marked `[x]`, or one answer left
|
|
89
|
+
under it, and no `✅` entry was answered and never folded in - implement it,
|
|
90
|
+
put its text into `SPECIFICATION.md`, and say so. A new question someone else
|
|
91
|
+
raised is work too. Close what this pass resolves rather than re-raising it.
|
|
92
|
+
3. **What the last pass left open.** The `Still open` section of the entry named
|
|
93
|
+
by `applied:`, in `.spec-sync/history/`. A specification line that has not
|
|
94
|
+
changed since the baseline is **not therefore implemented**: the baseline
|
|
95
|
+
records what the pass applied, and a pass can complete with items open. This
|
|
96
|
+
is the only place that says which.
|
|
97
|
+
4. **The implementation diff.** `agents.yaml` and every file under `agents/` -
|
|
98
|
+
instructions, prompts and skills. This side is what was built rather than
|
|
99
|
+
what was asked for, so it never narrows a pass: it says what moved under you
|
|
100
|
+
since the last one. An agent, tool grant, handoff, model, house rule, prompt
|
|
101
|
+
or skill edited outside a pass **widens** the pass to everything that names
|
|
102
|
+
what changed, and the edit is itself an item - either the specification
|
|
103
|
+
covers it, and you say which line, or it does not, and it is an **extra**
|
|
104
|
+
reported under `➕`.
|
|
105
|
+
|
|
106
|
+
## The four checks a narrow pass may never skip
|
|
107
|
+
|
|
108
|
+
However small the diff:
|
|
109
|
+
|
|
110
|
+
1. **`zen check` passes.** Run it before the first edit and again after the
|
|
111
|
+
last.
|
|
112
|
+
2. **`scripts/_setup.sh` has run to completion in this session, and re-runs
|
|
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.
|
|
115
|
+
3. **§9 of the copilot instructions, worked as a checklist**, group by group.
|
|
116
|
+
4. **The capability obligations in §0.1.** For every capability the project has
|
|
117
|
+
turned on, load that capability's skill and re-check what it requires - in
|
|
118
|
+
particular that `agents/memory-instructions.md` is still byte-identical to
|
|
119
|
+
`.github/skills/zen-memory/references/memory-instructions.md`. `zen init` and
|
|
120
|
+
`zen open` rewrite the reference, so this drifts without anybody editing the
|
|
121
|
+
project.
|
|
122
|
+
|
|
123
|
+
A narrow diff narrows reading and editing. It does not narrow verification.
|
|
124
|
+
|
|
125
|
+
## What else an incremental pass still owes
|
|
126
|
+
|
|
127
|
+
The reverse pass of §2 - reading each line and naming the specification item it
|
|
128
|
+
serves - runs over every file this pass touched, plus every file the
|
|
129
|
+
implementation diff named. It is the only pass that finds a line contradicting
|
|
130
|
+
another file, and an edit made this session, or made by hand since the last
|
|
131
|
+
pass, is exactly where a new contradiction comes from.
|
|
132
|
+
|
|
133
|
+
## The history entry
|
|
134
|
+
|
|
135
|
+
One file per pass, `.spec-sync/history/<stamp>.md`, where the stamp is UTC and
|
|
136
|
+
filename-safe: `date -u +%Y-%m-%dT%H%M%SZ`. Never edit a file that is already
|
|
137
|
+
there; a pass that did nothing still writes one saying so.
|
|
138
|
+
|
|
139
|
+
These headings, in this order, because the next pass reads them:
|
|
140
|
+
|
|
141
|
+
```markdown
|
|
142
|
+
# Sync pass 2026-09-13T104500Z
|
|
143
|
+
|
|
144
|
+
- **Mode** - incremental
|
|
145
|
+
- **Spec sha** - 3f9a… (`shasum -a 256 SPECIFICATION.md`)
|
|
146
|
+
|
|
147
|
+
## Scope
|
|
148
|
+
|
|
149
|
+
What drove the pass: the diff hunks, the feedback entries, what the last pass
|
|
150
|
+
left open. `full` if it was a full pass.
|
|
151
|
+
|
|
152
|
+
## Changed
|
|
153
|
+
|
|
154
|
+
One row per file changed, and the specification item it closes.
|
|
155
|
+
|
|
156
|
+
## Feedback
|
|
157
|
+
|
|
158
|
+
Entries opened and entries closed, by number.
|
|
159
|
+
|
|
160
|
+
## Still open
|
|
161
|
+
|
|
162
|
+
Every specification item this pass did **not** close, with its
|
|
163
|
+
`SPECIFICATION-FEEDBACK.md` anchor. Empty means empty - this is what the next
|
|
164
|
+
pass trusts.
|
|
165
|
+
|
|
166
|
+
## Checks
|
|
167
|
+
|
|
168
|
+
`zen check`, each `scripts/_setup.sh` step, and the §9 groups: pass or fail.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Write the baseline last
|
|
172
|
+
|
|
173
|
+
The one rule that must not be got wrong.
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
.github/skills/zen-spec-sync/scripts/snapshot.sh commit
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
runs **after** `zen check` passes, **after** `scripts/_setup.sh` has completed,
|
|
180
|
+
and **after** the history file is on disk. It takes the newest entry in
|
|
181
|
+
`.spec-sync/history/` - the stamps sort chronologically by name - records it in
|
|
182
|
+
`applied.txt`, and writes `manifest.txt` last of all. It refuses to run when
|
|
183
|
+
there is no history file, and when the newest one is the entry `applied.txt`
|
|
184
|
+
already names: that means this pass wrote no entry of its own.
|
|
185
|
+
|
|
186
|
+
A pass that moves the baseline early and then fails records a specification as
|
|
187
|
+
applied that was never applied - and the next pass diffs against it, sees
|
|
188
|
+
nothing, and skips the work for good. Failing with the baseline unmoved costs
|
|
189
|
+
one repeated pass; failing with it moved costs a requirement that is never
|
|
190
|
+
implemented and that nothing will report. Say in the chat which history file was
|
|
191
|
+
written and that the baseline moved.
|
|
192
|
+
|
|
193
|
+
## The first pass on a project that has no baseline
|
|
194
|
+
|
|
195
|
+
`mode: full`, so run the prompt as written, end to end. Then write the history
|
|
196
|
+
file and commit the baseline exactly as any other pass does - a project only
|
|
197
|
+
becomes incremental by having completed one full pass.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# The committed record of what the last completed /sync-with-spec pass applied.
|
|
4
|
+
#
|
|
5
|
+
# snapshot.sh status full | incremental | tampered, and what differs
|
|
6
|
+
# snapshot.sh diff the difference the next pass has to work
|
|
7
|
+
# snapshot.sh commit move the baseline on, once the pass has finished
|
|
8
|
+
#
|
|
9
|
+
# Run it from anywhere; it finds the project root from its own location, which
|
|
10
|
+
# `zen init` and `zen open` fix. The baseline is written last and only last —
|
|
11
|
+
# see .github/skills/zen-spec-sync/SKILL.md for why that is the whole point.
|
|
12
|
+
|
|
13
|
+
set -eu
|
|
14
|
+
cd "$(dirname "$0")/../../../.."
|
|
15
|
+
|
|
16
|
+
STATE=.spec-sync
|
|
17
|
+
BASE="$STATE/baseline"
|
|
18
|
+
HISTORY="$STATE/history"
|
|
19
|
+
MANIFEST="$BASE/manifest.txt"
|
|
20
|
+
SCRATCH=.tmp/spec-sync
|
|
21
|
+
|
|
22
|
+
# The intent: what the project is meant to be, and what was asked back about it.
|
|
23
|
+
INTENT="SPECIFICATION.md SPECIFICATION-FEEDBACK.md"
|
|
24
|
+
|
|
25
|
+
# The implementation: the wiring, and every instruction, prompt and skill file
|
|
26
|
+
# under agents/. One of those edited since the last pass is a change the pass
|
|
27
|
+
# has to account for, the same as a changed requirement is.
|
|
28
|
+
WIRING=agents.yaml
|
|
29
|
+
TREE=agents
|
|
30
|
+
|
|
31
|
+
# The history entry this baseline belongs to, hashed like the rest of it.
|
|
32
|
+
APPLIED=applied.txt
|
|
33
|
+
|
|
34
|
+
if [ ! -f "$WIRING" ] && [ ! -f SPECIFICATION.md ]; then
|
|
35
|
+
echo "snapshot.sh: no agents.yaml or SPECIFICATION.md in $PWD" >&2
|
|
36
|
+
exit 2
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Git cannot carry an empty directory, so these are made rather than expected.
|
|
40
|
+
mkdir -p "$BASE" "$HISTORY"
|
|
41
|
+
|
|
42
|
+
# Every path the record covers, on the working-tree side.
|
|
43
|
+
present() {
|
|
44
|
+
{
|
|
45
|
+
for file in $INTENT $WIRING; do
|
|
46
|
+
if [ -f "$file" ]; then echo "$file"; fi
|
|
47
|
+
done
|
|
48
|
+
if [ -d "$TREE" ]; then find "$TREE" -type f ! -name .DS_Store; fi
|
|
49
|
+
} | sort -u
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# The same, from both sides, so a file added or deleted since the last pass is
|
|
53
|
+
# a difference rather than a path nobody walks.
|
|
54
|
+
scope() {
|
|
55
|
+
{
|
|
56
|
+
present
|
|
57
|
+
for file in $INTENT $WIRING; do
|
|
58
|
+
if [ -f "$BASE/$file" ]; then echo "$file"; fi
|
|
59
|
+
done
|
|
60
|
+
if [ -d "$BASE/$TREE" ]; then
|
|
61
|
+
(cd "$BASE" && find "$TREE" -type f ! -name .DS_Store)
|
|
62
|
+
fi
|
|
63
|
+
} | sort -u
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# A file that is not there counts as an empty one, so adding or deleting it
|
|
67
|
+
# reads as a difference rather than as an error.
|
|
68
|
+
side() {
|
|
69
|
+
if [ -f "$1" ]; then echo "$1"; else echo /dev/null; fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
differs() {
|
|
73
|
+
! cmp -s "$(side "$BASE/$1")" "$(side "$1")"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Which baseline files no longer hash to what the manifest says they do.
|
|
77
|
+
# `FAILED` covers a changed file; `FAILED open or read` covers a missing one.
|
|
78
|
+
broken() {
|
|
79
|
+
(cd "$BASE" && shasum -a 256 -c manifest.txt 2>/dev/null || true) |
|
|
80
|
+
sed -n 's/: FAILED.*$//p'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# The stamps sort chronologically by name, which is the point of the format —
|
|
84
|
+
# an mtime would make a fast pass look like no pass at all.
|
|
85
|
+
newest() {
|
|
86
|
+
ls "$HISTORY"/*.md 2>/dev/null | sort | tail -1 || true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
status() {
|
|
90
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
91
|
+
echo "mode: full"
|
|
92
|
+
echo "reason: no baseline, so no pass has ever completed here"
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
failed="$(broken)"
|
|
96
|
+
if [ -n "$failed" ]; then
|
|
97
|
+
echo "mode: tampered"
|
|
98
|
+
while IFS= read -r file; do
|
|
99
|
+
if [ -n "$file" ]; then echo "unsound: $BASE/$file"; fi
|
|
100
|
+
done <<EOF
|
|
101
|
+
$failed
|
|
102
|
+
EOF
|
|
103
|
+
echo "reason: the baseline does not match its own manifest"
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
echo "mode: incremental"
|
|
107
|
+
if [ -f "$BASE/$APPLIED" ]; then
|
|
108
|
+
echo "applied: $(cat "$BASE/$APPLIED")"
|
|
109
|
+
fi
|
|
110
|
+
while IFS= read -r file; do
|
|
111
|
+
if [ -n "$file" ] && differs "$file"; then
|
|
112
|
+
echo "changed: $file"
|
|
113
|
+
fi
|
|
114
|
+
done <<EOF
|
|
115
|
+
$(scope)
|
|
116
|
+
EOF
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
show_diff() {
|
|
120
|
+
while IFS= read -r file; do
|
|
121
|
+
if [ -n "$file" ] && differs "$file"; then
|
|
122
|
+
diff -u -L "baseline/$file" -L "$file" \
|
|
123
|
+
"$(side "$BASE/$file")" "$(side "$file")" || true
|
|
124
|
+
fi
|
|
125
|
+
done <<EOF
|
|
126
|
+
$(scope)
|
|
127
|
+
EOF
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
commit() {
|
|
131
|
+
entry="$(newest)"
|
|
132
|
+
if [ -z "$entry" ]; then
|
|
133
|
+
echo "snapshot.sh: write $HISTORY/<stamp>.md before moving the baseline" >&2
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
stamp="$(basename "$entry" .md)"
|
|
137
|
+
if [ -f "$BASE/$APPLIED" ] && [ "$stamp" = "$(cat "$BASE/$APPLIED")" ]; then
|
|
138
|
+
echo "snapshot.sh: the baseline already names $stamp — this pass wrote no entry" >&2
|
|
139
|
+
exit 1
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
staged="$(present)"
|
|
143
|
+
rm -rf "$SCRATCH"
|
|
144
|
+
mkdir -p "$SCRATCH"
|
|
145
|
+
echo "$stamp" >"$SCRATCH/$APPLIED"
|
|
146
|
+
: >"$SCRATCH/manifest.txt"
|
|
147
|
+
while IFS= read -r file; do
|
|
148
|
+
if [ -n "$file" ]; then
|
|
149
|
+
mkdir -p "$SCRATCH/$(dirname "$file")"
|
|
150
|
+
cp "$file" "$SCRATCH/$file"
|
|
151
|
+
fi
|
|
152
|
+
done <<EOF
|
|
153
|
+
$staged
|
|
154
|
+
EOF
|
|
155
|
+
while IFS= read -r file; do
|
|
156
|
+
if [ -n "$file" ]; then
|
|
157
|
+
(cd "$SCRATCH" && shasum -a 256 "$file") >>"$SCRATCH/manifest.txt"
|
|
158
|
+
fi
|
|
159
|
+
done <<EOF
|
|
160
|
+
$APPLIED
|
|
161
|
+
$staged
|
|
162
|
+
EOF
|
|
163
|
+
|
|
164
|
+
while IFS= read -r file; do
|
|
165
|
+
if [ -n "$file" ]; then
|
|
166
|
+
mkdir -p "$BASE/$(dirname "$file")"
|
|
167
|
+
mv "$SCRATCH/$file" "$BASE/$file"
|
|
168
|
+
fi
|
|
169
|
+
done <<EOF
|
|
170
|
+
$APPLIED
|
|
171
|
+
$staged
|
|
172
|
+
EOF
|
|
173
|
+
# Everything in scope that still exists has just been staged, so a baseline
|
|
174
|
+
# file with no working copy is one this pass deleted.
|
|
175
|
+
while IFS= read -r file; do
|
|
176
|
+
if [ -n "$file" ] && [ "$file" != "$APPLIED" ] && [ ! -f "$file" ]; then
|
|
177
|
+
rm -f "$BASE/$file"
|
|
178
|
+
fi
|
|
179
|
+
done <<EOF
|
|
180
|
+
$(cd "$BASE" && find . -type f ! -name manifest.txt | sed 's|^\./||')
|
|
181
|
+
EOF
|
|
182
|
+
find "$BASE" -mindepth 1 -type d -empty -delete
|
|
183
|
+
# Last, so an interrupted commit leaves a baseline that reports itself as
|
|
184
|
+
# tampered rather than one that claims a pass it never finished.
|
|
185
|
+
mv "$SCRATCH/manifest.txt" "$MANIFEST"
|
|
186
|
+
rm -rf "$SCRATCH"
|
|
187
|
+
|
|
188
|
+
echo "applied: $stamp"
|
|
189
|
+
echo "recorded: $(printf '%s\n' "$staged" | sed '/^$/d' | wc -l | tr -d ' ') files"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case "${1:-status}" in
|
|
193
|
+
status) status ;;
|
|
194
|
+
diff) show_diff ;;
|
|
195
|
+
commit) commit ;;
|
|
196
|
+
*)
|
|
197
|
+
echo "usage: snapshot.sh [status|diff|commit]" >&2
|
|
198
|
+
exit 2
|
|
199
|
+
;;
|
|
200
|
+
esac
|
|
@@ -1,8 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"chat.useNestedAgentsMdFiles": false,
|
|
3
3
|
"chat.tools.terminal.enableAutoApprove": true,
|
|
4
|
-
"chat.tools.
|
|
5
|
-
"chat.tools.
|
|
6
|
-
"
|
|
4
|
+
"chat.tools.edits.autoApprove": true,
|
|
5
|
+
"chat.tools.terminal.autoApprove": {
|
|
6
|
+
"awk": true,
|
|
7
|
+
"basename": true,
|
|
8
|
+
"cat": true,
|
|
9
|
+
"cd": true,
|
|
10
|
+
"comm": true,
|
|
11
|
+
"cut": true,
|
|
12
|
+
"date": true,
|
|
13
|
+
"diff": true,
|
|
14
|
+
"dirname": true,
|
|
15
|
+
"du": true,
|
|
16
|
+
"echo": true,
|
|
17
|
+
"env": true,
|
|
18
|
+
"file": true,
|
|
19
|
+
"find": true,
|
|
20
|
+
"grep": true,
|
|
21
|
+
"head": true,
|
|
22
|
+
"jq": true,
|
|
23
|
+
"ls": true,
|
|
24
|
+
"nl": true,
|
|
25
|
+
"printf": true,
|
|
26
|
+
"pwd": true,
|
|
27
|
+
"realpath": true,
|
|
28
|
+
"rg": true,
|
|
29
|
+
"sed": true,
|
|
30
|
+
"sort": true,
|
|
31
|
+
"stat": true,
|
|
32
|
+
"tail": true,
|
|
33
|
+
"tr": true,
|
|
34
|
+
"tree": true,
|
|
35
|
+
"uniq": true,
|
|
36
|
+
"wc": true,
|
|
37
|
+
"which": true,
|
|
38
|
+
"tsc": true,
|
|
39
|
+
"zen": true,
|
|
40
|
+
"zn": true,
|
|
41
|
+
"zenera": true,
|
|
42
|
+
"/^git (branch|diff|log|show|status)\\b/": true,
|
|
43
|
+
"/^npm (test|run (build|format:check|lint|test|typecheck))\\b/": true,
|
|
44
|
+
"/\\bfind\\b[^|;]*\\s-(exec|execdir|ok|okdir|delete)\\b/": false,
|
|
45
|
+
"/\\bsed\\b[^|;]*\\s-[A-Za-z]*i/": false,
|
|
46
|
+
"/\\bawk\\b[^|;]*\\b(system|print\\s*>|printf\\s*>)/": false,
|
|
47
|
+
"/^\\.?\\/?scripts\\/[A-Za-z0-9_.-]+\\.sh\\b/": true,
|
|
48
|
+
"/^\\.?\\/?\\.github\\/skills\\/[A-Za-z0-9_-]+\\/scripts\\/[A-Za-z0-9_.-]+\\.sh\\b/": true,
|
|
49
|
+
"podman": true
|
|
7
50
|
}
|
|
8
51
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# What the agents work out, kept with the project rather than in the session
|
|
2
|
+
# that found it out. `agents/memory-instructions.md` is what they read about
|
|
3
|
+
# using it, and `zen memory` is how you look at it from the outside.
|
|
4
|
+
#
|
|
5
|
+
# Recall ranks by term overlap here, which finds a memory phrased the way the
|
|
6
|
+
# question was and misses the rest. This project's provider publishes no
|
|
7
|
+
# embeddings API; add one that does and point the store at it to rank by
|
|
8
|
+
# meaning instead:
|
|
9
|
+
#
|
|
10
|
+
# memory:
|
|
11
|
+
# embedding: main
|
|
12
|
+
#
|
|
13
|
+
# embeddings:
|
|
14
|
+
# main: openai:text-embedding-3-small
|
|
15
|
+
memory:
|
|
16
|
+
dir: memory
|