@vib795/agent-memory 0.6.4 → 0.7.0
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 +34 -14
- package/package.json +1 -1
- package/skills/handoff/SKILL.md +18 -3
- package/skills/recall/SKILL.md +5 -0
- package/skills/remember/SKILL.md +26 -0
- package/src/cli.js +94 -4
- package/src/compact.js +38 -5
- package/src/config.js +1 -0
- package/src/digest.js +276 -1
- package/src/setup.js +23 -8
package/README.md
CHANGED
|
@@ -349,11 +349,12 @@ lives under `~/.agents/memory`, nothing is written into the projects you point i
|
|
|
349
349
|
at, and there is no per-repo setup step. `cd` between projects freely: the store
|
|
350
350
|
does not move, split, or reset.
|
|
351
351
|
|
|
352
|
-
One
|
|
353
|
-
clone, `npm install -g .` symlinks rather than copies, so
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
352
|
+
One note, and it is about this repository rather than yours: if you installed from a
|
|
353
|
+
clone, `npm install -g .` symlinks rather than copies, so the skill links resolve back
|
|
354
|
+
into your working tree. `compact` detects that and refuses to write there — the files
|
|
355
|
+
are tracked, and one developer's note count committed and published is exactly what
|
|
356
|
+
happened for twenty releases. It reports them as skipped instead. No project repository
|
|
357
|
+
is ever written to.
|
|
357
358
|
|
|
358
359
|
What the working directory changes is *scope*, never location.
|
|
359
360
|
|
|
@@ -366,6 +367,7 @@ What the working directory changes is *scope*, never location.
|
|
|
366
367
|
| `search` | whole store | nothing — full text hits every note in every repo |
|
|
367
368
|
| `get` | whole store | the staleness line only; the note is found by id either way |
|
|
368
369
|
| `tree` | whole store | **filters it** — defaults to the current repo |
|
|
370
|
+
| `brief` | whole store | **filters it** — the same scoping as `tree` |
|
|
369
371
|
| `write` | whole store | **is stamped into the note** — see below |
|
|
370
372
|
|
|
371
373
|
The current repo is `git rev-parse --show-toplevel` reduced to its directory name.
|
|
@@ -403,6 +405,7 @@ Only `init` is a once-per-machine command, and `agent-memory setup` already ran
|
|
|
403
405
|
| `init` | once, via `setup`. Again only to register extra skill paths |
|
|
404
406
|
| `write` | every capture |
|
|
405
407
|
| `tree`, `get`, `search` | every lookup |
|
|
408
|
+
| `brief` | every capture, before `write` — what is already known here |
|
|
406
409
|
| `index` | repair only — `write` reindexes on every call. Run it after hand-editing or deleting notes, or after deleting `index.db` |
|
|
407
410
|
| `compact` | occasionally. Nothing schedules it: no daemon, no cron, no hook |
|
|
408
411
|
| `doctor` | after install, after an upgrade, and whenever something looks wrong |
|
|
@@ -427,15 +430,31 @@ GitHub, and it is the path to use behind a proxy that blocks or quarantines npm:
|
|
|
427
430
|
|
|
428
431
|
```bash
|
|
429
432
|
git clone https://github.com/vib795/agent-memory.git
|
|
430
|
-
|
|
433
|
+
cd agent-memory && npm pack
|
|
434
|
+
npm install -g ./vib795-agent-memory-*.tgz
|
|
431
435
|
agent-memory setup
|
|
432
436
|
```
|
|
433
437
|
|
|
434
|
-
**
|
|
438
|
+
**Pack first; do not install the directory.** `npm install -g ./agent-memory` looks
|
|
439
|
+
equivalent and is not: npm links the global install to that folder rather than copying
|
|
440
|
+
it, which shows up as an arrow in `npm list -g`:
|
|
441
|
+
|
|
442
|
+
```
|
|
443
|
+
`-- @vib795/agent-memory@0.6.5 -> .\..\..\..\agent-memory
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Move or delete the clone afterwards and the global install points at nothing — the same
|
|
447
|
+
breakage as the git-URL case below, arriving later and harder to trace. Installing a
|
|
448
|
+
packed tarball copies, so the clone becomes disposable. Verified on npm 11.x.
|
|
449
|
+
|
|
450
|
+
**Do not install from the git URL directly either.** `npm install -g <git-url>` does not
|
|
435
451
|
work for this package: npm resolves a git install through
|
|
436
452
|
`~/.npm/_cacache/tmp/git-clone*` and then removes that directory, leaving the global
|
|
437
|
-
install pointing at a path that no longer exists.
|
|
438
|
-
|
|
453
|
+
install pointing at a path that no longer exists. Verified on npm 11.18.
|
|
454
|
+
|
|
455
|
+
**And run the installed binary, not the checkout.** Skill links resolve relative to the
|
|
456
|
+
code that creates them, so `npm run setup` inside a clone aims every link at that clone.
|
|
457
|
+
Use `agent-memory setup`, which runs the copy npm installed.
|
|
439
458
|
|
|
440
459
|
Every release is mirrored to **GitHub Packages**. Treat that as redundancy rather
|
|
441
460
|
than a second front door: GitHub Packages requires authentication even for public
|
|
@@ -534,14 +553,15 @@ powershell -ExecutionPolicy Bypass -File .\install.ps1 # Windows
|
|
|
534
553
|
Both are thin wrappers over `agent-memory setup`; the linking logic lives in
|
|
535
554
|
`src/setup.js` so there is one implementation rather than three that drift.
|
|
536
555
|
|
|
537
|
-
Note that `npm install -g .` from a clone *symlinks* rather than copies, so
|
|
538
|
-
|
|
539
|
-
`skills/recall/SKILL.md`
|
|
540
|
-
|
|
556
|
+
Note that `npm install -g .` from a clone *symlinks* rather than copies, so the skill
|
|
557
|
+
links point back into your working tree. `compact` will not regenerate a description
|
|
558
|
+
there — `skills/recall/SKILL.md` and `skills/remember/SKILL.md` are tracked files, and
|
|
559
|
+
their committed descriptions are deliberately generic placeholders. `compact` prints
|
|
560
|
+
them as skipped, which is the intended outcome, not a failure.
|
|
541
561
|
|
|
542
562
|
Needs Node 22.5 or newer; `doctor` says so plainly if the version is too old.
|
|
543
563
|
|
|
544
|
-
Run `npm test` for the suite (
|
|
564
|
+
Run `npm test` for the suite (105 tests, no dependencies). CI runs it on Linux,
|
|
545
565
|
macOS and Windows across Node 22 and 24, and separately installs the packed tarball
|
|
546
566
|
and exercises it end to end on all three.
|
|
547
567
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vib795/agent-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Durable cross-repo knowledge graph for GitHub Copilot and Claude Code. Markdown source of truth, disposable SQLite index, zero runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-copilot",
|
package/skills/handoff/SKILL.md
CHANGED
|
@@ -129,6 +129,12 @@ repos:
|
|
|
129
129
|
agent: copilot | claude-code
|
|
130
130
|
---
|
|
131
131
|
|
|
132
|
+
> **If the repository you are reading this in is not listed under `repos:` above, you are
|
|
133
|
+
> replicating this work, not continuing it.** Follow Execution protocol, re-derive every
|
|
134
|
+
> path, branch name and version from the repository you are actually in, and read Next
|
|
135
|
+
> action as a record of what happened elsewhere rather than as an instruction. Do not open
|
|
136
|
+
> the repository this was written in.
|
|
137
|
+
|
|
132
138
|
## Orientation
|
|
133
139
|
|
|
134
140
|
3 to 5 sentences. What this thread is trying to accomplish and where it stands.
|
|
@@ -193,7 +199,9 @@ These separate a useful handoff from a readable paragraph that still leaves ques
|
|
|
193
199
|
3. Never quote or paraphrase the transcript. Record conclusions, not the path to them.
|
|
194
200
|
4. Anchor claims to a file path or a decision number. "We refactored the service
|
|
195
201
|
layer" is a failure. "`src/services/order.ts:42` now returns `Result<T>` instead
|
|
196
|
-
of throwing" is not.
|
|
202
|
+
of throwing" is not. When the thread spans more than one repository, name the repo
|
|
203
|
+
alongside the path: an unqualified path in a two-repo thread is a path the reader
|
|
204
|
+
goes looking for in the wrong tree, and finding it there is worse than not finding it.
|
|
197
205
|
5. Record only what the conversation actually established. Prefix anything you
|
|
198
206
|
inferred with `inferred:` so the next agent knows to verify it.
|
|
199
207
|
6. Never inline a diff or a patch. List changed files with one line each.
|
|
@@ -296,8 +304,15 @@ request, which is the only reason this step belongs here rather than in its own
|
|
|
296
304
|
- Zero durable knowledge is a valid outcome. Writing nothing beats writing noise.
|
|
297
305
|
<!-- extraction-rules:end -->
|
|
298
306
|
|
|
299
|
-
Your Decisions table
|
|
300
|
-
|
|
307
|
+
Your Decisions table, Constraints section and **Execution protocol** are usually already
|
|
308
|
+
the durable part. Write the protocol as a `convention`: it is the section a second
|
|
309
|
+
repository actually needs, and the one most easily lost, because a sequence of steps
|
|
310
|
+
reads like status even when it describes how every run of this kind is done. A handoff
|
|
311
|
+
that records the protocol while the graph does not still leaves the next repository
|
|
312
|
+
guessing — the handoff is read once, by whoever was handed the path, and the graph is
|
|
313
|
+
what `/recall` reaches for afterwards.
|
|
314
|
+
|
|
315
|
+
The Current task state section never is durable.
|
|
301
316
|
|
|
302
317
|
### Write it (ONE terminal call)
|
|
303
318
|
|
package/skills/recall/SKILL.md
CHANGED
|
@@ -62,6 +62,11 @@ and what a regex does badly.
|
|
|
62
62
|
- Pick 1 to 3 ids. More than 3 means the question is really several questions.
|
|
63
63
|
- Always include a `constraint` that touches the subject, even when the user did not
|
|
64
64
|
ask about limits. Constraints are what stop an approach that cannot ship.
|
|
65
|
+
- When the question is about **doing** the work rather than understanding it, also
|
|
66
|
+
include the `convention` that governs how that kind of work is executed. A constraint
|
|
67
|
+
tells you which steps are forbidden; only a procedure tells you what order the allowed
|
|
68
|
+
ones go in. Branch choreography and deploy ordering live here, and they are what a
|
|
69
|
+
second repository gets wrong when nobody surfaces them.
|
|
65
70
|
- Nothing in the tree looks relevant → go to Step 4.
|
|
66
71
|
|
|
67
72
|
---
|
package/skills/remember/SKILL.md
CHANGED
|
@@ -79,6 +79,32 @@ most turns.
|
|
|
79
79
|
|
|
80
80
|
---
|
|
81
81
|
|
|
82
|
+
## Step 0 — Read what is already known (ONE terminal call)
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
agent-memory brief
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Read-only, safe on every shell including PowerShell, and it costs no request of its
|
|
89
|
+
own — it rides inside the turn you are already answering.
|
|
90
|
+
|
|
91
|
+
It answers three things you would otherwise guess at:
|
|
92
|
+
|
|
93
|
+
- **What is already here.** Deduplication is by exact content, so the same claim in
|
|
94
|
+
different words becomes a second node. If the brief already lists it, either say
|
|
95
|
+
nothing or update that note by its id.
|
|
96
|
+
- **Which ids are real.** An `edges[].dst` or `supersedes` pointing at an id you
|
|
97
|
+
invented is accepted and then silently never connects. Take targets from the brief.
|
|
98
|
+
- **Which types are empty.** A store with no `constraint` has not recorded what the
|
|
99
|
+
environment forbids, which is the type that saves a future session a wasted retry.
|
|
100
|
+
|
|
101
|
+
If the brief lists an id under "already covered", that juncture was captured minutes
|
|
102
|
+
ago. Do not capture it again.
|
|
103
|
+
|
|
104
|
+
Skip this step only when the user named exactly what to write and it is plainly new.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
82
108
|
## Step 1 — Select what is durable
|
|
83
109
|
|
|
84
110
|
<!-- extraction-rules:start -->
|
package/src/cli.js
CHANGED
|
@@ -10,12 +10,15 @@ import {
|
|
|
10
10
|
openDb, reindex, searchNodes, getNodeRow, markAccessed, nodeCount, hasFts,
|
|
11
11
|
} from './index-db.js';
|
|
12
12
|
import { neighborhood, applyBudget } from './graph.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
buildTree, renderTree, buildDigest, buildCaptureNudge, buildBrief, renderBrief,
|
|
15
|
+
} from './digest.js';
|
|
14
16
|
import { compact, maybeCompact } from './compact.js';
|
|
15
17
|
import { staleness, currentRepo, reviewCandidates, captureGap } from './staleness.js';
|
|
16
18
|
import { setup as runSetup, unlinkSkills, danglingSkillLinks, SKILLS } from './setup.js';
|
|
17
19
|
import { detectTargets, installableTargets } from './targets.js';
|
|
18
|
-
import { join, dirname } from 'node:path';
|
|
20
|
+
import { join, dirname, resolve, sep } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
19
22
|
import { atomicWrite } from './atomic.js';
|
|
20
23
|
import { redactNodeForExport, buildReceipt, renderReceipt } from './pii.js';
|
|
21
24
|
|
|
@@ -29,6 +32,41 @@ import { redactNodeForExport, buildReceipt, renderReceipt } from './pii.js';
|
|
|
29
32
|
|
|
30
33
|
const MIN_NODE = [22, 5];
|
|
31
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Where this process is actually running from, and what version it is.
|
|
37
|
+
*
|
|
38
|
+
* "What am I running" is the first question in every install problem and used to need
|
|
39
|
+
* `npm list -g` to answer, which reports what npm believes rather than what is on PATH.
|
|
40
|
+
* These read the package next to the running code, so they answer for the copy that
|
|
41
|
+
* will actually execute.
|
|
42
|
+
*/
|
|
43
|
+
function packageRoot() {
|
|
44
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function installedVersion() {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(readFileSync(join(packageRoot(), 'package.json'), 'utf8')).version;
|
|
50
|
+
} catch {
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether the running code lives outside any `node_modules` tree.
|
|
57
|
+
*
|
|
58
|
+
* `npm install -g <folder>` links rather than copies, so a global install can be a
|
|
59
|
+
* pointer at a checkout the user will eventually tidy away — and skill links, which
|
|
60
|
+
* resolve relative to this file, follow it there. Node resolves symlinks before it sets
|
|
61
|
+
* `import.meta.url`, so the link itself is already invisible from in here; what stays
|
|
62
|
+
* visible, and is the thing that actually matters, is that the code is not sitting in an
|
|
63
|
+
* installed package. Running from a working copy is legitimate, so this reports the
|
|
64
|
+
* condition rather than failing on it.
|
|
65
|
+
*/
|
|
66
|
+
function runningFromWorkingCopy() {
|
|
67
|
+
return !packageRoot().split(sep).includes('node_modules');
|
|
68
|
+
}
|
|
69
|
+
|
|
32
70
|
function parseArgs(argv) {
|
|
33
71
|
const opts = { _: [] };
|
|
34
72
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -85,6 +123,7 @@ const USAGE = `agent-memory — durable cross-repo knowledge for coding agents
|
|
|
85
123
|
search <terms> [--limit N] full-text fallback when the tree misses
|
|
86
124
|
write --from-json <file> validated upsert; used by the skills
|
|
87
125
|
[--source <name>] [--repo <name>]
|
|
126
|
+
brief [--repo <name>] what is already known here, before capturing
|
|
88
127
|
compact dedup, decay, reindex, regenerate
|
|
89
128
|
doctor preflight and health report
|
|
90
129
|
export [--scope global|repo|all] knowledge worth carrying to another machine
|
|
@@ -93,6 +132,8 @@ const USAGE = `agent-memory — durable cross-repo knowledge for coding agents
|
|
|
93
132
|
engagement [show|list|use <name>] which client store this window writes to
|
|
94
133
|
[purge <name> --yes] delete one engagement's store entirely
|
|
95
134
|
|
|
135
|
+
--version this version, and the path it runs from
|
|
136
|
+
|
|
96
137
|
Add --json to any command for machine-readable output.
|
|
97
138
|
Engagement: ${ENGAGEMENT.name} (${ENGAGEMENT.source})
|
|
98
139
|
Store: ${paths.root}`;
|
|
@@ -179,7 +220,7 @@ function cmdSetup() {
|
|
|
179
220
|
if (r.compactError) {
|
|
180
221
|
lines.push(
|
|
181
222
|
'',
|
|
182
|
-
` Skills are installed, but refreshing the /recall
|
|
223
|
+
` Skills are installed, but refreshing the /recall and /remember descriptions failed: ${r.compactError}`,
|
|
183
224
|
' Run `agent-memory index` then `agent-memory compact` to retry just that step.',
|
|
184
225
|
);
|
|
185
226
|
}
|
|
@@ -248,6 +289,23 @@ function cmdTree(opts) {
|
|
|
248
289
|
return { ok: true, ...result, gap, text: renderTree({ ...result, gap }) };
|
|
249
290
|
}
|
|
250
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Tier 2 of the capture pipeline: what the store already knows, before writing to it.
|
|
294
|
+
*
|
|
295
|
+
* The counterpart to \`tree\`. Same scoping, same budget, opposite reader: \`tree\` tells an
|
|
296
|
+
* agent which note answers a question, this tells it which note it is about to write
|
|
297
|
+
* twice. One call, in a turn already paid for.
|
|
298
|
+
*/
|
|
299
|
+
function cmdBrief(opts) {
|
|
300
|
+
const cfg = loadConfig();
|
|
301
|
+
const db = openDb();
|
|
302
|
+
// \`--repo\` with no value means every repo, matching tree. Anything else names one.
|
|
303
|
+
const repo = opts.repo === true ? null : (opts.repo ?? currentRepo());
|
|
304
|
+
const result = buildBrief(db, { repo, cfg });
|
|
305
|
+
db.close();
|
|
306
|
+
return { ok: true, ...result, text: renderBrief(result) };
|
|
307
|
+
}
|
|
308
|
+
|
|
251
309
|
function cmdGet(opts) {
|
|
252
310
|
const cfg = loadConfig();
|
|
253
311
|
const id = opts._[0];
|
|
@@ -443,6 +501,7 @@ function cmdCompact() {
|
|
|
443
501
|
...r.decayed.map((d) => `archived ${d.id}, last seen ${d.lastSeen}`),
|
|
444
502
|
...r.malformed.map((m) => `warning: unparseable ${m.path}`),
|
|
445
503
|
`digest ${r.digestChars} chars`,
|
|
504
|
+
`capture nudge ${r.nudgeChars} chars`,
|
|
446
505
|
...r.skills.map((s) => `updated description in ${s}`),
|
|
447
506
|
...(r.skipped || []).map(
|
|
448
507
|
(s) => `skipped ${s}: inside this package's git checkout, so the file is tracked`
|
|
@@ -460,6 +519,25 @@ function cmdDoctor() {
|
|
|
460
519
|
// reading it against the wrong client is the mistake this is here to prevent.
|
|
461
520
|
add('engagement', true, `${ENGAGEMENT.name} (${ENGAGEMENT.source})`);
|
|
462
521
|
|
|
522
|
+
// Second, because "which version is this" preceded every other question in the one
|
|
523
|
+
// install failure this tool has actually been debugged through, and answering it
|
|
524
|
+
// needed a separate npm command that reports what npm believes rather than what ran.
|
|
525
|
+
add('version', true, `${installedVersion()} at ${packageRoot()}`);
|
|
526
|
+
|
|
527
|
+
// Skill links point at whatever copy of the code creates them. When that copy is a
|
|
528
|
+
// working directory rather than an installed package, deleting the directory dangles
|
|
529
|
+
// every link at once — which is exactly how this tool's own skill links were lost.
|
|
530
|
+
// Reported, not failed: running from a checkout is a normal thing to do deliberately.
|
|
531
|
+
add(
|
|
532
|
+
'runs from an installed package',
|
|
533
|
+
true,
|
|
534
|
+
runningFromWorkingCopy()
|
|
535
|
+
? `no — working copy at ${packageRoot()}; skill links will point here, so moving or ` +
|
|
536
|
+
'deleting it breaks them. For a durable install: `npm pack` then ' +
|
|
537
|
+
'`npm install -g <tgz>`, and re-run setup.'
|
|
538
|
+
: 'yes',
|
|
539
|
+
);
|
|
540
|
+
|
|
463
541
|
add('node version', nodeVersionOk(), `${process.versions.node} (need >= ${MIN_NODE.join('.')})`);
|
|
464
542
|
if (!nodeVersionOk()) {
|
|
465
543
|
return {
|
|
@@ -496,6 +574,11 @@ function cmdDoctor() {
|
|
|
496
574
|
const digest = buildDigest(db, { cfg });
|
|
497
575
|
add('digest within cap', digest.length <= cfg.digestChars, `${digest.length}/${cfg.digestChars} chars`);
|
|
498
576
|
|
|
577
|
+
// The nudge shares the cap and the silent fallback: over it, Tier 1 quietly becomes
|
|
578
|
+
// the generic string, which reads exactly like a store with nothing to report.
|
|
579
|
+
const nudge = buildCaptureNudge(db, { cfg });
|
|
580
|
+
add('capture nudge within cap', nudge.length <= cfg.digestChars, `${nudge.length}/${cfg.digestChars} chars`);
|
|
581
|
+
|
|
499
582
|
const registered = cfg.skillPaths || [];
|
|
500
583
|
const missing = registered.filter((p) => !existsSync(p));
|
|
501
584
|
add(
|
|
@@ -583,7 +666,7 @@ function cmdDoctor() {
|
|
|
583
666
|
// Staleness is a report, not a failure. Being told about it is the whole feature.
|
|
584
667
|
const advisory = new Set(['staleness', 'capture gap', 'skills linked']);
|
|
585
668
|
const fatal = checks.filter((c) => !c.ok && !advisory.has(c.name));
|
|
586
|
-
return { ok: fatal.length === 0, checks, stale, gap, digest, text };
|
|
669
|
+
return { ok: fatal.length === 0, checks, stale, gap, digest, nudge, text };
|
|
587
670
|
}
|
|
588
671
|
|
|
589
672
|
// --- dispatch ---------------------------------------------------------------
|
|
@@ -917,6 +1000,7 @@ const COMMANDS = {
|
|
|
917
1000
|
init: cmdInit,
|
|
918
1001
|
index: cmdIndex,
|
|
919
1002
|
tree: cmdTree,
|
|
1003
|
+
brief: cmdBrief,
|
|
920
1004
|
get: cmdGet,
|
|
921
1005
|
search: cmdSearch,
|
|
922
1006
|
write: cmdWrite,
|
|
@@ -933,6 +1017,12 @@ function main(argv) {
|
|
|
933
1017
|
process.stdout.write(`${USAGE}\n`);
|
|
934
1018
|
return 0;
|
|
935
1019
|
}
|
|
1020
|
+
if (cmd === '--version' || cmd === '-v' || cmd === 'version') {
|
|
1021
|
+
// Prints the path as well as the number. A version alone cannot tell you that the
|
|
1022
|
+
// binary on PATH belongs to a different install than the one you just upgraded.
|
|
1023
|
+
process.stdout.write(`${installedVersion()}\n${packageRoot()}\n`);
|
|
1024
|
+
return 0;
|
|
1025
|
+
}
|
|
936
1026
|
const fn = COMMANDS[cmd];
|
|
937
1027
|
if (!fn) {
|
|
938
1028
|
process.stderr.write(`Unknown command ${JSON.stringify(cmd)}.\n\n${USAGE}\n`);
|
package/src/compact.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { join, sep } from 'node:path';
|
|
3
|
+
import { join, sep, basename, dirname } from 'node:path';
|
|
4
4
|
import { loadConfig, paths } from './config.js';
|
|
5
5
|
import { listNotes, archiveNote, contentHash, nowIso, serializeNote } from './store.js';
|
|
6
6
|
import { atomicWrite } from './atomic.js';
|
|
7
7
|
import { openDb, reindex } from './index-db.js';
|
|
8
|
-
import { buildDigest, buildTree, renderTree } from './digest.js';
|
|
8
|
+
import { buildDigest, buildCaptureNudge, buildTree, renderTree } from './digest.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Compaction is pure code. No model is involved, and none should be.
|
|
@@ -203,9 +203,33 @@ export function writeSkillDescription(skillPath, description) {
|
|
|
203
203
|
return true;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Which skill a registered path belongs to.
|
|
208
|
+
*
|
|
209
|
+
* Derived from the path rather than stored beside it, because `setup` is what creates
|
|
210
|
+
* these paths and it only ever creates the two shapes below. Keeping `skillPaths` a
|
|
211
|
+
* flat list of strings means no config migration and no second source of truth about
|
|
212
|
+
* which skill is which — the layout already answers it.
|
|
213
|
+
*/
|
|
214
|
+
export function skillNameFromPath(p) {
|
|
215
|
+
const file = basename(p);
|
|
216
|
+
if (file === 'SKILL.md') return basename(dirname(p));
|
|
217
|
+
const m = file.match(/^(.+)\.prompt\.md$/);
|
|
218
|
+
return m ? m[1] : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Regenerate everything derived: ROUTING.md and each installed skill description.
|
|
223
|
+
*
|
|
224
|
+
* Two descriptions are generated, not one, and which a path receives is decided by the
|
|
225
|
+
* skill it belongs to. `remember` gets the capture nudge; everything else registered
|
|
226
|
+
* gets the digest. Before this, one text was written to every registered path, which is
|
|
227
|
+
* why `setup` could only ever register `recall` — handing `remember` the digest would
|
|
228
|
+
* have replaced a good description with a description of the wrong thing.
|
|
229
|
+
*/
|
|
207
230
|
function regenerate(db, cfg) {
|
|
208
231
|
const digest = buildDigest(db, { cfg });
|
|
232
|
+
const nudge = buildCaptureNudge(db, { cfg });
|
|
209
233
|
const tree = buildTree(db, { all: true, cfg });
|
|
210
234
|
|
|
211
235
|
const routing = [
|
|
@@ -227,9 +251,18 @@ function regenerate(db, cfg) {
|
|
|
227
251
|
skipped.push(p);
|
|
228
252
|
continue;
|
|
229
253
|
}
|
|
230
|
-
|
|
254
|
+
const text = skillNameFromPath(p) === 'remember' ? nudge : digest;
|
|
255
|
+
if (writeSkillDescription(p, text)) skills.push(p);
|
|
231
256
|
}
|
|
232
|
-
return {
|
|
257
|
+
return {
|
|
258
|
+
digest,
|
|
259
|
+
digestChars: digest.length,
|
|
260
|
+
nudge,
|
|
261
|
+
nudgeChars: nudge.length,
|
|
262
|
+
routing: paths.routing,
|
|
263
|
+
skills,
|
|
264
|
+
skipped,
|
|
265
|
+
};
|
|
233
266
|
}
|
|
234
267
|
|
|
235
268
|
/**
|
package/src/config.js
CHANGED
|
@@ -102,6 +102,7 @@ export const DEFAULTS = {
|
|
|
102
102
|
staleReviewCommits: 100, // above this, doctor flags it for review
|
|
103
103
|
captureGapCommits: 50, // repo movement with no capture at all before it is worth saying
|
|
104
104
|
compactThreshold: 10, // node-count delta that triggers an automatic compact
|
|
105
|
+
briefRecentMinutes: 120, // window the capture brief calls already covered
|
|
105
106
|
};
|
|
106
107
|
|
|
107
108
|
// Written by the installer: every SKILL.md whose description compact regenerates.
|
package/src/digest.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { loadConfig } from './config.js';
|
|
1
|
+
import { loadConfig, NOTE_TYPES } from './config.js';
|
|
2
|
+
import { captureGap, currentRepo } from './staleness.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Two-tier routing.
|
|
@@ -12,6 +13,11 @@ import { loadConfig } from './config.js';
|
|
|
12
13
|
*
|
|
13
14
|
* Neither tier costs a premium request. A request is charged per prompt, not per
|
|
14
15
|
* tool call, so both of these ride inside a turn that was already paid for.
|
|
16
|
+
*
|
|
17
|
+
* Tier 1 has two occupants, not one. `recall`'s description advertises what the store
|
|
18
|
+
* knows; `remember`'s advertises what it is missing. Both are the same mechanism — a
|
|
19
|
+
* line of frontmatter that code regenerates and every conversation loads — pointed at
|
|
20
|
+
* opposite halves of the same problem.
|
|
15
21
|
*/
|
|
16
22
|
|
|
17
23
|
// Never dropped from either tier. A constraint is what stops an agent from burning
|
|
@@ -126,6 +132,111 @@ export function buildDigest(db, { cfg = loadConfig() } = {}) {
|
|
|
126
132
|
return out;
|
|
127
133
|
}
|
|
128
134
|
|
|
135
|
+
// The routing clause for `remember`, and the mirror of USE_WHEN. It carries both the
|
|
136
|
+
// asked form and the unasked one, because the unasked one is the entire reason this
|
|
137
|
+
// description is regenerated at all: a memory that only grows when someone remembers
|
|
138
|
+
// to grow it stays thin. Never dropped, for the same reason USE_WHEN is never dropped.
|
|
139
|
+
const CAPTURE_WHEN =
|
|
140
|
+
'Use when the user says remember this, save this or note this down — and invoke it ' +
|
|
141
|
+
'unasked the moment a decision settles, a constraint surfaces, a root cause is ' +
|
|
142
|
+
'found or a convention is agreed.';
|
|
143
|
+
|
|
144
|
+
const NUDGE_GENERIC = 'Capture durable project knowledge into a cross-repo memory graph.';
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Notes that speak for this repository, counted the way `captureGap` scopes them.
|
|
148
|
+
*
|
|
149
|
+
* A node with no repos is global and applies everywhere, so it counts here; a node
|
|
150
|
+
* claiming other repos does not. This mirrors `captureGap`'s filter deliberately —
|
|
151
|
+
* two different answers to "does this note cover me" in one description would be a
|
|
152
|
+
* bug the reader could see.
|
|
153
|
+
*/
|
|
154
|
+
function repoScopedCount(db, repo, type = null) {
|
|
155
|
+
// Bound in SQL order: the type predicate precedes the repo one in the statement.
|
|
156
|
+
return db
|
|
157
|
+
.prepare(`
|
|
158
|
+
SELECT COUNT(*) AS c
|
|
159
|
+
FROM nodes n
|
|
160
|
+
WHERE n.archived = 0 ${type ? 'AND n.type = ?' : ''}
|
|
161
|
+
AND (NOT EXISTS (SELECT 1 FROM node_repos r WHERE r.node_id = n.id)
|
|
162
|
+
OR EXISTS (SELECT 1 FROM node_repos r WHERE r.node_id = n.id AND r.repo = ?))
|
|
163
|
+
`)
|
|
164
|
+
.get(...(type ? [type, repo] : [repo])).c;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Tier 1, second occupant: the `remember` skill description.
|
|
169
|
+
*
|
|
170
|
+
* `captureGap` has known since 0.5 how far a repository has moved with nothing written
|
|
171
|
+
* down in it, and that answer went only to `doctor` — a command a person runs on
|
|
172
|
+
* purpose, which is precisely the person who did not need telling. The signal never
|
|
173
|
+
* reached the one reader who could act on it mid-conversation. This routes it to the
|
|
174
|
+
* surface that is already loaded into every turn.
|
|
175
|
+
*
|
|
176
|
+
* Written as a *state*, never as an instruction. "340 commits since anything was
|
|
177
|
+
* captured here" is a fact the model can weigh against what just happened in the
|
|
178
|
+
* conversation; "remember to capture things" is wallpaper it stops seeing by the third
|
|
179
|
+
* turn. The distinction is the whole design: code supplies the timing signal, the model
|
|
180
|
+
* still decides whether anything durable actually happened.
|
|
181
|
+
*
|
|
182
|
+
* It goes quiet on a covered repository. A description that nags at a store which is
|
|
183
|
+
* already current teaches the reader to discount the line, and then it is worth nothing
|
|
184
|
+
* on the day it has something to say.
|
|
185
|
+
*
|
|
186
|
+
* Scoping caveat, stated because it is visible in the output: the gap is per repository
|
|
187
|
+
* and `compact` runs wherever the user happens to be, so this describes the repo where
|
|
188
|
+
* compaction last ran. That is why every variant names the repo out loud — a reader in
|
|
189
|
+
* a different tree can see the mismatch rather than act on a number that is not theirs.
|
|
190
|
+
* `maybeCompact` re-points it on the next write, so it self-corrects with use.
|
|
191
|
+
*/
|
|
192
|
+
export function buildCaptureNudge(db, { cfg = loadConfig(), cwd = process.cwd(), repo, gap } = {}) {
|
|
193
|
+
const g = gap !== undefined ? gap : captureGap(db, { cwd, cfg, repo });
|
|
194
|
+
|
|
195
|
+
const compose = (head) => {
|
|
196
|
+
const out = head ? `${head} ${CAPTURE_WHEN}` : `${NUDGE_GENERIC} ${CAPTURE_WHEN}`;
|
|
197
|
+
// Nothing here is a list, so there is no elastic middle to shed one item at a
|
|
198
|
+
// time. Over the cap, drop the whole head rather than truncate mid-sentence: a
|
|
199
|
+
// description that stops in the middle of a number reads as corrupted, and the
|
|
200
|
+
// routing clause is the part that must survive either way.
|
|
201
|
+
return out.length > cfg.digestChars ? `${NUDGE_GENERIC} ${CAPTURE_WHEN}` : out;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Outside a repository there is no gap to report and no repo to name. Say what the
|
|
205
|
+
// skill is for and stop, rather than inventing a number.
|
|
206
|
+
if (!g) return compose(null);
|
|
207
|
+
|
|
208
|
+
// Never captured in this repository. `captureGap` counts only notes carrying a
|
|
209
|
+
// `captured_sha`, so this is its answer to "has anyone written anything down here",
|
|
210
|
+
// and the commit branches below stay on that same definition. The broader count is
|
|
211
|
+
// reserved for the covered branches, where the question is how much knowledge applies
|
|
212
|
+
// here rather than how much of it was captured here.
|
|
213
|
+
if (g.notes === 0) {
|
|
214
|
+
// captureGap withholds its note on a repository too young for the absence to mean
|
|
215
|
+
// anything. Stay silent with it: nagging on commit three is how a signal gets
|
|
216
|
+
// discounted long before the day it matters.
|
|
217
|
+
return compose(
|
|
218
|
+
g.note ? `Nothing has ever been captured for ${g.repo} — ${g.commits} commits of history.` : null,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Captured, but the repository has moved a long way since the most recent one.
|
|
223
|
+
if (g.note) return compose(`${g.commits} commits since anything was captured for ${g.repo}.`);
|
|
224
|
+
|
|
225
|
+
// Current on commits, but blind in the type that matters most. `digest` privileges
|
|
226
|
+
// constraints and never drops them from Tier 1; a store holding none has not recorded
|
|
227
|
+
// the thing most likely to save a future session a wasted retry loop.
|
|
228
|
+
const notes = repoScopedCount(db, g.repo);
|
|
229
|
+
if (repoScopedCount(db, g.repo, PRIVILEGED) === 0) {
|
|
230
|
+
return compose(
|
|
231
|
+
`${notes} note${notes === 1 ? '' : 's'} for ${g.repo} and no constraint recorded — ` +
|
|
232
|
+
'what this environment forbids has never been written down.',
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Covered. Report the state plainly and let the routing clause do the rest.
|
|
237
|
+
return compose(`${notes} note${notes === 1 ? '' : 's'} for ${g.repo}, capture is current.`);
|
|
238
|
+
}
|
|
239
|
+
|
|
129
240
|
/**
|
|
130
241
|
* Tier 2: the routing tree, scoped.
|
|
131
242
|
*
|
|
@@ -207,3 +318,167 @@ export function renderTree(result) {
|
|
|
207
318
|
if (result.gap?.note) out.push(result.gap.note);
|
|
208
319
|
return out.join('\n');
|
|
209
320
|
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* What each type is for, shown only where one is missing.
|
|
324
|
+
*
|
|
325
|
+
* Taken from the table in the remember skill so there is one wording, not two that
|
|
326
|
+
* drift. Printed against a zero count it answers the question the count raises: not
|
|
327
|
+
* "you have none of these" but "here is what one would have said".
|
|
328
|
+
*/
|
|
329
|
+
const TYPE_HINT = {
|
|
330
|
+
constraint: 'what the environment or the org forbids',
|
|
331
|
+
decision: 'what was chosen, why, and what was rejected',
|
|
332
|
+
convention: 'how this codebase does something, and the gotcha',
|
|
333
|
+
system: 'how a thing works, anchored to a path:line',
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** Notes per type under the same scoping the tree uses, zeros included. */
|
|
337
|
+
function typeCounts(db, repo) {
|
|
338
|
+
const rows = repo
|
|
339
|
+
? db
|
|
340
|
+
.prepare(`
|
|
341
|
+
SELECT n.type AS type, COUNT(DISTINCT n.id) AS c
|
|
342
|
+
FROM nodes n
|
|
343
|
+
LEFT JOIN node_repos r ON r.node_id = n.id
|
|
344
|
+
WHERE n.archived = 0 AND (n.scope = 'global' OR r.repo = ?)
|
|
345
|
+
GROUP BY n.type
|
|
346
|
+
`)
|
|
347
|
+
.all(repo)
|
|
348
|
+
: db.prepare('SELECT type, COUNT(*) AS c FROM nodes WHERE archived = 0 GROUP BY type').all();
|
|
349
|
+
|
|
350
|
+
// Seeded from NOTE_TYPES so a type with no rows reports 0 rather than going absent.
|
|
351
|
+
// The absent ones are the entire point of this section.
|
|
352
|
+
const counts = new Map(NOTE_TYPES.map((t) => [t, 0]));
|
|
353
|
+
for (const r of rows) counts.set(r.type, r.c);
|
|
354
|
+
return counts;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Ids written recently enough that capturing them again would be a duplicate.
|
|
359
|
+
*
|
|
360
|
+
* The skill already forbids re-capturing a juncture that came up twice in one
|
|
361
|
+
* conversation, and that rule currently depends on the model remembering across a
|
|
362
|
+
* long turn. This makes it data instead.
|
|
363
|
+
*
|
|
364
|
+
* Recency is a proxy and is labelled as one: the CLI has no notion of a session, and
|
|
365
|
+
* inventing one would mean tracking state this tool deliberately does not keep. A
|
|
366
|
+
* window wide enough to cover the working session is the honest approximation.
|
|
367
|
+
*/
|
|
368
|
+
function recentlyCaptured(db, repo, cfg, now) {
|
|
369
|
+
// Same format store.js writes, so a lexicographic compare is a chronological one.
|
|
370
|
+
const cutoff = new Date(now - cfg.briefRecentMinutes * 60000)
|
|
371
|
+
.toISOString()
|
|
372
|
+
.replace(/\.\d{3}Z$/, 'Z');
|
|
373
|
+
const rows = repo
|
|
374
|
+
? db
|
|
375
|
+
.prepare(`
|
|
376
|
+
SELECT DISTINCT n.id AS id, n.updated AS updated
|
|
377
|
+
FROM nodes n
|
|
378
|
+
LEFT JOIN node_repos r ON r.node_id = n.id
|
|
379
|
+
WHERE n.archived = 0 AND n.updated >= ? AND (n.scope = 'global' OR r.repo = ?)
|
|
380
|
+
ORDER BY n.updated DESC, n.id
|
|
381
|
+
`)
|
|
382
|
+
.all(cutoff, repo)
|
|
383
|
+
: db
|
|
384
|
+
.prepare(`
|
|
385
|
+
SELECT id, updated FROM nodes
|
|
386
|
+
WHERE archived = 0 AND updated >= ? ORDER BY updated DESC, id
|
|
387
|
+
`)
|
|
388
|
+
.all(cutoff);
|
|
389
|
+
return rows.map((r) => r.id);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Tier 2 of the capture pipeline: what `remember` reads before it composes.
|
|
394
|
+
*
|
|
395
|
+
* The mirror of `buildTree`, scoped for a writer rather than a reader. Without it the
|
|
396
|
+
* skill composes blind, and blind composition has three failure modes this answers
|
|
397
|
+
* directly: it rewords a note that already exists (dedup is by exact content hash, so
|
|
398
|
+
* a paraphrase becomes a second node), it invents an `edges[].dst` id that never
|
|
399
|
+
* connects because a missing dst is legal, and it cannot see which type the store is
|
|
400
|
+
* missing at the one moment it is about to write something.
|
|
401
|
+
*
|
|
402
|
+
* One call, inside a turn that was already paid for. That economy is why the brief is
|
|
403
|
+
* a command the skill runs rather than anything loaded standing — and it matters more
|
|
404
|
+
* on Copilot, where a second round trip is a second premium request.
|
|
405
|
+
*
|
|
406
|
+
* The list is `buildTree`'s, deliberately: two answers to "which notes speak for this
|
|
407
|
+
* repo" in one tool would be a bug the reader could see.
|
|
408
|
+
*/
|
|
409
|
+
export function buildBrief(db, { repo, cwd = process.cwd(), cfg = loadConfig(), gap, now = Date.now() } = {}) {
|
|
410
|
+
const here = repo === undefined ? currentRepo(cwd) : repo;
|
|
411
|
+
const g = gap !== undefined ? gap : here ? captureGap(db, { cwd, cfg, repo: here }) : null;
|
|
412
|
+
const tree = buildTree(db, { repo: here, cfg });
|
|
413
|
+
const counts = typeCounts(db, here);
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
repo: here,
|
|
417
|
+
gap: g,
|
|
418
|
+
tree,
|
|
419
|
+
counts: Object.fromEntries(counts),
|
|
420
|
+
missing: NOTE_TYPES.filter((t) => counts.get(t) === 0),
|
|
421
|
+
recent: recentlyCaptured(db, here, cfg, now),
|
|
422
|
+
recentMinutes: cfg.briefRecentMinutes,
|
|
423
|
+
total: tree.total,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function renderBrief(result) {
|
|
428
|
+
const { repo, gap, tree, counts, missing, recent, recentMinutes } = result;
|
|
429
|
+
const scope = repo ?? 'all repos';
|
|
430
|
+
|
|
431
|
+
// The header carries the same signal the remember description does, at the same
|
|
432
|
+
// moment it is being acted on. A brief that opens with a note count while the repo
|
|
433
|
+
// has moved 300 commits since anyone wrote anything is burying its own headline.
|
|
434
|
+
// Composed from the gap fields rather than reusing \`gap.note\` verbatim: that string
|
|
435
|
+
// names the repo, and the header already has, so borrowing it stutters.
|
|
436
|
+
const state = !repo
|
|
437
|
+
? `${tree.total} notes in the store`
|
|
438
|
+
: gap?.note && gap.notes === 0
|
|
439
|
+
? `nothing captured here yet, over ${gap.commits} commits of history`
|
|
440
|
+
: gap?.note
|
|
441
|
+
? `${gap.commits} commits since anything was captured here`
|
|
442
|
+
: `${tree.total} note${tree.total === 1 ? '' : 's'} here, capture is current`;
|
|
443
|
+
const out = [`# capture brief: ${scope} — ${state}`];
|
|
444
|
+
|
|
445
|
+
if (tree.lines.length) {
|
|
446
|
+
out.push('', 'already known here — write the gap, not these');
|
|
447
|
+
const width = tree.lines.reduce((w, e) => Math.max(w, e.id.length), 0);
|
|
448
|
+
for (const e of tree.lines) {
|
|
449
|
+
out.push(`${e.type.padEnd(10)} ${e.id.padEnd(width)} ${e.title}${e.archived ? ' (archived)' : ''}`);
|
|
450
|
+
}
|
|
451
|
+
// Never silent. A truncated list that reads as the whole store is how a duplicate
|
|
452
|
+
// gets written against a note that was there the entire time.
|
|
453
|
+
if (tree.omitted.length) {
|
|
454
|
+
out.push(`${tree.omitted.length} more not shown, run agent-memory tree --all`);
|
|
455
|
+
}
|
|
456
|
+
out.push('', 'These ids are real. Use them as an `edges[].dst` or `supersedes` target;');
|
|
457
|
+
out.push('an id you invent is accepted and then never connects to anything.');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (missing.length) {
|
|
461
|
+
out.push('', 'nothing captured yet in these types');
|
|
462
|
+
const width = missing.reduce((w, t) => Math.max(w, t.length), 0);
|
|
463
|
+
for (const t of missing) out.push(` ${t.padEnd(width)} ${TYPE_HINT[t]}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const present = Object.entries(counts).filter(([, c]) => c > 0);
|
|
467
|
+
if (present.length) {
|
|
468
|
+
out.push('', `counts: ${present.map(([t, c]) => `${t} ${c}`).join(', ')}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (recent.length) {
|
|
472
|
+
// Written as the reason rather than the rule, because the rule is already in the
|
|
473
|
+
// skill and the model is being asked to apply it, not to learn it again.
|
|
474
|
+
out.push(
|
|
475
|
+
'',
|
|
476
|
+
`captured in the last ${recentMinutes} minutes, so already covered: ${recent.join(', ')}`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!tree.lines.length && !recent.length) {
|
|
481
|
+
out.push('', 'The store holds nothing for this repository yet. Anything durable is new.');
|
|
482
|
+
}
|
|
483
|
+
return out.join('\n');
|
|
484
|
+
}
|
package/src/setup.js
CHANGED
|
@@ -200,14 +200,27 @@ export function unlinkSkills() {
|
|
|
200
200
|
return { removed, kept };
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Skills whose description `compact` regenerates from store state.
|
|
205
|
+
*
|
|
206
|
+
* `recall` advertises what the store knows. `remember` advertises what it is missing,
|
|
207
|
+
* which is the only form of that signal that reaches the model at the moment capture is
|
|
208
|
+
* worth doing — `captureGap` had computed it since 0.5 and sent it only to `doctor`, a
|
|
209
|
+
* command run by the one person who did not need telling.
|
|
210
|
+
*
|
|
211
|
+
* `handoff` is deliberately absent: it describes itself, and there is no store-derived
|
|
212
|
+
* state that would make its description more useful than the one it ships with.
|
|
213
|
+
*/
|
|
214
|
+
export const REGENERATED = ['recall', 'remember'];
|
|
215
|
+
|
|
203
216
|
/**
|
|
204
217
|
* Install into every agent present on this machine, then build the store.
|
|
205
218
|
*
|
|
206
|
-
* Only `
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* original would never reach them.
|
|
219
|
+
* Only the skills in `REGENERATED` are registered, and `compact` now picks the text per
|
|
220
|
+
* skill rather than writing one digest to every path it is given. That is what makes
|
|
221
|
+
* registering a second skill safe; before it, doing so would have replaced a good
|
|
222
|
+
* description with a description of the wrong thing. Copies and prompt files register
|
|
223
|
+
* their own path, because rewriting the packaged original would never reach them.
|
|
211
224
|
*/
|
|
212
225
|
export function setup({ compactFn } = {}) {
|
|
213
226
|
const targets = installableTargets();
|
|
@@ -241,9 +254,11 @@ export function setup({ compactFn } = {}) {
|
|
|
241
254
|
}
|
|
242
255
|
|
|
243
256
|
const skillPaths = [
|
|
244
|
-
join(packagedSkillsDir(),
|
|
245
|
-
...copies.filter((c) => c.name
|
|
246
|
-
...installed
|
|
257
|
+
...REGENERATED.map((name) => join(packagedSkillsDir(), name, 'SKILL.md')),
|
|
258
|
+
...copies.filter((c) => REGENERATED.includes(c.name)).map((c) => join(c.path, 'SKILL.md')),
|
|
259
|
+
...installed
|
|
260
|
+
.filter((i) => i.mode === 'prompt' && REGENERATED.includes(i.name))
|
|
261
|
+
.map((i) => i.path),
|
|
247
262
|
];
|
|
248
263
|
// Drop registrations whose file is gone before adding the current ones. Renaming
|
|
249
264
|
// the checkout, moving it, or reinstalling under a different prefix each leave a
|