sneakoscope 5.3.0 → 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.
- package/README.md +4 -0
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/config/skills-manifest.json +58 -58
- package/dist/core/agents/agent-orchestrator.js +14 -2
- package/dist/core/agents/agent-proof-evidence.js +16 -0
- package/dist/core/bench.js +24 -2
- package/dist/core/codex-app/sks-menubar.js +68 -7
- package/dist/core/commands/agent-command.js +35 -1
- package/dist/core/commands/basic-cli.js +59 -0
- package/dist/core/commands/command-utils.js +14 -4
- package/dist/core/commands/goal-command.js +3 -0
- package/dist/core/commands/image-ux-review-command.js +4 -1
- package/dist/core/commands/mad-sks-command.js +10 -1
- package/dist/core/commands/naruto-command.js +12 -7
- package/dist/core/commands/ppt-command.js +40 -1
- package/dist/core/commands/qa-loop-command.js +4 -1
- package/dist/core/commands/release-command.js +3 -3
- package/dist/core/commands/research-command.js +4 -0
- package/dist/core/commands/route-success-helpers.js +2 -0
- package/dist/core/commands/run-command.js +22 -5
- package/dist/core/commands/seo-command.js +31 -4
- package/dist/core/db-safety.js +8 -1
- package/dist/core/feature-fixture-executor.js +262 -0
- package/dist/core/feature-fixtures.js +95 -52
- package/dist/core/fsx.js +1 -1
- package/dist/core/loops/loop-decomposer.js +4 -3
- package/dist/core/loops/loop-planner.js +7 -3
- package/dist/core/loops/loop-worker-runtime.js +6 -0
- package/dist/core/pipeline-internals/runtime-gates.js +39 -3
- package/dist/core/proof/selftest-proof-fixtures.js +8 -0
- package/dist/core/providers/glm/naruto/glm-naruto-requirement-coverage.js +18 -1
- package/dist/core/providers/glm/naruto/glm-naruto-requirement-ledger.js +28 -12
- package/dist/core/questions.js +12 -10
- package/dist/core/release/release-gate-node.js +2 -0
- package/dist/core/routes.js +11 -0
- package/dist/core/stop-gate/stop-gate-check.js +4 -0
- package/dist/core/stop-gate/stop-gate-writer.js +4 -0
- package/dist/core/trust-kernel/trust-report.js +10 -1
- package/dist/core/version.js +1 -1
- package/dist/core/work-order-ledger.js +60 -0
- package/dist/scripts/gate-policy-audit-check.js +2 -2
- package/dist/scripts/packlist-performance-check.js +7 -1
- package/dist/scripts/release-dag-full-coverage-check.js +1 -1
- package/dist/scripts/release-gate-existence-audit.js +1 -1
- package/dist/scripts/release-gate-planner.js +1 -1
- package/dist/scripts/release-metadata-1-19-check.js +1 -1
- package/package.json +5 -1
- package/schemas/release/release-gate-node.schema.json +1 -0
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
88
|
-
status:
|
|
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}`);
|
package/dist/core/db-safety.js
CHANGED
|
@@ -645,7 +645,14 @@ async function scanSupabaseMcpConfigs(root) {
|
|
|
645
645
|
}
|
|
646
646
|
function extractSupabaseMcpUrls(text) {
|
|
647
647
|
const out = new Set();
|
|
648
|
-
|
|
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
|