From 4575b97af0703fcca40040f75251ec0ed2a846a6 Mon Sep 17 00:00:00 2001 From: Utkarsh Singh <6995377+vib795@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:07:09 -0500 Subject: [PATCH] fix: close the last two seams in the skill content, and stop the docs recommending a broken install (0.6.5) 0.6.3 added an Execution protocol section so a handoff could carry the shape of an operation across repositories. Re-reading the three skills end to end against the failure that prompted it, that fix was only half wired. Two gaps, both on the exact path that failed: 1. Step 7 of /handoff told the agent that Decisions and Constraints were the durable part and said nothing about Execution protocol. So the protocol reached the handoff file and never reached the graph. The handoff is read once, by whoever was handed the path; the graph is what /recall reaches for afterwards. Writing to one and not the other rebuilds the same seam one layer up. 2. /recall's routing rule said to always surface a relevant constraint and never mentioned conventions. A constraint tells you which steps are forbidden. Only a procedure tells you what order the allowed ones go in. A branch protocol stored correctly by /remember could still go unread by /recall in the repository that needed it. Also hardened: the handoff body now carries a standing line under its frontmatter saying that a reader whose repository is not in `repos:` is replicating rather than continuing, and must re-derive paths and branches locally. 0.6.3 put that guidance in the pickup line, which only helps if the right line was pasted. In the file, it holds however the handoff was opened. And quality rule 4 now requires paths to be qualified by repository once a thread spans more than one, because an unqualified path in a two-repo thread is one the reader finds in the wrong tree. The packaging half comes from watching an install go wrong for an hour: - README recommended `npm install -g ./agent-memory` as the path for a registry that quarantines. npm links rather than copies there, so the documented workaround for locked-down environments left a global install pointing at a clone the user would eventually delete -- reproducing the dangling-link failure 0.6.4 exists to fix. It now says to `npm pack` first and install the tarball, and says plainly that the installed binary, not the checkout, is what must run setup. - `agent-memory --version` did not exist. "What am I running" is the first question in any install problem and it needed `npm list -g`, which reports what npm believes rather than what is on PATH. It prints the version and the path it runs from. - doctor reports the version, and whether the running code sits in an installed package or a working copy. The second is reported rather than failed: running from a checkout is a legitimate thing to do on purpose, and a wrong thing to do by accident, and only the user knows which this is. One implementation note worth recording: the first version of that check compared realpathSync against the package root to spot a symlinked install. It could never fire. Node resolves symlinks before setting import.meta.url, so the link is already invisible from inside the process. What remains observable is whether the code sits in a node_modules tree, which is the condition that actually matters. 97 tests, zero runtime and zero dev dependencies. Co-Authored-By: Claude Opus 5 (1M context) --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- README.md | 24 ++++++++++-- package-lock.json | 4 +- package.json | 2 +- skills/handoff/SKILL.md | 21 +++++++++-- skills/recall/SKILL.md | 5 +++ src/cli.js | 65 ++++++++++++++++++++++++++++++++- 8 files changed, 112 insertions(+), 13 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 24cd35a..8bacf4c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "agent-memory", "source": "./", "description": "Durable cross-repository memory for coding agents — built to survive an IT security review. Zero runtime dependencies, zero dev dependencies, no install script: nothing runs when you install it, and granting it your agents is a separate explicit command. Independently scanned, with a passing verdict. Adds /handoff, /remember and /recall over one local markdown store. Requires the CLI: npm install -g @vib795/agent-memory (Node >= 22.5).", - "version": "0.6.4", + "version": "0.6.5", "author": { "name": "Utkarsh Singh", "url": "https://github.com/vib795" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index aa19799..552e0a0 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agent-memory", "displayName": "agent-memory", - "version": "0.6.4", + "version": "0.6.5", "description": "Durable cross-repository memory for coding agents — built to survive an IT security review. Zero runtime dependencies, zero dev dependencies, no install script: nothing runs when you install it, and granting it your agents is a separate explicit command. Independently scanned, with a passing verdict. Adds /handoff, /remember and /recall over one local markdown store. Requires the CLI: npm install -g @vib795/agent-memory (Node >= 22.5).", "author": { "name": "Utkarsh Singh", diff --git a/README.md b/README.md index 0b2796f..04fc10f 100644 --- a/README.md +++ b/README.md @@ -427,15 +427,31 @@ GitHub, and it is the path to use behind a proxy that blocks or quarantines npm: ```bash git clone https://github.com/vib795/agent-memory.git -npm install -g ./agent-memory +cd agent-memory && npm pack +npm install -g ./vib795-agent-memory-*.tgz agent-memory setup ``` -**Do not install from the git URL directly.** `npm install -g ` does not +**Pack first; do not install the directory.** `npm install -g ./agent-memory` looks +equivalent and is not: npm links the global install to that folder rather than copying +it, which shows up as an arrow in `npm list -g`: + +``` +`-- @vib795/agent-memory@0.6.5 -> .\..\..\..\agent-memory +``` + +Move or delete the clone afterwards and the global install points at nothing — the same +breakage as the git-URL case below, arriving later and harder to trace. Installing a +packed tarball copies, so the clone becomes disposable. Verified on npm 11.x. + +**Do not install from the git URL directly either.** `npm install -g ` does not work for this package: npm resolves a git install through `~/.npm/_cacache/tmp/git-clone*` and then removes that directory, leaving the global -install pointing at a path that no longer exists. Cloning first avoids npm's git -handling entirely. Verified on npm 11.18. +install pointing at a path that no longer exists. Verified on npm 11.18. + +**And run the installed binary, not the checkout.** Skill links resolve relative to the +code that creates them, so `npm run setup` inside a clone aims every link at that clone. +Use `agent-memory setup`, which runs the copy npm installed. Every release is mirrored to **GitHub Packages**. Treat that as redundancy rather than a second front door: GitHub Packages requires authentication even for public diff --git a/package-lock.json b/package-lock.json index 7f7d1f2..9090e47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vib795/agent-memory", - "version": "0.6.4", + "version": "0.6.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vib795/agent-memory", - "version": "0.6.4", + "version": "0.6.5", "license": "MIT", "bin": { "agent-memory": "src/cli.js" diff --git a/package.json b/package.json index 038a86a..cd1288a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vib795/agent-memory", - "version": "0.6.4", + "version": "0.6.5", "description": "Durable cross-repo knowledge graph for GitHub Copilot and Claude Code. Markdown source of truth, disposable SQLite index, zero runtime dependencies.", "keywords": [ "github-copilot", diff --git a/skills/handoff/SKILL.md b/skills/handoff/SKILL.md index 61e40af..f7d7a70 100644 --- a/skills/handoff/SKILL.md +++ b/skills/handoff/SKILL.md @@ -129,6 +129,12 @@ repos: agent: copilot | claude-code --- +> **If the repository you are reading this in is not listed under `repos:` above, you are +> replicating this work, not continuing it.** Follow Execution protocol, re-derive every +> path, branch name and version from the repository you are actually in, and read Next +> action as a record of what happened elsewhere rather than as an instruction. Do not open +> the repository this was written in. + ## Orientation 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 3. Never quote or paraphrase the transcript. Record conclusions, not the path to them. 4. Anchor claims to a file path or a decision number. "We refactored the service layer" is a failure. "`src/services/order.ts:42` now returns `Result` instead - of throwing" is not. + of throwing" is not. When the thread spans more than one repository, name the repo + alongside the path: an unqualified path in a two-repo thread is a path the reader + goes looking for in the wrong tree, and finding it there is worse than not finding it. 5. Record only what the conversation actually established. Prefix anything you inferred with `inferred:` so the next agent knows to verify it. 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 - Zero durable knowledge is a valid outcome. Writing nothing beats writing noise. -Your Decisions table and Constraints section are usually already the durable part. -The Current task state section never is. +Your Decisions table, Constraints section and **Execution protocol** are usually already +the durable part. Write the protocol as a `convention`: it is the section a second +repository actually needs, and the one most easily lost, because a sequence of steps +reads like status even when it describes how every run of this kind is done. A handoff +that records the protocol while the graph does not still leaves the next repository +guessing — the handoff is read once, by whoever was handed the path, and the graph is +what `/recall` reaches for afterwards. + +The Current task state section never is durable. ### Write it (ONE terminal call) diff --git a/skills/recall/SKILL.md b/skills/recall/SKILL.md index 6c19a38..6cc35fc 100644 --- a/skills/recall/SKILL.md +++ b/skills/recall/SKILL.md @@ -62,6 +62,11 @@ and what a regex does badly. - Pick 1 to 3 ids. More than 3 means the question is really several questions. - Always include a `constraint` that touches the subject, even when the user did not ask about limits. Constraints are what stop an approach that cannot ship. +- When the question is about **doing** the work rather than understanding it, also + include the `convention` that governs how that kind of work is executed. A constraint + tells you which steps are forbidden; only a procedure tells you what order the allowed + ones go in. Branch choreography and deploy ordering live here, and they are what a + second repository gets wrong when nobody surfaces them. - Nothing in the tree looks relevant → go to Step 4. --- diff --git a/src/cli.js b/src/cli.js index 4e54faa..bb2b2c3 100755 --- a/src/cli.js +++ b/src/cli.js @@ -15,7 +15,8 @@ import { compact, maybeCompact } from './compact.js'; import { staleness, currentRepo, reviewCandidates, captureGap } from './staleness.js'; import { setup as runSetup, unlinkSkills, danglingSkillLinks, SKILLS } from './setup.js'; import { detectTargets, installableTargets } from './targets.js'; -import { join, dirname } from 'node:path'; +import { join, dirname, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { atomicWrite } from './atomic.js'; import { redactNodeForExport, buildReceipt, renderReceipt } from './pii.js'; @@ -29,6 +30,41 @@ import { redactNodeForExport, buildReceipt, renderReceipt } from './pii.js'; const MIN_NODE = [22, 5]; +/** + * Where this process is actually running from, and what version it is. + * + * "What am I running" is the first question in every install problem and used to need + * `npm list -g` to answer, which reports what npm believes rather than what is on PATH. + * These read the package next to the running code, so they answer for the copy that + * will actually execute. + */ +function packageRoot() { + return resolve(dirname(fileURLToPath(import.meta.url)), '..'); +} + +function installedVersion() { + try { + return JSON.parse(readFileSync(join(packageRoot(), 'package.json'), 'utf8')).version; + } catch { + return 'unknown'; + } +} + +/** + * Whether the running code lives outside any `node_modules` tree. + * + * `npm install -g ` links rather than copies, so a global install can be a + * pointer at a checkout the user will eventually tidy away — and skill links, which + * resolve relative to this file, follow it there. Node resolves symlinks before it sets + * `import.meta.url`, so the link itself is already invisible from in here; what stays + * visible, and is the thing that actually matters, is that the code is not sitting in an + * installed package. Running from a working copy is legitimate, so this reports the + * condition rather than failing on it. + */ +function runningFromWorkingCopy() { + return !packageRoot().split(sep).includes('node_modules'); +} + function parseArgs(argv) { const opts = { _: [] }; for (let i = 0; i < argv.length; i++) { @@ -93,6 +129,8 @@ const USAGE = `agent-memory — durable cross-repo knowledge for coding agents engagement [show|list|use ] which client store this window writes to [purge --yes] delete one engagement's store entirely + --version this version, and the path it runs from + Add --json to any command for machine-readable output. Engagement: ${ENGAGEMENT.name} (${ENGAGEMENT.source}) Store: ${paths.root}`; @@ -460,6 +498,25 @@ function cmdDoctor() { // reading it against the wrong client is the mistake this is here to prevent. add('engagement', true, `${ENGAGEMENT.name} (${ENGAGEMENT.source})`); + // Second, because "which version is this" preceded every other question in the one + // install failure this tool has actually been debugged through, and answering it + // needed a separate npm command that reports what npm believes rather than what ran. + add('version', true, `${installedVersion()} at ${packageRoot()}`); + + // Skill links point at whatever copy of the code creates them. When that copy is a + // working directory rather than an installed package, deleting the directory dangles + // every link at once — which is exactly how this tool's own skill links were lost. + // Reported, not failed: running from a checkout is a normal thing to do deliberately. + add( + 'runs from an installed package', + true, + runningFromWorkingCopy() + ? `no — working copy at ${packageRoot()}; skill links will point here, so moving or ` + + 'deleting it breaks them. For a durable install: `npm pack` then ' + + '`npm install -g `, and re-run setup.' + : 'yes', + ); + add('node version', nodeVersionOk(), `${process.versions.node} (need >= ${MIN_NODE.join('.')})`); if (!nodeVersionOk()) { return { @@ -933,6 +990,12 @@ function main(argv) { process.stdout.write(`${USAGE}\n`); return 0; } + if (cmd === '--version' || cmd === '-v' || cmd === 'version') { + // Prints the path as well as the number. A version alone cannot tell you that the + // binary on PATH belongs to a different install than the one you just upgraded. + process.stdout.write(`${installedVersion()}\n${packageRoot()}\n`); + return 0; + } const fn = COMMANDS[cmd]; if (!fn) { process.stderr.write(`Unknown command ${JSON.stringify(cmd)}.\n\n${USAGE}\n`);