greprag 5.82.1 → 5.83.1

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.
Files changed (43) hide show
  1. package/dist/claude-lifecycle-cutover.js +5 -16
  2. package/dist/commands/codex.js +4 -0
  3. package/dist/commands/collision-check.js +9 -6
  4. package/dist/commands/corpus/client.js +16 -0
  5. package/dist/commands/corpus/index.js +5 -2
  6. package/dist/commands/corpus/section.js +3 -3
  7. package/dist/commands/corpus/walk.js +3 -3
  8. package/dist/commands/delivery-reminder.js +13 -9
  9. package/dist/commands/delivery.js +64 -0
  10. package/dist/commands/docptr.js +10 -24
  11. package/dist/commands/init.js +4 -5
  12. package/dist/commands/load-primer-reminder.js +2 -2
  13. package/dist/commands/load.js +45 -12
  14. package/dist/commands/merge-guard.js +94 -9
  15. package/dist/commands/parity.js +0 -2
  16. package/dist/commands/pipe-wrap.js +4 -2
  17. package/dist/commands/skill-mirror-reminder.js +24 -6
  18. package/dist/commands/skill.js +24 -8
  19. package/dist/commands/skillgain.js +5 -1
  20. package/dist/commands/state-trigger.js +67 -31
  21. package/dist/delivery-config.js +69 -0
  22. package/dist/deploy-locks.js +18 -3
  23. package/dist/doc-mirror-client.js +39 -11
  24. package/dist/hook.js +35 -39
  25. package/dist/index.js +62 -27
  26. package/dist/native-adapter-refresh.js +114 -0
  27. package/dist/native-skill-adoption.js +20 -53
  28. package/dist/native-skill-mirror.js +130 -52
  29. package/dist/native-skill-pending.js +5 -2
  30. package/dist/opencode-plugin.bundle.js +1083 -160
  31. package/dist/opencode-plugin.js +6 -0
  32. package/dist/skill-activation-manifest.js +98 -22
  33. package/dist/skill-landing.js +6 -0
  34. package/dist/skill-mirror-files.js +59 -0
  35. package/package.json +1 -1
  36. package/scripts/postinstall.js +4 -19
  37. package/skill/greprag/SKILL.md +1 -1
  38. package/skill/greprag/docs/corpus.md +2 -2
  39. package/skill/greprag/docs/setup.md +1 -1
  40. package/skill/mechanic/SKILL.md +15 -40
  41. package/skill/mechanic/docs/skill-fix-conventions.md +2 -2
  42. package/skill/templates/chip-spawn.md +36 -20
  43. package/skill/templates/delivery.md +19 -5
@@ -42,20 +42,6 @@ const path = __importStar(require("path"));
42
42
  const native_skill_mirror_1 = require("./native-skill-mirror");
43
43
  const skill_mirror_files_1 = require("./skill-mirror-files");
44
44
  exports.CLAUDE_LIFECYCLE_SKILLS = ['commit', 'deploy', 'release'];
