sneakoscope 2.0.15 → 2.0.16

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.
@@ -165,10 +165,12 @@ async function narutoRun(parsed) {
165
165
  requestedClones: roster.agent_count,
166
166
  totalWorkItems: workGraph.total_work_items,
167
167
  pendingWorkQueueSize: workGraph.total_work_items,
168
- backend: schedulerBackend
168
+ backend: schedulerBackend,
169
+ parallelismMode: parsed.parallelism
169
170
  });
170
171
  const backendMinimum = schedulerBackend === 'fake' ? roster.agent_count : Math.min(roster.agent_count, 2);
171
- const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || Math.max(governor.safe_active_workers, backendMinimum), safe.cap));
172
+ const activeCap = parsed.parallelism === 'safe' ? safe.cap : MAX_NARUTO_AGENT_COUNT;
173
+ const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || Math.max(governor.safe_active_workers, backendMinimum), activeCap));
172
174
  const zellijVisiblePanes = Math.max(1, Math.min(activeSlots, governor.safe_zellij_visible_panes));
173
175
  const activePool = await runNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
174
176
  const runPreRunSmoke = parsed.smoke === true || process.env.SKS_NARUTO_PRE_RUN_SMOKE === '1';
@@ -281,6 +283,20 @@ async function narutoRun(parsed) {
281
283
  prompt: parsed.prompt
282
284
  });
283
285
  let liveZellij = null;
