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`);