greprag 5.82.1 → 5.83.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/claude-lifecycle-cutover.js +5 -16
  2. package/dist/commands/codex.js +4 -0
  3. package/dist/commands/collision-check.js +9 -6
  4. package/dist/commands/corpus/client.js +16 -0
  5. package/dist/commands/corpus/index.js +5 -2
  6. package/dist/commands/corpus/section.js +3 -3
  7. package/dist/commands/corpus/walk.js +3 -3
  8. package/dist/commands/delivery-reminder.js +13 -9
  9. package/dist/commands/delivery.js +64 -0
  10. package/dist/commands/docptr.js +10 -24
  11. package/dist/commands/init.js +4 -5
  12. package/dist/commands/load-primer-reminder.js +2 -2
  13. package/dist/commands/load.js +45 -12
  14. package/dist/commands/merge-guard.js +94 -9
  15. package/dist/commands/parity.js +0 -2
  16. package/dist/commands/pipe-wrap.js +4 -2
  17. package/dist/commands/skill-mirror-reminder.js +24 -6
  18. package/dist/commands/skill.js +24 -8
  19. package/dist/commands/skillgain.js +5 -1
  20. package/dist/commands/state-trigger.js +67 -31
  21. package/dist/delivery-config.js +69 -0
  22. package/dist/deploy-locks.js +18 -3
  23. package/dist/doc-mirror-client.js +39 -11
  24. package/dist/hook.js +35 -39
  25. package/dist/index.js +62 -27
  26. package/dist/native-adapter-refresh.js +114 -0
  27. package/dist/native-skill-adoption.js +20 -53
  28. package/dist/native-skill-mirror.js +130 -52
  29. package/dist/native-skill-pending.js +5 -2
  30. package/dist/opencode-plugin.bundle.js +1083 -160
  31. package/dist/opencode-plugin.js +6 -0
  32. package/dist/skill-activation-manifest.js +98 -22
  33. package/dist/skill-landing.js +6 -0
  34. package/dist/skill-mirror-files.js +59 -0
  35. package/package.json +1 -1
  36. package/scripts/postinstall.js +4 -19
  37. package/skill/greprag/SKILL.md +1 -1
  38. package/skill/greprag/docs/corpus.md +2 -2
  39. package/skill/greprag/docs/setup.md +1 -1
  40. package/skill/mechanic/SKILL.md +15 -40
  41. package/skill/mechanic/docs/skill-fix-conventions.md +2 -2
  42. package/skill/templates/chip-spawn.md +36 -20
  43. package/skill/templates/delivery.md +19 -5
@@ -47,6 +47,7 @@ exports.lockSession = lockSession;
47
47
  exports.lockOwnership = lockOwnership;
48
48
  exports.describeLockOwner = describeLockOwner;
49
49
  exports.gitDir = gitDir;
50
+ exports.commonGitDir = commonGitDir;
50
51
  exports.lockPath = lockPath;
51
52
  exports.deployLockPath = deployLockPath;
52
53
  exports.mergeLockPath = mergeLockPath;
@@ -117,9 +118,23 @@ function gitDir(cwd) {
117
118
  throw new Error('not a git checkout');
118
119
  return String(result.stdout).trim();
119
120
  }
120
- /** Locks live inside `.git`, so they are never tree dirt and can never be committed. */
121
+ /** The repository's one shared `.git`, the same directory from every linked worktree.
122
+ * `gitDir` is per-worktree (`.git/worktrees/<name>`), which is right for MERGE_HEAD and
123
+ * wrong for anything two worktrees must agree on: on 2026-10-06 a merge lock acquired
124
+ * from a linked worktree landed in that worktree's private dir, and the merge guard,
125
+ * reading the canonical checkout, refused the holder's own merge. */
126
+ function commonGitDir(cwd) {
127
+ const result = (0, proc_1.safeSpawnSync)('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
128
+ encoding: 'utf8', cwd, stdio: ['ignore', 'pipe', 'pipe'],
129
+ });
130
+ if (result.status !== 0)
131
+ throw new Error('not a git checkout');
132
+ return path.resolve(String(result.stdout).trim());
133
+ }
134
+ /** Locks live inside the shared `.git`, so they are never tree dirt, can never be
135
+ * committed, and name the same file whichever worktree takes or reads them. */
121
136
  function lockPath(cwd, name) {
122
- return path.join(gitDir(cwd), `${PREFIX}-${name}.lock`);
137
+ return path.join(commonGitDir(cwd), `${PREFIX}-${name}.lock`);
123
138
  }
