@sabaiway/agent-workflow-kit 2.1.0 → 3.1.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/CHANGELOG.md +94 -1
- package/README.md +7 -5
- package/SKILL.md +16 -8
- package/bin/install.mjs +11 -2
- package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +171 -22
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/capability.json +1 -1
- package/migrations/3.0.0-hardened-core-loop.md +44 -0
- package/migrations/README.md +1 -1
- package/package.json +2 -2
- package/references/hooks/gate-approve.mjs +1 -1
- package/references/modes/bootstrap.md +3 -3
- package/references/modes/commit-guard.md +19 -0
- package/references/modes/core-evidence.md +15 -0
- package/references/modes/coverage-check.md +12 -0
- package/references/modes/doc-parity.md +7 -6
- package/references/modes/gates.md +7 -6
- package/references/modes/grounding.md +1 -2
- package/references/modes/hook.md +1 -1
- package/references/modes/recommendations.md +1 -0
- package/references/modes/review-state.md +3 -3
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +2 -2
- package/references/modes/worktrees.md +120 -0
- package/references/scripts/archive-decisions.mjs +3 -3
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
- package/references/scripts/install-git-hooks.mjs +90 -18
- package/references/scripts/install-git-hooks.test.mjs +102 -0
- package/references/scripts/migrate-gates-branches.test.mjs +157 -0
- package/references/scripts/migrate-gates.mjs +395 -0
- package/references/scripts/migrate-gates.test.mjs +284 -0
- package/references/templates/agent_rules.md +2 -2
- package/tools/ack-write.mjs +1 -1
- package/tools/atomic-write.mjs +2 -2
- package/tools/autonomy-config.mjs +1 -1
- package/tools/autonomy-doctor.mjs +2 -2
- package/tools/autonomy-write.mjs +1 -1
- package/tools/bridge-settings-read.mjs +1 -1
- package/tools/bridge-settings.mjs +1 -1
- package/tools/changed-surface.mjs +16 -77
- package/tools/cheap-agents.mjs +2 -2
- package/tools/commands.mjs +26 -10
- package/tools/commit-guard.mjs +167 -0
- package/tools/core-evidence.mjs +914 -0
- package/tools/coverage-check.mjs +260 -0
- package/tools/delegation.mjs +1 -1
- package/tools/detect-backends.mjs +3 -3
- package/tools/doc-parity.mjs +11 -27
- package/tools/engine-source.mjs +1 -1
- package/tools/family-members.mjs +1 -1
- package/tools/family-registry.mjs +1 -1
- package/tools/fs-safe.mjs +1 -1
- package/tools/gate-hook.mjs +1 -1
- package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
- package/tools/grounding.mjs +19 -96
- package/tools/hide-footprint.mjs +1 -1
- package/tools/inject-methodology.mjs +72 -41
- package/tools/known-footprint.mjs +1 -1
- package/tools/labels.mjs +1 -1
- package/tools/lcov.mjs +6 -10
- package/tools/lens-region.mjs +1 -1
- package/tools/manifest/validate.mjs +19 -0
- package/tools/migrate-adr-store.mjs +5 -5
- package/tools/orchestration-config.mjs +1 -1
- package/tools/orchestration-write.mjs +1 -1
- package/tools/presentation.mjs +1 -1
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +62 -11
- package/tools/recommendations.mjs +97 -14
- package/tools/release-scan.mjs +10 -3
- package/tools/renderers.mjs +1 -1
- package/tools/review-state.mjs +291 -343
- package/tools/run-gates.mjs +216 -92
- package/tools/sandbox-masks.mjs +14 -14
- package/tools/semver-lite.mjs +1 -1
- package/tools/set-autonomy.mjs +1 -1
- package/tools/set-recipe.mjs +2 -2
- package/tools/setup-backends.mjs +1 -1
- package/tools/surface.mjs +1 -1
- package/tools/uninstall.mjs +1 -1
- package/tools/velocity-profile.mjs +9 -9
- package/tools/view-model.mjs +1 -1
- package/tools/worktrees.mjs +2292 -0
- package/references/modes/fold-completeness.md +0 -30
- package/references/modes/review-ledger.md +0 -34
- package/references/templates/verification-profile.json +0 -10
- package/tools/fold-completeness-run.mjs +0 -1120
- package/tools/fold-completeness.mjs +0 -672
- package/tools/review-ledger-core.mjs +0 -428
- package/tools/review-ledger-write.mjs +0 -647
- package/tools/review-ledger.mjs +0 -630
- package/tools/sarif.mjs +0 -52
- package/tools/verification-profile.mjs +0 -219
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
// Read-only: never writes, never commits, never runs a subscription CLI. The reused probes are all
|
|
29
29
|
// exported read-only surfaces of their owning tools (velocity/autonomy/doctor/backends/recipes/
|
|
30
30
|
// registry/sandbox-masks); the sandbox-masks and settings probes may run read-only git queries.
|
|
31
|
-
// Dependency-free, Node >=
|
|
31
|
+
// Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
32
32
|
|
|
33
|
-
import { readFileSync, readdirSync, lstatSync } from 'node:fs';
|
|
33
|
+
import { readFileSync, readdirSync, lstatSync, existsSync } from 'node:fs';
|
|
34
34
|
import { createHash } from 'node:crypto';
|
|
35
35
|
import { homedir } from 'node:os';
|
|
36
36
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -54,9 +54,12 @@ import { ACTIVITIES, resolveActivityRecipe } from './recipes.mjs';
|
|
|
54
54
|
import { surveyFamily, surveyGateHook } from './family-registry.mjs';
|
|
55
55
|
import { probeSandboxMasks, needsMasksApply } from './sandbox-masks.mjs';
|
|
56
56
|
import { shellQuoteArg } from './review-state.mjs';
|
|
57
|
+
import { isFinalCapableDeclaration } from './run-gates.mjs';
|
|
58
|
+
import { resolveGitHooksPath } from './commit-guard.mjs';
|
|
57
59
|
import { loadConfig } from './orchestration-config.mjs';
|
|
58
60
|
import { DEFAULT_BUNDLE_ROOT, SETTINGS_FILENAME, settingsPath, parseSettings, duplicateKeys } from './bridge-settings-read.mjs';
|
|
59
61
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
62
|
+
import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
|
|
60
63
|
|
|
61
64
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
62
65
|
const toolPath = (rel) => join(HERE, rel);
|
|
@@ -97,6 +100,7 @@ export const SEVERITIES = Object.freeze({
|
|
|
97
100
|
'review-recipe': SEVERITY_ATTENTION,
|
|
98
101
|
'gates-declaration': SEVERITY_OPTIONAL,
|
|
99
102
|
'gate-hook': SEVERITY_OPTIONAL,
|
|
103
|
+
'commit-guard': SEVERITY_OPTIONAL,
|
|
100
104
|
'read-lane': SEVERITY_OPTIONAL,
|
|
101
105
|
'read-lane.stale': SEVERITY_ATTENTION,
|
|
102
106
|
'read-lane.missing': SEVERITY_ATTENTION,
|
|
@@ -105,6 +109,7 @@ export const SEVERITIES = Object.freeze({
|
|
|
105
109
|
'agy-adddir': SEVERITY_OPTIONAL,
|
|
106
110
|
'agy-adddir.invalid-env': SEVERITY_ATTENTION,
|
|
107
111
|
'sandbox-lane': SEVERITY_OPTIONAL,
|
|
112
|
+
'worktrees-dir': SEVERITY_OPTIONAL,
|
|
108
113
|
});
|
|
109
114
|
// The per-item render tags (frozen presentation data, same language contract as the templates).
|
|
110
115
|
export const SEVERITY_LABELS = Object.freeze({
|
|
@@ -152,6 +157,7 @@ export const WHATS = Object.freeze({
|
|
|
152
157
|
'review-recipe': '{degraded}',
|
|
153
158
|
'gates-declaration': 'no declared gate matrix (docs/ai/gates.json absent or empty) — gates prompt one by one; the apply PREVIEWS its --apply line, writes nothing',
|
|
154
159
|
'gate-hook': '{n} declared gate(s) prompt per run — the gate-approval hook is not wired',
|
|
160
|
+
'commit-guard': 'the gate matrix is final-run-capable but no commit-guard arms the pre-commit hook — a commit needs no green receipt yet',
|
|
155
161
|
'read-lane': 'the gate hook is wired but the read-only compound lane is off — pipes/chains of seeded reads still prompt one by one',
|
|
156
162
|
'read-lane.stale': 'the read-lane is ON but the placed gate hook is stale — an old hook never reads lanes.json, so the lane is silently dark; reseed it',
|
|
157
163
|
'read-lane.missing': 'the gate hook is wired but its placed file is missing — every Bash call errors and the read-lane is dark; re-place it',
|
|
@@ -161,6 +167,7 @@ export const WHATS = Object.freeze({
|
|
|
161
167
|
'agy-adddir': 'agy-review is placed but AGY_REVIEW_ALLOW_ADDDIR is not set ({file}) — an oversized code review refuses instead of offloading',
|
|
162
168
|
'agy-adddir.invalid-env': 'AGY_REVIEW_ALLOW_ADDDIR is set to an INVALID value ({value}) — refuse-mode applies and the settings file is shadowed while it is set',
|
|
163
169
|
'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
|
|
170
|
+
'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
|
|
164
171
|
});
|
|
165
172
|
|
|
166
173
|
// ── the shape contract (D2): registry strings AND composed items stay one line under the cap ────
|
|
@@ -205,11 +212,13 @@ export const BENEFITS = Object.freeze({
|
|
|
205
212
|
'review-recipe': 'review coverage — the review recipe you configured actually runs instead of silently degrading',
|
|
206
213
|
'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
|
|
207
214
|
'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
|
|
215
|
+
'commit-guard': 'integrity — commits require the ONE green --final receipt at the exact staged fingerprint (consented pre-commit arm)',
|
|
208
216
|
'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
|
|
209
217
|
'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
|
|
210
218
|
'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
|
|
211
219
|
'agy-adddir': 'large reviews — an oversized agy code review offloads to a staging dir instead of refusing',
|
|
212
220
|
'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
|
|
221
|
+
'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
|
|
213
222
|
});
|
|
214
223
|
|
|
215
224
|
// A typed usage failure (exit 2) — the codebase's typed-error idiom (no classes).
|
|
@@ -292,7 +301,7 @@ const probeSandboxProvision = ({ root, deps, add, skip }) => {
|
|
|
292
301
|
const variant = plan.tuple ? 'sandbox-provision.installable' : 'sandbox-provision';
|
|
293
302
|
const reason = truncatedTo(oneLineOf(p.reason), templateBudget(WHATS[variant]) - (plan.tuple ? String(plan.tuple).length : 0));
|
|
294
303
|
// The doctor reads process.cwd() (deployment-gated) and takes no --cwd flag — the one-liner
|
|
295
|
-
// pins the target project via a cd prefix (
|
|
304
|
+
// pins the target project via a cd prefix (Segment B).
|
|
296
305
|
add('sandbox-provision', fillTemplate(WHATS[variant], { reason, tuple: plan.tuple }), `cd ${q(root)} && node ${q(toolPath('autonomy-doctor.mjs'))}`);
|
|
297
306
|
} catch (err) {
|
|
298
307
|
skip('sandbox-provision', err);
|
|
@@ -301,7 +310,7 @@ const probeSandboxProvision = ({ root, deps, add, skip }) => {
|
|
|
301
310
|
|
|
302
311
|
const probeReviewRecipe = ({ root, deps, add, skip }) => {
|
|
303
312
|
try {
|
|
304
|
-
// The VALIDATED reader (
|
|
313
|
+
// The VALIDATED reader (Segment B): a schema-invalid config (unknown activity/slot,
|
|
305
314
|
// bad recipe) throws here and becomes a stated skip — raw JSON.parse would silently ignore it.
|
|
306
315
|
const { config } = loadConfig(root, deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
|
|
307
316
|
const detection = detectBackends(deps);
|
|
@@ -325,14 +334,14 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
325
334
|
const sg = surveyGateHook(root, deps);
|
|
326
335
|
if (sg.error) throw new Error(sg.error);
|
|
327
336
|
if (sg.declarationPresent && sg.declaredGates === null) throw new Error(sg.declarationError ?? 'gates.json present but unreadable');
|
|
328
|
-
// An ABSENT file and the seeded-EMPTY list are equally undeclared (
|
|
329
|
-
// apply is the consent-gated
|
|
337
|
+
// An ABSENT file and the seeded-EMPTY list are equally undeclared (Segment B); the
|
|
338
|
+
// apply is the consent-gated gates-init PREVIEW (it proposes entries from the project's own
|
|
330
339
|
// scripts and writes only on an explicit yes) — never the runner.
|
|
331
340
|
if (!sg.declarationPresent || sg.declaredGates === 0) {
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
add('gates-declaration', fillTemplate(WHATS['gates-declaration'], {}), `node ${q(toolPath('
|
|
341
|
+
// gates-init writes ONLY with --apply — the apply field stays a PURE executable command
|
|
342
|
+
// (run-exactly-as-rendered feeds it to the shell); the two-step preview semantics live in
|
|
343
|
+
// WHAT, never as prose appended to the command.
|
|
344
|
+
add('gates-declaration', fillTemplate(WHATS['gates-declaration'], {}), `node ${q(toolPath('gates-init.mjs'))} --cwd ${q(root)}`);
|
|
336
345
|
return;
|
|
337
346
|
}
|
|
338
347
|
if (sg.declaredGates > 0 && !sg.wired) {
|
|
@@ -343,6 +352,56 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
343
352
|
}
|
|
344
353
|
};
|
|
345
354
|
|
|
355
|
+
// The D10 consumer surface: once the declaration is FINAL-capable (the canonical core checks
|
|
356
|
+
// present, the checker LAST — the run-gates helper is the one home of that rule), offer the
|
|
357
|
+
// consented commit-guard install — for a MANAGED guardless pre-commit hook AND for an absent one
|
|
358
|
+
// alike; an UNMANAGED hook is a loud skip (manual merge, never an overwrite offer); a
|
|
359
|
+
// non-final-capable declaration gets NO offer (an armed guard over a matrix that cannot mint a
|
|
360
|
+
// receipt would block every commit).
|
|
361
|
+
const INSTALLER_MARKER_RE = /:install-git-hooks\.mjs$/m;
|
|
362
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
363
|
+
// Convergence needs the EXACT canonical armed line the installer writes — full-line anchored on
|
|
364
|
+
// the kit's OWN resolved guard path. A commented-out line or a lookalike path (fake-commit-guard)
|
|
365
|
+
// must never read as armed (that would suppress the offer and leave commits unguarded).
|
|
366
|
+
const canonicalGuardLineRe = () => new RegExp(`^node "${escapeRe(toolPath('commit-guard.mjs'))}" --check$`, 'm');
|
|
367
|
+
const probeCommitGuard = ({ root, deps, add, skip }) => {
|
|
368
|
+
try {
|
|
369
|
+
const read = deps.readFile ?? readFileSync;
|
|
370
|
+
const gatesRaw = (() => {
|
|
371
|
+
try {
|
|
372
|
+
return String(read(join(root, 'docs', 'ai', 'gates.json'), 'utf8'));
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
})();
|
|
377
|
+
if (gatesRaw === null) return; // undeclared — the gates-declaration item owns that story
|
|
378
|
+
const gates = JSON.parse(gatesRaw).gates;
|
|
379
|
+
if (!isFinalCapableDeclaration(gates, root)) return; // no receipt to guard yet — no offer
|
|
380
|
+
const installer = join(root, 'scripts', 'install-git-hooks.mjs');
|
|
381
|
+
if (!(deps.exists ?? existsSync)(installer)) {
|
|
382
|
+
skip('commit-guard', new Error('the hook installer (scripts/install-git-hooks.mjs) is not deployed — run upgrade first'));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const hooksPath = (deps.gitHooksPath ?? resolveGitHooksPath)(root);
|
|
386
|
+
if (hooksPath === null) return; // not a git tree — nothing to hook
|
|
387
|
+
const hook = (() => {
|
|
388
|
+
try {
|
|
389
|
+
return String(read(join(hooksPath, 'pre-commit'), 'utf8'));
|
|
390
|
+
} catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
})();
|
|
394
|
+
if (hook !== null && canonicalGuardLineRe().test(hook)) return; // armed — converged (managed or not)
|
|
395
|
+
if (hook !== null && !INSTALLER_MARKER_RE.test(hook)) {
|
|
396
|
+
skip('commit-guard', new Error('an UNMANAGED pre-commit hook exists — merge the guard line by hand (the installer refuses to overwrite it)'));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
add('commit-guard', fillTemplate(WHATS['commit-guard'], {}), `node ${q(installer)} --commit-guard ${q(toolPath('commit-guard.mjs'))}`);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
skip('commit-guard', err);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
346
405
|
// The read-lane offer (AD-055 Part II, Help-through-Recommendations): once the gate hook is PLACED
|
|
347
406
|
// and WIRED, offer to enable the opt-in read-only compound lane. Mutually exclusive with the
|
|
348
407
|
// gate-hook item BY CONSTRUCTION — that item keys on `!wired`, this one on `wired` — so an unplaced
|
|
@@ -412,7 +471,7 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
|
|
|
412
471
|
if (!needsMasksApply(p)) return;
|
|
413
472
|
const variant = p.staleReal.length > 0 ? 'sandbox-masks.stale-real' : 'sandbox-masks';
|
|
414
473
|
// A stale-real-only fence (EMPTY derivation over a non-empty block) makes the plain --apply
|
|
415
|
-
// REFUSE — the exact one-liner must carry --clear there (
|
|
474
|
+
// REFUSE — the exact one-liner must carry --clear there (Segment B).
|
|
416
475
|
const apply = p.masks.length === 0 && p.staleReal.length > 0 ? `${p.applyCmd} --clear` : p.applyCmd;
|
|
417
476
|
add('sandbox-masks', fillTemplate(WHATS[variant], { n: p.masks.length, m: p.staleReal.length }), apply);
|
|
418
477
|
} catch (err) {
|
|
@@ -425,7 +484,7 @@ const probeAgyAdddir = ({ deps, add, skip }) => {
|
|
|
425
484
|
const probePlaced = deps.findWrapper ?? ((cmd) => findOnPath(cmd, deps).state === 'present');
|
|
426
485
|
if (!probePlaced('agy-review')) return;
|
|
427
486
|
// Configured means a VALID boolean value (the wrapper validates and falls back to the default
|
|
428
|
-
// on garbage — presence alone proves nothing
|
|
487
|
+
// on garbage — presence alone proves nothing). An explicit valid 0 is a user CHOICE
|
|
429
488
|
// (refuse mode) — respected, never nagged. env > file, the wrappers' own precedence.
|
|
430
489
|
const isValidBool = (v) => v === '0' || v === '1';
|
|
431
490
|
const env = deps.getenv ?? process.env;
|
|
@@ -626,7 +685,7 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
626
685
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
627
686
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
628
687
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
629
|
-
export const RISK_NOTED_KEYS = Object.freeze(['agy-adddir', 'sandbox-lane', 'read-lane']);
|
|
688
|
+
export const RISK_NOTED_KEYS = Object.freeze(['agy-adddir', 'sandbox-lane', 'read-lane', 'worktrees-dir']);
|
|
630
689
|
|
|
631
690
|
const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
632
691
|
try {
|
|
@@ -683,6 +742,28 @@ const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
|
683
742
|
}
|
|
684
743
|
};
|
|
685
744
|
|
|
745
|
+
// The worktrees-dir arming item (parallel feature worktrees): fires when the resolved worktrees
|
|
746
|
+
// parent dir (docs/ai/worktrees.json parentDir, else the repo's own parent) is not confirmed
|
|
747
|
+
// writable by a trusted host signal. The provision preflight remains the real create+delete test.
|
|
748
|
+
const probeWorktreesDir = ({ root, deps, add, skip }) => {
|
|
749
|
+
try {
|
|
750
|
+
const config = loadWorktreesConfig(root, deps);
|
|
751
|
+
const parent = config.parentDir == null ? dirname(root) : resolve(root, config.parentDir);
|
|
752
|
+
// the SAME canonical derivation the provision preflight probes (worktrees.mjs) — an absent
|
|
753
|
+
// configured dir resolves to its nearest existing ancestor, never a false denial
|
|
754
|
+
const probeDir = resolveProbeDir(parent, deps);
|
|
755
|
+
if (deps.canWriteDir?.(probeDir) === true) return;
|
|
756
|
+
const dir = truncatedTo(oneLineOf(probeDir), templateBudget(WHATS['worktrees-dir']));
|
|
757
|
+
add(
|
|
758
|
+
'worktrees-dir',
|
|
759
|
+
fillTemplate(WHATS['worktrees-dir'], { dir }),
|
|
760
|
+
`HAND-APPLY: add ${JSON.stringify(probeDir)} to sandbox.filesystem.allowWrite in .claude/settings.json on settings-native hosts; on harness-managed hosts grant this dir for the session or use the provision terminal fallback`,
|
|
761
|
+
);
|
|
762
|
+
} catch (err) {
|
|
763
|
+
skip('worktrees-dir', err);
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
686
767
|
// ── assembly (frozen presentation order) ─────────────────────────────────────────────────────────
|
|
687
768
|
const PROBES = Object.freeze([
|
|
688
769
|
probeVelocityItems,
|
|
@@ -690,11 +771,13 @@ const PROBES = Object.freeze([
|
|
|
690
771
|
probeSandboxProvision,
|
|
691
772
|
probeReviewRecipe,
|
|
692
773
|
probeGates,
|
|
774
|
+
probeCommitGuard,
|
|
693
775
|
probeReadLane,
|
|
694
776
|
probeFamilyFreshness,
|
|
695
777
|
probeMasksItem,
|
|
696
778
|
probeAgyAdddir,
|
|
697
779
|
probeSandboxLane,
|
|
780
|
+
probeWorktreesDir,
|
|
698
781
|
]);
|
|
699
782
|
|
|
700
783
|
export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
@@ -737,7 +820,7 @@ export const formatRecommendations = ({ items, skips }) => {
|
|
|
737
820
|
const verdict = composeVerdict({ attention, optional: items.length - attention, skipped: skips.length });
|
|
738
821
|
if (verdict == null) {
|
|
739
822
|
// The flow-optimal claim renders ONLY when every probe ran and none fired — an empty item
|
|
740
|
-
// list beside skipped checks would falsely attest optimality (
|
|
823
|
+
// list beside skipped checks would falsely attest optimality (Segment B).
|
|
741
824
|
lines.push(RECOMMENDATIONS_EMPTY_LINE);
|
|
742
825
|
return lines.join('\n');
|
|
743
826
|
}
|
package/tools/release-scan.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// tripping the (narrow) attribution rules. Only the TEST is self-excluded (it holds attribution
|
|
9
9
|
// fixtures); the scanner source contains no attribution construction, so it is scanned normally.
|
|
10
10
|
//
|
|
11
|
-
// Pure `scanText` for unit fixtures; `scanPaths`/CLI for the release tree. Dependency-free, Node >=
|
|
11
|
+
// Pure `scanText` for unit fixtures; `scanPaths`/CLI for the release tree. Dependency-free, Node >= 22.
|
|
12
12
|
|
|
13
13
|
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
14
14
|
import { join, extname, basename, resolve } from 'node:path';
|
|
@@ -35,6 +35,9 @@ const ATTRIBUTION = [
|
|
|
35
35
|
{ re: /reviewed by (claude|codex|chatgpt|gpt|gemini|copilot|cursor|the (ai|model|agent))/i, label: 'AI review attribution' },
|
|
36
36
|
{ re: /authored[- ]by[: ][^\n]{0,40}\b(claude|chatgpt|gemini|copilot)\b/i, label: 'AI authorship attribution' },
|
|
37
37
|
];
|
|
38
|
+
const REVIEWER_IDENTITY = [
|
|
39
|
+
{ re: /\b(?:agy|codex)(?:\s+|-)r\d+(?:(?:\+|\/)r?\d+)*(?:-[a-z0-9]+)*\b/i, label: 'reviewer-round identity' },
|
|
40
|
+
];
|
|
38
41
|
|
|
39
42
|
const allowlistCovers = (matched, allowlist) =>
|
|
40
43
|
allowlist.some((term) => matched.toLowerCase().includes(term.toLowerCase()));
|
|
@@ -49,6 +52,10 @@ export const scanText = (text, { allowlist = ALLOWLIST } = {}) => {
|
|
|
49
52
|
findings.push({ line: idx + 1, kind: 'attribution', detail: `${label}: "${m[0]}"` });
|
|
50
53
|
}
|
|
51
54
|
}
|
|
55
|
+
for (const { re, label } of REVIEWER_IDENTITY) {
|
|
56
|
+
const m = line.match(re);
|
|
57
|
+
if (m) findings.push({ line: idx + 1, kind: 'reviewer-identity', detail: `${label}: "${m[0]}"` });
|
|
58
|
+
}
|
|
52
59
|
});
|
|
53
60
|
return findings;
|
|
54
61
|
};
|
|
@@ -83,7 +90,7 @@ export const scanPaths = (paths, opts = {}) => {
|
|
|
83
90
|
return report;
|
|
84
91
|
};
|
|
85
92
|
|
|
86
|
-
const main = (argv) => {
|
|
93
|
+
export const main = (argv) => {
|
|
87
94
|
const reportOnly = argv.includes('--report');
|
|
88
95
|
const paths = argv.filter((a) => a !== '--report');
|
|
89
96
|
if (paths.length === 0) {
|
|
@@ -93,7 +100,7 @@ const main = (argv) => {
|
|
|
93
100
|
const report = scanPaths(paths);
|
|
94
101
|
for (const r of report) console.log(`[${r.kind}] ${r.file}:${r.line} — ${r.detail}`);
|
|
95
102
|
if (report.length === 0) {
|
|
96
|
-
console.log('[release-scan] clean — no AI attribution found.');
|
|
103
|
+
console.log('[release-scan] clean — no AI attribution or reviewer-round identity found.');
|
|
97
104
|
return;
|
|
98
105
|
}
|
|
99
106
|
console.log(`\n[release-scan] ${report.length} finding(s).`);
|
package/tools/renderers.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// the JSON mode is handled by the caller (it prints the envelope, not a render). The renderers do
|
|
6
6
|
// LAYOUT + GLYPHS only — phrasing + headline counts already live in the ViewModel.
|
|
7
7
|
//
|
|
8
|
-
// Pure, no side effects, Node >=
|
|
8
|
+
// Pure, no side effects, Node >= 22.
|
|
9
9
|
|
|
10
10
|
import { BLOCK_TITLES, SETTINGS_LABELS, glyphsFor, NO_DEPLOYMENT } from './presentation.mjs';
|
|
11
11
|
|