@vib795/agent-memory 0.6.2 → 0.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vib795/agent-memory",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",
@@ -55,6 +55,10 @@ Write-Output "--- GIT ---"
55
55
  Write-Output "ROOT=$(git rev-parse --show-toplevel 2>$null)"
56
56
  Write-Output "BRANCH=$(git branch --show-current 2>$null)"
57
57
  Write-Output "HEAD=$(git rev-parse --short HEAD 2>$null)"
58
+ Write-Output "--- BRANCHES ---"
59
+ git branch --sort=-committerdate --format='%(refname:short)' 2>$null | Select-Object -First 10
60
+ Write-Output "--- LOG ---"
61
+ git log --oneline -8 2>$null
58
62
  Write-Output "--- STATUS ---"
59
63
  git status --porcelain 2>$null
60
64
  Write-Output "UTC=$([DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ'))"
@@ -70,6 +74,8 @@ echo "--- GIT ---"
70
74
  echo "ROOT=$(git rev-parse --show-toplevel 2>/dev/null)"
71
75
  echo "BRANCH=$(git branch --show-current 2>/dev/null)"
72
76
  echo "HEAD=$(git rev-parse --short HEAD 2>/dev/null)"
77
+ echo "--- BRANCHES ---"; git branch --sort=-committerdate --format='%(refname:short)' 2>/dev/null | head -10
78
+ echo "--- LOG ---"; git log --oneline -8 2>/dev/null
73
79
  echo "--- STATUS ---"; git status --porcelain 2>/dev/null
74
80
  echo "UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
75
81
  ```
@@ -77,6 +83,12 @@ echo "UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
77
83
  If the directory is not a git repository, `ROOT`/`BRANCH`/`HEAD` come back empty.
78
84
  That is not an error. Record the working directory path and omit the git fields.
79
85
 
86
+ `BRANCHES` is there because the current branch is rarely the whole story. Work that
87
+ stages a change across two branches — one holding the old configuration for a first
88
+ run, another carrying the new one — leaves the second branch invisible to
89
+ `--show-current`, and a branch you were never shown is a branch you cannot record.
90
+ Read the list before writing Execution protocol.
91
+
80
92
  ---
81
93
 
82
94
  ## Step 2 — Identify the thread
@@ -132,6 +144,17 @@ Written for a reader with zero prior context.
132
144
  Things not discoverable by reading the code: environment restrictions,
133
145
  unavailable tooling, plan limits, deadlines, taste calls already settled.
134
146
 
147
+ ## Execution protocol
148
+
149
+ The sequence this work is run in, when the sequence is not obvious from the code.
150
+ Branch choreography, pipeline order, which environment goes first, what has to be
151
+ staged before what, what must be true before a step is safe to repeat.
152
+
153
+ Write the shape of the operation, not this run's position in it. "Run the pipeline
154
+ from a branch holding the old configuration, then from the working branch with the
155
+ new one" belongs here. "Dev is done, UAT is next" does not — that is Current task
156
+ state. Mandatory. `None.` when the work genuinely has no ordering.
157
+
135
158
  ## Rejected approaches
136
159
 
137
160
  What was tried and why it failed. Mandatory. This is the section that stops the
@@ -175,7 +198,8 @@ These separate a useful handoff from a readable paragraph that still leaves ques
175
198
  inferred with `inferred:` so the next agent knows to verify it.
176
199
  6. Never inline a diff or a patch. List changed files with one line each.
177
200
  7. Soft target 150 lines. On overflow, move detail into `<id>.detail.md` and
178
- reference it. Never drop Decisions or Rejected approaches to hit the target.
201
+ reference it. Never drop Decisions, Execution protocol, or Rejected approaches
202
+ to hit the target.
179
203
  8. Empty sections say `None.` They are never deleted. A missing section reads as
180
204
  an oversight; an explicit `None.` reads as a fact.
181
205
 
@@ -252,6 +276,13 @@ request, which is the only reason this step belongs here rather than in its own
252
276
  <!-- extraction-rules:start -->
253
277
  - A node is durable only if it will still be true next month. Task state is not
254
278
  durable and belongs in a handoff file, not in the graph.
279
+ - A repeatable **procedure** is durable even though it reads like task state, and it
280
+ is the exception most often missed. "Run the pipeline from a branch holding the old
281
+ configuration, then from the working branch carrying the new one" is a `convention`:
282
+ it will be true the next time anyone does this. The test is whether the sentence
283
+ describes *this run* — not durable — or *how this kind of run is done* — durable.
284
+ Branch choreography, deploy ordering and staging sequences all pass that test and
285
+ are routinely dropped because the previous rule makes them look like status.
255
286
  - Every `decision` node carries its why and its rejected alternatives, or it is not
256
287
  written. Rationale is the thing that never survives re-explanation.
257
288
  - Anything the conversation did not actually establish is `confidence: inferred`,
@@ -303,7 +334,19 @@ describe the same thing and the user should know.
303
334
 
304
335
  ---
305
336
 
306
- ## Step 8 — Print the pickup line
337
+ ## Step 8 — Print the pickup lines
338
+
339
+ Two different jobs pick up a handoff, and one line cannot serve both.
340
+
341
+ **Continuing** is the same work in another window: same repository, same branch, the
342
+ Next action is the next thing to do.
343
+
344
+ **Replicating** is the same *kind* of work in a different repository. There the Next
345
+ action is wrong by construction — it names branches, paths and environments that
346
+ belong to the repo this file was written in. An agent told to "follow the Next
347
+ action" in a different repo will either execute the wrong step or go read the
348
+ original repo off disk to work out what was meant, and both burn the user's credits
349
+ to arrive somewhere worse than a cold start.
307
350
 
308
351
  End your reply with exactly this, and nothing after it:
309
352
 
@@ -311,21 +354,34 @@ End your reply with exactly this, and nothing after it:
311
354
  Handoff written: <absolute path to id.md>
312
355
  Thread: <id> (<created|updated>)
313
356
 
314
- Paste this in the other window:
357
+ Continue in another window (same repo):
315
358
  Read <absolute path to id.md> and continue this work. Follow the Next action.
359
+
360
+ Replicate in a different repo:
361
+ Read <absolute path to id.md>. It describes work done in <repo name>. Do the
362
+ equivalent here: follow Execution protocol, and re-derive every specific — branch
363
+ names, file paths, module versions — from this repository. Do not open <repo name>
364
+ and do not assume its Next action applies. Tell me the plan before you edit.
316
365
  ```
