sneakoscope 5.1.2 → 5.5.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.
Files changed (97) hide show
  1. package/README.md +5 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/commands/doctor.js +32 -3
  7. package/dist/config/skills-manifest.json +58 -58
  8. package/dist/core/agents/agent-lifecycle.js +5 -3
  9. package/dist/core/agents/agent-orchestrator.js +14 -2
  10. package/dist/core/agents/agent-proof-evidence.js +16 -0
  11. package/dist/core/agents/native-cli-worker.js +0 -1
  12. package/dist/core/bench.js +24 -2
  13. package/dist/core/codex-app/sks-menubar.js +526 -133
  14. package/dist/core/commands/agent-command.js +35 -1
  15. package/dist/core/commands/basic-cli.js +61 -4
  16. package/dist/core/commands/command-utils.js +14 -4
  17. package/dist/core/commands/goal-command.js +3 -0
  18. package/dist/core/commands/image-ux-review-command.js +4 -1
  19. package/dist/core/commands/mad-sks-command.js +10 -1
  20. package/dist/core/commands/menubar-command.js +2 -1
  21. package/dist/core/commands/naruto-command.js +12 -7
  22. package/dist/core/commands/ppt-command.js +40 -1
  23. package/dist/core/commands/qa-loop-command.js +4 -1
  24. package/dist/core/commands/release-command.js +3 -3
  25. package/dist/core/commands/research-command.js +4 -0
  26. package/dist/core/commands/route-success-helpers.js +2 -0
  27. package/dist/core/commands/run-command.js +22 -5
  28. package/dist/core/commands/seo-command.js +31 -4
  29. package/dist/core/db-safety.js +8 -1
  30. package/dist/core/doctor/doctor-dirty-planner.js +51 -7
  31. package/dist/core/feature-fixture-executor.js +262 -0
  32. package/dist/core/feature-fixtures.js +95 -52
  33. package/dist/core/fsx.js +1 -1
  34. package/dist/core/loops/loop-decomposer.js +4 -3
  35. package/dist/core/loops/loop-planner.js +7 -3
  36. package/dist/core/loops/loop-worker-runtime.js +6 -0
  37. package/dist/core/naruto/naruto-real-worker-child.js +1 -1
  38. package/dist/core/pipeline-internals/runtime-gates.js +39 -3
  39. package/dist/core/proof/selftest-proof-fixtures.js +8 -0
  40. package/dist/core/providers/glm/naruto/glm-naruto-requirement-coverage.js +18 -1
  41. package/dist/core/providers/glm/naruto/glm-naruto-requirement-ledger.js +28 -12
  42. package/dist/core/questions.js +12 -10
  43. package/dist/core/release/release-gate-node.js +2 -0
  44. package/dist/core/routes.js +11 -0
  45. package/dist/core/stop-gate/stop-gate-check.js +4 -0
  46. package/dist/core/stop-gate/stop-gate-writer.js +4 -0
  47. package/dist/core/trust-kernel/trust-report.js +10 -1
  48. package/dist/core/update/update-migration-state.js +23 -3
  49. package/dist/core/version.js +1 -1
  50. package/dist/core/work-order-ledger.js +60 -0
  51. package/dist/core/zellij/zellij-slot-telemetry.js +1 -1
  52. package/dist/scripts/doctor-context7-mcp-repair-blackbox.js +16 -0
  53. package/dist/scripts/doctor-fix-production-blackbox.js +26 -0
  54. package/dist/scripts/doctor-startup-config-repair-blackbox.js +13 -0
  55. package/dist/scripts/doctor-supabase-mcp-repair-blackbox.js +14 -0
  56. package/dist/scripts/gate-policy-audit-check.js +2 -2
  57. package/dist/scripts/naruto-real-active-pool-runtime-check.js +26 -1
  58. package/dist/scripts/packlist-performance-check.js +7 -1
  59. package/dist/scripts/project-skill-dedupe-blackbox.js +36 -0
  60. package/dist/scripts/release-dag-full-coverage-check.js +1 -1
  61. package/dist/scripts/release-gate-existence-audit.js +1 -1
  62. package/dist/scripts/release-gate-planner.js +1 -1
  63. package/dist/scripts/release-gate-script-parity-check.js +44 -16
  64. package/dist/scripts/release-metadata-1-19-check.js +2 -2
  65. package/dist/scripts/sks-menubar-install-check.js +57 -20
  66. package/dist/scripts/supabase-secret-preservation-blackbox.js +29 -0
  67. package/dist/scripts/update-preserves-supabase-keys-blackbox.js +27 -0
  68. package/dist/scripts/zellij-slot-telemetry-runtime-check.js +1 -0
  69. package/package.json +12 -65
  70. package/schemas/release/release-gate-node.schema.json +1 -0
  71. package/bench/tasks/t01-off-by-one/repo/package.json +0 -9
  72. package/bench/tasks/t01-off-by-one/repo/src/pagination.js +0 -5
  73. package/bench/tasks/t01-off-by-one/repo/test.js +0 -5
  74. package/bench/tasks/t01-off-by-one/task.json +0 -13
  75. package/bench/tasks/t02-signature-cochange/repo/package.json +0 -9
  76. package/bench/tasks/t02-signature-cochange/repo/src/admin.js +0 -5
  77. package/bench/tasks/t02-signature-cochange/repo/src/audit.js +0 -5
  78. package/bench/tasks/t02-signature-cochange/repo/src/labels.js +0 -3
  79. package/bench/tasks/t02-signature-cochange/repo/src/profile.js +0 -5
  80. package/bench/tasks/t02-signature-cochange/repo/test.js +0 -9
  81. package/bench/tasks/t02-signature-cochange/task.json +0 -12
  82. package/bench/tasks/t03-type-puzzle/repo/package.json +0 -9
  83. package/bench/tasks/t03-type-puzzle/repo/src/config.js +0 -4
  84. package/bench/tasks/t03-type-puzzle/repo/test.js +0 -6
  85. package/bench/tasks/t03-type-puzzle/task.json +0 -13
  86. package/bench/tasks/t04-refactor-preserve/repo/package.json +0 -9
  87. package/bench/tasks/t04-refactor-preserve/repo/src/cart.js +0 -8
  88. package/bench/tasks/t04-refactor-preserve/repo/test.js +0 -4
  89. package/bench/tasks/t04-refactor-preserve/task.json +0 -12
  90. package/bench/tasks/t05-performance/repo/package.json +0 -9
  91. package/bench/tasks/t05-performance/repo/src/pairs.js +0 -9
  92. package/bench/tasks/t05-performance/repo/test.js +0 -11
  93. package/bench/tasks/t05-performance/task.json +0 -12
  94. package/bench/tasks/t06-mistake-rule/repo/package.json +0 -9
  95. package/bench/tasks/t06-mistake-rule/repo/src/loader.js +0 -6
  96. package/bench/tasks/t06-mistake-rule/repo/test.js +0 -7
  97. package/bench/tasks/t06-mistake-rule/task.json +0 -12
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { findLatestMission, loadMission } from '../mission.js';
2
+ import { findLatestMission, loadMission, missionDir } from '../mission.js';
3
3
  import { readJson, readText, sksRoot, writeJsonAtomic } from '../fsx.js';
