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.
- package/README.md +5 -1
- 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/commands/doctor.js +32 -3
- package/dist/config/skills-manifest.json +58 -58
- package/dist/core/agents/agent-lifecycle.js +5 -3
- package/dist/core/agents/agent-orchestrator.js +14 -2
- package/dist/core/agents/agent-proof-evidence.js +16 -0
- package/dist/core/agents/native-cli-worker.js +0 -1
- package/dist/core/bench.js +24 -2
- package/dist/core/codex-app/sks-menubar.js +526 -133
- package/dist/core/commands/agent-command.js +35 -1
- package/dist/core/commands/basic-cli.js +61 -4
- 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/menubar-command.js +2 -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/doctor/doctor-dirty-planner.js +51 -7
- 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/naruto/naruto-real-worker-child.js +1 -1
- 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/update/update-migration-state.js +23 -3
- package/dist/core/version.js +1 -1
- package/dist/core/work-order-ledger.js +60 -0
- package/dist/core/zellij/zellij-slot-telemetry.js +1 -1
- package/dist/scripts/doctor-context7-mcp-repair-blackbox.js +16 -0
- package/dist/scripts/doctor-fix-production-blackbox.js +26 -0
- package/dist/scripts/doctor-startup-config-repair-blackbox.js +13 -0
- package/dist/scripts/doctor-supabase-mcp-repair-blackbox.js +14 -0
- package/dist/scripts/gate-policy-audit-check.js +2 -2
- package/dist/scripts/naruto-real-active-pool-runtime-check.js +26 -1
- package/dist/scripts/packlist-performance-check.js +7 -1
- package/dist/scripts/project-skill-dedupe-blackbox.js +36 -0
- 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-gate-script-parity-check.js +44 -16
- package/dist/scripts/release-metadata-1-19-check.js +2 -2
- package/dist/scripts/sks-menubar-install-check.js +57 -20
- package/dist/scripts/supabase-secret-preservation-blackbox.js +29 -0
- package/dist/scripts/update-preserves-supabase-keys-blackbox.js +27 -0
- package/dist/scripts/zellij-slot-telemetry-runtime-check.js +1 -0
- package/package.json +12 -65
- package/schemas/release/release-gate-node.schema.json +1 -0
- package/bench/tasks/t01-off-by-one/repo/package.json +0 -9
- package/bench/tasks/t01-off-by-one/repo/src/pagination.js +0 -5
- package/bench/tasks/t01-off-by-one/repo/test.js +0 -5
- package/bench/tasks/t01-off-by-one/task.json +0 -13
- package/bench/tasks/t02-signature-cochange/repo/package.json +0 -9
- package/bench/tasks/t02-signature-cochange/repo/src/admin.js +0 -5
- package/bench/tasks/t02-signature-cochange/repo/src/audit.js +0 -5
- package/bench/tasks/t02-signature-cochange/repo/src/labels.js +0 -3
- package/bench/tasks/t02-signature-cochange/repo/src/profile.js +0 -5
- package/bench/tasks/t02-signature-cochange/repo/test.js +0 -9
- package/bench/tasks/t02-signature-cochange/task.json +0 -12
- package/bench/tasks/t03-type-puzzle/repo/package.json +0 -9
- package/bench/tasks/t03-type-puzzle/repo/src/config.js +0 -4
- package/bench/tasks/t03-type-puzzle/repo/test.js +0 -6
- package/bench/tasks/t03-type-puzzle/task.json +0 -13
- package/bench/tasks/t04-refactor-preserve/repo/package.json +0 -9
- package/bench/tasks/t04-refactor-preserve/repo/src/cart.js +0 -8
- package/bench/tasks/t04-refactor-preserve/repo/test.js +0 -4
- package/bench/tasks/t04-refactor-preserve/task.json +0 -12
- package/bench/tasks/t05-performance/repo/package.json +0 -9
- package/bench/tasks/t05-performance/repo/src/pairs.js +0 -9
- package/bench/tasks/t05-performance/repo/test.js +0 -11
- package/bench/tasks/t05-performance/task.json +0 -12
- package/bench/tasks/t06-mistake-rule/repo/package.json +0 -9
- package/bench/tasks/t06-mistake-rule/repo/src/loader.js +0 -6
- package/bench/tasks/t06-mistake-rule/repo/test.js +0 -7
- package/bench/tasks/t06-mistake-rule/task.json +0 -12
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]);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { MANAGED_ASSET_VERSION } from '../managed-assets/managed-assets-manifest.js';
|
|
4
5
|
import { PACKAGE_VERSION, packageRoot, sha256 } from '../fsx.js';
|
|
5
6
|
import { hashJson } from '../triwiki/triwiki-cache-key.js';
|
|
7
|
+
import { SKS_MENUBAR_LABEL, sksMenuBarPaths } from '../codex-app/sks-menubar.js';
|
|
6
8
|
export const DOCTOR_DIRTY_PLAN_SCHEMA = 'sks.doctor-dirty-plan.v2';
|
|
7
9
|
export function planDoctorDirtyRepair(root, phaseIds) {
|
|
8
10
|
const phases = phaseIds.map((id) => {
|
|
@@ -10,25 +12,30 @@ export function planDoctorDirtyRepair(root, phaseIds) {
|
|
|
10
12
|
const inputHash = phaseInputHash(root, id);
|
|
11
13
|
const postcheckRequired = phaseRequiresPostcheck(id);
|
|
12
14
|
const markerState = readMarker(marker);
|
|
15
|
+
const runtimeProbeFailed = runtimeProbeFailures(root, id);
|
|
13
16
|
if (!markerState)
|
|
14
|
-
return { id, status: 'dirty', reason: 'no_clean_marker', input_hash: inputHash, last_clean_proof_id: null, postcheck_required: postcheckRequired, postcheck_pending: false };
|
|
17
|
+
return { id, status: 'dirty', reason: 'no_clean_marker', input_hash: inputHash, last_clean_proof_id: null, postcheck_required: postcheckRequired, postcheck_pending: false, runtime_probe_failed: runtimeProbeFailed };
|
|
15
18
|
if (markerState.input_hash !== inputHash) {
|
|
16
|
-
return { id, status: 'dirty', reason: 'input_hash_changed', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false };
|
|
19
|
+
return { id, status: 'dirty', reason: 'input_hash_changed', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false, runtime_probe_failed: runtimeProbeFailed };
|
|
17
20
|
}
|
|
18
21
|
if (markerState.proof_id && !proofExists(root, markerState.proof_id)) {
|
|
19
|
-
return { id, status: 'dirty', reason: 'clean_proof_missing', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false };
|
|
22
|
+
return { id, status: 'dirty', reason: 'clean_proof_missing', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false, runtime_probe_failed: runtimeProbeFailed };
|
|
20
23
|
}
|
|
21
24
|
if (!markerState.repair_applied) {
|
|
22
|
-
return { id, status: 'dirty', reason: 'repair_not_applied', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false };
|
|
25
|
+
return { id, status: 'dirty', reason: 'repair_not_applied', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false, runtime_probe_failed: runtimeProbeFailed };
|
|
26
|
+
}
|
|
27
|
+
if (runtimeProbeFailed.length) {
|
|
28
|
+
return { id, status: 'dirty', reason: `runtime_probe_failed:${runtimeProbeFailed.join(',')}`, input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false, runtime_probe_failed: runtimeProbeFailed };
|
|
23
29
|
}
|
|
24
30
|
return {
|
|
25
31
|
id,
|
|
26
|
-
status: 'clean',
|
|
32
|
+
status: markerState.postcheck_pending ? 'dirty' : 'clean',
|
|
27
33
|
reason: postcheckRequired && markerState.postcheck_pending ? 'matching_repair_marker_postcheck_pending' : 'matching_clean_proof',
|
|
28
34
|
input_hash: inputHash,
|
|
29
35
|
last_clean_proof_id: markerState.proof_id,
|
|
30
36
|
postcheck_required: postcheckRequired,
|
|
31
|
-
postcheck_pending: postcheckRequired && markerState.postcheck_pending
|
|
37
|
+
postcheck_pending: postcheckRequired && markerState.postcheck_pending,
|
|
38
|
+
runtime_probe_failed: []
|
|
32
39
|
};
|
|
33
40
|
});
|
|
34
41
|
const plan = {
|
|
@@ -37,6 +44,7 @@ export function planDoctorDirtyRepair(root, phaseIds) {
|
|
|
37
44
|
phases,
|
|
38
45
|
dirty_count: phases.filter((phase) => phase.status === 'dirty').length,
|
|
39
46
|
clean_count: phases.filter((phase) => phase.status === 'clean').length,
|
|
47
|
+
runtime_probe_failed: phases.flatMap((phase) => phase.runtime_probe_failed.map((failure) => `${phase.id}:${failure}`)),
|
|
40
48
|
semantic_dirty_plan_path: dirtyPlanPath(root)
|
|
41
49
|
};
|
|
42
50
|
writeDirtyPlan(root, plan);
|
|
@@ -236,7 +244,43 @@ function proofExists(root, proofId) {
|
|
|
236
244
|
}
|
|
237
245
|
}
|
|
238
246
|
function phaseRequiresPostcheck(id) {
|
|
239
|
-
return /zellij|context7|startup|supabase|native|secret/i.test(id);
|
|
247
|
+
return /zellij|context7|startup|supabase|native|secret|menubar/i.test(id);
|
|
248
|
+
}
|
|
249
|
+
function runtimeProbeFailures(root, id) {
|
|
250
|
+
if (!id.includes('menubar') || process.platform !== 'darwin')
|
|
251
|
+
return [];
|
|
252
|
+
const failures = [];
|
|
253
|
+
const paths = sksMenuBarPaths(process.env.HOME, root);
|
|
254
|
+
if (!fs.existsSync(paths.executable_path))
|
|
255
|
+
failures.push('menubar_app_missing');
|
|
256
|
+
if (!fs.existsSync(paths.action_script_path))
|
|
257
|
+
failures.push('action_script_missing');
|
|
258
|
+
else {
|
|
259
|
+
try {
|
|
260
|
+
fs.accessSync(paths.action_script_path, fs.constants.X_OK);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
failures.push('action_script_not_executable');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const launchctl = process.env.SKS_MENUBAR_LAUNCHCTL || '/bin/launchctl';
|
|
267
|
+
if (!fs.existsSync(launchctl)) {
|
|
268
|
+
failures.push('launchctl_missing');
|
|
269
|
+
return failures;
|
|
270
|
+
}
|
|
271
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
272
|
+
const service = `${uid === null ? 'gui' : `gui/${uid}`}/${SKS_MENUBAR_LABEL}`;
|
|
273
|
+
const result = spawnSync(launchctl, ['print', service], {
|
|
274
|
+
encoding: 'utf8',
|
|
275
|
+
timeout: 500,
|
|
276
|
+
maxBuffer: 32 * 1024
|
|
277
|
+
});
|
|
278
|
+
const text = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
279
|
+
if (result.error)
|
|
280
|
+
failures.push(`launchctl_probe_error:${result.error.message}`);
|
|
281
|
+
else if (result.status !== 0 || !(/\bstate = running\b|\bpid = \d+\b/.test(text)))
|
|
282
|
+
failures.push('launchd_not_running');
|
|
283
|
+
return failures;
|
|
240
284
|
}
|
|
241
285
|
function writeDirtyPlan(root, plan) {
|
|
242
286
|
const file = dirtyPlanPath(root);
|
|
@@ -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
|