317
366
 
367
+ Print both. The user knows which window they are pasting into; you do not.
368
+
318
369
  ---
319
370
 
320
371
  ## Self-check before you write
321
372
 
322
- Answer all five. If any is no, fix the file before writing it.
373
+ Answer all six. If any is no, fix the file before writing it.
323
374
 
324
375
  1. Could a fresh agent execute **Next action** from this file alone?
325
- 2. Does every decision state why, and what was rejected?
326
- 3. Is Rejected approaches non-empty, or explicitly `None.`?
327
- 4. Is every claim anchored to a path or a decision number?
328
- 5. Is there a single secret, token, or connection string left in the body?
376
+ 2. Could an agent **in a different repository** replicate this work from this file
377
+ alone, without opening the repo it was written in? If it would have to go read
378
+ the original source to understand the shape of the change, Execution protocol is
379
+ too thin. This is the question that catches a handoff which reads well and is
380
+ still not portable.
381
+ 3. Does every decision state why, and what was rejected?
382
+ 4. Is Rejected approaches non-empty, or explicitly `None.`?
383
+ 5. Is every claim anchored to a path or a decision number?
384
+ 6. Is there a single secret, token, or connection string left in the body?
329
385
 
330
386
  ---
331
387
 
@@ -41,8 +41,14 @@ Invoke this yourself the moment one of these has just happened, in the same turn
41
41
 
42
42
  - a **decision** was settled, and the reasons and rejected options are still in view
43
43
  - a **constraint** surfaced — a blocked tool, a policy, an environment restriction
44
+ - a **tool you reached for was not there** — not on `PATH`, not installed, not
45
+ permitted. A command that failed because the machine does not have it is a
46
+ `constraint`, and an unrecorded one is a tool call you will spend again in every
47
+ future session on that machine
44
48
  - a **root cause** was found, as opposed to a symptom worked around
45
49
  - a **convention** was agreed, or discovered by reading the code
50
+ - a **procedure** ran end to end and worked — the order of the steps is now known,
51
+ and it is about to be forgotten
46
52
 
47
53
  Do not capture on a timer, on a tool count, or at every reply. Those produce volume,
