@zenera/cli 1.1.16 → 1.1.18
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 +110 -19
- package/dist/banner.d.ts +19 -0
- package/dist/banner.js +12 -9
- package/dist/catalog.js +19 -20
- package/dist/command.d.ts +3 -0
- package/dist/commands/check.js +5 -1
- package/dist/commands/index.js +4 -1
- package/dist/commands/init.js +42 -7
- package/dist/commands/inspect.js +77 -13
- package/dist/commands/key.js +49 -1
- package/dist/commands/memory.js +16 -3
- package/dist/commands/meta.d.ts +3 -0
- package/dist/commands/meta.js +596 -0
- package/dist/commands/run.js +64 -35
- package/dist/engine.d.ts +15 -0
- package/dist/engine.js +26 -1
- package/dist/env.d.ts +20 -0
- package/dist/env.js +65 -0
- package/dist/home.d.ts +2 -0
- package/dist/home.js +2 -0
- package/dist/keys.d.ts +2 -0
- package/dist/keys.js +2 -0
- package/dist/main.js +1 -1
- package/dist/meta.d.ts +221 -0
- package/dist/meta.js +695 -0
- package/dist/podman.js +64 -2
- package/dist/projects.d.ts +9 -1
- package/dist/projects.js +19 -2
- package/dist/resolve.js +7 -1
- package/dist/sandbox.d.ts +11 -0
- package/dist/sandbox.js +24 -4
- package/dist/scaffold.d.ts +6 -0
- package/dist/scaffold.js +56 -15
- package/dist/session.d.ts +10 -0
- package/dist/session.js +19 -1
- package/dist/term.d.ts +7 -0
- package/dist/term.js +8 -1
- package/dist/tui/app.js +33 -5
- package/dist/tui/wrap.d.ts +1 -1
- package/dist/tui/wrap.js +1 -1
- package/dist/validate.js +57 -15
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +313 -94
- package/templates/editor/.github/prompts/project-review.prompt.md +74 -0
- package/templates/editor/.github/prompts/spec-apply-feedback.prompt.md +142 -0
- package/templates/editor/.github/prompts/spec-sync-project.prompt.md +392 -0
- package/templates/editor/.github/skills/zen-cli/references/check.md +6 -3
- package/templates/editor/.github/skills/zen-cli/references/frame.md +5 -3
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +4 -3
- package/templates/editor/.github/skills/zen-cli/references/memory.md +14 -0
- package/templates/editor/.github/skills/zen-cli/references/projects.md +19 -2
- package/templates/editor/.github/skills/zen-cli/references/run.md +75 -16
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +6 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +153 -33
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +115 -0
- 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 +44 -4
- package/templates/parts/memory-terms.yaml.tmpl +16 -0
- package/templates/parts/memory.yaml.tmpl +12 -0
- package/templates/project/SPECIFICATION.md.tmpl +32 -2
- package/templates/project/agents/memory-instructions.md +278 -0
- package/templates/project/agents.yaml.tmpl +12 -3
- package/templates/project/env +28 -0
- package/templates/project/gitignore +4 -4
- package/templates/editor/.github/prompts/review-project.prompt.md +0 -36
- package/templates/editor/.github/prompts/sync-with-spec.prompt.md +0 -205
- /package/templates/project/{INSTRUCTIONS.md → agents/instructions.md} +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# The committed record of what the last completed /spec-sync-project 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,11 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"chat.useNestedAgentsMdFiles": false,
|
|
3
3
|
"chat.tools.terminal.enableAutoApprove": true,
|
|
4
|
-
"chat.tools.terminal.ignoreDefaultAutoApproveRules": true,
|
|
5
4
|
"chat.tools.edits.autoApprove": true,
|
|
6
5
|
"chat.tools.terminal.autoApprove": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
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
|
|
10
50
|
}
|
|
11
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
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
memory:
|
|
5
|
+
dir: memory
|
|
6
|
+
# Recall by meaning rather than by the words a question happened to reuse.
|
|
7
|
+
# The width is learned from the first response, so swapping this model
|
|
8
|
+
# later is refused rather than quietly mixing two vector spaces.
|
|
9
|
+
embedding: main
|
|
10
|
+
|
|
11
|
+
embeddings:
|
|
12
|
+
main: {{embedding}}
|
|
@@ -13,27 +13,57 @@ something in it — and report what was done in enough detail to be checked.
|
|
|
13
13
|
- One request at a time, in one workspace, with the person who asked it present.
|
|
14
14
|
- No schedule, no queue, nothing that outlives the session it was asked in.
|
|
15
15
|
|
|
16
|
+
## Models
|
|
17
|
+
|
|
18
|
+
- `{{modelRef}}` — one model for the whole project, used by every agent.
|
|
19
|
+
- Named once in `agents.yaml` so it can be swapped in one place. Nothing here
|
|
20
|
+
depends on which model it is; a project that later needs a cheaper model for
|
|
21
|
+
one job adds a second alias rather than scattering the choice.
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
- Run once, before any session: whatever has to exist before work can start —
|
|
26
|
+
an index built, a document fetched, a file generated.
|
|
27
|
+
- `scripts/_setup.sh` is the one command that does it, and `scripts/<name>.sh`
|
|
28
|
+
are the steps it runs in order. A step is safe to run twice and reports
|
|
29
|
+
`skipped` the second time.
|
|
30
|
+
- Currently: no steps. Nothing has to be prepared before a run.
|
|
31
|
+
|
|
16
32
|
## Agents
|
|
17
33
|
|
|
18
34
|
- `default` — the entry point and the only agent; nobody to hand work to.
|
|
19
35
|
- Establishes what it is looking at before it acts: reads a file before
|
|
20
36
|
changing it, runs a command rather than predicting its output.
|
|
37
|
+
- Remembers what it worked out, so a later session starts from it rather
|
|
38
|
+
than from nothing.
|
|
21
39
|
|
|
22
40
|
## Tools and boundaries
|
|
23
41
|
|
|
24
42
|
- Files — read, search, create, edit, move, delete. The workspace is the only
|
|
25
43
|
writable thing, symlinks included.
|
|
26
44
|
- Shell — commands run in the container, on that same workspace.
|
|
45
|
+
- Memory — a graph of what earlier runs established, recalled before new
|
|
46
|
+
questions and added to deliberately. What goes in it, and what does not, is
|
|
47
|
+
`agents/memory-instructions.md`.
|
|
27
48
|
- `assets/` is reference material, and read-only.
|
|
28
49
|
{{web}}
|
|
29
50
|
|
|
30
51
|
## Environment
|
|
31
52
|
|
|
32
|
-
-
|
|
53
|
+
- Memory: `memory/`, kept with the project and committed except for its
|
|
54
|
+
vectors, which are derived.
|
|
33
55
|
- Sandbox: built from `sandbox/Dockerfile`, persists between runs, and can
|
|
34
56
|
reach the network. What the work always needs belongs in the image, not in an
|
|
35
57
|
instruction that reinstalls it on every run.
|
|
36
|
-
|
|
58
|
+
|
|
59
|
+
## Testing
|
|
60
|
+
|
|
61
|
+
- `zen check` passes: the project loads, and every model, tool and skill an
|
|
62
|
+
agent is given resolves. It says nothing about whether the prose is right.
|
|
63
|
+
- `scripts/_setup.sh` run twice in a row reports every step `skipped` the
|
|
64
|
+
second time.
|
|
65
|
+
- A run against a real request: the workspace holds the change described, and
|
|
66
|
+
the reply's account of it matches what is on disk.
|
|
37
67
|
|
|
38
68
|
## Done means
|
|
39
69
|
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Memory
|
|
2
|
+
|
|
3
|
+
This project has a memory: a graph of what earlier runs worked out, kept with
|
|
4
|
+
the project rather than with the session. It is reached **only** through the
|
|
5
|
+
`memory_*` tools - `memory_search` to find, `memory_load` to read,
|
|
6
|
+
`memory_commit` to write.
|
|
7
|
+
|
|
8
|
+
It exists to make the next run cheaper. Work that cost something - a long
|
|
9
|
+
investigation, a fan-out of searches, a build, an invocation found by trial -
|
|
10
|
+
should be paid for once. A recollection is a cache hit: check it, do not
|
|
11
|
+
rebuild it.
|
|
12
|
+
|
|
13
|
+
## What belongs in it
|
|
14
|
+
|
|
15
|
+
One question decides both what to keep and how far to trust it:
|
|
16
|
+
|
|
17
|
+
**Can the next run get this itself, cheaply and current?**
|
|
18
|
+
|
|
19
|
+
| When the answer is | Keep |
|
|
20
|
+
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
21
|
+
| one read, one call, one command away | the way to get it - the route, the call, the script, the invocation |
|
|
22
|
+
| assembled from several sources, with a judgement on top | what you concluded, and a pointer to every source it came from |
|
|
23
|
+
| out of something that cannot change - a released spec, a pinned or versioned document | the finding itself, whole, in a file that names where it came from |
|
|
24
|
+
| a reading of the moment - an API response, a query result, what a script printed | the call that produces it, never the reading |
|
|
25
|
+
|
|
26
|
+
Remember the **route** when the answer is cheap, the **answer** when the route
|
|
27
|
+
is expensive, and never a value that is stale by the next run.
|
|
28
|
+
|
|
29
|
+
## How it is organised
|
|
30
|
+
|
|
31
|
+
A node is one thing worth keeping. It carries a `kind`, a `text` - the only
|
|
32
|
+
thing searched - and optionally a remembered file. An edge says how one node led
|
|
33
|
+
to another.
|
|
34
|
+
|
|
35
|
+
The two halves do different jobs, and the split is the same every time: the
|
|
36
|
+
**text is how the node is found** - what it is, what it establishes, where it
|
|
37
|
+
came from - and the **file is what the node holds**. Only the text is searched,
|
|
38
|
+
and only its first 160 characters are ever shown, so content written into it is
|
|
39
|
+
both unfindable and in the way. Once something is worth committing at all, the
|
|
40
|
+
usual shape is therefore a short description and a file behind it.
|
|
41
|
+
|
|
42
|
+
A node with no file is for what fits on **one line** - a path and a line range,
|
|
43
|
+
a command, a standing instruction, a single stated fact. If what you are keeping
|
|
44
|
+
runs past a line or two, it is content, and content goes in the file.
|
|
45
|
+
|
|
46
|
+
| Kind | Is |
|
|
47
|
+
| ------------ | ------------------------------------------------------------------------------------ |
|
|
48
|
+
| `task` | the request that started a piece of work |
|
|
49
|
+
| `plan` | an approach that worked and would work again |
|
|
50
|
+
| `fact` | a durable truth about this codebase, API or environment |
|
|
51
|
+
| `snippet` | a few lines of code or configuration |
|
|
52
|
+
| `file` | an artifact kept whole - a script to re-run, an answer or passage too big for a text |
|
|
53
|
+
| `operation` | an external call that was made - endpoint, command, query. The call, not its answer |
|
|
54
|
+
| `preference` | a standing instruction from the user |
|
|
55
|
+
|
|
56
|
+
| Relation | Reads as |
|
|
57
|
+
| ------------ | --------------------------------------------------- |
|
|
58
|
+
| `PRODUCED` | the source led to the target being made |
|
|
59
|
+
| `CALLS` | the source invokes the target |
|
|
60
|
+
| `SUPERSEDES` | the source replaces the target; the target is stale |
|
|
61
|
+
| `INFORMED` | the source was context the target was built from |
|
|
62
|
+
|
|
63
|
+
So a remembered piece of work is a subgraph, not a row:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
task the request that started this
|
|
67
|
+
PRODUCED → file | snippet | operation what came out of it
|
|
68
|
+
INFORMED ← fact what had to be known first
|
|
69
|
+
file
|
|
70
|
+
CALLS → operation what it calls
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
What you are shown is ranked, masked and clipped. **Assume a recollection is
|
|
74
|
+
part of the answer, never all of it.**
|
|
75
|
+
|
|
76
|
+
Relation is what decides where the clipping falls. Everything reached across
|
|
77
|
+
`PRODUCED`, `CALLS` or `SUPERSEDES` is the work itself; everything reached
|
|
78
|
+
across `INFORMED` is context, and context is what a short recollection drops
|
|
79
|
+
first - so the answer degrades by losing its background rather than by being cut
|
|
80
|
+
in half. Attach with `INFORMED` what **explains** a node, never what the next run
|
|
81
|
+
needs in order to **use** it: the endpoint a script calls, or the request that
|
|
82
|
+
produced it, belongs on the spine, where it is still there when the budget is
|
|
83
|
+
tight.
|
|
84
|
+
|
|
85
|
+
`INFORMED` also reads in a direction - from the background to the work it fed,
|
|
86
|
+
so a `fact` informs an answer and never the other way round. Written backwards
|
|
87
|
+
it still traverses, and still renders; it just says the answer was context for
|
|
88
|
+
its own source.
|
|
89
|
+
|
|
90
|
+
## Reading it
|
|
91
|
+
|
|
92
|
+
`memory_search` hands back an outline of a subgraph with each node's text cut to
|
|
93
|
+
160 characters. `memory_load` is what reads a node whole, by id - load anything
|
|
94
|
+
you are going to rely on.
|
|
95
|
+
|
|
96
|
+
A remembered file appears at `/memory/<id>.<ext>`. Open it or run it, but arrive
|
|
97
|
+
at that path through `memory_load`, never by looking around in the directory.
|
|
98
|
+
|
|
99
|
+
**Never read the store directly.** `/memory` holds `graph.json`, `vectors.f32`,
|
|
100
|
+
`vectors.json` and `manifest.json` - its internal format. Do not open, list,
|
|
101
|
+
grep, `cat` or `find` them, and do not point a shell command at that directory:
|
|
102
|
+
|
|
103
|
+
- it is ranked by **meaning**, so substring matching misses the hits;
|
|
104
|
+
- `graph.json` is every node ever committed, as one line;
|
|
105
|
+
- a raw read bypasses the audience mask and serves **superseded** nodes -
|
|
106
|
+
corrections that were withdrawn - as if they were current;
|
|
107
|
+
- it records no use, so the ranking that decides what gets recalled next decays;
|
|
108
|
+
- another run may be writing, and the manifest is written last.
|
|
109
|
+
|
|
110
|
+
A memory is a record, not an authority - but checking one is not the same as
|
|
111
|
+
redoing it. Every node names where its content came from, so confirm the
|
|
112
|
+
recollection there: the named lines still say what the node says, the route
|
|
113
|
+
still resolves, the command still exists. Work it out from nothing only when
|
|
114
|
+
that check fails, when the source is one that changes under you, or when being
|
|
115
|
+
wrong is expensive. A check that costs what the original investigation cost is
|
|
116
|
+
not a check - it is the investigation again, and the memory bought nothing.
|
|
117
|
+
|
|
118
|
+
## Searching it
|
|
119
|
+
|
|
120
|
+
Search **before** paying for anything: an investigation, an index query, a
|
|
121
|
+
build, a long command. The point is not to find a pointer - it is to arrive at
|
|
122
|
+
the answer without buying it twice.
|
|
123
|
+
|
|
124
|
+
One `memory_load` is not cheaper than re-reading the single file a node names.
|
|
125
|
+
It is far cheaper than redoing what produced that node: several documents read
|
|
126
|
+
in sequence, a fan-out of searches, a delegated lookup, a long run. Price a
|
|
127
|
+
recollection against what it **saves**, never against one read.
|
|
128
|
+
|
|
129
|
+
Phrase the query as the question you are actually asking, in the words a person
|
|
130
|
+
would use - node texts are written to be found that way. Narrow with `kinds`,
|
|
131
|
+
`newer_than`, `limit`, `max_hops` and `max_nodes` when a recollection comes back
|
|
132
|
+
broad rather than wrong.
|
|
133
|
+
|
|
134
|
+
Recalling nothing is not proof of anything: work it out normally, then commit
|
|
135
|
+
the result so the next run does not repeat it.
|
|
136
|
+
|
|
137
|
+
## Updating it
|
|
138
|
+
|
|
139
|
+
Commit when **the next run would otherwise have to work this out again** and
|
|
140
|
+
working it out cost more than a `memory_load` will. The measure is what the
|
|
141
|
+
answer COST, not what kind of thing it is: one cheap tool call away, do not;
|
|
142
|
+
five reads, a fan-out and a judgement, commit the judgement and not only the
|
|
143
|
+
five paths.
|
|
144
|
+
|
|
145
|
+
| Worth committing | Not worth committing |
|
|
146
|
+
| ----------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
147
|
+
| The route through the codebase to a recurring question | A verbatim copy of one living file, which a read gives current |
|
|
148
|
+
| What a schema or docs search resolved to, and what you made of it | The ranked guesses it returned on the way there |
|
|
149
|
+
| The call or command sequence that turned out to be necessary | A single tool call that answers in milliseconds |
|
|
150
|
+
| The exact invocation that finally worked, with its directory | The request restated, or notes on progress |
|
|
151
|
+
| A working script or request body, committed whole | What running it printed - a reading, which the script gives current |
|
|
152
|
+
| An interface that behaved differently from its documentation | What a call returned, which the call gives current |
|
|
153
|
+
| The passage itself, when its source cannot change under you | A passage quoted out of something that can |
|
|
154
|
+
| A proven absence - looked here and here, it is not there | Anything not actually verified |
|
|
155
|
+
|
|
156
|
+
A search over a schema or a documentation index costs a round trip and comes
|
|
157
|
+
back ranked, so it is the thing most worth not paying for twice. Commit what it
|
|
158
|
+
**resolved to** - the operation and its shape, or the document, heading and line
|
|
159
|
+
range - **and what you concluded from reading it**. The pointers alone make the
|
|
160
|
+
next run repeat the reading; the conclusion alone cannot be checked. Both, in
|
|
161
|
+
one subgraph, and the next run answers at once and verifies cheaply.
|
|
162
|
+
|
|
163
|
+
When the index is over something pinned - a released spec, a versioned reference
|
|
164
|
+
tree - go one further and carry the passage itself in a `file` node, headed with
|
|
165
|
+
the documents and line ranges it was taken from. The pointer says where to look
|
|
166
|
+
and the conclusion says what it meant, but only the passage saves the next run
|
|
167
|
+
from opening the sources to find out what they actually said.
|
|
168
|
+
|
|
169
|
+
Then the rules for writing it down:
|
|
170
|
+
|
|
171
|
+
**Commit the whole subgraph in one call.** A script with no record of what asked
|
|
172
|
+
for it, or a fact with nothing that acts on it, is a memory nobody can use.
|
|
173
|
+
|
|
174
|
+
**Say where the content came from.** This is what lets an incomplete
|
|
175
|
+
recollection be continued instead of believed:
|
|
176
|
+
|
|
177
|
+
| The node holds | Name |
|
|
178
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
179
|
+
| A fact about this codebase | the file and the line range it was read from - `src/models/factory.ts:120-168` |
|
|
180
|
+
| Anything spanning several files | every one of them, each with its range, in the order they are read |
|
|
181
|
+
| An API route or a behaviour | the document and operation id, or the request sent and the status that came back |
|
|
182
|
+
| Something a document states | document, heading and line range, next to what it states - the pointer is how the passage is checked, not a substitute for it |
|
|
183
|
+
| Something a command established | the exact command, its arguments, and the directory it ran in |
|
|
184
|
+
| A plan | the steps, and where each step's target lives |
|
|
185
|
+
| A negative result | where you looked, so the search is not repeated |
|
|
186
|
+
|
|
187
|
+
**Write the text as the question.** Not the way the system that produced it
|
|
188
|
+
names things, and with the identifying facts first - only the first 160
|
|
189
|
+
characters appear in a recollection.
|
|
190
|
+
|
|
191
|
+
| Instead of | Write |
|
|
192
|
+
| ----------------- | ------------------------------------------------------------------------------------------ |
|
|
193
|
+
| `fix for the 409` | `A duplicate customer email returns 409, not 422 - retry is wrong (src/billing.ts:88-104)` |
|
|
194
|
+
| `retry.ts` | `Where retries are configured: one policy for all adapters, src/models/factory.ts:40-96` |
|
|
195
|
+
|
|
196
|
+
**Put the content in a file, not in the text.** This is the usual shape, not a
|
|
197
|
+
special case: a script, a config, a query, a request body, a rendered table of
|
|
198
|
+
findings, a passage worth keeping whole, an answer that took several sources to
|
|
199
|
+
assemble - all of it is a `file` node split in two. The `text` describes the
|
|
200
|
+
content and says where it came from; the file holds the content itself, whole
|
|
201
|
+
and exactly as it was - the entire block, not a summary of it. Content in a file
|
|
202
|
+
costs nothing until a `memory_load` asks for it, and comes back at
|
|
203
|
+
`/memory/<id>.<ext>` to be read or run. Commit a node with no file only when the
|
|
204
|
+
whole of what you know is one line long.
|
|
205
|
+
|
|
206
|
+
**Name the sources inside the file as well as in the text.** The two halves
|
|
207
|
+
arrive separately - a recollection shows the text, and only a later
|
|
208
|
+
`memory_load` opens the file - so a file read on its own is an assertion with
|
|
209
|
+
nothing behind it. Open it with a header naming every document, heading and line
|
|
210
|
+
range the content came from, and when it was taken, then the content verbatim:
|
|
211
|
+
|
|
212
|
+
```markdown
|
|
213
|
+
<!-- reference-docs-1.4/overview.html.md:112-160 and
|
|
214
|
+
reference-docs-1.4/architecture.html.md:38-91
|
|
215
|
+
pinned 1.4 reference tree, read 2026-04-09 -->
|
|
216
|
+
|
|
217
|
+
...the passages, whole and unedited...
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Use whatever comment form the file's own format takes. A header costs two lines
|
|
221
|
+
and is the difference between a copy that can be re-checked against its source
|
|
222
|
+
and one that can only be believed.
|
|
223
|
+
|
|
224
|
+
Make the file self-contained - it will be opened in a different workspace, so it
|
|
225
|
+
takes its inputs as arguments and reads credentials from the environment - and
|
|
226
|
+
say so in the text. **Give it a configurable output path.** `/memory` is
|
|
227
|
+
read-only: a script that writes beside itself fails, while one that takes
|
|
228
|
+
`--out` writes into the workspace and is reusable unchanged. Use a `snippet`
|
|
229
|
+
only for the few lines that are read rather than run.
|
|
230
|
+
|
|
231
|
+
A remembered script runs from where it is - the mount is readable, so
|
|
232
|
+
`python3 /memory/<id>.py` or `node /memory/<id>.js` needs no copy first:
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
python3 /memory/01JD9Q7X8N2K4M6P8R0T2V4W6Y.py --out ./report.csv
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Read it before running it.** It is code an earlier run wrote, recalled by
|
|
239
|
+
similarity: `memory_load` returns its contents, or its path when it is large, in
|
|
240
|
+
which case read the file. Confirm it is the thing you want, and that its
|
|
241
|
+
arguments are what you think, before executing anything.
|
|
242
|
+
|
|
243
|
+
**Commit what produces the answer, not the material it was made from.** This is
|
|
244
|
+
about what to keep, not what form to keep it in - whatever survives the test
|
|
245
|
+
still goes in a file. A file the project holds: the pointer, never a copy - a
|
|
246
|
+
copy goes stale silently and looks exactly as authoritative when it is wrong. An
|
|
247
|
+
external call: the `operation` - route, auth, the parameters that mattered, the
|
|
248
|
+
status a case returns - never the body that came back. A script: the file and
|
|
249
|
+
the invocation that worked, never its output. What a run ESTABLISHED belongs in
|
|
250
|
+
the node's text - that it works, what it was run against, the verdict that was
|
|
251
|
+
the point; the rows it emitted do not.
|
|
252
|
+
|
|
253
|
+
Two things fall outside that. An answer you assembled from several sources is a
|
|
254
|
+
copy of none of them and exists nowhere in the project, so commit it, with a
|
|
255
|
+
pointer node per source `INFORMED`ing it. And a source that cannot change under
|
|
256
|
+
you (a released specification, a pinned dependency, a versioned reference tree)
|
|
257
|
+
does not go stale silently, so the finding may be kept whole.
|
|
258
|
+
|
|
259
|
+
When it is kept whole, keep it as a `file` node rather than as a longer `text`:
|
|
260
|
+
the text says what the passage establishes and names the documents it came from,
|
|
261
|
+
the file carries the whole block behind its provenance header. Say in the text
|
|
262
|
+
which kind of source it was, so a later run can see why a copy was allowed. A
|
|
263
|
+
pointer alone, with nothing of what the source said, makes the next run open all
|
|
264
|
+
of it again - which is the reading the memory was supposed to buy.
|
|
265
|
+
|
|
266
|
+
A body or a result set may still be worth keeping as a SPECIMEN - a shape to
|
|
267
|
+
write against, a case to reproduce. Commit it as a `file` node whose text says
|
|
268
|
+
what was asked, when, and that it is what came back then, never what the data
|
|
269
|
+
is. Keep credentials and personal data out of the store: it outlives the
|
|
270
|
+
session and every later run can read it.
|
|
271
|
+
|
|
272
|
+
**Correct by superseding, never by editing.** Commit the new node and link it to
|
|
273
|
+
the old one with `SUPERSEDES`. Recall then stops serving the wrong answer
|
|
274
|
+
without destroying the record of why it changed.
|
|
275
|
+
|
|
276
|
+
**Ask whether it will still be true next month.** Memory has no expiry and no
|
|
277
|
+
reviewer. A durable fact about this project survives; a fact about this
|
|
278
|
+
afternoon's state is how a graph becomes confidently wrong.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Who exists, and what they may reach for.
|
|
2
2
|
#
|
|
3
|
-
# This implements the Agents, Tools and boundaries, and Environment
|
|
4
|
-
# SPECIFICATION.md. Change what the system does there first, then bring this
|
|
5
|
-
# file to match it — `/sync-
|
|
3
|
+
# This implements the Models, Agents, Tools and boundaries, and Environment
|
|
4
|
+
# sections of SPECIFICATION.md. Change what the system does there first, then bring this
|
|
5
|
+
# file to match it — `/spec-sync-project` in your editor does exactly that.
|
|
6
6
|
#
|
|
7
7
|
version: 1
|
|
8
8
|
|
|
@@ -29,12 +29,21 @@ sandbox:
|
|
|
29
29
|
build:
|
|
30
30
|
dockerfile: sandbox/Dockerfile
|
|
31
31
|
|
|
32
|
+
{{memory}}
|
|
33
|
+
|
|
32
34
|
agents:
|
|
33
35
|
- name: default
|
|
34
36
|
description: The entry point.
|
|
35
37
|
# Instructions live in agents/prompts/<name>.md and are picked up by
|
|
36
38
|
# convention — no need to name the file here.
|
|
37
39
|
#
|
|
40
|
+
# The memory store above is opened for this agent: it can search, load
|
|
41
|
+
# and commit, and it recalls on its own before answering something new.
|
|
42
|
+
# The block form narrows that — `memory:` with `access: read` lets it use
|
|
43
|
+
# what is there without adding to it. Drop the line and it never sees the
|
|
44
|
+
# store at all.
|
|
45
|
+
memory: true
|
|
46
|
+
#
|
|
38
47
|
# workspace:* is every file tool at once, sandbox:* is the shell. Name
|
|
39
48
|
# them one by one to be narrower, or subtract: [workspace:*, -delete_file]
|
|
40
49
|
#
|