greprag 5.82.1 → 5.83.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.
Files changed (39) hide show
  1. package/dist/claude-lifecycle-cutover.js +5 -16
  2. package/dist/commands/codex.js +4 -0
  3. package/dist/commands/corpus/client.js +16 -0
  4. package/dist/commands/corpus/index.js +5 -2
  5. package/dist/commands/corpus/section.js +3 -3
  6. package/dist/commands/corpus/walk.js +3 -3
  7. package/dist/commands/delivery.js +64 -0
  8. package/dist/commands/docptr.js +10 -24
  9. package/dist/commands/init.js +4 -5
  10. package/dist/commands/load-primer-reminder.js +2 -2
  11. package/dist/commands/load.js +45 -12
  12. package/dist/commands/merge-guard.js +94 -9
  13. package/dist/commands/parity.js +0 -2
  14. package/dist/commands/pipe-wrap.js +4 -2
  15. package/dist/commands/skill-mirror-reminder.js +24 -6
  16. package/dist/commands/skill.js +24 -8
  17. package/dist/commands/skillgain.js +5 -1
  18. package/dist/commands/state-trigger.js +67 -31
  19. package/dist/delivery-config.js +69 -0
  20. package/dist/deploy-locks.js +18 -3
  21. package/dist/doc-mirror-client.js +39 -11
  22. package/dist/hook.js +35 -39
  23. package/dist/index.js +62 -27
  24. package/dist/native-adapter-refresh.js +114 -0
  25. package/dist/native-skill-adoption.js +20 -53
  26. package/dist/native-skill-mirror.js +130 -52
  27. package/dist/native-skill-pending.js +5 -2
  28. package/dist/opencode-plugin.bundle.js +1079 -159
  29. package/dist/opencode-plugin.js +6 -0
  30. package/dist/skill-activation-manifest.js +98 -22
  31. package/dist/skill-landing.js +6 -0
  32. package/dist/skill-mirror-files.js +59 -0
  33. package/package.json +1 -1
  34. package/scripts/postinstall.js +4 -19
  35. package/skill/greprag/docs/corpus.md +2 -2
  36. package/skill/greprag/docs/setup.md +1 -1
  37. package/skill/mechanic/SKILL.md +14 -39
  38. package/skill/mechanic/docs/skill-fix-conventions.md +2 -2
  39. package/skill/templates/chip-spawn.md +36 -20
@@ -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
  }
@@ -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
  }
@@ -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;
@@ -22,7 +22,7 @@
22
22
  * plumbing that happens to share a prefix, and `merge-base` is used BY the
23
23
  * provenance check — blocking it would break the gate with the guard.
24
24
  * · only a merge nothing already serialises. A command that takes the lock
25
- * itself, ahead of the merge and `&&`-joined to it, is already serialised.
25
+ * itself and stops on acquisition failure is already serialised.
26
26
  *
27
27
  * Holding the lock yourself is a pass. A peer holding it is the refusal that
28
28
  * matters: it is the exact collision, caught before the merge instead of after.
@@ -51,6 +51,12 @@
51
51
  * trips. Only `&&` earns this: after `;` or `||` the merge still runs when the
52
52
  * acquire failed, so those are refused, and the refusal now says so.
53
53
  *
54
+ * A THIRD, 2026-10-06 in C:/sift: an acquire run from a linked worktree wrote the
55
+ * lock into that worktree's private git dir, so this guard, reading the canonical
56
+ * checkout, refused the holder's own merge whatever its shape. Locks now live in the
57
+ * repository's shared `.git`, and a chained acquire counts when it writes the same
58
+ * lock file this guard reads.
59
+ *
54
60
  * # adr: adr/deploy-gate-cli.md
55
61
  */
56
62
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -126,11 +132,11 @@ function shellSegments(command) {
126
132
  let start = 0;
127
133
  let afterAnd = true;
128
134
  for (let match = separator.exec(masked); match; match = separator.exec(masked)) {
129
- segments.push({ text: source.slice(start, match.index), afterAnd });
135
+ segments.push({ text: source.slice(start, match.index), start, afterAnd });
130
136
  afterAnd = match[0] === '&&';
131
137
  start = match.index + match[0].length;
132
138
  }
133
- segments.push({ text: source.slice(start), afterAnd });
139
+ segments.push({ text: source.slice(start), start, afterAnd });
134
140
  return segments;
135
141
  }
