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
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
+ }
@@ -41,49 +41,7 @@ const os = __importStar(require("node:os"));
41
41
  const path = __importStar(require("node:path"));
42
42
  const native_skill_mirror_1 = require("./native-skill-mirror");
43
43
  const skill_mirror_files_1 = require("./skill-mirror-files");
44
- const STATE_FILE = '.greprag-mirror.json';
45
44
  const NATIVE_SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
46
- function emptyState() {
47
- return { version: 1, skills: {} };
48
- }
49
- function readState(root) {
50
- try {
51
- const parsed = JSON.parse(fs.readFileSync(path.join(root, STATE_FILE), 'utf8'));
52
- if (parsed?.version === 1 && parsed.skills && typeof parsed.skills === 'object')
53
- return parsed;
54
- }
55
- catch { /* first run or corrupt state: existing directories remain unmanaged */ }
56
- return emptyState();
57
- }
58
- function writeState(root, state) {
59
- fs.mkdirSync(root, { recursive: true });
60
- fs.writeFileSync(path.join(root, STATE_FILE), JSON.stringify(state, null, 2) + '\n', 'utf8');
61
- }
62
- function targetPath(root, skillName) {
63
- const target = path.resolve(root, skillName);
64
- const prefix = path.resolve(root) + path.sep;
65
- if (!target.startsWith(prefix))
66
- throw new Error('skill target escapes native root');
67
- return target;
68
- }
69
- function filePath(target, relative) {
70
- const resolved = path.resolve(target, ...relative.split('/'));
71
- const prefix = path.resolve(target) + path.sep;
72
- if (!resolved.startsWith(prefix))
73
- throw new Error('skill file escapes target directory');
74
- return resolved;
75
- }
76
- function readManagedFiles(target, managed) {
77
- try {
78
- return managed.files.map(relative => ({
79
- path: relative,
80
- content: fs.readFileSync(filePath(target, relative), 'utf8'),
81
- }));
82
- }
83
- catch {
84
- return null;
85
- }
86
- }
87
45
  function listSkillDirs(root) {
88
46
  try {
89
47
  return fs.readdirSync(root, { withFileTypes: true })
@@ -122,9 +80,12 @@ function countSkillFiles(root) {
122
80
  }
123
81
  function adoptionRoots(params) {
124
82
  const roots = [{ root: (0, native_skill_mirror_1.nativeSkillRoot)(params.platform, params.homeDir), scope: 'global' }];
125
- if (params.projectDir && params.projectId) {
83
+ const projectRoot = params.projectDir && params.projectId
84
+ ? (0, native_skill_mirror_1.writerOwnedProjectSkillRoot)(params.platform, params.projectDir, params.homeDir)
85
+ : null;
86
+ if (projectRoot) {
126
87
  roots.push({
127
- root: (0, native_skill_mirror_1.nativeProjectSkillRoot)(params.platform, params.projectDir),
88
+ root: projectRoot,
128
89
  scope: 'project',
129
90
  projectId: params.projectId,
130
91
  projectName: params.projectName,
@@ -169,12 +130,12 @@ function planNativeSkillAdoption(params) {
169
130
  projectId: params.projectId,
170
131
  projectName: params.projectName,
171
132
  })) {
172
- const state = readState(root.root);
133
+ const state = (0, native_skill_mirror_1.readState)(root.root);
173
134
  for (const skillName of listSkillDirs(root.root)) {
174
- const target = targetPath(root.root, skillName);
135
+ const target = (0, native_skill_mirror_1.targetPath)(root.root, skillName);
175
136
  const managed = state.skills[skillName];
176
137
  if (managed) {
177
- const currentFiles = readManagedFiles(target, managed);
138
+ const currentFiles = (0, native_skill_mirror_1.readManagedFiles)(target, managed);
178
139
  const currentHash = currentFiles ? (0, skill_mirror_files_1.computeSkillFileHash)(currentFiles) : '';
179
140
  const entry = { ...root, skillName, reason: currentHash === managed.hash ? 'managed/current' : 'managed/diverged' };
180
141
  if (currentHash === managed.hash)
@@ -224,8 +185,8 @@ function installAdoptedNativeSkill(params) {
224
185
  return 'skipped';
225
186
  if (!NATIVE_SKILL_NAME_RE.test(params.skill.skillName))
226
187
  return 'skipped';
227
- const target = targetPath(params.root, params.skill.skillName);
228
- const state = readState(params.root);
188
+ const target = (0, native_skill_mirror_1.targetPath)(params.root, params.skill.skillName);
189
+ const state = (0, native_skill_mirror_1.readState)(params.root);
229
190
  if (state.skills[params.skill.skillName])
230
191
  return 'blocked';
231
192
  const currentFiles = params.collectFiles(target);
@@ -234,17 +195,23 @@ function installAdoptedNativeSkill(params) {
234
195
  const adapterFiles = (0, native_skill_mirror_1.buildNativeSkillAdapter)(params.platform, params.skill);
235
196
  if (!adapterFiles)
236
197
  return 'skipped';
237
- fs.rmSync(target, { recursive: true, force: true });
238
- fs.mkdirSync(target, { recursive: true });
198
+ // Adoption replaces only the files it just moved into canon. The mirror
199
+ // carries SKILL.md and docs/**/*.md; everything else in the package
200
+ // (scripts/, references/, assets/, any non-markdown file) exists nowhere but
201
+ // this folder, and wiping the folder destroyed exactly those. Write the
202
+ // adapter, then remove the adopted files it did not overwrite, the same rule
203
+ // a managed adapter update follows. adr: adr/skill-bundle-shape.md
239
204
  for (const file of adapterFiles) {
240
- const destination = filePath(target, file.path);
205
+ const destination = (0, native_skill_mirror_1.filePath)(target, file.path);
241
206
  fs.mkdirSync(path.dirname(destination), { recursive: true });
242
207
  fs.writeFileSync(destination, file.content, 'utf8');
243
208
  }
209
+ const adapterPaths = new Set(adapterFiles.map(file => file.path));
210
+ (0, native_skill_mirror_1.removeTrackedFiles)(target, currentFiles.map(file => file.path).filter(relative => !adapterPaths.has(relative)));
244
211
  state.skills[params.skill.skillName] = {
245
212
  hash: (0, skill_mirror_files_1.computeSkillFileHash)(adapterFiles),
246
213
  files: adapterFiles.map(file => file.path).sort(),
247
214
  };
248
- writeState(params.root, state);
215
+ (0, native_skill_mirror_1.writeState)(params.root, state);
249
216
  return 'installed';
250
217
  }