greprag 5.82.0 → 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.
- package/dist/claude-lifecycle-cutover.js +5 -16
- package/dist/commands/codex.js +4 -0
- package/dist/commands/corpus/client.js +16 -0
- package/dist/commands/corpus/index.js +5 -2
- package/dist/commands/corpus/section.js +3 -3
- package/dist/commands/corpus/walk.js +3 -3
- package/dist/commands/delivery.js +64 -0
- package/dist/commands/docptr.js +10 -24
- package/dist/commands/init.js +4 -5
- package/dist/commands/load-primer-reminder.js +2 -2
- package/dist/commands/load.js +45 -12
- package/dist/commands/merge-guard.js +94 -9
- package/dist/commands/parity.js +0 -2
- package/dist/commands/pipe-wrap.js +4 -2
- package/dist/commands/skill-mirror-reminder.js +24 -6
- package/dist/commands/skill.js +24 -8
- package/dist/commands/skillgain.js +5 -1
- package/dist/commands/state-trigger.js +67 -31
- package/dist/delivery-config.js +69 -0
- package/dist/deploy-locks.js +18 -3
- package/dist/doc-mirror-client.js +39 -11
- package/dist/hook.js +35 -39
- package/dist/index.js +62 -27
- package/dist/native-adapter-refresh.js +114 -0
- package/dist/native-skill-adoption.js +20 -53
- package/dist/native-skill-mirror.js +130 -52
- package/dist/native-skill-pending.js +5 -2
- package/dist/opencode-plugin.bundle.js +1079 -159
- package/dist/opencode-plugin.js +6 -0
- package/dist/skill-activation-manifest.js +98 -22
- package/dist/skill-landing.js +6 -0
- package/dist/skill-mirror-files.js +59 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +27 -13
- package/skill/greprag/docs/corpus.md +2 -2
- package/skill/greprag/docs/setup.md +1 -1
- package/skill/mechanic/SKILL.md +14 -39
- package/skill/mechanic/docs/skill-fix-conventions.md +2 -2
- 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 —
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
|
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(
|
|
675
|
+
const rel = path.relative(root, abs).replace(/\\/g, '/');
|
|
693
676
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
|
|
694
677
|
continue;
|
|
695
|
-
if (!
|
|
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,
|
|
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 =>
|
|
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
|
|
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
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
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
|
|
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
|
-
|
|
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`.
|
|
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.
|
|
1657
|
-
*
|
|
1658
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
238
|
-
|
|
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
|
}
|