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
@@ -102,6 +102,7 @@ const procedure_1 = require("./procedure");
102
102
  const procedure_runtime_1 = require("./procedure-runtime");
103
103
  const opencode_plugin_png_1 = require("./opencode-plugin-png");
104
104
  const skill_activation_manifest_1 = require("./skill-activation-manifest");
105
+ const native_adapter_refresh_1 = require("./native-adapter-refresh");
105
106
  // ============================================================================
106
107
  // File-based debug log. opencode's plugin loader (Bun) does NOT route plugin
107
108
  // stderr into the opencode log file at ~/.local/share/opencode/log/*.log —
@@ -587,6 +588,11 @@ async function fetchSkillActivation(apiKey, cwd) {
587
588
  const data = await response.json();
588
589
  const rows = data.skills || [];
589
590
  cachedSkillRows = { projectId: anchor.projectId, rows, fetchedAt: now };
591
+ // Installed adapters follow canon, checked against this same roster.
592
+ // adr: adr/skill-bundle-shape.md
593
+ await (0, native_adapter_refresh_1.refreshNativeAdapters)({
594
+ platform: 'opencode', rows, apiUrl: API_URL, apiKey, homeDir: os.homedir(), projectDir: cwd,
595
+ }).catch(() => undefined);
590
596
  return (0, skill_activation_manifest_1.activationFromApiRows)({
591
597
  rows,
592
598
  platform: 'opencode',
@@ -42,11 +42,15 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  })();
43
43
  Object.defineProperty(exports, "__esModule", { value: true });
44
44
  exports.readNativeSkillNames = readNativeSkillNames;
45
+ exports.nativeAdapterGap = nativeAdapterGap;
45
46
  exports.buildMirroredSkillActivation = buildMirroredSkillActivation;
46
47
  exports.nativeAdapterInstallCommand = nativeAdapterInstallCommand;
48
+ exports.localCopyCutoverCommand = localCopyCutoverCommand;
47
49
  exports.activationFromApiRows = activationFromApiRows;
48
50
  const fs = __importStar(require("fs"));
49
51
  const path = __importStar(require("path"));
52
+ const skill_landing_1 = require("./skill-landing");
53
+ const skill_mirror_files_1 = require("./skill-mirror-files");
50
54
  const MAX_NATIVE_SKILL_FILES = 1_500;
51
55
  const MAX_SCAN_DEPTH = 7;
52
56
  function homeRoots(homeDir, platform) {
@@ -89,11 +93,42 @@ function frontmatterName(content) {
89
93
  const name = fm && /(?:^|\r?\n)name:\s*["']?([^\r\n"']+)/.exec(fm[1]);
90
94
  return name?.[1]?.trim() || '';
91
95
  }
92
- /** Bounded native-skill census. A directory basename is always accepted; a
93
- * frontmatter name is added when present. Missing/unreadable roots are silent. */
96
+ /** Bounded native-skill census: every name the harness lists, and whether the
97
+ * package under it is a generated adapter. A directory basename is always
98
+ * accepted; a frontmatter name is added when present. A folder holding a
99
+ * SKILL.md is one package: folders below it are its own files (docs, a
100
+ * published copy), never skills the harness lists. A linked folder is read
101
+ * through its link and recorded 'external', never descended; so is a package
102
+ * another installer owns (isSkillFolderOwnedElsewhere). Roots are read in
103
+ * the harness's precedence order (Claude Code runs a personal skill over a
104
+ * project one of the same name), and the first root that lists a name decides
105
+ * its kind. Within one root a name stays 'local' once any local package claims
106
+ * it, and 'external' outranks 'adapter'. Missing/unreadable roots are silent. */
94
107
  function readNativeSkillNames(params) {
95
- const names = new Set();
108
+ const names = new Map();
109
+ let rootNames = new Map();
110
+ const rank = { adapter: 0, external: 1, local: 2 };
111
+ const note = (name, kind) => {
112
+ const held = rootNames.get(name);
113
+ if (!held || rank[kind] > rank[held])
114
+ rootNames.set(name, kind);
115
+ };
96
116
  let visited = 0;
117
+ const readPackage = (dir, fileName, linked) => {
118
+ visited++;
119
+ let content = '';
120
+ try {
121
+ content = fs.readFileSync(path.join(dir, fileName), 'utf8');
122
+ }
123
+ catch { /* directory name still gives a useful identity */ }
124
+ const kind = linked || (0, skill_mirror_files_1.isSkillFolderOwnedElsewhere)(dir)
125
+ ? 'external'
126
+ : (0, skill_mirror_files_1.isGeneratedSkillAdapter)(content) ? 'adapter' : 'local';
127
+ note(path.basename(dir).toLowerCase(), kind);
128
+ const declared = frontmatterName(content);
129
+ if (declared)
130
+ note(declared.toLowerCase(), kind);
131
+ };
97
132
  const walk = (dir, depth) => {
98
133
  if (depth > MAX_SCAN_DEPTH || visited >= MAX_NATIVE_SKILL_FILES)
99
134
  return;
@@ -104,41 +139,74 @@ function readNativeSkillNames(params) {
104
139
  catch {
105
140
  return;
106
141
  }
142
+ const skillMd = depth > 0
143
+ ? entries.find(entry => entry.isFile() && entry.name.toLowerCase() === 'skill.md')
144
+ : undefined;
145
+ if (skillMd) {
146
+ readPackage(dir, skillMd.name, false);
147
+ return;
148
+ }
107
149
  for (const entry of entries) {
108
150
  if (visited >= MAX_NATIVE_SKILL_FILES)
109
151
  return;
110
- const full = path.join(dir, entry.name);
111
- if (entry.isDirectory()) {
112
- walk(full, depth + 1);
113
- continue;
152
+ const child = path.join(dir, entry.name);
153
+ if (entry.isDirectory())
154
+ walk(child, depth + 1);
155
+ else if ((0, skill_mirror_files_1.isLinkedSkillFolder)(child) && fs.existsSync(path.join(child, 'SKILL.md'))) {
156
+ readPackage(child, 'SKILL.md', true);
114
157
  }
115
- if (!entry.isFile() || entry.name.toLowerCase() !== 'skill.md')
116
- continue;
117
- visited++;
118
- names.add(path.basename(path.dirname(full)).toLowerCase());
119
- try {
120
- const declared = frontmatterName(fs.readFileSync(full, 'utf8'));
121
- if (declared)
122
- names.add(declared.toLowerCase());
123
- }
124
- catch { /* directory name still gives a useful identity */ }
125
158
  }
126
159
  };
127
160
  const roots = [
128
161
  ...homeRoots(params.homeDir, params.platform),
129
162
  ...repoRoots(params.cwd, params.platform),
130
163
  ];
131
- for (const root of roots)
164
+ for (const root of roots) {
165
+ rootNames = new Map();
132
166
  walk(root, 0);
167
+ for (const [name, kind] of rootNames)
168
+ if (!names.has(name))
169
+ names.set(name, kind);
170
+ }
133
171
  return names;
134
172
  }
173
+ /** Mirrored skills this harness does not run from canon. `missing` have no
174
+ * native package, so they never reach the harness's own skill list.
175
+ * `localCopies` are listed from a hand-kept package instead of an adapter, so
176
+ * the harness keeps running that copy after canon moves on. Only Claude Code
177
+ * reports them: its targeted cutover is the one writer that replaces a global
178
+ * package GrepRAG does not own, while the Codex/OpenCode sync leaves such
179
+ * packages in place. An external package is neither: the harness lists it, and
180
+ * its owner (a link target or another installer) is not GrepRAG. A bundled skill is never a gap; its canon is the bundle
181
+ * the installer writes as a real body. adr: adr/skill-bundle-shape.md */
182
+ function nativeAdapterGap(platform, mirrored, native) {
183
+ const missing = [];
184
+ const localCopies = [];
185
+ if (!platform)
186
+ return { missing, localCopies };
187
+ for (const { skillName, scope } of mirrored) {
188
+ if (!skillName || skill_landing_1.BUNDLED_SKILLS.includes(skillName))
189
+ continue;
190
+ const kind = native.get(skillName.toLowerCase());
191
+ if (!kind)
192
+ missing.push(skillName);
193
+ else if (kind === 'local' && platform === 'claude-code' && scope !== 'project')
194
+ localCopies.push(skillName);
195
+ }
196
+ return { missing, localCopies };
197
+ }
135
198
  /** Remove native-advertised duplicates while retaining the canonical count. */
136
- function buildMirroredSkillActivation(entries, nativeNames) {
199
+ function buildMirroredSkillActivation(entries, native, platform) {
137
200
  const valid = entries.filter(entry => entry.skillName && entry.triggerDescription);
138
201
  if (valid.length === 0)
139
202
  return undefined;
140
- const skills = valid.filter(entry => !nativeNames.has(entry.skillName.toLowerCase()));
141
- return { count: valid.length, skills, nativeCount: valid.length - skills.length };
203
+ const skills = valid.filter(entry => !native.has(entry.skillName.toLowerCase()));
204
+ return {
205
+ count: valid.length,
206
+ skills,
207
+ nativeCount: valid.length - skills.length,
208
+ ...nativeAdapterGap(platform, valid, native),
209
+ };
142
210
  }
143
211
  /** The one command that materializes native adapters for the named mirrored
144
212
  * skills on this harness. claude-code is not swept wholesale (adapters there
@@ -155,6 +223,13 @@ function nativeAdapterInstallCommand(platform, missingNames) {
155
223
  }
156
224
  return null;
157
225
  }
226
+ /** The command that replaces one hand-kept copy with its adapter, backing up
227
+ * the copy's SKILL.md first. Replacing a live copy is a per-skill call, so the
228
+ * command names one placeholder rather than the whole list. Null when there
229
+ * are none. */
230
+ function localCopyCutoverCommand(platform, localCopies) {
231
+ return localCopies.length > 0 ? nativeAdapterInstallCommand(platform, ['<name>']) : null;
232
+ }
158
233
  /** Convert the mirror API response into a harness-specific activation signal. */
159
234
  function activationFromApiRows(params) {
160
235
  const entries = params.rows
@@ -164,6 +239,7 @@ function activationFromApiRows(params) {
164
239
  skillName: row.skillName,
165
240
  description: typeof row.description === 'string' ? row.description : '',
166
241
  triggerDescription: row.triggerDescription,
242
+ scope: typeof row.scope === 'string' ? row.scope : undefined,
167
243
  }));
168
- return buildMirroredSkillActivation(entries, readNativeSkillNames(params));
244
+ return buildMirroredSkillActivation(entries, readNativeSkillNames(params), params.platform);
169
245
  }
@@ -170,6 +170,12 @@ function landBriefcase(gain, cwd, homeDir, projectName) {
170
170
  const dir = resolveSkillDir(gain.skill, cwd, homeDir);
171
171
  if (!dir)
172
172
  return 'deferred';
173
+ // A linked skill folder belongs to its target (a tracked repo file, another
174
+ // installer's store), so the fuse is not GrepRAG's to write there. The gain
175
+ // stays pending; a mirrored skill still gets it from `greprag load`.
176
+ // adr: adr/skill-fuse-delivery.md
177
+ if ((0, skill_mirror_files_1.isSkillFolderOwnedElsewhere)(dir))
178
+ return 'deferred';
173
179
  const skillMd = path.join(dir, 'SKILL.md');
174
180
  let body;
175
181
  try {
@@ -35,8 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.computeSkillFileHash = computeSkillFileHash;
37
37
  exports.isGeneratedSkillAdapter = isGeneratedSkillAdapter;
38
+ exports.isLinkedSkillFolder = isLinkedSkillFolder;
39
+ exports.isSkillFolderOwnedElsewhere = isSkillFolderOwnedElsewhere;
38
40
  exports.isSafeMirroredSkillFile = isSafeMirroredSkillFile;
39
41
  const crypto = __importStar(require("node:crypto"));
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
40
44
  function computeSkillFileHash(files) {
41
45
  const hash = crypto.createHash('sha256');
42
46
  for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
@@ -61,6 +65,61 @@ function isGeneratedSkillAdapter(skillMdBody) {
61
65
  const body = skillMdBody.replace(/^?---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
62
66
  return body.slice(0, 400).includes(GENERATED_ADAPTER_MARKER);
63
67
  }
68
+ /** Harness folders whose `skills` folder holds installed copies. */
69
+ const INSTALLED_SKILL_HARNESS_DIRS = ['.claude', '.agents', '.codex', '.opencode', 'opencode'];
70
+ /** True when a skill folder is a link (symlink or Windows junction), live or
71
+ * broken. A linked folder belongs to whatever it points at, often a tracked
72
+ * repo file or another installer's store.
73
+ *
74
+ * adr: adr/skill-bundle-shape.md */
75
+ function isLinkedSkillFolder(dir) {
76
+ try {
77
+ return fs.lstatSync(dir).isSymbolicLink();
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ }
83
+ /** True when another owner holds a skill folder: it is a link, its SKILL.md
84
+ * names the repository that publishes it, or `npx skills` installed it. A
85
+ * published package (OpenWriter's `openwriter setup`, GrepRAG's own bundle)
86
+ * is copied in by its product's installer from a release, and that installer
87
+ * rewrites it on upgrade. `npx skills` records every package it installs in
88
+ * `~/.agents/.skill-lock.json` and copies it into `~/.agents/skills` and
89
+ * `~/.claude/skills`. Both rules read only installed copies, in a harness's
90
+ * `skills` folder: a product's own source tree (GrepRAG's
91
+ * `packages/cli/skill/<name>`) names its repository too and is its author's
92
+ * to edit. The census reads such a folder as external, and no GrepRAG writer
93
+ * writes, replaces or prunes it. A generated adapter is always GrepRAG's own.
94
+ *
95
+ * adr: adr/skill-bundle-shape.md */
96
+ function isSkillFolderOwnedElsewhere(dir) {
97
+ if (isLinkedSkillFolder(dir))
98
+ return true;
99
+ const root = path.dirname(path.resolve(dir));
100
+ const harnessDir = path.basename(path.dirname(root));
101
+ if (path.basename(root) !== 'skills' || !INSTALLED_SKILL_HARNESS_DIRS.includes(harnessDir))
102
+ return false;
103
+ let skillMd = '';
104
+ try {
105
+ skillMd = fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8');
106
+ }
107
+ catch { /* no package yet */ }
108
+ if (isGeneratedSkillAdapter(skillMd))
109
+ return false;
110
+ const frontmatter = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd)?.[1] || '';
111
+ if (/^\s*repository:\s*\S/m.test(frontmatter))
112
+ return true;
113
+ if (harnessDir !== '.claude' && harnessDir !== '.agents')
114
+ return false;
115
+ try {
116
+ const lock = JSON.parse(fs.readFileSync(path.join(path.dirname(path.dirname(root)), '.agents', '.skill-lock.json'), 'utf8'));
117
+ return Boolean(lock?.skills && Object.prototype.hasOwnProperty.call(lock.skills, path.basename(dir)));
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ }
64
123
  function isSafeMirroredSkillFile(file) {
65
124
  if (file.path === 'SKILL.md')
66
125
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greprag",
3
- "version": "5.82.1",
3
+ "version": "5.83.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": {
@@ -7,7 +7,10 @@
7
7
  // immediately after installing the npm package.
8
8
  // * Other bundled skills — refresh-only. If the operator opted in earlier,
9
9
  // keep them current; otherwise leave their skill registry alone.
10
- // * Claude chip-spawn doc — refresh-only, because it is Claude-specific.
10
+ //
11
+ // The installed Claude chip-spawn doc (~/.claude/docs/chip-spawn.md) is retired:
12
+ // `greprag load chip-spawn` serves that doctrine from the package and cannot
13
+ // drift. Refreshing a leftover copy here kept a retired surface alive.
11
14
  //
12
15
  // OpenCode has no slash-skill directory in this package; `greprag init
13
16
  // --opencode` installs the plugin.
@@ -24,15 +27,11 @@ const platformTargets = [
24
27
  name: 'Claude Code',
25
28
  agentDir: path.join(home, '.claude'),
26
29
  skillsTarget: path.join(home, '.claude', 'skills'),
27
- docsTarget: path.join(home, '.claude', 'docs'),
28
- refreshClaudeDocs: true,
29
30
  },
30
31
  {
31
32
  name: 'Codex',
32
33
  agentDir: path.join(home, '.codex'),
33
34
  skillsTarget: path.join(home, '.codex', 'skills'),
34
- docsTarget: path.join(home, '.codex', 'docs'),
35
- refreshClaudeDocs: false,
36
35
  },
37
36
  ];
38
37
 
@@ -76,20 +75,6 @@ try {
76
75
  console.error(' greprag postinstall: skill sync skipped (' + err.message + ')');
77
76
  }
78
77
 
79
- const chipSpawnSrc = path.join(skillRoot, 'templates', 'chip-spawn.md');
80
- try {
81
- for (const target of platformTargets) {
82
- if (!target.refreshClaudeDocs) continue;
83
- const chipSpawnDest = path.join(target.docsTarget, 'chip-spawn.md');
84
- if (fs.existsSync(chipSpawnSrc) && fs.existsSync(chipSpawnDest)) {
85
- fs.copyFileSync(chipSpawnSrc, chipSpawnDest);
86
- refreshed.push(`${target.name} docs/chip-spawn.md`);
87
- }
88
- }
89
- } catch (err) {
90
- console.error(' greprag postinstall: chip-spawn refresh skipped (' + err.message + ')');
91
- }
92
-
93
78
  // The Codex startup watcher was retired in v5.x, but upgrades left its login
94
79
  // files in place. On Windows the leftover script loops every 5s, reruns the
95
80
  // retired command, and appends its error to codex-watch.log forever (600 MB
@@ -96,10 +96,10 @@ Omit `storeIds` (or use `--all`) to search every store under the calling tenant.
96
96
  ## Walk
97
97
 
98
98
  ```bash
99
- greprag corpus walk "<store>" <nodeId> [--before 2 --after 2]
99
+ greprag corpus walk "<store>" <nodeId> [--before 2 --after 2] [--max-chars N]
100
100
  ```
101
101
 
102
- Surrounding-context fetch. ~50ms, no LLM.
102
+ Surrounding-context fetch. ~50ms, no LLM. `walk` and `section` print each node's full text (search snippets stay short); `--max-chars N` is the opt-in cut.
103
103
 
104
104
  ## List / delete / status
105
105
 
@@ -145,7 +145,7 @@ Append missing entries to the existing arrays in `~/.claude/settings.json` (crea
145
145
  ```json
146
146
  { "matcher": "mcp__ccd_session__spawn_task", "hooks": [{ "type": "command", "command": "greprag-hook pre-spawn-check", "timeout": 3000 }] }
147
147
  ```
148
- Fires before every `spawn_task` chip dispatch. Validates against `~/.claude/CLAUDE.md § Chip Spawning`: title prefix `Chip: `, Block 1 (`git worktree add` in prompt), Block 2 (`greprag send` in prompt). Returns `permissionDecision: "deny"` with remediation on failure; the agent re-composes.
148
+ Fires before every `spawn_task` chip dispatch. Validates against `greprag load chip-spawn`: title prefix `Chip: ` and the current Block 1 (its `git branch -m` step that keeps the app-provided worktree and its `git worktree add` local-start step). Returns `permissionDecision: "deny"` with remediation on failure; the agent re-composes.
149
149
 
150
150
  The legacy `user_prompt_submit_notify` hook was removed in v5.6.1 — live inbox delivery is now the Monitor watcher's job. Do not install it.
151
151
 
@@ -19,8 +19,7 @@ license: MIT
19
19
 
20
20
  # Mechanic
21
21
 
22
- > **Memory-first friction reflex (refined 2026-07-28, grepragOS laws 3–4 —
23
- > supersedes the routing/queueing reflex below wherever they conflict).** Search
22
+ > **Memory-first friction reflex (grepragOS laws 3–4).** Search
24
23
  > Memory for the exact error/friction plus repo/tool first. Apply a clear
25
24
  > remembered overcome and continue. Only when Memory has no answer, the overcome
26
25
  > fails, or the same friction repeats does
@@ -30,12 +29,9 @@ license: MIT
30
29
  > fix → explain the friction and fix in human terms → commit useful work → hand it to
31
30
  > the mission owner. A full-goal chip with no live parent follows
32
31
  > `.greprag/delivery.json` as mission owner; no second lifecycle approval is
33
- > introduced. `greprag mechanic friction` (routing to
34
- > a live Mechanic) and `greprag fix log` (queueing) are RETIRED as reflexes —
35
- > `fix log` survives for audit/design-input notes; this skill's queue-digestion
36
- > method still applies to the EXISTING backlog and to audit notes. A chip titled
37
- > `FIX: <friction>` follows this contract; legacy `MECHANIC: <Mission>` chips
38
- > read the same way.
32
+ > introduced. `greprag fix log` is for audit/design-input notes only; this
33
+ > skill's queue-digestion method applies to the existing backlog and to audit
34
+ > notes. A chip titled `FIX: <friction>` follows this contract.
39
35
 
40
36
  **FIX type router.** Type means durable repair surface, not symptom:
41
37
  `harness` = hooks/watchers/task dispatch/harness behavior; `doctrine` =
@@ -69,7 +65,7 @@ The seam: **author freely (shadow), arm deliberately (operator).** Full doctrine
69
65
  - **Phase 0 — Fix digestion.** Census the queue tenant-wide FIRST (queues silo per project), then drain the chosen one: per fix, repair the broken mechanism (hooks first, code second) and/or record the durable gotcha, then retire. Deliberate + auto signals outrank mined ones, so this runs first.
70
66
  - **Phase A — Drift check.** Audit standing fixes for rot: stale paths, dead conventions (derived live), misfiled static knowledge, age.
71
67
  - **Phase B — Episodic mining.** Mine episodic prose summaries for friction worth a fix.
72
- - **Phase C — Global promotion.** Promote project-agnostic repairs to `~/.claude/docs/chip-spawn.md`.
68
+ - **Phase C — Global promotion.** Promote project-agnostic repairs into the chip-spawn template (`packages/cli/skill/templates/chip-spawn.md`), the one copy `greprag load chip-spawn` serves.
73
69
 
74
70
  ## The "worth repairing?" gate (core discipline)
75
71
 
@@ -86,7 +82,7 @@ Same gate the hourly compactor applies at write-time — Mechanic and compactor
86
82
  - **Tier-2 actions need explicit confirm** (arm/graduate, deny/transform, secrets·money·external communication, deleting claimed user work). Ordinary profile-declared repo delivery does not. Tier 0/1 just do + report.
87
83
  - **Removal-first repair.** Remove the offending text, gate, control, duplicated doctrine, or wrong boundary before adding new guidance. Add prose or controls only when no removal action can solve the friction; never wrap bad doctrine in explanatory caveats when deleting the bad boundary fixes it.
88
84
  - **Bulk is fine for Tier-0 when the set is named aloud** (clearing verified-stale / noise fixes); never bulk a Tier-2 action.
89
- - **Never edit `~/.claude/docs/chip-spawn.md` without confirm.** Global rules are sticky.
85
+ - **Never promote into the chip-spawn template without confirm.** Global rules are sticky.
90
86
  - **Cross-project carve-out.** Default to the current project (resolved via `greprag fix list`). The Phase 0 census MAY read sibling queues to show where fixes piled up; *draining* one requires the operator to name it, and every `add`/`delete` against it MUST carry `--project <that-project>`. Never touch a sibling silently.
91
87
  - **Harness scope is mandatory.** Friction carries its source harness (`claude-code`, `codex`, `opencode`). Repairs to hooks/skills/watchers must target that harness with `greprag mechanic install ... --harness <name>`. In a Codex session, default to `--harness codex`; use `--harness all` only for deliberately portable mechanisms.
92
88
  - **Delivery is committed-state convergence.** Before merging, pushing, deploying, publishing, tagging, or cutting a release, send one fire-and-forget notice to same-repo peers and continue immediately. Recipients reply only for a concrete conflict; silence is approval. Never poll, wait, inspect peer status, or follow up. Sweep every ready same-repo commit and ignore all uncommitted and untracked work. Resolve Git conflicts directly; no deploy lease or peer ownership ceremony exists.
@@ -157,7 +153,7 @@ mechanism and/or **record** the durable gotcha — and runs FIRST.
157
153
 
158
154
  - **Is there a broken MECHANISM?** Repair it on the lowest rung that holds:
159
155
  1. **Hooks first.** A PreToolUse guard, a UserPromptSubmit injection, a notification — same machinery as fix-triggers, ships as DATA, no deploy. Most mechanism fixes repair here.
160
- 2. **Code second.** Only when a hook can't express it: server logic, a bounce, a schema/CLI change. Don't implement code-tier inline — spawn a chip (`spawn_task`, `~/.claude/docs/chip-spawn.md`) or hand to the operator.
156
+ 2. **Code second.** Only when a hook can't express it: server logic, a bounce, a schema/CLI change. Don't implement code-tier inline — spawn a chip (`spawn_task`, method from `greprag load chip-spawn`) or hand to the operator.
161
157
  3. **Lock it (regression rung).** A code/mechanism repair MUST also mint a regression test from the REAL failure signal (the smell's evidence / a repro from the trace), so the *behavior* is locked, not just the lore. The queue's product is **fix + lore + locked test** — an ADR locks the *why* in prose; this locks the *what* in an executable. Don't retire the fix until the test exists (fails without the repair, passes with it).
162
158
 
163
159
  Recurring failure class → route through `/root-cause`: repair the pattern, never guard today's trigger.
@@ -217,38 +213,29 @@ mechanism and/or **record** the durable gotcha — and runs FIRST.
217
213
 
218
214
  Episodic memory is prose-only (the claim layer was dropped, 2026-06-08) — mine the daily/hourly **prose summaries** for friction worth recording. ~90% of raw learnings fail the gate; that's expected.
219
215
 
220
- 1. `greprag project-id` → `projectId`.
221
- 2. Pull the last 30 days of daily summaries:
222
- ```bash
223
- FROM=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
224
- TO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
225
- curl -sf "https://api.greprag.com/v1/memory/by-period?projectId=<projectId>&type=daily&from=$FROM&to=$TO&limit=30" \
226
- -H "Authorization: Bearer $GREPRAG_API_KEY"
227
- ```
228
- Parse `rows[]` (`created_at`, `body`).
229
- 3. Scan the prose for durable-property sentences. Markers: `learned that`, `the gotcha is`, `won't work because`, `turns out`, `requires`, `only … if`, `caps at`, `blocks`. Skip < 30 or > 300 chars.
230
- 4. **Gate every candidate.** Surface only passers, one at a time:
216
+ 1. Pull the last 30 daily summaries: `greprag memory daily --last 30 --format json` (add `--project <name>` for a sibling). Parse `rows[]` (`content`).
217
+ 2. Read each summary and pull sentences that state a durable property (a cause, a limit, a gotcha).
218
+ 3. **Gate every candidate.** Surface only passers, one at a time:
231
219
  ```
232
220
  Candidate (durable · reusable · non-obvious):
233
221
  "<sentence>"
234
222
  Record as a fix? (rec)ord / (e)dit / (s)kip
235
223
  ```
236
224
  record → ask scope (`general`; `env` for creds/vars; `<subsystem>-touch`) → `greprag fix log "<sentence>" --repaired --scope <scope>`. edit → polish to a clean rule first.
237
- 5. Summary: `Phase B: scanned N summaries, M passed the gate. Recorded A. Skipped S.`
225
+ 4. Summary: `Phase B: scanned N summaries, M passed the gate. Recorded A. Skipped S.`
238
226
 
239
227
  ## Phase C — Global promotion
240
228
 
241
229
  1. Re-pull `greprag fix list --repaired --format json`.
242
230
  2. Classify each: **project-specific** (skip — mentions `packages/`, a table, an internal service) vs **project-agnostic** (universal only — `worktree`, `npm`, `git`, `Claude Code`, `hook`, `~/.claude/`, generic env vars).
243
- 3. Before showing an agnostic candidate, verify `~/.claude/docs/chip-spawn.md` and `packages/cli/skill/templates/chip-spawn.md` are in sync (`diff`); if they differ, STOP Phase C and tell the operator to resolve manually.
244
- 4. Present each:
231
+ 3. Present each:
245
232
  ```
246
233
  [<nodeId>] universal-only:
247
234
  "<text>"
248
235
  Promote to global chip-spawn rule? (y)es / (n)o / (e)dit
249
236
  ```
250
- yes → append to BOTH chip-spawn files (under the numbered "Every chip prompt:" list, HARD-RULE styling when apt) → `greprag fix delete <nodeId>` → confirm `Promoted [<nodeId>] → global.`
251
- 5. Summary: `Phase C: evaluated N. Promoted P. Skipped S.`
237
+ yes → add it to `packages/cli/skill/templates/chip-spawn.md` in the greprag repo (HARD-RULE styling when apt) → `greprag fix delete <nodeId>` → confirm `Promoted [<nodeId>] → global.`
238
+ 4. Summary: `Phase C: evaluated N. Promoted P. Skipped S.`
252
239
 
253
240
  ## Report
254
241
 
@@ -278,9 +265,6 @@ Repairs (D3 rows attached to fixes) are managed with `greprag mechanic`, not by
278
265
  greprag mechanic set legacy role marker for mechanic follow-up rails;
279
266
  fan out `mechanic_live` to open sessions
280
267
  greprag mechanic role show whether this project is the live Mechanic
281
- greprag mechanic friction "obs" RETIRED as the reflex (2026-07-12): spawn a fix chip
282
- instead — `greprag fix spawn --type <type> "<unit>"`. The CLI refuses
283
- new working-session use.
284
268
  greprag mechanic reply --to <8hex> typed follow-up from Mechanic to source session;
285
269
  "msg" [--friction-id <id>] do not use generic send for mechanic workflow
286
270
  greprag mechanic status inventory: live (shadow|active) repairs grouped by status,
@@ -293,15 +277,6 @@ greprag mechanic why <nodeId> provenance: the friction → fix → repai
293
277
  greprag mechanic off / on PANIC SWITCH — local file, no network; suspends ALL repairs
294
278
  ```
295
279
 
296
- **Legacy Mechanic role.** A `--mechanic` inbox watcher can still receive
297
- legacy `mechanic_friction` / `mechanic_reply` rows in addition to its own session
298
- lane, but live working sessions no longer route friction there. Laws 3–4 search
299
- Memory first, then route each unresolved unit through
300
- `greprag fix spawn --type <type> "<one unit>"`. `greprag mechanic set` only
301
- fans out a notice that teaches this reflex and preserves the reply rail for old
302
- mechanic threads. The Mechanic replies with `greprag mechanic reply --to <source-session>
303
- "..."` only for legacy follow-up coordination.
304
-
305
280
  **Born shadow → graduate.** Every repair is born `shadow`: it matches and logs would-have-fired
306
281
  evidence but injects nothing. Graduation is evidence-first — when `status` flags a row `⚑`, review
307
282
  its fire history (`why`) and `enable` it. Nothing goes live without the operator; the gates are
@@ -22,7 +22,7 @@ Each: **signature** (how it shows up in the run) · **location** (where it lives
22
22
  ### 1. mis-route
23
23
  - **Signature:** the skill fired for a task outside its lane, OR you reached for it by an obvious phrase and it didn't trigger.
24
24
  - **Location:** the `description` / `Use when:` trigger list, or the `NOT for:` boundary.
25
- - **Fix:** add the missing trigger phrase to `Use when:` (verbatim, the phrase you'd actually type), OR add/sharpen a `NOT for:` line naming the lane it wrongly claimed. One phrase per defect — don't pad the list.
25
+ - **Fix:** broaden or add the intent category the description names (a verbatim phrase only when no category can express it; fold near-synonyms into one category), OR add/sharpen a `NOT for:` line naming the lane it wrongly claimed.
26
26
 
27
27
  ### 2. wrong-altitude
28
28
  - **Signature:** the skill did work it should have delegated (implemented inline when it should hand to a chip; drafted when it should route to a leaf), or the reverse.
@@ -53,6 +53,6 @@ Each: **signature** (how it shows up in the run) · **location** (where it lives
53
53
 
54
54
  - **One friction, one edit.** If a run surfaced two defects, make two separate edits, each shown for its own accept/revert. Never batch.
55
55
  - **Apply + show diff.** Make the edit, then show the unified diff and stop for one accept/revert. (This is the operator-chosen gate; the Mechanic "always confirm" rule still holds.)
56
- - **Bundled skills:** if the target is a greprag bundled skill, author the edit at `packages/cli/skill/<name>/` — never the `~/.claude/skills/<name>/` install artifact (it's overwritten on the next `init`). See greprag `CLAUDE.md`.
56
+ - **Edit the canonical source.** Managed skills: `greprag skill edit <name> --skill-id <id>`; greprag-bundled skills: `packages/cli/skill/<name>/`. Never edit a generated `~/.claude/skills/<name>/SKILL.md` adapter or install artifact.
57
57
  - **Line budget:** a fix must not grow SKILL.md without adding a real proactive-fire rule or per-task step. Converting wallpaper → Convention B is net-neutral or smaller; mis-route adds at most one line.
58
58
  - **Out of taxonomy → spawn, don't improvise.** Anything not matching the six becomes one `greprag fix spawn --type doctrine "<unit>"` chip. `greprag fix log` is only for deliberate audit/design notes.