124
139
  function deployLockPath(cwd, target) {
125
140
  return lockPath(cwd, `deploy-${target}`);
@@ -236,7 +251,7 @@ function releaseLock(file) {
236
251
  * recorded a claim and then vanished would otherwise wedge the checkout. */
237
252
  exports.CLAIM_MAX_AGE_MS = 24 * 60 * 60 * 1000;
238
253
  function landedClaimsPath(cwd) {
239
- return path.join(gitDir(cwd), `${PREFIX}-landed.json`);
254
+ return path.join(commonGitDir(cwd), `${PREFIX}-landed.json`);
240
255
  }
241
256
  /** True when `sha` is reachable from HEAD. False for an orphan, a rewritten commit,
242
257
  * or one that never existed — the caller distinguishes those with `commitExists`. */
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  /** Client-side project document mirror helpers.
3
3
  *
4
- * One rule set feeds:
4
+ * One rule set (eligibility + path root) feeds:
5
5
  * - `greprag doc mirror sync` / `greprag doc update`
6
6
  * - Stop-hook touched-doc upserts
7
7
  * - `greprag load` / `greprag status` freshness warnings
8
+ * - the doc-pointer capture, backfill and SessionStart reconcile
8
9
  */
9
10
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
11
  if (k2 === undefined) k2 = k;
@@ -46,6 +47,7 @@ exports.isDocMirrorEligible = isDocMirrorEligible;
46
47
  exports.titleFromMarkdown = titleFromMarkdown;
47
48
  exports.tagsFromPath = tagsFromPath;
48
49
  exports.docContentHash = docContentHash;
50
+ exports.docPathRoot = docPathRoot;
49
51
  exports.collectDocMirrorInventory = collectDocMirrorInventory;
50
52
  exports.collectTouchedDocMirrorDocs = collectTouchedDocMirrorDocs;
51
53
  exports.computeDocMirrorFreshness = computeDocMirrorFreshness;
@@ -67,6 +69,12 @@ function normalizeDocRel(input) {
67
69
  .replace(/\/+/g, '/')
68
70
  .trim();
69
71
  }
72
+ /** The project doc lists (this mirror and the doc pointers) hold project
73
+ * state. Skill trees are procedure that `greprag load`
74
+ * serves itself, so they never enter it: every harness keeps skills under a
75
+ * `skill/` or `skills/` folder (the bundled source and its templates included),
76
+ * and a `SKILL.md` marks a skill package wherever it sits.
77
+ * adr: adr/doc-mirror-state-only.md */
70
78
  function isDocMirrorEligible(relPath) {
71
79
  const p = normalizeDocRel(relPath);
72
80
  if (!p.toLowerCase().endsWith('.md'))
@@ -74,7 +82,7 @@ function isDocMirrorEligible(relPath) {
74
82
  const segments = p.split('/');
75
83
  const base = segments[segments.length - 1] || '';
76
84
  const lowerBase = base.toLowerCase();
77
- if (lowerBase === 'claude.md' || lowerBase === 'memory.md')
85
+ if (lowerBase === 'claude.md' || lowerBase === 'memory.md' || lowerBase === 'skill.md')
78
86
  return false;
79
87
  for (const seg of segments.slice(0, -1)) {
80
88
  if (!seg)
@@ -83,6 +91,8 @@ function isDocMirrorEligible(relPath) {
83
91
  return false;
84
92
  if (seg === 'node_modules' || seg === 'dist' || seg === 'build' || seg === 'vendor')
85
93
  return false;
94
+ if (seg === 'skill' || seg === 'skills')
95
+ return false;
86
96
  }
87
97
  return true;
88
98
  }
@@ -105,6 +115,22 @@ function tagsFromPath(relPath) {
105
115
  function docContentHash(content) {
106
116
  return crypto.createHash('sha256').update(content).digest('hex');
107
117
  }
118
+ /** The folder every project doc path is relative to: the checkout's top level,
119
+ * so a session opened in a subfolder records and reconciles the same paths as
120
+ * one at the root. A folder outside git is its own root.
121
+ * adr: adr/doc-mirror-state-only.md */
122
+ function docPathRoot(cwd) {
123
+ try {
124
+ const out = (0, proc_1.safeExecFileSync)('git', ['rev-parse', '--show-toplevel'], {
125
+ cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
126
+ });
127
+ const root = String(out).trim();
128
+ if (root)
129
+ return path.resolve(root);
130
+ }
131
+ catch { /* not a git checkout */ }
132
+ return path.resolve(cwd);
133
+ }
108
134
  function gitLsFiles(cwd) {
109
135
  try {
110
136
  const out = (0, proc_1.safeExecFileSync)('git', ['ls-files'], {
@@ -152,9 +178,9 @@ function walkMarkdownFiles(cwd, maxDocs = MAX_FILESYSTEM_DOCS) {
152
178
  visit(cwd);
153
179
  return { files, truncated };
154
180
  }
155
- function touchedToRel(cwd, filePath) {
181
+ function touchedToRel(root, cwd, filePath) {
156
182
  const abs = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
157
- const rel = normalizeDocRel(path.relative(cwd, abs));
183
+ const rel = normalizeDocRel(path.relative(root, abs));
158
184
  if (!rel || rel.startsWith('../') || path.isAbsolute(rel))
159
185
  return null;
160
186
  return rel;
@@ -201,20 +227,22 @@ function buildCollection(cwd, relPaths, source, presentForRefs, gitAvailable, tr
201
227
  };
202
228
  }
203
229
  function collectDocMirrorInventory(cwd) {
204
- const tracked = gitLsFiles(cwd);
230
+ const root = docPathRoot(cwd);
231
+ const tracked = gitLsFiles(root);
205
232
  if (tracked) {
206
- return buildCollection(cwd, tracked.filter(isDocMirrorEligible), 'git', new Set(tracked), true);
233
+ return buildCollection(root, tracked.filter(isDocMirrorEligible), 'git', new Set(tracked), true);
207
234
  }
208
- const walked = walkMarkdownFiles(cwd);
209
- return buildCollection(cwd, walked.files, 'filesystem', new Set(), false, walked.truncated);
235
+ const walked = walkMarkdownFiles(root);
236
+ return buildCollection(root, walked.files, 'filesystem', new Set(), false, walked.truncated);
210
237
  }
211
238
  function collectTouchedDocMirrorDocs(filesTouched, cwd) {
212
- const tracked = gitLsFiles(cwd);
239
+ const root = docPathRoot(cwd);
240
+ const tracked = gitLsFiles(root);
213
241
  const presentForRefs = tracked ? new Set(tracked) : new Set();
214
242
  const rels = filesTouched
215
- .map((f) => touchedToRel(cwd, f))
243
+ .map((f) => touchedToRel(root, cwd, f))
216
244
  .filter((f) => !!f && isDocMirrorEligible(f));
217
- return buildCollection(cwd, rels, tracked ? 'git' : 'filesystem', presentForRefs, !!tracked);
245
+ return buildCollection(root, rels, tracked ? 'git' : 'filesystem', presentForRefs, !!tracked);
218
246
  }
219
247
  function computeDocMirrorFreshness(local, remoteDocs) {
220
248
  const remote = new Map();
package/dist/hook.js CHANGED
@@ -91,6 +91,7 @@ const persona_1 = require("./commands/persona");
91
91
  const app_settings_1 = require("./app-settings");
92
92
  const inbox_primer_reminder_1 = require("./commands/inbox-primer-reminder");
93
93
  const skill_activation_manifest_1 = require("./skill-activation-manifest");
94
+ const native_adapter_refresh_1 = require("./native-adapter-refresh");
94
95
  const worktree_state_1 = require("./worktree-state");
95
96
  // Ingress-trigger bridge, Adapter A (LOCAL/state half) — the Stop hook computes
96
97
  // the deterministic state values (stress / turnCount / keyword-seen) from the
@@ -644,37 +645,18 @@ function capField(text) {
644
645
  //
645
646
  // The Document Pointer System's detect half. No new hook: the Stop hook already
646
647
  // sees every file the turn touched (filesTouched), so .md writes ride the same
647
- // event — a deterministic path filter here, the Flash-Lite worthiness judge
648
- // server-side. Best-effort end to end: a docptr failure never blocks a turn.
649
- /** Deterministic pre-filter — the cheap gate BEFORE the LLM judge. Rejects the
650
- * obvious never-index classes so they don't even cost a judge call: non-.md,
651
- * dot-directories (.tmp-*, .claude/, .github/ — harness/scratch, not project
652
- * docs), dependency/build trees, and the harness-owned CLAUDE.md/MEMORY.md
653
- * (always injected anyway — indexing them is circular noise). Judge-worthy
654
- * ambiguity (drafts, beats, plans) is the JUDGE's call, not this filter's. */
655
- function isDocPathEligible(relPath) {
656
- const p = relPath.replace(/\\/g, '/');
657
- if (!p.toLowerCase().endsWith('.md'))
658
- return false;
659
- const segments = p.split('/');
660
- const base = segments[segments.length - 1];
661
- if (base === 'CLAUDE.md' || base === 'MEMORY.md')
662
- return false;
663
- for (const seg of segments.slice(0, -1)) {
664
- if (seg.startsWith('.'))
665
- return false;
666
- if (seg === 'node_modules' || seg === 'dist' || seg === 'build' || seg === 'vendor')
667
- return false;
668
- }
669
- return true;
670
- }
648
+ // event — the doc mirror's eligibility rule here (isDocMirrorEligible: project
649
+ // state only, never skill trees or harness followers), the Flash-Lite worthiness
650
+ // judge server-side. Paths are relative to the checkout root (docPathRoot), the
651
+ // same root the mirror uses. Best-effort end to end: a docptr failure never
652
+ // blocks a turn.
671
653
  /** Head-snippet cap posted to the judge (server re-caps defensively). */
672
654
  const DOC_EVENT_SNIPPET_CHARS = 2_000;
673
655
  /** Max doc events per turn (mirrors the server's MAX_EVENTS_PER_POST). */
674
656
  const DOC_EVENTS_PER_TURN = 8;
675
657
  /** Turn's touched files → judge-ready doc events: project-relative eligible
676
658
  * .md paths + head snippets + a CLIENT-CONFIRMED reference manifest. Files
677
- * outside cwd (scratchpad, other repos) and unreadable files (deleted later in
659
+ * outside the checkout (scratchpad, other repos) and unreadable files (deleted later in
678
660
  * the turn) are skipped silently.
679
661
  *
680
662
  * The manifest is built HERE, on the side that has the tree (docs/doc-pointer-
@@ -684,15 +666,16 @@ const DOC_EVENTS_PER_TURN = 8;
684
666
  * LLM ref-extraction is retired to a back-compat fallback (old CLIs send no
685
667
  * refs). Tree unavailable → empty manifests (docs read fresh — bias-to-keep). */
686
668
  function collectDocEvents(filesTouched, cwd) {
669
+ const root = (0, doc_mirror_client_1.docPathRoot)(cwd);
687
670
  const staged = [];
688
671
  const seen = new Set();
689
672
  for (const abs of filesTouched) {
690
673
  if (staged.length >= DOC_EVENTS_PER_TURN)
691
674
  break;
692
- const rel = path.relative(cwd, abs).replace(/\\/g, '/');
675
+ const rel = path.relative(root, abs).replace(/\\/g, '/');
693
676
  if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
694
677
  continue;
695
- if (!isDocPathEligible(rel) || seen.has(rel))
678
+ if (!(0, doc_mirror_client_1.isDocMirrorEligible)(rel) || seen.has(rel))
696
679
  continue;
697
680
  try {
698
681
  const snippet = fs.readFileSync(abs, 'utf-8').slice(0, DOC_EVENT_SNIPPET_CHARS);
@@ -709,14 +692,14 @@ function collectDocEvents(filesTouched, cwd) {
709
692
  let present = new Set();
710
693
  try {
711
694
  const all = (0, proc_1.safeExecSync)('git ls-files', {
712
- cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
695
+ cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
713
696
  });
714
697
  present = new Set(all.split('\n').map((s) => s.trim().replace(/\\/g, '/')).filter(Boolean));
715
698
  }
716
699
  catch { /* not a git repo — manifests stay empty */ }
717
700
  return staged.map((e) => ({
718
701
  ...e,
719
- refs: present.size ? (0, docptr_refs_1.extractDocRefs)(e.snippet, e.path, cwd, present) : [],
702
+ refs: present.size ? (0, docptr_refs_1.extractDocRefs)(e.snippet, e.path, root, present) : [],
720
703
  }));
721
704
  }
722
705
  // ---------- Procedure span observer (docs/procedure-system.md) --------------
@@ -975,12 +958,13 @@ async function fetchDocPointers(apiUrl, apiKey, anchor, cwd) {
975
958
  let presentPaths; // eligible .md — the existence sweep
976
959
  let trackedAll; // ALL tracked files — the manifest oracle
977
960
  try {
961
+ const root = (0, doc_mirror_client_1.docPathRoot)(cwd);
978
962
  const md = (0, proc_1.safeExecSync)('git ls-files -- "*.md"', {
979
- cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
963
+ cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
980
964
  });
981
- presentPaths = md.split('\n').map(s => s.trim()).filter(s => isDocPathEligible(s));
965
+ presentPaths = md.split('\n').map(s => s.trim()).filter(s => (0, doc_mirror_client_1.isDocMirrorEligible)(s));
982
966
  const all = (0, proc_1.safeExecSync)('git ls-files', {
983
- cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
967
+ cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
984
968
  });
985
969
  trackedAll = new Set(all.split('\n').map(s => s.trim().replace(/\\/g, '/')).filter(Boolean));
986
970
  }
@@ -1624,7 +1608,9 @@ async function recap(input, mode = 'plain', opts = {}) {
1624
1608
  const skillGainsPromise = fetchAndLandSkillGains(cfg.apiUrl, cfg.apiKey, anchor, cwd);
1625
1609
  // Tier-3 skill activation manifest (docs/load-system.md): fetch canonical
1626
1610
  // full descriptions, then omit skills this harness already advertises
1627
- // natively. Best-effort; the renderer remains pure.
1611
+ // natively. Best-effort; the renderer remains pure. The same roster is what
1612
+ // installed adapters are checked against, so keeping them at canon costs no
1613
+ // extra round trip unless one is stale. adr: adr/skill-bundle-shape.md
1628
1614
  const mirroredSkillsPromise = (async () => {
1629
1615
  try {
1630
1616
  const params = new URLSearchParams({ scope: 'effective', projectId: anchor.projectId });
@@ -1634,8 +1620,12 @@ async function recap(input, mode = 'plain', opts = {}) {
1634
1620
  if (!res.ok)
1635
1621
  return undefined;
1636
1622
  const data = await res.json();
1623
+ const rows = data.skills || [];
1624
+ await (0, native_adapter_refresh_1.refreshNativeAdapters)({
1625
+ platform, rows, apiUrl: cfg.apiUrl, apiKey: cfg.apiKey, homeDir: os.homedir(), projectDir: cwd,
1626
+ }).catch(() => undefined);
1637
1627
  return (0, skill_activation_manifest_1.activationFromApiRows)({
1638
- rows: data.skills || [],
1628
+ rows,
1639
1629
  platform,
1640
1630
  cwd,
1641
1631
  homeDir: process.env.HOME || process.env.USERPROFILE || '',
@@ -2203,10 +2193,16 @@ function validateChip(title, prompt) {
2203
2193
  `"Chip: Fix stale README badge" or "Chip B: Build freshness engine". ` +
2204
2194
  `The prefix distinguishes chip-spawned sessions from IDE/human sessions in FleetView.`);
2205
2195
  }
2206
- if (!/\bgit\s+worktree\s+add\b/.test(prompt)) {
2207
- violations.push(`prompt missing worktree setup. ` +
2208
- `Block 1 requires the chip to open with: \`git worktree add .claude/worktrees/<slug> -b chip/<slug>\` ` +
2209
- `then \`cd\` into it. Chips never edit the main checkout.`);
2196
+ // Block 1 carries both steps: `git branch -m` keeps the fresh worktree the
2197
+ // desktop app already gave the chip, `git worktree add` makes one on a local
2198
+ // start. `git worktree add` alone is the old Block 1, which built a second
2199
+ // worktree beside the app's — reject it so no stale copy brings that back.
2200
+ if (!/\bgit\s+worktree\s+add\b/.test(prompt) || !/\bgit\s+branch\s+-m\b/.test(prompt)) {
2201
+ violations.push(`prompt missing the current worktree setup. ` +
2202
+ `Paste Block 1 verbatim from \`greprag load chip-spawn\`: it keeps the worktree the desktop app ` +
2203
+ `already gave the chip (renaming its fresh branch to chip/<slug>) and runs ` +
2204
+ `\`git worktree add .claude/worktrees/<slug> -b chip/<slug>\` only on a local start. ` +
2205
+ `Chips never edit the main checkout.`);
2210
2206
  }
2211
2207
  // NO report-back check. Chips report to the parent with native
2212
2208
  // `SendMessage to: "<parent-name>"`, which needs no greprag address and no
@@ -2220,7 +2216,7 @@ function validateChip(title, prompt) {
2220
2216
  * dropped by the CCD harness for MCP `spawn_task` calls (see
2221
2217
  * adr/spawn-task-hook-mode.md 2026-05-27 entry — Tier 2 augmentation
2222
2218
  * attempt). `permissionDecision: deny` IS honored, so the validator
2223
- * catches missing Block 1 / Block 2 markers; the agent re-composes the
2219
+ * catches a missing title prefix or Block 1 marker; the agent re-composes the
2224
2220
  * call with the templates from `greprag load chip-spawn`. */
2225
2221
  function handlePreSpawnCheck(input) {
2226
2222
  if (input.tool_name !== 'mcp__ccd_session__spawn_task')
package/dist/index.js CHANGED
@@ -325,16 +325,16 @@ async function runDesk(args) {
325
325
  process.exitCode = 1;
326
326
  }
327
327
  async function inbox(args) {
328
+ const sub = args[0];
329
+ if (sub === '--help' || sub === '-h' || sub === 'help') {
330
+ console.log(args[1] === 'watch' ? INBOX_WATCH_HELP : INBOX_HELP);
331
+ return;
332
+ }
328
333
  const cfg = getConfig();
329
334
  if (!cfg.apiKey) {
330
335
  console.error('Not configured. Run: greprag init');
331
336
  process.exit(1);
332
337
  }
333
- const sub = args[0];
334
- if (sub === '--help' || sub === '-h' || sub === 'help') {
335
- console.log(INBOX_HELP);
336
- return;
337
- }
338
338
  // A non-flag first arg that isn't a known subcommand is a typo — do NOT fall
339
339
  // through to list mode (which auto-marks every message read). The classic
340
340
  // slip is `inbox send` (the real reply command is top-level `greprag send`).
@@ -351,13 +351,6 @@ async function inbox(args) {
351
351
  }
352
352
  if (sub === 'watch') {
353
353
  const subArgs = args.slice(1);
354
- // --help MUST be checked before launching the watch. Pre-fix, `inbox
355
- // watch --help` started a live SSE watch instead of printing help (smell
356
- // 3d908baa) — the help flag silently armed a watcher. Print and return.
357
- if (subArgs.includes('--help') || subArgs.includes('-h')) {
358
- console.log(INBOX_WATCH_HELP);
359
- return;
360
- }
361
354
  // The Assistant role was removed 2026-09-03 (adr/assistant-role.md). Fail
362
355
  // loudly rather than silently arming a stock watcher — 5.44.0 already
363
356
  // shipped a build where `--assistant` was dropped without error and the
@@ -1059,16 +1052,16 @@ async function send(args) {
1059
1052
  }
1060
1053
  /** greprag discord <pair|unpair|me> — Discord DM bridge to inbox. */
1061
1054
  async function discord(args) {
1062
- const cfg = getConfig();
1063
- if (!cfg.apiKey) {
1064
- console.error('Not configured. Run: greprag init');
1065
- process.exit(1);
1066
- }
1067
1055
  const sub = args[0];
1068
1056
  if (!sub || sub === 'help' || sub === '--help') {
1069
1057
  console.log(discord_1.DISCORD_HELP);
1070
1058
  return;
1071
1059
  }
1060
+ const cfg = getConfig();
1061
+ if (!cfg.apiKey) {
1062
+ console.error('Not configured. Run: greprag init');
1063
+ process.exit(1);
1064
+ }
1072
1065
  if (sub === 'pair') {
1073
1066
  const anchor = (0, project_anchor_1.readAnchor)(process.cwd());
1074
1067
  if (!anchor.projectId) {
@@ -1318,7 +1311,10 @@ Commands:
1318
1311
  parent of repo root). Each becomes inbox-addressable.
1319
1312
  status [--json] [--claude|--codex|--opencode|--grok]
1320
1313
  Installation, auth, platform hooks/plugins, and project state
1321
- codex doctor Diagnose Codex hooks, trust setup, and current thread
1314
+ announce [--session <8hex>] [--path]
1315
+ Print the full SessionStart announce (the harness
1316
+ inlines only ~2KB; the rest is parked on disk)
1317
+ codex doctor Diagnose Codex hooks, trust setup, and current thread
1322
1318
  project-id Print the current project_id
1323
1319
  session-id [--full] Print this session's id (8-hex, 16-hex for UUIDv7, or full UUID with --full).
1324
1320
  discover [--json] Tenant-wide structure: every project, per-shape row counts,
@@ -1526,11 +1522,12 @@ Corpus (upload + search arbitrary text — books, codebases, references):
1526
1522
  Single-store search. <store> is a UUID or name.
1527
1523
  corpus search "<intent>" --all|--store <ref>... [--kind K] [--limit 8]
1528
1524
  Multistore search (RRF-fused). --all = every store.
1529
- corpus section <store> "<heading>"
1525
+ corpus section <store> "<heading>" [--max-chars N]
1530
1526
  Every node under a heading (forgiving: any depth,
1531
1527
  dash/case-insensitive).
1532
- corpus walk <store> <nodeId> [--before 2 --after 2]
1533
- Show a node and its surrounding context.
1528
+ corpus walk <store> <nodeId> [--before 2 --after 2] [--max-chars N]
1529
+ Show a node and its surrounding context. Both
1530
+ print full node text; --max-chars N cuts each node.
1534
1531
  corpus status <store> Write-side enrichment progress (per-node vocab bridge).
1535
1532
  corpus refresh <store> Manually re-check a tracked source (re-ingest changed blocks).
1536
1533
  corpus sources <store> Freshness state: source, policy, last change, next check.
@@ -1650,12 +1647,15 @@ Examples:
1650
1647
  greprag send "Bug in auth flow — repro at line 42" --to alice@greprag.com/abc12345 \
1651
1648
  --file src/auth.ts:42 --artifact commit:abc123
1652
1649
  `.trim();
1653
- /** Groups that ship their own detailed `--help`. `help --all` invokes each one
1650
+ /** Groups that ship their own detailed `--help`. A help flag anywhere after one
1651
+ * of these prints it (printCommandHelp), and `help --all` invokes each one
1654
1652
  * live and captures its output, so the assembled guide is rendered from the
1655
1653
  * exact printers an agent gets via `greprag <group> --help` — it cannot drift
1656
- * from them. New group with its own --help? Add it here. (The help-coverage
1657
- * test flags any dispatched command that never appears in a help surface.) */
1658
- const HELP_ALL_GROUPS = [
1654
+ * from them. Each is called as fn(['--help', ...words]) and must print and
1655
+ * return. New group with its own --help? Add it here, or its --help shows the
1656
+ * overview. (The help-coverage test flags any dispatched command that never
1657
+ * appears in a help surface.) */
1658
+ const HELP_GROUPS = [
1659
1659
  ['inbox', inbox],
1660
1660
  ['email', email_1.runEmail],
1661
1661
  ['memory', memory_1.runMemory],
@@ -1680,6 +1680,18 @@ const HELP_ALL_GROUPS = [
1680
1680
  ['opencode', opencode],
1681
1681
  ['crush', crush_1.runCrush],
1682
1682
  ['archive', archive_1.runArchive],
1683
+ ['init', () => { console.log(INIT_HELP); }],
1684
+ ['app', app_1.runApp],
1685
+ ['announce', runAnnounce],
1686
+ ['search', search_1.runSearch],
1687
+ ['session', session_1.runSession],
1688
+ ['context', context_governor_1.runContext],
1689
+ ['repodoc', repodoc_1.runRepoDoc],
1690
+ ['parity', parity_1.runParity],
1691
+ ['grok', grok_spawn_1.runGrok],
1692
+ ['docptr', docptr_1.runDocPtr],
1693
+ ['loadout', loadout_1.runLoadout],
1694
+ ['skillgain', skillgain_1.runSkillGain],
1683
1695
  ];
1684
1696
  const OPENCODE_HELP = `greprag opencode — bridge greprag inbox into a running opencode desktop
1685
1697
  session, plus a file-watching capture pipeline for episodic memory.
@@ -1805,6 +1817,8 @@ Run under a long-lived shell loop or as a Windows service:
1805
1817
  async function opencode(args) {
1806
1818
  const sub = args[0];
1807
1819
  if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
1820
+ if (args[1] === 'chip')
1821
+ return (0, opencode_chip_1.runOpencodeChip)(['--help', ...args.slice(2)]);
1808
1822
  console.log(OPENCODE_HELP);
1809
1823
  return;
1810
1824
  }
@@ -1859,13 +1873,25 @@ async function captureOutput(fn) {
1859
1873
  async function printHelpAll() {
1860
1874
  const bar = '═'.repeat(72);
1861
1875
  const parts = [HELP];
1862
- for (const [name, fn] of HELP_ALL_GROUPS) {
1876
+ for (const [name, fn] of HELP_GROUPS) {
1863
1877
  const body = (await captureOutput(() => fn(['--help']))).trim();
1864
1878
  if (body)
1865
1879
  parts.push(`${bar}\n greprag ${name}\n${bar}\n${body}`);
1866
1880
  }
1867
1881
  console.log(parts.join('\n\n'));
1868
1882
  }
1883
+ /** Help for `greprag <command> ... --help`. The words before the first flag let
1884
+ * a group show the part of its help they name (`inbox watch --help` → the
1885
+ * watch guide); a command without help of its own gets the overview. */
1886
+ async function printCommandHelp(command, subArgs) {
1887
+ const group = HELP_GROUPS.find(([name]) => name === command);
1888
+ if (!group) {
1889
+ console.log(HELP);
1890
+ return;
1891
+ }
1892
+ const words = subArgs.slice(0, subArgs.findIndex(arg => arg.startsWith('-')));
1893
+ await group[1](['--help', ...words]);
1894
+ }
1869
1895
  async function main() {
1870
1896
  const args = process.argv.slice(2);
1871
1897
  const command = args[0];
@@ -1881,6 +1907,16 @@ async function main() {
1881
1907
  console.log(HELP);
1882
1908
  return;
1883
1909
  }
1910
+ const subArgs = args.slice(1);
1911
+ // A help flag anywhere after the command asks for help, so the command never
1912
+ // runs. Each group parses its own argv and looks for help only in its first
1913
+ // slot, so one further along fell through into a real run: `skill mirror
1914
+ // sync --help` dropped the flag and synced every harness, `skill edit --help`
1915
+ // sent "--help" to the API as a skill name. adr: adr/cli-help-dispatch.md
1916
+ if (subArgs.some(arg => arg === '--help' || arg === '-h')) {
1917
+ await printCommandHelp(command, subArgs);
1918
+ return;
1919
+ }
1884
1920
  // Load ~/.greprag/.env ONCE for every command. Commands that read
1885
1921
  // process.env.GREPRAG_API_KEY directly (load, loadout, …) used to work only
1886
1922
  // in shells with the var exported — a fresh terminal got "GREPRAG_API_KEY
@@ -1888,7 +1924,6 @@ async function main() {
1888
1924
  // config themselves) worked fine (field incident 2026-07-12, tenant tanya).
1889
1925
  // Dispatch-level ensureEnv kills the whole per-command-forgot-env class.
1890
1926
  ensureEnv();
1891
- const subArgs = args.slice(1);
1892
1927
  switch (command) {
1893
1928
  case 'app':
1894
1929
  case 'dashboard': return (0, app_1.runApp)(subArgs);
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /** Keep installed native skill adapters at canon.
3
+ *
4
+ * Adapters were written only by explicit syncs (init, `skill mirror sync`,
5
+ * `--only`), so a canon change made anywhere — `skill mirror apply`, another
6
+ * session, another machine — left them behind until someone re-synced by hand.
7
+ *
8
+ * SessionStart calls this with the mirror roster it already fetched. Detection
9
+ * is local: each managed adapter's stamps against the roster. Only a stale
10
+ * adapter costs a fetch, and every write goes through the writer the sync
11
+ * commands use, so the ownership ledger and the divergence guard still decide
12
+ * what may be replaced. Refresh-only: no adapter is created or removed here.
13
+ */
14
+ // adr: adr/skill-bundle-shape.md
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.readAdapterStamps = readAdapterStamps;
17
+ exports.staleNativeAdapters = staleNativeAdapters;
18
+ exports.refreshNativeAdapters = refreshNativeAdapters;
19
+ const claude_lifecycle_cutover_1 = require("./claude-lifecycle-cutover");
20
+ const native_skill_mirror_1 = require("./native-skill-mirror");
21
+ const skill_mirror_files_1 = require("./skill-mirror-files");
22
+ /** The identity stamps a generated adapter carries in its frontmatter. */
23
+ function readAdapterStamps(content) {
24
+ const frontmatter = /^---\n([\s\S]*?)\n---/.exec(content.replace(/\r\n?/g, '\n'))?.[1] || '';
25
+ const field = (key) => new RegExp(`^${key}:[ \\t]*(\\S+)`, 'm').exec(frontmatter)?.[1];
26
+ const revision = field('greprag_revision');
27
+ return {
28
+ skillId: field('greprag_skill_id'),
29
+ contentHash: field('greprag_content_hash'),
30
+ revision: revision === undefined ? undefined : Number(revision),
31
+ format: field('greprag_adapter_format'),
32
+ };
33
+ }
34
+ /** Managed adapters whose canon or renderer has moved since they were written.
35
+ * An adapter edited locally, so its bytes no longer match the ledger, stays
36
+ * with the divergence guard and costs no fetch. A missing one stays missing. */
37
+ function staleNativeAdapters(params) {
38
+ const globalRoot = (0, native_skill_mirror_1.nativeSkillRoot)(params.platform, params.homeDir);
39
+ const roots = [globalRoot];
40
+ // Claude adapters live only in the home root (claude-lifecycle-cutover.ts).
41
+ const projectRoot = params.projectDir && params.platform !== 'claude-code'
42
+ ? (0, native_skill_mirror_1.writerOwnedProjectSkillRoot)(params.platform, params.projectDir, params.homeDir)
43
+ : null;
44
+ if (projectRoot)
45
+ roots.push(projectRoot);
46
+ const byId = new Map();
47
+ for (const row of params.rows) {
48
+ if (typeof row.skillName === 'string' && typeof row.skillId === 'string') {
49
+ byId.set(row.skillId, row);
50
+ }
51
+ }
52
+ const format = (0, native_skill_mirror_1.nativeAdapterFormat)(params.platform);
53
+ const stale = [];
54
+ for (const root of roots) {
55
+ for (const [skillName, managed] of Object.entries((0, native_skill_mirror_1.readState)(root).skills)) {
56
+ let files;
57
+ try {
58
+ files = (0, native_skill_mirror_1.readManagedFiles)((0, native_skill_mirror_1.targetPath)(root, skillName), managed);
59
+ }
60
+ catch {
61
+ continue;
62
+ }
63
+ if (!files?.length || (0, skill_mirror_files_1.computeSkillFileHash)(files) !== managed.hash)
64
+ continue;
65
+ const stamps = readAdapterStamps(files.find(file => file.path === 'SKILL.md')?.content || '');
66
+ const row = byId.get(stamps.skillId || (root === globalRoot ? `global:${skillName}` : ''));
67
+ if (!row || row.skillName !== skillName)
68
+ continue;
69
+ // The content hash is canon's identity. Revision only decides for a row
70
+ // that predates content hashes; comparing it otherwise would fight writers
71
+ // that do not know the revision.
72
+ const canonMoved = row.contentHash
73
+ ? row.contentHash !== stamps.contentHash
74
+ : typeof row.revision === 'number' && row.revision !== stamps.revision;
75
+ if (canonMoved || stamps.format !== format)
76
+ stale.push({ root, row });
77
+ }
78
+ }
79
+ return stale;
80
+ }
81
+ /** Rewrite each stale managed adapter of this harness from canon, through the
82
+ * writer the sync commands use. One adapter per write, so a diverged one holds
83
+ * back no other. A harness without native adapters is a no-op. */
84
+ async function refreshNativeAdapters(params) {
85
+ const platform = params.platform;
86
+ if (platform !== 'claude-code' && platform !== 'codex' && platform !== 'opencode') {
87
+ return { refreshed: 0, blocked: 0 };
88
+ }
89
+ const stale = staleNativeAdapters({ ...params, platform });
90
+ const skills = await Promise.all(stale.map(({ row }) => (0, native_skill_mirror_1.fetchNativeMirroredSkill)(params.apiUrl, params.apiKey, row).catch(() => null)));
91
+ let refreshed = 0;
92
+ let blocked = 0;
93
+ for (const skill of skills) {
94
+ if (!skill)
95
+ continue; // canon unreachable: the next session start retries
96
+ try {
97
+ const result = platform === 'claude-code'
98
+ ? (0, claude_lifecycle_cutover_1.applyClaudeLifecycleCutover)({ skills: [skill], names: [skill.skillName], homeDir: params.homeDir })
99
+ : (0, native_skill_mirror_1.applyNativeSkillMirror)({
100
+ platform,
101
+ skills: [skill],
102
+ homeDir: params.homeDir,
103
+ projectDir: params.projectDir,
104
+ prune: false,
105
+ });
106
+ refreshed += result.updated;
107
+ blocked += result.blocked;
108
+ }
109
+ catch {
110
+ blocked++;
111
+ }
112
+ }
113
+ return { refreshed, blocked };
114
+ }