136
142
  /** Quote-aware tokens. A quoted run is one token with its quotes removed, so
@@ -297,9 +303,10 @@ function buildMergeGuardReason(held, ownership) {
297
303
  ' greprag merge-lock acquire --label "<what you are merging>"',
298
304
  ' git merge --ff-only <rebased branch>',
299
305
  ' greprag merge-lock release --landed <the merge sha> --label "<what it was>"',
300
- 'One call works too, as long as the acquire comes first and every step is joined by',
301
- '`&&` — that is what stops the merge when someone else holds the lock. A `;` or `||`',
302
- 'between them runs the merge anyway, so those are refused.',
306
+ 'One call works too when the acquire comes first and failure stops the merge:',
307
+ 'use an `&&` chain, or in PowerShell a try/finally block with an immediate',
308
+ '`if ($LASTEXITCODE -ne 0) { throw ... }` after acquire. Bare `;` and `||`',
309
+ 'let the merge run after a failed acquire, so those are refused.',
303
310
  '`--landed` is what lets the deploy prove YOUR work is in what ships, rather than only',
304
311
  'that production serves HEAD. Merging in a worktree instead? That is never blocked.',
305
312
  ].join(' ');
@@ -357,7 +364,8 @@ function guardMergeAt(input, segments, mergeAt) {
357
364
  // truncating it locally to 8 was a second copy of identity logic waiting to disagree
358
365
  // with the one in session-id.ts.
359
366
  const ownership = (0, deploy_locks_1.lockOwnership)(held, input.session_id);
360
- const chainedLock = takesLockForTarget(segments, mergeAt, cwd, target);
367
+ const chainedLock = takesLockForTarget(segments, mergeAt, cwd, target)
368
+ || powerShellTryTakesLock(input.tool_input?.command ?? input.tool_input?.cmd ?? '', segments[mergeAt].start, cwd, target);
361
369
  if (ownership !== 'mine' && !chainedLock)
362
370
  return denyMerge(input, buildMergeGuardReason(held, ownership));
363
371
  // Git owns the final freshness check AT mutation time. Even if master changes
@@ -403,11 +411,88 @@ function takesLockForTarget(segments, mergeAt, cwd, target) {
403
411
  return false;
404
412
  if (!isMergeLockAcquire(segments[i - 1].text))
405
413
  continue;
406
- const lockDir = mergeCwd(segments, i - 1, cwd);
407
- return (0, deploy_gate_1.normalizePath)(mergeWorktreeRoot([], lockDir) ?? lockDir) === (0, deploy_gate_1.normalizePath)(target);
414
+ return acquiresTargetLock(mergeCwd(segments, i - 1, cwd), target);
415
+ }
416
+ return false;
417
+ }
418
+ /** An acquire run in `dir` serialises a merge into `target` exactly when it writes the
419
+ * lock the guard reads for `target`. The lock is repository-wide, so an acquire from a
420
+ * linked worktree counts; comparing worktree roots refused that holder on 2026-10-06. */
421
+ function acquiresTargetLock(dir, target) {
422
+ try {
423
+ return (0, deploy_gate_1.normalizePath)((0, deploy_locks_1.mergeLockPath)(dir)) === (0, deploy_gate_1.normalizePath)((0, deploy_locks_1.mergeLockPath)(target));
424
+ }
425
+ catch {
426
+ return false;
427
+ }
428
+ }
429
+ /** PowerShell's `try { acquire; if ($LASTEXITCODE -ne 0) { throw ... }; merge }
430
+ * finally { release }` is a fail-closed lock sequence too. Inspect only direct
431
+ * statements in the try body; a bare semicolon or an acquire nested in a branch
432
+ * must not excuse a merge. */
433
+ function powerShellTryTakesLock(command, mergeStart, cwd, target) {
434
+ const masked = maskQuoted(command);
435
+ const tryStart = /(?:^|[;\n])\s*try\s*\{/gi;
436
+ for (let match = tryStart.exec(masked); match; match = tryStart.exec(masked)) {
437
+ const open = masked.indexOf('{', match.index);
438
+ const close = matchingBrace(masked, open);
439
+ if (close < 0 || mergeStart <= open || mergeStart >= close)
440
+ continue;
441
+ const suffix = masked.slice(close + 1);
442
+ const finallyMatch = /^\s*finally\s*\{/.exec(suffix);
443
+ if (!finallyMatch || matchingBrace(masked, close + 1 + finallyMatch[0].lastIndexOf('{')) < 0)
444
+ continue;
445
+ const statements = powerShellStatements(command, masked, open + 1, close);
446
+ const mergeIndex = statements.findIndex(s => mergeStart >= s.start && mergeStart < s.end);
447
+ if (mergeIndex < 2 || mergeGlobalOpts(statements[mergeIndex].text)?.length !== 0)
448
+ continue;
449
+ // A location change would make the lock's target ambiguous. The regular
450
+ // chained-command path already resolves simple cd/Set-Location sequences.
451
+ if (/(?:^|[;\n])\s*(?:cd|chdir|sl|Set-Location|Push-Location|Pop-Location|pushd|popd)\b/i.test(masked.slice(0, mergeStart)))
452
+ continue;
453
+ if (!acquiresTargetLock(cwd, target))
454
+ continue;
455
+ for (let i = mergeIndex - 2; i >= 0; i--) {
456
+ if (!isMergeLockAcquire(statements[i].text))
457
+ continue;
458
+ if (/[|&]/.test(maskQuoted(statements[i].text)))
459
+ continue;
460
+ if (!/^if\s*\(\s*\$LASTEXITCODE\s*-ne\s*0\s*\)\s*\{\s*throw\b[^{}]*\}\s*$/i.test(statements[i + 1].text.trim()))
461
+ continue;
462
+ if (statements.slice(i + 2, mergeIndex).some(s => /\bmerge-lock\s+release\b/i.test(s.text)))
463
+ continue;
464
+ return true;
465
+ }
408
466
  }
409
467
  return false;
410
468
  }
469
+ function matchingBrace(masked, open) {
470
+ let depth = 0;
471
+ for (let i = open; i < masked.length; i++) {
472
+ if (masked[i] === '{')
473
+ depth++;
474
+ else if (masked[i] === '}' && --depth === 0)
475
+ return i;
476
+ }
477
+ return -1;
478
+ }
479
+ function powerShellStatements(command, masked, start, end) {
480
+ const statements = [];
481
+ let depth = 0;
482
+ let from = start;
483
+ for (let i = start; i < end; i++) {
484
+ if (masked[i] === '{')
485
+ depth++;
486
+ else if (masked[i] === '}')
487
+ depth--;
488
+ else if (depth === 0 && (masked[i] === ';' || masked[i] === '\n')) {
489
+ statements.push({ text: command.slice(from, i).trim(), start: from, end: i });
490
+ from = i + 1;
491
+ }
492
+ }
493
+ statements.push({ text: command.slice(from, end).trim(), start: from, end });
494
+ return statements.filter(s => s.text);
495
+ }
411
496
  function denyMerge(input, reason) {
412
497
  return {
413
498
  hookSpecificOutput: {
@@ -80,10 +80,8 @@ const SEED_UNITS = [
80
80
  {
81
81
  name: 'chip-spawn',
82
82
  followers: [
83
- { harness: 'claude-code', candidate: '~/.claude/docs/chip-spawn.md' },
84
83
  { harness: 'claude-code', candidate: 'packages/cli/skill/templates/chip-spawn.md' },
85
84
  { harness: 'codex', candidate: 'packages/cli/skill/templates/codex-chip-spawn.md' },
86
- { harness: 'codex', candidate: 'docs/codex-chip-spawn.md' },
87
85
  { harness: 'opencode', candidate: null, note: 'no dedicated opencode chip-spawn doc found — chips ride greprag opencode chip + templates/chip-leader-opencode.md covers the leader shape only' },
88
86
  ],
89
87
  },