@sabaiway/agent-workflow-kit 14.34.0 → 14.36.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 +64 -0
- package/README.md +7 -7
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/advisor-writers.md +51 -0
- package/references/agents/review-lens.md +1 -1
- package/references/modes/agents.md +4 -4
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/gates.md +1 -1
- package/references/modes/mcp.md +3 -3
- package/references/modes/recommendations.md +10 -10
- package/references/modes/upgrade.md +1 -1
- package/references/templates/vehicles.json +5 -1
- package/tools/cheap-agents-read.mjs +25 -5
- package/tools/cheap-agents.mjs +28 -17
- package/tools/commands.mjs +1 -1
- package/tools/mcp-registration.mjs +1 -14
- package/tools/mcp.mjs +22 -23
- package/tools/recommendations.mjs +58 -49
- package/tools/reference-profile.mjs +2 -2
- package/tools/vehicle-settings.mjs +41 -19
- package/tools/write-lanes.mjs +35 -7
|
@@ -55,7 +55,7 @@ import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-re
|
|
|
55
55
|
import { probeSandboxMasks, needsMasksApply, patternToRel } from './sandbox-masks.mjs';
|
|
56
56
|
import { shellQuoteArg } from './review-state.mjs';
|
|
57
57
|
import { isFinalCapableDeclaration } from './run-gates.mjs';
|
|
58
|
-
import { loadDeclaration, canonicalCheckerGates, coverageProducerPrecedes, isKitOwnedCheckerGate,
|
|
58
|
+
import { loadDeclaration, canonicalCheckerGates, coverageProducerPrecedes, isKitOwnedCheckerGate, LCOV_PRODUCER_KEY } from './gates-declaration.mjs';
|
|
59
59
|
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
60
60
|
// The typed channel's READ-ONLY leaf only — never tools/mcp.mjs, which reaches the write core.
|
|
61
61
|
import {
|
|
@@ -87,7 +87,8 @@ import { ATTRIBUTION_KEY, LEGACY_ATTRIBUTION_KEY, attributionHidesText, effectiv
|
|
|
87
87
|
import { planGlobalRemoval } from './hide-footprint.mjs';
|
|
88
88
|
import { NODE_EVIDENCE, NODE_EVIDENCE_SCRIPTS, probeNodeEvidence } from './node-evidence.mjs';
|
|
89
89
|
// The vehicle READINESS comes from the read-only half — the same survey `recipes` and `status` read.
|
|
90
|
-
import { surveyExecutorVehicle, readExecutorPosture, executorBodyDiffers, executorTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
|
|
90
|
+
import { surveyExecutorVehicle, readExecutorPosture, executorBodyDiffers, executorTemplate, reviewLensTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION, REVIEW_LENS_VEHICLE } from './cheap-agents-read.mjs';
|
|
91
|
+
import { readVehicles, resolveExecutor, resolveReviewLens } from './vehicle-settings.mjs';
|
|
91
92
|
// The ack store's path, keys, lane registry, fingerprint and guarded reader live in their own leaf
|
|
92
93
|
// (contract: kit/ack-store) — `status` reads the same store, and a second copy is what drifts.
|
|
93
94
|
import {
|
|
@@ -119,6 +120,8 @@ export { ACKS_FILE, ACKS_LANE_KEY, ACKS_WORKTREES_DIR_KEY, ACKS_COVERAGE_DOMAIN_
|
|
|
119
120
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
120
121
|
const toolPath = (rel) => join(HERE, rel);
|
|
121
122
|
const q = shellQuoteArg;
|
|
123
|
+
// A user variant's own step, as the lane leaf words it: the rendered apply of every user item.
|
|
124
|
+
const userStepOf = (variant, root) => applySlot(variant, { toolsDir: HERE, root }).apply;
|
|
122
125
|
const ABSENT_CODE = 'ENOENT';
|
|
123
126
|
const ENFORCEMENT_KIT_ROOT = resolve(HERE, '..');
|
|
124
127
|
const ENFORCEMENT_BUNDLE_ROOT = join(ENFORCEMENT_KIT_ROOT, 'references', 'scripts');
|
|
@@ -163,6 +166,7 @@ export const SEVERITIES = Object.freeze({
|
|
|
163
166
|
'review-recipe': SEVERITY_ATTENTION,
|
|
164
167
|
'gates-declaration': SEVERITY_OPTIONAL,
|
|
165
168
|
'gates-inert': SEVERITY_ATTENTION,
|
|
169
|
+
'gates-inert.unfillable': SEVERITY_ATTENTION,
|
|
166
170
|
'gates-inert.no-verification': SEVERITY_ATTENTION,
|
|
167
171
|
// Both third outcomes are attention, and both therefore block the flow-optimal line MECHANICALLY:
|
|
168
172
|
// an added item makes the verdict non-null. One reports a pair that is dead on a tree the closed
|
|
@@ -255,6 +259,7 @@ export const composeVerdict = ({ attention, optional, skipped }) => {
|
|
|
255
259
|
// banned tokens), never a fixture-coverage gamble. A pure-placeholder template marks a WHAT whose
|
|
256
260
|
// content is fully dynamic (capped at composition by truncation-with-count) — except `profile-gap`, whose
|
|
257
261
|
// WHAT is never truncated: the leaf's suite pins every composed WHAT under the cap.
|
|
262
|
+
const GATES_INERT_WHAT = 'the declared coverage checker ({id}) has no producer before it — it certifies nothing this run, or reads a stale lcov';
|
|
258
263
|
export const WHATS = Object.freeze({
|
|
259
264
|
'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
|
|
260
265
|
'velocity-core.rule': 'auto mode refuses a harness apply, so work cannot stay in auto mode — the apply ask rule is not seeded',
|
|
@@ -267,7 +272,8 @@ export const WHATS = Object.freeze({
|
|
|
267
272
|
'sandbox-provision.installable': 'the OS sandbox is unavailable: {reason} — installable via the doctor (consent tuple {tuple})',
|
|
268
273
|
'review-recipe': '{degraded}',
|
|
269
274
|
'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',
|
|
270
|
-
'gates-inert':
|
|
275
|
+
'gates-inert': GATES_INERT_WHAT,
|
|
276
|
+
'gates-inert.unfillable': GATES_INERT_WHAT,
|
|
271
277
|
'gates-inert.no-verification': "all {n} declared gate(s) are the kit's own checkers — the matrix runs no project-verification command",
|
|
272
278
|
'gates-inert.producer-unrecognized': 'the coverage checker ({id}) has NO producer declared and none offerable, on a tree the changed-line domain barely reaches',
|
|
273
279
|
'gates-inert.coverage-domain-narrow': 'certification covers the assessable minority only — {ext} dominate(s) the tracked tree, and the changed-line domain excludes them',
|
|
@@ -286,6 +292,7 @@ export const WHATS = Object.freeze({
|
|
|
286
292
|
'mcp-channel': "the kit's read-only MCP server is not registered here — path questions and literal searches stay shell strings",
|
|
287
293
|
'mcp-channel.differing': 'an "{server}" MCP entry is already declared here and DIFFERS from the registration this kit copy would write',
|
|
288
294
|
agents: '{n} bundled subagent vehicle(s) not placed (Claude Code) — {ro} read-only, {ex} the full-tool executor; the apply PREVIEWS first',
|
|
295
|
+
'agents.lens-rederive': 'the placed review-lens.md differs from its body derived from docs/ai/vehicles.json; {n} vehicle(s) to place ({ro} read-only, {ex} executor)',
|
|
289
296
|
'executor-vehicle': '{n} slot(s) configured subagent but the executor vehicle is {state}{reason} — every such slot runs solo until it is usable',
|
|
290
297
|
'executor-vehicle.differing': 'the placed executor vehicle differs from the body derived from docs/ai/vehicles.json — a hand edit stands until it is re-derived',
|
|
291
298
|
attribution: 'AGENTS.md says attribution off but .claude/settings.json does not hide the harness trailer and PR text',
|
|
@@ -426,8 +433,8 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
426
433
|
{ id: 'delegated-execution', mode: 'set-recipe', advisorKey: 'review-recipe' },
|
|
427
434
|
{ id: 'agents', mode: 'agents', advisorKey: 'agents' },
|
|
428
435
|
// A DISTINCT capability from the offer above: the offer converges the moment nothing is left to
|
|
429
|
-
// PLACE, which a customized-but-unusable executor
|
|
430
|
-
// state that makes a configured subagent carrier run solo.
|
|
436
|
+
// PLACE and the review lens matches its derived body, which a customized-but-unusable executor
|
|
437
|
+
// also satisfies — so it can never observe the state that makes a configured subagent carrier run solo.
|
|
431
438
|
{ id: 'executor-vehicle', mode: 'agents', advisorKey: 'executor-vehicle' },
|
|
432
439
|
// Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
|
|
433
440
|
// a TRUST-POSTURE change. Velocity presents its full posture at its own consent moment;
|
|
@@ -588,7 +595,8 @@ const probeReviewRecipe = ({ root, deps, add, skip }) => {
|
|
|
588
595
|
}
|
|
589
596
|
}
|
|
590
597
|
if (degraded.length > 0) {
|
|
591
|
-
add('review-recipe', fillTemplate(WHATS['review-recipe'], { degraded: capList(degraded, templateBudget(WHATS['review-recipe'])) }),
|
|
598
|
+
add('review-recipe', fillTemplate(WHATS['review-recipe'], { degraded: capList(degraded, templateBudget(WHATS['review-recipe'])) }),
|
|
599
|
+
userStepOf('review-recipe', root));
|
|
592
600
|
}
|
|
593
601
|
} catch (err) {
|
|
594
602
|
skip('review-recipe', err);
|
|
@@ -651,8 +659,8 @@ const probeGates = ({ root, deps, add, skip, shared }) => {
|
|
|
651
659
|
// gate is not a kit checker. Cause A's apply follows what the FILL can actually do, which the
|
|
652
660
|
// D-8 placement rule changed: when the checker is the declaration's LAST gate and the project's own
|
|
653
661
|
// scripts yield an offerable producer, the fill now PLACES that producer before the checker, so the
|
|
654
|
-
// remedy is the ordinary consent-gated preview.
|
|
655
|
-
//
|
|
662
|
+
// remedy is the ordinary consent-gated preview. Where no offerable producer exists, or the checker
|
|
663
|
+
// is not last, the arm is the user item `gates-inert.unfillable` — the edit is the project's own,
|
|
656
664
|
// because the fill never reorders entries it did not write.
|
|
657
665
|
//
|
|
658
666
|
// An ABSENT or EMPTY declaration belongs to the gates-declaration item; a malformed one throws out
|
|
@@ -765,8 +773,9 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
765
773
|
add(
|
|
766
774
|
'gates-inert',
|
|
767
775
|
fillTemplate(WHATS['gates-inert.producer-unrecognized'], { id }),
|
|
768
|
-
|
|
776
|
+
userStepOf('gates-inert.producer-unrecognized', root),
|
|
769
777
|
'gates-inert.producer-unrecognized',
|
|
778
|
+
`${id} has no producer declared and none offerable — the fill offers only terminating-class script names, so a recognized body under another name (a "ci" script running node --test) is declared as its own gate entry, never marked`,
|
|
770
779
|
);
|
|
771
780
|
return;
|
|
772
781
|
}
|
|
@@ -788,13 +797,12 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
788
797
|
: colliding !== undefined
|
|
789
798
|
? `a producer IS offerable here, but its id "${colliding.id}" is already declared, so the fill refuses it as a collision — rename that gate, or repoint it at the producer form`
|
|
790
799
|
: `no offerable producer exists here, and the fill never reorders entries it did not write`;
|
|
791
|
-
|
|
792
|
-
'gates-inert',
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
);
|
|
800
|
+
if (checkerIsLast && producer) {
|
|
801
|
+
add('gates-inert', fillTemplate(WHATS['gates-inert'], { id }), fillPreviewFor(root, [producer.id]));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
add('gates-inert', fillTemplate(WHATS['gates-inert.unfillable'], { id }), userStepOf('gates-inert.unfillable', root),
|
|
805
|
+
'gates-inert.unfillable', blocked);
|
|
798
806
|
return;
|
|
799
807
|
}
|
|
800
808
|
// The source-size checker is one of the kit's OWN checkers, and it is deliberately NOT
|
|
@@ -887,15 +895,11 @@ const probeSourceSize = ({ root, deps, add, skip }) => {
|
|
|
887
895
|
}
|
|
888
896
|
// The id SQUATTER — the id says the practice is declared, the cmd is not this checker under any
|
|
889
897
|
// reading. `--adopt` here mints the record and is then refused by the fill on the id collision:
|
|
890
|
-
// correct against a squatter, and useless as a RENDERED remedy, so this arm
|
|
891
|
-
// edits that actually resolve it
|
|
898
|
+
// correct against a squatter, and useless as a RENDERED remedy, so this arm is a user item whose
|
|
899
|
+
// step names the edits that actually resolve it.
|
|
892
900
|
if (gates.some((gate) => gate.id === SOURCE_SIZE_GATE_ID)) {
|
|
893
|
-
add(
|
|
894
|
-
'source-size'
|
|
895
|
-
fillTemplate(WHATS['source-size.id-squatter'], {}),
|
|
896
|
-
`HAND-APPLY: in ${GATES_REL}, either rename the "${SOURCE_SIZE_GATE_ID}" gate to an id of its own, or repoint its cmd at node ${q(tool)} --check — then re-run recommendations`,
|
|
897
|
-
'source-size.id-squatter',
|
|
898
|
-
);
|
|
901
|
+
add('source-size', fillTemplate(WHATS['source-size.id-squatter'], {}), userStepOf('source-size.id-squatter', root),
|
|
902
|
+
'source-size.id-squatter');
|
|
899
903
|
return;
|
|
900
904
|
}
|
|
901
905
|
add('source-size', fillTemplate(WHATS['source-size'], {}), adoptLine);
|
|
@@ -1079,16 +1083,19 @@ const probeStateBlockHook = ({ root, deps, add, skip }) => {
|
|
|
1079
1083
|
// surfaced its absence: `agents` is the family's SECOND `.claude/` writer, the `help` Tune tail
|
|
1080
1084
|
// advertises it, and the advisor had no entry for it — so a user who never runs `help` never learned
|
|
1081
1085
|
// it existed while the advisor reported the deployment optimal. Found by the guard, not by an incident.
|
|
1082
|
-
//
|
|
1086
|
+
// A PLACE action and the bundled review lens's `re-derive` (its placed body differs from the one derived from docs/ai/vehicles.json) count as gaps: `already-current` is converged; a customized read-only vehicle or derived lens is
|
|
1083
1087
|
// the user's own edit the writer never clobbers and the advisor never nags about; the executor is not preserved but re-derived from docs/ai/vehicles.json (its `re-derive` answer is not a gap of this offer).
|
|
1084
1088
|
const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
1085
1089
|
try {
|
|
1086
|
-
const
|
|
1090
|
+
const fs = readFsDeps(deps);
|
|
1091
|
+
const answer = readVehicles(root, fs.readFile, fs.lstat);
|
|
1092
|
+
const { posture, reason } = resolveExecutor(answer);
|
|
1087
1093
|
if (posture === null) {
|
|
1088
1094
|
skip('agents', new Error(`${reason} — fix that file first`));
|
|
1089
1095
|
return;
|
|
1090
1096
|
}
|
|
1091
|
-
const
|
|
1097
|
+
const derived = [executorTemplate(posture, deps), reviewLensTemplate(resolveReviewLens(answer).posture, deps)];
|
|
1098
|
+
const preflight = preflightCheapAgents({ cwd: root, derived }, deps);
|
|
1092
1099
|
// The writer refuses below the expected lineage, so offering its command would hand the user a
|
|
1093
1100
|
// guaranteed failure — the honest surface is a stated skip naming the recovery.
|
|
1094
1101
|
if (!preflight.stampOk) {
|
|
@@ -1096,7 +1103,8 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
|
1096
1103
|
return;
|
|
1097
1104
|
}
|
|
1098
1105
|
const toPlace = preflight.plan.filter((item) => item.action === 'place');
|
|
1099
|
-
|
|
1106
|
+
const lensRederive = preflight.plan.some((item) => item.name === REVIEW_LENS_VEHICLE && item.action === 're-derive');
|
|
1107
|
+
if (toPlace.length === 0 && !lensRederive) return; // converged
|
|
1100
1108
|
// The writer's contract is «--dry-run first, ALWAYS» (references/modes/agents.md, and its stated
|
|
1101
1109
|
// invariant «preview by default»), so the rendered line is the PREVIEW — this item joins the
|
|
1102
1110
|
// dry-run-preview class the mode doc already documents for the gates-declaration seeder, where the
|
|
@@ -1105,7 +1113,7 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
|
1105
1113
|
// The hidden-mode reconcile rides the detail, never the apply line: it is wrong to run on a
|
|
1106
1114
|
// VISIBLE deployment, and the apply slot must stay one pure executable command.
|
|
1107
1115
|
const executors = toPlace.filter((item) => item.name === EXECUTOR_VEHICLE).length;
|
|
1108
|
-
add('agents', fillTemplate(WHATS.agents, { n: toPlace.length, ex: executors, ro: toPlace.length - executors }),
|
|
1116
|
+
add('agents', fillTemplate(WHATS[lensRederive ? 'agents.lens-rederive' : 'agents'], { n: toPlace.length, ex: executors, ro: toPlace.length - executors }),
|
|
1109
1117
|
`node ${q(toolPath('cheap-agents.mjs'))} --cwd ${q(root)}`, 'agents',
|
|
1110
1118
|
`hidden-mode deployments only: after the --apply the preview prints, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
|
|
1111
1119
|
);
|
|
@@ -1116,8 +1124,8 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
|
1116
1124
|
|
|
1117
1125
|
// The subagent carrier's ONE instrument (contract: kit/carriers). Neither neighbour can report this
|
|
1118
1126
|
// state: `probeReviewRecipe` skips a slot configured `subagent` by construction, and the offer above
|
|
1119
|
-
// judges only what is left to PLACE — a customized-but-broken
|
|
1120
|
-
// project could declare the carrier and have every such slot run solo, unseen.
|
|
1127
|
+
// judges only what is left to PLACE and the bundled lens's re-derive — a customized-but-broken executor
|
|
1128
|
+
// gives it neither. So a project could declare the carrier and have every such slot run solo, unseen.
|
|
1121
1129
|
const VEHICLE_BROKEN_STATES = Object.freeze(['missing', 'unusable']);
|
|
1122
1130
|
|
|
1123
1131
|
const writerBlock = (root, deps) => {
|
|
@@ -1166,7 +1174,7 @@ const renderBrokenExecutor = ({ root, deps, add, skip }) => {
|
|
|
1166
1174
|
const blocked = writerBlock(root, deps);
|
|
1167
1175
|
if (blocked) preconditions.push(`${blocked} — fix that`);
|
|
1168
1176
|
const room = templateBudget(WHATS['executor-vehicle']) - String(configured.length).length - survey.state.length - 2;
|
|
1169
|
-
|
|
1177
|
+
add(
|
|
1170
1178
|
'executor-vehicle',
|
|
1171
1179
|
fillTemplate(WHATS['executor-vehicle'], {
|
|
1172
1180
|
n: configured.length,
|
|
@@ -1179,9 +1187,10 @@ const renderBrokenExecutor = ({ root, deps, add, skip }) => {
|
|
|
1179
1187
|
'executor-vehicle',
|
|
1180
1188
|
`${preconditions.length ? `precondition: ${preconditions.join('; ')}; ` : ''}${HIDDEN_AGENTS_FOLLOW_UP(root)}`,
|
|
1181
1189
|
);
|
|
1190
|
+
return true;
|
|
1182
1191
|
} catch (err) {
|
|
1183
1192
|
skip('executor-vehicle', err);
|
|
1184
|
-
return
|
|
1193
|
+
return true;
|
|
1185
1194
|
}
|
|
1186
1195
|
};
|
|
1187
1196
|
|
|
@@ -1342,6 +1351,8 @@ export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', 'read-lane', 'workt
|
|
|
1342
1351
|
// spec-check's own census; a recorded decline (the `spec-adoption` ack lane) is the fact that
|
|
1343
1352
|
// silences the item, and an unreadable store is a stated skip so the flow-optimal line never renders
|
|
1344
1353
|
// over it. The not-adopted apply is the spec-layer ensure; the decline preview rides the recipe line.
|
|
1354
|
+
// The one opening of every offer's decline alternative: a chat write the agent runs on the user's decline.
|
|
1355
|
+
export const DECLINE_ALTERNATIVE = 'decline alternative (asked apart, before the confirmation; on your decline the agent records it instead of the apply, never after it): ';
|
|
1345
1356
|
export const probeSpecAdoption = ({ root, deps, add, skip }) => {
|
|
1346
1357
|
try {
|
|
1347
1358
|
const survey = surveySpecAdoption(root, deps);
|
|
@@ -1357,7 +1368,7 @@ export const probeSpecAdoption = ({ root, deps, add, skip }) => {
|
|
|
1357
1368
|
fillTemplate(WHATS['spec-adoption'], {}),
|
|
1358
1369
|
`node ${q(toolPath('ensure-configs.mjs'))} --reconcile --only ${SPEC_LAYER_ENSURE} --cwd ${q(root)}`,
|
|
1359
1370
|
'spec-adoption',
|
|
1360
|
-
|
|
1371
|
+
`${DECLINE_ALTERNATIVE}${decline}`,
|
|
1361
1372
|
);
|
|
1362
1373
|
return;
|
|
1363
1374
|
}
|
|
@@ -1477,13 +1488,12 @@ export const probeAdrStore = ({ root, deps, add, skip }) => {
|
|
|
1477
1488
|
try {
|
|
1478
1489
|
const shape = ADR_LAYOUT_SHAPES[surveyAdrLayoutStrict(root, deps)];
|
|
1479
1490
|
if (!shape) return; // migrated, or no ADR substrate at all — nothing to offer
|
|
1480
|
-
//
|
|
1481
|
-
//
|
|
1482
|
-
// dry-run. A runnable one-liner here would auto-run a tree-mutating migration on stale consent.
|
|
1491
|
+
// The apply is the bare dry run: consent takes two steps, as profile-gap's does — the first yes
|
|
1492
|
+
// runs it and the plan is relayed, the same line with --apply runs only on a second yes over it.
|
|
1483
1493
|
add(
|
|
1484
1494
|
'adr-store-migration',
|
|
1485
1495
|
fillTemplate(WHATS['adr-store-migration'], { shape }),
|
|
1486
|
-
`
|
|
1496
|
+
`node ${q(toolPath('migrate-adr-store.mjs'))} --dry-run --cwd ${q(root)}`,
|
|
1487
1497
|
);
|
|
1488
1498
|
} catch (err) {
|
|
1489
1499
|
skip('adr-store-migration', err);
|
|
@@ -1534,7 +1544,7 @@ export const probeJevConnect = ({ root, deps, add, skip }) => {
|
|
|
1534
1544
|
const place = placeOf({ env, platform: host.platform, hostname: host.hostname, container: host.container });
|
|
1535
1545
|
const decline = `node ${q(toolPath('ack-write.mjs'))} --lane jev-connect --fingerprint ${JEV_CONNECT_DECLINE} --cwd ${q(root)}`;
|
|
1536
1546
|
add('jev-connect', WHATS['jev-connect'], applySlot('jev-connect', { toolsDir: HERE }).apply, 'jev-connect',
|
|
1537
|
-
`${place ? `run the apply in ${place}; ` : ''}
|
|
1547
|
+
`${place ? `run the apply in ${place}; ` : ''}${DECLINE_ALTERNATIVE}${decline}`);
|
|
1538
1548
|
} catch (err) {
|
|
1539
1549
|
skip('jev-connect', err);
|
|
1540
1550
|
}
|
|
@@ -1568,7 +1578,7 @@ export const probeJevSkill = ({ root, deps, add, skip }) => {
|
|
|
1568
1578
|
.map(({ path, reason }) => `left untouched: ${printable(path)} — ${printable(reason)}; `).join('');
|
|
1569
1579
|
const record = `node ${q(toolPath('ack-write.mjs'))} --lane jev-skill --fingerprint ${decline} --cwd ${q(root)}`;
|
|
1570
1580
|
add('jev-skill', what, line, behind.length ? 'jev-skill.earlier' : 'jev-skill',
|
|
1571
|
-
`${place ? `run the apply in ${place}; ` : ''}${untouched}${note ? `${note}; ` : ''}
|
|
1581
|
+
`${place ? `run the apply in ${place}; ` : ''}${untouched}${note ? `${note}; ` : ''}${DECLINE_ALTERNATIVE}${record}`);
|
|
1572
1582
|
} catch (err) {
|
|
1573
1583
|
skip('jev-skill', err);
|
|
1574
1584
|
}
|
|
@@ -1664,10 +1674,9 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
1664
1674
|
// which item fired, and only this field says which ARM of it did — so a consumer (the pre-publish
|
|
1665
1675
|
// smoke asserting a specific false-green never returns) can assert the exact outcome instead of
|
|
1666
1676
|
// pattern-matching prose that is free to be reworded.
|
|
1667
|
-
// `detail` (optional) is an extra rendered `recipe:` line —
|
|
1668
|
-
// the capped WHAT and
|
|
1669
|
-
// line
|
|
1670
|
-
// agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
|
|
1677
|
+
// `detail` (optional) is an extra rendered `recipe:` line — an item's own facts or its alternative,
|
|
1678
|
+
// TOO LONG for the capped WHAT and outside the pure-command apply (such as the bridge-tier
|
|
1679
|
+
// missing-entries line or the worktrees-dir ack fallback). Single-line like apply; absent otherwise.
|
|
1671
1680
|
// Returns whether the item really RENDERED. One probe's disposition depends on another's having
|
|
1672
1681
|
// spoken (the marker-stale ⟷ read-lane.stale precedence), and "the conditions still look right"
|
|
1673
1682
|
// is not the same fact as "an item exists" — the shape backstop can refuse, and a condition read
|
|
@@ -1757,9 +1766,9 @@ Usage:
|
|
|
1757
1766
|
|
|
1758
1767
|
Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
|
|
1759
1768
|
one composed verdict line opens every non-optimal render, then per item {severity · what is
|
|
1760
|
-
sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (
|
|
1761
|
-
|
|
1762
|
-
|
|
1769
|
+
sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (an item's own
|
|
1770
|
+
facts or its alternative, such as the bridge-tier missing-entries line or
|
|
1771
|
+
the worktrees-dir ack fallback) · the harness check line · the exact consent-gated apply one-liner}. --cwd is
|
|
1763
1772
|
REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
|
|
1764
1773
|
section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
|
|
1765
1774
|
stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
|
|
@@ -1770,8 +1779,8 @@ allow rules, <wrapper> * exclusions (codex-exec * where an execute slot delegate
|
|
|
1770
1779
|
sandbox.network.allowedDomains and state dirs in sandbox.filesystem.allowWrite. Where the host
|
|
1771
1780
|
honors the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed
|
|
1772
1781
|
command of the project can reach each host, and every sandboxed command can write each state dir;
|
|
1773
|
-
plan/diff review modes and delegated execution keep their prompt.
|
|
1774
|
-
the user's own step.
|
|
1782
|
+
plan/diff review modes and delegated execution keep their prompt.
|
|
1783
|
+
A user item stays the user's own step.
|
|
1775
1784
|
|
|
1776
1785
|
Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
|
|
1777
1786
|
rendered (items or empty); 1 error; 2 usage.`;
|
|
@@ -22,8 +22,8 @@ const target = (activity, slot, candidates) => Object.freeze({ activity, slot, c
|
|
|
22
22
|
export const TIER_TARGETS = Object.freeze([
|
|
23
23
|
target('epic', 'author', ['subagent', 'solo']),
|
|
24
24
|
target('epic', 'review', ['reviewed', 'solo']),
|
|
25
|
-
target('task', 'author', ['
|
|
26
|
-
target('task', 'execute', ['
|
|
25
|
+
target('task', 'author', ['subagent', 'solo']),
|
|
26
|
+
target('task', 'execute', ['subagent', 'solo']),
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
29
|
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -3,11 +3,14 @@ import { join } from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
export const VEHICLES_REL = 'docs/ai/vehicles.json';
|
|
5
5
|
export const EXECUTOR_DEFAULTS = Object.freeze({ model: 'opus', effort: 'high', fallback: 'sonnet' });
|
|
6
|
+
export const REVIEW_LENS_DEFAULTS = Object.freeze({ model: 'opus', effort: 'high' });
|
|
6
7
|
|
|
7
8
|
const README_KEY = '_README';
|
|
8
9
|
const EXECUTOR_KEY = 'executor';
|
|
9
|
-
const
|
|
10
|
+
const LENS_KEY = 'review-lens';
|
|
11
|
+
const TOP_LEVEL_KEYS = Object.freeze([README_KEY, EXECUTOR_KEY, LENS_KEY]);
|
|
10
12
|
const EXECUTOR_KEYS = Object.freeze(Object.keys(EXECUTOR_DEFAULTS));
|
|
13
|
+
const LENS_KEYS = Object.freeze(Object.keys(REVIEW_LENS_DEFAULTS));
|
|
11
14
|
const OBJECT_TYPE = 'object';
|
|
12
15
|
const STRING_TYPE = 'string';
|
|
13
16
|
const EMPTY_STRING = '';
|
|
@@ -44,6 +47,25 @@ const unreadable = (reason) => ({ state: UNREADABLE, reason });
|
|
|
44
47
|
const describeError = (error) => String(error?.code ?? error?.message ?? error);
|
|
45
48
|
const formatReadError = (error) => `${VEHICLES_REL}: ${UNREADABLE} (${describeError(error)})`;
|
|
46
49
|
|
|
50
|
+
const validateSection = (value, rel, name, keys) => {
|
|
51
|
+
const section = value[name];
|
|
52
|
+
if (!isPlainObject(section)) return refuseValue(rel, name, OBJECT_REQUIRED);
|
|
53
|
+
for (const key of Object.keys(section)) {
|
|
54
|
+
if (!keys.includes(key)) return refuseValue(rel, `${name}.${key}`, UNKNOWN_KEY);
|
|
55
|
+
}
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const qualifiedKey = `${name}.${key}`;
|
|
58
|
+
if (!Object.hasOwn(section, key)) return refuseValue(rel, qualifiedKey, REQUIRED_KEY);
|
|
59
|
+
const token = section[key];
|
|
60
|
+
if (typeof token !== STRING_TYPE) return refuseValue(rel, qualifiedKey, STRING_REQUIRED);
|
|
61
|
+
if (token === EMPTY_STRING) return refuseValue(rel, qualifiedKey, NONEMPTY_REQUIRED);
|
|
62
|
+
if (!TOKEN_PATTERN.test(token) || token.trim() !== token) {
|
|
63
|
+
return refuseValue(rel, qualifiedKey, TOKEN_REQUIRED);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, values: Object.fromEntries(keys.map((key) => [key, section[key]])) };
|
|
67
|
+
};
|
|
68
|
+
|
|
47
69
|
export const validateVehicles = (value, rel = VEHICLES_REL) => {
|
|
48
70
|
if (!isPlainObject(value)) return refuseValue(rel, BODY_NAME, OBJECT_REQUIRED);
|
|
49
71
|
for (const key of Object.keys(value)) {
|
|
@@ -53,26 +75,15 @@ export const validateVehicles = (value, rel = VEHICLES_REL) => {
|
|
|
53
75
|
return refuseValue(rel, README_KEY, STRING_REQUIRED);
|
|
54
76
|
}
|
|
55
77
|
if (!Object.hasOwn(value, EXECUTOR_KEY)) return refuseValue(rel, EXECUTOR_KEY, REQUIRED_KEY);
|
|
56
|
-
const executor = value
|
|
57
|
-
if (!
|
|
58
|
-
|
|
59
|
-
if (!EXECUTOR_KEYS.includes(key)) return refuseValue(rel, `${EXECUTOR_KEY}.${key}`, UNKNOWN_KEY);
|
|
60
|
-
}
|
|
61
|
-
for (const key of EXECUTOR_KEYS) {
|
|
62
|
-
const qualifiedKey = `${EXECUTOR_KEY}.${key}`;
|
|
63
|
-
if (!Object.hasOwn(executor, key)) return refuseValue(rel, qualifiedKey, REQUIRED_KEY);
|
|
64
|
-
const token = executor[key];
|
|
65
|
-
if (typeof token !== STRING_TYPE) return refuseValue(rel, qualifiedKey, STRING_REQUIRED);
|
|
66
|
-
if (token === EMPTY_STRING) return refuseValue(rel, qualifiedKey, NONEMPTY_REQUIRED);
|
|
67
|
-
if (!TOKEN_PATTERN.test(token) || token.trim() !== token) {
|
|
68
|
-
return refuseValue(rel, qualifiedKey, TOKEN_REQUIRED);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (executor.fallback === executor.model) {
|
|
78
|
+
const executor = validateSection(value, rel, EXECUTOR_KEY, EXECUTOR_KEYS);
|
|
79
|
+
if (!executor.ok) return executor;
|
|
80
|
+
if (executor.values.fallback === executor.values.model) {
|
|
72
81
|
return refuseValue(rel, EXECUTOR_KEY, DISTINCT_FALLBACK_REQUIRED);
|
|
73
82
|
}
|
|
74
|
-
const {
|
|
75
|
-
return { ok: true, settings
|
|
83
|
+
const settings = { executor: executor.values };
|
|
84
|
+
if (!Object.hasOwn(value, LENS_KEY)) return { ok: true, settings };
|
|
85
|
+
const lens = validateSection(value, rel, LENS_KEY, LENS_KEYS);
|
|
86
|
+
return lens.ok ? { ok: true, settings: { ...settings, [LENS_KEY]: lens.values } } : lens;
|
|
76
87
|
};
|
|
77
88
|
|
|
78
89
|
const probeEntry = (path, lstat) => {
|
|
@@ -125,3 +136,14 @@ export const resolveExecutor = (answer) => {
|
|
|
125
136
|
if (answer?.state === UNREADABLE) return { posture: null, reason: answer.reason };
|
|
126
137
|
throw fail(CONFIG_EXIT_CODE, `${VEHICLES_REL}: ${UNKNOWN_STATE} ${String(answer?.state)}`);
|
|
127
138
|
};
|
|
139
|
+
|
|
140
|
+
export const resolveReviewLens = (answer) => {
|
|
141
|
+
if (answer?.state === PRESENT && Object.hasOwn(answer.settings, LENS_KEY)) {
|
|
142
|
+
return { posture: { ...answer.settings[LENS_KEY], source: FILE_SOURCE }, reason: null };
|
|
143
|
+
}
|
|
144
|
+
if (answer?.state === PRESENT || answer?.state === ABSENT) {
|
|
145
|
+
return { posture: { ...REVIEW_LENS_DEFAULTS, source: DEFAULT_SOURCE }, reason: null };
|
|
146
|
+
}
|
|
147
|
+
if (answer?.state === UNREADABLE) return { posture: null, reason: answer.reason };
|
|
148
|
+
throw fail(CONFIG_EXIT_CODE, `${VEHICLES_REL}: ${UNKNOWN_STATE} ${String(answer?.state)}`);
|
|
149
|
+
};
|
package/tools/write-lanes.mjs
CHANGED
|
@@ -3,6 +3,10 @@ import { dirCovers } from './declared-paths.mjs';
|
|
|
3
3
|
import { claudeDirOf, quoteArg, skillTargets } from './jev-facts.mjs';
|
|
4
4
|
import { targetsFor } from './jev-connect.mjs';
|
|
5
5
|
import { BUNDLED_SCRIPT_NAMES, INSTALLER_REL, SCRIPTS_REL } from './git-hooks.mjs';
|
|
6
|
+
import { dqUnsafePath } from './checker-claim.mjs';
|
|
7
|
+
import { isLineUnsafe } from './source-size-refusal.mjs';
|
|
8
|
+
import { SOURCE_SIZE_GATE_ID } from './source-size-gate-cmd.mjs';
|
|
9
|
+
import { GATES_REL } from './gates-declaration.mjs';
|
|
6
10
|
|
|
7
11
|
export const LANE_CHAT = 'chat';
|
|
8
12
|
export const LANE_HARNESS = 'harness';
|
|
@@ -16,16 +20,17 @@ export const VARIANT_LANES = Object.freeze({
|
|
|
16
20
|
'autonomy-render': LANE_HARNESS,
|
|
17
21
|
'sandbox-provision': LANE_USER,
|
|
18
22
|
'sandbox-provision.installable': LANE_USER,
|
|
19
|
-
'review-recipe':
|
|
23
|
+
'review-recipe': LANE_USER,
|
|
20
24
|
'gates-declaration': LANE_CHAT,
|
|
21
25
|
'gates-inert': LANE_CHAT,
|
|
26
|
+
'gates-inert.unfillable': LANE_USER,
|
|
22
27
|
'gates-inert.no-verification': LANE_CHAT,
|
|
23
|
-
'gates-inert.producer-unrecognized':
|
|
28
|
+
'gates-inert.producer-unrecognized': LANE_USER,
|
|
24
29
|
'gates-inert.coverage-domain-narrow': LANE_CHAT,
|
|
25
30
|
'source-size': LANE_CHAT,
|
|
26
31
|
'source-size.unminted': LANE_CHAT,
|
|
27
32
|
'source-size.adopted-elsewhere': LANE_CHAT,
|
|
28
|
-
'source-size.id-squatter':
|
|
33
|
+
'source-size.id-squatter': LANE_USER,
|
|
29
34
|
'gate-hook': LANE_HARNESS,
|
|
30
35
|
'gate-hook.marker-stale': LANE_HARNESS,
|
|
31
36
|
'commit-guard': LANE_HARNESS,
|
|
@@ -109,6 +114,15 @@ const getGuardPaths = (facts) => [preCommitOf(facts), join(facts.root, INSTALLER
|
|
|
109
114
|
|
|
110
115
|
const formatCommand = (argv) => argv.map(quoteArg).join(' ');
|
|
111
116
|
const TERMINAL_STEP = 'run in a terminal of your own: ';
|
|
117
|
+
const OWN_STEP = 'your own step — ';
|
|
118
|
+
const RERUN_ADVISOR = ', then run /agent-workflow-kit recommendations';
|
|
119
|
+
|
|
120
|
+
const squatterStep = (facts) => {
|
|
121
|
+
const tool = join(facts.toolsDir, 'source-size-check.mjs');
|
|
122
|
+
const rename = `${OWN_STEP}in ${GATES_REL} rename the "${SOURCE_SIZE_GATE_ID}" gate to an id of its own`;
|
|
123
|
+
if (dqUnsafePath(tool) || isLineUnsafe(tool)) return `${rename}${RERUN_ADVISOR}`;
|
|
124
|
+
return `${rename}, or repoint its cmd at node "${tool}" --check${RERUN_ADVISOR}`;
|
|
125
|
+
};
|
|
112
126
|
|
|
113
127
|
// rejudged: the written value derives from state init's record does not watch, so init re-runs its check before the apply.
|
|
114
128
|
const createForm = (file, preview, apply, writes, envDependent = false, userStep = null, rejudged = false) => Object.freeze({
|
|
@@ -155,13 +169,13 @@ const FORMS = Object.freeze({
|
|
|
155
169
|
'jev-skill': SKILL_FORM,
|
|
156
170
|
'jev-skill.earlier': SKILL_FORM,
|
|
157
171
|
'jev-connect': createForm('jev-connect.mjs', null, [], getStartupPaths, true,
|
|
158
|
-
(facts, argv) =>
|
|
172
|
+
(facts, argv) => `${OWN_STEP}it asks for the key in your terminal and the key never goes into the chat — `
|
|
159
173
|
+ `${TERMINAL_STEP}${formatCommand(argv.apply)}`),
|
|
160
174
|
'family-freshness': Object.freeze({
|
|
161
|
-
userStep: () =>
|
|
175
|
+
userStep: () => `${OWN_STEP}${TERMINAL_STEP}${formatCommand(['npx', '@sabaiway/agent-workflow-kit@latest', 'init'])}`,
|
|
162
176
|
}),
|
|
163
177
|
'sandbox-provision': Object.freeze({
|
|
164
|
-
userStep: (facts) =>
|
|
178
|
+
userStep: (facts) => `${OWN_STEP}the doctor has no install it can run for you here (the reason is this item's text) — `
|
|
165
179
|
+ `${TERMINAL_STEP}cd ${quoteArg(facts.root)} && ${formatCommand(['node', join(facts.toolsDir, 'autonomy-doctor.mjs')])}`,
|
|
166
180
|
}),
|
|
167
181
|
enforcement: createForm('git-hooks.mjs',
|
|
@@ -171,7 +185,21 @@ const FORMS = Object.freeze({
|
|
|
171
185
|
'worktrees-dir': createForm('velocity-profile.mjs',
|
|
172
186
|
['--worktrees', '--cwd', '<root>'], ['--apply', '--worktrees', '--cwd', '<root>'], LOCAL_SETTINGS_WRITES, false, null, true),
|
|
173
187
|
'worktrees-dir.home': Object.freeze({
|
|
174
|
-
userStep: () =>
|
|
188
|
+
userStep: () => `${OWN_STEP}point docs/ai/worktrees.json parentDir at a dedicated directory${RERUN_ADVISOR}`,
|
|
189
|
+
}),
|
|
190
|
+
'gates-inert.unfillable': Object.freeze({
|
|
191
|
+
userStep: () => `${OWN_STEP}in ${GATES_REL} declare or move a suite gate carrying the coverage reporters before the coverage checker`
|
|
192
|
+
+ ` (references/modes/gates.md names the exact form), or drop that checker${RERUN_ADVISOR}`,
|
|
193
|
+
}),
|
|
194
|
+
'gates-inert.producer-unrecognized': Object.freeze({
|
|
195
|
+
userStep: () => `${OWN_STEP}in ${GATES_REL} mark the gate that writes the lcov with "lcovProducer": true`
|
|
196
|
+
+ ` (references/modes/gates.md names the key), or drop the coverage checker${RERUN_ADVISOR}`,
|
|
197
|
+
}),
|
|
198
|
+
'source-size.id-squatter': Object.freeze({ userStep: squatterStep }),
|
|
199
|
+
'review-recipe': Object.freeze({
|
|
200
|
+
userStep: () => `${OWN_STEP}set up the backend the recipe names with /agent-workflow-kit setup (it places the bridge skill`
|
|
201
|
+
+ ' and links its wrappers, and names the CLI install and sign-in that stay yours; /agent-workflow-kit backends shows what is'
|
|
202
|
+
+ ` missing), or re-pick the recipe with /agent-workflow-kit set-recipe${RERUN_ADVISOR}`,
|
|
175
203
|
}),
|
|
176
204
|
});
|
|
177
205
|
|