4
4
  import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
5
5
  import { parseAgentCommandArgs } from '../agents/agent-command-surface.js';
@@ -23,6 +23,9 @@ export async function agentCommand(commandOrArgs = 'agent', maybeArgs = []) {
23
23
  }
24
24
  async function agentRun(parsed) {
25
25
  const result = await runNativeAgentOrchestrator({ ...parsed, routeCommand: 'sks agent run', routeBlackboxKind: 'actual_agent_command' });
26
+ if (normalizeRouteName(parsed.route) === 'release-review' && result.mission_id) {
27
+ await writeReleaseReviewNativeAgentPlan(parsed, result);
28
+ }
26
29
  return emit(parsed, result, () => {
27
30
  console.log('Native agent mission: ' + result.mission_id);
28
31
  console.log('Backend: ' + result.backend);
@@ -30,6 +33,37 @@ async function agentRun(parsed) {
30
33
  console.log('Proof: ' + result.proof.status);
31
34
  });
32
35
  }
36
+ function normalizeRouteName(route = '') {
37
+ return String(route || '').replace(/^\$/, '').trim().toLowerCase();
38
+ }
39
+ /**
40
+ * $Release-Review runs through the generic native agent orchestrator (sks agent run
41
+ * --route "$Release-Review"), which does not itself know it is a release-review run.
42
+ * Write the route-specific release-review-native-agent-plan.json summary artifact here
43
+ * so `route-release-review`'s fixture contract (plan file + agent proof evidence +
44
+ * agent effort policy) is genuinely satisfied by this command rather than only partially.
45
+ */
46
+ async function writeReleaseReviewNativeAgentPlan(parsed, result) {
47
+ const root = await sksRoot();
48
+ const dir = missionDir(root, result.mission_id);
49
+ const plan = {
50
+ schema: 'sks.release-review-native-agent-plan.v1',
51
+ ok: Boolean(result.ok),
52
+ mission_id: result.mission_id,
53
+ route: '$Release-Review',
54
+ route_command: 'sks agent run',
55
+ backend: result.backend,
56
+ prompt: parsed.prompt,
57
+ roster: {
58
+ agent_count: result.roster?.agent_count ?? parsed.agents ?? null,
59
+ concurrency: result.roster?.concurrency ?? parsed.concurrency ?? null
60
+ },
61
+ proof_status: result.proof?.status || null,
62
+ agent_proof_evidence: 'agents/agent-proof-evidence.json',
63
+ agent_effort_policy: 'agents/agent-effort-policy.json'
64
+ };
65
+ await writeJsonAtomic(path.join(dir, 'release-review-native-agent-plan.json'), plan);
66
+ }
33
67
  async function agentPlan(parsed) {
34
68
  const root = await sksRoot();
35
69
  const roster = buildAgentRoster({ agents: parsed.agents, concurrency: parsed.concurrency, prompt: parsed.prompt, readonly: parsed.readonly });
@@ -1,15 +1,14 @@
1
1
  import path from 'node:path';
2
- import os from 'node:os';
3
- import fsp from 'node:fs/promises';
4
2
  import { spawnSync } from 'node:child_process';
5
3
  import { COMMANDS, LEGACY_COMMAND_ALIASES } from '../../cli/command-registry.js';
6
4
  import { flag } from '../../cli/args.js';
7
5
  import { printJson, sksTextLogo } from '../../cli/output.js';
8
6
  import { ui as cliUi } from '../../cli/cli-theme.js';
9
- import { PACKAGE_VERSION, ensureDir, exists, nowIso, projectRoot, readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
7
+ import { PACKAGE_VERSION, ensureDir, exists, nowIso, projectRoot, readJson, sksRoot, tmpdir, writeJsonAtomic } from '../fsx.js';
10
8
  import { COMMAND_CATALOG, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS, USAGE_TOPICS, routePrompt, routeReasoning, reasoningInstruction } from '../routes.js';
11
9
  import { initProject, normalizeInstallScope, sksCommandPrefix } from '../init.js';
12
10
  import { buildFeatureRegistry, validateFeatureRegistry } from '../feature-registry.js';
11
+ import { runFeatureFixture } from '../feature-fixture-executor.js';
13
12
  import { hooksExplainReport } from '../../cli/feature-commands.js';
14
13
  import { writeSelftestRouteProof } from '../proof/selftest-proof-fixtures.js';
15
14
  import { createMission } from '../mission.js';
@@ -281,9 +280,11 @@ export async function postinstallCommand(args = []) {
281
280
  return postinstall({ bootstrap: bootstrapCommand, args });
282
281
  }
283
282
  export async function selftestCommand(args = []) {
283
+ if (flag(args, '--real'))
284
+ return selftestRealCommand(args);
284
285
  process.env.CI = 'true';
285
286
  const root = await projectRoot();
286
- const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'sks-selftest-'));
287
+ const tmp = tmpdir('sks-selftest-');
287
288
  await ensureDir(tmp);
288
289
  const registry = await buildFeatureRegistry({ root });
289
290
  const coverage = validateFeatureRegistry(registry);
@@ -309,6 +310,62 @@ export async function selftestCommand(args = []) {
309
310
  return printJson(result);
310
311
  console.log('SKS selftest passed');
311
312
  }
313
+ /**
314
+ * `sks selftest --real`: actually spawns every feature fixture whose kind is
315
+ * 'execute' or 'execute_and_validate_artifacts', derives real pass/fail status
316
+ * from the process exit code (and, for execute_and_validate_artifacts, from
317
+ * whether the declared expected_artifacts actually exist and match their
318
+ * declared schema), and writes a JSON report that explicitly lists fixtures
319
+ * skipped because they are mock/wiring_only kind rather than silently omitting
320
+ * them. This is additive: plain `sks selftest --mock` behavior is unchanged.
321
+ */
322
+ export async function selftestRealCommand(args = []) {
323
+ process.env.CI = 'true';
324
+ const root = await projectRoot();
325
+ const registry = await buildFeatureRegistry({ root });
326
+ const executableKinds = new Set(['execute', 'execute_and_validate_artifacts']);
327
+ const results = [];
328
+ const skippedWiringOnly = [];
329
+ for (const feature of registry.features || []) {
330
+ const fx = feature.fixture || {};
331
+ if (executableKinds.has(fx.kind)) {
332
+ const run = await runFeatureFixture(feature, { root });
333
+ results.push(run);
334
+ }
335
+ else if (fx.kind === 'mock' || fx.kind === 'wiring_only' || fx.quality === 'wiring_only') {
336
+ skippedWiringOnly.push({ id: feature.id, kind: fx.kind, reason: fx.reason || 'mock_or_wiring_only_kind_not_executed' });
337
+ }
338
+ }
339
+ const failures = results.filter((row) => !row.ok);
340
+ const ok = failures.length === 0;
341
+ const report = {
342
+ schema: 'sks.selftest-real.v1',
343
+ ok,
344
+ version: PACKAGE_VERSION,
345
+ generated_at: nowIso(),
346
+ root,
347
+ checked: results.length,
348
+ passed: results.filter((row) => row.ok).length,
349
+ failed: failures.length,
350
+ results,
351
+ skipped_wiring_only: skippedWiringOnly,
352
+ skipped_wiring_only_count: skippedWiringOnly.length,
353
+ blockers: failures.flatMap((row) => row.blockers || [])
354
+ };
355
+ const reportPath = path.join(root, '.sneakoscope', 'reports', 'selftest-real-report.json');
356
+ await writeJsonAtomic(reportPath, report);
357
+ const output = { ...report, report_file: path.relative(root, reportPath) };
358
+ if (flag(args, '--json'))
359
+ return printJson(output);
360
+ console.log(`SKS selftest --real: ${ok ? 'passed' : 'blocked'} (checked=${results.length}, skipped_wiring_only=${skippedWiringOnly.length})`);
361
+ console.log(`Report: ${path.relative(root, reportPath)}`);
362
+ if (!ok) {
363
+ for (const blocker of report.blockers)
364
+ console.log(`- ${blocker}`);
365
+ process.exitCode = 1;
366
+ }
367
+ return output;
368
+ }
312
369
  export async function reasoningCommand(args = []) {
313
370
  const prompt = args.filter((arg) => !String(arg).startsWith('--')).join(' ').trim();
314
371
  const route = routePrompt(prompt || '$SKS');
@@ -1,8 +1,17 @@
1
1
  import { findLatestMission, listSessionStates } from '../mission.js';
2
2
  import { DOLLAR_SKILL_NAMES, RECOMMENDED_SKILLS } from '../routes.js';
3
3
  export const flag = (args = [], name) => args.includes(name);
4
- export function promptOf(args = []) {
5
- return args.filter((x) => !String(x).startsWith('--')).join(' ').trim();
4
+ // Blindly dropping every "--"-prefixed argv token (the previous behavior of
5
+ // promptOf/positionalArgs) silently deletes any part of an unquoted work-order
6
+ // prompt that happens to contain a flag-lookalike phrase (e.g. "--files 로
7
+ // 확인해라"). Only strip tokens that are actually recognized boolean/global
8
+ // flags; anything else stays in the reconstructed prompt.
9
+ const KNOWN_BOOLEAN_FLAGS = new Set([
10
+ '--json', '--mock', '--execute', '--auto', '--visual', '--research', '--db',
11
+ '--legacy-goal-runtime', '--help', '-h'
12
+ ]);
13
+ export function promptOf(args = [], knownFlags = KNOWN_BOOLEAN_FLAGS) {
14
+ return args.filter((x) => !knownFlags.has(String(x))).join(' ').trim();
6
15
  }
7
16
  export async function resolveMissionId(root, arg) {
8
17
  if (arg && arg !== 'latest')
@@ -56,8 +65,9 @@ export function positionalArgs(args = []) {
56
65
  i += 1;
57
66
  continue;
58
67
  }
59
- if (!arg.startsWith('--'))
60
- out.push(arg);
68
+ if (KNOWN_BOOLEAN_FLAGS.has(arg))
69
+ continue;
70
+ out.push(arg);
61
71
  }
62
72
  return out;
63
73
  }
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { exists, readJson, sksRoot } from '../fsx.js';
3
3
  import { initProject } from '../init.js';
4
4
  import { createMission, loadMission, setCurrent, stateFile } from '../mission.js';
5
+ import { createAndWriteWorkOrderLedgerForPrompt, closeWorkOrderLedgerForRouteResult } from '../work-order-ledger.js';
5
6
  import { GOAL_BRIDGE_ARTIFACT, GOAL_WORKFLOW_ARTIFACT, updateGoalWorkflow, writeGoalWorkflow } from '../goal-workflow.js';
6
7
  import { flag, promptOf, resolveMissionId } from './command-utils.js';
7
8
  import { compileGoalToLoopPlan } from '../loops/goal-to-loop-compat.js';
@@ -37,11 +38,13 @@ async function goalCreate(args) {
37
38
  if (flag(args, '--legacy-goal-runtime') || process.env.SKS_LEGACY_GOAL_RUNTIME === '1')
38
39
  return legacyGoalCreate(root, prompt, args);
39
40
  const { id, dir, mission } = await createMission(root, { mode: 'goal', prompt });
41
+ await createAndWriteWorkOrderLedgerForPrompt(dir, { missionId: id, route: 'Goal', prompt });
40
42
  const workflow = await writeGoalWorkflow(dir, mission, { action: 'create', prompt });
41
43
  const plan = await compileGoalToLoopPlan({ root, missionId: id, goalText: prompt, legacyGoalOptions: { native_goal: workflow.native_goal } });
42
44
  const result = await runLoopPlan({ root, plan, parallelism: 'balanced' });
43
45
  const gate = await evaluateLocalGate({ root, missionId: id, gateFile: 'loop-graph-proof.json' });
44
46
  const ok = result.ok === true && gate.ok === true;
47
+ await closeWorkOrderLedgerForRouteResult(dir, { ok, blockers: gate.blockers || [] });
45
48
  await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: ok ? 'GOAL_LOOP_COMPLETED' : 'GOAL_LOOP_BLOCKED', questions_allowed: true, implementation_allowed: true, native_goal: workflow.native_goal, stop_gate: 'loop-graph-proof.json', stop_gate_blockers: gate.blockers }, { replace: true });
46
49
  if (!ok)
47
50
  process.exitCode = 1;
@@ -13,6 +13,7 @@ import { sha256File, imageDimensions } from '../wiki-image/image-hash.js';
13
13
  import { writeRouteCollaborationArtifacts } from '../agents/route-collaboration-ledger.js';
14
14
  import { codexChromeExtensionStatus } from '../codex-app.js';
15
15
  import { requireCodexImagegen } from '../imagegen/require-imagegen.js';
16
+ import { evaluateGate } from '../stop-gate/gate-evaluator.js';
16
17
  const ONE_BY_ONE_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMB/axX7V8AAAAASUVORK5CYII=';
17
18
  const IMAGE_UX_REVIEW_ARTIFACT_PATHS = {
18
19
  policy: IMAGE_UX_REVIEW_POLICY_ARTIFACT,
@@ -358,9 +359,11 @@ async function statusImageUxReview(root, args = []) {
358
359
  const gate = await readJson(path.join(dir, 'image-ux-review-gate.json'), null);
359
360
  const issueLedger = await readJson(path.join(dir, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT), null);
360
361
  const generatedLedger = await readJson(path.join(dir, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT), null);
361
- const result = { schema: 'sks.image-ux-review-status.v2', ok: true, mission_id: missionId, gate, issue_ledger: issueLedger, generated_review_ledger: generatedLedger };
362
+ const gateVerdict = await evaluateGate(root, missionId, 'image-ux-review-gate.json');
363
+ const result = { schema: 'sks.image-ux-review-status.v2', ok: true, mission_id: missionId, gate, gate_verdict: gateVerdict, issue_ledger: issueLedger, generated_review_ledger: generatedLedger };
362
364
  if (flag(args, '--json'))
363
365
  return printJson(result);
366
+ console.log(gateVerdict.verdict);
364
367
  console.log(`Image UX Review mission: ${missionId}`);
365
368
  console.log(`Gate: ${gate?.status || (gate?.passed ? 'passed' : gate ? 'present' : 'missing')}`);
366
369
  if (gate?.verified_level)
@@ -26,6 +26,7 @@ import { repairZellijForSks } from '../zellij/zellij-self-heal.js';
26
26
  import { buildMadGlmLaunchArtifact, buildMadGlmLaunchProfileNoWrite, resolveMadGlmLaunchKey, writeMadGlmCodexWrapper } from '../providers/glm/glm-mad-launch.js';
27
27
  import { GLM_MAD_MODE } from '../providers/glm/glm-52-settings.js';
28
28
  import { assertNonGlmMadRoute } from '../routes/model-mode-router.js';
29
+ import { evaluateGate } from '../stop-gate/gate-evaluator.js';
29
30
  const MAD_SKS_DEFAULT_TTL_MS = 10 * 60 * 1000;
30
31
  export async function madHighCommand(args = [], deps = {}) {
31
32
  const subcommand = firstSubcommand(args);
@@ -1013,6 +1014,12 @@ async function madSksSubcommand(subcommand, args = []) {
1013
1014
  if (subcommand === 'doctor' || subcommand === 'status') {
1014
1015
  const protectedCore = resolveProtectedCore({ packageRoot: packageRoot(), targetRoot });
1015
1016
  const before = await snapshotProtectedCore(packageRoot(), 'status');
1017
+ const statusMissionId = await findLatestMission(root);
1018
+ const gateVerdict = statusMissionId
1019
+ ? await evaluateGate(root, statusMissionId, 'mad-sks-gate.json')
1020
+ : await evaluateGate(root, 'no-mission', 'mad-sks-gate.json');
1021
+ if (!json)
1022
+ console.log(gateVerdict.verdict);
1016
1023
  return emit({
1017
1024
  schema: subcommand === 'doctor' ? 'sks.mad-sks-doctor.v1' : 'sks.mad-sks-status.v1',
1018
1025
  ok: true,
@@ -1022,7 +1029,9 @@ async function madSksSubcommand(subcommand, args = []) {
1022
1029
  protected_core_snapshot: before,
1023
1030
  protected_core_immutable: !protectedCore.engine_source_exception,
1024
1031
  protected_core_write_allowed: protectedCore.engine_source_exception,
1025
- permission_active: false
1032
+ permission_active: false,
1033
+ mission_id: statusMissionId,
1034
+ gate_verdict: gateVerdict
1026
1035
  }, json);
1027
1036
  }
1028
1037
  if (subcommand === 'close' || subcommand === 'revoke') {
@@ -97,7 +97,8 @@ function printStatus(result) {
97
97
  console.log(`Running: ${result.running ? 'yes' : 'no'}`);
98
98
  console.log(`Launchd: ${result.launchd.ok ? result.launchd.state || 'present' : result.launchd.error || 'missing'}`);
99
99
  console.log(`Version: ${result.build_stamp?.package_version || 'unknown'} (package ${result.package_version})`);
100
- console.log(`Target: ${result.action_target.sks_entry || 'missing'} ${result.action_target.sks_entry_exists ? '(ok)' : '(missing)'}`);
100
+ console.log(`Target: ${result.action_target.sks_entry || 'runtime'} ${result.action_target.ok ? '(smoke ok)' : '(smoke failed)'}`);
101
+ console.log(`Codex sync:${result.codex_sync.bundle_id || 'disabled'} ${result.codex_sync.codex_running === null ? '' : result.codex_sync.codex_running ? '(running)' : '(not running)'}`);
101
102
  console.log(`Signature: ${result.signature.identifier || 'unknown'} ${result.signature.ok ? '(ok)' : '(check)'}`);
102
103
  if (result.warnings.length) {
103
104
  console.log('Warnings:');
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { ui as cliUi } from '../../cli/cli-theme.js';
3
3
  import { createMission, findLatestMission, loadMission, setCurrent } from '../mission.js';
4
+ import { createAndWriteWorkOrderLedgerForPrompt, closeWorkOrderLedgerForRouteResult } from '../work-order-ledger.js';
4
5
  import { nowIso, readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
5
6
  import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
6
7
  import { classifyOllamaWorkerSlice } from '../agents/agent-runner-ollama.js';
@@ -95,6 +96,7 @@ async function narutoRun(parsed) {
95
96
  maxAgentCount: MAX_NARUTO_AGENT_COUNT
96
97
  });
97
98
  const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
99
+ await createAndWriteWorkOrderLedgerForPrompt(mission.dir, { missionId: mission.id, route: 'Naruto', prompt: parsed.prompt });
98
100
  await writeCodex0138CapabilityArtifacts(root, { missionId: mission.id }).catch(() => null);
99
101
  await writeCodex0139CapabilityArtifacts(root, { missionId: mission.id }).catch(() => null);
100
102
  const gitWorktreeCapability = writeCapable
@@ -280,13 +282,13 @@ async function narutoRun(parsed) {
280
282
  rebalance_ready: rebalancePolicy.ok === true,
281
283
  concurrency_governor_ready: true,
282
284
  active_pool_simulated: activePool.ok === true,
283
- verification_dag_ready: true,
284
- gpt_final_pack_ready: true,
285
+ verification_dag_ready: Array.isArray(verificationDag?.tasks),
286
+ gpt_final_pack_ready: Boolean(gptFinalPack?.schema),
285
287
  zellij_dashboard_ready: zellijDashboard.ok === true,
286
288
  native_agent_proof: false,
287
289
  final_arbiter_accepted: false,
288
290
  session_cleanup: false,
289
- blockers: [],
291
+ blockers: [...(workGraph.blockers || []), ...(allocationPolicy.blockers || [])],
290
292
  updated_at: nowIso()
291
293
  });
292
294
  await setCurrent(root, {
@@ -417,9 +419,11 @@ async function narutoRun(parsed) {
417
419
  const regressionProof = summarizeRegressionProof(workGraph, result);
418
420
  await writeJsonAtomic(path.join(mission.dir, 'regression-proof-summary.json'), regressionProof);
419
421
  const tddOk = !regressionProof.required || (regressionProof.regression_test_added && regressionProof.regression_test_failed_before_fix && regressionProof.regression_test_passed_after_fix);
422
+ const narutoGateFullPassed = result.ok === true && nativeProofOk && finalAccepted && parallelRuntimeOk && tddOk && workGraph.ok === true && allocationPolicy.ok === true;
423
+ const narutoGateFullBlockers = [...(result.proof?.blockers || []), ...(parallelRuntimeOk ? [] : ['naruto_parallel_runtime_proof_below_gate']), ...(tddOk ? [] : ['tdd_evidence_missing']), ...(workGraph.blockers || []), ...(allocationPolicy.blockers || [])];
420
424
  await writeJsonAtomic(path.join(mission.dir, 'naruto-gate.json'), {
421
425
  schema: 'sks.naruto-gate.v1',
422
- passed: result.ok === true && nativeProofOk && finalAccepted && parallelRuntimeOk && tddOk,
426
+ passed: narutoGateFullPassed,
423
427
  mission_id: mission.id,
424
428
  clone_roster_built: true,
425
429
  clone_count: roster.agent_count,
@@ -429,8 +433,8 @@ async function narutoRun(parsed) {
429
433
  rebalance_ready: rebalancePolicy.ok === true,
430
434
  concurrency_governor_ready: true,
431
435
  active_pool_simulated: activePool.ok === true,
432
- verification_dag_ready: true,
433
- gpt_final_pack_ready: true,
436
+ verification_dag_ready: Array.isArray(verificationDag?.tasks),
437
+ gpt_final_pack_ready: Boolean(gptFinalPack?.schema),
434
438
  zellij_dashboard_ready: zellijDashboard.ok === true,
435
439
  native_agent_proof: nativeProofOk,
436
440
  parallel_runtime_proof: parallelRuntimeOk,
@@ -440,9 +444,10 @@ async function narutoRun(parsed) {
440
444
  regression_proof: 'regression-proof-summary.json',
441
445
  final_arbiter_accepted: finalAccepted,
442
446
  session_cleanup: result.proof?.all_sessions_closed === true || nativeProofOk,
443
- blockers: [...(result.proof?.blockers || []), ...(parallelRuntimeOk ? [] : ['naruto_parallel_runtime_proof_below_gate']), ...(tddOk ? [] : ['tdd_evidence_missing'])],
447
+ blockers: narutoGateFullBlockers,
444
448
  updated_at: nowIso()
445
449
  });
450
+ await closeWorkOrderLedgerForRouteResult(mission.dir, { ok: narutoGateFullPassed, blockers: narutoGateFullBlockers });
446
451
  const clones = result.roster?.agent_count ?? roster.agent_count;
447
452
  const localWorkerSummary = summarizeNarutoLocalWorkerResult(localWorker, result);
448
453
  // Finalizer policy: when local LLM workers contributed patches, the GPT
@@ -337,6 +337,45 @@ function mockPptFixtureGate(gate = {}) {
337
337
  blockers: ['ppt_fixture_mode_cannot_claim_real']
338
338
  };
339
339
  }
340
+ // The image-asset-ledger's `assets` array (see buildPptImageAssetLedger in ../ppt.ts) is the
341
+ // authoritative list of raster/bitmap image assets planned or generated for the deck; every
342
+ // entry in it is a raster PNG produced (or pending) via Codex App $imagegen/gpt-image-2. When the
343
+ // ledger omits `imagegen_evidence` outright, we must NOT assume the imagegen-required policy was
344
+ // satisfied. Instead derive a safe default from that asset list: any raster asset present forces
345
+ // the derived evidence to fail-closed (required + not passed) rather than silently pass.
346
+ function deriveImagegenEvidenceDefault(imageAssetLedger) {
347
+ if (imageAssetLedger?.imagegen_evidence)
348
+ return imageAssetLedger.imagegen_evidence;
349
+ const rasterAssets = Array.isArray(imageAssetLedger?.assets) ? imageAssetLedger.assets : [];
350
+ const rasterAssetCount = rasterAssets.length;
351
+ if (rasterAssetCount === 0) {
352
+ return {
353
+ schema: 'sks.ppt-imagegen-evidence.v1',
354
+ required: false,
355
+ passed: true,
356
+ blockers: [],
357
+ derived: true,
358
+ derivation_basis: { raster_asset_count: 0, source: 'derived_default_no_raster_assets' }
359
+ };
360
+ }
361
+ return {
362
+ schema: 'sks.ppt-imagegen-evidence.v1',
363
+ required: true,
364
+ passed: false,
365
+ required_count: imageAssetLedger?.required_count || rasterAssetCount,
366
+ generated_count: imageAssetLedger?.generated_count || 0,
367
+ generated_image_evidence: false,
368
+ assets: [],
369
+ blockers: ['imagegen_evidence_missing'],
370
+ derived: true,
371
+ derivation_basis: {
372
+ raster_asset_count: rasterAssetCount,
373
+ source: 'derived_default_raster_assets_present',
374
+ note: `imagegen_evidence section was absent from the image-asset-ledger while ${rasterAssetCount} raster asset(s) were present; failing closed instead of silently passing.`
375
+ },
376
+ passed_note: 'imagegen_evidence_missing: ledger omitted imagegen_evidence despite raster assets requiring Codex App imagegen verification'
377
+ };
378
+ }
340
379
  export async function evaluatePptGateArtifacts(dir, baseGate = {}) {
341
380
  const factLedger = await readJson(path.join(dir, PPT_FACT_LEDGER_ARTIFACT), null);
342
381
  const imageAssetLedger = await readJson(path.join(dir, PPT_IMAGE_ASSET_LEDGER_ARTIFACT), null);
@@ -370,7 +409,7 @@ export async function evaluatePptGateArtifacts(dir, baseGate = {}) {
370
409
  const renderReportPassed = renderReport?.passed === true;
371
410
  const factLedgerPassed = factLedger?.passed === true && Number(factLedger.unsupported_critical_claims_count || 0) === 0;
372
411
  const imageAssetLedgerPassed = imageAssetLedger?.passed === true;
373
- const imagegenEvidence = imageAssetLedger?.imagegen_evidence || { schema: 'sks.ppt-imagegen-evidence.v1', required: false, passed: true, blockers: [] };
412
+ const imagegenEvidence = deriveImagegenEvidenceDefault(imageAssetLedger);
374
413
  const imagegenEvidencePassed = imagegenEvidence?.required === true ? imagegenEvidence?.passed === true : true;
375
414
  const reviewLedgerPassed = reviewLedger?.passed === true;
376
415
  const iterationReportPassed = iterationReport?.passed === true;
@@ -14,6 +14,7 @@ import { maybeFinalizeRoute } from '../proof/auto-finalize.js';
14
14
  import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
15
15
  import { flag, promptOf, readBoundedIntegerFlag, readFlagValue, readMaxCycles, resolveMissionId, safeReadTextFile } from './command-utils.js';
16
16
  import { runCodexAppHandoff, qaLoopShouldRequestAppHandoff } from '../codex-app/codex-app-handoff.js';
17
+ import { evaluateGate } from '../stop-gate/gate-evaluator.js';
17
18
  import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
18
19
  import { writeCodexAccountUsageArtifacts } from '../usage/codex-account-usage.js';
19
20
  import { buildQaLoopBudgetPolicy, selectQaLoopEscalatedEffort } from '../qa-loop/qa-loop-budget-policy.js';
@@ -441,8 +442,10 @@ async function qaLoopStatus(args) {
441
442
  const desktop = await readJson(path.join(dir, 'qa-loop', 'app-handoff.json'), null);
442
443
  const desktopConfirmation = await readJson(path.join(dir, 'qa-loop', 'app-handoff-confirmation.json'), null);
443
444
  const desktopReviewComplete = desktopConfirmation?.verdict === 'pass';
445
+ const gateVerdict = await evaluateGate(root, id, 'qa-gate.json');
444
446
  if (flag(args, '--json'))
445
- return console.log(JSON.stringify({ mission, state, qa: status, desktop_app_handoff: desktop, desktop_app_confirmation: desktopConfirmation, desktop_review_complete: desktopReviewComplete, native_agent_plan: nativeAgentPlan, agent_sessions: agentSessions?.sessions || null }, null, 2));
447
+ return console.log(JSON.stringify({ mission, state, qa: status, desktop_app_handoff: desktop, desktop_app_confirmation: desktopConfirmation, desktop_review_complete: desktopReviewComplete, native_agent_plan: nativeAgentPlan, agent_sessions: agentSessions?.sessions || null, gate_verdict: gateVerdict }, null, 2));
448
+ console.log(gateVerdict.verdict);
446
449
  console.log('SKS QA-LOOP Status\n');
447
450
  console.log(`Mission: ${id}`);
448
451
  console.log(`Phase: ${state.phase || mission.phase}`);
@@ -27,7 +27,7 @@ export async function releaseCommand(args = []) {
27
27
  await writeTextAtomic(stdoutPath, String(result.stdout || ''));
28
28
  await writeTextAtomic(stderrPath, String(result.stderr || ''));
29
29
  const readiness = await findReleaseReadinessReport(root);
30
- const requiredSections = ['five_lane_review', 'integration_evidence', 'session_cleanup'];
30
+ const requiredSections = [];
31
31
  const missingSections = requiredSections.filter((section) => readiness.report?.[section] == null);
32
32
  if (readiness.report)
33
33
  await writeJsonAtomic(path.join(mission.dir, 'release-readiness-report.json'), readiness.report);
@@ -53,14 +53,14 @@ export async function releaseCommand(args = []) {
53
53
  stdout_tail: tail(String(result.stdout || '')),
54
54
  stderr_tail: tail(String(result.stderr || ''))
55
55
  };
56
+ if (!report.ok)
57
+ process.exitCode = result.status || 1;
56
58
  if (json)
57
59
  return printJson(report);
58
60
  if (result.stdout)
59
61
  process.stdout.write(result.stdout);
60
62
  if (result.stderr)
61
63
  process.stderr.write(result.stderr);
62
- if (!report.ok)
63
- process.exitCode = result.status || 1;
64
64
  return report;
65
65
  }
66
66
  async function findReleaseReadinessReport(root) {
@@ -23,6 +23,7 @@ import { readImplementationBlueprint, validateImplementationBlueprint } from '..
23
23
  import { readExperimentPlan, validateExperimentPlan } from '../research/experiment-plan.js';
24
24
  import { readReplicationPack, validateReplicationPack } from '../research/replication-pack.js';
25
25
  import { readResearchFinalReview } from '../research/research-final-reviewer.js';
26
+ import { evaluateGate } from '../stop-gate/gate-evaluator.js';
26
27
  const RESEARCH_DEFAULT_MAX_CYCLES = 12;
27
28
  const RESEARCH_DEFAULT_CYCLE_TIMEOUT_MINUTES = 120;
28
29
  const RESEARCH_MIN_CYCLE_TIMEOUT_MINUTES = 15;
@@ -360,6 +361,8 @@ async function researchStatus(args) {
360
361
  const { dir, mission } = await loadMission(root, id);
361
362
  const state = await readJson(stateFile(root), {});
362
363
  const gate = await readJson(path.join(dir, 'research-gate.evaluated.json'), await readJson(path.join(dir, 'research-gate.json'), null));
364
+ const gateVerdict = await evaluateGate(root, id, 'research-gate.json');
365
+ console.log(gateVerdict.verdict);
363
366
  const ledger = await readJson(path.join(dir, 'novelty-ledger.json'), null);
364
367
  const sourceLedger = await readJson(path.join(dir, 'source-ledger.json'), null);
365
368
  const agentLedger = await readJson(path.join(dir, 'agent-ledger.json'), null);
@@ -398,6 +401,7 @@ async function researchStatus(args) {
398
401
  autoresearch_cycle_policy: plan?.autoresearch_cycle_policy || null,
399
402
  legacy_alias_policy: plan?.native_agent_plan?.legacy_artifact_alias_policy || null,
400
403
  gate,
404
+ gate_verdict: gateVerdict,
401
405
  novelty_entries: ledger?.entries?.length ?? null,
402
406
  source_entries: sourceLedger?.sources?.length ?? null,
403
407
  source_layers_required: sourceLayerRows.length || gate?.metrics?.source_layers_required || gate?.source_layers_required || null,
@@ -36,6 +36,8 @@ export async function evaluateLocalGate(input) {
36
36
  blockers.push('gate_not_passed');
37
37
  if (gate.ok === false)
38
38
  blockers.push('gate_ok_false');
39
+ if (gate.execution_class === 'mock_fixture')
40
+ blockers.push('gate_execution_class_mock_fixture');
39
41
  if (Array.isArray(gate.blockers) && gate.blockers.length)
40
42
  blockers.push(...gate.blockers.map(String));
41
43
  if (Array.isArray(gate.missing_fields) && gate.missing_fields.length)
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { exists, projectRoot, runProcess, writeJsonAtomic } from '../fsx.js';
3
3
  import { createMission, missionDir, setCurrent } from '../mission.js';
4
+ import { createAndWriteWorkOrderLedgerForPrompt, closeWorkOrderLedgerForRouteResult } from '../work-order-ledger.js';
4
5
  import { maybeFinalizeRoute } from '../proof/auto-finalize.js';
5
6
  import { routePrompt } from '../routes.js';
6
7
  import { latestTrustReport } from '../trust-kernel/trust-report.js';
@@ -57,6 +58,7 @@ export async function runCommand(args = []) {
57
58
  console.log(`Next: ${classification.next_action}`);
58
59
  return result;
59
60
  }
61
+ await createAndWriteWorkOrderLedgerForPrompt(dir, { missionId: id, route: route.command, prompt });
60
62
  if (execute)
61
63
  return executeRunRoute(root, { id, dir, route, prompt, args, classification, auto });
62
64
  return finalizeMockRun(root, { id, route, prompt, args, classification, mode });
@@ -85,6 +87,7 @@ async function finalizeMockRun(root, { id, route, prompt, args, classification,
85
87
  });
86
88
  const trust = await loadTrustReport(root, id);
87
89
  const completionOk = proof.ok && proof.proof?.status !== 'mock_only' && gate.passed === true;
90
+ await closeWorkOrderLedgerForRouteResult(missionDir(root, id), { ok: completionOk, blockers: gate.blockers });
88
91
  await setCurrent(root, {
89
92
  mission_id: id,
90
93
  mode: 'RUN',
@@ -154,12 +157,14 @@ async function executeRunRoute(root, context) {
154
157
  : await loadTrustReport(root, id);
155
158
  const autoVerification = auto ? await runAutoVerification(root, id) : null;
156
159
  const autoOk = autoVerification?.ok ?? true;
160
+ const executeOk = execution.ok && proof.ok && autoOk;
161
+ await closeWorkOrderLedgerForRouteResult(dir, { ok: executeOk, blockers: execution.blockers });
157
162
  await setCurrent(root, {
158
163
  mission_id: id,
159
164
  mode: 'RUN',
160
165
  route: route.id,
161
166
  route_command: route.command,
162
- phase: execution.ok && proof.ok && autoOk ? 'RUN_EXECUTE_DONE' : 'RUN_EXECUTE_BLOCKED',
167
+ phase: executeOk ? 'RUN_EXECUTE_DONE' : 'RUN_EXECUTE_BLOCKED',
163
168
  implementation_allowed: execution.ok,
164
169
  nested_mission_id: execution.nested_mission_id,
165
170
  completion_proof: 'completion-proof.json',
@@ -167,7 +172,7 @@ async function executeRunRoute(root, context) {
167
172
  });
168
173
  const result = {
169
174
  schema: 'sks.run.v2',
170
- ok: execution.ok && proof.ok && autoOk,
175
+ ok: executeOk,
171
176
  mission_id: id,
172
177
  route: route.command,
173
178
  mode: auto ? 'auto' : 'execute',
@@ -242,11 +247,21 @@ async function executeRouteCommand(root, route, prompt, { auto = false } = {}) {
242
247
  });
243
248
  }
244
249
  const commandArgs = safeRouteExecutionArgs(route, prompt, { auto });
250
+ // safeRouteExecutionArgs() falls back to `team <prompt> --mock` for any route it
251
+ // doesn't have a dedicated safe live command for, and several dedicated branches
252
+ // (DB/Wiki/Fast-Mode/with-local-llm-on/Commit/Commit-And-Push) run a fixed
253
+ // command that never references the prompt at all. Both must be labeled
254
+ // honestly rather than as a live-route completion: a --mock invocation cannot
255
+ // claim a real completion (14차 honest-mock principle), and a fixed command
256
+ // that never saw the prompt cannot claim to have addressed it.
257
+ const isMockFallback = commandArgs.includes('--mock');
258
+ const promptDelivered = Boolean(prompt) && commandArgs.includes(prompt);
245
259
  const result = await runSks(root, commandArgs);
246
260
  return routeExecutionResult(route, ['sks', ...commandArgs].join(' '), result, {
247
- okStatus: 'completed',
248
- trustStatus: 'verified_partial',
249
- executionKind: 'live_route',
261
+ okStatus: isMockFallback ? 'verified_partial' : 'completed',
262
+ trustStatus: isMockFallback ? 'mock_only' : 'verified_partial',
263
+ executionKind: isMockFallback ? 'mock_safe' : 'live_route',
264
+ promptDelivered,
250
265
  });
251
266
  }
252
267
  async function runAutoVerification(root, missionId) {
@@ -331,6 +346,8 @@ function routeExecutionResult(route, command, result, options = {}) {
331
346
  };
332
347
  if (options.steps)
333
348
  execution.steps = options.steps;
349
+ if (options.promptDelivered !== undefined)
350
+ execution.prompt_delivered = options.promptDelivered;
334
351
  return execution;
335
352
  }
336
353
  function classifyRunRoute(prompt, args) {
@@ -3,6 +3,7 @@ import { printJson } from '../../cli/output.js';
3
3
  import { projectRoot } from '../fsx.js';
4
4
  import { flag, readOption } from './command-utils.js';
5
5
  import { runSearchVisibilityApply, runSearchVisibilityAudit, runSearchVisibilityDoctor, runSearchVisibilityFixture, runSearchVisibilityPlan, runSearchVisibilityRollback, runSearchVisibilityStatus, runSearchVisibilityVerify, resolveSearchVisibilityMission, } from '../search-visibility/index.js';
6
+ import { evaluateGate } from '../stop-gate/gate-evaluator.js';
6
7
  import { evaluateLocalGate } from './route-success-helpers.js';
7
8
  export async function seoCommand(args = []) {
8
9
  return runSearchVisibilityCommand('seo', args, 'seo');
@@ -22,8 +23,12 @@ export async function runSearchVisibilityCommand(mode, args = [], displayCommand
22
23
  if (!mission)
23
24
  blockers.push('mission_required_for_apply');
24
25
  else {
26
+ // explicit_apply_only precondition: a mutation plan must already exist on disk,
27
+ // proving a prior `plan` step was run and reviewed before this apply can mutate anything.
25
28
  const planGate = await evaluateLocalGate({ root: mission.root, dir: mission.dir, gateFile: mode === 'seo' ? 'seo-gate.json' : 'geo-gate.json', requiredArtifacts: ['search-visibility/mutation-plan.json'] });
26
- blockers.push(...planGate.blockers.filter((blocker) => blocker !== 'gate_not_passed' && blocker !== 'gate_ok_false'));
29
+ if (planGate.blockers.includes('missing_artifact:search-visibility/mutation-plan.json'))
30
+ blockers.push('seo_apply_missing_mutation_plan');
31
+ blockers.push(...planGate.blockers.filter((blocker) => blocker !== 'gate_not_passed' && blocker !== 'gate_ok_false' && blocker !== 'missing_artifact:search-visibility/mutation-plan.json'));
27
32
  }
28
33
  if (blockers.length) {
29
34
  result = { schema: 'sks.search-visibility.apply-command.v1', ok: false, route: mode === 'seo' ? '$SEO-GEO-OPTIMIZER' : '$SEO-GEO-OPTIMIZER', status: 'blocked', blockers };
@@ -75,18 +80,38 @@ async function applySearchVisibilityGateResult(mode, action, result, options) {
75
80
  : action === 'status'
76
81
  ? []
77
82
  : ['search-visibility/verification-report.json'];
83
+ const gateFile = mode === 'seo' ? 'seo-gate.json' : 'geo-gate.json';
78
84
  const evaluated = await evaluateLocalGate({
79
85
  root: mission.root,
80
86
  dir: mission.dir,
81
- gateFile: mode === 'seo' ? 'seo-gate.json' : 'geo-gate.json',
87
+ gateFile,
82
88
  requiredArtifacts
83
89
  });
84
90
  const blockers = [...new Set([...(result.blockers || []), ...evaluated.blockers])];
91
+ if (action === 'apply') {
92
+ // explicit_apply_only postcondition: apply must not report success unless both the mutation
93
+ // plan and the rollback manifest it produces are actually present on disk after execution.
94
+ if (blockers.includes('missing_artifact:search-visibility/mutation-plan.json') && !blockers.includes('seo_apply_missing_mutation_plan'))
95
+ blockers.push('seo_apply_missing_mutation_plan');
96
+ if (blockers.includes('missing_artifact:search-visibility/rollback-manifest.json') && !blockers.includes('seo_apply_missing_rollback_manifest'))
97
+ blockers.push('seo_apply_missing_rollback_manifest');
98
+ }
99
+ // Re-evaluate against the authoritative on-disk gate (catches mock/fixture-only gates,
100
+ // not just the presence checks `evaluateLocalGate` performs) before reporting final success.
101
+ const authoritative = await evaluateGate(mission.root, mission.id, gateFile);
102
+ if (!authoritative.pass) {
103
+ for (const reason of authoritative.reasons) {
104
+ if (!blockers.includes(reason))
105
+ blockers.push(reason);
106
+ }
107
+ }
108
+ const ok = result.ok === true && evaluated.ok === true && authoritative.pass === true && blockers.length === 0;
85
109
  return {
86
110
  ...result,
87
- ok: result.ok === true && evaluated.ok === true,
88
- status: result.ok === true && evaluated.ok === true ? result.status : 'blocked',
111
+ ok,
112
+ status: ok ? result.status : 'blocked',
89
113
  gate_evaluation: evaluated,
114
+ gate_verdict: authoritative.verdict,
90
115
  blockers
91
116
  };
92
117
  }
@@ -167,6 +192,8 @@ function isBlocked(value) {
167
192
  }
168
193
  function printHuman(mode, action, value) {
169
194
  const rec = value && typeof value === 'object' ? value : {};
195
+ if (action === 'status' && rec.gate_verdict)
196
+ console.log(String(rec.gate_verdict));
170
197
  console.log(`SKS ${mode.toUpperCase()} ${action}: ${rec.status || (rec.ok === false ? 'blocked' : 'ok')}`);
171
198
  if (rec.mission_id)
172
199
  console.log(`Mission: ${rec.mission_id}`);