45
- const STATE_FILE = '.greprag-mirror.json';
46
- function readState(root) {
47
- try {
48
- const parsed = JSON.parse(fs.readFileSync(path.join(root, STATE_FILE), 'utf8'));
49
- if (parsed?.version === 1 && parsed.skills && typeof parsed.skills === 'object')
50
- return parsed;
51
- }
52
- catch { /* first targeted cutover */ }
53
- return { version: 1, skills: {} };
54
- }
55
- function writeState(root, state) {
56
- fs.mkdirSync(root, { recursive: true });
57
- fs.writeFileSync(path.join(root, STATE_FILE), JSON.stringify(state, null, 2) + '\n', 'utf8');
58
- }
59
45
  function adapterFor(skill) {
60
46
  const files = (0, native_skill_mirror_1.buildNativeSkillAdapter)('claude-code', skill, { allowClaudeCode: true });
61
47
  if (!files || files.length !== 1 || files[0].path !== 'SKILL.md') {
@@ -88,10 +74,13 @@ function requiredSkills(skills, names) {
88
74
  function applyClaudeLifecycleCutover(params) {
89
75
  const homeDir = params.homeDir || os.homedir();
90
76
  const root = (0, native_skill_mirror_1.nativeSkillRoot)('claude-code', homeDir);
91
- const state = readState(root);
77
+ const state = (0, native_skill_mirror_1.readState)(root);
92
78
  const names = params.names?.length ? params.names : exports.CLAUDE_LIFECYCLE_SKILLS;
93
79
  const prepared = requiredSkills(params.skills, names).map(skill => {
94
80
  const target = path.join(root, skill.skillName);
81
+ if ((0, skill_mirror_files_1.isSkillFolderOwnedElsewhere)(target)) {
82
+ throw new Error(`Claude skill folder is owned elsewhere (a link or another installer's package): ${skill.skillName}; not written.`);
83
+ }
95
84
  const skillMd = path.join(target, 'SKILL.md');
96
85
  const adapterFiles = adapterFor(skill);
97
86
  const nextHash = (0, skill_mirror_files_1.computeSkillFileHash)(adapterFiles);
@@ -138,7 +127,7 @@ function applyClaudeLifecycleCutover(params) {
138
127
  else
139
128
  result.installed++;
140
129
  }
141
- writeState(root, state);
130
+ (0, native_skill_mirror_1.writeState)(root, state);
142
131
  return result;
143
132
  }
144
133
  async function syncClaudeLifecycleCutover(params) {
@@ -472,6 +472,10 @@ function printCodexWatcherRetired() {
472
472
  async function runCodex(args) {
473
473
  const sub = args[0];
474
474
  if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
475
+ if (args[1] === 'chip')
476
+ return (0, command_1.runCodexChip)(['--help', ...args.slice(2)]);
477
+ if (args[1] === 'app-server')
478
+ return (0, codex_app_server_1.runCodexAppServer)(['--help', ...args.slice(2)]);
475
479
  console.log(HELP);
476
480
  return;
477
481
  }
@@ -114,22 +114,25 @@ function dedupePeers(peers, announced) {
114
114
  const union = Array.from(new Set([...announced, ...peers.map(p => p.short)]));
115
115
  return { fresh, union };
116
116
  }
117
+ // adr: adr/delivery-announce-pilot.md
117
118
  /** The LOUD directive — same `additionalContext` shape as the arm directive,
118
- * naming each peer + instructing message-then-worktree. PURE. */
119
+ * naming each peer + instructing message-then-worktree. PURE.
120
+ * It asks no heads-up of Claude Code peers: Claude Code holds a message to a
121
+ * session in another permission mode for that session's user and may let it
122
+ * expire, and the sender cannot see modes, so silence there is not approval.
123
+ * Delivery is the per-harness Delivery announce's alone, so step 3 defers. */
119
124
  function buildCollisionDirective(peers, myShort, alias) {
120
125
  const handle = alias || '<handle>';
121
126
  const lines = peers.map(p => ` · ${p.short}${p.title ? ` — "${p.title}"` : ''}`);
122
127
  const plural = peers.length === 1 ? '' : 's';
123
128
  return (`COLLISION DETECTED — ${peers.length} other live session${plural} working in this same repo right now:\n`
124
129
  + lines.join('\n') + '\n'
125
- + `Coordinate BEFORE touching shared files: (1) message each peer now with a contextual heads-up — `
130
+ + `Coordinate BEFORE touching shared files: (1) message each peer above now with a contextual heads-up — `
126
131
  + `Codex peers: use \`codex_app.list_threads\` to find the task and its repo/workspace, then \`codex_app.send_message_to_thread\` to send the heads-up; `
127
- + `Claude Code peers: use your harness's own session list + send tools — the 8-hex ids above are GREPRAG addresses and are not accepted by them; `
128
132
  + `peers OUTSIDE this harness (opencode / Grok / another tenant): \`greprag send "…what you're about to touch…" --to ${handle}@greprag.com/<their-8hex> --from-session ${myShort}\` `
129
133
  + `(YOU compose and send it — this is a notice, not an auto-send; recipients reply only for a concrete conflict, silence is approval, and you never poll or wait; watcher rows are not the Codex peer source of truth); `
130
- + `(2) do your code work in a worktree on its own branch (\`git worktree add .claude/worktrees/<slug> -b <branch>\`); `
131
- + `(3) at completion resolve \`.greprag/delivery.json\`, send one fire-and-forget delivery notice to PEER sessions only — never to a chip (title \`Chip: \` / \`Chip <Label>: \`), which reports to its own parent and has no business with merge, deploy or master — then continue immediately without polling, waiting, status reads, or follow-ups; `
132
- + `sweep every ready same-repo commit, ignore all uncommitted and untracked work, resolve conflicts directly, and finish the declared push/deploy/release path. Never edit the shared default checkout concurrently.`);
134
+ + `(2) do your code work in a worktree on its own branch (\`git worktree add .claude/worktrees/<slug> -b <branch>\`), never in the shared default checkout; `
135
+ + `(3) at delivery, notify peers only as this harness's Delivery announce says.`);
133
136
  }
134
137
  /** Reduce the watcher list to OTHER live sessions that are NOT in my repo
135
138
  * (cross-repo). Excludes self, same-repo collisions (those get the LOUD
@@ -47,6 +47,8 @@ exports.abortNotConfigured = abortNotConfigured;
47
47
  exports.apiCall = apiCall;
48
48
  exports.apiGet = apiGet;
49
49
  exports.apiDelete = apiDelete;
50
+ exports.formatPassage = formatPassage;
51
+ exports.getMaxChars = getMaxChars;
50
52
  exports.getFlag = getFlag;
51
53
  exports.getFlags = getFlags;
52
54
  exports.extractPositionals = extractPositionals;
@@ -143,6 +145,20 @@ async function apiDelete(url, apiKey, body) {
143
145
  * Listing and search now answer from the same classification.
144
146
  * adr: adr/searchable-store-kinds.md, adr: adr/corpus-tags.md */
145
147
  exports.INTERNAL_KINDS = ['inbox', 'fix', 'docptr', 'skillgain', 'parity', 'doctrine'];
148
+ /** Body of one node for `walk` / `section`, indented under its header line.
149
+ * Full text by default — these commands exist to read the whole passage around
150
+ * a hit (search snippets stay short). `maxChars` is the explicit opt-in cut. */
151
+ function formatPassage(content, maxChars) {
152
+ const text = maxChars && maxChars > 0 && content.length > maxChars
153
+ ? content.slice(0, maxChars) + `... [+${content.length - maxChars} chars]`
154
+ : content;
155
+ return text.split('\n').map((line) => ` ${line}`).join('\n');
156
+ }
157
+ /** `--max-chars N` → N, or undefined (full text). */
158
+ function getMaxChars(args) {
159
+ const raw = getFlag(args, '--max-chars');
160
+ return raw ? parseInt(raw, 10) : undefined;
161
+ }
146
162
  // -- Arg helpers -----------------------------------------------------------
147
163
  function getFlag(args, flag) {
148
164
  const idx = args.indexOf(flag);
@@ -57,10 +57,13 @@ Subcommands:
57
57
  --all = every store under the tenant;
58
58
  --store repeatable; --kind filters to
59
59
  one kind. Hits are labeled by store.
60
- section <store> "<heading>" Every node under a heading (forgiving
60
+ section <store> "<heading>" [--max-chars N]
61
+ Every node under a heading (forgiving
61
62
  match: any depth, dash/case-insensitive).
62
- walk <store> <nodeId> [--before 2 --after 2]
63
+ walk <store> <nodeId> [--before 2 --after 2] [--max-chars N]
63
64
  Show a node and its neighbors.
65
+ section/walk print full node text;
66
+ --max-chars N cuts each node.
64
67
  status <store> Show write-side enrichment progress
65
68
  (per-node vocabulary bridge).
66
69
  refresh <store> Manually re-check a tracked source —
@@ -13,9 +13,10 @@ async function sectionCmd(args) {
13
13
  const storeRef = args[0];
14
14
  const headingPath = args[1];
15
15
  if (!storeRef || !headingPath) {
16
- console.error('Usage: greprag corpus section <storeIdOrName> "<heading>"\n e.g. greprag corpus section "Scientific Advertising" "Chapter 7"\n Forgiving match: any depth, case-/dash-insensitive, leaf prefix.');
16
+ console.error('Usage: greprag corpus section <storeIdOrName> "<heading>" [--max-chars N]\n e.g. greprag corpus section "Scientific Advertising" "Chapter 7"\n Forgiving match: any depth, case-/dash-insensitive, leaf prefix.');
17
17
  process.exit(1);
18
18
  }
19
+ const maxChars = (0, client_1.getMaxChars)(args);
19
20
  const store = await (0, client_1.resolveStore)(storeRef, cfg.apiUrl, cfg.apiKey);
20
21
  const url = `${cfg.apiUrl}/v1/stores/${store.id}/section?path=${encodeURIComponent(headingPath)}`;
21
22
  const res = await (0, client_1.apiGet)(url, cfg.apiKey);
@@ -24,9 +25,8 @@ async function sectionCmd(args) {
24
25
  console.log(`\n${store.name} · section "${headingPath}" — ${res.nodes.length} node${res.nodes.length === 1 ? '' : 's'}\n`);
25
26
  for (const n of res.nodes) {
26
27
  const heading = n.headingPath?.length ? ` · ${n.headingPath.join(' / ')}` : '';
27
- const snippet = n.content.length > 200 ? n.content.slice(0, 197) + '...' : n.content;
28
28
  console.log(` [${n.nodeId}] pos=${n.position}${heading}`);
29
- console.log(` ${snippet.replace(/\n/g, ' ')}\n`);
29
+ console.log(`${(0, client_1.formatPassage)(n.content, maxChars)}\n`);
30
30
  }
31
31
  if (res.nodes.length === 0) {
32
32
  console.log('No nodes under that heading. Check a hit\'s headingPath from `corpus search`, or broaden the heading.');
@@ -13,11 +13,12 @@ async function walkCmd(args) {
13
13
  const storeRef = args[0];
14
14
  const nodeId = args[1];
15
15
  if (!storeRef || !nodeId) {
16
- console.error('Usage: greprag corpus walk <storeIdOrName> <nodeId> [--before 2 --after 2]');
16
+ console.error('Usage: greprag corpus walk <storeIdOrName> <nodeId> [--before 2 --after 2] [--max-chars N]');
17
17
  process.exit(1);
18
18
  }
19
19
  const before = parseInt((0, client_1.getFlag)(args, '--before') || '2', 10);
20
20
  const after = parseInt((0, client_1.getFlag)(args, '--after') || '2', 10);
21
+ const maxChars = (0, client_1.getMaxChars)(args);
21
22
  const store = await (0, client_1.resolveStore)(storeRef, cfg.apiUrl, cfg.apiKey);
22
23
  // Step 1: fetch the anchor node to learn its position.
23
24
  const anchorRes = await (0, client_1.apiGet)(`${cfg.apiUrl}/v1/stores/${store.id}/by-node-id/${nodeId}`, cfg.apiKey);
@@ -33,8 +34,7 @@ async function walkCmd(args) {
33
34
  const isAnchor = n.nodeId === nodeId;
34
35
  const marker = isAnchor ? '★' : ' ';
35
36
  const heading = n.headingPath?.length ? ` · ${n.headingPath.join(' / ')}` : '';
36
- const snippet = n.content.length > 320 ? n.content.slice(0, 317) + '...' : n.content;
37
37
  console.log(` ${marker} [${n.nodeId}] pos=${n.position}${heading}`);
38
- console.log(` ${snippet.replace(/\n/g, ' ')}\n`);
38
+ console.log(`${(0, client_1.formatPassage)(n.content, maxChars)}\n`);
39
39
  }
40
40
  }
@@ -18,18 +18,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.deliveryControlModule = void 0;
19
19
  exports.buildDeliveryAnnounce = buildDeliveryAnnounce;
20
20
  // adr: adr/delivery-announce-pilot.md
21
- /** How THIS harness finds same-repo peers — the one line that legitimately
22
- * differs per platform. Everything else about notifying (once, fire-and-forget,
23
- * chips excluded, why greprag is not the address book) is shared prose that
24
- * belongs in `greprag load delivery`, not in nine duplicated sentences here. */
21
+ /** How THIS harness treats same-repo peers at delivery — the one line that
22
+ * legitimately differs per platform. Everything else about notifying (once,
23
+ * fire-and-forget, chips excluded, why Claude Code sends nothing) is shared
24
+ * prose that belongs in `greprag load delivery`, not duplicated here. */
25
25
  function coordinationLine(platform) {
26
+ // Claude Code holds a message to a session in another permission mode for that
27
+ // session's user and may let it expire, and the sender cannot see the mode. A
28
+ // notice there can silently not arrive, so its silence was never approval.
29
+ if (platform === 'claude-code') {
30
+ return 'Send same-repo peers no delivery notice: Git is the coordination. Never poll, wait on, or ask peer sessions for status.';
31
+ }
26
32
  const how = platform === 'codex'
27
33
  ? 'codex_app.list_threads + codex_app.send_message_to_thread'
28
- : platform === 'claude-code'
29
- ? "this harness's own session-list + send tools (never a greprag id)"
30
- : platform === 'opencode'
31
- ? 'the GrepRAG registry'
32
- : 'your harness\'s own peer tools';
34
+ : platform === 'opencode'
35
+ ? 'the GrepRAG registry'
36
+ : 'your harness\'s own peer tools';
33
37
  // Codex keeps its one extra word: `wait_threads` is the specific tool a Codex
34
38
  // session reaches for when it decides to wait for a reply it was told not to
35
39
  // wait for. A named tool is a rule; "do not wait" alone was not enough.
@@ -1,12 +1,49 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.runDelivery = runDelivery;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
4
39
  const delivery_config_1 = require("../delivery-config");
5
40
  const HELP = `greprag delivery — resolve the current repo's delivery authority
6
41
 
7
42
  Usage:
8
43
  greprag delivery resolve [--json]
9
44
  greprag delivery status [--json]
45
+ greprag delivery init [--write] draft this repo's profile (report only unless --write;
46
+ never overwrites an existing profile)
10
47
 
11
48
  Reads .greprag/delivery.json when present. During migration only, falls back to
12
49
  docs/deploy.md, docs/releases.md, then CLAUDE.md. Provider commands remain in
@@ -17,6 +54,8 @@ function runDelivery(args) {
17
54
  return;
18
55
  }
19
56
  const sub = args[0] || 'resolve';
57
+ if (sub === 'init')
58
+ return runInit(args.includes('--write'));
20
59
  if (sub !== 'resolve' && sub !== 'status') {
21
60
  console.error(`Unknown delivery command: ${sub}\n\n${HELP}`);
22
61
  process.exitCode = 1;
@@ -56,3 +95,28 @@ function runDelivery(args) {
56
95
  if (result.mode === 'missing' || result.mode === 'invalid')
57
96
  process.exitCode = 1;
58
97
  }
98
+ function runInit(write) {
99
+ const draft = (0, delivery_config_1.draftDeliveryProfile)(process.cwd());
100
+ if (!draft) {
101
+ console.error('Not inside a Git repository.');
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+ const target = (0, delivery_config_1.deliveryProfilePath)(draft.root);
106
+ if (fs.existsSync(target)) {
107
+ console.log(`Profile already exists: ${target}\nEdit it by hand; init never overwrites.`);
108
+ return;
109
+ }
110
+ const body = JSON.stringify(draft.profile, null, 2) + '\n';
111
+ console.log(`Draft profile for ${draft.root}:\n${body}`);
112
+ console.log(`Deploy signals: ${draft.providerSignals.join(', ') || 'none (commit-and-merge only)'}`);
113
+ for (const item of draft.todo)
114
+ console.log(` todo: ${item}`);
115
+ if (!write) {
116
+ console.log('\nReport only. Rerun with --write to save it.');
117
+ return;
118
+ }
119
+ fs.mkdirSync(path.dirname(target), { recursive: true });
120
+ fs.writeFileSync(target, body);
121
+ console.log(`\nWrote ${target}. Commit it.`);
122
+ }
@@ -51,9 +51,11 @@ var __importStar = (this && this.__importStar) || (function () {
51
51
  Object.defineProperty(exports, "__esModule", { value: true });
52
52
  exports.runDocPtr = runDocPtr;
53
53
  const fs = __importStar(require("node:fs"));
54
+ const path = __importStar(require("node:path"));
54
55
  const proc_1 = require("../proc");
55
56
  const project_anchor_1 = require("../project-anchor");
56
57
  const docptr_refs_1 = require("../docptr-refs");
58
+ const doc_mirror_client_1 = require("../doc-mirror-client");
57
59
  function getConfig() {
58
60
  return {
59
61
  apiUrl: process.env.GREPRAG_API_URL || 'https://api.greprag.com',
@@ -67,25 +69,6 @@ USAGE
67
69
  Judge every tracked, eligible .md and register the core ones — the
68
70
  one-time catch-up for docs that predate the system or are never edited.
69
71
  Idempotent. --missing-only skips already-registered docs.`;
70
- /** Mirror of hook.ts isDocPathEligible — the deterministic pre-filter (non-.md,
71
- * dot-dirs, build trees, harness-owned CLAUDE.md/MEMORY.md). Duplicated because
72
- * the CLI command files are self-contained (zero cross-file coupling). */
73
- function isDocPathEligible(relPath) {
74
- const p = relPath.replace(/\\/g, '/');
75
- if (!p.toLowerCase().endsWith('.md'))
76
- return false;
77
- const segments = p.split('/');
78
- const base = segments[segments.length - 1];
79
- if (base === 'CLAUDE.md' || base === 'MEMORY.md')
80
- return false;
81
- for (const seg of segments.slice(0, -1)) {
82
- if (seg.startsWith('.'))
83
- return false;
84
- if (seg === 'node_modules' || seg === 'dist' || seg === 'build' || seg === 'vendor')
85
- return false;
86
- }
87
- return true;
88
- }
89
72
  const SNIPPET_CHARS = 2_000;
90
73
  const EVENTS_PER_POST = 8;
91
74
  /** Judging runs server-side in waitUntil (async, after the POST returns), so a
@@ -109,13 +92,16 @@ async function runDocPtr(args) {
109
92
  const missingOnly = args.includes('--missing-only');
110
93
  const cwd = process.cwd();
111
94
  const anchor = (0, project_anchor_1.readAnchor)(cwd);
95
+ // Paths are relative to the checkout root, so a backfill run from a subfolder
96
+ // registers the same paths the hook and the doc mirror use.
97
+ const root = (0, doc_mirror_client_1.docPathRoot)(cwd);
112
98
  // Enumerate tracked, eligible docs.
113
99
  let tracked;
114
100
  try {
115
101
  const out = (0, proc_1.safeExecFileSync)('git', ['ls-files', '--', '*.md'], {
116
- cwd, encoding: 'utf-8',
102
+ cwd: root, encoding: 'utf-8',
117
103
  });
118
- tracked = out.split('\n').map(s => s.trim()).filter(s => s && isDocPathEligible(s));
104
+ tracked = out.split('\n').map(s => s.trim()).filter(s => s && (0, doc_mirror_client_1.isDocMirrorEligible)(s));
119
105
  }
120
106
  catch {
121
107
  console.error('Not a git repo (or git unavailable) — backfill needs `git ls-files`.');
@@ -158,7 +144,7 @@ async function runDocPtr(args) {
158
144
  // manifest. Best-effort: an empty set yields empty manifests (docs read fresh).
159
145
  let present = new Set();
160
146
  try {
161
- const all = (0, proc_1.safeExecFileSync)('git', ['ls-files'], { cwd, encoding: 'utf-8' });
147
+ const all = (0, proc_1.safeExecFileSync)('git', ['ls-files'], { cwd: root, encoding: 'utf-8' });
162
148
  present = new Set(all.split('\n').map(s => s.trim().replace(/\\/g, '/')).filter(Boolean));
163
149
  }
164
150
  catch { /* not a git repo — manifests stay empty */ }
@@ -166,11 +152,11 @@ async function runDocPtr(args) {
166
152
  const events = [];
167
153
  for (const rel of targets) {
168
154
  try {
169
- const snippet = fs.readFileSync(rel, 'utf-8').slice(0, SNIPPET_CHARS);
155
+ const snippet = fs.readFileSync(path.join(root, rel), 'utf-8').slice(0, SNIPPET_CHARS);
170
156
  if (!snippet.trim())
171
157
  continue;
172
158
  const p = rel.replace(/\\/g, '/');
173
- events.push({ path: p, snippet, refs: present.size ? (0, docptr_refs_1.extractDocRefs)(snippet, p, cwd, present) : [] });
159
+ events.push({ path: p, snippet, refs: present.size ? (0, docptr_refs_1.extractDocRefs)(snippet, p, root, present) : [] });
174
160
  }
175
161
  catch { /* skip */ }
176
162
  }
@@ -1629,11 +1629,10 @@ function applySettings(settings, apiKey) {
1629
1629
  }
1630
1630
  }
1631
1631
  // PreToolUse (pre-spawn-check) — fires BEFORE every `mcp__ccd_session__spawn_task`
1632
- // call. Validates the call against three machine-checkable rules from the inline
1633
- // chip-spawning convention in ~/.claude/CLAUDE.md § Chip Spawning: title prefix
1634
- // `Chip: `, Block 1 (`git worktree add` in prompt), Block 2 (`greprag send` in
1635
- // prompt). Returns permissionDecision: "deny" with remediation when any rule
1636
- // fails; the agent re-composes the call.
1632
+ // call. Validates the call against two machine-checkable rules from
1633
+ // `greprag load chip-spawn`: title prefix `Chip: ` and Block 1 (`git branch -m`
1634
+ // and `git worktree add` in prompt). Returns permissionDecision: "deny" with remediation when
1635
+ // either rule fails; the agent re-composes the call.
1637
1636
  const preSpawnCheckHook = {
1638
1637
  matcher: 'mcp__ccd_session__spawn_task',
1639
1638
  hooks: [{ type: 'command', command: 'greprag-hook pre-spawn-check', timeout: 3000 }],
@@ -29,10 +29,10 @@ exports.LOAD_PRIMER = [
29
29
  '• Pointers (below) name a trigger + the entry to load at that moment. Or browse the catalog yourself any time you suspect a method exists.',
30
30
  ].join('\n');
31
31
  /** POINTER — the one standing chip pointer. Thin: trigger + load command. The
32
- * method (Block 1/Block 2) and the multi-chip upgrade both live in the payload. */
32
+ * method (Block 1 + report-back) and the multi-chip upgrade both live in the payload. */
33
33
  exports.CHIP_SPAWN_POINTER = [
34
34
  '[greprag chips — agent coordination is built in (messaging plumbing in the inbox primer).]',
35
- '• About to spawn a chip (`spawn_task`)? → run `greprag load chip-spawn` FIRST for the method (Block 1 worktree + Block 2 report-back); it carries the multi-chip upgrade path inline. Non-negotiable — the spawn validator rejects an unstructured chip.',
35
+ '• About to spawn a chip (`spawn_task`)? → run `greprag load chip-spawn` FIRST for the method (Block 1 worktree + report-back); it carries the multi-chip upgrade path inline. Non-negotiable — the spawn validator rejects an unstructured chip.',
36
36
  ].join('\n');
37
37
  /** POINTER — OpenCode's native chip path (the fix-chip era vehicle: `greprag
38
38
  * opencode chip` HTTP spawn, goals, native child sessions). Supersedes the
@@ -51,6 +51,7 @@ var __importStar = (this && this.__importStar) || (function () {
51
51
  };
52
52
  })();
53
53
  Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.bundleOwnsSkill = bundleOwnsSkill;
54
55
  exports.runLoad = runLoad;
55
56
  exports.normalizeLoadName = normalizeLoadName;
56
57
  const fs = __importStar(require("fs"));
@@ -72,7 +73,7 @@ const LIBRARY = {
72
73
  },
73
74
  'chip-spawn': {
74
75
  files: ['skill/templates/chip-spawn.md'],
75
- purpose: 'How to spawn one chip correctly (Block 1 worktree + Block 2 report-back). Carries the multi-chip upgrade path inline.',
76
+ purpose: 'How to spawn one chip correctly (Block 1 worktree + report-back). Carries the multi-chip upgrade path inline.',
76
77
  },
77
78
  'codex-chip-spawn': {
78
79
  files: ['skill/templates/codex-chip-spawn.md'],
@@ -118,6 +119,13 @@ const LIBRARY = {
118
119
  function payloadPath(file) {
119
120
  return path.join(__dirname, '..', '..', file);
120
121
  }
122
+ /** True when the CLI ships `name` as its own skill bundle (`skill/<name>/`) and
123
+ * serves it from this library. The bundle is that skill's canon, so its global
124
+ * id means the bundle; a mirror row under the same name can only be a copy of
125
+ * an installed bundle, frozen when it was captured. adr: adr/skill-adapter-never-canon.md */
126
+ function bundleOwnsSkill(name) {
127
+ return LIBRARY[name]?.files[0] === `skill/${name}/SKILL.md`;
128
+ }
121
129
  // ---------- Tier 3 — the skill mirror (docs/load-system.md) -------------------
122
130
  const API_URL_DEFAULT = 'https://api.greprag.com';
123
131
  const MIRROR_TIMEOUT_MS = 4_000;
@@ -185,25 +193,28 @@ async function printCatalog() {
185
193
  console.log(` ${'agents'.padEnd(width)} Deprecated alias for instructions.`);
186
194
  // Tier-3 section: the operator's own skills, auto-mirrored from use.
187
195
  const data = await mirrorGet('/v1/skillmirror');
188
- const skills = data?.skills || [];
196
+ // A mirror row under a bundled skill's name is a stale copy; the bundled
197
+ // entry above is that skill.
198
+ const skills = (data?.skills || [])
199
+ .filter((s) => !bundleOwnsSkill(s.skillName || ''));
189
200
  if (skills.length > 0) {
190
201
  // A mirrored skill with no native adapter on this harness never reaches the
191
202
  // harness's own skill list — the agent can only reach it through this
192
203
  // catalog. Mark the gap here and name the command that closes it.
204
+ // A hand-kept copy does reach that list, but the harness runs the copy and
205
+ // never sees canon move on, so it is marked too (with `~`).
193
206
  const platform = (0, harness_1.inferCurrentHarness)() || '';
194
- const installed = platform
207
+ const native = platform
195
208
  ? (0, skill_activation_manifest_1.readNativeSkillNames)({ platform, cwd: process.cwd(), homeDir: os.homedir() })
196
- : new Set();
197
- const missing = platform
198
- ? skills.map((s) => s.skillName || '').filter((n) => n && !installed.has(n.toLowerCase()))
199
- : [];
209
+ : new Map();
210
+ const { missing, localCopies } = (0, skill_activation_manifest_1.nativeAdapterGap)(platform, skills, native);
200
211
  const w = Math.max(...skills.map((s) => (s.skillName || '').length), 8);
201
212
  console.log('\nYour mirrored skills (auto-fresh from use — work on any machine/harness):\n');
202
213
  for (const s of skills) {
203
214
  if (!s.skillName)
204
215
  continue;
205
216
  const scope = s.scope === 'project' && s.projectId ? ` [project:${s.projectId.slice(0, 8)}]` : '';
206
- const gap = missing.includes(s.skillName) ? ' *' : '';
217
+ const gap = missing.includes(s.skillName) ? ' *' : localCopies.includes(s.skillName) ? ' ~' : '';
207
218
  console.log(` ${(s.skillName + scope + gap).padEnd(w + 20)} ${(s.description || '').slice(0, 110)}`);
208
219
  }
209
220
  const installCommand = (0, skill_activation_manifest_1.nativeAdapterInstallCommand)(platform, missing);
@@ -212,6 +223,13 @@ async function printCatalog() {
212
223
  console.log(' in this harness\'s own skill list. Install the adapters with:\n');
213
224
  console.log(` ${installCommand}\n`);
214
225
  }
226
+ const cutoverCommand = (0, skill_activation_manifest_1.localCopyCutoverCommand)(platform, localCopies);
227
+ if (cutoverCommand) {
228
+ console.log(`\n ~ ${localCopies.length} skill(s) run from a hand-kept copy in ${platform}, not an adapter — the harness`);
229
+ console.log(' keeps running that copy when the mirrored skill changes. Replacing a copy is a per-skill');
230
+ console.log(' call; its SKILL.md is backed up first:\n');
231
+ console.log(` ${cutoverCommand}\n`);
232
+ }
215
233
  }
216
234
  console.log('\n greprag load <name> print the full entry / mirrored skill');
217
235
  console.log(' greprag load /<name> print a user-named slash skill');
@@ -375,8 +393,13 @@ async function printStateDocHints(name, skillBody) {
375
393
  const data = await mirrorPost(`/v1/doc/${projectId}/search`, { query, limit: 5 });
376
394
  if (!data)
377
395
  return;
396
+ // This block calls what it lists authoritative, so it lists only what the
397
+ // mirror would accept today. Skill files mirrored before skill trees were
398
+ // excluded, or by an older CLI, stay until a full `greprag doc update` prunes
399
+ // them.
400
+ // adr: adr/doc-mirror-state-only.md
378
401
  const docs = (data.docs || [])
379
- .filter((d) => typeof d.path === 'string' && d.path.trim().length > 0)
402
+ .filter((d) => typeof d.path === 'string' && (0, doc_mirror_client_1.isDocMirrorEligible)(d.path))
380
403
  .slice(0, 5);
381
404
  if (docs.length > 0) {
382
405
  process.stdout.write('\n[STATE DOCS — relevant project state from the current repo mirror]\n');
@@ -484,12 +507,22 @@ async function runLoad(args) {
484
507
  console.error('No canonical GrepRAG Instructions found. Seed them with `greprag instructions adopt <CLAUDE.md|AGENTS.md>`.');
485
508
  process.exit(1);
486
509
  }
487
- const entry = LIBRARY[name];
510
+ // An explicit --skill-id names one mirrored skill, so it outranks a bundled
511
+ // entry that merely shares the name, and a miss is an error rather than a
512
+ // quiet fallback to the bundled payload. A skill the CLI ships as its own
513
+ // bundle is the exception: its global id is the bundle, so every harness's
514
+ // launcher reaches the same canon. adr: adr/skill-adapter-never-canon.md
515
+ const mirroredArgs = parseMirroredLoadArgs(args.slice(1));
516
+ const bundleCanon = mirroredArgs.skillId === `global:${name}` && bundleOwnsSkill(name);
517
+ const entry = mirroredArgs.skillId && !bundleCanon ? undefined : LIBRARY[name];
488
518
  if (!entry) {
489
- // Not bundled → try the Tier-3 skill mirror before giving up.
490
- const mirroredArgs = parseMirroredLoadArgs(args.slice(1));
491
519
  if (await printMirroredSkill(name, mirroredArgs))
492
520
  return;
521
+ if (mirroredArgs.skillId) {
522
+ console.error(`No mirrored skill "${name}" with id ${mirroredArgs.skillId}.`);
523
+ process.exitCode = 1;
524
+ return;
525
+ }
493
526
  console.error(`Unknown load entry: ${name}\n`);
494
527
  await printCatalog();
495
528
  process.exitCode = 1;