greprag 5.78.7 → 5.79.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.
@@ -66,11 +66,11 @@ function adapterFor(skill) {
66
66
  function safeTimestamp(now) {
67
67
  return now.toISOString().replace(/[:.]/g, '-');
68
68
  }
69
- function requiredSkills(skills) {
69
+ function requiredSkills(skills, names) {
70
70
  const byName = new Map(skills
71
71
  .filter(skill => skill.scope !== 'project')
72
72
  .map(skill => [skill.skillName, skill]));
73
- return exports.CLAUDE_LIFECYCLE_SKILLS.map(name => {
73
+ return names.map(name => {
74
74
  const skill = byName.get(name);
75
75
  if (!skill)
76
76
  throw new Error(`Canonical Claude lifecycle skill missing: ${name}.`);
@@ -80,12 +80,17 @@ function requiredSkills(skills) {
80
80
  /**
81
81
  * Explicit, narrow Claude cutover. Only SKILL.md becomes mirror-managed;
82
82
  * companion scripts and docs remain untouched in the native package.
83
+ *
84
+ * `names` selects which mirrored skills to materialize; it defaults to the
85
+ * lifecycle triple. Naming skills explicitly is how a Claude Code operator
86
+ * closes an adapter gap without arming a fleet-wide sweep.
83
87
  */
84
88
  function applyClaudeLifecycleCutover(params) {
85
89
  const homeDir = params.homeDir || os.homedir();
86
90
  const root = (0, native_skill_mirror_1.nativeSkillRoot)('claude-code', homeDir);
87
91
  const state = readState(root);
88
- const prepared = requiredSkills(params.skills).map(skill => {
92
+ const names = params.names?.length ? params.names : exports.CLAUDE_LIFECYCLE_SKILLS;
93
+ const prepared = requiredSkills(params.skills, names).map(skill => {
89
94
  const target = path.join(root, skill.skillName);
90
95
  const skillMd = path.join(target, 'SKILL.md');
91
96
  const adapterFiles = adapterFor(skill);
@@ -137,16 +142,18 @@ function applyClaudeLifecycleCutover(params) {
137
142
  return result;
138
143
  }
139
144
  async function syncClaudeLifecycleCutover(params) {
145
+ const names = params.names?.length ? [...new Set(params.names)] : [...exports.CLAUDE_LIFECYCLE_SKILLS];
140
146
  const list = await fetch(`${params.apiUrl}/v1/skillmirror`, {
141
147
  headers: { Authorization: `Bearer ${params.apiKey}` },
142
148
  });
143
149
  if (!list.ok)
144
150
  throw new Error(`skill mirror list failed: HTTP ${list.status}`);
145
151
  const data = await list.json();
146
- const wanted = new Set(exports.CLAUDE_LIFECYCLE_SKILLS);
152
+ const wanted = new Set(names);
147
153
  const rows = (data.skills || []).filter(row => wanted.has(row.skillName) && row.scope !== 'project');
148
- if (rows.length !== exports.CLAUDE_LIFECYCLE_SKILLS.length) {
149
- throw new Error('Canonical Claude lifecycle skill list is incomplete; native files preserved.');
154
+ if (rows.length !== names.length) {
155
+ const missing = names.filter(name => !rows.some(row => row.skillName === name));
156
+ throw new Error(`Not in the global skill mirror: ${missing.join(', ')}; native files preserved.`);
150
157
  }
151
158
  const fetched = await Promise.all(rows.map(async (row) => {
152
159
  const query = new URLSearchParams({ skillId: row.skillId || `global:${row.skillName}` });
@@ -170,5 +177,6 @@ async function syncClaudeLifecycleCutover(params) {
170
177
  return applyClaudeLifecycleCutover({
171
178
  skills: fetched,
172
179
  homeDir: params.homeDir,
180
+ names,
173
181
  });
174
182
  }
@@ -55,6 +55,9 @@ exports.runLoad = runLoad;
55
55
  exports.normalizeLoadName = normalizeLoadName;
56
56
  const fs = __importStar(require("fs"));
57
57
  const path = __importStar(require("path"));
58
+ const os = __importStar(require("os"));
59
+ const harness_1 = require("../harness");
60
+ const skill_activation_manifest_1 = require("../skill-activation-manifest");
58
61
  const project_anchor_1 = require("../project-anchor");
59
62
  const proc_1 = require("../proc");
60
63
  const skill_staleness_1 = require("../skill-staleness");
@@ -171,13 +174,30 @@ async function printCatalog() {
171
174
  const data = await mirrorGet('/v1/skillmirror');
172
175
  const skills = data?.skills || [];
173
176
  if (skills.length > 0) {
177
+ // A mirrored skill with no native adapter on this harness never reaches the
178
+ // harness's own skill list — the agent can only reach it through this
179
+ // catalog. Mark the gap here and name the command that closes it.
180
+ const platform = (0, harness_1.inferCurrentHarness)() || '';
181
+ const installed = platform
182
+ ? (0, skill_activation_manifest_1.readNativeSkillNames)({ platform, cwd: process.cwd(), homeDir: os.homedir() })
183
+ : new Set();
184
+ const missing = platform
185
+ ? skills.map((s) => s.skillName || '').filter((n) => n && !installed.has(n.toLowerCase()))
186
+ : [];
174
187
  const w = Math.max(...skills.map((s) => (s.skillName || '').length), 8);
175
188
  console.log('\nYour mirrored skills (auto-fresh from use — work on any machine/harness):\n');
176
189
  for (const s of skills) {
177
190
  if (!s.skillName)
178
191
  continue;
179
192
  const scope = s.scope === 'project' && s.projectId ? ` [project:${s.projectId.slice(0, 8)}]` : '';
180
- console.log(` ${(s.skillName + scope).padEnd(w + 20)} ${(s.description || '').slice(0, 110)}`);
193
+ const gap = missing.includes(s.skillName) ? ' *' : '';
194
+ console.log(` ${(s.skillName + scope + gap).padEnd(w + 20)} ${(s.description || '').slice(0, 110)}`);
195
+ }
196
+ const installCommand = (0, skill_activation_manifest_1.nativeAdapterInstallCommand)(platform, missing);
197
+ if (installCommand) {
198
+ console.log(`\n * ${missing.length} skill(s) have no ${platform} adapter installed — they never appear`);
199
+ console.log(' in this harness\'s own skill list. Install the adapters with:\n');
200
+ console.log(` ${installCommand}\n`);
181
201
  }
182
202
  }
183
203
  console.log('\n greprag load <name> print the full entry / mirrored skill');
@@ -8,13 +8,14 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.skillMirrorAnnounceModule = exports.SKILL_ACTIVATION_MANIFEST_MAX_CHARS = void 0;
10
10
  exports.buildSkillMirrorAnnounce = buildSkillMirrorAnnounce;
11
+ const skill_activation_manifest_1 = require("../skill-activation-manifest");
11
12
  exports.SKILL_ACTIVATION_MANIFEST_MAX_CHARS = 64_000;
12
13
  function renderEntry(skill) {
13
14
  const description = skill.triggerDescription.trim().replace(/\r\n?/g, '\n');
14
15
  const lines = description.split('\n');
15
16
  return `- ${skill.skillName}: ${lines[0]}${lines.slice(1).map(line => `\n ${line}`).join('')}`;
16
17
  }
17
- function buildSkillMirrorAnnounce(m) {
18
+ function buildSkillMirrorAnnounce(m, platform) {
18
19
  if (!m || m.count <= 0)
19
20
  return null;
20
21
  if (m.skills.length === 0) {
@@ -23,6 +24,21 @@ function buildSkillMirrorAnnounce(m) {
23
24
  + 'keep storage mechanics internal unless diagnosing a failure or the user asks how it works. '
24
25
  + 'Use `greprag load <skill>` to read skill instructions.]';
25
26
  }
27
+ // The entries this harness does not advertise are exactly the mirrored
28
+ // skills with no native adapter installed. Name the gap and the one command
29
+ // that closes it, or it stays invisible until the operator spots it by hand.
30
+ const installCommand = (0, skill_activation_manifest_1.nativeAdapterInstallCommand)(platform || '', m.skills.map(s => s.skillName));
31
+ const gapSection = installCommand ? [
32
+ '### Missing Native Adapters',
33
+ `${m.skills.length} of these skills are in your GrepRAG mirror but have NO adapter installed in this harness, `
34
+ + 'so they never appear in the native skill list. Install the adapters once with:',
35
+ '',
36
+ ` ${installCommand}`,
37
+ '',
38
+ 'Offer that command when the user asks why a skill is missing, or when you needed one of them this session. '
39
+ + 'Until adapters are installed, the trigger rules below are the only way these skills activate.',
40
+ '',
41
+ ] : [];
26
42
  const header = [
27
43
  '## GrepRAG Skills',
28
44
  `Slash skills are available through \`greprag load\`; ${m.skills.length} skills need this startup trigger list`
@@ -30,6 +46,7 @@ function buildSkillMirrorAnnounce(m) {
30
46
  + '.',
31
47
  'When discussing skills with the user, say use/create/update/refresh `/skill`; keep storage mechanics internal unless diagnosing a failure or the user asks how it works.',
32
48
  '',
49
+ ...gapSection,
33
50
  '### Trigger Rules',
34
51
  'If the user names a skill (with or without `/`) OR the request clearly matches a skill\'s description below, '
35
52
  + 'you MUST activate that skill for the current turn. Match meaning, not exact keywords.',
@@ -58,6 +75,6 @@ exports.skillMirrorAnnounceModule = {
58
75
  source: 'prompt',
59
76
  dependsOn: ['load-primer'], // references `greprag load` — the primer establishes it
60
77
  detect: (_env) => ({ tier: 'silent' }), // announce-only
61
- announce: (env) => buildSkillMirrorAnnounce(env.mirroredSkills),
78
+ announce: (env) => buildSkillMirrorAnnounce(env.mirroredSkills, env.platform),
62
79
  reminder: () => null,
63
80
  };
@@ -47,6 +47,7 @@ var __importStar = (this && this.__importStar) || (function () {
47
47
  };
48
48
  })();
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.readOnlyNames = readOnlyNames;
50
51
  exports.runSkill = runSkill;
51
52
  const fs = __importStar(require("fs"));
52
53
  const os = __importStar(require("os"));
@@ -59,6 +60,22 @@ const skill_mirror_client_1 = require("../skill-mirror-client");
59
60
  const skill_landing_1 = require("../skill-landing");
60
61
  const skill_staleness_1 = require("../skill-staleness");
61
62
  const CORE_SKILL = 'greprag';
63
+ const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
64
+ /** `--only a,b` / `--only=a,b` / repeated `--only a --only b` → skill names. */
65
+ function readOnlyNames(args) {
66
+ const raw = [];
67
+ for (let i = 0; i < args.length; i++) {
68
+ if (args[i] === '--only' && args[i + 1])
69
+ raw.push(args[++i]);
70
+ else if (args[i].startsWith('--only='))
71
+ raw.push(args[i].slice('--only='.length));
72
+ }
73
+ const names = [...new Set(raw.flatMap(value => value.split(',')).map(v => v.trim()).filter(Boolean))];
74
+ const invalid = names.filter(name => !SKILL_NAME_RE.test(name));
75
+ if (invalid.length > 0)
76
+ throw new Error(`--only got invalid skill name(s): ${invalid.join(', ')}`);
77
+ return names;
78
+ }
62
79
  const SKILL_HELP = `greprag skill — manage bundled advisor skills
63
80
 
64
81
  USAGE
@@ -69,6 +86,8 @@ USAGE
69
86
  Export/edit/apply helper; opens $EDITOR unless --no-open or Codex
70
87
  greprag skill mirror sync [harness] Install native GrepRAG skill adapters
71
88
  claude-code --cutover-lifecycle Cut over commit/deploy/release only
89
+ claude-code --only <a,b> Install adapters for named mirrored skills
90
+ (closes the gaps greprag load marks with *)
72
91
  greprag skill mirror export <name> --to <dir>
73
92
  Export canonical skill bytes for editing
74
93
  greprag skill mirror apply <dir> --skill <name> --expected-hash <hash>
@@ -631,7 +650,11 @@ async function runSkill(args) {
631
650
  }
632
651
  const dryRun = args.includes('--dry-run');
633
652
  const claudeLifecycleCutover = args.includes('--cutover-lifecycle');
634
- const positional = args.slice(2).filter(arg => !arg.startsWith('-'));
653
+ // `--only a,b` names the mirrored skills to materialize as claude-code
654
+ // adapters — the one command that closes an adapter gap the catalog and
655
+ // the SessionStart announce report, without arming a fleet-wide sweep.
656
+ const onlyNames = readOnlyNames(args.slice(2));
657
+ const positional = args.slice(2).filter((arg, index, all) => !arg.startsWith('-') && all[index - 1] !== '--only');
635
658
  const requested = positional[0] || 'all';
636
659
  const platforms = requested === 'all'
637
660
  ? [...native_skill_mirror_1.NATIVE_SKILL_ADAPTER_PLATFORMS]
@@ -645,23 +668,28 @@ async function runSkill(args) {
645
668
  if (claudeLifecycleCutover && requested !== 'claude-code') {
646
669
  throw new Error('--cutover-lifecycle requires harness claude-code.');
647
670
  }
671
+ if (onlyNames.length > 0 && requested !== 'claude-code') {
672
+ throw new Error('--only requires harness claude-code.');
673
+ }
674
+ const claudeTargeted = claudeLifecycleCutover || onlyNames.length > 0;
648
675
  const apiUrl = process.env.GREPRAG_API_URL || 'https://api.greprag.com';
649
676
  const anchor = (0, project_anchor_1.readAnchor)(process.cwd());
650
677
  if (requested === 'all') {
651
678
  console.log('claude-code: native skill adapter sync disabled until Claude cutover approval.');
652
679
  }
653
680
  for (const platform of platforms) {
654
- if (platform === 'claude-code' && claudeLifecycleCutover) {
681
+ if (platform === 'claude-code' && claudeTargeted) {
682
+ const names = onlyNames.length > 0 ? onlyNames : [...claude_lifecycle_cutover_1.CLAUDE_LIFECYCLE_SKILLS];
655
683
  if (dryRun) {
656
- console.log(`claude-code: targeted lifecycle cutover would manage only ${claude_lifecycle_cutover_1.CLAUDE_LIFECYCLE_SKILLS.join(', ')}; companion files remain untouched.`);
684
+ console.log(`claude-code: targeted cutover would manage only ${names.join(', ')}; companion files remain untouched.`);
657
685
  continue;
658
686
  }
659
687
  const apiKey = process.env.GREPRAG_API_KEY || '';
660
688
  if (!apiKey)
661
689
  throw new Error('GREPRAG_API_KEY not set — run `greprag init` first.');
662
- const result = await (0, claude_lifecycle_cutover_1.syncClaudeLifecycleCutover)({ apiUrl, apiKey });
690
+ const result = await (0, claude_lifecycle_cutover_1.syncClaudeLifecycleCutover)({ apiUrl, apiKey, names });
663
691
  const active = result.installed + result.updated + result.current;
664
- console.log(`claude-code: ${active} lifecycle adapter(s) active; all other Claude skills remain unmanaged.`);
692
+ console.log(`claude-code: ${active} adapter(s) active (${names.join(', ')}); all other Claude skills remain unmanaged.`);
665
693
  if (result.backupDir)
666
694
  console.log(`claude-code: replaced prompts backed up at ${result.backupDir}`);
667
695
  continue;
package/dist/hook-once.js CHANGED
@@ -41,6 +41,16 @@ const fs = __importStar(require("fs"));
41
41
  const os = __importStar(require("os"));
42
42
  const path = __importStar(require("path"));
43
43
  const TTL_MS = 60_000;
44
+ /** Events that fire once per TOOL CALL, not once per turn.
45
+ *
46
+ * The stamp key is (session, event, subcommand, turn), so it can only dedupe an
47
+ * event whose natural rate is one per turn — i.e. a leaked duplicate hook
48
+ * registration double-firing `store`/`recap`/`notify`. A per-call event issues
49
+ * many invocations under that ONE key, so claiming it lets the first call
50
+ * through and silently drops every later one. For a validator such as
51
+ * `pre-spawn-check` that is a fail-OPEN: the second chip of a turn is never
52
+ * checked. These events are not dedupable by this key and are never claimed. */
53
+ const PER_TOOL_CALL_EVENTS = new Set(['PreToolUse', 'PostToolUse']);
44
54
  function stampPath(sessionId, event, sub, turnId) {
45
55
  const safe = [sessionId, event, sub, turnId].join('-').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 180);
46
56
  return path.join(os.homedir(), '.greprag', 'hook-once', `${safe}.json`);
@@ -49,6 +59,8 @@ function stampPath(sessionId, event, sub, turnId) {
49
59
  function claimHookOnce(sessionId, event, subcommand, turnId) {
50
60
  if (!sessionId)
51
61
  return true;
62
+ if (event && PER_TOOL_CALL_EVENTS.has(event))
63
+ return true;
52
64
  const key = turnId || 'noturn';
53
65
  const file = stampPath(sessionId, event || 'unknown', subcommand, key);
54
66
  try {
package/dist/hook.js CHANGED
@@ -2084,12 +2084,11 @@ function validateChip(title, prompt) {
2084
2084
  `Block 1 requires the chip to open with: \`git worktree add .claude/worktrees/<slug> -b chip/<slug>\` ` +
2085
2085
  `then \`cd\` into it. Chips never edit the main checkout.`);
2086
2086
  }
2087
- if (!/\bgreprag\s+send\b/.test(prompt)) {
2088
- violations.push(`prompt missing report-back instruction. ` +
2089
- `Block 2 requires the chip to send a status message via: ` +
2090
- `\`greprag send "<status>: <commit> on chip/<slug>" --to <handle>@greprag.com/<parent-session-id> ` +
2091
- `--from-session <own-session-id> --artifact commit:<hash>\`.`);
2092
- }
2087
+ // NO report-back check. Chips report to the parent with native
2088
+ // `SendMessage to: "<parent-name>"`, which needs no greprag address and no
2089
+ // armed watcher on the parent. Requiring `greprag send` here denied every
2090
+ // correctly-composed native chip. See adr/prespawn-augmentation.md
2091
+ // 2026-09-03 entry.
2093
2092
  return violations;
2094
2093
  }
2095
2094
  /** Validate the chip prompt at the PreToolUse boundary. Validation-only —
@@ -2118,6 +2117,25 @@ function handlePreSpawnCheck(input) {
2118
2117
  },
2119
2118
  }) + '\n');
2120
2119
  }
2120
+ /** A validator that receives no payload has validated nothing. Fail-open is
2121
+ * the worse failure here: the caller trusts the gate, so a silent exit 0
2122
+ * ALLOWS an unchecked chip. Only `pre-spawn-check` is a gate — every other
2123
+ * subcommand is additive (recap/store/notify), so a lost payload there is
2124
+ * correctly a no-op. Exits the process; never returns for the gate case. */
2125
+ function denyOnMissingPayload(subcommand, why) {
2126
+ if (subcommand !== 'pre-spawn-check')
2127
+ return;
2128
+ process.stdout.write(JSON.stringify({
2129
+ hookSpecificOutput: {
2130
+ hookEventName: 'PreToolUse',
2131
+ permissionDecision: 'deny',
2132
+ permissionDecisionReason: `greprag pre-spawn validator received no payload (${why}), so the chip ` +
2133
+ `prompt was never checked. Re-call spawn_task; if this repeats, the ` +
2134
+ `greprag hook is misconfigured — run \`greprag doctor\`.`,
2135
+ },
2136
+ }) + '\n');
2137
+ process.exit(0);
2138
+ }
2121
2139
  /** PreToolUse mechanic dispatcher (Chip B) — `greprag-hook guard`. All real
2122
2140
  * logic lives in ./guard (runGuard); this wrapper resolves the projectId and
2123
2141
  * emits the hook JSON. Hard fail-open posture: any error → emit nothing,
@@ -2202,9 +2220,12 @@ async function main() {
2202
2220
  chunks.push(chunk);
2203
2221
  }
2204
2222
  const raw = Buffer.concat(chunks).toString('utf-8').trim();
2205
- input = (0, hook_runtime_1.normalizeHookInput)(raw ? JSON.parse(raw) : {});
2223
+ if (!raw)
2224
+ denyOnMissingPayload(subcommand, 'empty stdin');
2225
+ input = (0, hook_runtime_1.normalizeHookInput)(JSON.parse(raw));
2206
2226
  }
2207
2227
  catch {
2228
+ denyOnMissingPayload(subcommand, 'unparseable stdin');
2208
2229
  process.exit(0);
2209
2230
  }
2210
2231
  if (!(0, hook_once_1.claimHookOnce)(input.session_id, input.hook_event_name, subcommand, input.turn_id)) {
@@ -2183,6 +2183,118 @@ var skillGainAnnounceModule = {
2183
2183
  reminder: () => null
2184
2184
  };
2185
2185
 
2186
+ // src/skill-activation-manifest.ts
2187
+ var fs2 = __toESM(require("fs"));
2188
+ var path2 = __toESM(require("path"));
2189
+ var MAX_NATIVE_SKILL_FILES = 1500;
2190
+ var MAX_SCAN_DEPTH = 7;
2191
+ function homeRoots(homeDir, platform) {
2192
+ if (platform === "claude-code") {
2193
+ return [path2.join(homeDir, ".claude", "skills")];
2194
+ }
2195
+ if (platform === "codex") {
2196
+ return [
2197
+ path2.join(homeDir, ".codex", "skills"),
2198
+ path2.join(homeDir, ".agents", "skills"),
2199
+ path2.join(homeDir, ".codex", "plugins", "cache")
2200
+ ];
2201
+ }
2202
+ return [path2.join(homeDir, ".config", "opencode", "skills")];
2203
+ }
2204
+ function ancestorDirs(cwd) {
2205
+ const dirs = [];
2206
+ let current = path2.resolve(cwd);
2207
+ for (let depth = 0; depth < 16; depth++) {
2208
+ dirs.push(current);
2209
+ const parent = path2.dirname(current);
2210
+ if (parent === current)
2211
+ break;
2212
+ current = parent;
2213
+ }
2214
+ return dirs;
2215
+ }
2216
+ function repoRoots(cwd, platform) {
2217
+ return ancestorDirs(cwd).flatMap((dir) => {
2218
+ if (platform === "claude-code")
2219
+ return [path2.join(dir, ".claude", "skills")];
2220
+ if (platform === "codex") {
2221
+ return [path2.join(dir, ".codex", "skills"), path2.join(dir, ".agents", "skills")];
2222
+ }
2223
+ return [path2.join(dir, ".opencode", "skills")];
2224
+ });
2225
+ }
2226
+ function frontmatterName(content) {
2227
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
2228
+ const name = fm && /(?:^|\r?\n)name:\s*["']?([^\r\n"']+)/.exec(fm[1]);
2229
+ return name?.[1]?.trim() || "";
2230
+ }
2231
+ function readNativeSkillNames(params) {
2232
+ const names = /* @__PURE__ */ new Set();
2233
+ let visited = 0;
2234
+ const walk = (dir, depth) => {
2235
+ if (depth > MAX_SCAN_DEPTH || visited >= MAX_NATIVE_SKILL_FILES)
2236
+ return;
2237
+ let entries;
2238
+ try {
2239
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
2240
+ } catch {
2241
+ return;
2242
+ }
2243
+ for (const entry of entries) {
2244
+ if (visited >= MAX_NATIVE_SKILL_FILES)
2245
+ return;
2246
+ const full = path2.join(dir, entry.name);
2247
+ if (entry.isDirectory()) {
2248
+ walk(full, depth + 1);
2249
+ continue;
2250
+ }
2251
+ if (!entry.isFile() || entry.name.toLowerCase() !== "skill.md")
2252
+ continue;
2253
+ visited++;
2254
+ names.add(path2.basename(path2.dirname(full)).toLowerCase());
2255
+ try {
2256
+ const declared = frontmatterName(fs2.readFileSync(full, "utf8"));
2257
+ if (declared)
2258
+ names.add(declared.toLowerCase());
2259
+ } catch {
2260
+ }
2261
+ }
2262
+ };
2263
+ const roots = [
2264
+ ...homeRoots(params.homeDir, params.platform),
2265
+ ...repoRoots(params.cwd, params.platform)
2266
+ ];
2267
+ for (const root of roots)
2268
+ walk(root, 0);
2269
+ return names;
2270
+ }
2271
+ function buildMirroredSkillActivation(entries, nativeNames) {
2272
+ const valid = entries.filter((entry) => entry.skillName && entry.triggerDescription);
2273
+ if (valid.length === 0)
2274
+ return void 0;
2275
+ const skills = valid.filter((entry) => !nativeNames.has(entry.skillName.toLowerCase()));
2276
+ return { count: valid.length, skills, nativeCount: valid.length - skills.length };
2277
+ }
2278
+ function nativeAdapterInstallCommand(platform, missingNames) {
2279
+ if (missingNames.length === 0)
2280
+ return null;
2281
+ if (platform === "claude-code") {
2282
+ return `greprag skill mirror sync claude-code --only ${[...missingNames].sort().join(",")}`;
2283
+ }
2284
+ if (platform === "codex" || platform === "opencode") {
2285
+ return `greprag skill mirror sync ${platform}`;
2286
+ }
2287
+ return null;
2288
+ }
2289
+ function activationFromApiRows(params) {
2290
+ const entries = params.rows.filter((row) => typeof row.skillName === "string" && !!row.skillName && typeof row.triggerDescription === "string" && !!row.triggerDescription).map((row) => ({
2291
+ skillName: row.skillName,
2292
+ description: typeof row.description === "string" ? row.description : "",
2293
+ triggerDescription: row.triggerDescription
2294
+ }));
2295
+ return buildMirroredSkillActivation(entries, readNativeSkillNames(params));
2296
+ }
2297
+
2186
2298
  // src/commands/skill-mirror-reminder.ts
2187
2299
  var SKILL_ACTIVATION_MANIFEST_MAX_CHARS = 64e3;
2188
2300
  function renderEntry(skill) {
@@ -2191,17 +2303,31 @@ function renderEntry(skill) {
2191
2303
  return `- ${skill.skillName}: ${lines[0]}${lines.slice(1).map((line) => `
2192
2304
  ${line}`).join("")}`;
2193
2305
  }
2194
- function buildSkillMirrorAnnounce(m) {
2306
+ function buildSkillMirrorAnnounce(m, platform) {
2195
2307
  if (!m || m.count <= 0)
2196
2308
  return null;
2197
2309
  if (m.skills.length === 0) {
2198
2310
  return "[greprag skills: slash skills are available. When discussing skills with the user, say use/create/update/refresh `/skill`; keep storage mechanics internal unless diagnosing a failure or the user asks how it works. Use `greprag load <skill>` to read skill instructions.]";
2199
2311
  }
2312
+ const installCommand = nativeAdapterInstallCommand(
2313
+ platform || "",
2314
+ m.skills.map((s) => s.skillName)
2315
+ );
2316
+ const gapSection = installCommand ? [
2317
+ "### Missing Native Adapters",
2318
+ `${m.skills.length} of these skills are in your GrepRAG mirror but have NO adapter installed in this harness, so they never appear in the native skill list. Install the adapters once with:`,
2319
+ "",
2320
+ ` ${installCommand}`,
2321
+ "",
2322
+ "Offer that command when the user asks why a skill is missing, or when you needed one of them this session. Until adapters are installed, the trigger rules below are the only way these skills activate.",
2323
+ ""
2324
+ ] : [];
2200
2325
  const header = [
2201
2326
  "## GrepRAG Skills",
2202
2327
  `Slash skills are available through \`greprag load\`; ${m.skills.length} skills need this startup trigger list` + (m.nativeCount > 0 ? ` (${m.nativeCount} already-listed duplicate${m.nativeCount === 1 ? "" : "s"} omitted)` : "") + ".",
2203
2328
  "When discussing skills with the user, say use/create/update/refresh `/skill`; keep storage mechanics internal unless diagnosing a failure or the user asks how it works.",
2204
2329
  "",
2330
+ ...gapSection,
2205
2331
  "### Trigger Rules",
2206
2332
  "If the user names a skill (with or without `/`) OR the request clearly matches a skill's description below, you MUST activate that skill for the current turn. Match meaning, not exact keywords.",
2207
2333
  "",
@@ -2232,7 +2358,7 @@ var skillMirrorAnnounceModule = {
2232
2358
  // references `greprag load` — the primer establishes it
2233
2359
  detect: (_env) => ({ tier: "silent" }),
2234
2360
  // announce-only
2235
- announce: (env) => buildSkillMirrorAnnounce(env.mirroredSkills),
2361
+ announce: (env) => buildSkillMirrorAnnounce(env.mirroredSkills, env.platform),
2236
2362
  reminder: () => null
2237
2363
  };
2238
2364
 
@@ -2430,23 +2556,23 @@ function collectAnnounces(env, registry = REGISTRY) {
2430
2556
  }
2431
2557
 
2432
2558
  // src/app-settings.ts
2433
- var fs3 = __toESM(require("node:fs"));
2559
+ var fs4 = __toESM(require("node:fs"));
2434
2560
  var os2 = __toESM(require("node:os"));
2435
- var path3 = __toESM(require("node:path"));
2561
+ var path4 = __toESM(require("node:path"));
2436
2562
 
2437
2563
  // src/project-anchor.ts
2438
- var path2 = __toESM(require("path"));
2439
- var fs2 = __toESM(require("fs"));
2564
+ var path3 = __toESM(require("path"));
2565
+ var fs3 = __toESM(require("fs"));
2440
2566
  var crypto2 = __toESM(require("crypto"));
2441
2567
  var os = __toESM(require("os"));
2442
2568
  var ANCHOR_DIR = ".greprag";
2443
2569
  var ANCHOR_FILE = "project.json";
2444
2570
  var LEGACY_ANCHOR_DIR = ".claude";
2445
2571
  function anchorPathIn(dir) {
2446
- return path2.join(dir, ANCHOR_DIR, ANCHOR_FILE);
2572
+ return path3.join(dir, ANCHOR_DIR, ANCHOR_FILE);
2447
2573
  }
2448
2574
  function legacyAnchorPathIn(dir) {
2449
- return path2.join(dir, LEGACY_ANCHOR_DIR, ANCHOR_FILE);
2575
+ return path3.join(dir, LEGACY_ANCHOR_DIR, ANCHOR_FILE);
2450
2576
  }
2451
2577
  function globalAnchorPath() {
2452
2578
  return anchorPathIn(os.homedir());
@@ -2457,22 +2583,22 @@ function legacyGlobalAnchorPath() {
2457
2583
  function findExistingAnchor(startDir) {
2458
2584
  const homeAnchor = globalAnchorPath();
2459
2585
  const legacyHomeAnchor = legacyGlobalAnchorPath();
2460
- let dir = path2.resolve(startDir);
2586
+ let dir = path3.resolve(startDir);
2461
2587
  while (true) {
2462
2588
  const candidate = anchorPathIn(dir);
2463
- if (candidate !== homeAnchor && fs2.existsSync(candidate))
2589
+ if (candidate !== homeAnchor && fs3.existsSync(candidate))
2464
2590
  return candidate;
2465
2591
  const legacyCandidate = legacyAnchorPathIn(dir);
2466
- if (legacyCandidate !== legacyHomeAnchor && fs2.existsSync(legacyCandidate))
2592
+ if (legacyCandidate !== legacyHomeAnchor && fs3.existsSync(legacyCandidate))
2467
2593
  return legacyCandidate;
2468
- const parent = path2.dirname(dir);
2594
+ const parent = path3.dirname(dir);
2469
2595
  if (parent === dir)
2470
2596
  return null;
2471
2597
  dir = parent;
2472
2598
  }
2473
2599
  }
2474
2600
  function isEphemeralCwd2(cwd) {
2475
- const norm = path2.resolve(cwd).replace(/\\/g, "/").toLowerCase();
2601
+ const norm = path3.resolve(cwd).replace(/\\/g, "/").toLowerCase();
2476
2602
  if (norm.includes("/appdata/roaming/claude/local-agent-mode-sessions/"))
2477
2603
  return true;
2478
2604
  if (norm.includes("/appdata/local/claude/local-agent-mode-sessions/"))
@@ -2510,7 +2636,7 @@ function computeGitDerivedProjectId2(workingDir) {
2510
2636
  }
2511
2637
  }
2512
2638
  function deterministicProjectId2(workingDir) {
2513
- const normalized = path2.resolve(workingDir).toLowerCase();
2639
+ const normalized = path3.resolve(workingDir).toLowerCase();
2514
2640
  const hash = crypto2.createHash("sha256").update(normalized).digest("hex");
2515
2641
  return [
2516
2642
  hash.slice(0, 8),
@@ -2524,7 +2650,7 @@ function deterministicProjectId2(workingDir) {
2524
2650
  }
2525
2651
  function tryReadAnchorFileContents(filePath) {
2526
2652
  try {
2527
- const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
2653
+ const raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
2528
2654
  const notifyRaw = raw.inbox_notify;
2529
2655
  const inboxNotify = notifyRaw === "off" || notifyRaw === "session_start_only" ? notifyRaw : "every_turn";
2530
2656
  return {
@@ -2563,10 +2689,10 @@ function readAnchor2(cwd) {
2563
2689
  }
2564
2690
  const gitId = computeGitDerivedProjectId2(cwd);
2565
2691
  if (gitId) {
2566
- const root2 = path2.resolve(cwd);
2692
+ const root2 = path3.resolve(cwd);
2567
2693
  return {
2568
2694
  projectId: gitId,
2569
- projectName: fileContents?.projectName || path2.basename(root2).toLowerCase(),
2695
+ projectName: fileContents?.projectName || path3.basename(root2).toLowerCase(),
2570
2696
  initialized: true,
2571
2697
  source: "git",
2572
2698
  anchorPath: existingPath || anchorPathIn(root2),
@@ -2580,7 +2706,7 @@ function readAnchor2(cwd) {
2580
2706
  };
2581
2707
  }
2582
2708
  if (isEphemeralCwd2(cwd)) {
2583
- const globalPath = fs2.existsSync(globalAnchorPath()) ? globalAnchorPath() : legacyGlobalAnchorPath();
2709
+ const globalPath = fs3.existsSync(globalAnchorPath()) ? globalAnchorPath() : legacyGlobalAnchorPath();
2584
2710
  const globalContents = tryReadAnchorFileContents(globalPath);
2585
2711
  if (globalContents && globalContents.projectId && globalContents.projectName) {
2586
2712
  return {
@@ -2599,10 +2725,10 @@ function readAnchor2(cwd) {
2599
2725
  };
2600
2726
  }
2601
2727
  }
2602
- const root = path2.resolve(cwd);
2728
+ const root = path3.resolve(cwd);
2603
2729
  return {
2604
2730
  projectId: deterministicProjectId2(root),
2605
- projectName: fileContents?.projectName || path2.basename(root).toLowerCase(),
2731
+ projectName: fileContents?.projectName || path3.basename(root).toLowerCase(),
2606
2732
  initialized: false,
2607
2733
  source: "hash",
2608
2734
  anchorPath: existingPath || anchorPathIn(root),
@@ -2619,14 +2745,14 @@ function readAnchor2(cwd) {
2619
2745
  // src/app-settings.ts
2620
2746
  function readJson(file) {
2621
2747
  try {
2622
- const parsed = JSON.parse(fs3.readFileSync(file, "utf8"));
2748
+ const parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
2623
2749
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
2624
2750
  } catch {
2625
2751
  return {};
2626
2752
  }
2627
2753
  }
2628
2754
  function localAppSettingsPath(homeDir = os2.homedir()) {
2629
- return path3.join(homeDir, ".greprag", "settings.json");
2755
+ return path4.join(homeDir, ".greprag", "settings.json");
2630
2756
  }
2631
2757
  function readLocalAppSettings(homeDir = os2.homedir()) {
2632
2758
  const raw = readJson(localAppSettingsPath(homeDir));
@@ -2683,8 +2809,8 @@ function getOpenCodeReminders(env) {
2683
2809
  }
2684
2810
 
2685
2811
  // src/procedure.ts
2686
- var path4 = __toESM(require("path"));
2687
- var fs4 = __toESM(require("fs"));
2812
+ var path5 = __toESM(require("path"));
2813
+ var fs5 = __toESM(require("fs"));
2688
2814
 
2689
2815
  // src/delivery-lifecycle.ts
2690
2816
  var DELIVERY_LIFECYCLE_VERBS = [
@@ -2703,14 +2829,14 @@ function isDeliveryLifecycleVerb(verb) {
2703
2829
  var PROCEDURE_STORE_VERSION = "2";
2704
2830
  function stateDir() {
2705
2831
  const home = process.env.HOME || process.env.USERPROFILE || "";
2706
- return path4.join(home, ".greprag", "state");
2832
+ return path5.join(home, ".greprag", "state");
2707
2833
  }
2708
2834
  function procedureStorePath(projectId) {
2709
- return path4.join(stateDir(), `procedures-${projectId}.json`);
2835
+ return path5.join(stateDir(), `procedures-${projectId}.json`);
2710
2836
  }
2711
2837
  function readProcedureStore(projectId) {
2712
2838
  try {
2713
- const raw = fs4.readFileSync(procedureStorePath(projectId), "utf-8");
2839
+ const raw = fs5.readFileSync(procedureStorePath(projectId), "utf-8");
2714
2840
  const parsed = JSON.parse(raw);
2715
2841
  if (parsed && Array.isArray(parsed.procedures)) {
2716
2842
  return normalizeProcedureStore({
@@ -2944,8 +3070,8 @@ function activeProcedureAnnounces(store) {
2944
3070
  var crypto4 = __toESM(require("crypto"));
2945
3071
 
2946
3072
  // src/procedure-watch.ts
2947
- var path5 = __toESM(require("path"));
2948
- var fs5 = __toESM(require("fs"));
3073
+ var path6 = __toESM(require("path"));
3074
+ var fs6 = __toESM(require("fs"));
2949
3075
  var TIER1_LEARN_TRIGGERS = [
2950
3076
  { verb: "deploy", triggers: ["deploy", "deploy the api", "deploy the worker", "redeploy"], steps: "", status: "seeded" },
2951
3077
  { verb: "push", triggers: ["push", "git push", "push to remote", "push it up", "push upstream"], steps: "", status: "seeded", destructive: true },
@@ -2978,24 +3104,24 @@ function openWatch(verb, phase, openedAt, shadowRunId) {
2978
3104
  }
2979
3105
  function stateDir2() {
2980
3106
  const home = process.env.HOME || process.env.USERPROFILE || "";
2981
- return path5.join(home, ".greprag", "state");
3107
+ return path6.join(home, ".greprag", "state");
2982
3108
  }
2983
3109
  function procedureWatchPath(projectId) {
2984
- return path5.join(stateDir2(), `procedure-watch-${projectId}.json`);
3110
+ return path6.join(stateDir2(), `procedure-watch-${projectId}.json`);
2985
3111
  }
2986
3112
  function hasProcedureWatch(projectId) {
2987
3113
  try {
2988
- return fs5.existsSync(procedureWatchPath(projectId));
3114
+ return fs6.existsSync(procedureWatchPath(projectId));
2989
3115
  } catch {
2990
3116
  return false;
2991
3117
  }
2992
3118
  }
2993
3119
  function writeProcedureWatch(projectId, watch) {
2994
3120
  const file = procedureWatchPath(projectId);
2995
- const dir = path5.dirname(file);
2996
- if (!fs5.existsSync(dir))
2997
- fs5.mkdirSync(dir, { recursive: true });
2998
- fs5.writeFileSync(file, JSON.stringify(watch, null, 2) + "\n");
3121
+ const dir = path6.dirname(file);
3122
+ if (!fs6.existsSync(dir))
3123
+ fs6.mkdirSync(dir, { recursive: true });
3124
+ fs6.writeFileSync(file, JSON.stringify(watch, null, 2) + "\n");
2999
3125
  }
3000
3126
  function openWatchIfIdle(projectId, verb, phase, shadowRunId) {
3001
3127
  if (hasProcedureWatch(projectId))
@@ -3006,20 +3132,20 @@ function openWatchIfIdle(projectId, verb, phase, shadowRunId) {
3006
3132
 
3007
3133
  // src/procedure-shadow.ts
3008
3134
  var crypto3 = __toESM(require("crypto"));
3009
- var fs6 = __toESM(require("fs"));
3010
- var path6 = __toESM(require("path"));
3135
+ var fs7 = __toESM(require("fs"));
3136
+ var path7 = __toESM(require("path"));
3011
3137
  function stateDir3() {
3012
3138
  const home = process.env.HOME || process.env.USERPROFILE || "";
3013
- return path6.join(home, ".greprag", "state");
3139
+ return path7.join(home, ".greprag", "state");
3014
3140
  }
3015
3141
  function procedureShadowPath(projectId) {
3016
- return path6.join(stateDir3(), `procedure-shadow-${projectId}.jsonl`);
3142
+ return path7.join(stateDir3(), `procedure-shadow-${projectId}.jsonl`);
3017
3143
  }
3018
3144
  function appendShadowEvent(projectId, event) {
3019
3145
  try {
3020
3146
  const file = procedureShadowPath(projectId);
3021
- fs6.mkdirSync(path6.dirname(file), { recursive: true });
3022
- fs6.appendFileSync(file, JSON.stringify(event) + "\n");
3147
+ fs7.mkdirSync(path7.dirname(file), { recursive: true });
3148
+ fs7.appendFileSync(file, JSON.stringify(event) + "\n");
3023
3149
  } catch {
3024
3150
  }
3025
3151
  }
@@ -3738,107 +3864,6 @@ function recodeMessagesToPng(messages, opts) {
3738
3864
  return stats;
3739
3865
  }
3740
3866
 
3741
- // src/skill-activation-manifest.ts
3742
- var fs7 = __toESM(require("fs"));
3743
- var path7 = __toESM(require("path"));
3744
- var MAX_NATIVE_SKILL_FILES = 1500;
3745
- var MAX_SCAN_DEPTH = 7;
3746
- function homeRoots(homeDir, platform) {
3747
- if (platform === "claude-code") {
3748
- return [path7.join(homeDir, ".claude", "skills")];
3749
- }
3750
- if (platform === "codex") {
3751
- return [
3752
- path7.join(homeDir, ".codex", "skills"),
3753
- path7.join(homeDir, ".agents", "skills"),
3754
- path7.join(homeDir, ".codex", "plugins", "cache")
3755
- ];
3756
- }
3757
- return [path7.join(homeDir, ".config", "opencode", "skills")];
3758
- }
3759
- function ancestorDirs(cwd) {
3760
- const dirs = [];
3761
- let current = path7.resolve(cwd);
3762
- for (let depth = 0; depth < 16; depth++) {
3763
- dirs.push(current);
3764
- const parent = path7.dirname(current);
3765
- if (parent === current)
3766
- break;
3767
- current = parent;
3768
- }
3769
- return dirs;
3770
- }
3771
- function repoRoots(cwd, platform) {
3772
- return ancestorDirs(cwd).flatMap((dir) => {
3773
- if (platform === "claude-code")
3774
- return [path7.join(dir, ".claude", "skills")];
3775
- if (platform === "codex") {
3776
- return [path7.join(dir, ".codex", "skills"), path7.join(dir, ".agents", "skills")];
3777
- }
3778
- return [path7.join(dir, ".opencode", "skills")];
3779
- });
3780
- }
3781
- function frontmatterName(content) {
3782
- const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
3783
- const name = fm && /(?:^|\r?\n)name:\s*["']?([^\r\n"']+)/.exec(fm[1]);
3784
- return name?.[1]?.trim() || "";
3785
- }
3786
- function readNativeSkillNames(params) {
3787
- const names = /* @__PURE__ */ new Set();
3788
- let visited = 0;
3789
- const walk = (dir, depth) => {
3790
- if (depth > MAX_SCAN_DEPTH || visited >= MAX_NATIVE_SKILL_FILES)
3791
- return;
3792
- let entries;
3793
- try {
3794
- entries = fs7.readdirSync(dir, { withFileTypes: true });
3795
- } catch {
3796
- return;
3797
- }
3798
- for (const entry of entries) {
3799
- if (visited >= MAX_NATIVE_SKILL_FILES)
3800
- return;
3801
- const full = path7.join(dir, entry.name);
3802
- if (entry.isDirectory()) {
3803
- walk(full, depth + 1);
3804
- continue;
3805
- }
3806
- if (!entry.isFile() || entry.name.toLowerCase() !== "skill.md")
3807
- continue;
3808
- visited++;
3809
- names.add(path7.basename(path7.dirname(full)).toLowerCase());
3810
- try {
3811
- const declared = frontmatterName(fs7.readFileSync(full, "utf8"));
3812
- if (declared)
3813
- names.add(declared.toLowerCase());
3814
- } catch {
3815
- }
3816
- }
3817
- };
3818
- const roots = [
3819
- ...homeRoots(params.homeDir, params.platform),
3820
- ...repoRoots(params.cwd, params.platform)
3821
- ];
3822
- for (const root of roots)
3823
- walk(root, 0);
3824
- return names;
3825
- }
3826
- function buildMirroredSkillActivation(entries, nativeNames) {
3827
- const valid = entries.filter((entry) => entry.skillName && entry.triggerDescription);
3828
- if (valid.length === 0)
3829
- return void 0;
3830
- const skills = valid.filter((entry) => !nativeNames.has(entry.skillName.toLowerCase()));
3831
- return { count: valid.length, skills, nativeCount: valid.length - skills.length };
3832
- }
3833
- function activationFromApiRows(params) {
3834
- const entries = params.rows.filter((row) => typeof row.skillName === "string" && !!row.skillName && typeof row.triggerDescription === "string" && !!row.triggerDescription).map((row) => ({
3835
- skillName: row.skillName,
3836
- description: typeof row.description === "string" ? row.description : "",
3837
- triggerDescription: row.triggerDescription
3838
- }));
3839
- return buildMirroredSkillActivation(entries, readNativeSkillNames(params));
3840
- }
3841
-
3842
3867
  // src/opencode-plugin.ts
3843
3868
  var DEBUG_LOG_PATH = path8.join(os3.homedir(), ".greprag", "opencode-plugin-debug.log");
3844
3869
  var _debugLogReady = false;
@@ -43,6 +43,7 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  Object.defineProperty(exports, "__esModule", { value: true });
44
44
  exports.readNativeSkillNames = readNativeSkillNames;
45
45
  exports.buildMirroredSkillActivation = buildMirroredSkillActivation;
46
+ exports.nativeAdapterInstallCommand = nativeAdapterInstallCommand;
46
47
  exports.activationFromApiRows = activationFromApiRows;
47
48
  const fs = __importStar(require("fs"));
48
49
  const path = __importStar(require("path"));
@@ -139,6 +140,21 @@ function buildMirroredSkillActivation(entries, nativeNames) {
139
140
  const skills = valid.filter(entry => !nativeNames.has(entry.skillName.toLowerCase()));
140
141
  return { count: valid.length, skills, nativeCount: valid.length - skills.length };
141
142
  }
143
+ /** The one command that materializes native adapters for the named mirrored
144
+ * skills on this harness. claude-code is not swept wholesale (adapters there
145
+ * are opt-in per name); codex/opencode sync their whole roster. Null when
146
+ * nothing is missing. */
147
+ function nativeAdapterInstallCommand(platform, missingNames) {
148
+ if (missingNames.length === 0)
149
+ return null;
150
+ if (platform === 'claude-code') {
151
+ return `greprag skill mirror sync claude-code --only ${[...missingNames].sort().join(',')}`;
152
+ }
153
+ if (platform === 'codex' || platform === 'opencode') {
154
+ return `greprag skill mirror sync ${platform}`;
155
+ }
156
+ return null;
157
+ }
142
158
  /** Convert the mirror API response into a harness-specific activation signal. */
143
159
  function activationFromApiRows(params) {
144
160
  const entries = params.rows
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greprag",
3
- "version": "5.78.7",
3
+ "version": "5.79.0",
4
4
  "description": "Private GrepRAG client for existing Claude Code, Codex, OpenCode, and Grok Build tenants.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -2,21 +2,23 @@
2
2
 
3
3
  **Grok Build?** Stop. Run `greprag load grok-chip-spawn`. This entry is Claude `spawn_task`.
4
4
 
5
- The `pre-spawn-check` PreToolUse hook **validates** chip prompts at the spawn boundary, but **cannot inject content** — `modifiedInput` is silently dropped by the CCD harness for MCP `spawn_task` calls (see `adr/spawn-task-hook-mode.md` 2026-05-27 entry). The agent writes Block 1 + Block 2 into the prompt itself. Validator rejects with `permissionDecision: deny` and a remediation reason if Block 1 or Block 2 markers are missing.
5
+ The `pre-spawn-check` PreToolUse hook **validates** chip prompts at the spawn boundary, but **cannot inject content** — `modifiedInput` is silently dropped by the CCD harness for MCP `spawn_task` calls (see `adr/spawn-task-hook-mode.md` 2026-05-27 entry). The agent writes Block 1 into the prompt itself. Validator rejects with `permissionDecision: deny` and a remediation reason if the `Chip: ` title prefix or the Block 1 worktree marker is missing.
6
+
7
+ **Chips talk over native `SendMessage`, not greprag.** Claude Code addresses live sessions by name on this machine — `ListAgents` to discover, `SendMessage to: "<name>"` to send, delivered at the recipient's next tool boundary. The parent gets native start and completion notifications for every chip it spawns. So there is no IN-FLIGHT ping, no `greprag send` report-back, and **no watcher to arm**. greprag's role in the chip loop is now content only (the `fix list` pre-pull below, and `fix spawn` as mission generator).
6
8
 
7
9
  > **Part of a multi-chip mission?** If this chip is one of ≥2 aimed at a single objective, you should already be inside a chip-leader plan — your **base branch** and **merge target** (the integration branch, *never* master) come from it. If you're not, stop and run `greprag load chip-leader` first. A lone chip targeting its own objective proceeds here directly.
8
10
 
9
11
  ## What the agent provides
10
12
 
11
13
  - `title: "Chip: <verb-phrase>"` — `Chip: ` prefix enforced. **Multi-chip mission?** Use the leader-assigned label: `"Chip <Label>: <verb-phrase>"` (Label = `A`/`B`/`C`… per workstream, from chip-leader) — e.g. `"Chip B: Build freshness engine"`. The validator accepts `Chip: `, `Chip A: `, `Chip 1: ` (regex `^Chip( [A-Za-z0-9]+)?: `). The label is the shared handle the leader and chip both use end-to-end (title → report-back self-ID → merge references).
12
- - `prompt:` — Block 1 + task body + Block 2 (templates below).
14
+ - `prompt:` — Block 1 + task body (template below), ending with the report-back line.
13
15
  - `cwd:` — optional, lands the chip in a different project root. Repo paths: `~/.greprag/projects.json`.
14
16
 
15
17
  Add `mode: interactive` as the first line of the task body to pause the chip for human reply (default is autonomous).
16
18
 
17
- ## Block 1 — Setup (verbatim, substitute `<slug>`, `<handle>` + parent session id)
19
+ ## Block 1 — Setup (verbatim, substitute `<slug>`)
18
20
 
19
- `<slug>` = title slugified (e.g. `Chip: Fix synthesis loop` → `fix-synthesis-loop`). `<8-hex-parent>` = first 8 hex of YOUR session id (printed by SessionStart as "greprag session id: ..."). `<handle>` = operator's greprag handle. `<own-session-id>` stays literal — the chip substitutes it from its own SessionStart output.
21
+ `<slug>` = title slugified (e.g. `Chip: Fix synthesis loop` → `fix-synthesis-loop`). `<parent-name>` = YOUR native session name, the one `ListAgents` prints as "This session is `<name>`" that is the chip's reply address, not a greprag 8-hex.
20
22
 
21
23
  ````
22
24
  **Setup — do this FIRST:**
@@ -39,9 +41,6 @@ else
39
41
  cd ".claude/worktrees/<slug>"
40
42
  fi
41
43
 
42
- greprag send "IN-FLIGHT: chip/<slug> launched — working" \
43
- --to <handle>@greprag.com/<8-hex-parent> --from-session <own-session-id>
44
-
45
44
  # Pristine worktrees check out source only — if the repo ships a worktree
46
45
  # bootstrap (local dependency bootstrap + gitignored dist/ build), run it now
47
46
  # or cross-package tests fail with a cryptic MODULE_NOT_FOUND. No-op in repos
@@ -49,7 +48,7 @@ greprag send "IN-FLIGHT: chip/<slug> launched — working" \
49
48
  if [ -f scripts/worktree-bootstrap.cjs ]; then node scripts/worktree-bootstrap.cjs; fi
50
49
  ```
51
50
 
52
- Your parent's session id is `<8-hex-parent>`. The `IN-FLIGHT` ping is non-negotiable without it the parent assumes the chip card was never launched.
51
+ Your parent session is named `<parent-name>`. When you finish, report to it with native `SendMessage` (`to: "<parent-name>"`): the commit hash, the branch `chip/<slug>`, and a one-line result.
53
52
 
54
53
  ---
55
54
  ````
@@ -62,40 +61,26 @@ greprag session retitle <your-own-8hex> "Chip <Label> — <workstream>"
62
61
 
63
62
  The leader dictates the exact string (e.g. `Chip A — Converter format coverage`); substitute your own 8-hex from SessionStart. This force-sets the registry title so the watcher list + Discord `/switch` line up with the leader's labels. Single chips skip this.
64
63
 
65
- ## Block 2 — Report back (verbatim, substitute `<slug>` + parent session id)
64
+ ## Report back native `SendMessage`
66
65
 
67
- Handle is the operator's greprag handle (from `~/.greprag/identity.json`, field `handle` — strip the `@greprag.com` suffix).
66
+ Close the task body with this (substitute `<slug>` + `<parent-name>`):
68
67
 
69
68
  ````
70
69
  ---
71
70
 
72
- **Block 2 Report back via greprag inbox:**
71
+ **Report back:** when the work is committed, send your result to the parent with native `SendMessage`:
73
72
 
74
- ```bash
75
- greprag send "<status>: <commit hash> on chip/<slug> <one-line>" \
76
- --to <handle>@greprag.com/<8-hex-parent> \
77
- --from-session <own-session-id> \
78
- --artifact commit:<hash>
79
- ```
73
+ - `to`: `"<parent-name>"`
74
+ - `message`: status, commit hash, branch `chip/<slug>`, and a one-line summary.
80
75
 
81
- Substitute `<own-session-id>` from your SessionStart hook output.
76
+ The parent is a live session on this machine no greprag address, no watcher. If `SendMessage` errors because the parent has ended, stop and leave the commit on the branch; the parent picks it up at merge.
82
77
 
83
78
  **Cleanup discipline (HARD RULE):** chip prompts forbid `git clean`, `git reset --hard`, `git worktree remove`, `git checkout <other>`, raw `rm -rf` outside the worktree's tracked files.
84
79
  ````
85
80
 
86
- ## After spawning — arm YOUR OWN watcher (HARD RULE)
87
-
88
- The chip reports back to **your** session id via `greprag send`. Nothing else reliably arms you to receive it: the SessionStart arm-directive and the turn-2 UserPromptSubmit fallback are agent-choice and may not fire, and a report that lands in an unwatched inbox sits silently until someone runs `greprag inbox` by hand. So **immediately after `spawn_task` returns, arm your own watcher** — unless you already have one running this session (one watcher covers every chip you spawn; never double-arm):
89
-
90
- Use the **registered** form — copy the exact command the SessionStart / UserPromptSubmit arm hook prints **verbatim**. It already carries `--owner-pid <claude-pid>`, which only the hook can resolve (the hook is a live descendant of claude.exe; your shell is not):
91
-
92
- ```
93
- Monitor (persistent:true): `greprag inbox watch --session <your-8hex> --json --owner-pid <pid>`
94
- ```
95
-
96
- `<your-8hex>` = your own session id (from SessionStart). **Do NOT hand-fill `--owner-pid` from a shell PID** — the reaper reads `--owner-pid` as "the claude.exe that owns me," so a non-claude PID there reads as a dead owner and the watch is reaped. **Do NOT add `--monitor-pid` / `read … /proc/$$/winpid`** — that form was reverted 2026-06-11 (it stamped the transient arm-line bash, not the Monitor task, and false-killed live watchers); the flag is dead code now. **Do NOT use a bare `while true; greprag inbox watch …` loop** — it never registers in the desk-line, so the arm-probe reports you "NOT armed" and other sessions can't see you as live-reachable. Simplest and correct: **paste the arm hook's printed command verbatim** — its `--owner-pid` is the only one the reaper trusts. (A watch armed with no `--owner-pid` at all is fine too — it is kept, never false-killed — it just isn't owner-pid-reapable if hard-killed.) Arming is what makes the chip's "done" report surface live instead of being lost — the spawn is only half the loop.
81
+ ## After spawning — do nothing
97
82
 
98
- **Launch state:** the Block 1 `IN-FLIGHT` ping is your only signal the operator actually clicked the chip card. No ping = assume the card is still unpresseddon't wait on results from a chip that was never launched.
83
+ No watcher. No arming. The harness tells you when the operator starts the chip and again when it ends, and the chip's own `SendMessage` lands at your next tool boundary. Never arm a `Monitor` on greprag's inbox for chip traffic the inbox watcher exists for Discord, inbound email, and cold opens, not for chips.
99
84
 
100
85
  ## Before composing
101
86