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.
- package/dist/claude-lifecycle-cutover.js +5 -16
- package/dist/commands/codex.js +4 -0
- package/dist/commands/collision-check.js +9 -6
- 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-reminder.js +13 -9
- 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 +1083 -160
- 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 +4 -19
- package/skill/greprag/SKILL.md +1 -1
- package/skill/greprag/docs/corpus.md +2 -2
- package/skill/greprag/docs/setup.md +1 -1
- package/skill/mechanic/SKILL.md +15 -40
- package/skill/mechanic/docs/skill-fix-conventions.md +2 -2
- package/skill/templates/chip-spawn.md +36 -20
- package/skill/templates/delivery.md +19 -5
package/dist/deploy-locks.js
CHANGED
|
@@ -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
|
-
/**
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
230
|
+
const root = docPathRoot(cwd);
|
|
231
|
+
const tracked = gitLsFiles(root);
|
|
205
232
|
if (tracked) {
|
|
206
|
-
return buildCollection(
|
|
233
|
+
return buildCollection(root, tracked.filter(isDocMirrorEligible), 'git', new Set(tracked), true);
|
|
207
234
|
}
|
|
208
|
-
const walked = walkMarkdownFiles(
|
|
209
|
-
return buildCollection(
|
|
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
|
|
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(
|
|
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 —
|
|
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
|
+
}
|