sneakoscope 2.0.11 → 2.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- 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/build-manifest.json +11 -8
- package/dist/core/agents/agent-orchestrator.js +279 -1
- package/dist/core/agents/agent-scheduler.js +12 -1
- package/dist/core/agents/agent-slot-pane-binding-proof.js +3 -3
- package/dist/core/agents/agent-work-queue.js +26 -2
- package/dist/core/agents/agent-worker-pipeline.js +2 -0
- package/dist/core/agents/native-cli-session-swarm.js +2 -2
- package/dist/core/commands/naruto-command.js +104 -51
- package/dist/core/fsx.js +1 -1
- package/dist/core/git/git-worktree-merge-queue.js +34 -14
- package/dist/core/naruto/naruto-rebalance-policy.js +15 -3
- package/dist/core/naruto/naruto-work-graph.js +13 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-slot-column-anchor.js +163 -4
- package/dist/core/zellij/zellij-worker-pane-manager.js +13 -7
- package/dist/scripts/agent-real-codex-in-zellij-worker-pane-check.js +8 -2
- package/dist/scripts/agent-slot-pane-binding-proof-check.js +4 -4
- package/dist/scripts/codex-sdk-release-review-pipeline-check.js +2 -1
- package/dist/scripts/codex-sdk-zellij-pane-binding-check.js +2 -2
- package/dist/scripts/git-worktree-cross-rebase-check.js +13 -1
- package/dist/scripts/git-worktree-merge-queue-check.js +1 -0
- package/dist/scripts/local-collab-worktree-gpt-final-apply-policy-check.js +63 -0
- package/dist/scripts/naruto-actual-worker-control-plane-check.js +30 -3
- package/dist/scripts/naruto-allocation-runtime-wiring-check.js +92 -0
- package/dist/scripts/naruto-orchestrator-runtime-source-check.js +65 -6
- package/dist/scripts/naruto-rebalance-policy-check.js +15 -2
- package/dist/scripts/naruto-shadow-clone-swarm-check.js +1 -1
- package/dist/scripts/release-dag-full-coverage-check.js +4 -0
- package/dist/scripts/release-real-check.js +258 -77
- package/dist/scripts/zellij-first-slot-down-stack-check.js +1 -1
- package/dist/scripts/zellij-first-slot-down-stack-real-check.js +344 -4
- package/dist/scripts/zellij-right-column-manager-check.js +1 -1
- package/dist/scripts/zellij-slot-column-anchor-check.js +23 -2
- package/dist/scripts/zellij-slot-only-ui-check.js +3 -1
- package/dist/scripts/zellij-slot-renderer-proof-semantics-check.js +59 -0
- package/dist/scripts/zellij-worker-pane-manager-check.js +23 -1
- package/dist/scripts/zellij-worker-pane-real-ui-blackbox.js +21 -4
- package/package.json +5 -2
|
@@ -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
|
|
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,
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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:
|
|
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);
|
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.
|
|
8
|
+
export const PACKAGE_VERSION = '2.0.12';
|
|
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
|
-
|
|
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
|
|
103
|
+
const cherryPick = await runGitCommand(integrationWorktreePath, ['cherry-pick', '--allow-empty', '-X', 'theirs', checkpoint.commit_hash || ''], {
|
|
84
104
|
timeoutMs: 120000
|
|
85
105
|
});
|
|
86
|
-
if (
|
|
106
|
+
if (cherryPick.ok) {
|
|
87
107
|
return {
|
|
88
108
|
ok: true,
|
|
89
109
|
worker_id: checkpoint.worker_id,
|
|
90
|
-
strategy: '
|
|
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, ['
|
|
97
|
-
const
|
|
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 (
|
|
120
|
+
if (merge.ok) {
|
|
101
121
|
return {
|
|
102
122
|
ok: true,
|
|
103
123
|
worker_id: checkpoint.worker_id,
|
|
104
|
-
strategy: '
|
|
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, ['
|
|
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: '
|
|
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
|
-
`
|
|
120
|
-
`
|
|
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'
|
|
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
|
|
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,
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '2.0.
|
|
1
|
+
export const PACKAGE_VERSION = '2.0.12';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -5,18 +5,32 @@ export function renderZellijSlotColumnAnchor(input = {}) {
|
|
|
5
5
|
const visible = Math.max(1, nonNegativeInt(input.visiblePaneCap, active || 1));
|
|
6
6
|
const headless = nonNegativeInt(input.headlessWorkers, 0);
|
|
7
7
|
const queue = nonNegativeInt(input.queueDepth, 0);
|
|
8
|
-
|
|
8
|
+
const header = `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}`;
|
|
9
|
+
const workers = Array.isArray(input.workerRows) ? input.workerRows : [];
|
|
10
|
+
if (!workers.length)
|
|
11
|
+
return header;
|
|
12
|
+
const maxRows = Math.max(1, nonNegativeInt(input.maxWorkerRows, input.mode === 'full-debug' ? 24 : 12));
|
|
13
|
+
const visibleRows = workers.slice(0, maxRows);
|
|
14
|
+
const hidden = Math.max(0, workers.length - visibleRows.length);
|
|
15
|
+
return [
|
|
16
|
+
header,
|
|
17
|
+
...visibleRows.map((row, index) => renderWorkerRow(row, index + 1)),
|
|
18
|
+
...(hidden ? [`+${hidden} more worker${hidden === 1 ? '' : 's'}`] : [])
|
|
19
|
+
].join('\n');
|
|
9
20
|
}
|
|
10
21
|
export async function renderZellijSlotColumnAnchorFromArtifacts(input) {
|
|
11
22
|
const root = path.resolve(input.artifactRoot);
|
|
12
23
|
const missionDir = inferMissionDir(root, input.missionId);
|
|
13
24
|
const snapshot = await readJson(path.join(missionDir, 'zellij-dashboard-snapshot.json'));
|
|
14
25
|
const rightColumn = await readJson(path.join(missionDir, 'zellij-right-column-state.json'));
|
|
15
|
-
const
|
|
26
|
+
const swarm = await readJson(path.join(root, 'agent-native-cli-session-swarm.json'))
|
|
27
|
+
|| await readJson(path.join(missionDir, 'agents', 'agent-native-cli-session-swarm.json'));
|
|
28
|
+
const workerRows = await buildWorkerRows(root, missionDir, rightColumn, swarm);
|
|
29
|
+
const activeWorkers = Number(snapshot?.active_workers ?? workerRows.filter((row) => row.status === 'running' || row.status === 'launching').length ?? 0);
|
|
16
30
|
const visiblePaneCap = Number(snapshot?.visible_panes ?? Math.max(1, rightColumn?.visible_worker_panes?.length || activeWorkers || 1));
|
|
17
|
-
const headlessWorkers = Number(snapshot?.headless_workers ??
|
|
31
|
+
const headlessWorkers = Number(snapshot?.headless_workers ?? workerRows.filter((row) => row.placement === 'headless' && (!row.status || row.status === 'running')).length ?? 0);
|
|
18
32
|
const queueDepth = Number(snapshot?.queue_depth ?? 0);
|
|
19
|
-
const anchorInput = { activeWorkers, visiblePaneCap, headlessWorkers, queueDepth };
|
|
33
|
+
const anchorInput = { activeWorkers, visiblePaneCap, headlessWorkers, queueDepth, workerRows };
|
|
20
34
|
if (input.mode !== undefined)
|
|
21
35
|
anchorInput.mode = input.mode;
|
|
22
36
|
return renderZellijSlotColumnAnchor(anchorInput);
|
|
@@ -47,12 +61,157 @@ async function readJson(file) {
|
|
|
47
61
|
return null;
|
|
48
62
|
}
|
|
49
63
|
}
|
|
64
|
+
async function buildWorkerRows(root, missionDir, rightColumn, swarm) {
|
|
65
|
+
const byKey = new Map();
|
|
66
|
+
const records = Array.isArray(swarm?.records) ? swarm.records : [];
|
|
67
|
+
const recordByKey = new Map();
|
|
68
|
+
for (const record of records) {
|
|
69
|
+
const key = workerKey(record?.slot_id, record?.generation_index);
|
|
70
|
+
if (key)
|
|
71
|
+
recordByKey.set(key, record);
|
|
72
|
+
}
|
|
73
|
+
for (const pane of Array.isArray(rightColumn?.visible_worker_panes) ? rightColumn.visible_worker_panes : []) {
|
|
74
|
+
const key = workerKey(pane?.slot_id, pane?.generation_index);
|
|
75
|
+
if (!key)
|
|
76
|
+
continue;
|
|
77
|
+
const record = recordByKey.get(key);
|
|
78
|
+
byKey.set(key, await hydrateWorkerRow(root, missionDir, {
|
|
79
|
+
slotId: String(pane.slot_id),
|
|
80
|
+
generationIndex: Number(pane.generation_index || 1),
|
|
81
|
+
placement: 'zellij-pane',
|
|
82
|
+
status: pane.status || record?.status || 'running',
|
|
83
|
+
paneId: pane.pane_id || record?.zellij_pane_id || null,
|
|
84
|
+
yOrder: Number(pane.y_order || 0)
|
|
85
|
+
}, record));
|
|
86
|
+
}
|
|
87
|
+
for (const row of Array.isArray(rightColumn?.headless_workers) ? rightColumn.headless_workers : []) {
|
|
88
|
+
const key = workerKey(row?.slot_id, row?.generation_index);
|
|
89
|
+
if (!key)
|
|
90
|
+
continue;
|
|
91
|
+
const record = recordByKey.get(key);
|
|
92
|
+
byKey.set(key, await hydrateWorkerRow(root, missionDir, {
|
|
93
|
+
slotId: String(row.slot_id),
|
|
94
|
+
generationIndex: Number(row.generation_index || 1),
|
|
95
|
+
placement: 'headless',
|
|
96
|
+
status: row.status || record?.status || 'running',
|
|
97
|
+
reason: row.reason || record?.headless_reason || null,
|
|
98
|
+
yOrder: 9000
|
|
99
|
+
}, record));
|
|
100
|
+
}
|
|
101
|
+
for (const record of records) {
|
|
102
|
+
const key = workerKey(record?.slot_id, record?.generation_index);
|
|
103
|
+
if (!key || byKey.has(key))
|
|
104
|
+
continue;
|
|
105
|
+
byKey.set(key, await hydrateWorkerRow(root, missionDir, {
|
|
106
|
+
slotId: String(record.slot_id || record.agent_id || 'slot-?'),
|
|
107
|
+
generationIndex: Number(record.generation_index || 1),
|
|
108
|
+
placement: record.worker_placement || (record.zellij_pane_id ? 'zellij-pane' : 'process'),
|
|
109
|
+
status: record.status || 'running',
|
|
110
|
+
paneId: record.zellij_pane_id || null,
|
|
111
|
+
yOrder: 5000
|
|
112
|
+
}, record));
|
|
113
|
+
}
|
|
114
|
+
return [...byKey.values()].sort((a, b) => {
|
|
115
|
+
const statusDelta = statusWeight(a.status) - statusWeight(b.status);
|
|
116
|
+
if (statusDelta)
|
|
117
|
+
return statusDelta;
|
|
118
|
+
const yDelta = Number(a.yOrder || 0) - Number(b.yOrder || 0);
|
|
119
|
+
if (yDelta)
|
|
120
|
+
return yDelta;
|
|
121
|
+
return String(a.slotId).localeCompare(String(b.slotId));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function hydrateWorkerRow(root, missionDir, base, record) {
|
|
125
|
+
const artifactDir = resolveArtifactDir(root, missionDir, record?.worker_artifact_dir);
|
|
126
|
+
const result = artifactDir ? await readJson(path.join(artifactDir, 'worker-result.json')) : null;
|
|
127
|
+
const intake = artifactDir ? await readJson(path.join(artifactDir, 'worker-intake.json')) : null;
|
|
128
|
+
const heartbeatPath = artifactDir ? path.join(artifactDir, 'worker-heartbeat.jsonl') : null;
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
status: result?.status || base.status || record?.status || 'running',
|
|
132
|
+
backend: result?.backend || record?.backend || intake?.backend || null,
|
|
133
|
+
role: result?.persona_id || intake?.agent?.naruto_role || intake?.agent?.role || intake?.agent?.persona_id || null,
|
|
134
|
+
task: firstText([
|
|
135
|
+
result?.summary,
|
|
136
|
+
Array.isArray(result?.changed_files) ? result.changed_files[0] : null,
|
|
137
|
+
intake?.slice?.description,
|
|
138
|
+
intake?.slice?.title,
|
|
139
|
+
intake?.slice?.id,
|
|
140
|
+
base.reason
|
|
141
|
+
]),
|
|
142
|
+
worktreeId: result?.worktree?.id || record?.worktree?.id || intake?.worktree?.id || null,
|
|
143
|
+
heartbeatAgeMs: heartbeatPath ? await heartbeatAgeMs(heartbeatPath) : null
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function renderWorkerRow(row, index) {
|
|
147
|
+
const slot = `${trimInline(row.slotId || 'slot-?', 12)} g${Math.max(1, Math.floor(Number(row.generationIndex) || 1))}`;
|
|
148
|
+
const status = trimInline(row.status || 'running', 9);
|
|
149
|
+
const backend = trimInline(row.backend || row.placement || '-', 12);
|
|
150
|
+
const worktree = trimInline(row.worktreeId || row.role || '-', 10);
|
|
151
|
+
const task = trimInline(row.task || row.reason || '-', 38);
|
|
152
|
+
return `${String(index).padStart(2, '0')} ${slot} ${status} ${backend} ${worktree} · ${task} · hb ${formatHeartbeat(row.heartbeatAgeMs)}`;
|
|
153
|
+
}
|
|
154
|
+
function resolveArtifactDir(root, missionDir, value) {
|
|
155
|
+
if (!value)
|
|
156
|
+
return null;
|
|
157
|
+
const text = String(value);
|
|
158
|
+
if (path.isAbsolute(text))
|
|
159
|
+
return text;
|
|
160
|
+
return path.join(root, text);
|
|
161
|
+
}
|
|
162
|
+
async function heartbeatAgeMs(file) {
|
|
163
|
+
try {
|
|
164
|
+
return Date.now() - (await fs.promises.stat(file)).mtimeMs;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function formatHeartbeat(ageMs) {
|
|
171
|
+
if (ageMs == null)
|
|
172
|
+
return '?';
|
|
173
|
+
if (ageMs < 1000)
|
|
174
|
+
return 'now';
|
|
175
|
+
return `${Math.max(1, Math.round(ageMs / 1000))}s`;
|
|
176
|
+
}
|
|
177
|
+
function firstText(values) {
|
|
178
|
+
for (const value of values) {
|
|
179
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
180
|
+
if (text)
|
|
181
|
+
return text;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function workerKey(slotId, generationIndex) {
|
|
186
|
+
const slot = String(slotId || '').trim();
|
|
187
|
+
if (!slot)
|
|
188
|
+
return null;
|
|
189
|
+
return `${slot}:g${Math.max(1, Math.floor(Number(generationIndex) || 1))}`;
|
|
190
|
+
}
|
|
191
|
+
function statusWeight(status) {
|
|
192
|
+
const text = String(status || '').toLowerCase();
|
|
193
|
+
if (text === 'running' || text === 'launching')
|
|
194
|
+
return 0;
|
|
195
|
+
if (text === 'failed')
|
|
196
|
+
return 1;
|
|
197
|
+
if (text === 'draining')
|
|
198
|
+
return 2;
|
|
199
|
+
if (text === 'closed')
|
|
200
|
+
return 3;
|
|
201
|
+
return 4;
|
|
202
|
+
}
|
|
50
203
|
function nonNegativeInt(value, fallback) {
|
|
51
204
|
const parsed = Number(value);
|
|
52
205
|
if (!Number.isFinite(parsed) || parsed < 0)
|
|
53
206
|
return fallback;
|
|
54
207
|
return Math.floor(parsed);
|
|
55
208
|
}
|
|
209
|
+
function trimInline(value, max) {
|
|
210
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
211
|
+
if (text.length <= max)
|
|
212
|
+
return text;
|
|
213
|
+
return text.slice(0, Math.max(1, max - 3)) + '...';
|
|
214
|
+
}
|
|
56
215
|
function shellQuote(value) {
|
|
57
216
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
58
217
|
}
|