sneakoscope 5.3.0 → 5.5.1

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 (54) hide show
  1. package/README.md +4 -0
  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/cli/install-helpers.js +17 -0
  7. package/dist/config/skills-manifest.json +58 -58
  8. package/dist/core/agents/agent-orchestrator.js +14 -2
  9. package/dist/core/agents/agent-proof-evidence.js +16 -0
  10. package/dist/core/bench.js +24 -2
  11. package/dist/core/codex-app/sks-menubar.js +83 -10
  12. package/dist/core/commands/agent-command.js +35 -1
  13. package/dist/core/commands/basic-cli.js +59 -0
  14. package/dist/core/commands/command-utils.js +14 -4
  15. package/dist/core/commands/goal-command.js +3 -0
  16. package/dist/core/commands/image-ux-review-command.js +4 -1
  17. package/dist/core/commands/mad-sks-command.js +10 -1
  18. package/dist/core/commands/naruto-command.js +12 -7
  19. package/dist/core/commands/ppt-command.js +40 -1
  20. package/dist/core/commands/qa-loop-command.js +4 -1
  21. package/dist/core/commands/release-command.js +3 -3
  22. package/dist/core/commands/research-command.js +4 -0
  23. package/dist/core/commands/route-success-helpers.js +2 -0
  24. package/dist/core/commands/run-command.js +22 -5
  25. package/dist/core/commands/seo-command.js +31 -4
  26. package/dist/core/db-safety.js +8 -1
  27. package/dist/core/feature-fixture-executor.js +262 -0
  28. package/dist/core/feature-fixtures.js +95 -52
  29. package/dist/core/fsx.js +1 -1
  30. package/dist/core/loops/loop-decomposer.js +4 -3
  31. package/dist/core/loops/loop-planner.js +7 -3
  32. package/dist/core/loops/loop-worker-runtime.js +6 -0
  33. package/dist/core/pipeline-internals/runtime-gates.js +39 -3
  34. package/dist/core/proof/selftest-proof-fixtures.js +8 -0
  35. package/dist/core/providers/glm/naruto/glm-naruto-requirement-coverage.js +18 -1
  36. package/dist/core/providers/glm/naruto/glm-naruto-requirement-ledger.js +28 -12
  37. package/dist/core/questions.js +12 -10
  38. package/dist/core/release/release-gate-node.js +2 -0
  39. package/dist/core/routes.js +11 -0
  40. package/dist/core/stop-gate/stop-gate-check.js +4 -0
  41. package/dist/core/stop-gate/stop-gate-writer.js +4 -0
  42. package/dist/core/trust-kernel/trust-report.js +10 -1
  43. package/dist/core/update/update-migration-state.js +15 -1
  44. package/dist/core/update-check.js +44 -2
  45. package/dist/core/version.js +1 -1
  46. package/dist/core/work-order-ledger.js +60 -0
  47. package/dist/scripts/gate-policy-audit-check.js +2 -2
  48. package/dist/scripts/packlist-performance-check.js +7 -1
  49. package/dist/scripts/release-dag-full-coverage-check.js +1 -1
  50. package/dist/scripts/release-gate-existence-audit.js +1 -1
  51. package/dist/scripts/release-gate-planner.js +1 -1
  52. package/dist/scripts/release-metadata-1-19-check.js +1 -1
  53. package/package.json +5 -1
  54. package/schemas/release/release-gate-node.schema.json +1 -0
