@sabaiway/agent-workflow-kit 14.31.0 → 14.33.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.
@@ -12,7 +12,8 @@ import {
12
12
  writeAutonomyProfile, writeVelocityProfile,
13
13
  } from './velocity-profile.mjs';
14
14
  import { KIT_GROUNDING_TOOL, bundledSandboxRecipe, usedBridges } from './bridge-sandbox-recipe.mjs';
15
- import { mergeAllowWrite, stateDirsOf } from './bridge-state-dirs.mjs';
15
+ import { homeRelative, mergeAllowWrite, stateDirsOf } from './bridge-state-dirs.mjs';
16
+ import { nearestExistingDir, worktreesGrantDir, worktreesGrantHoldsHome } from './worktrees.mjs';
16
17
  import { loadConfig } from './orchestration-config.mjs';
17
18
  import { HOOK_FILE_REL, HOOKS_DIR, buildHookSettingsEntry, mergeHookEntry, writeGateHook } from './gate-hook.mjs';
18
19
  import { STATE_BLOCK_HOOK_FILE_REL, buildStopHookEntry, mergeStopEntry, writeStateBlockHook } from './state-block-hook.mjs';
@@ -23,6 +24,7 @@ import { planFor } from './jev-skill.mjs';
23
24
  import { SKILL_FILES, claudeDirOf, skillTargets } from './jev-facts.mjs';
24
25
  import { findOnPath } from './detect-backends.mjs';
25
26
  import { consoleArgv } from './write-lanes.mjs';
27
+ import { BUNDLED_SCRIPT_NAMES, INSTALLER_REL, SCRIPTS_REL, formatPlan, planGitHooks } from './git-hooks.mjs';
26
28
  import { isDirectRun } from './direct-run.mjs';
27
29
  import { dirCovers as covers } from './declared-paths.mjs';
28
30
 
@@ -34,8 +36,10 @@ const HOOK_VARIANTS = ['gate-hook', 'read-lane.missing', ...RESEED_VARIANTS];
34
36
  const STOP = 'state-block';
35
37
  const MCP_VARIANTS = ['mcp-channel', 'mcp-channel.differing'];
36
38
  const AGENT_VARIANTS = ['agents', 'executor-vehicle'];
39
+ const GIT_VARIANTS = ['enforcement', 'commit-guard'];
40
+ const WORKTREES = 'worktrees-dir';
37
41
  const VARIANTS = [...VELOCITY_VARIANTS, 'autonomy-render', ...HOOK_VARIANTS, STOP, ...MCP_VARIANTS, ...AGENT_VARIANTS,
38
- ...SKILL_VARIANTS];
42
+ ...SKILL_VARIANTS, ...GIT_VARIANTS, WORKTREES];
39
43
  const HOOK_KEYS = ['hooks.PreToolUse', 'hooks.Stop'];
40
44
  const CREDENTIALS = 'sandbox.credentials.envVars';
41
45
  const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -179,7 +183,12 @@ export const scopeOf = (variant, facts, deps = {}) => {
179
183
  set('permissions.ask', RENDER_OWNED_REDLINE_RULES);
180
184
  set('permissions.deny', RENDER_OWNED_REDLINE_RULES);
181
185
  allowed[CREDENTIALS] = (value) => value.mode === 'deny';
182
- } else if (HOOK_VARIANTS.includes(variant)) set('hooks.PreToolUse', [buildHookSettingsEntry()]);
186
+ }
187
+ const grantDir = variant === WORKTREES ? worktreesGrantDir(facts.root, facts) : null;
188
+ if (grantDir !== null && !worktreesGrantHoldsHome(grantDir, facts.home, facts)) {
189
+ set('sandbox.filesystem.allowWrite', [homeRelative(grantDir, facts.home)]);
190
+ }
191
+ if (HOOK_VARIANTS.includes(variant)) set('hooks.PreToolUse', [buildHookSettingsEntry()]);
183
192
  else if (variant === STOP) set('hooks.Stop', [buildStopHookEntry()]);
