@sabaiway/agent-workflow-kit 14.33.0 → 14.35.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 +77 -0
- package/README.md +6 -6
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/migrations/1.2.0-agent-attribution.md +10 -8
- package/migrations/3.0.0-hardened-core-loop.md +9 -13
- package/package.json +1 -1
- package/references/advisor-writers.md +51 -0
- package/references/contracts.md +2 -2
- package/references/modes/bootstrap.md +5 -3
- package/references/modes/gates.md +1 -1
- package/references/modes/mcp.md +3 -3
- package/references/modes/recommendations.md +12 -10
- package/references/modes/upgrade.md +6 -6
- package/references/shared/composition-handoff.md +3 -3
- package/references/shared/deploy-tail.md +2 -2
- package/references/templates/vehicles.json +1 -1
- package/tools/apply-danger-check.mjs +37 -12
- package/tools/attribution.mjs +133 -0
- package/tools/cheap-agents-read.mjs +12 -0
- package/tools/commands.mjs +1 -1
- package/tools/commit-guard.mjs +3 -16
- package/tools/ensure-configs.mjs +6 -6
- package/tools/ensure-executor.mjs +13 -30
- package/tools/ensure-vocabulary.mjs +4 -4
- package/tools/git-hooks.mjs +41 -3
- package/tools/hide-footprint.mjs +87 -61
- package/tools/init-project.mjs +1 -1
- package/tools/mcp-registration.mjs +1 -14
- package/tools/mcp.mjs +22 -23
- package/tools/recommendations.mjs +131 -68
- package/tools/upgrade-runlist.mjs +1 -1
- package/tools/write-lanes.mjs +42 -7
package/tools/mcp.mjs
CHANGED
|
@@ -22,21 +22,21 @@
|
|
|
22
22
|
// • the preflight is READ-ONLY: an absent `.claude/` is a NAMED state, and the dir is created on
|
|
23
23
|
// `--apply` only (assertCreatableDirSafe mkdirs, so it may not run in a preview);
|
|
24
24
|
// • a MASKED target (an OS sandbox injects a character device where `.mcp.json` would be — this
|
|
25
|
-
// repo is exactly that case) is not a failure: the kit
|
|
26
|
-
//
|
|
25
|
+
// repo is exactly that case) is not a failure: the kit writes nothing, exits 0 and names the
|
|
26
|
+
// advisor's mcp-channel item, whose check line re-judges outside the sandbox;
|
|
27
27
|
// • never `settings.local.json`; never commits.
|
|
28
28
|
//
|
|
29
29
|
// The read half lives in mcp-registration.mjs, which the advisor and `uninstall` use — so what is
|
|
30
30
|
// there and what would be written are computed by one module, never by two that can disagree.
|
|
31
31
|
//
|
|
32
|
-
// Exit codes: 0 done / dry-run (incl. the
|
|
32
|
+
// Exit codes: 0 done / dry-run (incl. the masked state); 1 precondition STOP; 2 usage.
|
|
33
33
|
// Dependency-free beyond the kit's own exports, Node >= 22. No side effects on import.
|
|
34
34
|
|
|
35
35
|
import {
|
|
36
36
|
lstatSync, openSync, closeSync, fstatSync, writeFileSync, ftruncateSync, fsyncSync,
|
|
37
37
|
renameSync, constants,
|
|
38
38
|
} from 'node:fs';
|
|
39
|
-
import { join, resolve } from 'node:path';
|
|
39
|
+
import { dirname, join, resolve } from 'node:path';
|
|
40
40
|
import { fileURLToPath } from 'node:url';
|
|
41
41
|
import { assertCreatableDirSafe, writeContainedFileAtomic } from './atomic-write.mjs';
|
|
42
42
|
import { isDirectRun } from './direct-run.mjs';
|
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
CLAUDE_DIR_REL,
|
|
47
47
|
ENABLED_KEY,
|
|
48
48
|
MCP_JSON_REL,
|
|
49
|
-
SERVERS_KEY,
|
|
50
49
|
SERVER_NAME,
|
|
51
50
|
SETTINGS_REL,
|
|
52
51
|
STATE,
|
|
@@ -54,9 +53,9 @@ import {
|
|
|
54
53
|
mergeMcpJson,
|
|
55
54
|
mergeSettings,
|
|
56
55
|
readRegistration,
|
|
57
|
-
renderFragments,
|
|
58
56
|
} from './mcp-registration.mjs';
|
|
59
57
|
import { shellQuoteArg } from './review-state.mjs';
|
|
58
|
+
import { applySlot } from './write-lanes.mjs';
|
|
60
59
|
|
|
61
60
|
const q = shellQuoteArg;
|
|
62
61
|
|
|
@@ -83,7 +82,8 @@ Default is --dry-run (a preview that prints the exact entry and writes nothing).
|
|
|
83
82
|
|
|
84
83
|
Without --replace, an existing "${SERVER_NAME}" entry that STRUCTURALLY DIFFERS is refused unwritten (key
|
|
85
84
|
order is ignored). Where ${MCP_JSON_REL} is a device node, FIFO or socket — an OS sandbox
|
|
86
|
-
mask is the usual cause —
|
|
85
|
+
mask is the usual cause — nothing is written, and the report names the advisor's mcp-channel
|
|
86
|
+
item, which applies the registration by the consent route.
|
|
87
87
|
Never writes settings.local.json; never commits.`;
|
|
88
88
|
|
|
89
89
|
export const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
@@ -105,9 +105,9 @@ const lstatNoFollow = (path, lstat = lstatSync) => {
|
|
|
105
105
|
// A target we could not read or parse is never overwritten: a merge over a file whose current
|
|
106
106
|
// content is unknown is a clobber wearing a merge's name.
|
|
107
107
|
//
|
|
108
|
-
// `maskedAllowed` is TRUE for `.mcp.json` alone. That file has a sanctioned
|
|
109
|
-
//
|
|
110
|
-
// mask there is an ordinary refusal rather than a second, unplanned success path.
|
|
108
|
+
// `maskedAllowed` is TRUE for `.mcp.json` alone. That file has a sanctioned route — the advisor's
|
|
109
|
+
// mcp-channel item, whose check line re-judges outside the sandbox — and `settings.json` has none, so
|
|
110
|
+
// a mask there is an ordinary refusal rather than a second, unplanned success path.
|
|
111
111
|
const assertTargetUsable = (target, { maskedAllowed = false } = {}) => {
|
|
112
112
|
if (target.state === STATE.MASKED) {
|
|
113
113
|
if (maskedAllowed) return;
|
|
@@ -168,7 +168,7 @@ export const preflightMcp = ({ cwd, replace = false }, deps = {}) => {
|
|
|
168
168
|
};
|
|
169
169
|
|
|
170
170
|
// The plan is pure over the registration: what is missing, and the exact body each file would get.
|
|
171
|
-
// Both bodies are built even when nothing is written — they ARE the preview
|
|
171
|
+
// Both bodies are built even when nothing is written — they ARE the preview.
|
|
172
172
|
export const planMcp = (registration) => ({
|
|
173
173
|
writeMcpJson: !registration.mcpJson.matches,
|
|
174
174
|
writeSettings: !registration.settings.complete,
|
|
@@ -225,9 +225,8 @@ const writeTarget = (root, target, body, deps, stop) => {
|
|
|
225
225
|
|
|
226
226
|
export const writeMcp = ({ cwd, dryRun = true, replace = false } = {}, deps = {}) => {
|
|
227
227
|
const preflight = preflightMcp({ cwd: cwd ?? deps.cwd ?? process.cwd(), replace }, deps);
|
|
228
|
-
const base = { ...preflight, dryRun, wrote: false };
|
|
229
|
-
if (preflight.masked) return
|
|
230
|
-
if (dryRun) return base;
|
|
228
|
+
const base = { ...preflight, dryRun, replace, wrote: false };
|
|
229
|
+
if (preflight.masked || dryRun) return base;
|
|
231
230
|
|
|
232
231
|
const { root, registration, plan } = preflight;
|
|
233
232
|
const stop = (message) => makeMcpError(MCP_SYMLINK, message);
|
|
@@ -286,19 +285,19 @@ const settingsLine = (result) => {
|
|
|
286
285
|
return ` - ${SETTINGS_REL}: ${dryRun ? 'would set' : 'set'} ${parts.join(' · ')}`;
|
|
287
286
|
};
|
|
288
287
|
|
|
289
|
-
// The
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
// whole-file body pasted as instructed would delete every server it could not see.
|
|
288
|
+
// The masked report names the item that applies the registration by the consent route: its check
|
|
289
|
+
// line, run once more outside the sandbox, reads the real file this mode cannot see through the mask.
|
|
290
|
+
// A run under --replace routes through the differing arm, whose check and apply carry the replace.
|
|
293
291
|
const maskedReport = (result) => {
|
|
294
292
|
const target = result.registration.mcpJson;
|
|
293
|
+
const variant = result.replace ? 'mcp-channel.differing' : 'mcp-channel';
|
|
294
|
+
const { check, apply } = applySlot(variant, { toolsDir: dirname(MCP_TOOL), root: result.root });
|
|
295
295
|
return [
|
|
296
296
|
// The CLASS is what was observed; the sandbox mask is the usual CAUSE but is not established here.
|
|
297
|
-
`agent-workflow MCP registration —
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
`
|
|
301
|
-
indented(result.fragments.settings),
|
|
297
|
+
`agent-workflow MCP registration — nothing was written: ${MCP_JSON_REL} is a ${target.className} (an OS sandbox device mask is the usual cause).`,
|
|
298
|
+
" the advisor's mcp-channel item applies the registration by the consent route — its check line runs once more outside the sandbox and the apply line runs on the yes given on that result:",
|
|
299
|
+
` check: ${check}`,
|
|
300
|
+
` apply: ${apply}`,
|
|
302
301
|
POSTURE_LINE,
|
|
303
302
|
HIDDEN_MODE_LINE,
|
|
304
303
|
].join(LF);
|
|
@@ -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 {
|
|
@@ -75,16 +75,19 @@ import { CHECKER_CLAIM, INITIAL_ADOPTION_REASON, SOURCE_SIZE_GATE_ID, classifySo
|
|
|
75
75
|
// The declared-path resolution + segment containment this item's convergence lane shares with the
|
|
76
76
|
// autonomy render's allowWrite degrade — ONE leaf, so the two answers cannot drift.
|
|
77
77
|
import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
|
|
78
|
-
import {
|
|
78
|
+
import { readPreCommitState } from './git-hooks.mjs';
|
|
79
|
+
export { resolveGitHooksPath, readPreCommitState, canonicalGuardLineRe, INSTALLER_MARKER_RE } from './git-hooks.mjs';
|
|
79
80
|
import { loadConfig } from './orchestration-config.mjs';
|
|
80
81
|
import { homeRelative } from './bridge-state-dirs.mjs';
|
|
81
82
|
import { sandboxEnabled, HOST_HONORS_QUALIFIER } from './bridge-wiring.mjs';
|
|
82
83
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
83
84
|
import { nearestExistingDir, worktreesGrantDir, worktreesGrantHoldsHome } from './worktrees.mjs';
|
|
84
|
-
import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
|
|
85
|
+
import { preflightCheapAgents, writeCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
|
|
86
|
+
import { ATTRIBUTION_KEY, LEGACY_ATTRIBUTION_KEY, attributionHidesText, effectiveAttribution, readAttributionChoice, wrongTypedKey } from './attribution.mjs';
|
|
87
|
+
import { planGlobalRemoval } from './hide-footprint.mjs';
|
|
85
88
|
import { NODE_EVIDENCE, NODE_EVIDENCE_SCRIPTS, probeNodeEvidence } from './node-evidence.mjs';
|
|
86
89
|
// The vehicle READINESS comes from the read-only half — the same survey `recipes` and `status` read.
|
|
87
|
-
import { surveyExecutorVehicle, readExecutorPosture, executorTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
|
|
90
|
+
import { surveyExecutorVehicle, readExecutorPosture, executorBodyDiffers, executorTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
|
|
88
91
|
// The ack store's path, keys, lane registry, fingerprint and guarded reader live in their own leaf
|
|
89
92
|
// (contract: kit/ack-store) — `status` reads the same store, and a second copy is what drifts.
|
|
90
93
|
import {
|
|
@@ -116,6 +119,8 @@ export { ACKS_FILE, ACKS_LANE_KEY, ACKS_WORKTREES_DIR_KEY, ACKS_COVERAGE_DOMAIN_
|
|
|
116
119
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
117
120
|
const toolPath = (rel) => join(HERE, rel);
|
|
118
121
|
const q = shellQuoteArg;
|
|
122
|
+
// A user variant's own step, as the lane leaf words it: the rendered apply of every user item.
|
|
123
|
+
const userStepOf = (variant, root) => applySlot(variant, { toolsDir: HERE, root }).apply;
|
|
119
124
|
const ABSENT_CODE = 'ENOENT';
|
|
120
125
|
const ENFORCEMENT_KIT_ROOT = resolve(HERE, '..');
|
|
121
126
|
const ENFORCEMENT_BUNDLE_ROOT = join(ENFORCEMENT_KIT_ROOT, 'references', 'scripts');
|
|
@@ -160,6 +165,7 @@ export const SEVERITIES = Object.freeze({
|
|
|
160
165
|
'review-recipe': SEVERITY_ATTENTION,
|
|
161
166
|
'gates-declaration': SEVERITY_OPTIONAL,
|
|
162
167
|
'gates-inert': SEVERITY_ATTENTION,
|
|
168
|
+
'gates-inert.unfillable': SEVERITY_ATTENTION,
|
|
163
169
|
'gates-inert.no-verification': SEVERITY_ATTENTION,
|
|
164
170
|
// Both third outcomes are attention, and both therefore block the flow-optimal line MECHANICALLY:
|
|
165
171
|
// an added item makes the verdict non-null. One reports a pair that is dead on a tree the closed
|
|
@@ -194,6 +200,12 @@ export const SEVERITIES = Object.freeze({
|
|
|
194
200
|
// names the subagent carrier over a vehicle that cannot carry it — a configured declaration that
|
|
195
201
|
// is broken, which is what attention means.
|
|
196
202
|
'executor-vehicle': SEVERITY_ATTENTION,
|
|
203
|
+
// A hand edit of the placed executor is not a broken declaration — the vehicle still carries — so
|
|
204
|
+
// re-deriving it is an offer.
|
|
205
|
+
'executor-vehicle.differing': SEVERITY_OPTIONAL,
|
|
206
|
+
// The AGENTS.md block is a configured choice the harness does not honour.
|
|
207
|
+
attribution: SEVERITY_ATTENTION,
|
|
208
|
+
'footprint-global': SEVERITY_OPTIONAL,
|
|
197
209
|
'family-freshness': SEVERITY_ATTENTION,
|
|
198
210
|
'adr-store-migration': SEVERITY_ATTENTION,
|
|
199
211
|
'sandbox-masks': SEVERITY_OPTIONAL,
|
|
@@ -246,6 +258,7 @@ export const composeVerdict = ({ attention, optional, skipped }) => {
|
|
|
246
258
|
// banned tokens), never a fixture-coverage gamble. A pure-placeholder template marks a WHAT whose
|
|
247
259
|
// content is fully dynamic (capped at composition by truncation-with-count) — except `profile-gap`, whose
|
|
248
260
|
// WHAT is never truncated: the leaf's suite pins every composed WHAT under the cap.
|
|
261
|
+
const GATES_INERT_WHAT = 'the declared coverage checker ({id}) has no producer before it — it certifies nothing this run, or reads a stale lcov';
|
|
249
262
|
export const WHATS = Object.freeze({
|
|
250
263
|
'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
|
|
251
264
|
'velocity-core.rule': 'auto mode refuses a harness apply, so work cannot stay in auto mode — the apply ask rule is not seeded',
|
|
@@ -258,7 +271,8 @@ export const WHATS = Object.freeze({
|
|
|
258
271
|
'sandbox-provision.installable': 'the OS sandbox is unavailable: {reason} — installable via the doctor (consent tuple {tuple})',
|
|
259
272
|
'review-recipe': '{degraded}',
|
|
260
273
|
'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',
|
|
261
|
-
'gates-inert':
|
|
274
|
+
'gates-inert': GATES_INERT_WHAT,
|
|
275
|
+
'gates-inert.unfillable': GATES_INERT_WHAT,
|
|
262
276
|
'gates-inert.no-verification': "all {n} declared gate(s) are the kit's own checkers — the matrix runs no project-verification command",
|
|
263
277
|
'gates-inert.producer-unrecognized': 'the coverage checker ({id}) has NO producer declared and none offerable, on a tree the changed-line domain barely reaches',
|
|
264
278
|
'gates-inert.coverage-domain-narrow': 'certification covers the assessable minority only — {ext} dominate(s) the tracked tree, and the changed-line domain excludes them',
|
|
@@ -278,6 +292,9 @@ export const WHATS = Object.freeze({
|
|
|
278
292
|
'mcp-channel.differing': 'an "{server}" MCP entry is already declared here and DIFFERS from the registration this kit copy would write',
|
|
279
293
|
agents: '{n} bundled subagent vehicle(s) not placed (Claude Code) — {ro} read-only, {ex} the full-tool executor; the apply PREVIEWS first',
|
|
280
294
|
'executor-vehicle': '{n} slot(s) configured subagent but the executor vehicle is {state}{reason} — every such slot runs solo until it is usable',
|
|
295
|
+
'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',
|
|
296
|
+
attribution: 'AGENTS.md says attribution off but .claude/settings.json does not hide the harness trailer and PR text',
|
|
297
|
+
'footprint-global': 'legacy kit ignore line(s) sit in {file} — they ignore kit paths in every repository here',
|
|
281
298
|
'family-freshness': '{parts}',
|
|
282
299
|
'adr-store-migration': 'still on the retired 3-tier ADR layout — {shape}',
|
|
283
300
|
'sandbox-masks': '{n} sandbox mask(s) clutter git status — the managed exclude block is absent or stale',
|
|
@@ -343,6 +360,8 @@ export const BENEFITS = Object.freeze({
|
|
|
343
360
|
'mcp-channel': 'velocity — path facts and literal searches arrive as typed tool calls whose arguments are JSON fields, never a shell string',
|
|
344
361
|
agents: 'cost and quiet — cheap-model mechanical work, no shell on a read-only vehicle (no prompt flood); the executor carries slices you verify',
|
|
345
362
|
'executor-vehicle': 'carrier readiness — a slot you configured subagent dispatches the subagent it names instead of silently running solo',
|
|
363
|
+
attribution: 'attribution — the harness adds no Co-Authored-By trailer, PR text or session link, as your AGENTS.md block requires',
|
|
364
|
+
'footprint-global': 'hygiene — the legacy machine-global excludes stop ignoring kit paths in every other repository on this host',
|
|
346
365
|
'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
|
|
347
366
|
'adr-store-migration': 'durability — every decision becomes its own file with a generated navigator, instead of one hand-rotated pile',
|
|
348
367
|
'sandbox-masks': 'zero clutter — a mask git ignores through the block leaves git status; a mount-only one leaves git add -A and the review fingerprint too',
|
|
@@ -352,6 +371,10 @@ export const BENEFITS = Object.freeze({
|
|
|
352
371
|
'jev-connect': 'decisions — a typed choice with a confidence in under a second for your code and the agent\'s scripts, once for every project here',
|
|
353
372
|
'jev-skill': 'the TypeSafe skill for Claude Code, Codex and Antigravity CLI, one pinned verified copy per agent root, from one command you run',
|
|
354
373
|
});
|
|
374
|
+
// The one arm whose gain differs from its base item's (BENEFITS stays keyed by item key).
|
|
375
|
+
export const VARIANT_BENEFITS = Object.freeze({
|
|
376
|
+
'executor-vehicle.differing': 'carrier fidelity — the placed executor runs the model and effort docs/ai/vehicles.json names, not a hand edit',
|
|
377
|
+
});
|
|
355
378
|
|
|
356
379
|
// ── the CLOSED opt-in capability registry (OPT-IN-SHIPS-INVISIBLE) ──────────────────────────────
|
|
357
380
|
// 3.14.0 shipped a capability with every surface an AGENT reads and NO advisor entry, so `upgrade`
|
|
@@ -388,6 +411,8 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
388
411
|
{ id: 'read-lane', mode: 'hook', advisorKey: 'read-lane' },
|
|
389
412
|
{ id: 'commit-guard', mode: 'commit-guard', advisorKey: 'commit-guard' },
|
|
390
413
|
{ id: 'enforcement', mode: 'bootstrap', advisorKey: 'enforcement' },
|
|
414
|
+
{ id: 'attribution', mode: 'bootstrap', advisorKey: 'attribution' },
|
|
415
|
+
{ id: 'footprint-global', mode: 'bootstrap', advisorKey: 'footprint-global' },
|
|
391
416
|
{ id: 'state-block', mode: 'state-block-guard', advisorKey: 'state-block' },
|
|
392
417
|
{ id: 'sandbox-masks', mode: 'sandbox-masks', advisorKey: 'sandbox-masks' },
|
|
393
418
|
{ id: 'mcp-channel', mode: 'mcp', advisorKey: 'mcp-channel' },
|
|
@@ -568,7 +593,8 @@ const probeReviewRecipe = ({ root, deps, add, skip }) => {
|
|
|
568
593
|
}
|
|
569
594
|
}
|
|
570
595
|
if (degraded.length > 0) {
|
|
571
|
-
add('review-recipe', fillTemplate(WHATS['review-recipe'], { degraded: capList(degraded, templateBudget(WHATS['review-recipe'])) }),
|
|
596
|
+
add('review-recipe', fillTemplate(WHATS['review-recipe'], { degraded: capList(degraded, templateBudget(WHATS['review-recipe'])) }),
|
|
597
|
+
userStepOf('review-recipe', root));
|
|
572
598
|
}
|
|
573
599
|
} catch (err) {
|
|
574
600
|
skip('review-recipe', err);
|
|
@@ -631,8 +657,8 @@ const probeGates = ({ root, deps, add, skip, shared }) => {
|
|
|
631
657
|
// gate is not a kit checker. Cause A's apply follows what the FILL can actually do, which the
|
|
632
658
|
// D-8 placement rule changed: when the checker is the declaration's LAST gate and the project's own
|
|
633
659
|
// scripts yield an offerable producer, the fill now PLACES that producer before the checker, so the
|
|
634
|
-
// remedy is the ordinary consent-gated preview.
|
|
635
|
-
//
|
|
660
|
+
// remedy is the ordinary consent-gated preview. Where no offerable producer exists, or the checker
|
|
661
|
+
// is not last, the arm is the user item `gates-inert.unfillable` — the edit is the project's own,
|
|
636
662
|
// because the fill never reorders entries it did not write.
|
|
637
663
|
//
|
|
638
664
|
// An ABSENT or EMPTY declaration belongs to the gates-declaration item; a malformed one throws out
|
|
@@ -745,8 +771,9 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
745
771
|
add(
|
|
746
772
|
'gates-inert',
|
|
747
773
|
fillTemplate(WHATS['gates-inert.producer-unrecognized'], { id }),
|
|
748
|
-
|
|
774
|
+
userStepOf('gates-inert.producer-unrecognized', root),
|
|
749
775
|
'gates-inert.producer-unrecognized',
|
|
776
|
+
`${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`,
|
|
750
777
|
);
|
|
751
778
|
return;
|
|
752
779
|
}
|
|
@@ -768,13 +795,12 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
768
795
|
: colliding !== undefined
|
|
769
796
|
? `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`
|
|
770
797
|
: `no offerable producer exists here, and the fill never reorders entries it did not write`;
|
|
771
|
-
|
|
772
|
-
'gates-inert',
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
);
|
|
798
|
+
if (checkerIsLast && producer) {
|
|
799
|
+
add('gates-inert', fillTemplate(WHATS['gates-inert'], { id }), fillPreviewFor(root, [producer.id]));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
add('gates-inert', fillTemplate(WHATS['gates-inert.unfillable'], { id }), userStepOf('gates-inert.unfillable', root),
|
|
803
|
+
'gates-inert.unfillable', blocked);
|
|
778
804
|
return;
|
|
779
805
|
}
|
|
780
806
|
// The source-size checker is one of the kit's OWN checkers, and it is deliberately NOT
|
|
@@ -867,15 +893,11 @@ const probeSourceSize = ({ root, deps, add, skip }) => {
|
|
|
867
893
|
}
|
|
868
894
|
// The id SQUATTER — the id says the practice is declared, the cmd is not this checker under any
|
|
869
895
|
// reading. `--adopt` here mints the record and is then refused by the fill on the id collision:
|
|
870
|
-
// correct against a squatter, and useless as a RENDERED remedy, so this arm
|
|
871
|
-
// edits that actually resolve it
|
|
896
|
+
// correct against a squatter, and useless as a RENDERED remedy, so this arm is a user item whose
|
|
897
|
+
// step names the edits that actually resolve it.
|
|
872
898
|
if (gates.some((gate) => gate.id === SOURCE_SIZE_GATE_ID)) {
|
|
873
|
-
add(
|
|
874
|
-
'source-size'
|
|
875
|
-
fillTemplate(WHATS['source-size.id-squatter'], {}),
|
|
876
|
-
`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`,
|
|
877
|
-
'source-size.id-squatter',
|
|
878
|
-
);
|
|
899
|
+
add('source-size', fillTemplate(WHATS['source-size.id-squatter'], {}), userStepOf('source-size.id-squatter', root),
|
|
900
|
+
'source-size.id-squatter');
|
|
879
901
|
return;
|
|
880
902
|
}
|
|
881
903
|
add('source-size', fillTemplate(WHATS['source-size'], {}), adoptLine);
|
|
@@ -890,27 +912,11 @@ const probeSourceSize = ({ root, deps, add, skip }) => {
|
|
|
890
912
|
// alike; an UNMANAGED hook is a loud skip naming the user's own step (never an overwrite offer); a
|
|
891
913
|
// non-final-capable declaration gets NO offer (an armed guard over a matrix that cannot mint a
|
|
892
914
|
// receipt would block every commit).
|
|
893
|
-
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
const canonicalGuardLineRe = () => new RegExp(`^node "${escapeRe(toolPath('commit-guard.mjs'))}" --check$`, 'm');
|
|
899
|
-
// The one reader of git's pre-commit: whose hook it is, and whether the canonical guard line arms it.
|
|
900
|
-
export const readPreCommitState = (root, deps = {}) => {
|
|
901
|
-
const hooksPath = (deps.gitHooksPath ?? resolveGitHooksPath)(root);
|
|
902
|
-
if (hooksPath === null) return { state: 'no-hooks-dir', armed: false };
|
|
903
|
-
const path = join(hooksPath, 'pre-commit');
|
|
904
|
-
const hook = (() => {
|
|
905
|
-
try {
|
|
906
|
-
if (readsAbsent(deps, path)) return null;
|
|
907
|
-
return String((deps.readFile ?? readFileSync)(path, 'utf8'));
|
|
908
|
-
} catch {
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
})();
|
|
912
|
-
if (hook === null) return { state: 'absent', armed: false };
|
|
913
|
-
return { state: INSTALLER_MARKER_RE.test(hook) ? 'managed' : 'unmanaged', armed: canonicalGuardLineRe().test(hook) };
|
|
915
|
+
// The advisor asks git under its own process env; only the commit-guard apply passes the installer's.
|
|
916
|
+
const readHookState = (root, deps) => {
|
|
917
|
+
const hook = readPreCommitState(root, { ...deps, env: undefined });
|
|
918
|
+
if (hook.state === 'unreadable') throw new Error(`${hook.path} is unreadable (${hook.code}) — the pre-commit hook cannot be judged`);
|
|
919
|
+
return hook;
|
|
914
920
|
};
|
|
915
921
|
const formsApply = (variant, root) => consoleArgv(variant, { toolsDir: HERE, root }).apply.map(q).join(' ');
|
|
916
922
|
const probeCommitGuard = ({ root, deps, add, skip }) => {
|
|
@@ -931,7 +937,7 @@ const probeCommitGuard = ({ root, deps, add, skip }) => {
|
|
|
931
937
|
skip('commit-guard', new Error(enforcementAdmits(root, deps) ? COMMIT_GUARD_INSTALLER_VIA_ITEM : COMMIT_GUARD_INSTALLER_OWN_STEP));
|
|
932
938
|
return;
|
|
933
939
|
}
|
|
934
|
-
const { state, armed } =
|
|
940
|
+
const { state, armed } = readHookState(root, deps);
|
|
935
941
|
if (armed || state === 'no-hooks-dir') return; // armed (managed or not), or not a git tree — converged
|
|
936
942
|
if (state === 'unmanaged') {
|
|
937
943
|
skip('commit-guard', new Error(COMMIT_GUARD_UNMANAGED_HOOK));
|
|
@@ -977,7 +983,7 @@ const enforcementOffer = (root, deps) => {
|
|
|
977
983
|
if (readEnforcementNode(destination, lstat) === null) continue;
|
|
978
984
|
throw new Error(`${destination} already exists; the enforcement copy would clobber it`);
|
|
979
985
|
}
|
|
980
|
-
if (
|
|
986
|
+
if (readHookState(root, deps).state === 'unmanaged') throw new Error(ENFORCEMENT_UNMANAGED_HOOK);
|
|
981
987
|
return true;
|
|
982
988
|
};
|
|
983
989
|
|
|
@@ -1125,16 +1131,31 @@ const writerBlock = (root, deps) => {
|
|
|
1125
1131
|
}
|
|
1126
1132
|
};
|
|
1127
1133
|
|
|
1134
|
+
const HIDDEN_AGENTS_FOLLOW_UP = (root) => `hidden-mode deployments only: after the apply, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`;
|
|
1135
|
+
|
|
1128
1136
|
const probeExecutorVehicle = ({ root, deps, add, skip }) => {
|
|
1137
|
+
if (renderBrokenExecutor({ root, deps, add, skip })) return;
|
|
1138
|
+
try {
|
|
1139
|
+
if (!executorBodyDiffers(root, deps)) return;
|
|
1140
|
+
const plan = writeCheapAgents({ cwd: root, dryRun: true }, deps);
|
|
1141
|
+
if (!plan.stampOk) throw new Error(`run /agent-workflow-kit upgrade first (deployment stamp ${plan.stamp ?? 'none'}, expected ${EXPECTED_WORKFLOW_VERSION})`);
|
|
1142
|
+
add('executor-vehicle', WHATS['executor-vehicle.differing'], formsApply('executor-vehicle.differing', root),
|
|
1143
|
+
'executor-vehicle.differing', HIDDEN_AGENTS_FOLLOW_UP(root));
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
skip('executor-vehicle', err);
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
const renderBrokenExecutor = ({ root, deps, add, skip }) => {
|
|
1129
1150
|
try {
|
|
1130
1151
|
// The VALIDATED reader, as probeReviewRecipe uses it: a schema-invalid config is a stated skip,
|
|
1131
1152
|
// never an item computed over a shape nothing accepted.
|
|
1132
1153
|
const { config } = loadConfig(root, deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
|
|
1133
1154
|
const configured = Object.entries(ACTIVITIES).flatMap(([activity, def]) =>
|
|
1134
1155
|
Object.keys(def.slots).filter((slot) => config?.[activity]?.[slot] === 'subagent'));
|
|
1135
|
-
if (configured.length === 0) return;
|
|
1156
|
+
if (configured.length === 0) return false;
|
|
1136
1157
|
const survey = (deps.surveyVehicle ?? surveyExecutorVehicle)(root, deps);
|
|
1137
|
-
if (!VEHICLE_BROKEN_STATES.includes(survey.state)) return;
|
|
1158
|
+
if (!VEHICLE_BROKEN_STATES.includes(survey.state)) return false;
|
|
1138
1159
|
const reason = safeLine(survey.reason ?? '');
|
|
1139
1160
|
const { posture, reason: settingsReason } = readExecutorPosture(root, deps);
|
|
1140
1161
|
const stamp = readStamp(join(root, WORKFLOW_STAMP), readFsDeps(deps));
|
|
@@ -1158,10 +1179,12 @@ const probeExecutorVehicle = ({ root, deps, add, skip }) => {
|
|
|
1158
1179
|
// frontmatter or read-only tools included), so those states need no precondition; every precondition above is a refusal the apply would meet.
|
|
1159
1180
|
`node ${q(toolPath('cheap-agents.mjs'))} --apply --cwd ${q(root)}`,
|
|
1160
1181
|
'executor-vehicle',
|
|
1161
|
-
`${preconditions.length ? `precondition: ${preconditions.join('; ')}; ` : ''}
|
|
1182
|
+
`${preconditions.length ? `precondition: ${preconditions.join('; ')}; ` : ''}${HIDDEN_AGENTS_FOLLOW_UP(root)}`,
|
|
1162
1183
|
);
|
|
1184
|
+
return true;
|
|
1163
1185
|
} catch (err) {
|
|
1164
1186
|
skip('executor-vehicle', err);
|
|
1187
|
+
return true;
|
|
1165
1188
|
}
|
|
1166
1189
|
};
|
|
1167
1190
|
|
|
@@ -1314,7 +1337,7 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
1314
1337
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
1315
1338
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
1316
1339
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
1317
|
-
export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement', 'profile-gap', 'jev-connect', 'jev-skill']);
|
|
1340
|
+
export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement', 'profile-gap', 'jev-connect', 'jev-skill', 'attribution', 'footprint-global']);
|
|
1318
1341
|
|
|
1319
1342
|
// The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
|
|
1320
1343
|
// zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
|
|
@@ -1322,6 +1345,8 @@ export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', 'read-lane', 'workt
|
|
|
1322
1345
|
// spec-check's own census; a recorded decline (the `spec-adoption` ack lane) is the fact that
|
|
1323
1346
|
// silences the item, and an unreadable store is a stated skip so the flow-optimal line never renders
|
|
1324
1347
|
// over it. The not-adopted apply is the spec-layer ensure; the decline preview rides the recipe line.
|
|
1348
|
+
// The one opening of every offer's decline alternative: a chat write the agent runs on the user's decline.
|
|
1349
|
+
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): ';
|
|
1325
1350
|
export const probeSpecAdoption = ({ root, deps, add, skip }) => {
|
|
1326
1351
|
try {
|
|
1327
1352
|
const survey = surveySpecAdoption(root, deps);
|
|
@@ -1337,7 +1362,7 @@ export const probeSpecAdoption = ({ root, deps, add, skip }) => {
|
|
|
1337
1362
|
fillTemplate(WHATS['spec-adoption'], {}),
|
|
1338
1363
|
`node ${q(toolPath('ensure-configs.mjs'))} --reconcile --only ${SPEC_LAYER_ENSURE} --cwd ${q(root)}`,
|
|
1339
1364
|
'spec-adoption',
|
|
1340
|
-
|
|
1365
|
+
`${DECLINE_ALTERNATIVE}${decline}`,
|
|
1341
1366
|
);
|
|
1342
1367
|
return;
|
|
1343
1368
|
}
|
|
@@ -1457,13 +1482,12 @@ export const probeAdrStore = ({ root, deps, add, skip }) => {
|
|
|
1457
1482
|
try {
|
|
1458
1483
|
const shape = ADR_LAYOUT_SHAPES[surveyAdrLayoutStrict(root, deps)];
|
|
1459
1484
|
if (!shape) return; // migrated, or no ADR substrate at all — nothing to offer
|
|
1460
|
-
//
|
|
1461
|
-
//
|
|
1462
|
-
// dry-run. A runnable one-liner here would auto-run a tree-mutating migration on stale consent.
|
|
1485
|
+
// The apply is the bare dry run: consent takes two steps, as profile-gap's does — the first yes
|
|
1486
|
+
// runs it and the plan is relayed, the same line with --apply runs only on a second yes over it.
|
|
1463
1487
|
add(
|
|
1464
1488
|
'adr-store-migration',
|
|
1465
1489
|
fillTemplate(WHATS['adr-store-migration'], { shape }),
|
|
1466
|
-
`
|
|
1490
|
+
`node ${q(toolPath('migrate-adr-store.mjs'))} --dry-run --cwd ${q(root)}`,
|
|
1467
1491
|
);
|
|
1468
1492
|
} catch (err) {
|
|
1469
1493
|
skip('adr-store-migration', err);
|
|
@@ -1514,7 +1538,7 @@ export const probeJevConnect = ({ root, deps, add, skip }) => {
|
|
|
1514
1538
|
const place = placeOf({ env, platform: host.platform, hostname: host.hostname, container: host.container });
|
|
1515
1539
|
const decline = `node ${q(toolPath('ack-write.mjs'))} --lane jev-connect --fingerprint ${JEV_CONNECT_DECLINE} --cwd ${q(root)}`;
|
|
1516
1540
|
add('jev-connect', WHATS['jev-connect'], applySlot('jev-connect', { toolsDir: HERE }).apply, 'jev-connect',
|
|
1517
|
-
`${place ? `run the apply in ${place}; ` : ''}
|
|
1541
|
+
`${place ? `run the apply in ${place}; ` : ''}${DECLINE_ALTERNATIVE}${decline}`);
|
|
1518
1542
|
} catch (err) {
|
|
1519
1543
|
skip('jev-connect', err);
|
|
1520
1544
|
}
|
|
@@ -1548,7 +1572,7 @@ export const probeJevSkill = ({ root, deps, add, skip }) => {
|
|
|
1548
1572
|
.map(({ path, reason }) => `left untouched: ${printable(path)} — ${printable(reason)}; `).join('');
|
|
1549
1573
|
const record = `node ${q(toolPath('ack-write.mjs'))} --lane jev-skill --fingerprint ${decline} --cwd ${q(root)}`;
|
|
1550
1574
|
add('jev-skill', what, line, behind.length ? 'jev-skill.earlier' : 'jev-skill',
|
|
1551
|
-
`${place ? `run the apply in ${place}; ` : ''}${untouched}${note ? `${note}; ` : ''}
|
|
1575
|
+
`${place ? `run the apply in ${place}; ` : ''}${untouched}${note ? `${note}; ` : ''}${DECLINE_ALTERNATIVE}${record}`);
|
|
1552
1576
|
} catch (err) {
|
|
1553
1577
|
skip('jev-skill', err);
|
|
1554
1578
|
}
|
|
@@ -1564,9 +1588,48 @@ export const probeProfileGaps = ({ root, deps, add, skip }) => {
|
|
|
1564
1588
|
}
|
|
1565
1589
|
};
|
|
1566
1590
|
|
|
1591
|
+
// The attribution item by its table (spec init-project, part in-session-writes): a choice of off the
|
|
1592
|
+
// effective settings do not honour; the writer's preflight runs before it renders, so no rendered check always exits 3.
|
|
1593
|
+
const probeAttribution = ({ root, deps, add, skip }) => {
|
|
1594
|
+
try {
|
|
1595
|
+
if (readAttributionChoice(root, deps) !== 'off') return;
|
|
1596
|
+
const project = readSettingsFile(join(root, SETTINGS_FILE), { ...deps, cwd: root }).data;
|
|
1597
|
+
const local = readSettingsFile(join(root, SETTINGS_LOCAL_FILE), { ...deps, cwd: root }).data;
|
|
1598
|
+
const effective = effectiveAttribution(project, local);
|
|
1599
|
+
if (attributionHidesText(effective)) return;
|
|
1600
|
+
preflightVelocityProfile({ cwd: root }, deps);
|
|
1601
|
+
for (const [file, data] of [[SETTINGS_FILE, project], [SETTINGS_LOCAL_FILE, local]]) {
|
|
1602
|
+
const wrong = wrongTypedKey(data);
|
|
1603
|
+
if (wrong) throw new Error(`${file}: ${wrong} — the attribution item cannot judge it`);
|
|
1604
|
+
}
|
|
1605
|
+
const setsText = (value) => value !== null && typeof value === 'object' && ['commit', 'pr'].some((key) => Object.hasOwn(value, key));
|
|
1606
|
+
const localKey = Object.hasOwn(local ?? {}, ATTRIBUTION_KEY) && !attributionHidesText({ [ATTRIBUTION_KEY]: local[ATTRIBUTION_KEY] }) ? ATTRIBUTION_KEY
|
|
1607
|
+
: local?.[LEGACY_ATTRIBUTION_KEY] === true && !setsText(effective[ATTRIBUTION_KEY]) ? LEGACY_ATTRIBUTION_KEY : null;
|
|
1608
|
+
if (localKey) throw new Error(`${SETTINGS_LOCAL_FILE} sets ${localKey} so the harness adds its text — a local-scope override is your own edit`);
|
|
1609
|
+
add('attribution', WHATS.attribution, formsApply('attribution', root));
|
|
1610
|
+
} catch (err) {
|
|
1611
|
+
skip('attribution', err);
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
// The leftover machine-global block: the one plan the hide reports and the --remove-global mode removes,
|
|
1616
|
+
// asked of git under the home the advisor was given.
|
|
1617
|
+
const probeFootprintGlobal = ({ root, deps, add, skip }) => {
|
|
1618
|
+
try {
|
|
1619
|
+
const home = deps.home ?? deps.jevHost?.home ?? homedir();
|
|
1620
|
+
const plan = (deps.planGlobalRemoval ?? planGlobalRemoval)({ env: { ...process.env, HOME: home }, home }, root, { home });
|
|
1621
|
+
if (plan === null) return;
|
|
1622
|
+
const file = truncatedTo(plan.source, templateBudget(WHATS['footprint-global']));
|
|
1623
|
+
add('footprint-global', fillTemplate(WHATS['footprint-global'], { file }), formsApply('footprint-global', root));
|
|
1624
|
+
} catch (err) {
|
|
1625
|
+
skip('footprint-global', err);
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1567
1629
|
// ── assembly (frozen presentation order) ─────────────────────────────────────────────────────────
|
|
1568
1630
|
const PROBES = Object.freeze([
|
|
1569
1631
|
probeVelocityItems,
|
|
1632
|
+
probeAttribution,
|
|
1570
1633
|
probeAutonomyItems,
|
|
1571
1634
|
probeSandboxProvision,
|
|
1572
1635
|
probeReviewRecipe,
|
|
@@ -1582,6 +1645,7 @@ const PROBES = Object.freeze([
|
|
|
1582
1645
|
probeFamilyFreshness,
|
|
1583
1646
|
probeAdrStore,
|
|
1584
1647
|
probeMasksItem,
|
|
1648
|
+
probeFootprintGlobal,
|
|
1585
1649
|
probeWorktreesDir,
|
|
1586
1650
|
probeMcpChannel,
|
|
1587
1651
|
probeSpecAdoption,
|
|
@@ -1604,10 +1668,9 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
1604
1668
|
// which item fired, and only this field says which ARM of it did — so a consumer (the pre-publish
|
|
1605
1669
|
// smoke asserting a specific false-green never returns) can assert the exact outcome instead of
|
|
1606
1670
|
// pattern-matching prose that is free to be reworded.
|
|
1607
|
-
// `detail` (optional) is an extra rendered `recipe:` line —
|
|
1608
|
-
// the capped WHAT and
|
|
1609
|
-
// line
|
|
1610
|
-
// agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
|
|
1671
|
+
// `detail` (optional) is an extra rendered `recipe:` line — an item's own facts or its alternative,
|
|
1672
|
+
// TOO LONG for the capped WHAT and outside the pure-command apply (such as the bridge-tier
|
|
1673
|
+
// missing-entries line or the worktrees-dir ack fallback). Single-line like apply; absent otherwise.
|
|
1611
1674
|
// Returns whether the item really RENDERED. One probe's disposition depends on another's having
|
|
1612
1675
|
// spoken (the marker-stale ⟷ read-lane.stale precedence), and "the conditions still look right"
|
|
1613
1676
|
// is not the same fact as "an item exists" — the shape backstop can refuse, and a condition read
|
|
@@ -1629,7 +1692,7 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
1629
1692
|
home: deps.jevHost?.home ?? deps.home ?? '',
|
|
1630
1693
|
platform: deps.jevHost?.platform ?? deps.platform,
|
|
1631
1694
|
});
|
|
1632
|
-
items.push({ key, variant, severity: SEVERITIES[variant], lane: laneOf(variant), what, benefit: BENEFITS[key],
|
|
1695
|
+
items.push({ key, variant, severity: SEVERITIES[variant], lane: laneOf(variant), what, benefit: VARIANT_BENEFITS[variant] ?? BENEFITS[key],
|
|
1633
1696
|
check: slot?.check ?? null, apply: slot?.apply ?? apply, detail });
|
|
1634
1697
|
return true;
|
|
1635
1698
|
};
|
|
@@ -1697,9 +1760,9 @@ Usage:
|
|
|
1697
1760
|
|
|
1698
1761
|
Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
|
|
1699
1762
|
one composed verdict line opens every non-optimal render, then per item {severity · what is
|
|
1700
|
-
sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (
|
|
1701
|
-
|
|
1702
|
-
|
|
1763
|
+
sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (an item's own
|
|
1764
|
+
facts or its alternative, such as the bridge-tier missing-entries line or
|
|
1765
|
+
the worktrees-dir ack fallback) · the harness check line · the exact consent-gated apply one-liner}. --cwd is
|
|
1703
1766
|
REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
|
|
1704
1767
|
section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
|
|
1705
1768
|
stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
|
|
@@ -1710,8 +1773,8 @@ allow rules, <wrapper> * exclusions (codex-exec * where an execute slot delegate
|
|
|
1710
1773
|
sandbox.network.allowedDomains and state dirs in sandbox.filesystem.allowWrite. Where the host
|
|
1711
1774
|
honors the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed
|
|
1712
1775
|
command of the project can reach each host, and every sandboxed command can write each state dir;
|
|
1713
|
-
plan/diff review modes and delegated execution keep their prompt.
|
|
1714
|
-
the user's own step.
|
|
1776
|
+
plan/diff review modes and delegated execution keep their prompt.
|
|
1777
|
+
A user item stays the user's own step.
|
|
1715
1778
|
|
|
1716
1779
|
Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
|
|
1717
1780
|
rendered (items or empty); 1 error; 2 usage.`;
|