@@ -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}`);
@@ -645,7 +645,14 @@ async function scanSupabaseMcpConfigs(root) {
645
645
  }
646
646
  function extractSupabaseMcpUrls(text) {
647
647
  const out = new Set();
648
- const re = /https:\/\/mcp\.supabase\.com\/mcp[^"'\s)>,]*/gi;
648
+ // Config files (TOML/JSON) always quote URL values, so a closing quote or
649
+ // whitespace is the only reliable terminator. A comma is a legitimate query-string
650
+ // character (e.g. "...&features=database,docs") - excluding it here silently
651
+ // truncated multi-value features lists, which happened to be benign only because
652
+ // the truncated remainder still contained an allowed feature. Stopping at ')'/'>' is
653
+ // unnecessary for structured config scanning and risks the same truncation for any
654
+ // URL whose query string legitimately contains those characters (URL-encoded or not).
655
+ const re = /https:\/\/mcp\.supabase\.com\/mcp[^"'\s]*/gi;
649
656
  let m;
650
657
  while ((m = re.exec(text)))
651
658
  out.add(m[0]);
@@ -0,0 +1,262 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { resolveExpectedArtifactPath } from './feature-fixture-runner.js';
5
+ import { findLatestMission } from './mission.js';
6
+ const FIXTURE_COMMAND_TIMEOUT_MS = 60_000;
7
+ /** Per-fixture override, set via the fixture()'s `extra.timeout_ms` for commands
8
+ * that are legitimately slower than the 60s default (real agent/swarm orchestration,
9
+ * multi-step pipelines) rather than actually hung. */
10
+ function fixtureTimeoutMs(fixture) {
11
+ const override = Number(fixture?.timeout_ms);
12
+ return Number.isFinite(override) && override > 0 ? override : FIXTURE_COMMAND_TIMEOUT_MS;
13
+ }
14
+ /**
15
+ * Actually spawns a feature fixture's declared `command` string (safely tokenized,
16
+ * no shell interpolation) for `execute` / `execute_and_validate_artifacts` fixtures,
17
+ * checks the real exit code, validates any expected_artifacts that were declared
18
+ * (string path or { path, schema } object form), and derives a real pass/fail status
19
+ * from what actually happened rather than trusting fixture.status.
20
+ *
21
+ * This is distinct from runFeatureFixture() in feature-fixture-runner.ts, which only
22
+ * executes fixtures present in an explicit safe-args allowlist. This function executes
23
+ * whatever the fixture itself declares as its command, so it must tokenize defensively
24
+ * and never hand a raw string to a shell.
25
+ */
26
+ export async function runFeatureFixture(feature, { root = process.cwd() } = {}) {
27
+ const id = feature?.id || feature?.featureId || 'unknown';
28
+ const fixture = feature?.fixture || feature || {};
29
+ const kind = fixture.kind;
30
+ const claimedStatus = fixture.status;
31
+ const expected = normalizeExpectedArtifacts(fixture.expected_artifacts);
32
+ if (kind !== 'execute' && kind !== 'execute_and_validate_artifacts') {
33
+ return {
34
+ id,
35
+ kind,
36
+ executed: false,
37
+ skipped: true,
38
+ skip_reason: kind === 'mock' || kind === 'wiring_only' ? 'mock_or_wiring_only' : `kind_not_executable:${kind}`,
39
+ claimed_status: claimedStatus,
40
+ actual_status: claimedStatus,
41
+ ok: true,
42
+ command: fixture.command || null,
43
+ artifacts: [],
44
+ blockers: []
45
+ };
46
+ }
47
+ if (!fixture.command || !String(fixture.command).trim()) {
48
+ return {
49
+ id,
50
+ kind,
51
+ executed: false,
52
+ skipped: false,
53
+ claimed_status: claimedStatus,
54
+ actual_status: 'missing',
55
+ ok: false,
56
+ command: null,
57
+ artifacts: [],
58
+ blockers: [`${id}:fixture_command_missing`]
59
+ };
60
+ }
61
+ const tokens = tokenizeCommand(fixture.command);
62
+ const spawnEnv = { ...process.env, CI: 'true', SKS_SKIP_NPM_FRESHNESS_CHECK: '1', SKS_ENSURE_DIST_NO_REBUILD: '1' };
63
+ const isSksCommand = tokens[0] === 'sks';
64
+ const [spawnCommand, spawnArgs] = isSksCommand
65
+ ? [process.execPath, [resolveSksEntrypoint(root), ...tokens.slice(1)]]
66
+ : [tokens[0] ?? '', tokens.slice(1)];
67
+ const timeoutMs = fixtureTimeoutMs(fixture);
68
+ const spawnResult = spawnSync(spawnCommand, spawnArgs, {
69
+ cwd: root,
70
+ encoding: 'utf8',
71
+ timeout: timeoutMs,
72
+ maxBuffer: 1024 * 1024 * 20,
73
+ env: spawnEnv
74
+ });
75
+ const timedOut = spawnResult.error?.code === 'ETIMEDOUT';
76
+ const exitOk = !timedOut && spawnResult.status === 0;
77
+ // Most fixture commands create a fresh, uniquely-IDed mission and write their
78
+ // expected artifacts (completion-proof.json, <route>-gate.json, ...) inside that
79
+ // mission's own directory, not at the project root. Without a mission id,
80
+ // resolveExpectedArtifactPath() falls back to root-relative resolution and every
81
+ // such artifact is reported "missing" even when the command succeeded. Prefer the
82
+ // mission id the command itself printed in its --json stdout (unambiguous, immune
83
+ // to races with unrelated concurrent sessions on a shared machine); only fall back
84
+ // to a filesystem mtime scan if the command's own output didn't carry one.
85
+ const missionIdFromOutput = extractMissionId(spawnResult.stdout);
86
+ const latestMissionId = missionIdFromOutput
87
+ ?? (kind === 'execute_and_validate_artifacts' ? await findLatestMission(root).catch(() => null) : null);
88
+ const artifacts = kind === 'execute_and_validate_artifacts'
89
+ ? expected.map((artifact) => inspectArtifact(root, artifact, latestMissionId))
90
+ : [];
91
+ const artifactFailures = artifacts.filter((artifact) => !artifact.ok).map((artifact) => `${id}:${artifact.path}:${artifact.failure || 'artifact_invalid'}`);
92
+ const blockers = [];
93
+ if (!exitOk)
94
+ blockers.push(timedOut ? `${id}:command_timeout_${timeoutMs}` : `${id}:command_exit_${spawnResult.status}`);
95
+ blockers.push(...artifactFailures);
96
+ const ok = blockers.length === 0;
97
+ const actualStatus = ok ? 'pass' : 'blocked';
98
+ // "ok" (the ultimate pass/fail signal selftest --real counts) must mean "the
99
+ // registry's claimed status matches reality" - NOT "the underlying command exited
100
+ // 0" - those are different questions. Several fixtures intentionally run a
101
+ // command that always exits non-zero by design (an honest mock/blocked
102
+ // demonstration, per the execution_class:'mock_fixture' hardening elsewhere in
103
+ // this codebase); such a fixture correctly declaring claimed_status:'blocked' is
104
+ // a PASSING self-consistency check, not a failure, even though blockers still
105
+ // faithfully records the real command_exit_N/artifact issues for anyone auditing
106
+ // what actually happened. Only an actual mismatch between claim and reality is a
107
+ // real fixture-registry defect worth failing selftest --real over.
108
+ const statusMatches = !claimedStatus || claimedStatus === actualStatus;
109
+ if (!statusMatches) {
110
+ blockers.push(`${id}:fixture_status_claim_mismatch:claimed=${claimedStatus}:actual=${actualStatus}`);
111
+ }
112
+ return {
113
+ id,
114
+ kind,
115
+ executed: true,
116
+ skipped: false,
117
+ command: fixture.command,
118
+ args: tokens.slice(1),
119
+ exit_code: spawnResult.status,
120
+ signal: spawnResult.signal || null,
121
+ timed_out: timedOut,
122
+ claimed_status: claimedStatus,
123
+ actual_status: actualStatus,
124
+ ok: statusMatches,
125
+ stdout_bytes: Buffer.byteLength(spawnResult.stdout || ''),
126
+ stderr_bytes: Buffer.byteLength(spawnResult.stderr || ''),
127
+ stderr_tail: String(spawnResult.stderr || '').slice(-800),
128
+ artifacts,
129
+ blockers,
130
+ resolved_mission_id: latestMissionId
131
+ };
132
+ }
133
+ /**
134
+ * Pulls a mission id out of a command's JSON stdout. Handles the common field
135
+ * names/shapes used across sks commands (`mission_id`, `missionId`, a top-level
136
+ * `id` that matches the `M-<timestamp>-<suffix>` pattern, or a nested `mission.id`),
137
+ * and tolerates stdout that has trailing non-JSON log lines by scanning for the
138
+ * last parseable JSON object.
139
+ */
140
+ function extractMissionId(stdout) {
141
+ if (!stdout)
142
+ return null;
143
+ const candidates = [];
144
+ for (const line of String(stdout).split('\n')) {
145
+ const trimmed = line.trim();
146
+ if (!trimmed.startsWith('{'))
147
+ continue;
148
+ try {
149
+ candidates.push(JSON.parse(trimmed));
150
+ }
151
+ catch {
152
+ // not a standalone JSON line; the payload may still be a multi-line pretty-printed
153
+ // object, handled by the whole-stdout attempt below.
154
+ }
155
+ }
156
+ try {
157
+ candidates.push(JSON.parse(stdout));
158
+ }
159
+ catch {
160
+ // pretty-printed JSON mixed with other stdout; ignore.
161
+ }
162
+ const missionIdPattern = /^M-\d{8}-\d{6}-[a-z0-9]+$/i;
163
+ for (const parsed of candidates.reverse()) {
164
+ if (!parsed || typeof parsed !== 'object')
165
+ continue;
166
+ const direct = parsed.mission_id || parsed.missionId || parsed.mission?.id || parsed.completion_proof?.mission_id || parsed.decision?.mission_id;
167
+ if (typeof direct === 'string' && direct)
168
+ return direct;
169
+ if (typeof parsed.id === 'string' && missionIdPattern.test(parsed.id))
170
+ return parsed.id;
171
+ }
172
+ return null;
173
+ }
174
+ function inspectArtifact(root, artifact, latestMissionId) {
175
+ const file = resolveExpectedArtifactPath(root, artifact.path, { latestMissionId });
176
+ const exists = fs.existsSync(file);
177
+ const relPath = path.isAbsolute(artifact.path) ? artifact.path : artifact.path;
178
+ if (!exists) {
179
+ return { path: relPath, schema: artifact.schema, exists: false, ok: Boolean(artifact.optional), failure: artifact.optional ? undefined : 'missing' };
180
+ }
181
+ if (!file.endsWith('.json')) {
182
+ const content = fs.readFileSync(file, 'utf8');
183
+ return { path: relPath, schema: artifact.schema, exists: true, ok: content.trim().length > 0, failure: content.trim().length ? undefined : 'empty' };
184
+ }
185
+ let parsed;
186
+ try {
187
+ parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
188
+ }
189
+ catch {
190
+ return { path: relPath, schema: artifact.schema, exists: true, ok: false, failure: 'json_parse' };
191
+ }
192
+ if (!artifact.schema)
193
+ return { path: relPath, schema: null, exists: true, ok: true };
194
+ const schemaOk = parsed.schema === artifact.schema || parsed.schema_version != null;
195
+ return { path: relPath, schema: artifact.schema, exists: true, ok: schemaOk, failure: schemaOk ? undefined : 'schema_mismatch', actual_schema: parsed.schema || null };
196
+ }
197
+ function normalizeExpectedArtifacts(items = []) {
198
+ return (items || []).map((artifact) => {
199
+ if (typeof artifact === 'string')
200
+ return { path: artifact, schema: inferSchema(artifact) };
201
+ return { path: artifact.path, schema: artifact.schema || inferSchema(artifact.path), optional: Boolean(artifact.optional) };
202
+ });
203
+ }
204
+ function inferSchema(file = '') {
205
+ if (file.includes('completion-proof'))
206
+ return 'sks.completion-proof.v1';
207
+ if (file.includes('image-voxel-ledger'))
208
+ return 'sks.image-voxel-ledger.v1';
209
+ if (file.includes('visual-anchors'))
210
+ return 'sks.visual-anchors.v1';
211
+ return null;
212
+ }
213
+ function resolveSksEntrypoint(root) {
214
+ const candidates = [
215
+ path.join(root, 'dist', 'bin', 'sks.js'),
216
+ path.resolve('dist', 'bin', 'sks.js')
217
+ ];
218
+ const found = candidates.find((candidate) => fs.existsSync(candidate));
219
+ return found ?? candidates[0] ?? path.resolve('dist', 'bin', 'sks.js');
220
+ }
221
+ /**
222
+ * Minimal, defensive command-line tokenizer: splits on whitespace while respecting
223
+ * single and double quotes. Never passed to a shell — used purely to build a spawnSync
224
+ * argv array, so shell metacharacters inside tokens are inert.
225
+ */
226
+ export function tokenizeCommand(command) {
227
+ const tokens = [];
228
+ let current = '';
229
+ let quote = null;
230
+ let hasToken = false;
231
+ for (let i = 0; i < command.length; i += 1) {
232
+ const ch = command[i] ?? '';
233
+ if (quote) {
234
+ if (ch === quote) {
235
+ quote = null;
236
+ }
237
+ else {
238
+ current += ch;
239
+ }
240
+ continue;
241
+ }
242
+ if (ch === '"' || ch === "'") {
243
+ quote = ch;
244
+ hasToken = true;
245
+ continue;
246
+ }
247
+ if (/\s/.test(ch)) {
248
+ if (hasToken) {
249
+ tokens.push(current);
250
+ current = '';
251
+ hasToken = false;
252
+ }
253
+ continue;
254
+ }
255
+ current += ch;
256
+ hasToken = true;
257
+ }
258
+ if (hasToken)
259
+ tokens.push(current);
260
+ return tokens;
261
+ }
262
+ //# sourceMappingURL=feature-fixture-executor.js.map