184
193
  else if (MCP_VARIANTS.includes(variant)) {
185
194
  set(`${SERVERS_KEY}.${SERVER_NAME}`, [buildServerEntry(join(facts.toolsDir, 'mcp-server.mjs'))]);
@@ -193,14 +202,25 @@ export const scopeOf = (variant, facts, deps = {}) => {
193
202
  const directories = SKILL_VARIANTS.includes(variant) ? [] : [join(facts.root, CLAUDE_DIR)];
194
203
  if (HOOK_VARIANTS.includes(variant) || variant === STOP) directories.push(join(facts.root, HOOKS_DIR));
195
204
  if (AGENT_VARIANTS.includes(variant)) directories.push(join(facts.root, AGENTS_DIR));
205
+ const existingAncestor = grantDir === null ? null : nearestExistingDir(grantDir, facts);
206
+ for (let at = grantDir; at !== existingAncestor; at = dirname(at)) directories.push(at);
196
207
  for (const target of targets) {
197
208
  for (let at = target.root; at !== target.base && covers(target.base, at); at = dirname(at)) directories.push(at);
198
209
  filePaths.push(target.path);
199
210
  }
211
+ const scripts = join(facts.root, SCRIPTS_REL);
212
+ if (GIT_VARIANTS.includes(variant)) {
213
+ directories.splice(0, directories.length, scripts);
214
+ filePaths.push(...BUNDLED_SCRIPT_NAMES.map((name) => join(scripts, name)));
215
+ }
216
+ const runs = GIT_VARIANTS.includes(variant) ? ['node', INSTALLER_REL,
217
+ ...(variant === 'commit-guard' ? ['--commit-guard', join(facts.toolsDir, 'commit-guard.mjs')] : [])] : null;
200
218
  return {
219
+ admitsRun: (argv) => runs !== null && equal(argv, runs),
201
220
  admits: ({ path, key, change, value }) => {
202
- const correctFile = key.startsWith(`${SERVERS_KEY}.`) ? path === MCP_JSON_REL : path === SETTINGS_FILE;
203
- const gains = variant === 'bridge-tier' || HOOK_KEYS.includes(key) || key === ENABLED_KEY || key === CREDENTIALS
221
+ const correctFile = key.startsWith(`${SERVERS_KEY}.`) ? path === MCP_JSON_REL
222
+ : path === (variant === WORKTREES ? SETTINGS_LOCAL_FILE : SETTINGS_FILE);
223
+ const gains = variant === 'bridge-tier' || variant === WORKTREES || HOOK_KEYS.includes(key) || key === ENABLED_KEY || key === CREDENTIALS
204
224
  || (VELOCITY_VARIANTS.includes(variant) && key === 'permissions.ask');
205
225
  return correctFile && (!gains || change === 'added') && Boolean(allowed[key]?.(value));
206
226
  },
@@ -213,7 +233,7 @@ const jsonAt = (path) => existsSync(path) ? JSON.parse(readFileSync(path, 'utf8'
213
233
  const halted = (lines) => Object.assign(new Error(lines.join('\n')), { code: 'CHECK_HALTED', lines });
214
234
  const maskedFiles = (variant, facts) => {
215
235
  const paths = VELOCITY_VARIANTS.includes(variant) || variant === 'autonomy-render' || HOOK_VARIANTS.includes(variant)
216
- || variant === STOP ? [SETTINGS_FILE, SETTINGS_LOCAL_FILE] : MCP_VARIANTS.includes(variant) ? [MCP_JSON_REL, SETTINGS_FILE] : [];
236
+ || variant === STOP || variant === WORKTREES ? [SETTINGS_FILE, SETTINGS_LOCAL_FILE] : MCP_VARIANTS.includes(variant) ? [MCP_JSON_REL, SETTINGS_FILE] : [];
217
237
  return paths.flatMap((path) => lstatSync(join(facts.root, path), { throwIfNoEntry: false })?.isCharacterDevice()
218
238
  ? [`masked: ${path}`] : []);
219
239
  };
@@ -221,6 +241,8 @@ export const plannedChange = (variant, facts, deps = {}) => {
221
241
  const files = [];
222
242
  const writes = [];
223
243
  const temporaries = [];
244
+ const created = [];
245
+ let runs = null;
224
246
  const writerDeps = writerFacts(facts, deps);
225
247
  const addFile = (path, before, after) => {
226
248
  files.push({ path, before: before ?? null, after });
@@ -280,6 +302,15 @@ export const plannedChange = (variant, facts, deps = {}) => {
280
302
  temporaries.push(`temporary: ${dirname(target.root)}/.${stem}.kit-tmp-*`);
281
303
  if (target.action === 'replace') temporaries.push(`temporary: ${dirname(target.root)}/.${stem}.kit-old-*`);
282
304
  }
305
+ } else if (GIT_VARIANTS.includes(variant)) {
306
+ const plan = (deps.planGitHooks ?? planGitHooks)({ root: facts.root, variant, toolsDir: facts.toolsDir });
307
+ for (const copy of plan.copies.filter(({ action }) => action === 'created')) writes.push({ path: copy.path, bytes: copy.bytes });
308
+ runs = { argv: plan.runs.shown, line: formatPlan(plan).at(-1) };
309
+ } else if (variant === WORKTREES) {
310
+ const plan = writeVelocityProfile({ cwd: facts.root, dryRun: true, worktrees: true }, writerDeps);
311
+ const before = plan.localSettings.data ?? null;
312
+ addFile(SETTINGS_LOCAL_FILE, before, mergeAllowWrite(before ?? {}, plan.worktreesDirToAdd));
313
+ created.push(...plan.worktreesDirsToCreate);
283
314
  } else throw new Error(`unsupported variant: ${variant}`);
284
315
  } catch (error) {
285
316
  throw error.code === 'CHECK_HALTED' ? error : halted([error.message]);
@@ -289,7 +320,8 @@ export const plannedChange = (variant, facts, deps = {}) => {
289
320
  for (const path of [...changedFiles.map(({ path }) => join(facts.root, path)), ...writes.map(({ path }) => path)]) {
290
321
  for (let parent = dirname(path); !existsSync(parent); parent = dirname(parent)) directories.add(parent);
291
322
  }
292
- return { files, writes, temporaries, directories: [...directories] };
323
+ for (const path of created) directories.add(path);
324
+ return { files, writes, temporaries, directories: [...directories], runs };
293
325
  };
294
326
 
295
327
  export const checkApply = (variant, facts, deps = {}) => {
@@ -318,6 +350,10 @@ export const checkApply = (variant, facts, deps = {}) => {
318
350
  }
319
351
  for (const path of plan.directories) pathLine(path, true, `writes: ${display(path)}/ created`);
320
352
  lines.push(...plan.temporaries);
353
+ if (plan.runs) {
354
+ lines.push(plan.runs.line);
355
+ if (!scope.admitsRun(plan.runs.argv)) refused.push({ path: 'runs:', key: plan.runs.argv.join(' '), reason: `beyond the declared scope of ${variant}` });
356
+ }
321
357
  if (refused.length) {
322
358
  for (const entry of refused) lines.push(`refused: ${entry.path} ${entry.key} — ${entry.reason}`);
323
359
  lines.push('nothing written');
@@ -369,7 +405,7 @@ export const main = (argv, io = {}) => {
369
405
  platform: resolved.platform, claudeDir: args['claude-dir'] };
370
406
  let result;
371
407
  try {
372
- result = checkApply(args.variant, facts, { detect: resolved.detect });
408
+ result = checkApply(args.variant, facts, { detect: resolved.detect, planGitHooks: resolved.planGitHooks });
373
409
  } catch (error) {
374
410
  for (const line of error.lines ?? [error.message]) resolved.log(line);
375
411
  return 3;
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ // git-hooks.mjs — the thin writer of the two git-hook items (`enforcement`, `commit-guard`): it copies
3
+ // the bundled scripts create-only and spawns the project's own scripts/install-git-hooks.mjs, which
4
+ // decides the hook at the write. The leaf decides nothing about the hook: the danger check binds the
5
+ // copies and the installer bytes this dry run answers, never git's hooks dir.
6
+ import { lstatSync, readFileSync, readdirSync, rmdirSync, statSync, unlinkSync } from 'node:fs';
7
+ import { createHash } from 'node:crypto';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { writeProjectFileCreateOnly } from './atomic-write.mjs';
12
+ import { NODE_EVIDENCE_SCRIPTS } from './node-evidence.mjs';
13
+ import { shellQuoteArg } from './repo-lex.mjs';
14
+ import { isDirectRun } from './direct-run.mjs';
15
+
16
+ const TOOLS = dirname(fileURLToPath(import.meta.url));
17
+ export const SCRIPTS_REL = 'scripts';
18
+ export const INSTALLER_REL = 'scripts/install-git-hooks.mjs';
19
+ const bundleDir = (toolsDir) => join(toolsDir, '..', 'references', 'scripts');
20
+ export const BUNDLED_SCRIPT_NAMES = Object.freeze(readdirSync(bundleDir(TOOLS)).filter((name) => name.endsWith('.mjs')).sort());
21
+
22
+ const EXIT_OK = 0;
23
+ const EXIT_INCOMPLETE = 1;
24
+ const EXIT_USAGE = 2;
25
+ const EXIT_REFUSED = 3;
26
+ const USAGE = 'usage: git-hooks (--enforcement | --commit-guard) [--cwd <root>] [--apply]';
27
+ const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
28
+ const usageError = (message) => Object.assign(new Error(message), { exitCode: EXIT_USAGE });
29
+ const refusal = (message) => Object.assign(new Error(message), { exitCode: EXIT_REFUSED });
30
+
31
+ const nodeAt = (path, lstat) => {
32
+ try {
33
+ return lstat(path);
34
+ } catch (error) {
35
+ if (error?.code === 'ENOENT') return null;
36
+ throw error;
37
+ }
38
+ };
39
+ const isPlainDir = (node) => node !== null && node.isDirectory() && !node.isSymbolicLink();
40
+ const isFile = (node) => node !== null && node.isFile() && !node.isSymbolicLink();
41
+
42
+ // The dry run: what the apply copies and the installer it runs, with the bytes of each.
43
+ export const planGitHooks = ({ root, variant, toolsDir = TOOLS, lstat = lstatSync, readFile = readFileSync }) => {
44
+ const scripts = join(root, SCRIPTS_REL);
45
+ const scriptsNode = nodeAt(scripts, lstat);
46
+ if (scriptsNode !== null && !isPlainDir(scriptsNode)) throw refusal(`${scripts} is not a plain directory`);
47
+ const kinds = new Map();
48
+ const names = variant === 'enforcement' ? BUNDLED_SCRIPT_NAMES : [INSTALLER_REL.slice(SCRIPTS_REL.length + 1)];
49
+ for (const name of scriptsNode === null ? [] : names) {
50
+ const path = join(scripts, name);
51
+ const node = nodeAt(path, lstat);
52
+ if (node !== null && !isFile(node)) throw refusal(`${path} is not a regular file`);
53
+ kinds.set(name, node === null ? 'absent' : 'file');
54
+ }
55
+ const installer = join(root, INSTALLER_REL);
56
+ const deployed = NODE_EVIDENCE_SCRIPTS.some((name) => kinds.get(name) === 'file');
57
+ const copies = variant === 'enforcement' && !deployed ? BUNDLED_SCRIPT_NAMES.map((name) => {
58
+ if (kinds.get(name) === 'file') return { name, path: join(scripts, name), action: 'kept' };
59
+ return { name, path: join(scripts, name), action: 'created', bytes: readFile(join(bundleDir(toolsDir), name)) };
60
+ }) : [];
61
+ const placed = copies.find(({ path, action }) => path === installer && action === 'created');
62
+ if (!placed && !isFile(nodeAt(installer, lstat))) throw refusal(`${installer} is absent — nothing here places the hook installer`);
63
+ const guard = variant === 'commit-guard' ? ['--commit-guard', join(toolsDir, 'commit-guard.mjs')] : [];
64
+ return {
65
+ root,
66
+ variant,
67
+ scriptsCreated: scriptsNode === null,
68
+ copies,
69
+ runs: { argv: ['node', installer, ...guard], shown: ['node', INSTALLER_REL, ...guard], bytes: placed ? placed.bytes : readFile(installer) },
70
+ };
71
+ };
72
+
73
+ export const formatPlan = (plan) => [
74
+ ...plan.copies.map(({ name, action, bytes }) => `copy: ${SCRIPTS_REL}/${name} ${action}${action === 'created' ? ` sha256:${sha(bytes)}` : ''}`),
75
+ `runs: ${plan.runs.shown.map(shellQuoteArg).join(' ')} sha256:${sha(plan.runs.bytes)}`,
76
+ ];
77
+
78
+ // Removes exactly the files this run created, then scripts/ when this run created it and it is empty;
79
+ // every path it cannot remove is answered for the caller to name.
80
+ const rollBack = (plan, created, io) => {
81
+ const scripts = join(plan.root, SCRIPTS_REL);
82
+ const left = [];
83
+ const dirNow = () => nodeAt(scripts, io.lstat);
84
+ for (const path of created) {
85
+ const dir = dirNow();
86
+ const node = nodeAt(path, io.lstat);
87
+ if (!isPlainDir(dir) || dir.ino !== created.dirIno || !isFile(node)) {
88
+ left.push(path);
89
+ continue;
90
+ }
91
+ try {
92
+ io.unlink(path);
93
+ } catch {
94
+ left.push(path);
95
+ }
96
+ }
97
+ if (plan.scriptsCreated && created.dirIno !== undefined) {
98
+ const dir = dirNow();
99
+ let empty = false;
100
+ try {
101
+ empty = isPlainDir(dir) && dir.ino === created.dirIno && io.readdir(scripts).length === 0;
102
+ } catch {
103
+ empty = false;
104
+ }
105
+ if (empty) {
106
+ try {
107
+ io.rmdir(scripts);
108
+ } catch {
109
+ left.push(scripts);
110
+ }
111
+ } else if (!isPlainDir(dir) || dir.ino !== created.dirIno) left.push(scripts);
112
+ }
113
+ return left;
114
+ };
115
+
116
+ export const applyGitHooks = (plan, io = {}) => {
117
+ const resolved = { lstat: lstatSync, unlink: unlinkSync, rmdir: rmdirSync, readdir: readdirSync, env: process.env,
118
+ log: console.log, error: console.error, writeFile: writeProjectFileCreateOnly,
119
+ spawn: (argv, options) => spawnSync(argv[0], argv.slice(1), options), ...io };
120
+ const created = [];
121
+ const leftovers = [];
122
+ const fail = (lines) => {
123
+ for (const line of lines) resolved.error(line);
124
+ for (const path of [...rollBack(plan, created, resolved), ...leftovers]) resolved.error(`not removed: ${path}`);
125
+ return EXIT_INCOMPLETE;
126
+ };
127
+ const scripts = join(plan.root, SCRIPTS_REL);
128
+ const noteDir = () => {
129
+ const dir = nodeAt(scripts, resolved.lstat);
130
+ if (created.dirIno === undefined && isPlainDir(dir)) created.dirIno = dir.ino;
131
+ };
132
+ for (const copy of plan.copies.filter(({ action }) => action === 'created')) {
133
+ let result;
134
+ try {
135
+ result = resolved.writeFile(plan.root, `${SCRIPTS_REL}/${copy.name}`, copy.bytes);
136
+ } catch (error) {
137
+ noteDir();
138
+ return fail([error?.message ?? String(error)]);
139
+ }
140
+ noteDir();
141
+ if (result.tmpLeftBehind) leftovers.push(result.tmpLeftBehind);
142
+ if (!result.created) return fail([`${copy.path} appeared after the check — the file found there stands`]);
143
+ created.push(copy.path);
144
+ }
145
+ const child = resolved.spawn(plan.runs.argv, { cwd: plan.root, env: resolved.env, stdio: 'inherit' });
146
+ if (child.status !== 0) return fail([`installer exited ${child.status ?? child.signal ?? child.error?.code}`]);
147
+ for (const path of leftovers) resolved.error(`not removed: ${path}`);
148
+ return EXIT_OK;
149
+ };
150
+
151
+ export const parseArgs = (argv) => {
152
+ if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) return { help: true };
153
+ const args = { apply: false, cwd: undefined, variants: [] };
154
+ for (let index = 0; index < argv.length; index += 1) {
155
+ const flag = argv[index];
156
+ if (flag === '--apply') args.apply = true;
157
+ else if (flag === '--enforcement' || flag === '--commit-guard') args.variants.push(flag.slice(2));
158
+ else if (flag === '--cwd') {
159
+ const value = argv[index + 1];
160
+ if (value === undefined || value.startsWith('-')) throw usageError('--cwd takes a value');
161
+ args.cwd = value;
162
+ index += 1;
163
+ } else throw usageError(`unknown argument: ${flag}`);
164
+ }
165
+ if (args.variants.length !== 1) throw usageError('pass exactly one of --enforcement and --commit-guard');
166
+ return { help: false, apply: args.apply, cwd: args.cwd, variant: args.variants[0] };
167
+ };
168
+
169
+ export const main = (argv, io = {}) => {
170
+ const resolved = { log: console.log, error: console.error, cwd: process.cwd(), toolsDir: TOOLS, ...io };
171
+ let args;
172
+ try {
173
+ args = parseArgs(argv);
174
+ if (args.help) {
175
+ resolved.log(USAGE);
176
+ return EXIT_OK;
177
+ }
178
+ args.root = resolve(resolved.cwd, args.cwd ?? '.');
179
+ if (!statSync(args.root, { throwIfNoEntry: false })?.isDirectory()) throw usageError(`--cwd is not a directory: ${args.root}`);
180
+ } catch (error) {
181
+ resolved.error(error.message);
182
+ resolved.error(USAGE);
183
+ return EXIT_USAGE;
184
+ }
185
+ let plan;
186
+ try {
187
+ plan = planGitHooks({ root: args.root, variant: args.variant, toolsDir: resolved.toolsDir });
188
+ } catch (error) {
189
+ resolved.error(error.message);
190
+ return EXIT_REFUSED;
191
+ }
192
+ if (!args.apply) {
193
+ for (const line of formatPlan(plan)) resolved.log(line);
194
+ return EXIT_OK;
195
+ }
196
+ return applyGitHooks(plan, resolved);
197
+ };
198
+
199
+ if (isDirectRun(import.meta.url)) process.exitCode = main(process.argv.slice(2));
@@ -13,7 +13,7 @@ import { preflightInit } from './init-preflight.mjs';
13
13
  import { buildRecommendations, hostFacts } from './recommendations.mjs';
14
14
  import { compareSemver } from './semver-lite.mjs';
15
15
  import { EXPECTED_WORKFLOW_VERSION } from './velocity-profile.mjs';
16
- import { LANE_HARNESS, VARIANT_LANES, consoleArgv, isEnvDependent, isProtectedPath, laneOf, writesOf } from './write-lanes.mjs';
16
+ import { LANE_HARNESS, VARIANT_LANES, consoleArgv, isEnvDependent, isProtectedPath, isRejudged, laneOf, writesOf } from './write-lanes.mjs';
17
17
 
18
18
  const INSIDE_LINE = 'you are inside an agent; run this in your own terminal';
19
19
  const APPLY_PROMPT = 'apply? y/N: ';
@@ -217,17 +217,20 @@ const previewQueue = async (context, queue) => {
217
217
  context.print(result.stdout);
218
218
  const tuple = result.stdout.match(/--apply\s+(\S+)/)?.[1];
219
219
  const facts = { ...context.facts, tuple };
220
+ let checkDigest = null;
220
221
  if (laneOf(item.variant) === LANE_HARNESS) {
221
222
  try {
222
- const { lines, refused } = context.checkApply(item.variant, facts, {});
223
+ const { lines, refused, digest } = context.checkApply(item.variant, facts, {});
223
224
  for (const line of lines) context.print(line);
224
225
  if (refused.length > 0) throw new Error(refused.map(({ key }) => key).join(', '));
226
+ checkDigest = digest;
225
227
  } catch (error) {
226
228
  context.print(`not applied: ${item.variant} — the danger check refused: ${error.message}`);
227
229
  continue;
228
230
  }
229
231
  }
230
- batch.push({ ...item, applyArgv: consoleArgv(item.variant, facts).apply, writes: writesOf(item.variant, facts) });
232
+ batch.push({ ...item, checkFacts: facts, checkDigest, applyArgv: consoleArgv(item.variant, facts).apply,
233
+ writes: writesOf(item.variant, facts) });
231
234
  printNote(context, item.key);
232
235
  }
233
236
  for (const item of queue.filter(({ variant }) => consoleArgv(variant, context.facts) === null)) {
@@ -254,11 +257,24 @@ const takeRecord = (context, queue) => {
254
257
  }
255
258
  };
256
259
 
260
+ // A rejudged item applies only while its check, run again now, answers the digest its preview showed; else the reason.
261
+ const rejudgeFailure = (context, item) => {
262
+ try {
263
+ const { refused, digest } = context.checkApply(item.variant, item.checkFacts, {});
264
+ if (refused.length > 0) return `it refuses ${refused.map(({ key }) => key).join(', ')}`;
265
+ return digest === item.checkDigest ? null : 'its digest moved';
266
+ } catch (error) {
267
+ return String(error?.message || error);
268
+ }
269
+ };
270
+
257
271
  const runPass = async (context, queue) => {
258
272
  const record = takeRecord(context, queue);
259
273
  if (record === null) return { code: 1, successes: 0, variants: [] };
260
- const batch = await previewQueue(context, queue);
261
- if (batch.length === 0) return { code: 0, successes: 0, variants: [] };
274
+ const previewed = await previewQueue(context, queue);
275
+ if (previewed.length === 0) return { code: 0, successes: 0, variants: [] };
276
+ // Rejudged items go first: an earlier writer of the batch (one creating .claude/) would move their check.
277
+ const batch = [...previewed.filter(({ variant }) => isRejudged(variant)), ...previewed.filter(({ variant }) => !isRejudged(variant))];
262
278
  const before = new Map(record);
263
279
  const outcome = { code: 0, successes: 0, variants: batch.map((item) => item.variant) };
264
280
  context.print(RUN_NONE);
@@ -271,6 +287,15 @@ const runPass = async (context, queue) => {
271
287
  outcome.code = 1;
272
288
  break;
273
289
  }
290
+ const failure = isRejudged(item.variant) ? rejudgeFailure(context, item) : null;
291
+ if (failure !== null) {
292
+ context.print(`not applied: ${item.variant} — its danger check no longer answers the previewed result: ${failure}`);
293
+ for (const later of batch.slice(index + 1)) {
294
+ context.print(`not applied: ${later.variant} — an earlier item's danger check no longer answers the previewed result`);
295
+ }
296
+ outcome.code = 1;
297
+ break;
298
+ }
274
299
  const result = await launch(context, item.applyArgv, ['inherit', 'pipe', 'inherit']);
275
300
  if (result.stdout) context.print(result.stdout);
276
301
  const success = result.valid && (result.status === 0 || (result.status === 5 && result.stdout.includes('install completed')));