48
54
  and volume is what makes a graph useless — a store full of task chatter is worse than
@@ -78,6 +84,13 @@ most turns.
78
84
  <!-- extraction-rules:start -->
79
85
  - A node is durable only if it will still be true next month. Task state is not
80
86
  durable and belongs in a handoff file, not in the graph.
87
+ - A repeatable **procedure** is durable even though it reads like task state, and it
88
+ is the exception most often missed. "Run the pipeline from a branch holding the old
89
+ configuration, then from the working branch carrying the new one" is a `convention`:
90
+ it will be true the next time anyone does this. The test is whether the sentence
91
+ describes *this run* — not durable — or *how this kind of run is done* — durable.
92
+ Branch choreography, deploy ordering and staging sequences all pass that test and
93
+ are routinely dropped because the previous rule makes them look like status.
81
94
  - Every `decision` node carries its why and its rejected alternatives, or it is not
82
95
  written. Rationale is the thing that never survives re-explanation.
83
96
  - Anything the conversation did not actually establish is `confidence: inferred`,
@@ -97,7 +110,7 @@ Types:
97
110
  |---|---|
98
111
  | `system` | how a thing works, anchored to a `path:line` |
99
112
  | `decision` | what was chosen, why, and what was rejected |
100
- | `convention` | how this codebase does something, and the gotcha |
113
+ | `convention` | how this codebase does something, and the gotcha — including multi-step procedures, branch choreography, and deploy ordering |
101
114
  | `constraint` | what the environment or the org forbids |
102
115
 
103
116
  ---
package/src/cli.js CHANGED
@@ -164,6 +164,26 @@ function cmdSetup() {
164
164
  );
165
165
  }
166
166
 
167
+ // Naming what did not install is the whole point. A run that silently installs
168
+ // eleven of twelve reads as a success and sends the user back to an editor that is
169
+ // still missing a skill.
170
+ if (r.failed.length) {
171
+ lines.push('', ' Failed:');
172
+ for (const f of r.failed) lines.push(` [failed] ${f.path} — ${f.error}`);
173
+ lines.push(
174
+ '',
175
+ ' A path that will not clear is usually a leftover link from an install that has',
176
+ ' since moved. `agent-memory uninstall` reclaims those, then run setup again.',
177
+ );
178
+ }
179
+ if (r.compactError) {
180
+ lines.push(
181
+ '',
182
+ ` Skills are installed, but refreshing the /recall description failed: ${r.compactError}`,
183
+ ' Run `agent-memory index` then `agent-memory compact` to retry just that step.',
184
+ );
185
+ }
186
+
167
187
  return {
168
188
  ok: true,
169
189
  ...r,
package/src/setup.js CHANGED
@@ -55,11 +55,45 @@ function clear(path) {
55
55
  // end stay put — which is exactly what passing `recursive` here would risk.
56
56
  rmdirSync(path);
57
57
  }
58
+ // `force` tells rmSync to swallow ENOENT, and a junction whose target is gone can
59
+ // answer the stat rmSync makes with ENOENT while the reparse point itself stays on
60
+ // disk. The removal then reports success and changes nothing, after which
61
+ // symlinkSync fails EEXIST and the copy fallback lands on a path that still
62
+ // exists. Checking is what turns that silent no-op into an error naming the path.
63
+ if (isLink(path)) rmdirSync(path);
58
64
  } else if (existsSync(path)) {
59
65
  rmSync(path, { recursive: true, force: true });
60
66
  }
61
67
  }
62
68
 
