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
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* plumbing that happens to share a prefix, and `merge-base` is used BY the
|
|
23
23
|
* provenance check — blocking it would break the gate with the guard.
|
|
24
24
|
* · only a merge nothing already serialises. A command that takes the lock
|
|
25
|
-
* itself
|
|
25
|
+
* itself and stops on acquisition failure is already serialised.
|
|
26
26
|
*
|
|
27
27
|
* Holding the lock yourself is a pass. A peer holding it is the refusal that
|
|
28
28
|
* matters: it is the exact collision, caught before the merge instead of after.
|
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
* trips. Only `&&` earns this: after `;` or `||` the merge still runs when the
|
|
52
52
|
* acquire failed, so those are refused, and the refusal now says so.
|
|
53
53
|
*
|
|
54
|
+
* A THIRD, 2026-10-06 in C:/sift: an acquire run from a linked worktree wrote the
|
|
55
|
+
* lock into that worktree's private git dir, so this guard, reading the canonical
|
|
56
|
+
* checkout, refused the holder's own merge whatever its shape. Locks now live in the
|
|
57
|
+
* repository's shared `.git`, and a chained acquire counts when it writes the same
|
|
58
|
+
* lock file this guard reads.
|
|
59
|
+
*
|
|
54
60
|
* # adr: adr/deploy-gate-cli.md
|
|
55
61
|
*/
|
|
56
62
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -126,11 +132,11 @@ function shellSegments(command) {
|
|
|
126
132
|
let start = 0;
|
|
127
133
|
let afterAnd = true;
|
|
128
134
|
for (let match = separator.exec(masked); match; match = separator.exec(masked)) {
|
|
129
|
-
segments.push({ text: source.slice(start, match.index), afterAnd });
|
|
135
|
+
segments.push({ text: source.slice(start, match.index), start, afterAnd });
|
|
130
136
|
afterAnd = match[0] === '&&';
|
|
131
137
|
start = match.index + match[0].length;
|
|
132
138
|
}
|
|
133
|
-
segments.push({ text: source.slice(start), afterAnd });
|
|
139
|
+
segments.push({ text: source.slice(start), start, afterAnd });
|
|
134
140
|
return segments;
|
|
135
141
|
}
|
|
136
142
|
/** Quote-aware tokens. A quoted run is one token with its quotes removed, so
|
|
@@ -297,9 +303,10 @@ function buildMergeGuardReason(held, ownership) {
|
|
|
297
303
|
' greprag merge-lock acquire --label "<what you are merging>"',
|
|
298
304
|
' git merge --ff-only <rebased branch>',
|
|
299
305
|
' greprag merge-lock release --landed <the merge sha> --label "<what it was>"',
|
|
300
|
-
'One call works too
|
|
301
|
-
'
|
|
302
|
-
'
|
|
306
|
+
'One call works too when the acquire comes first and failure stops the merge:',
|
|
307
|
+
'use an `&&` chain, or in PowerShell a try/finally block with an immediate',
|
|
308
|
+
'`if ($LASTEXITCODE -ne 0) { throw ... }` after acquire. Bare `;` and `||`',
|
|
309
|
+
'let the merge run after a failed acquire, so those are refused.',
|
|
303
310
|
'`--landed` is what lets the deploy prove YOUR work is in what ships, rather than only',
|
|
304
311
|
'that production serves HEAD. Merging in a worktree instead? That is never blocked.',
|
|
305
312
|
].join(' ');
|
|
@@ -357,7 +364,8 @@ function guardMergeAt(input, segments, mergeAt) {
|
|
|
357
364
|
// truncating it locally to 8 was a second copy of identity logic waiting to disagree
|
|
358
365
|
// with the one in session-id.ts.
|
|
359
366
|
const ownership = (0, deploy_locks_1.lockOwnership)(held, input.session_id);
|
|
360
|
-
const chainedLock = takesLockForTarget(segments, mergeAt, cwd, target)
|
|
367
|
+
const chainedLock = takesLockForTarget(segments, mergeAt, cwd, target)
|
|
368
|
+
|| powerShellTryTakesLock(input.tool_input?.command ?? input.tool_input?.cmd ?? '', segments[mergeAt].start, cwd, target);
|
|
361
369
|
if (ownership !== 'mine' && !chainedLock)
|
|
362
370
|
return denyMerge(input, buildMergeGuardReason(held, ownership));
|
|
363
371
|
// Git owns the final freshness check AT mutation time. Even if master changes
|
|
@@ -403,11 +411,88 @@ function takesLockForTarget(segments, mergeAt, cwd, target) {
|
|
|
403
411
|
return false;
|
|
404
412
|
if (!isMergeLockAcquire(segments[i - 1].text))
|
|
405
413
|
continue;
|
|
406
|
-
|
|
407
|
-
|
|
414
|
+
return acquiresTargetLock(mergeCwd(segments, i - 1, cwd), target);
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
/** An acquire run in `dir` serialises a merge into `target` exactly when it writes the
|
|
419
|
+
* lock the guard reads for `target`. The lock is repository-wide, so an acquire from a
|
|
420
|
+
* linked worktree counts; comparing worktree roots refused that holder on 2026-10-06. */
|
|
421
|
+
function acquiresTargetLock(dir, target) {
|
|
422
|
+
try {
|
|
423
|
+
return (0, deploy_gate_1.normalizePath)((0, deploy_locks_1.mergeLockPath)(dir)) === (0, deploy_gate_1.normalizePath)((0, deploy_locks_1.mergeLockPath)(target));
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/** PowerShell's `try { acquire; if ($LASTEXITCODE -ne 0) { throw ... }; merge }
|
|
430
|
+
* finally { release }` is a fail-closed lock sequence too. Inspect only direct
|
|
431
|
+
* statements in the try body; a bare semicolon or an acquire nested in a branch
|
|
432
|
+
* must not excuse a merge. */
|
|
433
|
+
function powerShellTryTakesLock(command, mergeStart, cwd, target) {
|
|
434
|
+
const masked = maskQuoted(command);
|
|
435
|
+
const tryStart = /(?:^|[;\n])\s*try\s*\{/gi;
|
|
436
|
+
for (let match = tryStart.exec(masked); match; match = tryStart.exec(masked)) {
|
|
437
|
+
const open = masked.indexOf('{', match.index);
|
|
438
|
+
const close = matchingBrace(masked, open);
|
|
439
|
+
if (close < 0 || mergeStart <= open || mergeStart >= close)
|
|
440
|
+
continue;
|
|
441
|
+
const suffix = masked.slice(close + 1);
|
|
442
|
+
const finallyMatch = /^\s*finally\s*\{/.exec(suffix);
|
|
443
|
+
if (!finallyMatch || matchingBrace(masked, close + 1 + finallyMatch[0].lastIndexOf('{')) < 0)
|
|
444
|
+
continue;
|
|
445
|
+
const statements = powerShellStatements(command, masked, open + 1, close);
|
|
446
|
+
const mergeIndex = statements.findIndex(s => mergeStart >= s.start && mergeStart < s.end);
|
|
447
|
+
if (mergeIndex < 2 || mergeGlobalOpts(statements[mergeIndex].text)?.length !== 0)
|
|
448
|
+
continue;
|
|
449
|
+
// A location change would make the lock's target ambiguous. The regular
|
|
450
|
+
// chained-command path already resolves simple cd/Set-Location sequences.
|
|
451
|
+
if (/(?:^|[;\n])\s*(?:cd|chdir|sl|Set-Location|Push-Location|Pop-Location|pushd|popd)\b/i.test(masked.slice(0, mergeStart)))
|
|
452
|
+
continue;
|
|
453
|
+
if (!acquiresTargetLock(cwd, target))
|
|
454
|
+
continue;
|
|
455
|
+
for (let i = mergeIndex - 2; i >= 0; i--) {
|
|
456
|
+
if (!isMergeLockAcquire(statements[i].text))
|
|
457
|
+
continue;
|
|
458
|
+
if (/[|&]/.test(maskQuoted(statements[i].text)))
|
|
459
|
+
continue;
|
|
460
|
+
if (!/^if\s*\(\s*\$LASTEXITCODE\s*-ne\s*0\s*\)\s*\{\s*throw\b[^{}]*\}\s*$/i.test(statements[i + 1].text.trim()))
|
|
461
|
+
continue;
|
|
462
|
+
if (statements.slice(i + 2, mergeIndex).some(s => /\bmerge-lock\s+release\b/i.test(s.text)))
|
|
463
|
+
continue;
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
408
466
|
}
|
|
409
467
|
return false;
|
|
410
468
|
}
|
|
469
|
+
function matchingBrace(masked, open) {
|
|
470
|
+
let depth = 0;
|
|
471
|
+
for (let i = open; i < masked.length; i++) {
|
|
472
|
+
if (masked[i] === '{')
|
|
473
|
+
depth++;
|
|
474
|
+
else if (masked[i] === '}' && --depth === 0)
|
|
475
|
+
return i;
|
|
476
|
+
}
|
|
477
|
+
return -1;
|
|
478
|
+
}
|
|
479
|
+
function powerShellStatements(command, masked, start, end) {
|
|
480
|
+
const statements = [];
|
|
481
|
+
let depth = 0;
|
|
482
|
+
let from = start;
|
|
483
|
+
for (let i = start; i < end; i++) {
|
|
484
|
+
if (masked[i] === '{')
|
|
485
|
+
depth++;
|
|
486
|
+
else if (masked[i] === '}')
|
|
487
|
+
depth--;
|
|
488
|
+
else if (depth === 0 && (masked[i] === ';' || masked[i] === '\n')) {
|
|
489
|
+
statements.push({ text: command.slice(from, i).trim(), start: from, end: i });
|
|
490
|
+
from = i + 1;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
statements.push({ text: command.slice(from, end).trim(), start: from, end });
|
|
494
|
+
return statements.filter(s => s.text);
|
|
495
|
+
}
|
|
411
496
|
function denyMerge(input, reason) {
|
|
412
497
|
return {
|
|
413
498
|
hookSpecificOutput: {
|
package/dist/commands/parity.js
CHANGED
|
@@ -80,10 +80,8 @@ const SEED_UNITS = [
|
|
|
80
80
|
{
|
|
81
81
|
name: 'chip-spawn',
|
|
82
82
|
followers: [
|
|
83
|
-
{ harness: 'claude-code', candidate: '~/.claude/docs/chip-spawn.md' },
|
|
84
83
|
{ harness: 'claude-code', candidate: 'packages/cli/skill/templates/chip-spawn.md' },
|
|
85
84
|
{ harness: 'codex', candidate: 'packages/cli/skill/templates/codex-chip-spawn.md' },
|
|
86
|
-
{ harness: 'codex', candidate: 'docs/codex-chip-spawn.md' },
|
|
87
85
|
{ harness: 'opencode', candidate: null, note: 'no dedicated opencode chip-spawn doc found — chips ride greprag opencode chip + templates/chip-leader-opencode.md covers the leader shape only' },
|
|
88
86
|
],
|
|
89
87
|
},
|
|
@@ -189,13 +189,15 @@ function searchFlagsDisqualify(tokens) {
|
|
|
189
189
|
}
|
|
190
190
|
/** First non-flag token = the search pattern, sanitized into a crush
|
|
191
191
|
* `--query` hint: strip quotes, keep word chars/space/dot/dash only
|
|
192
|
-
* (safe inside double quotes), collapse whitespace,
|
|
192
|
+
* (safe inside double quotes), drop leading dashes, collapse whitespace,
|
|
193
|
+
* cap at 80 chars. A hint of `--help` or `-h` would reach the CLI as the help
|
|
194
|
+
* flag, which prints help in place of the crushed output (adr/cli-help-dispatch.md). */
|
|
193
195
|
function extractSearchQuery(tokens) {
|
|
194
196
|
for (const tok of tokens) {
|
|
195
197
|
if (tok.startsWith('-'))
|
|
196
198
|
continue;
|
|
197
199
|
const unquoted = tok.replace(/^["']|["']$/g, '');
|
|
198
|
-
const clean = unquoted.replace(/[^\w\s.-]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
200
|
+
const clean = unquoted.replace(/[^\w\s.-]/g, ' ').replace(/^[\s-]+/, '').replace(/\s+/g, ' ').trim();
|
|
199
201
|
return clean.slice(0, 80);
|
|
200
202
|
}
|
|
201
203
|
return '';
|
|
@@ -18,19 +18,36 @@ function renderEntry(skill) {
|
|
|
18
18
|
function buildSkillMirrorAnnounce(m, platform) {
|
|
19
19
|
if (!m || m.count <= 0)
|
|
20
20
|
return null;
|
|
21
|
+
// A hand-kept copy is in the native skill list, so it is not in the trigger
|
|
22
|
+
// list below, but the harness runs that copy and never sees canon move on.
|
|
23
|
+
// Name each one; replacing a live copy is the user's call, skill by skill.
|
|
24
|
+
const cutoverCommand = (0, skill_activation_manifest_1.localCopyCutoverCommand)(platform || '', m.localCopies);
|
|
25
|
+
const copies = m.localCopies.length === 1 ? 'one mirrored skill' : `${m.localCopies.length} mirrored skills`;
|
|
26
|
+
const copySection = cutoverCommand ? [
|
|
27
|
+
'### Hand-Kept Copies',
|
|
28
|
+
`This harness lists ${copies} from a hand-kept copy instead of an adapter, `
|
|
29
|
+
+ `so it keeps running that copy when the mirrored skill changes: ${m.localCopies.join(', ')}. `
|
|
30
|
+
+ 'Replacing a copy with its adapter is the user\'s call, one skill at a time (the old SKILL.md is backed up first):',
|
|
31
|
+
'',
|
|
32
|
+
` ${cutoverCommand}`,
|
|
33
|
+
'',
|
|
34
|
+
'Offer that command when the user asks why a skill behaves out of date, or when you needed one of them this session.',
|
|
35
|
+
'',
|
|
36
|
+
] : [];
|
|
21
37
|
if (m.skills.length === 0) {
|
|
22
|
-
|
|
38
|
+
const line = '[greprag skills: slash skills are available. '
|
|
23
39
|
+ 'When discussing skills with the user, say use/create/update/refresh `/skill`; '
|
|
24
40
|
+ 'keep storage mechanics internal unless diagnosing a failure or the user asks how it works. '
|
|
25
41
|
+ 'Use `greprag load <skill>` to read skill instructions.]';
|
|
42
|
+
return [line, '', ...copySection].join('\n').trimEnd();
|
|
26
43
|
}
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
const installCommand = (0, skill_activation_manifest_1.nativeAdapterInstallCommand)(platform || '', m.
|
|
44
|
+
// Mirrored skills with no native package never reach the native skill list.
|
|
45
|
+
// Name the gap and the one command that closes it, or it stays invisible
|
|
46
|
+
// until the operator spots it by hand.
|
|
47
|
+
const installCommand = (0, skill_activation_manifest_1.nativeAdapterInstallCommand)(platform || '', m.missing);
|
|
31
48
|
const gapSection = installCommand ? [
|
|
32
49
|
'### Missing Native Adapters',
|
|
33
|
-
`${m.
|
|
50
|
+
`${m.missing.length} of these skills are in your GrepRAG mirror but have NO adapter installed in this harness, `
|
|
34
51
|
+ 'so they never appear in the native skill list. Install the adapters once with:',
|
|
35
52
|
'',
|
|
36
53
|
` ${installCommand}`,
|
|
@@ -47,6 +64,7 @@ function buildSkillMirrorAnnounce(m, platform) {
|
|
|
47
64
|
'When discussing skills with the user, say use/create/update/refresh `/skill`; keep storage mechanics internal unless diagnosing a failure or the user asks how it works.',
|
|
48
65
|
'',
|
|
49
66
|
...gapSection,
|
|
67
|
+
...copySection,
|
|
50
68
|
'### Trigger Rules',
|
|
51
69
|
'If the user names a skill (with or without `/`) OR the request clearly matches a skill\'s description below, '
|
|
52
70
|
+ 'you MUST activate that skill for the current turn. Match meaning, not exact keywords.',
|
package/dist/commands/skill.js
CHANGED
|
@@ -59,6 +59,7 @@ const project_anchor_1 = require("../project-anchor");
|
|
|
59
59
|
const skill_mirror_client_1 = require("../skill-mirror-client");
|
|
60
60
|
const skill_landing_1 = require("../skill-landing");
|
|
61
61
|
const skill_staleness_1 = require("../skill-staleness");
|
|
62
|
+
const load_1 = require("./load");
|
|
62
63
|
const CORE_SKILL = 'greprag';
|
|
63
64
|
const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
64
65
|
/** `--only a,b` / `--only=a,b` / repeated `--only a --only b` → skill names. */
|
|
@@ -342,6 +343,7 @@ function forwardedMirrorArgs(args) {
|
|
|
342
343
|
}
|
|
343
344
|
return parts.join(' ');
|
|
344
345
|
}
|
|
346
|
+
/** Returns whether it wrote: under `check` it only lists the plan, so a caller reports success only on true. */
|
|
345
347
|
function writeSkillPackage(targetDir, files, opts) {
|
|
346
348
|
const target = path.resolve(targetDir);
|
|
347
349
|
if (fs.existsSync(target) && !dirEmpty(target) && !opts.force) {
|
|
@@ -357,7 +359,8 @@ function writeSkillPackage(targetDir, files, opts) {
|
|
|
357
359
|
console.log(` write ${file.path}`);
|
|
358
360
|
for (const relative of removals)
|
|
359
361
|
console.log(` remove ${relative}`);
|
|
360
|
-
|
|
362
|
+
console.log('Check only: nothing written.');
|
|
363
|
+
return false;
|
|
361
364
|
}
|
|
362
365
|
fs.mkdirSync(target, { recursive: true });
|
|
363
366
|
for (const relative of removals)
|
|
@@ -367,6 +370,7 @@ function writeSkillPackage(targetDir, files, opts) {
|
|
|
367
370
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
368
371
|
fs.writeFileSync(destination, file.content.endsWith('\n') ? file.content : file.content + '\n', 'utf8');
|
|
369
372
|
}
|
|
373
|
+
return true;
|
|
370
374
|
}
|
|
371
375
|
function printSkillMirrorConflict(params) {
|
|
372
376
|
const local = new Map(params.localFiles.map(file => [file.path, file.content]));
|
|
@@ -395,8 +399,9 @@ async function runSkillMirrorExport(args) {
|
|
|
395
399
|
.filter((file) => typeof file.path === 'string' && typeof file.content === 'string');
|
|
396
400
|
if (!files.length)
|
|
397
401
|
throw new Error(`No mirrored files returned for ${name}.`);
|
|
398
|
-
writeSkillPackage(to, files, { force: args.includes('--force'), check: args.includes('--check'), exact: true })
|
|
399
|
-
|
|
402
|
+
if (!writeSkillPackage(to, files, { force: args.includes('--force'), check: args.includes('--check'), exact: true }))
|
|
403
|
+
return;
|
|
404
|
+
console.log(`Exported ${name} revision ${Number(data.revision || 0)} (${String(data.contentHash || '')})`);
|
|
400
405
|
}
|
|
401
406
|
async function loadMirrorPackage(skill, args) {
|
|
402
407
|
const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}${mirrorLookupQuery(args)}`);
|
|
@@ -428,7 +433,16 @@ function runEditor(target) {
|
|
|
428
433
|
throw new Error(`editor exited with status ${result.status}`);
|
|
429
434
|
}
|
|
430
435
|
}
|
|
436
|
+
/** A skill the CLI ships as its own bundle has one canon, the bundle, and
|
|
437
|
+
* `greprag load` serves it for that skill's global id. A mirror write under
|
|
438
|
+
* its name would land in a row nothing reads. */
|
|
439
|
+
function refuseBundledSkillWrite(skill) {
|
|
440
|
+
if (!(0, load_1.bundleOwnsSkill)(skill))
|
|
441
|
+
return;
|
|
442
|
+
throw new Error(`${skill} ships with the greprag CLI, so its canon is the bundle, not the skill mirror. Edit packages/cli/skill/${skill}/ in the greprag repo.`);
|
|
443
|
+
}
|
|
431
444
|
async function applySkillPackage(params) {
|
|
445
|
+
refuseBundledSkillWrite(params.skill);
|
|
432
446
|
const hash = (0, skill_mirror_client_1.computeMirrorHash)(params.files);
|
|
433
447
|
const body = {
|
|
434
448
|
files: params.files,
|
|
@@ -465,6 +479,7 @@ async function runSkillEdit(args) {
|
|
|
465
479
|
const skill = args[0];
|
|
466
480
|
if (!skill)
|
|
467
481
|
throw new Error('Usage: greprag skill edit <name> [--dir <dir>] [--no-open] [--force] [--keep]');
|
|
482
|
+
refuseBundledSkillWrite(skill);
|
|
468
483
|
const explicitDir = getArg(args, '--dir') || getArg(args, '--to');
|
|
469
484
|
const noOpen = args.includes('--no-open');
|
|
470
485
|
const keep = args.includes('--keep') || Boolean(explicitDir);
|
|
@@ -476,7 +491,7 @@ async function runSkillEdit(args) {
|
|
|
476
491
|
force: args.includes('--force') || !explicitDir,
|
|
477
492
|
exact: true,
|
|
478
493
|
});
|
|
479
|
-
console.log(`Editing ${skill} revision ${loaded.revision} (${loaded.contentHash
|
|
494
|
+
console.log(`Editing ${skill} revision ${loaded.revision} (${loaded.contentHash})`);
|
|
480
495
|
console.log(`Work dir: ${workDir}`);
|
|
481
496
|
const skipEditor = noOpen || codexHarnessDetected();
|
|
482
497
|
if (skipEditor) {
|
|
@@ -498,7 +513,7 @@ async function runSkillEdit(args) {
|
|
|
498
513
|
}
|
|
499
514
|
await assertCurrentMirrorHash({ skill, expectedHash: loaded.contentHash, localFiles: edited, args });
|
|
500
515
|
const data = await applySkillPackage({ skill, files: edited, expectedHash: loaded.contentHash, args });
|
|
501
|
-
console.log(`Applied ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(edited))
|
|
516
|
+
console.log(`Applied ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(edited))})`);
|
|
502
517
|
if (!keep)
|
|
503
518
|
fs.rmSync(workDir, { recursive: true, force: true });
|
|
504
519
|
}
|
|
@@ -521,7 +536,7 @@ async function runSkillMirrorApply(args) {
|
|
|
521
536
|
expectedRevision: expectedRevisionRaw ? Number(expectedRevisionRaw) : undefined,
|
|
522
537
|
args,
|
|
523
538
|
});
|
|
524
|
-
console.log(`Applied ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(files))
|
|
539
|
+
console.log(`Applied ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(files))})`);
|
|
525
540
|
}
|
|
526
541
|
function snapshotNameArg(args) {
|
|
527
542
|
const explicit = getArg(args, '--name');
|
|
@@ -593,7 +608,8 @@ async function runSkillMirrorSnapshot(args) {
|
|
|
593
608
|
if (!to)
|
|
594
609
|
throw new Error('Usage: greprag skill mirror snapshot restore <skill> --name <snapshot> --to <dir> [--force] [--check]');
|
|
595
610
|
const files = snapshotPresentFiles(snapshot);
|
|
596
|
-
writeSkillPackage(to, files, { force: rest.includes('--force'), check: rest.includes('--check'), exact: true })
|
|
611
|
+
if (!writeSkillPackage(to, files, { force: rest.includes('--force'), check: rest.includes('--check'), exact: true }))
|
|
612
|
+
return;
|
|
597
613
|
console.log(`Restored snapshot ${name} for ${skill} to ${path.resolve(to)}. Run apply with the expected revision if this should become canon.`);
|
|
598
614
|
return;
|
|
599
615
|
}
|
|
@@ -614,7 +630,7 @@ async function runSkillMirrorSnapshot(args) {
|
|
|
614
630
|
}
|
|
615
631
|
await assertCurrentMirrorHash({ skill, expectedHash, localFiles: files, args: rest });
|
|
616
632
|
const data = await applySkillPackage({ skill, files, expectedHash, args: rest });
|
|
617
|
-
console.log(`Promoted snapshot ${name} for ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(files))
|
|
633
|
+
console.log(`Promoted snapshot ${name} for ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(files))})`);
|
|
618
634
|
return;
|
|
619
635
|
}
|
|
620
636
|
throw new Error('Usage: greprag skill mirror snapshot create|list|show|restore|promote <skill> [--name <snapshot>]');
|
|
@@ -40,6 +40,11 @@ Gains land automatically at session start as a briefcase (fuse line + focused
|
|
|
40
40
|
doc) and SELF-APPLY on the skill's next invocation.`;
|
|
41
41
|
async function runSkillGain(args) {
|
|
42
42
|
const sub = args[0];
|
|
43
|
+
// Help needs no key: anything that is not a verb, a help flag included.
|
|
44
|
+
if (sub !== 'list' && sub !== 'done' && sub !== 'reject') {
|
|
45
|
+
console.log(HELP);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
43
48
|
const cfg = getConfig();
|
|
44
49
|
if (!cfg.apiKey) {
|
|
45
50
|
console.error('GREPRAG_API_KEY not set — run `greprag init` first.');
|
|
@@ -112,5 +117,4 @@ async function runSkillGain(args) {
|
|
|
112
117
|
}
|
|
113
118
|
return;
|
|
114
119
|
}
|
|
115
|
-
console.log(HELP);
|
|
116
120
|
}
|
|
@@ -60,6 +60,7 @@ exports.isFixSpawnDispatch = isFixSpawnDispatch;
|
|
|
60
60
|
exports.computeBootstrapFriction = computeBootstrapFriction;
|
|
61
61
|
exports.scanKeywords = scanKeywords;
|
|
62
62
|
exports.computeStateValues = computeStateValues;
|
|
63
|
+
const crypto = __importStar(require("crypto"));
|
|
63
64
|
const fs = __importStar(require("fs"));
|
|
64
65
|
/** Stress level enum, encoded numerically so the `state` DSL can threshold it
|
|
65
66
|
* with gte/lte (0 calm · 1 elevated · 2 high). */
|
|
@@ -101,9 +102,25 @@ function isRealUser(rec) {
|
|
|
101
102
|
}
|
|
102
103
|
return true;
|
|
103
104
|
}
|
|
105
|
+
// adr: adr/stress-target-keying.md
|
|
106
|
+
/** Tools whose input is a CHANGE to a file, not a lookup of it. */
|
|
107
|
+
const WRITE_TOOLS = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit']);
|
|
104
108
|
function toolTargetKey(b) {
|
|
105
109
|
const name = b.name || '';
|
|
106
110
|
const inp = (b.input || {});
|
|
111
|
+
// A write repeats only when the same change is issued again. Keyed on the
|
|
112
|
+
// path, every successive edit of one document read as rep + retry, and a
|
|
113
|
+
// report built across operator prompts paged FRICTION HIGH for turns after
|
|
114
|
+
// its last real error (paybot-website 02d0ecbf, 2026-10-05). Writes still
|
|
115
|
+
// reach churn through `writes`, which stays path-keyed.
|
|
116
|
+
if (WRITE_TOOLS.has(name)) {
|
|
117
|
+
let body = '';
|
|
118
|
+
try {
|
|
119
|
+
body = JSON.stringify(inp);
|
|
120
|
+
}
|
|
121
|
+
catch { /* unserializable → empty */ }
|
|
122
|
+
return `${name}:${crypto.createHash('sha1').update(body).digest('hex').slice(0, 16)}`;
|
|
123
|
+
}
|
|
107
124
|
let key = (inp.command ?? inp.file_path ?? inp.pattern ?? inp.path);
|
|
108
125
|
if (typeof key !== 'string') {
|
|
109
126
|
try {
|
|
@@ -149,6 +166,7 @@ function parseTurns(transcriptPath, maxTurns = 40) {
|
|
|
149
166
|
const turns = [];
|
|
150
167
|
const pending = new Map();
|
|
151
168
|
let total = 0;
|
|
169
|
+
let seq = 0;
|
|
152
170
|
let cur = null;
|
|
153
171
|
const flush = () => {
|
|
154
172
|
if (cur) {
|
|
@@ -172,7 +190,7 @@ function parseTurns(transcriptPath, maxTurns = 40) {
|
|
|
172
190
|
if (isRealUser(rec)) {
|
|
173
191
|
flush();
|
|
174
192
|
total++;
|
|
175
|
-
cur = { proseWords: 0, toolTargets: [], errors: 0, nTools: 0, writes: [], failures: [] };
|
|
193
|
+
cur = { proseWords: 0, toolTargets: [], errors: 0, nTools: 0, writes: [], failures: [], successes: [] };
|
|
176
194
|
continue;
|
|
177
195
|
}
|
|
178
196
|
if (!cur)
|
|
@@ -202,20 +220,25 @@ function parseTurns(transcriptPath, maxTurns = 40) {
|
|
|
202
220
|
}
|
|
203
221
|
else if (rec.type === 'user') { // tool_result records
|
|
204
222
|
for (const b of blocks(rec)) {
|
|
205
|
-
if (b
|
|
223
|
+
if (!b || b.type !== 'tool_result')
|
|
224
|
+
continue;
|
|
225
|
+
// The soft regex feeds `errors` (the validated stress feature) only.
|
|
226
|
+
if (resultIsError(b))
|
|
206
227
|
cur.errors++;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
228
|
+
const useId = typeof b.tool_use_id === 'string' ? b.tool_use_id : '';
|
|
229
|
+
const prior = useId ? pending.get(useId) : null;
|
|
230
|
+
if (!prior)
|
|
231
|
+
continue;
|
|
232
|
+
seq++;
|
|
233
|
+
// A ToolFailure (the bootstrap classifier's input) requires HARD
|
|
234
|
+
// evidence the command itself failed — the harness's is_error flag,
|
|
235
|
+
// never error-looking output text: a successful `… | grep -i error`
|
|
236
|
+
// matches ERROR_RE by construction (false-page a0d1a7e2 2026-07-12).
|
|
237
|
+
// Everything else settled OK — the evidence that clears a signature.
|
|
238
|
+
if (b.is_error === true)
|
|
239
|
+
cur.failures.push({ ...prior, seq, error: toolResultText(b).slice(0, 500) });
|
|
240
|
+
else
|
|
241
|
+
cur.successes.push({ ...prior, seq });
|
|
219
242
|
}
|
|
220
243
|
}
|
|
221
244
|
}
|
|
@@ -446,9 +469,20 @@ function commandFamily(command) {
|
|
|
446
469
|
// Quote-stripped: a tool token inside a quoted payload must not mint the
|
|
447
470
|
// dedupe key — the original incident's fix-spawn argument embedded `node`,
|
|
448
471
|
// colliding with the real failure's family and inflating `repeats` to 2.
|
|
472
|
+
// Bare `node` keys on the script's BASENAME: `node a.js` and `node b.js` are
|
|
473
|
+
// different shapes, while `node <typo-dir>/h.js` and `node <dir>/h.js` are one
|
|
474
|
+
// (the resolved-typo re-fire, sift 65bed6f0 2026-09-18).
|
|
449
475
|
const c = stripQuotedSpans(command).toLowerCase();
|
|
450
|
-
const m = c.match(/\b(?:npm\s+run\s+\S+|npm\s+\S+|pnpm\s+\S+|yarn\s+\S+|npx\s+\S+|fnm\s+\S+|corepack\s+\S+|turbo\s+\S+|vitest\b|jest\b|tsc\b|node
|
|
451
|
-
|
|
476
|
+
const m = c.match(/\b(?:npm\s+run\s+\S+|npm\s+\S+|pnpm\s+\S+|yarn\s+\S+|npx\s+\S+|fnm\s+\S+|corepack\s+\S+|turbo\s+\S+|vitest\b|jest\b|tsc\b|node(?:\s+\S+)?)/);
|
|
477
|
+
const family = (m?.[0] || c).replace(/\s+/g, ' ');
|
|
478
|
+
const node = family.match(/^node (\S+)$/);
|
|
479
|
+
return (node ? `node ${node[1].replace(/\\/g, '/').split('/').pop()}` : family).slice(0, 80);
|
|
480
|
+
}
|
|
481
|
+
/** `run` settled after `failure` in transcript order (turn, then seq). */
|
|
482
|
+
function settledAfter(run, runTurn, failure, failureTurn) {
|
|
483
|
+
if (runTurn !== failureTurn)
|
|
484
|
+
return runTurn > failureTurn;
|
|
485
|
+
return (run.seq ?? -1) > (failure.seq ?? Infinity);
|
|
452
486
|
}
|
|
453
487
|
/** Detect setup/toolchain failures that are likely repairable once for all future
|
|
454
488
|
* sessions. This is deliberately narrower than generic stress: source-code
|
|
@@ -468,21 +502,23 @@ function computeBootstrapFriction(turns) {
|
|
|
468
502
|
if (!failures.length)
|
|
469
503
|
return none;
|
|
470
504
|
const latest = failures[failures.length - 1];
|
|
471
|
-
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
// (
|
|
476
|
-
//
|
|
477
|
-
//
|
|
505
|
+
const latestFamily = commandFamily(latest.failure.command);
|
|
506
|
+
// A signature lives only until a later SUCCESSFUL run resolves it. Without
|
|
507
|
+
// this the directive re-paged every turn the failure stayed in the window:
|
|
508
|
+
// after compliance (incident 250ac565, 2026-07-12) and after the very next
|
|
509
|
+
// run of the same shape succeeded (sift 65bed6f0, 2026-09-18). Two resolvers:
|
|
510
|
+
// - dispatch: a `greprag fix spawn` that RAN at/after the latest failure's
|
|
511
|
+
// turn — the remedy is in motion (same-turn spawns count: conservative-quiet);
|
|
512
|
+
// - recovery: the same command shape succeeded after the latest failure —
|
|
513
|
+
// the toolchain works now, the failure was transient.
|
|
514
|
+
// A FAILED run never resolves. A recurrence after either re-fires, since the
|
|
515
|
+
// new failure becomes `latest` and postdates the resolver.
|
|
478
516
|
for (let turn = latest.turn; turn < turns.length; turn++) {
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
const cmd = tg.slice('Bash:'.length);
|
|
485
|
-
if (isFixSpawnDispatch(cmd) && !failedCmds.has(cmd))
|
|
517
|
+
for (const s of turns[turn].successes || []) {
|
|
518
|
+
if (isFixSpawnDispatch(s.command))
|
|
519
|
+
return none;
|
|
520
|
+
if (commandFamily(s.command) === latestFamily
|
|
521
|
+
&& settledAfter(s, turn, latest.failure, latest.turn))
|
|
486
522
|
return none;
|
|
487
523
|
}
|
|
488
524
|
}
|
|
@@ -491,7 +527,7 @@ function computeBootstrapFriction(turns) {
|
|
|
491
527
|
const key = `${f.signal}:${commandFamily(f.failure.command)}`;
|
|
492
528
|
counts.set(key, (counts.get(key) || 0) + 1);
|
|
493
529
|
}
|
|
494
|
-
const latestKey = `${latest.signal}:${
|
|
530
|
+
const latestKey = `${latest.signal}:${latestFamily}`;
|
|
495
531
|
const repeats = counts.get(latestKey) || 1;
|
|
496
532
|
const strongSingle = latest.signal === 'command-not-found'
|
|
497
533
|
|| latest.signal === 'fnm-node-bootstrap'
|
package/dist/delivery-config.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
exports.deliveryProfilePath = deliveryProfilePath;
|
|
38
|
+
exports.draftDeliveryProfile = draftDeliveryProfile;
|
|
38
39
|
exports.resolveDeliveryProfile = resolveDeliveryProfile;
|
|
39
40
|
const fs = __importStar(require("fs"));
|
|
40
41
|
const path = __importStar(require("path"));
|
|
@@ -160,6 +161,74 @@ function parseProfile(root, raw) {
|
|
|
160
161
|
deploySource, releaseSource, errors,
|
|
161
162
|
};
|
|
162
163
|
}
|
|
164
|
+
function git(cwd, command) {
|
|
165
|
+
try {
|
|
166
|
+
return (0, proc_1.safeExecSync)(`git ${command}`, {
|
|
167
|
+
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
168
|
+
}).trim();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const PROVIDER_SIGNALS = [
|
|
175
|
+
['wrangler.toml', 'Cloudflare Workers/Pages (wrangler)'],
|
|
176
|
+
['wrangler.jsonc', 'Cloudflare Workers/Pages (wrangler)'],
|
|
177
|
+
['wrangler.json', 'Cloudflare Workers/Pages (wrangler)'],
|
|
178
|
+
['Procfile', 'Heroku (Procfile)'],
|
|
179
|
+
['app.json', 'Heroku (app.json)'],
|
|
180
|
+
['fly.toml', 'Fly.io'],
|
|
181
|
+
['vercel.json', 'Vercel'],
|
|
182
|
+
['netlify.toml', 'Netlify'],
|
|
183
|
+
['Dockerfile', 'Docker image'],
|
|
184
|
+
];
|
|
185
|
+
/** Draft a profile from the repo's own Git facts. Deploy targets stay a stub until a deploy doc exists. */
|
|
186
|
+
// adr: adr/delivery-profile.md
|
|
187
|
+
function draftDeliveryProfile(cwd) {
|
|
188
|
+
const root = gitRoot(cwd);
|
|
189
|
+
if (!root)
|
|
190
|
+
return null;
|
|
191
|
+
const remotes = git(root, 'remote').split(/\r?\n/).filter(Boolean);
|
|
192
|
+
const remote = remotes.includes('origin') ? 'origin' : remotes[0] || 'none';
|
|
193
|
+
const originHead = remote !== 'none' ? git(root, `symbolic-ref --short refs/remotes/${remote}/HEAD`) : '';
|
|
194
|
+
const branches = git(root, 'branch --format=%(refname:short)').split(/\r?\n/);
|
|
195
|
+
const defaultBranch = originHead.replace(`${remote}/`, '')
|
|
196
|
+
|| ['main', 'master'].find((b) => branches.includes(b))
|
|
197
|
+
|| git(root, 'branch --show-current') || 'main';
|
|
198
|
+
const commonDir = git(root, 'rev-parse --path-format=absolute --git-common-dir');
|
|
199
|
+
const canonicalRoot = (commonDir ? path.dirname(commonDir) : root).replace(/\\/g, '/');
|
|
200
|
+
const providerSignals = PROVIDER_SIGNALS
|
|
201
|
+
.filter(([file]) => fs.existsSync(path.join(root, file))).map(([, label]) => label);
|
|
202
|
+
try {
|
|
203
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
204
|
+
if (pkg.name && pkg.private !== true)
|
|
205
|
+
providerSignals.push(`npm package ${pkg.name}`);
|
|
206
|
+
}
|
|
207
|
+
catch { /* no package.json */ }
|
|
208
|
+
const unique = [...new Set(providerSignals)];
|
|
209
|
+
const hasDeployDoc = fs.existsSync(path.join(root, 'docs', 'deploy.md'));
|
|
210
|
+
const todo = [];
|
|
211
|
+
if (remote === 'none')
|
|
212
|
+
todo.push('no Git remote: delivery is commit-and-merge only until one is added');
|
|
213
|
+
if (unique.length && !hasDeployDoc) {
|
|
214
|
+
todo.push('write docs/deploy.md (exact deploy command + production check), then add a deploy block pointing at it');
|
|
215
|
+
}
|
|
216
|
+
if (hasDeployDoc) {
|
|
217
|
+
todo.push('rename the "production" target to match docs/deploy.md; set pushDeploys=true if a push alone ships it');
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
root,
|
|
221
|
+
providerSignals: unique,
|
|
222
|
+
todo,
|
|
223
|
+
profile: {
|
|
224
|
+
version: 1,
|
|
225
|
+
git: { defaultBranch, remote, integrationStrategy: 'merge', pushDeploys: false },
|
|
226
|
+
deploy: hasDeployDoc
|
|
227
|
+
? { source: 'docs/deploy.md', canonicalRoot, targets: [{ id: 'production', procedureId: 'deploy:production' }] }
|
|
228
|
+
: undefined,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
163
232
|
// adr: adr/delivery-profile.md
|
|
164
233
|
function resolveDeliveryProfile(cwd) {
|
|
165
234
|
const root = gitRoot(cwd);
|