sneakoscope 2.0.11 → 2.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +9 -3
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/build-manifest.json +27 -8
  8. package/dist/core/agents/agent-orchestrator.js +279 -1
  9. package/dist/core/agents/agent-scheduler.js +12 -1
  10. package/dist/core/agents/agent-slot-pane-binding-proof.js +3 -3
  11. package/dist/core/agents/agent-work-queue.js +26 -2
  12. package/dist/core/agents/agent-worker-pipeline.js +2 -0
  13. package/dist/core/agents/native-cli-session-swarm.js +2 -2
  14. package/dist/core/codex-control/codex-sdk-adapter.js +10 -0
  15. package/dist/core/codex-control/codex-task-runner.js +4 -2
  16. package/dist/core/commands/naruto-command.js +104 -51
  17. package/dist/core/commands/research-command.js +43 -4
  18. package/dist/core/fsx.js +1 -1
  19. package/dist/core/git/git-worktree-merge-queue.js +34 -14
  20. package/dist/core/naruto/naruto-rebalance-policy.js +15 -3
  21. package/dist/core/naruto/naruto-work-graph.js +13 -0
  22. package/dist/core/research/claim-evidence-matrix.js +160 -0
  23. package/dist/core/research/experiment-plan.js +53 -0
  24. package/dist/core/research/falsification.js +18 -0
  25. package/dist/core/research/implementation-blueprint-markdown.js +31 -0
  26. package/dist/core/research/implementation-blueprint.js +66 -0
  27. package/dist/core/research/replication-pack.js +50 -0
  28. package/dist/core/research/research-cycle-runner.js +25 -0
  29. package/dist/core/research/research-final-reviewer.js +58 -0
  30. package/dist/core/research/research-handoff.js +51 -0
  31. package/dist/core/research/research-prompt-contract.js +24 -0
  32. package/dist/core/research/research-quality-contract.js +61 -0
  33. package/dist/core/research/research-report-quality.js +67 -0
  34. package/dist/core/research/research-stage-runner.js +16 -0
  35. package/dist/core/research/research-work-graph.js +75 -0
  36. package/dist/core/research/source-quality-report.js +94 -0
  37. package/dist/core/research.js +344 -44
  38. package/dist/core/version.js +1 -1
  39. package/dist/core/zellij/zellij-slot-column-anchor.js +165 -4
  40. package/dist/core/zellij/zellij-slot-pane-renderer.js +259 -16
  41. package/dist/core/zellij/zellij-worker-pane-manager.js +13 -7
  42. package/dist/scripts/agent-real-codex-in-zellij-worker-pane-check.js +8 -2
  43. package/dist/scripts/agent-slot-pane-binding-proof-check.js +4 -4
  44. package/dist/scripts/codex-sdk-release-review-pipeline-check.js +2 -1
  45. package/dist/scripts/codex-sdk-research-pipeline-check.js +7 -0
  46. package/dist/scripts/codex-sdk-zellij-pane-binding-check.js +2 -2
  47. package/dist/scripts/git-worktree-cross-rebase-check.js +13 -1
  48. package/dist/scripts/git-worktree-merge-queue-check.js +1 -0
  49. package/dist/scripts/local-collab-worktree-gpt-final-apply-policy-check.js +63 -0
  50. package/dist/scripts/naruto-actual-worker-control-plane-check.js +30 -3
  51. package/dist/scripts/naruto-allocation-runtime-wiring-check.js +92 -0
  52. package/dist/scripts/naruto-orchestrator-runtime-source-check.js +65 -6
  53. package/dist/scripts/naruto-rebalance-policy-check.js +15 -2
  54. package/dist/scripts/naruto-shadow-clone-swarm-check.js +1 -1
  55. package/dist/scripts/packlist-performance-check.js +1 -1
  56. package/dist/scripts/release-dag-full-coverage-check.js +4 -0
  57. package/dist/scripts/release-real-check.js +258 -77
  58. package/dist/scripts/research-quality-gate-check.js +86 -0
  59. package/dist/scripts/zellij-first-slot-down-stack-check.js +1 -1
  60. package/dist/scripts/zellij-first-slot-down-stack-real-check.js +344 -4
  61. package/dist/scripts/zellij-right-column-manager-check.js +1 -1
  62. package/dist/scripts/zellij-slot-column-anchor-check.js +45 -3
  63. package/dist/scripts/zellij-slot-only-ui-check.js +3 -1
  64. package/dist/scripts/zellij-slot-pane-renderer-check.js +73 -5
  65. package/dist/scripts/zellij-slot-renderer-proof-semantics-check.js +59 -0
  66. package/dist/scripts/zellij-worker-pane-manager-check.js +23 -1
  67. package/dist/scripts/zellij-worker-pane-real-ui-blackbox.js +21 -4
  68. package/package.json +17 -2
  69. package/schemas/research/claim-evidence-matrix.schema.json +37 -0
  70. package/schemas/research/experiment-plan.schema.json +17 -0
  71. package/schemas/research/implementation-blueprint.schema.json +30 -0
  72. package/schemas/research/replication-pack.schema.json +17 -0
  73. package/schemas/research/research-final-review.schema.json +16 -0
  74. package/schemas/research/research-quality-contract.schema.json +37 -0
  75. package/schemas/research/source-quality-report.schema.json +18 -0