286
+ if (!parsed.json) {
287
+ console.log('$Naruto starting:');
288
+ console.log(' clones requested: ' + roster.agent_count);
289
+ console.log(' work items: ' + workGraph.total_work_items);
290
+ console.log(' target active workers: ' + activeSlots);
291
+ console.log(' visible panes: ' + zellijVisiblePanes);
292
+ console.log(' headless workers: ' + Math.max(0, activeSlots - zellijVisiblePanes));
293
+ console.log(' backend: ' + schedulerBackend);
294
+ console.log(' parallelism mode: ' + parsed.parallelism);
295
+ if (activeSlots < roster.agent_count)
296
+ console.log(' cap reasons: ' + (governor.reasons.join(', ') || 'host safety cap'));
297
+ if (parsed.parallelism !== 'safe' && activeSlots < 10)
298
+ console.log(' warning: active workers below 10 in non-safe mode');
299
+ }
284
300
  if (!parsed.json && !parsed.mock && !parsed.noOpenZellij) {
285
301
  liveZellij = await launchZellijLayout({
286
302
  root,
@@ -356,8 +372,11 @@ async function narutoRun(parsed) {
356
372
  narutoRebalancePolicy: rebalancePolicy,
357
373
  json: parsed.json
358
374
  });
375
+ const parallelRuntime = result.parallel_runtime_proof || null;
359
376
  const nativeProofOk = result.proof?.ok === true || result.proof?.status === 'passed';
360
377
  const finalAccepted = result.proof?.status === 'passed' || result.proof?.gpt_final_status === 'approved';
378
+ const parallelRuntimeOk = !parsed.mock || roster.agent_count < 16 || (parallelRuntime?.passed === true
379
+ && Number(parallelRuntime.max_observed_active_workers || 0) >= Math.min(16, activeSlots));
361
380
  await writeJsonAtomic(path.join(mission.dir, 'naruto-gate.json'), {
362
381
  schema: 'sks.naruto-gate.v1',
363
382
  passed: result.ok === true && nativeProofOk && finalAccepted,
@@ -374,9 +393,10 @@ async function narutoRun(parsed) {
374
393
  gpt_final_pack_ready: true,
375
394
  zellij_dashboard_ready: zellijDashboard.ok === true,
376
395
  native_agent_proof: nativeProofOk,
396
+ parallel_runtime_proof: parallelRuntimeOk,
377
397
  final_arbiter_accepted: finalAccepted,
378
398
  session_cleanup: result.proof?.all_sessions_closed === true || nativeProofOk,
379
- blockers: result.proof?.blockers || [],
399
+ blockers: [...(result.proof?.blockers || []), ...(parallelRuntimeOk ? [] : ['naruto_parallel_runtime_proof_below_gate'])],
380
400
  updated_at: nowIso()
381
401
  });
382
402
  await setCurrent(root, {
@@ -442,6 +462,15 @@ async function narutoRun(parsed) {
442
462
  worker_lifecycle_sample: realActivePoolSmoke.worker_lifecycle.slice(0, 5)
443
463
  }
444
464
  },
465
+ parallel_runtime: parallelRuntime ? {
466
+ proof_path: path.join(result.ledger_root || '', 'parallel-runtime-proof.json'),
467
+ max_observed_active_workers: parallelRuntime.max_observed_active_workers,
468
+ unique_worker_pids: parallelRuntime.unique_worker_pids,
469
+ speedup_ratio: parallelRuntime.speedup_ratio,
470
+ visible_panes: parallelRuntime.visible_panes,
471
+ headless_workers: parallelRuntime.headless_workers,
472
+ passed: parallelRuntime.passed
473
+ } : null,
445
474
  local_worker: localWorkerSummary,
446
475
  proof: result.proof?.status || 'missing',
447
476
  run: compactNarutoRunResult(result),
@@ -455,6 +484,13 @@ async function narutoRun(parsed) {
455
484
  console.log('Backend: ' + result.backend);
456
485
  console.log('Roles: ' + roleDistribution.entries.map((entry) => `${entry.role}:${entry.count}`).join(', '));
457
486
  console.log('Proof: ' + summary.proof);
487
+ if (summary.parallel_runtime) {
488
+ console.log('$Naruto parallel proof:');
489
+ console.log(' max active workers: ' + summary.parallel_runtime.max_observed_active_workers);
490
+ console.log(' unique PIDs: ' + summary.parallel_runtime.unique_worker_pids);
491
+ console.log(' speedup: ' + summary.parallel_runtime.speedup_ratio + 'x');
492
+ console.log(' result: ' + (summary.parallel_runtime.passed ? 'passed' : 'blocked'));
493
+ }
458
494
  if (summary.zellij?.ok && summary.zellij.capability?.status === 'ok')
459
495
  console.log('Zellij: prepared ' + zellijVisiblePanes + ' visible active clone lane(s) in ' + summary.zellij.session_name + '; dashboard tracks ' + Math.max(0, activeSlots - zellijVisiblePanes) + ' headless active worker(s)');
460
496
  else if (summary.zellij?.ok)
@@ -716,9 +752,16 @@ function parseNarutoArgs(args = []) {
716
752
  const noOpenZellij = hasFlag(args, '--no-open-zellij') || hasFlag(args, '--no-zellij');
717
753
  const attach = hasFlag(args, '--attach');
718
754
  const smoke = hasFlag(args, '--smoke');
719
- 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']);
755
+ const parallelism = normalizeParallelism(readOption(args, '--parallelism', 'extreme'));
756
+ 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', '--parallelism']);
720
757
  const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
721
- return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, json, missionId, noOpenZellij, attach, smoke };
758
+ return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, json, missionId, noOpenZellij, attach, smoke, parallelism };
759
+ }
760
+ function normalizeParallelism(value) {
761
+ const text = String(value || 'extreme').toLowerCase();
762
+ if (text === 'safe' || text === 'balanced' || text === 'extreme')
763
+ return text;
764
+ return 'extreme';
722
765
  }
723
766
  async function writeNarutoArtifacts(ledgerRoot, artifacts) {
724
767
  await writeJsonAtomic(path.join(ledgerRoot, 'naruto-work-graph.json'), artifacts.workGraph);
@@ -25,182 +25,6 @@ export async function team(args = []) {
25
25
  if (teamSubcommands.has(args[0]))
26
26
  return teamCommand(args[0], args.slice(1));
27
27
  return redirectTeamCreateToNaruto(args);
28
- const jsonOutput = flag(args, '--json');
29
- const mock = flag(args, '--mock');
30
- const openZellij = !mock && !jsonOutput && !flag(args, '--no-open-zellij') && !flag(args, '--no-zellij');
31
- const useOllama = flag(args, '--ollama') || flag(args, '--local-model');
32
- const noOllama = flag(args, '--no-ollama') || flag(args, '--no-local-model');
33
- const ollamaModel = readFlagValue(args, '--ollama-model', readFlagValue(args, '--local-model-model', '')) || null;
34
- const ollamaBaseUrl = readFlagValue(args, '--ollama-base-url', readFlagValue(args, '--local-model-base-url', '')) || null;
35
- const cleanCreateArgs = stripTeamCreateControlArgs(args);
36
- const opts = parseTeamCreateArgs(cleanCreateArgs);
37
- const { prompt, agentSessions, roleCounts, roster } = opts;
38
- const targetActiveSlots = readBoundedIntegerFlag(args, '--target-active-slots', roster.bundle_size, 1, 20);
39
- const visualLaneCount = roster.bundle_size;
40
- const desiredWorkItemCount = readBoundedIntegerFlag(args, '--work-items', targetActiveSlots, 1, 200);
41
- const minimumWorkItems = readBoundedIntegerFlag(args, '--minimum-work-items', targetActiveSlots, 1, 200);
42
- const maxQueueExpansion = readBoundedIntegerFlag(args, '--max-queue-expansion', 10, 0, 200);
43
- const profile = readFlagValue(args, '--profile', '') || null;
44
- const writeMode = readFlagValue(args, '--write-mode', flag(args, '--parallel-write') ? 'parallel' : 'off');
45
- const applyPatches = flag(args, '--apply-patches');
46
- const dryRunPatches = flag(args, '--dry-run-patches') || flag(args, '--dryrun-patches');
47
- const maxWriteAgents = readBoundedIntegerFlag(args, '--max-write-agents', Math.min(roster.bundle_size, 5), 1, 20);
48
- if (!prompt) {
49
- console.error('Usage: sks team "task" [20:agents] [executor:5 reviewer:6 user:1] [--agents N] [--work-items N] [--target-active-slots N] [--profile NAME] [--write-mode off|proof-safe|parallel|serial] [--apply-patches] [--ollama|--no-ollama] [--no-open-zellij] [--json] [--mock]');
50
- process.exitCode = 1;
51
- return;
52
- }
53
- const root = await sksRoot();
54
- if (!(await exists(path.join(root, '.sneakoscope'))))
55
- await initProject(root, {});
56
- const { id, dir } = await createMission(root, { mode: 'team', prompt });
57
- const schema = buildQuestionSchema(prompt);
58
- await writeQuestions(dir, schema);
59
- const plan = buildTeamPlan(id, prompt, { agentSessions, roleCounts, roster, targetActiveSlots });
60
- await writeJsonAtomic(path.join(dir, 'team-plan.json'), plan);
61
- await writeJsonAtomic(path.join(dir, SSOT_GUARD_ARTIFACT), plan.ssot_guard);
62
- await writeTextAtomic(path.join(dir, 'team-workflow.md'), teamWorkflowMarkdown(plan));
63
- const liveFiles = await initTeamLive(id, dir, prompt, { agentSessions, roleCounts, roster });
64
- await writeJsonAtomic(path.join(dir, 'team-roster.json'), { schema_version: 1, mission_id: id, role_counts: roleCounts, agent_sessions: agentSessions, bundle_size: roster.bundle_size, roster, confirmed: true, source: 'default_or_prompt_team_spec' });
65
- const fromChatImgRequired = hasFromChatImgSignal(prompt);
66
- const teamReasoning = teamReasoningPolicy(prompt, roster);
67
- const promptEffort = teamReasoning.prompt_policy?.effort || 'medium';
68
- const runtime = await writeTeamRuntimeArtifacts(dir, plan, {});
69
- const effortDecision = await writeEffortDecision(dir, {
70
- mission_id: id,
71
- task_id: 'TEAM-INTAKE',
72
- route: fromChatImgRequired ? 'from-chat-img' : 'team',
73
- prompt,
74
- tool_use: promptEffort === 'medium',
75
- multi_step_decision: promptEffort !== 'low',
76
- spans_many_files: promptEffort === 'high' || promptEffort === 'xhigh',
77
- is_deterministic: promptEffort === 'low',
78
- has_verified_skill: true,
79
- high_risk: promptEffort === 'high' || promptEffort === 'xhigh',
80
- risk_scores: {
81
- security: /security|auth|permission|database|supabase|sql|보안|권한|데이터베이스/i.test(prompt) ? 0.8 : 0.1,
82
- destructive_action: /delete|drop|reset|remove|삭제|초기화/i.test(prompt) ? 0.8 : 0.1,
83
- user_impact: /release|publish|deploy|commit|push|production|배포|커밋|푸쉬|운영/i.test(prompt) ? 0.8 : 0.3
84
- }
85
- });
86
- const workOrder = createWorkOrderLedger({ missionId: id, route: fromChatImgRequired ? 'from-chat-img' : 'team', sourcesComplete: !fromChatImgRequired, requests: [{ verbatim: prompt, normalized_requirement: prompt, implementation_tasks: ['TASK-001'], status: 'pending' }] });
87
- await writeWorkOrderLedger(dir, workOrder);
88
- if (fromChatImgRequired)
89
- await writeFromChatImgArtifacts(dir, { missionId: id, requests: [{ verbatim: prompt }], ambiguities: ['image source inventory must be completed before implementation'] });
90
- let liveZellij = null;
91
- if (!mock && openZellij) {
92
- liveZellij = await launchTeamZellijView({ root, missionId: id, ledgerRoot: path.join(dir, 'agents'), slotCount: visualLaneCount, dryRun: false, attach: false });
93
- if (liveZellij?.ok && liveZellij.capability?.status === 'ok')
94
- console.log(`Zellij: prepared ${visualLaneCount} native agent lane(s) in ${liveZellij.session_name}. Attach with: ${liveZellij.attach_command_with_env || liveZellij.attach_command}`);
95
- else if (liveZellij?.ok)
96
- console.log(`Zellij: optional live panes unavailable (${(liveZellij.warnings || []).join('; ') || liveZellij.capability?.status || 'unknown'}).`);
97
- else
98
- console.log(`Zellij: blocked (${Array.from(new Set(liveZellij?.blockers || [])).join('; ')})`);
99
- }
100
- const nativeAgentRun = await runNativeAgentOrchestrator({
101
- root,
102
- missionId: id,
103
- route: '$Team',
104
- prompt,
105
- backend: mock ? 'fake' : 'codex-sdk',
106
- mock,
107
- agents: roster.bundle_size,
108
- targetActiveSlots,
109
- visualLaneCount,
110
- desiredWorkItemCount,
111
- minimumWorkItems,
112
- maxQueueExpansion,
113
- concurrency: Math.min(agentSessions, roster.bundle_size),
114
- readonly: !applyPatches && writeMode === 'off',
115
- profile,
116
- writeMode: writeMode,
117
- applyPatches,
118
- dryRunPatches,
119
- maxWriteAgents,
120
- ollamaEnabled: useOllama && !noOllama,
121
- noOllama,
122
- ollamaModel,
123
- ollamaBaseUrl,
124
- routeCommand: 'sks team',
125
- routeBlackboxKind: 'actual_team_command'
126
- });
127
- await appendTeamEvent(dir, {
128
- agent: 'native_agent_orchestrator',
129
- phase: 'native_agent_intake',
130
- type: nativeAgentRun.ok ? 'complete' : 'blocked',
131
- artifact: 'agents/agent-proof-evidence.json',
132
- message: 'Native agent orchestrator completed with ' + nativeAgentRun.backend + ' backend; proof ' + (nativeAgentRun.proof?.status || 'unknown') + '.'
133
- });
134
- let dashboardState = await writeTeamDashboardState(dir, { missionId: id, mission: { id, mode: 'team' }, effort: effortDecision.selected_effort, phase: 'intake', next_action: fromChatImgRequired ? 'complete visual source inventory and work-order mapping' : 'run Team native agent intake agents' });
135
- await writeJsonAtomic(path.join(dir, 'team-gate.json'), { passed: false, team_roster_confirmed: true, native_agent_proof: nativeAgentRun.proof?.ok === true, agent_central_ledger: true, analysis_artifact: false, triwiki_refreshed: false, triwiki_validated: false, ssot_guard: false, consensus_artifact: false, ...runtime.gate_fields, implementation_team_fresh: false, review_artifact: false, integration_evidence: false, session_cleanup: false, context7_evidence: false, ...(fromChatImgRequired ? { from_chat_img_required: true, from_chat_img_request_coverage: false } : {}) });
136
- dashboardState = await writeTeamDashboardState(dir, { missionId: id, mission: { id, mode: 'team' }, effort: effortDecision.selected_effort, phase: 'intake', next_action: fromChatImgRequired ? 'complete visual source inventory and work-order mapping' : 'run Team native agent intake agents' });
137
- const route = routePrompt(`$Team ${prompt}`) || ROUTES.find((candidate) => candidate.id === 'Team');
138
- const routeReason = routeReasoning(route, prompt);
139
- const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task: prompt, required: false, ambiguity: { required: false, status: 'team_cli_direct' } });
140
- await setCurrent(root, { mission_id: id, route: 'Team', route_command: '$Team', mode: 'TEAM', phase: mock ? 'TEAM_FIXTURE_DONE' : 'TEAM_NATIVE_AGENT_INTAKE', questions_allowed: false, implementation_allowed: true, context7_required: false, context7_verified: mock, subagents_required: false, subagents_verified: true, native_sessions_required: true, native_sessions_verified: nativeAgentRun.proof?.ok === true, reflection_required: true, visible_progress_required: true, context_tracking: 'triwiki', required_skills: route?.requiredSkills || ['team'], stop_gate: 'team-gate.json', reasoning_effort: routeReason.effort, reasoning_profile: routeReason.profile, reasoning_temporary: true, team_agent_reasoning_policy: teamReasoning, goal_continuation: pipelinePlan.goal_continuation, agent_sessions: agentSessions, target_active_slots: targetActiveSlots, role_counts: roleCounts, team_roster_confirmed: true, team_graph_ready: runtime.ok, team_live_ready: true, from_chat_img_required: fromChatImgRequired, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT, native_agent_backend: nativeAgentRun.backend, native_agent_proof: 'agents/agent-proof-evidence.json', prompt });
141
- const result = {
142
- mission_id: id,
143
- mission_dir: dir,
144
- plan: path.join(dir, 'team-plan.json'),
145
- workflow: path.join(dir, 'team-workflow.md'),
146
- team_graph: path.join(dir, TEAM_GRAPH_ARTIFACT),
147
- runtime_tasks: path.join(dir, TEAM_RUNTIME_TASKS_ARTIFACT),
148
- decomposition_report: path.join(dir, TEAM_DECOMPOSITION_ARTIFACT),
149
- worker_inbox_dir: path.join(dir, TEAM_INBOX_DIR),
150
- live: liveFiles.live,
151
- transcript: liveFiles.transcript,
152
- dashboard: liveFiles.dashboard,
153
- dashboard_state: path.join(dir, ARTIFACT_FILES.team_dashboard_state),
154
- effort_decision: path.join(dir, ARTIFACT_FILES.effort_decision),
155
- work_order_ledger: path.join(dir, ARTIFACT_FILES.work_order_ledger),
156
- pipeline_plan: path.join(dir, PIPELINE_PLAN_ARTIFACT),
157
- dashboard_state_valid: dashboardState.ok,
158
- context_pack: path.join(root, '.sneakoscope', 'wiki', 'context-pack.json'),
159
- agent_sessions: agentSessions,
160
- bundle_size: roster.bundle_size,
161
- target_active_slots: targetActiveSlots,
162
- visual_lane_count: visualLaneCount,
163
- desired_work_items: desiredWorkItemCount,
164
- minimum_work_items: minimumWorkItems,
165
- max_queue_expansion: maxQueueExpansion,
166
- role_counts: roleCounts,
167
- questions: path.join(dir, 'questions.md'),
168
- native_agent_run: nativeAgentRun,
169
- codex_agents: ['native_agent_orchestrator', 'agent_central_ledger', 'agent_proof_evidence', 'agent_review_lane', 'agent_integration_lane']
170
- };
171
- if (mock) {
172
- await writeTextAtomic(path.join(dir, 'team-analysis.md'), `# Team Native Agent Analysis\n\nMock Team fixture completed native agent intake for ${id}.\n`);
173
- await writeTextAtomic(path.join(dir, 'team-consensus.md'), `# Team Consensus\n\nMock Team fixture consensus reached for ${id}.\n`);
174
- await writeTextAtomic(path.join(dir, 'team-review.md'), `# Team Review\n\nMock Team fixture review completed with ${MIN_TEAM_REVIEWER_LANES} validation lanes for ${id}.\n`);
175
- await writeTextAtomic(path.join(dir, 'context7-evidence.jsonl'), `${JSON.stringify({ schema: 'sks.context7-evidence.v1', mission_id: id, route: '$Team', status: 'mock_not_required', generated_at: nowIso() })}\n`);
176
- const cleanup = { schema_version: 1, mission_id: id, status: 'clean', passed: true, all_sessions_closed: true, outstanding_sessions: 0, live_transcript_finalized: true, mock: true, generated_at: nowIso() };
177
- await writeJsonAtomic(path.join(dir, TEAM_SESSION_CLEANUP_ARTIFACT), cleanup);
178
- const gate = { passed: true, team_roster_confirmed: true, native_agent_proof: nativeAgentRun.proof?.ok === true, agent_central_ledger: true, analysis_artifact: true, triwiki_refreshed: true, triwiki_validated: true, ssot_guard: true, consensus_artifact: true, ...runtime.gate_fields, implementation_team_fresh: true, review_artifact: true, review_lanes: MIN_TEAM_REVIEWER_LANES, integration_evidence: true, session_cleanup: true, context7_evidence: true, mock: true };
179
- await writeJsonAtomic(path.join(dir, 'team-gate.json'), gate);
180
- const proof = await maybeFinalizeRoute(root, { missionId: id, route: '$Team', gateFile: 'team-gate.json', gate, mock: true, statusHint: 'verified_partial', artifacts: ['agents/agent-proof-evidence.json', SSOT_GUARD_ARTIFACT, 'team-gate.json', TEAM_SESSION_CLEANUP_ARTIFACT, 'team-plan.json', 'team-runtime-tasks.json', 'completion-proof.json'], claims: [{ id: 'team-fixture-complete', status: 'verified_partial' }], command: { cmd: `sks team "${prompt}" --mock`, status: 0 } });
181
- result.mock = true;
182
- result.proof = proof.validation;
183
- }
184
- else {
185
- result.zellij = liveZellij || await launchTeamZellijView({ root, missionId: id, ledgerRoot: path.join(dir, 'agents'), slotCount: visualLaneCount, dryRun: jsonOutput || !openZellij, attach: false });
186
- if (openZellij && result.zellij?.ok && result.zellij.capability?.status === 'ok' && shouldAutoAttachTeamZellij(args)) {
187
- attachZellijSessionInteractive(result.zellij.session_name, { cwd: root, configPath: result.zellij.clipboard_config_path });
188
- }
189
- }
190
- if (jsonOutput)
191
- return console.log(JSON.stringify(result, null, 2));
192
- console.log(`Team mission created: ${id}`);
193
- console.log(`Agent sessions: ${agentSessions}`);
194
- console.log(`Role counts: ${formatRoleCounts(roleCounts)}`);
195
- console.log(`Review policy: minimum ${MIN_TEAM_REVIEWER_LANES} reviewer/QA validation lanes`);
196
- if (result.zellij?.ok && result.zellij.capability?.status === 'ok')
197
- console.log(`Zellij: prepared ${visualLaneCount} native agent lane(s) in ${result.zellij.session_name}`);
198
- else if (result.zellij?.ok)
199
- console.log(`Zellij: optional live panes unavailable (${(result.zellij.warnings || []).join('; ') || result.zellij.capability?.status || 'unknown'})`);
200
- else if (!mock)
201
- console.log(`Zellij: blocked (${Array.from(new Set(result.zellij?.blockers || [])).join('; ')})`);
202
- console.log(`Watch: sks team watch ${id}`);
203
- console.log(`Artifacts: .sneakoscope/missions/${id}`);
204
28
  }
205
29
  async function redirectTeamCreateToNaruto(args = []) {
206
30
  const root = await sksRoot();
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
- import { exists, readJson, writeJsonAtomic, readText, nowIso, appendJsonlBounded } from './fsx.js';
2
+ import { exists, readJson, writeJsonAtomic, readText, nowIso, appendJsonlBounded, sha256 } from './fsx.js';
3
3
  import { missionDir, setCurrent } from './mission.js';
4
4
  import { evaluateMadSksPermissionGate, isMadSksRouteState } from './permission-gates.js';
5
5
  import { resolveMadDbMutationPolicy } from './mad-db/mad-db-policy-resolver.js';
6
- import { consumeMadDbCapability } from './mad-db/mad-db-capability.js';
7
- import { appendMadDbLedgerEvent } from './mad-db/mad-db-ledger.js';
6
+ import { recordMadDbOperation } from './mad-db/mad-db-capability.js';
7
+ import { appendMadDbLedgerEvent, appendMadDbOperationLifecycle } from './mad-db/mad-db-ledger.js';
8
8
  export const DEFAULT_DB_SAFETY_POLICY = Object.freeze({
9
9
  schema_version: 1,
10
10
  mode: 'read_only_default',
@@ -510,6 +510,16 @@ export async function checkDbOperation(root, state, payload, { duringNoQuestion
510
510
  const madDb = await resolveMadDbMutationPolicy(root, state, classification);
511
511
  if (madDb.allowed === true && state?.mission_id) {
512
512
  const madDbDecision = madDb;
513
+ const operationId = `mad-db-op-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
514
+ const sqlHash = classification.sql?.statements?.length ? sha256(String(classification.sql.statements.join('\n'))) : null;
515
+ await appendMadDbOperationLifecycle(root, state.mission_id, {
516
+ type: 'db_operation.started',
517
+ operationId,
518
+ cycleId: madDbDecision.cycle_id,
519
+ toolName: classification.toolName || null,
520
+ sqlHash,
521
+ destructive: classification.level === 'destructive'
522
+ });
513
523
  const decision = {
514
524
  allowed: true,
515
525
  action: 'allow',
@@ -522,11 +532,29 @@ export async function checkDbOperation(root, state, payload, { duringNoQuestion
522
532
  one_cycle_only: true,
523
533
  cycle_id: madDbDecision.cycle_id,
524
534
  capability_file: 'mad-db-capability.json',
525
- consumed: true
535
+ consumed: false,
536
+ operation_id: operationId,
537
+ operation_count: Number(madDbDecision.operation_count || 0) + 1,
538
+ max_operations: madDbDecision.max_operations || 20
526
539
  }
527
540
  };
528
- await appendMadDbLedgerEvent(root, state.mission_id, { type: 'db_mutation.allowed', cycle_id: madDbDecision.cycle_id, mode: madDbDecision.mode, classification });
529
- await consumeMadDbCapability(root, state.mission_id, { consumedBy: 'db-safety-checkDbOperation', reason: 'db_mutation_allowed' });
541
+ await appendMadDbLedgerEvent(root, state.mission_id, { type: 'db_mutation.allowed', cycle_id: madDbDecision.cycle_id, mode: madDbDecision.mode, classification, operation_id: operationId });
542
+ await appendMadDbOperationLifecycle(root, state.mission_id, {
543
+ type: 'db_operation.allowed',
544
+ operationId,
545
+ cycleId: madDbDecision.cycle_id,
546
+ toolName: classification.toolName || null,
547
+ sqlHash,
548
+ destructive: classification.level === 'destructive',
549
+ resultStatus: 'unknown_pending_tool_result'
550
+ });
551
+ const updatedCapability = await recordMadDbOperation(root, state.mission_id, {
552
+ operationId,
553
+ ...(classification.toolName ? { toolName: classification.toolName } : {}),
554
+ ...(sqlHash ? { sqlHash } : {})
555
+ });
556
+ decision.mad_db.consumed = updatedCapability?.consumed === true;
557
+ decision.mad_db.operation_count = updatedCapability?.operation_count ?? decision.mad_db.operation_count;
530
558
  await appendJsonlBounded(path.join(missionDir(root, state.mission_id), 'db-safety.jsonl'), { ts: nowIso(), decision });
531
559
  return decision;
532
560
  }
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.15';
8
+ export const PACKAGE_VERSION = '2.0.16';
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() {
@@ -4,6 +4,7 @@ import { gitBlocker, runGitCommand } from './git-worktree-runner.js';
4
4
  import { resolveGitWorktreeRoot } from './git-worktree-root.js';
5
5
  export async function evaluateGitWorktreeCapability(input = {}) {
6
6
  const requireGitWorktree = input.requireGitWorktree === true || process.env.SKS_REQUIRE_GIT_WORKTREE === '1';
7
+ const disabledByEnv = process.env.SKS_DISABLE_GIT_WORKTREE === '1';
7
8
  const detection = await detectGitRepo(input.root || process.cwd());
8
9
  const blockers = [...detection.blockers];
9
10
  const gitAvailable = Boolean(detection.git_binary);
@@ -24,6 +25,23 @@ export async function evaluateGitWorktreeCapability(input = {}) {
24
25
  blockers
25
26
  };
26
27
  }
28
+ if (disabledByEnv) {
29
+ if (requireGitWorktree)
30
+ blockers.push('git_worktree_disabled_by_env');
31
+ return {
32
+ schema: 'sks.git-worktree-capability.v1',
33
+ ok: blockers.length === 0,
34
+ mode: 'patch-envelope-only',
35
+ require_git_worktree: requireGitWorktree,
36
+ git_available: gitAvailable,
37
+ is_git_repo: true,
38
+ worktree_supported: false,
39
+ worktree_probe_attempted: false,
40
+ detection,
41
+ root_resolution: null,
42
+ blockers: [...new Set(blockers)]
43
+ };
44
+ }
27
45
  const rootResolution = resolveGitWorktreeRoot({
28
46
  repoRoot: detection.root,
29
47
  missionId: input.missionId || 'capability'
@@ -4,7 +4,21 @@ import { ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
4
4
  import { evaluateGitWorktreeCapability } from './git-worktree-capability.js';
5
5
  import { gitBlocker, gitOutputLine, runGitCommand } from './git-worktree-runner.js';
6
6
  import { sanitizePathPart } from './git-worktree-root.js';
7
+ import { appendParallelRuntimeEvent } from '../agents/parallel-runtime-proof.js';
7
8
  export async function allocateWorkerWorktree(input) {
9
+ const eventRoot = input.repoRoot || process.cwd();
10
+ const writeProofEvents = await shouldWriteWorktreeAllocationProof(eventRoot, input.missionId);
11
+ if (writeProofEvents)
12
+ await appendParallelRuntimeEvent(eventRoot, input.missionId, {
13
+ event_type: 'worktree_allocation_started',
14
+ slot_id: input.slotId || input.workerId || null,
15
+ generation_index: input.generationIndex || 1,
16
+ session_id: null,
17
+ pid: null,
18
+ backend: 'git-worktree',
19
+ placement: 'unknown',
20
+ worktree_id: input.workerId || null
21
+ }).catch(() => undefined);
8
22
  const capability = await evaluateGitWorktreeCapability({
9
23
  root: input.repoRoot || process.cwd(),
10
24
  missionId: input.missionId,
@@ -64,8 +78,63 @@ export async function allocateWorkerWorktree(input) {
64
78
  blockers: [...new Set(blockers)]
65
79
  };
66
80
  await appendWorktreeManifest(allocation);
81
+ if (writeProofEvents)
82
+ await appendParallelRuntimeEvent(eventRoot, input.missionId, {
83
+ event_type: 'worktree_allocation_completed',
84
+ slot_id: allocation.slot_id,
85
+ generation_index: allocation.generation_index,
86
+ session_id: null,
87
+ pid: null,
88
+ backend: 'git-worktree',
89
+ placement: 'unknown',
90
+ worktree_id: `${allocation.slot_id}-gen-${allocation.generation_index}`,
91
+ meta: {
92
+ ok: allocation.ok,
93
+ worktree_path: allocation.worktree_path,
94
+ branch: allocation.branch,
95
+ blockers: allocation.blockers
96
+ }
97
+ }).catch(() => undefined);
67
98
  return allocation;
68
99
  }
100
+ export async function allocateWorkerWorktreesBatch(input) {
101
+ const maxParallel = Math.max(1, Math.floor(Number(input.maxParallel || 1)));
102
+ const queue = uniqueWorktreeAllocationInputs(input.workers);
103
+ const allocations = [];
104
+ const workers = Array.from({ length: Math.min(maxParallel, queue.length) }, async () => {
105
+ while (queue.length) {
106
+ const next = queue.shift();
107
+ if (!next)
108
+ continue;
109
+ allocations.push(await allocateWorkerWorktree({
110
+ repoRoot: input.root,
111
+ missionId: input.missionId,
112
+ workerId: next.workerId,
113
+ ...(next.slotId === undefined ? {} : { slotId: next.slotId }),
114
+ ...(next.generationIndex === undefined ? {} : { generationIndex: next.generationIndex }),
115
+ ...(next.baseRef === undefined ? {} : { baseRef: next.baseRef }),
116
+ ...(next.branchPrefix === undefined ? {} : { branchPrefix: next.branchPrefix })
117
+ }));
118
+ }
119
+ });
120
+ await Promise.all(workers);
121
+ return allocations;
122
+ }
123
+ function uniqueWorktreeAllocationInputs(workers = []) {
124
+ const seen = new Set();
125
+ const unique = [];
126
+ for (const worker of workers) {
127
+ const workerId = sanitizePathPart(worker.workerId || 'worker');
128
+ const slotId = sanitizePathPart(worker.slotId || workerId);
129
+ const generationIndex = Math.max(1, Math.floor(Number(worker.generationIndex || 1)));
130
+ const key = `${workerId}:${slotId}:${generationIndex}`;
131
+ if (seen.has(key))
132
+ continue;
133
+ seen.add(key);
134
+ unique.push({ ...worker, workerId, slotId, generationIndex });
135
+ }
136
+ return unique;
137
+ }
69
138
  async function appendWorktreeManifest(allocation) {
70
139
  const current = await readJson(allocation.manifest_path, null).catch(() => null);
71
140
  const allocations = Array.isArray(current?.allocations) ? current.allocations : [];
@@ -84,6 +153,17 @@ async function appendWorktreeManifest(allocation) {
84
153
  };
85
154
  await writeJsonAtomic(allocation.manifest_path, manifest);
86
155
  }
156
+ async function shouldWriteWorktreeAllocationProof(root, missionId) {
157
+ if (process.env.SKS_GIT_WORKTREE_ALLOCATION_PROOF === '0')
158
+ return false;
159
+ try {
160
+ const stat = await fsp.stat(path.join(root, '.sneakoscope', 'missions', missionId, 'agents'));
161
+ return stat.isDirectory();
162
+ }
163
+ catch {
164
+ return false;
165
+ }
166
+ }
87
167
  function sanitizeBranchPart(value) {
88
168
  return sanitizePathPart(value).replace(/\./g, '-').slice(0, 48) || 'item';
89
169
  }
@@ -1,4 +1,5 @@
1
1
  import { nowIso } from '../fsx.js';
2
+ import { allocateWorkerWorktreesBatch as allocateBatch } from './git-worktree-manager.js';
2
3
  export function planGitWorktreePool(input) {
3
4
  const reusable = [...(input.reusableWorktrees || [])];
4
5
  const assignments = input.workerIds.map((workerId) => {
@@ -20,4 +21,7 @@ export function planGitWorktreePool(input) {
20
21
  blockers: []
21
22
  };
22
23
  }
24
+ export async function allocateWorkerWorktreesBatch(input) {
25
+ return allocateBatch(input);
26
+ }
23
27
  //# sourceMappingURL=git-worktree-pool.js.map
@@ -27,7 +27,9 @@ export async function createMadDbCapability(root, input) {
27
27
  },
28
28
  consumed: false,
29
29
  consumed_at: null,
30
- consumed_by: null
30
+ consumed_by: null,
31
+ max_operations: Math.max(1, Math.floor(Number(process.env.SKS_MAD_DB_MAX_OPERATIONS || 20))),
32
+ operation_count: 0
31
33
  };
32
34
  const dir = missionDir(root, input.missionId);
33
35
  await writeJsonAtomic(path.join(dir, MAD_DB_CAPABILITY_FILE), capability);
@@ -52,9 +54,39 @@ export function isMadDbCapabilityActive(capability, nowMs = Date.now()) {
52
54
  return capability.enabled === true
53
55
  && capability.consumed !== true
54
56
  && capability.one_cycle_only === true
57
+ && Number(capability.operation_count || 0) < Number(capability.max_operations || 20)
55
58
  && Number.isFinite(expires)
56
59
  && expires > nowMs;
57
60
  }
61
+ export async function recordMadDbOperation(root, missionId, input = {}) {
62
+ const capability = await readMadDbCapability(root, missionId);
63
+ if (!isMadDbCapabilityActive(capability))
64
+ return capability;
65
+ const operationCount = Number(capability.operation_count || 0) + 1;
66
+ const maxOperations = Math.max(1, Number(capability.max_operations || 20));
67
+ const updated = {
68
+ ...capability,
69
+ operation_count: operationCount,
70
+ max_operations: maxOperations
71
+ };
72
+ const dir = missionDir(root, missionId);
73
+ await writeJsonAtomic(path.join(dir, MAD_DB_CAPABILITY_FILE), updated);
74
+ await appendJsonlBounded(path.join(dir, 'mad-db-ledger.jsonl'), {
75
+ ts: nowIso(),
76
+ type: 'db_operation.counted',
77
+ mission_id: missionId,
78
+ cycle_id: updated.cycle_id,
79
+ operation_id: input.operationId || null,
80
+ tool_name: input.toolName || null,
81
+ sql_hash: input.sqlHash || null,
82
+ operation_count: operationCount,
83
+ max_operations: maxOperations
84
+ });
85
+ if (operationCount >= maxOperations) {
86
+ return consumeMadDbCapability(root, missionId, { consumedBy: 'db-safety-checkDbOperation', reason: 'mad_db_max_operations_reached' });
87
+ }
88
+ return updated;
89
+ }
58
90
  export async function consumeMadDbCapability(root, missionId, input = {}) {
59
91
  const capability = await readMadDbCapability(root, missionId);
60
92
  if (!isMadDbCapabilityActive(capability))
@@ -14,4 +14,18 @@ export async function appendMadDbLedgerEvent(root, missionId, event) {
14
14
  await writeJsonAtomic(path.join(dir, 'mad-db-ledger.latest.json'), row).catch(() => undefined);
15
15
  return row;
16
16
  }
17
+ export async function appendMadDbOperationLifecycle(root, missionId, input) {
18
+ return appendMadDbLedgerEvent(root, missionId, {
19
+ type: input.type,
20
+ operation_id: input.operationId,
21
+ cycle_id: input.cycleId || null,
22
+ mcp_server: input.mcpServer || null,
23
+ tool_name: input.toolName || null,
24
+ sql_hash: input.sqlHash || null,
25
+ destructive: input.destructive === true,
26
+ result_status: input.resultStatus || 'unknown_pending_tool_result',
27
+ row_count: input.rowCount ?? null,
28
+ error: input.error || null
29
+ });
30
+ }
17
31
  //# sourceMappingURL=mad-db-ledger.js.map
@@ -20,6 +20,8 @@ export async function resolveMadDbMutationPolicy(root, state = {}, classificatio
20
20
  audit_required: true,
21
21
  mission_id: missionId,
22
22
  cycle_id: capability.cycle_id,
23
+ operation_count: capability.operation_count || 0,
24
+ max_operations: capability.max_operations || 20,
23
25
  capability
24
26
  };
25
27
  }
@@ -9,6 +9,7 @@ export function decideNarutoConcurrency(input = {}) {
9
9
  const hardware = probeHardwareCapacity(input.hardware || {});
10
10
  const zellijVisiblePaneCap = normalizePositiveInt(input.zellijVisiblePaneCap, Math.min(8, Math.max(4, Math.floor(hardware.terminal_rows / 5))));
11
11
  const backend = String(input.backend || 'codex-sdk');
12
+ const parallelismMode = normalizeParallelismMode(input.parallelismMode);
12
13
  const freeGb = hardware.free_memory_bytes / (1024 * 1024 * 1024);
13
14
  const totalGb = hardware.total_memory_bytes / (1024 * 1024 * 1024);
14
15
  const reclaimableFloorGb = totalGb >= 32 ? 16 : totalGb >= 16 ? 8 : totalGb >= 8 ? 4 : Math.max(1, freeGb);
@@ -33,7 +34,12 @@ export function decideNarutoConcurrency(input = {}) {
33
34
  const rawSafe = Math.max(1, Math.min(requestedClones, totalWorkItems, memoryCap, fdCap, cpuCap + ioCap, gitWorktreeCap + processCap, backendBudget, queueCap, leaseCap, 100));
34
35
  const pressure = monitorNarutoResourcePressure(hardware, { activeWorkers: rawSafe, zellijVisiblePaneCap });
35
36
  const backpressure = applyNarutoBackpressure(rawSafe, pressure);
36
- const safeActiveWorkers = Math.max(1, Math.min(rawSafe, backpressure.adjusted_active_workers));
37
+ const currentSafeActiveWorkers = Math.max(1, Math.min(rawSafe, backpressure.adjusted_active_workers));
38
+ const safeActiveWorkers = parallelismMode === 'extreme'
39
+ ? rawSafe
40
+ : parallelismMode === 'balanced'
41
+ ? Math.max(1, Math.min(rawSafe, Math.max(16, Math.floor(rawSafe * 0.75))))
42
+ : currentSafeActiveWorkers;
37
43
  const safeVisible = Math.min(safeActiveWorkers, zellijVisiblePaneCap);
38
44
  const reasons = [
39
45
  ...(memoryCap < requestedClones ? ['memory_cap'] : []),
@@ -60,11 +66,18 @@ export function decideNarutoConcurrency(input = {}) {
60
66
  git_worktree_parallel: gitWorktreeCap,
61
67
  cpu_io_parallel: cpuCap + ioCap,
62
68
  verification_parallel: Math.max(1, Math.min(hardware.cpu_core_count * 2, safeActiveWorkers, 16)),
69
+ parallelism_mode: parallelismMode,
63
70
  reasons: [...new Set(reasons)],
64
71
  backpressure: backpressure.backpressure,
65
72
  hardware
66
73
  };
67
74
  }
75
+ function normalizeParallelismMode(value) {
76
+ const text = String(value || process.env.SKS_NARUTO_PARALLELISM || 'extreme').toLowerCase();
77
+ if (text === 'safe' || text === 'balanced' || text === 'extreme')
78
+ return text;
79
+ return 'extreme';
80
+ }
68
81
  function normalizePositiveInt(value, fallback) {
69
82
  const parsed = Number(value);
70
83
  if (!Number.isFinite(parsed) || parsed < 1)
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '2.0.15';
1
+ export const PACKAGE_VERSION = '2.0.16';
2
2
  //# sourceMappingURL=version.js.map