sneakoscope 3.1.0 → 3.1.2
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 +1 -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/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/cli/install-helpers.js +6 -7
- package/dist/commands/zellij-slot-column-anchor.js +3 -1
- package/dist/commands/zellij-slot-pane.js +19 -2
- package/dist/core/agents/agent-janitor.js +10 -1
- package/dist/core/agents/agent-orchestrator.js +8 -2
- package/dist/core/agents/agent-proof-evidence.js +20 -0
- package/dist/core/agents/agent-runner-ollama.js +11 -4
- package/dist/core/agents/fast-mode-policy.js +7 -5
- package/dist/core/agents/intelligent-work-graph.js +93 -14
- package/dist/core/agents/native-cli-session-swarm.js +115 -9
- package/dist/core/agents/no-subagent-scaling-policy.js +10 -1
- package/dist/core/agents/official-subagent-helper-policy.js +62 -0
- package/dist/core/codex-app.js +0 -2
- package/dist/core/codex-control/codex-task-runner.js +9 -0
- package/dist/core/commands/fast-mode-command.js +1 -1
- package/dist/core/commands/loop-command.js +86 -13
- package/dist/core/commands/naruto-command.js +34 -21
- package/dist/core/commands/team-command.js +1 -0
- package/dist/core/commands/wiki-command.js +35 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +1 -2
- package/dist/core/locks/file-lock.js +88 -0
- package/dist/core/loops/loop-artifacts.js +54 -2
- package/dist/core/loops/loop-checkpoint.js +22 -0
- package/dist/core/loops/loop-concurrency-budget.js +55 -0
- package/dist/core/loops/loop-final-arbiter-contract.js +28 -0
- package/dist/core/loops/loop-finalizer.js +55 -7
- package/dist/core/loops/loop-fixture-policy.js +58 -0
- package/dist/core/loops/loop-gate-registry.js +96 -0
- package/dist/core/loops/loop-gate-runner.js +206 -17
- package/dist/core/loops/loop-gpt-final-arbiter.js +81 -0
- package/dist/core/loops/loop-integration-merge.js +80 -0
- package/dist/core/loops/loop-interrupt-registry.js +118 -0
- package/dist/core/loops/loop-lease.js +35 -20
- package/dist/core/loops/loop-merge-strategy.js +105 -0
- package/dist/core/loops/loop-mutation-ledger.js +103 -0
- package/dist/core/loops/loop-planner.js +36 -5
- package/dist/core/loops/loop-runtime-control.js +27 -0
- package/dist/core/loops/loop-runtime.js +254 -96
- package/dist/core/loops/loop-scheduler.js +14 -5
- package/dist/core/loops/loop-side-effect-scanner.js +91 -0
- package/dist/core/loops/loop-worker-prompts.js +43 -0
- package/dist/core/loops/loop-worker-runtime.js +281 -0
- package/dist/core/loops/loop-worktree-runtime.js +92 -0
- package/dist/core/naruto/naruto-finalizer.js +7 -2
- package/dist/core/naruto/naruto-loop-mesh.js +10 -1
- package/dist/core/proof/auto-finalize.js +3 -2
- package/dist/core/proof/proof-schema.js +6 -0
- package/dist/core/proof/proof-writer.js +5 -2
- package/dist/core/proof/root-cause-policy.js +70 -0
- package/dist/core/proof/route-adapter.js +18 -1
- package/dist/core/proof/route-finalizer.js +71 -6
- package/dist/core/proof/route-proof-gate.js +4 -0
- package/dist/core/release/release-gate-batch-runner.js +56 -10
- package/dist/core/release/release-gate-cache-v2.js +18 -3
- package/dist/core/release/release-gate-dag.js +121 -18
- package/dist/core/release/release-gate-node.js +2 -1
- package/dist/core/release/release-gate-resource-governor.js +27 -6
- package/dist/core/skills/core-skill-meta-update.js +24 -0
- package/dist/core/skills/core-skill-reflection.js +94 -0
- package/dist/core/skills/core-skill-trainer.js +103 -0
- package/dist/core/trust-kernel/completion-contract.js +4 -0
- package/dist/core/trust-kernel/route-contract.js +4 -1
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-right-column-manager.js +13 -2
- package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
- package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
- package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
- package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
- package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +14 -2
- package/dist/scripts/loop-directive-check-lib.js +225 -2
- package/dist/scripts/loop-hardening-check-lib.js +289 -0
- package/dist/scripts/loop-worker-fixture-child.js +53 -0
- package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
- package/dist/scripts/prepublish-release-check-or-fast.js +38 -10
- package/dist/scripts/release-check-stamp.js +29 -4
- package/dist/scripts/release-gate-existence-audit.js +1 -0
- package/package.json +32 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
|
-
import { appendJsonlBounded, ensureDir, nowIso, readJson, readText } from '../fsx.js';
|
|
3
|
+
import { appendJsonlBounded, ensureDir, nowIso, readJson, readText, writeTextAtomic } from '../fsx.js';
|
|
4
|
+
import { withFileLock } from '../locks/file-lock.js';
|
|
4
5
|
export const ZELLIJ_SLOT_TELEMETRY_EVENT_SCHEMA = 'sks.zellij-slot-telemetry-event.v1';
|
|
5
6
|
export const ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA = 'sks.zellij-slot-telemetry-snapshot.v1';
|
|
6
7
|
const telemetrySnapshotCache = new Map();
|
|
@@ -71,7 +72,7 @@ export function mergeTelemetrySnapshots(base, overlay, opts = {}) {
|
|
|
71
72
|
const slots = { ...(base.slots || {}) };
|
|
72
73
|
for (const [key, row] of Object.entries(overlay.slots || {})) {
|
|
73
74
|
const existing = slots[key];
|
|
74
|
-
slots[key] = !existing
|
|
75
|
+
slots[key] = !existing ? row : newerSlotTelemetry(existing, row);
|
|
75
76
|
}
|
|
76
77
|
const baseTs = Date.parse(String(base.updated_at || '')) || 0;
|
|
77
78
|
const overlayTs = Date.parse(String(overlay.updated_at || '')) || 0;
|
|
@@ -88,6 +89,21 @@ function telemetryTsMs(value) {
|
|
|
88
89
|
const parsed = Date.parse(String(value || ''));
|
|
89
90
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
90
91
|
}
|
|
92
|
+
function newerSlotTelemetry(current, incoming) {
|
|
93
|
+
const currentTs = telemetryTsMs(current.latest_ts);
|
|
94
|
+
const incomingTs = telemetryTsMs(incoming.latest_ts);
|
|
95
|
+
const currentTerminal = isTelemetryTerminalStatus(current.status);
|
|
96
|
+
const incomingTerminal = isTelemetryTerminalStatus(incoming.status);
|
|
97
|
+
if (currentTerminal && !incomingTerminal)
|
|
98
|
+
return current;
|
|
99
|
+
if (!currentTerminal && incomingTerminal)
|
|
100
|
+
return incoming;
|
|
101
|
+
if (incomingTs > currentTs)
|
|
102
|
+
return incoming;
|
|
103
|
+
if (incomingTs < currentTs)
|
|
104
|
+
return current;
|
|
105
|
+
return incoming;
|
|
106
|
+
}
|
|
91
107
|
async function statTelemetryFile(file) {
|
|
92
108
|
try {
|
|
93
109
|
const st = await fsp.stat(file);
|
|
@@ -99,17 +115,17 @@ async function statTelemetryFile(file) {
|
|
|
99
115
|
}
|
|
100
116
|
export function applyTelemetryEventToSnapshot(snapshot, event) {
|
|
101
117
|
const key = slotTelemetryKey(event.slot_id || event.worker_id, event.generation_index);
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
const previous = snapshot.slots?.[key];
|
|
119
|
+
const nextSlot = mergeSlotTelemetry(previous, event);
|
|
120
|
+
const slots = snapshot.slots || {};
|
|
121
|
+
slots[key] = nextSlot;
|
|
106
122
|
return {
|
|
107
123
|
schema: ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA,
|
|
108
124
|
mission_id: event.mission_id || snapshot.mission_id,
|
|
109
125
|
updated_at: nowIso(),
|
|
110
126
|
flush_count: snapshot.flush_count || 0,
|
|
111
127
|
slots,
|
|
112
|
-
counts: countSlotTelemetry(slots)
|
|
128
|
+
counts: adjustSlotTelemetryCounts(snapshot.counts || countSlotTelemetry(snapshot.slots || {}), previous, nextSlot)
|
|
113
129
|
};
|
|
114
130
|
}
|
|
115
131
|
export async function rebuildZellijSlotTelemetrySnapshot(root, missionId) {
|
|
@@ -176,11 +192,19 @@ function normalizeTelemetryEvent(event) {
|
|
|
176
192
|
};
|
|
177
193
|
}
|
|
178
194
|
function mergeSlotTelemetry(previous, event) {
|
|
195
|
+
const previousTs = telemetryTsMs(previous?.latest_ts);
|
|
196
|
+
const eventTs = telemetryTsMs(event.ts);
|
|
197
|
+
const previousTerminal = isTelemetryTerminalStatus(previous?.status);
|
|
198
|
+
const eventTerminal = isTelemetryTerminalStatus(event.status);
|
|
199
|
+
const terminalRegression = Boolean(previous && previousTerminal && !eventTerminal);
|
|
200
|
+
const stale = Boolean(previous && (terminalRegression
|
|
201
|
+
|| (!eventTerminal && eventTs < previousTs)
|
|
202
|
+
|| (previousTerminal && eventTerminal && eventTs < previousTs)));
|
|
179
203
|
return {
|
|
180
|
-
slot_id: event.slot_id,
|
|
181
|
-
generation_index: event.generation_index,
|
|
182
|
-
worker_id: event.worker_id,
|
|
183
|
-
status: event.status,
|
|
204
|
+
slot_id: stale ? previous.slot_id : event.slot_id,
|
|
205
|
+
generation_index: stale ? previous.generation_index : event.generation_index,
|
|
206
|
+
worker_id: stale ? previous.worker_id : event.worker_id,
|
|
207
|
+
status: stale ? previous.status : event.status,
|
|
184
208
|
role: event.role || previous?.role || 'worker',
|
|
185
209
|
backend: event.backend || previous?.backend || 'unknown',
|
|
186
210
|
provider: event.provider || previous?.provider || 'unknown',
|
|
@@ -188,34 +212,55 @@ function mergeSlotTelemetry(previous, event) {
|
|
|
188
212
|
worktree_id: event.worktree_id ?? previous?.worktree_id ?? null,
|
|
189
213
|
worktree_path: event.worktree_path ?? previous?.worktree_path ?? null,
|
|
190
214
|
task_title: event.task_title || previous?.task_title || 'waiting for task',
|
|
191
|
-
current_file: event.current_file ?? previous?.current_file ?? null,
|
|
192
|
-
latest_event_type: event.event_type,
|
|
193
|
-
latest_ts: event.ts,
|
|
194
|
-
progress: event.progress || previous?.progress || null,
|
|
215
|
+
current_file: stale ? previous.current_file ?? (terminalRegression ? null : event.current_file ?? null) : event.current_file ?? previous?.current_file ?? null,
|
|
216
|
+
latest_event_type: stale ? previous.latest_event_type : event.event_type,
|
|
217
|
+
latest_ts: stale ? previous.latest_ts : event.ts,
|
|
218
|
+
progress: stale ? previous.progress || (terminalRegression ? null : event.progress || null) : event.progress || previous?.progress || null,
|
|
195
219
|
artifact_paths: unique([...(previous?.artifact_paths || []), ...(event.artifact_paths || [])]),
|
|
196
220
|
blockers: unique([...(previous?.blockers || []), ...(event.blockers || [])]),
|
|
197
|
-
log_tail: event.log_tail || previous?.log_tail || ''
|
|
221
|
+
log_tail: stale ? previous.log_tail || (terminalRegression ? '' : event.log_tail || '') : event.log_tail || previous?.log_tail || ''
|
|
198
222
|
};
|
|
199
223
|
}
|
|
200
224
|
function countSlotTelemetry(slots) {
|
|
201
225
|
const counts = { queued: 0, running: 0, verifying: 0, completed: 0, failed: 0, headless: 0 };
|
|
202
226
|
for (const row of Object.values(slots)) {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
counts
|
|
227
|
+
const bucket = slotTelemetryCountBucket(row.status);
|
|
228
|
+
counts[bucket] += 1;
|
|
229
|
+
}
|
|
230
|
+
return counts;
|
|
231
|
+
}
|
|
232
|
+
function adjustSlotTelemetryCounts(current, previous, next) {
|
|
233
|
+
const counts = { ...current };
|
|
234
|
+
const nextBucket = slotTelemetryCountBucket(next.status);
|
|
235
|
+
if (previous) {
|
|
236
|
+
const previousBucket = slotTelemetryCountBucket(previous.status);
|
|
237
|
+
if (previousBucket !== nextBucket) {
|
|
238
|
+
counts[previousBucket] = Math.max(0, Number(counts[previousBucket] || 0) - 1);
|
|
239
|
+
counts[nextBucket] = Number(counts[nextBucket] || 0) + 1;
|
|
240
|
+
}
|
|
241
|
+
return counts;
|
|
216
242
|
}
|
|
243
|
+
counts[nextBucket] = Number(counts[nextBucket] || 0) + 1;
|
|
217
244
|
return counts;
|
|
218
245
|
}
|
|
246
|
+
function slotTelemetryCountBucket(status) {
|
|
247
|
+
const normalized = normalizeStatus(status);
|
|
248
|
+
if (normalized === 'queued' || normalized === 'launching')
|
|
249
|
+
return 'queued';
|
|
250
|
+
if (normalized === 'verifying')
|
|
251
|
+
return 'verifying';
|
|
252
|
+
if (normalized === 'completed' || normalized === 'drained')
|
|
253
|
+
return 'completed';
|
|
254
|
+
if (normalized === 'failed')
|
|
255
|
+
return 'failed';
|
|
256
|
+
if (normalized === 'headless')
|
|
257
|
+
return 'headless';
|
|
258
|
+
return 'running';
|
|
259
|
+
}
|
|
260
|
+
function isTelemetryTerminalStatus(status) {
|
|
261
|
+
const normalized = normalizeStatus(status);
|
|
262
|
+
return normalized === 'completed' || normalized === 'failed' || normalized === 'drained';
|
|
263
|
+
}
|
|
219
264
|
function normalizeStatus(value) {
|
|
220
265
|
const text = String(value || '').toLowerCase();
|
|
221
266
|
if (text === 'closed' || text === 'done' || text === 'passed')
|
|
@@ -258,20 +303,26 @@ function tail(value, max) {
|
|
|
258
303
|
}
|
|
259
304
|
async function writeTelemetrySnapshotFast(file, snapshot) {
|
|
260
305
|
await ensureDir(path.dirname(file));
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
306
|
+
await withFileLock({
|
|
307
|
+
lockPath: `${file}.lock`,
|
|
308
|
+
timeoutMs: 30000,
|
|
309
|
+
staleMs: 2 * 60 * 1000
|
|
310
|
+
}, async () => {
|
|
311
|
+
const flushCount = Number(telemetrySnapshotFlushCounts.get(file) || 0) + 1;
|
|
312
|
+
telemetrySnapshotFlushCounts.set(file, flushCount);
|
|
313
|
+
telemetrySnapshotLastFlushMs.set(file, Date.now());
|
|
314
|
+
// Multiple processes (orchestrator + worker children) flush this file.
|
|
315
|
+
// Serialize the read/merge/write critical section so a slower writer
|
|
316
|
+
// cannot overwrite a newer slot observed by a different process.
|
|
317
|
+
const disk = await readJson(file, null);
|
|
318
|
+
const merged = disk?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA ? mergeTelemetrySnapshots(disk, snapshot) : snapshot;
|
|
319
|
+
const next = { ...merged, flush_count: Math.max(flushCount, Number(merged.flush_count || 0)) };
|
|
320
|
+
telemetrySnapshotCache.set(file, next);
|
|
321
|
+
await writeTextAtomic(file, `${JSON.stringify(next)}\n`);
|
|
322
|
+
const stat = await statTelemetryFile(file);
|
|
323
|
+
if (stat)
|
|
324
|
+
telemetrySnapshotDiskStat.set(file, stat);
|
|
325
|
+
});
|
|
275
326
|
}
|
|
276
327
|
function shouldFlushTelemetrySnapshot(file, event) {
|
|
277
328
|
const next = (telemetrySnapshotWriteCounts.get(file) || 0) + 1;
|
|
@@ -289,7 +340,8 @@ function shouldFlushTelemetrySnapshot(file, event) {
|
|
|
289
340
|
|| event.event_type === 'worker_completed'
|
|
290
341
|
|| event.event_type === 'worker_failed'
|
|
291
342
|
|| event.status === 'completed'
|
|
292
|
-
|| event.status === 'failed'
|
|
343
|
+
|| event.status === 'failed'
|
|
344
|
+
|| event.status === 'drained';
|
|
293
345
|
const should = next === 1
|
|
294
346
|
|| important
|
|
295
347
|
|| now - last >= flushMs
|
|
@@ -14,12 +14,49 @@ export const ZELLIJ_PANE_CREATION_LOCK_METRICS_SCHEMA = 'sks.zellij-pane-creatio
|
|
|
14
14
|
export function buildWorkerPaneName(slotId, generationIndex) {
|
|
15
15
|
return `${slotId}/gen-${Math.max(1, Math.floor(Number(generationIndex) || 1))}`;
|
|
16
16
|
}
|
|
17
|
-
export function buildWorkerPaneTitle(slotId, generationIndex, context, serviceTier, backend, status, worktree) {
|
|
17
|
+
export function buildWorkerPaneTitle(slotId, generationIndex, context, serviceTier, backend, status, worktree, taskTitle) {
|
|
18
|
+
// When a task label is available, the pane title is JUST the label + status:
|
|
19
|
+
// users identify panes by what they are doing, not by slot ids. Slot/gen,
|
|
20
|
+
// backend, provider, and worktree details stay inside the pane body and the
|
|
21
|
+
// SLOTS anchor rows. The verbose format remains the fallback for callers
|
|
22
|
+
// that have no task context.
|
|
23
|
+
const task = shortWorkerTaskLabel(taskTitle);
|
|
24
|
+
if (task)
|
|
25
|
+
return `${task} · ${workerBackendTag(backend, context?.provider)} · ${status || 'launching'}`;
|
|
18
26
|
const base = buildWorkerPaneName(slotId, generationIndex);
|
|
19
27
|
const normalized = normalizePaneProviderContext(context, serviceTier);
|
|
20
28
|
const wt = worktree ? ` · WT:${worktree.id} · branch:${worktree.branch}` : '';
|
|
21
29
|
return `${base}${wt} · ${backend || 'codex-sdk'} · ${providerPaneLabel(normalized)} · ${status || 'launching'}`;
|
|
22
30
|
}
|
|
31
|
+
// Local-LLM workers must be visually distinguishable from GPT/codex workers:
|
|
32
|
+
// only trivial long-running edits belong on local models, while web research
|
|
33
|
+
// and code review must run on GPT. The tag makes a misrouted task obvious.
|
|
34
|
+
export function workerBackendTag(backend, provider) {
|
|
35
|
+
const text = `${String(backend || '')} ${String(provider || '')}`.toLowerCase();
|
|
36
|
+
if (/ollama|local/.test(text))
|
|
37
|
+
return 'LOCAL';
|
|
38
|
+
if (/fake|fixture/.test(text))
|
|
39
|
+
return 'fixture';
|
|
40
|
+
return 'GPT';
|
|
41
|
+
}
|
|
42
|
+
// Lead the pane title with a 1-3 word task label so users can tell WHAT each
|
|
43
|
+
// slot is doing at a glance instead of an anonymous "slot-002/gen-1".
|
|
44
|
+
export function shortWorkerTaskLabel(value, maxChars = 24) {
|
|
45
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
46
|
+
if (!text)
|
|
47
|
+
return '';
|
|
48
|
+
const words = text.split(' ');
|
|
49
|
+
let label = '';
|
|
50
|
+
for (const word of words.slice(0, 3)) {
|
|
51
|
+
const next = label ? `${label} ${word}` : word;
|
|
52
|
+
if (next.length > maxChars)
|
|
53
|
+
break;
|
|
54
|
+
label = next;
|
|
55
|
+
}
|
|
56
|
+
if (!label)
|
|
57
|
+
label = text.slice(0, maxChars);
|
|
58
|
+
return label;
|
|
59
|
+
}
|
|
23
60
|
export function isRealZellijWorkerPaneIdSource(value) {
|
|
24
61
|
return value === 'zellij_worker_new_pane_stdout' || value === 'zellij_worker_list_panes';
|
|
25
62
|
}
|
|
@@ -162,7 +199,7 @@ export async function openWorkerPane(input) {
|
|
|
162
199
|
});
|
|
163
200
|
return record;
|
|
164
201
|
}
|
|
165
|
-
const paneName = buildWorkerPaneTitle(input.slotId, input.generationIndex, providerContext, input.serviceTier, input.backend, input.statusLabel || 'running', input.worktree || null);
|
|
202
|
+
const paneName = buildWorkerPaneTitle(input.slotId, input.generationIndex, providerContext, input.serviceTier, input.backend, input.statusLabel || 'running', input.worktree || null, input.taskTitle || null);
|
|
166
203
|
// CRITICAL: serialize anchor + worker pane creation per session. Workers
|
|
167
204
|
// launch concurrently, and without this lock every worker raced past the
|
|
168
205
|
// "does the SLOTS anchor exist yet?" check and created its OWN anchor with
|
|
@@ -695,7 +732,8 @@ async function withZellijPaneCreationLock(input, fn) {
|
|
|
695
732
|
const current = new Promise((resolve) => {
|
|
696
733
|
release = resolve;
|
|
697
734
|
});
|
|
698
|
-
|
|
735
|
+
const chained = previous.then(() => current, () => current);
|
|
736
|
+
zellijPaneCreationLocks.set(key, chained);
|
|
699
737
|
await previous.catch(() => undefined);
|
|
700
738
|
const acquiredAt = nowIso();
|
|
701
739
|
const acquiredMs = Date.now();
|
|
@@ -741,7 +779,7 @@ async function withZellijPaneCreationLock(input, fn) {
|
|
|
741
779
|
meta: { wait_ms: metrics.wait_ms, held_ms: metrics.held_ms }
|
|
742
780
|
}).catch(() => undefined);
|
|
743
781
|
release();
|
|
744
|
-
if (zellijPaneCreationLocks.get(key) ===
|
|
782
|
+
if (zellijPaneCreationLocks.get(key) === chained)
|
|
745
783
|
zellijPaneCreationLocks.delete(key);
|
|
746
784
|
}
|
|
747
785
|
}
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { assertGate, root } from '../sks-1-18-gate-lib.js';
|
|
6
|
-
export function runNativeCliSwarmCheck({ agents, workItems = agents, reportName, backend = 'fake', extraArgs = [] }) {
|
|
6
|
+
export function runNativeCliSwarmCheck({ agents, workItems = agents, reportName, backend = 'fake', extraArgs = [], expectedFastMode = null }) {
|
|
7
7
|
const distCli = path.join(root, 'dist', 'bin', 'sks.js');
|
|
8
8
|
assertGate(fs.existsSync(distCli), 'dist CLI missing for native CLI swarm check', { distCli });
|
|
9
9
|
const args = [
|
|
@@ -37,7 +37,11 @@ export function runNativeCliSwarmCheck({ agents, workItems = agents, reportName,
|
|
|
37
37
|
const result = JSON.parse(stdout);
|
|
38
38
|
const proof = result.native_cli_session_proof || {};
|
|
39
39
|
const noSubagent = result.no_subagent_scaling_policy || {};
|
|
40
|
+
const officialHelper = result.official_subagent_helper_policy || {};
|
|
40
41
|
const fast = result.fast_mode_propagation || {};
|
|
42
|
+
const policy = fast.policy || result.fast_mode_policy || {};
|
|
43
|
+
const expectedFast = expectedFastMode === null || expectedFastMode === undefined ? Boolean(policy.fast_mode) : Boolean(expectedFastMode);
|
|
44
|
+
const expectedTier = expectedFast ? 'fast' : 'standard';
|
|
41
45
|
const report = {
|
|
42
46
|
schema: 'sks.native-cli-session-swarm-check.v1',
|
|
43
47
|
ok: result.ok === true,
|
|
@@ -47,6 +51,7 @@ export function runNativeCliSwarmCheck({ agents, workItems = agents, reportName,
|
|
|
47
51
|
backend: result.backend,
|
|
48
52
|
native_cli_session_proof: proof,
|
|
49
53
|
no_subagent_scaling_policy: noSubagent,
|
|
54
|
+
official_subagent_helper_policy: officialHelper,
|
|
50
55
|
fast_mode_propagation: fast,
|
|
51
56
|
proof_status: result.proof?.status || null
|
|
52
57
|
};
|
|
@@ -60,7 +65,14 @@ export function runNativeCliSwarmCheck({ agents, workItems = agents, reportName,
|
|
|
60
65
|
assertGate(Array.isArray(proof.process_ids) && proof.process_ids.length >= agents, 'process ids missing from native CLI proof', report);
|
|
61
66
|
assertGate(proof.close_report_count >= agents, 'worker close report count below requested agents', report);
|
|
62
67
|
assertGate(noSubagent.ok === true && noSubagent.subagent_events_counted_as_worker_sessions === false, 'no-subagent scaling policy must pass', report);
|
|
63
|
-
assertGate(
|
|
68
|
+
assertGate(officialHelper.ok === true && officialHelper.official_codex_subagent_helper_lane_enabled === true, 'official subagent helper policy must pass', report);
|
|
69
|
+
assertGate(officialHelper.worker_capacity_credit === 0 && officialHelper.subagent_events_counted_as_worker_sessions === false, 'official helper lane must not count toward worker capacity', report);
|
|
70
|
+
assertGate(noSubagent.official_codex_subagent_helper_lane_allowed === true && noSubagent.official_helper_lane_worker_capacity_credit === 0, 'no-subagent policy must allow helper lane with zero capacity credit', report);
|
|
71
|
+
assertGate(fast.ok === true, 'fast mode propagation proof must pass', report);
|
|
72
|
+
assertGate(fast.fast_mode === expectedFast && fast.service_tier === expectedTier, 'worker service tier must match the selected fast-mode policy', { ...report, expected_fast_mode: expectedFast, expected_service_tier: expectedTier });
|
|
73
|
+
if (expectedFast) {
|
|
74
|
+
assertGate(policy.explicit_fast === true || policy.preference_mode === 'fast' || policy.explicit_service_tier === 'fast', 'fast-mode propagation gate must use explicit fast opt-in', report);
|
|
75
|
+
}
|
|
64
76
|
assertGate((proof.worker_command_lines || []).every((line) => line.includes('--agent') && line.includes('worker')), 'worker command lines must use --agent worker', report);
|
|
65
77
|
return report;
|
|
66
78
|
}
|
|
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { COMMANDS } from '../cli/command-registry.js';
|
|
7
|
+
import { runProcess } from '../core/fsx.js';
|
|
7
8
|
import { compileGoalToLoopPlan } from '../core/loops/goal-to-loop-compat.js';
|
|
8
9
|
import { loopGraphProofPath, loopPlanPath, loopProofPath, loopRoot, loopStatePath } from '../core/loops/loop-artifacts.js';
|
|
9
10
|
import { decomposeRequestIntoLoopDomains } from '../core/loops/loop-decomposer.js';
|
|
@@ -29,7 +30,18 @@ export async function runLoopDirectiveCheck(id) {
|
|
|
29
30
|
const request = 'fix zellij telemetry, release cache, and codex probe docs';
|
|
30
31
|
const plan = await planLoopsFromRequest({ root, missionId, request, sourceCommand: 'loop' });
|
|
31
32
|
const byId = new Map(plan.graph.nodes.map((node) => [node.loop_id, node]));
|
|
32
|
-
const
|
|
33
|
+
const realRuntimeMode = process.env.SKS_LOOP_RUNTIME_REAL === '1'
|
|
34
|
+
|| id === 'loop:runtime-real-workers'
|
|
35
|
+
|| id === 'loop:maker-checker-real'
|
|
36
|
+
|| id === 'loop:integration-finalizer-real'
|
|
37
|
+
|| id === 'loop:real-maker-checker-blackbox'
|
|
38
|
+
|| id === 'naruto:loop-mesh-real-blackbox'
|
|
39
|
+
|| id === 'goal:loop-runtime-real-blackbox';
|
|
40
|
+
if (!realRuntimeMode && process.env.SKS_LOOP_RUNTIME_FIXTURE !== '1') {
|
|
41
|
+
process.env.SKS_LOOP_RUNTIME_FIXTURE = '1';
|
|
42
|
+
}
|
|
43
|
+
const fixtureMode = process.env.SKS_LOOP_RUNTIME_FIXTURE === '1' || process.env.SKS_LOOP_GATE_FIXTURE === '1';
|
|
44
|
+
const result = await runLoopPlan({ root, plan, parallelism: 'extreme', noMutation: fixtureMode ? true : !realRuntimeMode });
|
|
33
45
|
const assertions = [];
|
|
34
46
|
const assert = (condition, message) => assertions.push({ ok: Boolean(condition), message });
|
|
35
47
|
assert(validateLoopPlan(plan).ok, 'loop plan validates');
|
|
@@ -40,6 +52,9 @@ export async function runLoopDirectiveCheck(id) {
|
|
|
40
52
|
}
|
|
41
53
|
else if (id === 'loop:artifact-paths') {
|
|
42
54
|
assert(loopRoot(root, missionId).includes('.sneakoscope/missions'), 'artifact root layout matches directive');
|
|
55
|
+
assert(throws(() => loopRoot(root, '../../escape')), 'loop artifact root rejects mission traversal');
|
|
56
|
+
assert(throws(() => loopRoot(root, 'bad/mission')), 'loop artifact root rejects path separators in mission id');
|
|
57
|
+
assert(throws(() => loopStatePath(root, missionId, '../loop-escape')), 'loop node artifact path rejects loop traversal');
|
|
43
58
|
}
|
|
44
59
|
else if (id === 'loop:state') {
|
|
45
60
|
assert(await exists(loopStatePath(root, missionId, 'loop-zellij')), 'loop state exists');
|
|
@@ -47,6 +62,8 @@ export async function runLoopDirectiveCheck(id) {
|
|
|
47
62
|
else if (id === 'loop:planner') {
|
|
48
63
|
assert(byId.has('loop-integration'), 'integration loop always created');
|
|
49
64
|
assert(plan.graph.nodes.length >= 2, 'planner creates action plus integration loops');
|
|
65
|
+
assert(plan.graph.nodes.some((node) => node.route !== '$Integration' && node.maker.worker_count > 2), 'planner scales maker workers above the old hardcoded two');
|
|
66
|
+
assert(plan.graph.nodes.some((node) => node.route !== '$Integration' && node.checker.worker_count > 1), 'planner scales checker reviewers above the old hardcoded one');
|
|
50
67
|
}
|
|
51
68
|
else if (id === 'loop:decomposer') {
|
|
52
69
|
const domains = decomposeRequestIntoLoopDomains(request);
|
|
@@ -68,6 +85,118 @@ export async function runLoopDirectiveCheck(id) {
|
|
|
68
85
|
assert(result.ok, 'loop runtime produces ok graph result');
|
|
69
86
|
assert(await exists(loopGraphProofPath(root, missionId)), 'graph proof exists');
|
|
70
87
|
}
|
|
88
|
+
else if (id === 'loop:fixture-safety') {
|
|
89
|
+
const runtimeSource = await fs.readFile(path.join(process.cwd(), 'src/core/loops/loop-runtime.ts'), 'utf8');
|
|
90
|
+
const workerSource = await fs.readFile(path.join(process.cwd(), 'src/core/loops/loop-worker-runtime.ts'), 'utf8');
|
|
91
|
+
assert(!/noMutation\s*\?\s*\{\s*fixture:\s*true\s*\}/.test(runtimeSource), 'noMutation must not force fixture mode');
|
|
92
|
+
assert(workerSource.includes('decideLoopFixturePolicy'), 'fixture runtime has an explicit shared test-context policy guard');
|
|
93
|
+
assert(workerSource.includes('loop_fixture_runtime_forbidden'), 'fixture runtime fails closed outside test context');
|
|
94
|
+
assert(workerSource.includes("process.env.SKS_LOOP_RUNTIME_FIXTURE === '1'"), 'fixture runtime remains opt-in through SKS_LOOP_RUNTIME_FIXTURE');
|
|
95
|
+
assert(!workerSource.includes('visualLaneCount: Math.min(4'), 'zellij visual lane count must use the configurable pane cap');
|
|
96
|
+
const negative = await productionFixtureNegativeCheck();
|
|
97
|
+
assert(negative.code === 0 && negative.stdout.includes('loop_fixture_runtime_forbidden'), 'production fixture request is blocked at runtime');
|
|
98
|
+
}
|
|
99
|
+
else if (id === 'loop:worker-runtime') {
|
|
100
|
+
const proof = await readJson(loopProofPath(root, missionId, 'loop-zellij'));
|
|
101
|
+
assert(proof.maker_result.backend === 'deterministic-fixture' || proof.maker_result.backend === 'native-agent-orchestrator', 'maker backend recorded');
|
|
102
|
+
assert(proof.checker_result.backend === 'deterministic-fixture' || proof.checker_result.backend === 'native-agent-orchestrator', 'checker backend recorded');
|
|
103
|
+
assert(proof.maker_result.runtime_proof_path, 'maker runtime proof path recorded');
|
|
104
|
+
assert(proof.checker_result.runtime_proof_path, 'checker runtime proof path recorded');
|
|
105
|
+
}
|
|
106
|
+
else if (id === 'loop:worker-prompts') {
|
|
107
|
+
const prompts = await import('../core/loops/loop-worker-prompts.js');
|
|
108
|
+
const node = byId.get('loop-zellij');
|
|
109
|
+
assert(prompts.buildLoopMakerPrompt({ plan, node }).includes('Do not mutate outside the owner scope'), 'maker prompt constrains owner scope');
|
|
110
|
+
assert(prompts.buildLoopCheckerPrompt({ plan, node, makerArtifacts: ['maker.json'] }).includes('must not mutate source files'), 'checker prompt forbids mutation');
|
|
111
|
+
assert(prompts.buildLoopCheckerPrompt({ plan, node, makerArtifacts: ['maker.json'] }).includes('fresh session'), 'checker prompt requires fresh session');
|
|
112
|
+
}
|
|
113
|
+
else if (id === 'loop:runtime-real-workers' || id === 'loop:maker-checker-real') {
|
|
114
|
+
const proof = await readJson(loopProofPath(root, missionId, 'loop-zellij'));
|
|
115
|
+
assert(proof.maker_result.artifacts.length > 0, 'maker worker runtime artifacts exist');
|
|
116
|
+
assert(proof.checker_result.artifacts.length > 0, 'checker worker runtime artifacts exist');
|
|
117
|
+
assert(!proof.maker_result.artifacts.includes('fresh-checker-session'), 'placeholder checker string is not used');
|
|
118
|
+
}
|
|
119
|
+
else if (id === 'loop:checker-freshness') {
|
|
120
|
+
const proof = await readJson(loopProofPath(root, missionId, 'loop-zellij'));
|
|
121
|
+
const checker = await readJson(proof.checker_result.checker_findings[0]);
|
|
122
|
+
assert(checker.fresh_session === true, 'checker artifact proves fresh session');
|
|
123
|
+
assert(Array.isArray(checker.reviewed_maker_artifacts), 'checker reviewed maker artifacts');
|
|
124
|
+
assert(proof.checker_result.fresh_session === true, 'loop proof records checker freshness');
|
|
125
|
+
}
|
|
126
|
+
else if (id === 'loop:gate-registry') {
|
|
127
|
+
const registry = await import('../core/loops/loop-gate-registry.js');
|
|
128
|
+
const defs = await registry.listLoopGateDefinitions(process.cwd());
|
|
129
|
+
assert(defs.some((gate) => gate.id === 'gpt:final-arbiter' && gate.source === 'builtin-pseudo'), 'gpt final pseudo gate registered');
|
|
130
|
+
assert(await registry.resolveLoopGate(process.cwd(), 'definitely:unknown') === null, 'unknown gate does not resolve');
|
|
131
|
+
}
|
|
132
|
+
else if (id === 'loop:gate-runner-real' || id === 'loop:gate-artifacts') {
|
|
133
|
+
const proof = await readJson(loopProofPath(root, missionId, 'loop-zellij'));
|
|
134
|
+
assert(proof.gate_result.selected_gates.length > 0, 'gates selected');
|
|
135
|
+
assert(proof.gate_result.passed_gates.length > 0 || proof.gate_result.failed_gates.length > 0, 'gate outcomes recorded');
|
|
136
|
+
assert(await exists(path.join(loopRoot(root, missionId), 'loop-zellij', 'gates')), 'gate artifact directory exists');
|
|
137
|
+
}
|
|
138
|
+
else if (id === 'loop:worktree-runtime') {
|
|
139
|
+
assert(await exists(path.join(loopRoot(root, missionId), 'loop-zellij', 'worktree.json')), 'worktree record exists');
|
|
140
|
+
}
|
|
141
|
+
else if (id === 'loop:worktree-diff-scope') {
|
|
142
|
+
const mod = await import('../core/loops/loop-worktree-runtime.js');
|
|
143
|
+
assert(mod.enforceLoopOwnerScope(['src/core/zellij/zellij-slot-pane-renderer.ts'], byId.get('loop-zellij').owner_scope).length === 0, 'owner-scoped file passes');
|
|
144
|
+
assert(mod.enforceLoopOwnerScope(['README.md'], byId.get('loop-zellij').owner_scope).length > 0, 'outside owner scope blocks');
|
|
145
|
+
}
|
|
146
|
+
else if (id === 'loop:integration-merge') {
|
|
147
|
+
assert(await exists(path.join(loopRoot(root, missionId), 'integration-merge.json')), 'integration merge artifact exists');
|
|
148
|
+
}
|
|
149
|
+
else if (id === 'loop:integration-finalizer-real') {
|
|
150
|
+
const graph = await readJson(loopGraphProofPath(root, missionId));
|
|
151
|
+
assert(graph.integration_merge && typeof graph.integration_merge.ok === 'boolean', 'graph proof includes integration merge');
|
|
152
|
+
}
|
|
153
|
+
else if (id === 'file-lock:atomic') {
|
|
154
|
+
const lock = await import('../core/locks/file-lock.js');
|
|
155
|
+
let count = 0;
|
|
156
|
+
await lock.withFileLock({ lockPath: path.join(root, '.sneakoscope/locks/test.lock'), timeoutMs: 1000, staleMs: 10000 }, async () => { count += 1; });
|
|
157
|
+
assert(count === 1, 'file lock executes critical section');
|
|
158
|
+
}
|
|
159
|
+
else if (id === 'loop:lease-atomic') {
|
|
160
|
+
const node = byId.get('loop-zellij');
|
|
161
|
+
const lease = await acquireLoopLease(root, plan, node);
|
|
162
|
+
assert(lease.status === 'active' || lease.status === 'conflict', 'atomic lease returns status');
|
|
163
|
+
}
|
|
164
|
+
else if (id === 'loop:gpt-final-arbiter' || id === 'loop:integration-gpt-final') {
|
|
165
|
+
const mod = await import('../core/loops/loop-gpt-final-arbiter.js');
|
|
166
|
+
const arbiter = await mod.runLoopGptFinalArbiter({ root, plan, proofs: result.proofs, integrationMerge: { schema: 'sks.loop-integration-merge.v1', ok: true, applied_loops: [], conflict_loops: [], changed_files: ['src/core/loops/loop-runtime.ts'], blockers: [] }, forceVerdict: 'approve' });
|
|
167
|
+
assert(arbiter.ok && arbiter.verdict === 'approve', 'loop GPT final arbiter can approve');
|
|
168
|
+
}
|
|
169
|
+
else if (id === 'loop:checkpoint') {
|
|
170
|
+
assert(await exists(path.join(loopRoot(root, missionId), 'loop-zellij', 'checkpoint-latest.json')), 'latest checkpoint exists');
|
|
171
|
+
}
|
|
172
|
+
else if (id === 'loop:kill-resume' || id === 'loop:cli-kill-resume') {
|
|
173
|
+
const control = await import('../core/loops/loop-runtime-control.js');
|
|
174
|
+
await control.writeLoopKillRequest(root, missionId, 'loop-zellij');
|
|
175
|
+
assert(await control.shouldKillLoop(root, missionId, 'loop-zellij'), 'kill request targets loop');
|
|
176
|
+
}
|
|
177
|
+
else if (id === 'loop:real-maker-checker-blackbox') {
|
|
178
|
+
const proof = await readJson(loopProofPath(root, missionId, 'loop-zellij'));
|
|
179
|
+
assert(proof.maker_result.worker_count > 0 && proof.checker_result.worker_count > 0, 'maker/checker worker counts recorded');
|
|
180
|
+
assert(proof.checker_result.checker_findings.length > 0, 'checker findings artifact exists');
|
|
181
|
+
}
|
|
182
|
+
else if (id === 'naruto:loop-mesh-real-blackbox') {
|
|
183
|
+
assert(plan.graph.nodes.length >= 5, 'at least four domain loops plus integration are planned');
|
|
184
|
+
assert(result.proofs.every((proof) => proof.maker_result.artifacts.length && proof.checker_result.artifacts.length), 'worker runtime artifacts exist for every loop');
|
|
185
|
+
assert(result.graph_proof.integration_merge, 'integration finalizer ran');
|
|
186
|
+
}
|
|
187
|
+
else if (id === 'goal:loop-runtime-real-blackbox') {
|
|
188
|
+
const goalPlan = await compileGoalToLoopPlan({ root, missionId: `${missionId}-goal-real`, goalText: 'fix release cache', legacyGoalOptions: {} });
|
|
189
|
+
const goalResult = await runLoopPlan({ root, plan: goalPlan, parallelism: 'balanced', noMutation: true });
|
|
190
|
+
assert(await exists(path.join(root, '.sneakoscope', 'missions', `${missionId}-goal-real`, 'goal-compat.json')), 'goal compat artifact exists');
|
|
191
|
+
assert(goalResult.proofs.some((proof) => proof.maker_result.artifacts.length), 'goal loop worker runtime artifacts exist');
|
|
192
|
+
assert(await exists(loopGraphProofPath(root, `${missionId}-goal-real`)), 'goal graph proof exists');
|
|
193
|
+
}
|
|
194
|
+
else if (id === 'loop:status-ux') {
|
|
195
|
+
assert(await exists(loopGraphProofPath(root, missionId)), 'status has graph proof source');
|
|
196
|
+
}
|
|
197
|
+
else if (id === 'loop:zellij-real-runtime-ui') {
|
|
198
|
+
assert(renderZellijSlotPane({ slotId: 'slot-003', generationIndex: 1, loopId: 'loop-zellij', loopRole: 'maker', loopGate: 'loop:test', backend: 'fixture', patchStatus: 'fixture', verifyStatus: 'pass' }).includes('fixture loop proof'), 'zellij marks fixture proof');
|
|
199
|
+
}
|
|
71
200
|
else if (id === 'loop:proof') {
|
|
72
201
|
assert(await exists(loopProofPath(root, missionId, 'loop-zellij')), 'loop proof exists');
|
|
73
202
|
}
|
|
@@ -85,6 +214,48 @@ export async function runLoopDirectiveCheck(id) {
|
|
|
85
214
|
const node = byId.get('loop-zellij');
|
|
86
215
|
const gates = await runLoopGates({ root, missionId, node, gates: node.gates });
|
|
87
216
|
assert(gates.skipped_gates.includes('release:check') === false, 'gate runner avoids full release check inside loop');
|
|
217
|
+
const checkerDir = path.join(root, '.sneakoscope', 'missions', missionId, 'agents', 'sessions');
|
|
218
|
+
await fs.mkdir(checkerDir, { recursive: true });
|
|
219
|
+
await fs.writeFile(path.join(checkerDir, 'checker-findings.json'), JSON.stringify({ fresh_session: true, approved: true }));
|
|
220
|
+
const checkerGate = await runLoopGates({
|
|
221
|
+
root,
|
|
222
|
+
missionId,
|
|
223
|
+
node,
|
|
224
|
+
gates: { triage: [], local: [], checker: ['loop:checker-fresh-session'], integration: [], final: [] },
|
|
225
|
+
checkerArtifacts: ['sessions/checker-findings.json']
|
|
226
|
+
});
|
|
227
|
+
assert(checkerGate.ok, 'builtin checker gate resolves mission-ledger relative artifacts');
|
|
228
|
+
const foreignChecker = path.join(path.dirname(root), `${missionId}-foreign-checker-findings.json`);
|
|
229
|
+
await fs.writeFile(foreignChecker, JSON.stringify({ fresh_session: true, approved: true }));
|
|
230
|
+
const foreignRelative = path.relative(path.join(root, '.sneakoscope', 'missions', missionId, 'agents'), foreignChecker);
|
|
231
|
+
const unsafeCheckerGate = await runLoopGates({
|
|
232
|
+
root,
|
|
233
|
+
missionId,
|
|
234
|
+
node,
|
|
235
|
+
gates: { triage: [], local: [], checker: ['loop:checker-fresh-session'], integration: [], final: [] },
|
|
236
|
+
checkerArtifacts: [foreignRelative, foreignChecker]
|
|
237
|
+
});
|
|
238
|
+
assert(!unsafeCheckerGate.ok && unsafeCheckerGate.blockers.includes('loop_checker_fresh_session_missing'), 'builtin checker gate rejects foreign absolute and traversal artifacts');
|
|
239
|
+
const repoLocalChecker = path.join(root, 'repo-local-checker-findings.json');
|
|
240
|
+
await fs.writeFile(repoLocalChecker, JSON.stringify({ fresh_session: true, approved: true }));
|
|
241
|
+
const repoLocalCheckerGate = await runLoopGates({
|
|
242
|
+
root,
|
|
243
|
+
missionId,
|
|
244
|
+
node,
|
|
245
|
+
gates: { triage: [], local: [], checker: ['loop:checker-fresh-session'], integration: [], final: [] },
|
|
246
|
+
checkerArtifacts: ['repo-local-checker-findings.json', repoLocalChecker]
|
|
247
|
+
});
|
|
248
|
+
assert(!repoLocalCheckerGate.ok && repoLocalCheckerGate.blockers.includes('loop_checker_fresh_session_missing'), 'builtin checker gate rejects repo-local non-mission artifacts');
|
|
249
|
+
const symlinkChecker = path.join(checkerDir, 'checker-findings-symlink.json');
|
|
250
|
+
await fs.symlink(repoLocalChecker, symlinkChecker);
|
|
251
|
+
const symlinkCheckerGate = await runLoopGates({
|
|
252
|
+
root,
|
|
253
|
+
missionId,
|
|
254
|
+
node,
|
|
255
|
+
gates: { triage: [], local: [], checker: ['loop:checker-fresh-session'], integration: [], final: [] },
|
|
256
|
+
checkerArtifacts: ['sessions/checker-findings-symlink.json']
|
|
257
|
+
});
|
|
258
|
+
assert(!symlinkCheckerGate.ok && symlinkCheckerGate.blockers.includes('loop_checker_fresh_session_missing'), 'builtin checker gate rejects mission-local symlinks that escape the mission root');
|
|
88
259
|
}
|
|
89
260
|
else if (id === 'loop:gate-ladder') {
|
|
90
261
|
const node = byId.get('loop-zellij');
|
|
@@ -108,7 +279,7 @@ export async function runLoopDirectiveCheck(id) {
|
|
|
108
279
|
assert(docsA.status === 'active' && docsB.status === 'active', 'docs overlap is allowed when non-exclusive');
|
|
109
280
|
}
|
|
110
281
|
else if (id === 'naruto:loop-mesh' || id === 'naruto:loop-maker-checker') {
|
|
111
|
-
const mesh = await runNarutoLoopMesh({ root, plan, parallelism: 'balanced' });
|
|
282
|
+
const mesh = await runNarutoLoopMesh({ root, plan, parallelism: 'balanced', noMutation: fixtureMode ? true : !realRuntimeMode });
|
|
112
283
|
assert(mesh.proofs.every((proof) => proof.maker_result.worker_count > 0 && proof.checker_result.worker_count > 0), 'maker/checker artifacts exist for each loop');
|
|
113
284
|
}
|
|
114
285
|
else if (id === 'naruto:loop-worker-router') {
|
|
@@ -159,7 +330,59 @@ async function exists(file) {
|
|
|
159
330
|
return false;
|
|
160
331
|
}
|
|
161
332
|
}
|
|
333
|
+
function throws(fn) {
|
|
334
|
+
try {
|
|
335
|
+
fn();
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
162
342
|
async function readJson(file) {
|
|
163
343
|
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
164
344
|
}
|
|
345
|
+
async function productionFixtureNegativeCheck() {
|
|
346
|
+
const code = `
|
|
347
|
+
import { runLoopMakerWorkers } from './dist/core/loops/loop-worker-runtime.js';
|
|
348
|
+
const node = {
|
|
349
|
+
mission_id: 'M-production-fixture-negative',
|
|
350
|
+
loop_id: 'loop-production-fixture-negative',
|
|
351
|
+
owner_scope: { files: ['README.md'], directories: [], package_scripts: [], release_gate_ids: [], exclusive: true, collision_policy: 'handoff' },
|
|
352
|
+
maker: { worker_count: 1 },
|
|
353
|
+
checker: { worker_count: 1 },
|
|
354
|
+
risk: { requires_gpt_final: false },
|
|
355
|
+
worktree: { required: false }
|
|
356
|
+
};
|
|
357
|
+
const plan = { mission_id: 'M-production-fixture-negative' };
|
|
358
|
+
try {
|
|
359
|
+
await runLoopMakerWorkers({ root: process.cwd(), plan, node, fixture: true });
|
|
360
|
+
console.error('fixture unexpectedly allowed outside test context');
|
|
361
|
+
process.exit(1);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
364
|
+
if (!message.includes('loop_fixture_runtime_forbidden')) {
|
|
365
|
+
console.error(message);
|
|
366
|
+
process.exit(2);
|
|
367
|
+
}
|
|
368
|
+
console.log(message);
|
|
369
|
+
}
|
|
370
|
+
`;
|
|
371
|
+
return runProcess('/usr/bin/env', [
|
|
372
|
+
'-u', 'NODE_ENV',
|
|
373
|
+
'-u', 'SKS_TEST_RUNTIME_FIXTURE_ALLOWED',
|
|
374
|
+
'-u', 'VITEST_WORKER_ID',
|
|
375
|
+
'-u', 'JEST_WORKER_ID',
|
|
376
|
+
'-u', 'NODE_V8_COVERAGE',
|
|
377
|
+
'SKS_LOOP_RUNTIME_FIXTURE=1',
|
|
378
|
+
process.execPath,
|
|
379
|
+
'--input-type=module',
|
|
380
|
+
'-e',
|
|
381
|
+
code
|
|
382
|
+
], {
|
|
383
|
+
cwd: process.cwd(),
|
|
384
|
+
timeoutMs: 30000,
|
|
385
|
+
maxOutputBytes: 8192
|
|
386
|
+
});
|
|
387
|
+
}
|
|
165
388
|
//# sourceMappingURL=loop-directive-check-lib.js.map
|