@@ -1,5 +1,8 @@
1
+ import path from 'node:path';
2
+ import { appendJsonl } from '../fsx.js';
1
3
  import { buildCodexSdkConfig } from './codex-sdk-config-policy.js';
2
4
  import { buildCodexSdkEnv } from './codex-sdk-env-policy.js';
5
+ import { translateCodexSdkEvent } from './codex-event-translator.js';
3
6
  export async function runRealCodexSdkTask(input, policy) {
4
7
  const mod = await import('@openai/codex-sdk');
5
8
  const Codex = mod.Codex || mod.default?.Codex || mod.default;
@@ -22,9 +25,15 @@ export async function runRealCodexSdkTask(input, policy) {
22
25
  const thread = resumeId ? codex.resumeThread(resumeId, threadOptions) : codex.startThread(threadOptions);
23
26
  const events = [];
24
27
  let finalResponse = '';
28
+ let liveEventsWritten = false;
29
+ const liveEventPath = input.mutationLedgerRoot ? path.join(input.mutationLedgerRoot, 'codex-sdk-events.jsonl') : null;
25
30
  const streamed = await thread.runStreamed(buildSdkInput(input), { outputSchema: input.outputSchema });
26
31
  for await (const event of streamed.events) {
27
32
  events.push(event);
33
+ if (liveEventPath) {
34
+ await appendJsonl(liveEventPath, translateCodexSdkEvent(event));
35
+ liveEventsWritten = true;
36
+ }
28
37
  if (event?.type === 'item.completed' && event?.item?.type === 'agent_message')
29
38
  finalResponse = String(event.item.text || '');
30
39
  }
@@ -37,6 +46,7 @@ export async function runRealCodexSdkTask(input, policy) {
37
46
  finalResponse,
38
47
  structuredOutput,
39
48
  blockers: [],
49
+ liveEventsWritten,
40
50
  raw: { item_count: events.filter((event) => String(event?.type || '').startsWith('item.')).length }
41
51
  };
42
52
  }
@@ -67,8 +67,10 @@ export async function runCodexTask(input) {
67
67
  }
68
68
  const events = Array.isArray(adapterResult?.events) ? adapterResult.events : [];
69
69
  const translatedEvents = translateCodexSdkEvents(events);
70
- for (const event of translatedEvents)
71
- await appendJsonl(path.join(root, 'codex-sdk-events.jsonl'), event);
70
+ if (adapterResult?.liveEventsWritten !== true) {
71
+ for (const event of translatedEvents)
72
+ await appendJsonl(path.join(root, 'codex-sdk-events.jsonl'), event);
73
+ }
72
74
  if (adapterResult?.reliabilityShield)
73
75
  await writeJsonAtomic(path.join(root, 'codex-reliability-shield.json'), adapterResult.reliabilityShield);
74
76
  const structuredOutput = adapterResult?.structuredOutput;
@@ -94,7 +94,7 @@ async function narutoRun(parsed) {
94
94
  const localWorker = await resolveNarutoLocalWorkerMode(parsed);
95
95
  const schedulerBackend = localWorker.auto_select_eligible ? 'ollama' : parsed.backend;
96
96
  const safe = systemSafeNarutoConcurrency({ backend: schedulerBackend });
97
- const workGraph = buildNarutoWorkGraph({
97
+ const baseWorkGraph = buildNarutoWorkGraph({
98
98
  prompt: parsed.prompt,
99
99
  requestedClones: roster.agent_count,
100
100
  totalWorkItems: parsed.workItems,
@@ -104,9 +104,21 @@ async function narutoRun(parsed) {
104
104
  maxActiveWorkers: parsed.concurrency || safe.cap,
105
105
  worktreePolicy
106
106
  });
107
+ const baseRoleDistribution = buildNarutoRoleDistribution(baseWorkGraph.work_items, { readonly: parsed.readonly });
108
+ const allocationWorkers = buildNarutoAllocationWorkers(baseWorkGraph, baseRoleDistribution, roster);
109
+ const allocationAssignments = allocateNarutoTasksToWorkers(baseWorkGraph.work_items, allocationWorkers);
110
+ const workGraph = buildNarutoWorkGraph({
111
+ prompt: parsed.prompt,
112
+ requestedClones: roster.agent_count,
113
+ totalWorkItems: parsed.workItems,
114
+ readonly: parsed.readonly,
115
+ writeCapable,
116
+ leaseBasePath: patchEnvelopeBasePath,
117
+ maxActiveWorkers: parsed.concurrency || safe.cap,
118
+ worktreePolicy,
119
+ allocationAssignments
120
+ });
107
121
  const roleDistribution = buildNarutoRoleDistribution(workGraph.work_items, { readonly: parsed.readonly });
108
- const allocationWorkers = buildNarutoAllocationWorkers(workGraph, roleDistribution, roster);
109
- const allocationAssignments = allocateNarutoTasksToWorkers(workGraph.work_items, allocationWorkers);
110
122
  const allocationPolicy = {
111
123
  schema: 'sks.naruto-allocation-policy.v1',
112
124
  generated_at: nowIso(),
@@ -135,7 +147,7 @@ async function narutoRun(parsed) {
135
147
  blockers: allocationWorkers.length ? [] : ['naruto_allocation_workers_missing']
136
148
  };
137
149
  const rebalanceDecisions = rebalanceNarutoReadyWork({
138
- tasks: workGraph.work_items.map((item) => ({ ...item, owner: null, status: 'pending' })),
150
+ tasks: workGraph.work_items.map((item) => ({ ...item, status: 'pending' })),
139
151
  workers: allocationWorkers.map((worker) => ({ ...worker, alive: true, state: 'idle' })),
140
152
  completedTaskIds: [],
141
153
  reclaimedTaskIds: []
@@ -159,52 +171,38 @@ async function narutoRun(parsed) {
159
171
  const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || Math.max(governor.safe_active_workers, backendMinimum), safe.cap));
160
172
  const zellijVisiblePanes = Math.max(1, Math.min(activeSlots, governor.safe_zellij_visible_panes));
161
173
  const activePool = await runNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
162
- const realRuntimeSmokeGraph = buildNarutoWorkGraph({
163
- prompt: parsed.prompt,
164
- requestedClones: Math.min(2, roster.agent_count),
165
- totalWorkItems: Math.min(2, workGraph.total_work_items),
166
- readonly: true,
167
- writeCapable: false,
168
- leaseBasePath: patchEnvelopeBasePath,
169
- maxActiveWorkers: Math.min(2, activeSlots),
170
- worktreePolicy: {
171
- mode: 'patch-envelope-only',
172
- required: false,
173
- main_repo_root: worktreePolicy.main_repo_root,
174
- worktree_root: null,
175
- fallback_reason: 'pre_run_smoke_never_owns_production_runtime'
176
- }
177
- });
178
- const realRuntimeWorktreePolicy = {
179
- mode: 'patch-envelope-only',
180
- required: false,
181
- main_repo_root: worktreePolicy.main_repo_root,
182
- worktree_root: null,
183
- fallback_reason: 'pre_run_smoke_never_owns_production_runtime'
184
- };
185
- const realActivePool = await runNarutoRealActivePool({
186
- graph: realRuntimeSmokeGraph,
187
- governor: { ...governor, safe_active_workers: Math.min(2, activeSlots), safe_zellij_visible_panes: Math.min(1, zellijVisiblePanes) },
188
- spawnWorker: async (item, placement) => spawnActualNarutoWorker({
174
+ const runPreRunSmoke = parsed.smoke === true || process.env.SKS_NARUTO_PRE_RUN_SMOKE === '1';
175
+ const realActivePoolSmoke = runPreRunSmoke
176
+ ? await runNarutoControlPlaneSmoke({
189
177
  root,
190
178
  missionId: mission.id,
191
- item,
192
- placement,
193
- backend: 'fake',
194
- worktreePolicy: realRuntimeWorktreePolicy,
195
- zellijSessionName: `sks-${mission.id}`,
196
- visiblePaneCap: zellijVisiblePanes
197
- }),
198
- collectWorker: async (handle) => collectActualNarutoWorker(handle),
199
- enqueueVerification: async () => undefined,
200
- updateDashboard: async () => undefined
201
- });
202
- const realActivePoolSmoke = {
203
- ...realActivePool,
204
- runtime_source_of_truth: 'pre_run_smoke_only',
205
- production_runtime_source_of_truth: 'agent-orchestrator-scheduler',
206
- smoke_graph_total_work_items: realRuntimeSmokeGraph.total_work_items
207
- };
179
+ prompt: parsed.prompt,
180
+ rosterCount: roster.agent_count,
181
+ totalWorkItems: workGraph.total_work_items,
182
+ patchEnvelopeBasePath,
183
+ worktreePolicy,
184
+ governor,
185
+ activeSlots,
186
+ zellijVisiblePanes
187
+ })
188
+ : {
189
+ schema: 'sks.naruto-active-pool.v1',
190
+ ok: true,
191
+ status: 'skipped',
192
+ runtime_source_of_truth: 'agent-orchestrator-scheduler',
193
+ production_runtime_source_of_truth: 'agent-orchestrator-scheduler',
194
+ fallback_reason: 'pre_run_smoke_never_owns_production_runtime',
195
+ reason: 'pre_run_smoke_disabled_for_production',
196
+ active_cap: 0,
197
+ max_observed_active_workers: 0,
198
+ average_active_workers: 0,
199
+ active_pool_utilization: 0,
200
+ refill_latency_ms_p95: 0,
201
+ visible_workers: 0,
202
+ headless_workers: 0,
203
+ worker_lifecycle: [],
204
+ smoke_graph_total_work_items: 0
205
+ };
208
206
  const verificationDag = buildNarutoVerificationDag(workGraph, { cwd: root });
209
207
  const gptFinalPack = buildNarutoGptFinalPack({
210
208
  missionId: mission.id,
@@ -284,7 +282,8 @@ async function narutoRun(parsed) {
284
282
  concurrency: activeSlots,
285
283
  targetActiveSlots: activeSlots,
286
284
  visualLaneCount: zellijVisiblePanes,
287
- desiredWorkItemCount: parsed.workItems,
285
+ desiredWorkItemCount: workGraph.total_work_items,
286
+ minimumWorkItems: workGraph.total_work_items,
288
287
  maxAgentCount: MAX_NARUTO_AGENT_COUNT,
289
288
  narutoMode: true,
290
289
  clones: roster.agent_count,
@@ -307,6 +306,9 @@ async function narutoRun(parsed) {
307
306
  noFast: false,
308
307
  writeMode: writeCapable ? parsed.writeMode || 'parallel' : 'off',
309
308
  gitWorktreePolicy: worktreePolicy,
309
+ narutoWorkGraph: workGraph,
310
+ narutoAllocationPolicy: allocationPolicy,
311
+ narutoRebalancePolicy: rebalancePolicy,
310
312
  json: parsed.json
311
313
  });
312
314
  const clones = result.roster?.agent_count ?? roster.agent_count;
@@ -323,7 +325,7 @@ async function narutoRun(parsed) {
323
325
  concurrency: result.target_active_slots ?? activeSlots,
324
326
  target_active_slots: result.target_active_slots ?? activeSlots,
325
327
  runtime_source_of_truth: 'agent-orchestrator-scheduler',
326
- pre_run_real_active_pool_source: 'smoke_only',
328
+ pre_run_real_active_pool_source: runPreRunSmoke ? 'smoke_only' : 'skipped',
327
329
  concurrency_capped: clones > (result.target_active_slots ?? activeSlots),
328
330
  system: { cores: safe.cores, free_gb: safe.free_gb, safe_concurrency: safe.cap, heavy_backend: safe.heavy },
329
331
  work_graph: {
@@ -409,6 +411,56 @@ function compactNarutoRunResult(result) {
409
411
  }
410
412
  };
411
413
  }
414
+ async function runNarutoControlPlaneSmoke(input) {
415
+ const smokeGraph = buildNarutoWorkGraph({
416
+ prompt: input.prompt,
417
+ requestedClones: Math.min(2, input.rosterCount),
418
+ totalWorkItems: Math.min(2, input.totalWorkItems),
419
+ readonly: true,
420
+ writeCapable: false,
421
+ leaseBasePath: input.patchEnvelopeBasePath,
422
+ maxActiveWorkers: Math.min(2, input.activeSlots),
423
+ worktreePolicy: {
424
+ mode: 'patch-envelope-only',
425
+ required: false,
426
+ main_repo_root: input.worktreePolicy.main_repo_root,
427
+ worktree_root: null,
428
+ fallback_reason: 'pre_run_smoke_never_owns_production_runtime'
429
+ }
430
+ });
431
+ const smokeWorktreePolicy = {
432
+ mode: 'patch-envelope-only',
433
+ required: false,
434
+ main_repo_root: input.worktreePolicy.main_repo_root,
435
+ worktree_root: null,
436
+ fallback_reason: 'pre_run_smoke_never_owns_production_runtime'
437
+ };
438
+ const realActivePool = await runNarutoRealActivePool({
439
+ graph: smokeGraph,
440
+ governor: { ...input.governor, safe_active_workers: Math.min(2, input.activeSlots), safe_zellij_visible_panes: Math.min(1, input.zellijVisiblePanes) },
441
+ spawnWorker: async (item, placement) => spawnActualNarutoWorker({
442
+ root: input.root,
443
+ missionId: input.missionId,
444
+ item,
445
+ placement,
446
+ backend: 'fake',
447
+ worktreePolicy: smokeWorktreePolicy,
448
+ zellijSessionName: `sks-${input.missionId}`,
449
+ visiblePaneCap: input.zellijVisiblePanes
450
+ }),
451
+ collectWorker: async (handle) => collectActualNarutoWorker(handle),
452
+ enqueueVerification: async () => undefined,
453
+ updateDashboard: async () => undefined
454
+ });
455
+ return {
456
+ ...realActivePool,
457
+ status: 'smoke_completed',
458
+ runtime_source_of_truth: 'pre_run_smoke_only',
459
+ production_runtime_source_of_truth: 'agent-orchestrator-scheduler',
460
+ fallback_reason: 'pre_run_smoke_never_owns_production_runtime',
461
+ smoke_graph_total_work_items: smokeGraph.total_work_items
462
+ };
463
+ }
412
464
  function buildNarutoAllocationWorkers(workGraph, roleDistribution, roster) {
413
465
  const workItems = Array.isArray(workGraph?.work_items) ? workGraph.work_items : [];
414
466
  const roleByWorkItem = new Map((roleDistribution?.work_item_roles || []).map((row) => [String(row.work_item_id), String(row.role || '')]));
@@ -582,9 +634,10 @@ function parseNarutoArgs(args = []) {
582
634
  const ollamaBaseUrl = String(readOption(args, '--ollama-base-url', readOption(args, '--local-model-base-url', '')) || '') || null;
583
635
  const noOpenZellij = hasFlag(args, '--no-open-zellij') || hasFlag(args, '--no-zellij');
584
636
  const attach = hasFlag(args, '--attach');
637
+ const smoke = hasFlag(args, '--smoke');
585
638
  const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url']);
586
639
  const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
587
- return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, json, missionId, noOpenZellij, attach };
640
+ return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, json, missionId, noOpenZellij, attach, smoke };
588
641
  }
589
642
  async function writeNarutoArtifacts(ledgerRoot, artifacts) {
590
643
  await writeJsonAtomic(path.join(ledgerRoot, 'naruto-work-graph.json'), artifacts.workGraph);
@@ -14,6 +14,15 @@ import { scanDbSafety } from '../db-safety.js';
14
14
  import { maybeFinalizeRoute } from '../proof/auto-finalize.js';
15
15
  import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
16
16
  import { flag, positionalArgs, readFlagValue, readMaxCycles, readBoundedIntegerFlag, resolveMissionId, safeReadTextFile } from './command-utils.js';
17
+ import { writeResearchWorkGraph } from '../research/research-work-graph.js';
18
+ import { runResearchCycle } from '../research/research-cycle-runner.js';
19
+ import { readResearchQualityContract } from '../research/research-quality-contract.js';
20
+ import { readClaimEvidenceMatrix } from '../research/claim-evidence-matrix.js';
21
+ import { readSourceQualityReport } from '../research/source-quality-report.js';
22
+ import { readImplementationBlueprint, validateImplementationBlueprint } from '../research/implementation-blueprint.js';
23
+ import { readExperimentPlan, validateExperimentPlan } from '../research/experiment-plan.js';
24
+ import { readReplicationPack, validateReplicationPack } from '../research/replication-pack.js';
25
+ import { readResearchFinalReview } from '../research/research-final-reviewer.js';
17
26
  const RESEARCH_DEFAULT_MAX_CYCLES = 12;
18
27
  const RESEARCH_DEFAULT_CYCLE_TIMEOUT_MINUTES = 120;
19
28
  const RESEARCH_MIN_CYCLE_TIMEOUT_MINUTES = 15;
@@ -128,9 +137,12 @@ async function researchRun(args) {
128
137
  const dryRunPatches = flag(args, '--dry-run-patches') || flag(args, '--dryrun-patches');
129
138
  const maxWriteAgents = readBoundedIntegerFlag(args, '--max-write-agents', Math.min(requestedAgents, 5), 1, 20);
130
139
  const mock = flag(args, '--mock');
140
+ const researchWorkGraph = await writeResearchWorkGraph(dir, plan);
141
+ const graphWorkItemCount = Math.max(1, Number(researchWorkGraph.total_work_items || researchWorkGraph.work_items?.length || 0));
142
+ await runResearchCycle(dir, researchWorkGraph, { cycle: 0, status: mock ? 'mock_native_orchestrator_planned' : 'native_orchestrator_planned' });
131
143
  await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_RUNNING_NO_QUESTIONS', questions_allowed: false, implementation_allowed: false, research_real_run_required: !mock, research_cycle_timeout_minutes: cycleTimeoutMinutes });
132
144
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.run.started', maxCycles, mock, cycleTimeoutMinutes, real_run_required: !mock });
133
- const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: flag(args, '--autoresearch') ? '$AutoResearch' : '$Research', prompt: mission.prompt || plan.prompt || 'Research run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount, minimumWorkItems, maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: !(applyPatches && writeMode !== 'off'), profile, writeMode: writeMode, applyPatches, dryRunPatches, maxWriteAgents, roster: plan.native_agent_plan, routeCommand: 'sks research run', routeBlackboxKind: 'actual_research_command' });
145
+ const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: flag(args, '--autoresearch') ? '$AutoResearch' : '$Research', prompt: mission.prompt || plan.prompt || 'Research run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount: Math.max(desiredWorkItemCount, graphWorkItemCount), minimumWorkItems: Math.max(minimumWorkItems, Math.min(graphWorkItemCount, targetActiveSlots)), maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: true, profile, writeMode: writeMode, applyPatches: false, dryRunPatches, maxWriteAgents, roster: plan.native_agent_plan, routeCommand: 'sks research run', routeBlackboxKind: 'actual_research_command', narutoWorkGraph: researchWorkGraph });
134
146
  await writeJsonAtomic(path.join(dir, 'research-native-agent-run.json'), nativeAgentRun);
135
147
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.native_agents.completed', backend: nativeAgentRun.backend, ok: nativeAgentRun.ok, proof: nativeAgentRun.proof?.status });
136
148
  if (mock) {
@@ -141,7 +153,7 @@ async function researchRun(args) {
141
153
  const proof = await maybeFinalizeRoute(root, { missionId: id, route: '$Research', gateFile: 'research-gate.json', gate: gate.gate || gate, artifacts: ['agents/agent-proof-evidence.json', 'research-native-agent-run.json', 'research-gate.json', 'research-report.md', researchPaperArtifactForPlan(plan), 'source-ledger.json', 'agent-ledger.json', 'debate-ledger.json', 'completion-proof.json'], mock, command: { cmd: `sks research run ${id} --mock`, status: 0 } });
142
154
  await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: gate.passed ? 'RESEARCH_DONE' : 'RESEARCH_PAUSED', questions_allowed: true, implementation_allowed: false });
143
155
  if (flag(args, '--json'))
144
- return console.log(JSON.stringify({ schema: flag(args, '--autoresearch') ? 'sks.autoresearch-run.v1' : 'sks.research-run.v1', ok: proof.ok, mission_id: id, gate, proof: proof.validation, native_agent_run: nativeAgentRun, agent_batches: plan.agent_batches, autoresearch_cycle_policy: plan.autoresearch_cycle_policy }, null, 2));
156
+ return console.log(JSON.stringify({ schema: flag(args, '--autoresearch') ? 'sks.autoresearch-run.v1' : 'sks.research-run.v1', ok: proof.ok, mission_id: id, gate, quality_metrics: gate.metrics || null, proof: proof.validation, native_agent_run: nativeAgentRun, research_work_graph: researchWorkGraph, agent_batches: plan.agent_batches, autoresearch_cycle_policy: plan.autoresearch_cycle_policy }, null, 2));
145
157
  console.log(`Mock research done: ${id}`);
146
158
  console.log(`Gate: ${gate.passed ? 'passed' : 'blocked'}`);
147
159
  return;
@@ -178,6 +190,7 @@ async function researchRun(args) {
178
190
  const cycleDir = path.join(dir, 'research', `cycle-${cycle}`);
179
191
  const outputFile = path.join(cycleDir, 'final.md');
180
192
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle.start', cycle, timeoutMinutes: cycleTimeoutMinutes, profile, enforced_reasoning_effort: 'xhigh' });
193
+ await runResearchCycle(dir, researchWorkGraph, { cycle, status: 'codex_research_cycle_started' });
181
194
  const prompt = buildResearchPrompt({ id, mission, plan, cycle, previous: last });
182
195
  const result = await runCodexExec({ root, prompt, outputFile, json: true, profile, extraArgs: researchCodexArgs, logDir: cycleDir, timeoutMs: cycleTimeoutMs });
183
196
  await writeJsonAtomic(path.join(cycleDir, 'process.json'), { code: result.code, stdout_tail: result.stdout, stderr_tail: result.stderr, stdout_bytes: result.stdoutBytes, stderr_bytes: result.stderrBytes, truncated: result.truncated, timed_out: result.timedOut });
@@ -212,7 +225,7 @@ async function researchRun(args) {
212
225
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.done', cycle });
213
226
  await enforceRetention(root).catch(() => { });
214
227
  if (flag(args, '--json'))
215
- return console.log(JSON.stringify({ schema: flag(args, '--autoresearch') ? 'sks.autoresearch-run.v1' : 'sks.research-run.v1', ok: proof.ok, mission_id: id, gate, proof: proof.validation, agent_batches: plan.agent_batches, autoresearch_cycle_policy: plan.autoresearch_cycle_policy }, null, 2));
228
+ return console.log(JSON.stringify({ schema: flag(args, '--autoresearch') ? 'sks.autoresearch-run.v1' : 'sks.research-run.v1', ok: proof.ok, mission_id: id, gate, quality_metrics: gate.metrics || null, proof: proof.validation, research_work_graph: researchWorkGraph, agent_batches: plan.agent_batches, autoresearch_cycle_policy: plan.autoresearch_cycle_policy }, null, 2));
216
229
  console.log(`Research done: ${id}`);
217
230
  return;
218
231
  }
@@ -246,6 +259,16 @@ async function researchStatus(args) {
246
259
  const agentRows = Array.isArray(agentLedger?.agents) ? agentLedger.agents : [];
247
260
  const sourceLayerRows = Array.isArray(sourceLedger?.source_layers) ? sourceLedger.source_layers : [];
248
261
  const sourceLayersCovered = sourceLayerRows.filter((layer) => layer.status === 'covered' && ((Array.isArray(layer.source_ids) && layer.source_ids.length) || (Array.isArray(layer.counterevidence_ids) && layer.counterevidence_ids.length))).length;
262
+ const qualityContract = await readResearchQualityContract(dir);
263
+ const claimMatrix = await readClaimEvidenceMatrix(dir);
264
+ const sourceQualityReport = await readSourceQualityReport(dir);
265
+ const implementationBlueprint = await readImplementationBlueprint(dir);
266
+ const experimentPlan = await readExperimentPlan(dir);
267
+ const replicationPack = await readReplicationPack(dir);
268
+ const finalReview = await readResearchFinalReview(dir);
269
+ const blueprintValidation = validateImplementationBlueprint(implementationBlueprint, qualityContract);
270
+ const experimentValidation = validateExperimentPlan(experimentPlan, qualityContract);
271
+ const replicationValidation = validateReplicationPack(replicationPack);
249
272
  console.log(JSON.stringify({
250
273
  mission,
251
274
  state,
@@ -275,7 +298,23 @@ async function researchStatus(args) {
275
298
  research_paper_artifact: paperArtifact.name,
276
299
  paper_present: Boolean(paperText.trim()),
277
300
  paper_sections: countResearchPaperSections(paperText),
278
- falsification_cases: falsificationLedger?.cases?.length ?? null
301
+ falsification_cases: falsificationLedger?.cases?.length ?? null,
302
+ research_quality: {
303
+ contract: qualityContract,
304
+ report_word_count: gate?.metrics?.report_word_count ?? null,
305
+ claim_evidence_matrix_present: claimMatrix.present,
306
+ key_claims: claimMatrix.key_claim_ids.length,
307
+ triangulated_claims: claimMatrix.triangulated_claim_count,
308
+ claim_matrix_blockers: claimMatrix.blockers,
309
+ source_quality_report_ok: sourceQualityReport?.ok === true,
310
+ implementation_blueprint_sections: Array.isArray(implementationBlueprint?.sections) ? implementationBlueprint.sections.length : null,
311
+ implementation_blueprint_ok: blueprintValidation.ok,
312
+ experiment_steps: Array.isArray(experimentPlan?.steps) ? experimentPlan.steps.length : null,
313
+ experiment_plan_ok: experimentValidation.ok,
314
+ replication_pack_ok: replicationValidation.ok,
315
+ final_review_approved: finalReview?.approved === true,
316
+ final_review_blockers: finalReview?.blockers || []
317
+ }
279
318
  }, null, 2));
280
319
  }
281
320
  async function researchCodeMutationSnapshot(root, missionId = null) {
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '2.0.11';
8
+ export const PACKAGE_VERSION = '2.0.13';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
@@ -54,14 +54,34 @@ export async function applyGitWorktreeMergeQueue(input) {
54
54
  input: diff.diff,
55
55
  timeoutMs: 30000
56
56
  });
57
- if (apply.ok)
57
+ if (apply.ok) {
58
58
  appliedCount += 1;
59
+ strategyResults.push({
60
+ ok: true,
61
+ worker_id: diff.worker_id,
62
+ strategy: 'diff-apply-3way',
63
+ commit_hash: null,
64
+ conflict_files: [],
65
+ changed_files: diff.changed_files,
66
+ blockers: []
67
+ });
68
+ }
59
69
  else {
60
- conflicts.push(summarizeGitWorktreeConflict({
70
+ const conflict = summarizeGitWorktreeConflict({
61
71
  workerId: diff.worker_id,
62
72
  changedFiles: diff.changed_files,
63
73
  stderr: apply.stderr || apply.stdout
64
- }));
74
+ });
75
+ strategyResults.push({
76
+ ok: false,
77
+ worker_id: diff.worker_id,
78
+ strategy: 'diff-apply-3way',
79
+ commit_hash: null,
80
+ conflict_files: conflict.conflict_files || diff.changed_files,
81
+ changed_files: diff.changed_files,
82
+ blockers: conflict.blockers || ['git_worktree_diff_apply_failed']
83
+ });
84
+ conflicts.push(conflict);
65
85
  }
66
86
  }
67
87
  const blockers = conflicts.length ? ['git_worktree_merge_queue_conflicts'] : [];
@@ -80,44 +100,44 @@ export async function applyGitWorktreeMergeQueue(input) {
80
100
  };
81
101
  }
82
102
  async function applyCheckpointCommit(integrationWorktreePath, checkpoint) {
83
- const merge = await runGitCommand(integrationWorktreePath, ['merge', '--no-ff', '--no-edit', checkpoint.commit_hash || ''], {
103
+ const cherryPick = await runGitCommand(integrationWorktreePath, ['cherry-pick', '--allow-empty', '-X', 'theirs', checkpoint.commit_hash || ''], {
84
104
  timeoutMs: 120000
85
105
  });
86
- if (merge.ok) {
106
+ if (cherryPick.ok) {
87
107
  return {
88
108
  ok: true,
89
109
  worker_id: checkpoint.worker_id,
90
- strategy: 'merge',
110
+ strategy: 'checkpoint-cherry-pick',
91
111
  commit_hash: checkpoint.commit_hash,
92
112
  conflict_files: [],
93
113
  blockers: []
94
114
  };
95
115
  }
96
- await runGitCommand(integrationWorktreePath, ['merge', '--abort'], { timeoutMs: 30000 }).catch(() => null);
97
- const cherryPick = await runGitCommand(integrationWorktreePath, ['cherry-pick', checkpoint.commit_hash || ''], {
116
+ await runGitCommand(integrationWorktreePath, ['cherry-pick', '--abort'], { timeoutMs: 30000 }).catch(() => null);
117
+ const merge = await runGitCommand(integrationWorktreePath, ['merge', '--no-ff', '--no-edit', '-X', 'theirs', checkpoint.commit_hash || ''], {
98
118
  timeoutMs: 120000
99
119
  });
100
- if (cherryPick.ok) {
120
+ if (merge.ok) {
101
121
  return {
102
122
  ok: true,
103
123
  worker_id: checkpoint.worker_id,
104
- strategy: 'cherry-pick',
124
+ strategy: 'checkpoint-merge',
105
125
  commit_hash: checkpoint.commit_hash,
106
126
  conflict_files: [],
107
127
  blockers: []
108
128
  };
109
129
  }
110
130
  const conflictFiles = await runGitCommand(integrationWorktreePath, ['diff', '--name-only', '--diff-filter=U'], { timeoutMs: 30000 }).catch(() => null);
111
- await runGitCommand(integrationWorktreePath, ['cherry-pick', '--abort'], { timeoutMs: 30000 }).catch(() => null);
131
+ await runGitCommand(integrationWorktreePath, ['merge', '--abort'], { timeoutMs: 30000 }).catch(() => null);
112
132
  return {
113
133
  ok: false,
114
134
  worker_id: checkpoint.worker_id,
115
- strategy: 'merge_then_cherry-pick',
135
+ strategy: 'checkpoint-cherry-pick-then-merge',
116
136
  commit_hash: checkpoint.commit_hash,
117
137
  conflict_files: String(conflictFiles?.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean),
118
138
  blockers: [
119
- `git_worktree_checkpoint_merge_failed:${merge.stderr_tail || merge.stdout_tail}`,
120
- `git_worktree_checkpoint_cherry_pick_failed:${cherryPick.stderr_tail || cherryPick.stdout_tail}`
139
+ `git_worktree_checkpoint_cherry_pick_failed:${cherryPick.stderr_tail || cherryPick.stdout_tail}`,
140
+ `git_worktree_checkpoint_merge_failed:${merge.stderr_tail || merge.stdout_tail}`
121
141
  ]
122
142
  };
123
143
  }
@@ -3,11 +3,14 @@ export function rebalanceNarutoReadyWork(input) {
3
3
  const completed = new Set((input.completedTaskIds || []).map(String));
4
4
  const reclaimed = new Set((input.reclaimedTaskIds || []).map(String));
5
5
  const idle = input.workers.filter((worker) => worker.alive && ['idle', 'done', 'unknown'].includes(worker.state));
6
+ const activeWorkerIds = new Set(input.workers.filter((worker) => worker.alive).map((worker) => worker.id));
7
+ const activeWritePaths = new Set([...(input.activeWritePaths || []), ...(input.currentAssignments || []).flatMap((row) => row.write_paths || [])].map(normalizePath));
6
8
  if (!idle.length)
7
9
  return [];
8
10
  const ready = input.tasks
9
- .filter((task) => (task.status || 'pending') === 'pending' && !task.owner)
11
+ .filter((task) => (task.status || 'pending') === 'pending')
10
12
  .filter((task) => task.dependencies.every((dep) => completed.has(dep)))
13
+ .filter((task) => task.write_paths.every((file) => !activeWritePaths.has(normalizePath(file))))
11
14
  .sort((left, right) => {
12
15
  const reclaimedOrder = Number(!reclaimed.has(left.id)) - Number(!reclaimed.has(right.id));
13
16
  return reclaimedOrder || left.id.localeCompare(right.id);
@@ -15,12 +18,18 @@ export function rebalanceNarutoReadyWork(input) {
15
18
  const decisions = [];
16
19
  const assignments = [...(input.currentAssignments || [])];
17
20
  for (const task of ready) {
18
- const decision = chooseNarutoTaskOwner(task, idle, assignments);
21
+ const requestedOwner = task.owner ? String(task.owner) : '';
22
+ const ownerActive = requestedOwner && activeWorkerIds.has(requestedOwner);
23
+ const ownerIdle = ownerActive ? idle.some((worker) => worker.id === requestedOwner) : false;
24
+ if (requestedOwner && ownerActive && !ownerIdle)
25
+ continue;
26
+ const candidateWorkers = ownerIdle ? idle.filter((worker) => worker.id === requestedOwner) : idle;
27
+ const decision = chooseNarutoTaskOwner({ ...task, owner: null }, candidateWorkers, assignments);
19
28
  decisions.push({
20
29
  type: 'assign',
21
30
  task_id: task.id,
22
31
  worker_id: decision.owner,
23
- reason: `${reclaimed.has(task.id) ? 'reclaimed ready work' : 'idle worker pickup'}; ${decision.reason}`
32
+ reason: `${reclaimed.has(task.id) ? 'reclaimed ready work' : requestedOwner && !ownerActive ? `owner inactive:${requestedOwner}` : 'idle worker pickup'}; ${decision.reason}`
24
33
  });
25
34
  assignments.push({
26
35
  task_id: task.id,
@@ -33,4 +42,7 @@ export function rebalanceNarutoReadyWork(input) {
33
42
  }
34
43
  return decisions;
35
44
  }
45
+ function normalizePath(file) {
46
+ return String(file || '').replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
47
+ }
36
48
  //# sourceMappingURL=naruto-rebalance-policy.js.map
@@ -42,6 +42,12 @@ export function buildNarutoWorkGraph(input = {}) {
42
42
  fallback_reason: writeCapable ? 'git_capability_not_evaluated' : 'readonly_or_write_disabled'
43
43
  };
44
44
  const workItems = [];
45
+ const assignmentById = new Map();
46
+ for (const row of input.allocationAssignments || []) {
47
+ const id = String(row.task_id || row.id || '');
48
+ if (id)
49
+ assignmentById.set(id, row);
50
+ }
45
51
  for (let index = 0; index < totalWorkItems; index += 1) {
46
52
  const id = `NW-${String(index + 1).padStart(6, '0')}`;
47
53
  const kind = kindCycle[index % kindCycle.length] || 'verification';
@@ -53,6 +59,8 @@ export function buildNarutoWorkGraph(input = {}) {
53
59
  ...writePaths.map((file) => ({ path: file, kind: 'write' })),
54
60
  ...readPaths.map((file) => ({ path: file, kind: 'read' }))
55
61
  ];
62
+ const assignment = assignmentById.get(id);
63
+ const allocationHints = assignment?.allocation_hints || assignment?.hints || null;
56
64
  workItems.push({
57
65
  id,
58
66
  kind,
@@ -73,6 +81,11 @@ export function buildNarutoWorkGraph(input = {}) {
73
81
  requires_verification: kind !== 'research' && kind !== 'final_review_input_pack',
74
82
  requires_gpt_final: writePaths.length > 0 || kind === 'final_review_input_pack'
75
83
  },
84
+ owner: assignment?.owner ?? null,
85
+ allocation_reason: assignment?.allocation_reason ?? null,
86
+ allocation_score: assignment?.allocation_score ?? null,
87
+ allocation_hints: allocationHints,
88
+ lane: assignment?.owner ?? null,
76
89
  ...(writePaths.length > 0 ? {
77
90
  worktree: {
78
91
  mode: worktreePolicy.mode,