69
+ /**
70
+ * Decide whether a skill link at a path we manage was put there by this tool.
71
+ *
72
+ * Identity cannot be "points at where we live right now". An install that has since
73
+ * moved, or a checkout that was deleted, leaves a link that test would disown — and a
74
+ * disowned link is unreclaimable: `uninstall` keeps it as "not ours" and `setup`
75
+ * cannot replace what it will not clear, so the user is left holding a broken link
76
+ * that no command in this tool will fix.
77
+ */
78
+ function ownsLink(link, name) {
79
+ let dest;
80
+ try {
81
+ dest = resolve(readlinkSync(link));
82
+ } catch {
83
+ // A link sitting at one of our paths whose target cannot even be read is ours to
84
+ // clear. Nothing downstream can use it either.
85
+ return true;
86
+ }
87
+ if (dest === join(packagedSkillsDir(), name)) return true;
88
+ // Dangling. Nothing is lost by reclaiming a link that points nowhere, and leaving it
89
+ // is precisely what deadlocks both commands.
90
+ if (!existsSync(dest)) return true;
91
+ // Live, but pointing into some other agent-memory tree — an older global install, or
92
+ // a checkout the user set up from. The signature is that its parent holds all three
93
+ // of our skills, which a hand-written skill directory would not.
94
+ return SKILLS.every((s) => existsSync(join(dirname(dest), s, 'SKILL.md')));
95
+ }
96
+
63
97
  /**
64
98
  * Link one skill into one agent directory.
65
99
  *
@@ -130,7 +164,6 @@ export function danglingSkillLinks() {
130
164
  * that indexed them; removing them is a separate, deliberate act.
131
165
  */
132
166
  export function unlinkSkills() {
133
- const packaged = packagedSkillsDir();
134
167
  const removed = [];
135
168
  const kept = [];
136
169
 
@@ -141,11 +174,9 @@ export function unlinkSkills() {
141
174
  if (!existsSync(link) && !isLink(link)) continue;
142
175
  let owned = false;
143
176
  try {
144
- owned = isLink(link)
145
- ? resolve(readlinkSync(link)) === join(packaged, name)
146
- : existsSync(join(link, 'SKILL.md'));
177
+ owned = isLink(link) ? ownsLink(link, name) : existsSync(join(link, 'SKILL.md'));
147
178
  } catch {
148
- // A link we cannot read is a link we cannot claim. Leave it.
179
+ // A directory we cannot stat is one we cannot claim. Leave it.
149
180
  owned = false;
150
181
  }
151
182
  if (owned) {
@@ -182,15 +213,30 @@ export function setup({ compactFn } = {}) {
182
213
  const targets = installableTargets();
183
214
  const installed = [];
184
215
  const copies = [];
216
+ const failed = [];
185
217
 
186
218
  for (const target of targets) {
187
219
  for (const name of SKILLS) {
188
- const r =
189
- target.kind === 'skill-dir'
190
- ? linkSkill(name, target.dir)
191
- : writePromptFile(name, target.dir);
192
- installed.push({ ...r, target: target.id, label: target.label });
193
- if (r.mode === 'copy') copies.push(r);
220
+ // Isolated per skill. One unwritable target used to abort the whole run and
221
+ // discard the report with it, so a user whose `.copilot` link was wedged got no
222
+ // output at all and no hint that the other eleven had been fine. A failure here
223
+ // is data to print, not a reason to stop.
224
+ try {
225
+ const r =
226
+ target.kind === 'skill-dir'
227
+ ? linkSkill(name, target.dir)
228
+ : writePromptFile(name, target.dir);
229
+ installed.push({ ...r, target: target.id, label: target.label });
230
+ if (r.mode === 'copy') copies.push(r);
231
+ } catch (err) {
232
+ failed.push({
233
+ name,
234
+ target: target.id,
235
+ label: target.label,
236
+ path: join(target.dir, target.kind === 'skill-dir' ? name : `${name}.prompt.md`),
237
+ error: err.message,
238
+ });
239
+ }
194
240
  }
195
241
  }
196
242
 
@@ -209,11 +255,25 @@ export function setup({ compactFn } = {}) {
209
255
 
210
256
  // compact is passed in so this module does not pull the database into memory just
211
257
  // to make some symlinks.
212
- const result = compactFn ? compactFn() : null;
258
+ //
259
+ // Its failure must not sink the run. Linking is already done and written to disk by
260
+ // this point, so throwing here would throw away an accurate report of work that
261
+ // actually happened and leave the user with no idea any of it succeeded. compact
262
+ // touches every registered skill path, which is exactly the set most likely to hold
263
+ // a stale entry, so it is the step most likely to throw.
264
+ let result = null;
265
+ let compactError = null;
266
+ try {
267
+ result = compactFn ? compactFn() : null;
268
+ } catch (err) {
269
+ compactError = err.message;
270
+ }
213
271
  return {
214
272
  targets,
215
273
  installed,
216
274
  copies,
275
+ failed,
276
+ compactError,
217
277
  skillPaths,
218
278
  home: agentHome(),
219
279
  digest: result?.digest ?? null,