sneakoscope 2.0.10 → 2.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/build-manifest.json +24 -8
- package/dist/cli/command-registry.js +1 -0
- package/dist/cli/install-helpers.js +8 -20
- package/dist/commands/doctor.js +5 -9
- package/dist/commands/zellij-slot-column-anchor.js +23 -0
- package/dist/core/agents/agent-orchestrator.js +59 -11
- package/dist/core/agents/agent-patch-schema.js +8 -1
- package/dist/core/commands/naruto-command.js +118 -19
- package/dist/core/fsx.js +1 -1
- package/dist/core/git/git-worktree-checkpoint.js +52 -0
- package/dist/core/git/git-worktree-cross-rebase.js +54 -0
- package/dist/core/git/git-worktree-merge-queue.js +69 -0
- package/dist/core/git/git-worktree-patch-envelope.js +8 -1
- package/dist/core/init.js +2 -2
- package/dist/core/naruto/naruto-allocation-policy.js +99 -0
- package/dist/core/naruto/naruto-real-worker-child.js +110 -11
- package/dist/core/naruto/naruto-rebalance-policy.js +36 -0
- package/dist/core/naruto/naruto-task-hints.js +71 -0
- package/dist/core/pipeline/finalize-pipeline-result.js +3 -1
- package/dist/core/pipeline/gpt-final-required.js +22 -2
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-right-column-manager.js +45 -2
- package/dist/core/zellij/zellij-slot-column-anchor.js +59 -0
- package/dist/core/zellij/zellij-worker-pane-manager.js +70 -9
- package/dist/scripts/git-worktree-checkpoint-check.js +20 -0
- package/dist/scripts/git-worktree-cross-rebase-check.js +27 -0
- package/dist/scripts/naruto-actual-worker-control-plane-check.js +29 -0
- package/dist/scripts/naruto-allocation-policy-check.js +33 -0
- package/dist/scripts/naruto-extreme-parallelism-real-check.js +5 -4
- package/dist/scripts/naruto-orchestrator-runtime-source-check.js +11 -0
- package/dist/scripts/naruto-real-active-pool-runtime-check.js +4 -2
- package/dist/scripts/naruto-rebalance-policy-check.js +28 -0
- package/dist/scripts/naruto-shadow-clone-swarm-check.js +7 -3
- package/dist/scripts/release-dag-full-coverage-check.js +15 -1
- package/dist/scripts/release-readiness-report.js +1 -1
- package/dist/scripts/zellij-first-slot-down-stack-check.js +20 -0
- package/dist/scripts/zellij-first-slot-down-stack-real-check.js +16 -0
- package/dist/scripts/zellij-right-column-manager-check.js +7 -2
- package/dist/scripts/zellij-slot-column-anchor-check.js +24 -0
- package/dist/scripts/zellij-slot-only-ui-check.js +4 -2
- package/dist/scripts/zellij-worker-pane-manager-single-owner-check.js +11 -4
- package/package.json +12 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { createMission, findLatestMission, loadMission } from '../mission.js';
|
|
3
|
-
import { readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { nowIso, readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
|
|
4
4
|
import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
|
|
5
5
|
import { classifyOllamaWorkerSlice } from '../agents/agent-runner-ollama.js';
|
|
6
6
|
import { buildNarutoCloneRoster, systemSafeNarutoConcurrency } from '../agents/agent-roster.js';
|
|
@@ -12,6 +12,8 @@ import { buildNarutoRoleDistribution } from '../naruto/naruto-role-policy.js';
|
|
|
12
12
|
import { decideNarutoConcurrency } from '../naruto/naruto-concurrency-governor.js';
|
|
13
13
|
import { runNarutoActivePool, runNarutoRealActivePool } from '../naruto/naruto-active-pool.js';
|
|
14
14
|
import { collectActualNarutoWorker, spawnActualNarutoWorker } from '../naruto/naruto-real-worker-runtime.js';
|
|
15
|
+
import { allocateNarutoTasksToWorkers } from '../naruto/naruto-allocation-policy.js';
|
|
16
|
+
import { rebalanceNarutoReadyWork } from '../naruto/naruto-rebalance-policy.js';
|
|
15
17
|
import { buildNarutoVerificationDag } from '../naruto/naruto-verification-dag.js';
|
|
16
18
|
import { buildNarutoGptFinalPack } from '../naruto/naruto-gpt-final-pack.js';
|
|
17
19
|
import { planNarutoZellijDashboard } from '../zellij/zellij-naruto-dashboard.js';
|
|
@@ -103,6 +105,50 @@ async function narutoRun(parsed) {
|
|
|
103
105
|
worktreePolicy
|
|
104
106
|
});
|
|
105
107
|
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
|
+
const allocationPolicy = {
|
|
111
|
+
schema: 'sks.naruto-allocation-policy.v1',
|
|
112
|
+
generated_at: nowIso(),
|
|
113
|
+
ok: allocationWorkers.length > 0 && allocationAssignments.length === workGraph.work_items.length,
|
|
114
|
+
scoring_model: {
|
|
115
|
+
same_primary_role: 18,
|
|
116
|
+
declared_role: 12,
|
|
117
|
+
same_path_lane: 12,
|
|
118
|
+
overlap_each: 4,
|
|
119
|
+
assigned_task_penalty_each: -4,
|
|
120
|
+
write_conflict_penalty: -20,
|
|
121
|
+
dependency_incomplete: '-Infinity'
|
|
122
|
+
},
|
|
123
|
+
workers: allocationWorkers,
|
|
124
|
+
assignments: allocationAssignments.map((row) => ({
|
|
125
|
+
task_id: row.id,
|
|
126
|
+
owner: row.owner,
|
|
127
|
+
score: Number.isFinite(row.allocation_score) ? row.allocation_score : '-Infinity',
|
|
128
|
+
reason: row.allocation_reason,
|
|
129
|
+
role: row.required_role,
|
|
130
|
+
kind: row.kind,
|
|
131
|
+
paths: row.hints.paths,
|
|
132
|
+
domains: row.hints.domains,
|
|
133
|
+
write_paths: row.hints.writePaths
|
|
134
|
+
})),
|
|
135
|
+
blockers: allocationWorkers.length ? [] : ['naruto_allocation_workers_missing']
|
|
136
|
+
};
|
|
137
|
+
const rebalanceDecisions = rebalanceNarutoReadyWork({
|
|
138
|
+
tasks: workGraph.work_items.map((item) => ({ ...item, owner: null, status: 'pending' })),
|
|
139
|
+
workers: allocationWorkers.map((worker) => ({ ...worker, alive: true, state: 'idle' })),
|
|
140
|
+
completedTaskIds: [],
|
|
141
|
+
reclaimedTaskIds: []
|
|
142
|
+
});
|
|
143
|
+
const rebalancePolicy = {
|
|
144
|
+
schema: 'sks.naruto-rebalance-policy.v1',
|
|
145
|
+
generated_at: nowIso(),
|
|
146
|
+
ok: true,
|
|
147
|
+
trigger: 'idle_worker_ready_queue',
|
|
148
|
+
decisions: rebalanceDecisions,
|
|
149
|
+
blocked_by_dependency_count: workGraph.work_items.filter((item) => item.dependencies.length > 0).length,
|
|
150
|
+
blockers: []
|
|
151
|
+
};
|
|
106
152
|
const governor = decideNarutoConcurrency({
|
|
107
153
|
requestedClones: roster.agent_count,
|
|
108
154
|
totalWorkItems: workGraph.total_work_items,
|
|
@@ -113,24 +159,38 @@ async function narutoRun(parsed) {
|
|
|
113
159
|
const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || Math.max(governor.safe_active_workers, backendMinimum), safe.cap));
|
|
114
160
|
const zellijVisiblePanes = Math.max(1, Math.min(activeSlots, governor.safe_zellij_visible_panes));
|
|
115
161
|
const activePool = await runNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
|
|
116
|
-
const
|
|
117
|
-
|
|
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: {
|
|
118
171
|
mode: 'patch-envelope-only',
|
|
119
172
|
required: false,
|
|
120
173
|
main_repo_root: worktreePolicy.main_repo_root,
|
|
121
174
|
worktree_root: null,
|
|
122
|
-
fallback_reason: '
|
|
175
|
+
fallback_reason: 'pre_run_smoke_never_owns_production_runtime'
|
|
123
176
|
}
|
|
124
|
-
|
|
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
|
+
};
|
|
125
185
|
const realActivePool = await runNarutoRealActivePool({
|
|
126
|
-
graph:
|
|
127
|
-
governor: { ...governor, safe_active_workers: activeSlots },
|
|
186
|
+
graph: realRuntimeSmokeGraph,
|
|
187
|
+
governor: { ...governor, safe_active_workers: Math.min(2, activeSlots), safe_zellij_visible_panes: Math.min(1, zellijVisiblePanes) },
|
|
128
188
|
spawnWorker: async (item, placement) => spawnActualNarutoWorker({
|
|
129
189
|
root,
|
|
130
190
|
missionId: mission.id,
|
|
131
191
|
item,
|
|
132
192
|
placement,
|
|
133
|
-
backend:
|
|
193
|
+
backend: 'fake',
|
|
134
194
|
worktreePolicy: realRuntimeWorktreePolicy,
|
|
135
195
|
zellijSessionName: `sks-${mission.id}`,
|
|
136
196
|
visiblePaneCap: zellijVisiblePanes
|
|
@@ -139,6 +199,12 @@ async function narutoRun(parsed) {
|
|
|
139
199
|
enqueueVerification: async () => undefined,
|
|
140
200
|
updateDashboard: async () => undefined
|
|
141
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
|
+
};
|
|
142
208
|
const verificationDag = buildNarutoVerificationDag(workGraph, { cwd: root });
|
|
143
209
|
const gptFinalPack = buildNarutoGptFinalPack({
|
|
144
210
|
missionId: mission.id,
|
|
@@ -162,7 +228,9 @@ async function narutoRun(parsed) {
|
|
|
162
228
|
roleDistribution,
|
|
163
229
|
governor,
|
|
164
230
|
activePool,
|
|
165
|
-
realActivePool,
|
|
231
|
+
realActivePool: realActivePoolSmoke,
|
|
232
|
+
allocationPolicy,
|
|
233
|
+
rebalancePolicy,
|
|
166
234
|
verificationDag,
|
|
167
235
|
gptFinalPack,
|
|
168
236
|
zellijDashboard,
|
|
@@ -254,6 +322,8 @@ async function narutoRun(parsed) {
|
|
|
254
322
|
max_clones: MAX_NARUTO_AGENT_COUNT,
|
|
255
323
|
concurrency: result.target_active_slots ?? activeSlots,
|
|
256
324
|
target_active_slots: result.target_active_slots ?? activeSlots,
|
|
325
|
+
runtime_source_of_truth: 'agent-orchestrator-scheduler',
|
|
326
|
+
pre_run_real_active_pool_source: 'smoke_only',
|
|
257
327
|
concurrency_capped: clones > (result.target_active_slots ?? activeSlots),
|
|
258
328
|
system: { cores: safe.cores, free_gb: safe.free_gb, safe_concurrency: safe.cap, heavy_backend: safe.heavy },
|
|
259
329
|
work_graph: {
|
|
@@ -267,6 +337,8 @@ async function narutoRun(parsed) {
|
|
|
267
337
|
},
|
|
268
338
|
git_worktree: gitWorktreeCapability,
|
|
269
339
|
role_distribution: roleDistribution,
|
|
340
|
+
allocation_policy: allocationPolicy,
|
|
341
|
+
rebalance_policy: rebalancePolicy,
|
|
270
342
|
concurrency_governor: governor,
|
|
271
343
|
active_pool: {
|
|
272
344
|
ok: activePool.ok,
|
|
@@ -274,16 +346,18 @@ async function narutoRun(parsed) {
|
|
|
274
346
|
refill_events: activePool.refill_events,
|
|
275
347
|
completed_count: activePool.completed_count,
|
|
276
348
|
real_runtime: {
|
|
277
|
-
ok:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
349
|
+
ok: realActivePoolSmoke.ok,
|
|
350
|
+
runtime_source_of_truth: realActivePoolSmoke.runtime_source_of_truth,
|
|
351
|
+
production_runtime_source_of_truth: realActivePoolSmoke.production_runtime_source_of_truth,
|
|
352
|
+
active_cap: realActivePoolSmoke.active_cap,
|
|
353
|
+
max_observed_active_workers: realActivePoolSmoke.max_observed_active_workers,
|
|
354
|
+
average_active_workers: realActivePoolSmoke.average_active_workers,
|
|
355
|
+
active_pool_utilization: realActivePoolSmoke.active_pool_utilization,
|
|
356
|
+
refill_latency_ms_p95: realActivePoolSmoke.refill_latency_ms_p95,
|
|
357
|
+
visible_workers: realActivePoolSmoke.visible_workers,
|
|
358
|
+
headless_workers: realActivePoolSmoke.headless_workers,
|
|
359
|
+
worker_lifecycle_count: realActivePoolSmoke.worker_lifecycle.length,
|
|
360
|
+
worker_lifecycle_sample: realActivePoolSmoke.worker_lifecycle.slice(0, 5)
|
|
287
361
|
}
|
|
288
362
|
},
|
|
289
363
|
local_worker: localWorkerSummary,
|
|
@@ -335,6 +409,27 @@ function compactNarutoRunResult(result) {
|
|
|
335
409
|
}
|
|
336
410
|
};
|
|
337
411
|
}
|
|
412
|
+
function buildNarutoAllocationWorkers(workGraph, roleDistribution, roster) {
|
|
413
|
+
const workItems = Array.isArray(workGraph?.work_items) ? workGraph.work_items : [];
|
|
414
|
+
const roleByWorkItem = new Map((roleDistribution?.work_item_roles || []).map((row) => [String(row.work_item_id), String(row.role || '')]));
|
|
415
|
+
const rosterRows = Array.isArray(roster?.roster) ? roster.roster : [];
|
|
416
|
+
const count = Math.max(1, Math.min(Number(roster?.agent_count || rosterRows.length || workItems.length || 1), Math.max(1, workItems.length || 1)));
|
|
417
|
+
return Array.from({ length: count }, (_unused, index) => {
|
|
418
|
+
const agent = rosterRows[index] || {};
|
|
419
|
+
const item = workItems[index % Math.max(1, workItems.length)] || {};
|
|
420
|
+
const role = String(agent.naruto_role || agent.role || roleByWorkItem.get(String(item.id || '')) || item.required_role || 'worker');
|
|
421
|
+
return {
|
|
422
|
+
id: String(agent.id || `clone-${String(index + 1).padStart(3, '0')}`),
|
|
423
|
+
role,
|
|
424
|
+
lane: narutoAllocationLane(item)
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
function narutoAllocationLane(item) {
|
|
429
|
+
const firstPath = String((item?.write_paths || item?.target_paths || item?.readonly_paths || [])[0] || '');
|
|
430
|
+
const parts = firstPath.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
431
|
+
return parts.slice(0, Math.min(2, parts.length)).join('/') || null;
|
|
432
|
+
}
|
|
338
433
|
function summarizeNarutoLocalWorkerResult(localWorker, result) {
|
|
339
434
|
const backendCounts = {};
|
|
340
435
|
const rows = Array.isArray(result?.results) ? result.results : [];
|
|
@@ -498,6 +593,10 @@ async function writeNarutoArtifacts(ledgerRoot, artifacts) {
|
|
|
498
593
|
await writeJsonAtomic(path.join(ledgerRoot, 'naruto-active-pool.json'), artifacts.activePool);
|
|
499
594
|
if (artifacts.realActivePool)
|
|
500
595
|
await writeJsonAtomic(path.join(ledgerRoot, 'naruto-real-active-pool.json'), artifacts.realActivePool);
|
|
596
|
+
if (artifacts.allocationPolicy)
|
|
597
|
+
await writeJsonAtomic(path.join(ledgerRoot, 'naruto-allocation-policy.json'), artifacts.allocationPolicy);
|
|
598
|
+
if (artifacts.rebalancePolicy)
|
|
599
|
+
await writeJsonAtomic(path.join(ledgerRoot, 'naruto-rebalance-policy.json'), artifacts.rebalancePolicy);
|
|
501
600
|
await writeJsonAtomic(path.join(ledgerRoot, 'naruto-verification-dag.json'), artifacts.verificationDag);
|
|
502
601
|
await writeJsonAtomic(path.join(ledgerRoot, 'naruto-gpt-final-pack.json'), artifacts.gptFinalPack);
|
|
503
602
|
await writeJsonAtomic(path.join(ledgerRoot, 'naruto-zellij-dashboard.json'), artifacts.zellijDashboard);
|
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.11';
|
|
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() {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { nowIso } from '../fsx.js';
|
|
2
|
+
import { gitBlocker, gitOutputLine, runGitCommand } from './git-worktree-runner.js';
|
|
3
|
+
export async function checkpointWorkerWorktree(input) {
|
|
4
|
+
const status = await runGitCommand(input.worktreePath, ['status', '--porcelain=v1', '--untracked-files=all']);
|
|
5
|
+
const names = await runGitCommand(input.worktreePath, ['diff', '--name-only', 'HEAD']);
|
|
6
|
+
const untracked = await runGitCommand(input.worktreePath, ['ls-files', '--others', '--exclude-standard']);
|
|
7
|
+
const changedFiles = [...new Set([...lines(names.stdout), ...lines(untracked.stdout), ...statusFiles(status.stdout)])];
|
|
8
|
+
const blockers = [...(status.ok ? [] : [gitBlocker('git_worktree_status_failed', status)])];
|
|
9
|
+
const requested = input.mode || 'auto';
|
|
10
|
+
const commitMode = requested === 'checkpoint-commit' || (requested === 'auto' && changedFiles.length > 1);
|
|
11
|
+
if (!changedFiles.length || blockers.length) {
|
|
12
|
+
return report(input, requested, 'noop', null, changedFiles, blockers);
|
|
13
|
+
}
|
|
14
|
+
if (!commitMode)
|
|
15
|
+
return report(input, requested, 'diff-envelope', null, changedFiles, blockers);
|
|
16
|
+
const add = await runGitCommand(input.worktreePath, ['add', '-A'], { timeoutMs: 30000 });
|
|
17
|
+
if (!add.ok)
|
|
18
|
+
blockers.push(gitBlocker('git_worktree_checkpoint_add_failed', add));
|
|
19
|
+
const commit = blockers.length ? null : await runGitCommand(input.worktreePath, ['commit', '--no-verify', '-m', `sks(worker): checkpoint ${input.workerId}/${input.taskId}`], { timeoutMs: 120000 });
|
|
20
|
+
if (commit && !commit.ok)
|
|
21
|
+
blockers.push(gitBlocker('git_worktree_checkpoint_commit_failed', commit));
|
|
22
|
+
const head = blockers.length ? null : await runGitCommand(input.worktreePath, ['rev-parse', 'HEAD']);
|
|
23
|
+
const hash = head?.ok ? gitOutputLine(head) : null;
|
|
24
|
+
return report(input, requested, blockers.length ? 'noop' : 'checkpoint-commit', hash, changedFiles, blockers);
|
|
25
|
+
}
|
|
26
|
+
function report(input, mode, applied, commitHash, changedFiles, blockers) {
|
|
27
|
+
return {
|
|
28
|
+
schema: 'sks.git-worktree-checkpoint.v1',
|
|
29
|
+
ok: blockers.length === 0,
|
|
30
|
+
generated_at: nowIso(),
|
|
31
|
+
worktree_path: input.worktreePath,
|
|
32
|
+
repo_root: input.repoRoot,
|
|
33
|
+
worker_id: input.workerId,
|
|
34
|
+
task_id: input.taskId,
|
|
35
|
+
mode_requested: mode,
|
|
36
|
+
mode_applied: applied,
|
|
37
|
+
commit_hash: commitHash,
|
|
38
|
+
changed_files: changedFiles,
|
|
39
|
+
blockers
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function lines(text) {
|
|
43
|
+
return String(text || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
function statusFiles(text) {
|
|
46
|
+
return lines(text).map((line) => {
|
|
47
|
+
const match = line.match(/^.{2}\s+(.*)$/) || line.match(/^\S+\s+(.*)$/);
|
|
48
|
+
const file = (match?.[1] || line).trim();
|
|
49
|
+
return file.includes(' -> ') ? file.split(' -> ').pop()?.trim() || file : file;
|
|
50
|
+
}).filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=git-worktree-checkpoint.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { nowIso } from '../fsx.js';
|
|
2
|
+
import { gitBlocker, gitOutputLine, runGitCommand } from './git-worktree-runner.js';
|
|
3
|
+
export async function crossRebaseIdleWorktrees(input) {
|
|
4
|
+
const records = [];
|
|
5
|
+
for (const worker of input.workers) {
|
|
6
|
+
const before = await runGitCommand(worker.worktree_path, ['rev-parse', 'HEAD']);
|
|
7
|
+
const beforeHead = before.ok ? gitOutputLine(before) : null;
|
|
8
|
+
if (!['idle', 'done', 'failed', 'unknown'].includes(worker.state)) {
|
|
9
|
+
records.push(record(worker, 'skipped', 'worker_not_idle', beforeHead, beforeHead, []));
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const status = await runGitCommand(worker.worktree_path, ['status', '--porcelain=v1', '--untracked-files=all']);
|
|
13
|
+
if (!status.ok) {
|
|
14
|
+
records.push(record(worker, 'failed', 'status_failed', beforeHead, beforeHead, [gitBlocker('git_worktree_cross_rebase_status_failed', status)]));
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (status.stdout.trim()) {
|
|
18
|
+
records.push(record(worker, 'skipped', 'dirty_worktree_skipped', beforeHead, beforeHead, []));
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const rebase = await runGitCommand(worker.worktree_path, ['rebase', input.integrationHead], { timeoutMs: 120000 });
|
|
22
|
+
if (!rebase.ok) {
|
|
23
|
+
await runGitCommand(worker.worktree_path, ['rebase', '--abort'], { timeoutMs: 30000 }).catch(() => null);
|
|
24
|
+
records.push(record(worker, 'failed', 'rebase_failed', beforeHead, beforeHead, [gitBlocker('git_worktree_cross_rebase_failed', rebase)]));
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const after = await runGitCommand(worker.worktree_path, ['rev-parse', 'HEAD']);
|
|
28
|
+
records.push(record(worker, 'applied', 'rebased_to_integration_head', beforeHead, after.ok ? gitOutputLine(after) : null, []));
|
|
29
|
+
}
|
|
30
|
+
const blockers = records.flatMap((row) => row.blockers);
|
|
31
|
+
return {
|
|
32
|
+
schema: 'sks.git-worktree-cross-rebase.v1',
|
|
33
|
+
ok: blockers.length === 0,
|
|
34
|
+
generated_at: nowIso(),
|
|
35
|
+
integration_head: input.integrationHead,
|
|
36
|
+
applied_count: records.filter((row) => row.status === 'applied').length,
|
|
37
|
+
skipped_count: records.filter((row) => row.status === 'skipped').length,
|
|
38
|
+
records,
|
|
39
|
+
blockers
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function record(worker, status, reason, beforeHead, afterHead, blockers) {
|
|
43
|
+
return {
|
|
44
|
+
worker_id: worker.worker_id,
|
|
45
|
+
worktree_path: worker.worktree_path,
|
|
46
|
+
state: worker.state,
|
|
47
|
+
status,
|
|
48
|
+
reason,
|
|
49
|
+
before_head: beforeHead,
|
|
50
|
+
after_head: afterHead,
|
|
51
|
+
blockers
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=git-worktree-cross-rebase.js.map
|
|
@@ -3,10 +3,35 @@ import { runGitCommand } from './git-worktree-runner.js';
|
|
|
3
3
|
import { summarizeGitWorktreeConflict } from './git-worktree-conflict-resolver.js';
|
|
4
4
|
export async function applyGitWorktreeMergeQueue(input) {
|
|
5
5
|
const conflicts = [];
|
|
6
|
+
const strategyResults = [];
|
|
6
7
|
const changedFiles = new Set();
|
|
7
8
|
let appliedCount = 0;
|
|
8
9
|
let skippedCleanCount = 0;
|
|
10
|
+
let checkpointCommitCount = 0;
|
|
11
|
+
for (const checkpoint of input.checkpoints || []) {
|
|
12
|
+
for (const file of checkpoint.changed_files || [])
|
|
13
|
+
changedFiles.add(file);
|
|
14
|
+
if (checkpoint.mode_applied !== 'checkpoint-commit' || !checkpoint.commit_hash)
|
|
15
|
+
continue;
|
|
16
|
+
checkpointCommitCount += 1;
|
|
17
|
+
const merged = await applyCheckpointCommit(input.integrationWorktreePath, checkpoint);
|
|
18
|
+
strategyResults.push(merged);
|
|
19
|
+
if (merged.ok)
|
|
20
|
+
appliedCount += 1;
|
|
21
|
+
else
|
|
22
|
+
conflicts.push({
|
|
23
|
+
worker_id: checkpoint.worker_id,
|
|
24
|
+
changed_files: checkpoint.changed_files,
|
|
25
|
+
strategy: 'checkpoint-commit',
|
|
26
|
+
blockers: merged.blockers,
|
|
27
|
+
conflict_files: merged.conflict_files
|
|
28
|
+
});
|
|
29
|
+
}
|
|
9
30
|
for (const diff of input.diffs) {
|
|
31
|
+
if ((input.checkpoints || []).some((checkpoint) => checkpoint.worker_id === diff.worker_id && checkpoint.mode_applied === 'checkpoint-commit' && checkpoint.commit_hash)) {
|
|
32
|
+
skippedCleanCount += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
10
35
|
for (const file of diff.changed_files)
|
|
11
36
|
changedFiles.add(file);
|
|
12
37
|
if (diff.clean || !diff.diff.trim()) {
|
|
@@ -46,10 +71,54 @@ export async function applyGitWorktreeMergeQueue(input) {
|
|
|
46
71
|
generated_at: nowIso(),
|
|
47
72
|
integration_worktree_path: input.integrationWorktreePath,
|
|
48
73
|
applied_count: appliedCount,
|
|
74
|
+
checkpoint_commit_count: checkpointCommitCount,
|
|
49
75
|
skipped_clean_count: skippedCleanCount,
|
|
50
76
|
conflicts,
|
|
77
|
+
strategy_results: strategyResults,
|
|
51
78
|
changed_files: [...changedFiles],
|
|
52
79
|
blockers
|
|
53
80
|
};
|
|
54
81
|
}
|
|
82
|
+
async function applyCheckpointCommit(integrationWorktreePath, checkpoint) {
|
|
83
|
+
const merge = await runGitCommand(integrationWorktreePath, ['merge', '--no-ff', '--no-edit', checkpoint.commit_hash || ''], {
|
|
84
|
+
timeoutMs: 120000
|
|
85
|
+
});
|
|
86
|
+
if (merge.ok) {
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
worker_id: checkpoint.worker_id,
|
|
90
|
+
strategy: 'merge',
|
|
91
|
+
commit_hash: checkpoint.commit_hash,
|
|
92
|
+
conflict_files: [],
|
|
93
|
+
blockers: []
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
await runGitCommand(integrationWorktreePath, ['merge', '--abort'], { timeoutMs: 30000 }).catch(() => null);
|
|
97
|
+
const cherryPick = await runGitCommand(integrationWorktreePath, ['cherry-pick', checkpoint.commit_hash || ''], {
|
|
98
|
+
timeoutMs: 120000
|
|
99
|
+
});
|
|
100
|
+
if (cherryPick.ok) {
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
worker_id: checkpoint.worker_id,
|
|
104
|
+
strategy: 'cherry-pick',
|
|
105
|
+
commit_hash: checkpoint.commit_hash,
|
|
106
|
+
conflict_files: [],
|
|
107
|
+
blockers: []
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
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);
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
worker_id: checkpoint.worker_id,
|
|
115
|
+
strategy: 'merge_then_cherry-pick',
|
|
116
|
+
commit_hash: checkpoint.commit_hash,
|
|
117
|
+
conflict_files: String(conflictFiles?.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean),
|
|
118
|
+
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}`
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
55
124
|
//# sourceMappingURL=git-worktree-merge-queue.js.map
|
|
@@ -18,7 +18,14 @@ export function buildGitWorktreePatchEnvelope(input) {
|
|
|
18
18
|
base_head: input.diff.base_head,
|
|
19
19
|
worktree_head: input.diff.worktree_head,
|
|
20
20
|
changed_files: changedFiles,
|
|
21
|
-
diff_bytes: input.diff.diff_bytes
|
|
21
|
+
diff_bytes: input.diff.diff_bytes,
|
|
22
|
+
checkpoint: input.checkpoint ? {
|
|
23
|
+
schema: input.checkpoint.schema,
|
|
24
|
+
mode_applied: input.checkpoint.mode_applied,
|
|
25
|
+
commit_hash: input.checkpoint.commit_hash,
|
|
26
|
+
changed_files: input.checkpoint.changed_files,
|
|
27
|
+
blockers: input.checkpoint.blockers
|
|
28
|
+
} : null
|
|
22
29
|
},
|
|
23
30
|
operations: [{
|
|
24
31
|
op: 'git_apply_patch',
|
package/dist/core/init.js
CHANGED
|
@@ -237,7 +237,7 @@ function isSksManagedHook(hook) {
|
|
|
237
237
|
const command = String(hook.command || '');
|
|
238
238
|
return hook.type === 'command' && /\bhook\s+(?:session-start|user-prompt-submit|pre-tool|post-tool|permission-request|pre-compact|post-compact|subagent-start|subagent-stop|stop)\b/.test(command) && /\b(?:sks|sneakoscope|sks\.js)\b/.test(command);
|
|
239
239
|
}
|
|
240
|
-
const AGENTS_BLOCK = "\n# Sneakoscope Codex Managed Rules\n\nThis repository uses Sneakoscope Codex.\n\n## Core Rules\n\n- Codex native `/goal` workflows are the persisted continuation surface; Ralph is removed from the user-facing SKS surface.\n- Keep runtime state bounded: raw logs go to files, prompts get tails/summaries, and `sks gc` may prune stale artifacts.\n- Codex App hooks do not force SKS update prompts during ordinary work. CLI update surfaces (`sks update-check`, `sks update check`, and
|
|
240
|
+
const AGENTS_BLOCK = "\n# Sneakoscope Codex Managed Rules\n\nThis repository uses Sneakoscope Codex.\n\n## Core Rules\n\n- Codex native `/goal` workflows are the persisted continuation surface; Ralph is removed from the user-facing SKS surface.\n- Keep runtime state bounded: raw logs go to files, prompts get tails/summaries, and `sks gc` may prune stale artifacts.\n- Codex App hooks, launch paths, and `sks doctor --fix` do not force SKS update prompts during ordinary work. Manual CLI update surfaces (`sks update-check`, `sks update check`, and `sks update now`) remain available when the operator explicitly asks for them.\n- Versioning is explicit: use `sks versioning bump` when preparing release metadata. SKS must not install Git pre-commit hooks.\n- Installed harness files are immutable to LLM edits: `.codex/*`, `.agents/skills/`, `.codex/agents/`, `.sneakoscope/*policy*.json`, `AGENTS.md`, and `node_modules/sneakoscope`. The Sneakoscope engine source repo is the only automatic exception.\n- OMX/DCodex conflicts block setup/doctor. Show `sks conflicts prompt`; cleanup requires explicit human approval.\n- Do not stop at a plan when implementation was requested. Finish, verify, or report the hard blocker.\n- Do not create unrequested fallback implementation code. If the requested path is impossible, block with evidence instead of inventing substitute behavior.\n\n## Routes\n\n- General execution/code-changing prompts default to `$Team`: native agent intake agents, TriWiki refresh/validate, read-only debate, consensus, concrete runtime task graph/inboxes, fresh executor team, minimum five-lane Team review, integration, Honest Mode.\n- `$Computer-Use` / `$CU` is the maximum-speed Codex Computer Use lane for native macOS, desktop-app, OS-settings, and non-web visual tasks only. Web, browser, localhost, website, webapp, and web-based app verification must use the Codex Chrome Extension path first and halt rapidly if the extension is not installed/enabled.\n- `$Goal` is a fast bridge/overlay for Codex native `/goal` create/pause/resume/clear persistence controls; implementation continues through the selected SKS execution route.\n- TriWiki recall must stay bounded. Use `sks wiki sweep` to record demote, soft-forget, archive, delete, promote-to-skill, and promote-to-rule candidates instead of injecting every old claim.\n- Team missions must keep schema-backed evidence current: `work-order-ledger.json`, `effort-decision.json`, `team-dashboard-state.json`, and route-specific visual/dogfood artifacts where applicable. Team completion requires at least five independent reviewer/QA validation lanes before integration or final, even when a prompt requests fewer reviewers. Use `sks validate-artifacts latest` before claiming those artifacts pass.\n- `$DFix` is Direct Fix: only tiny copy/config/docs/labels/spacing/translation/simple mechanical edits, bypassing the main pipeline, Team, TriWiki/TriFix/reflection recording, and persistent route state; it still uses a one-line DFix-specific Honest check before final. Broad implementation stays on `$Team`, while UI design specifics follow the relevant design/UI route rules. `$PPT` is the restrained, information-first HTML/PDF presentation route and must seal delivery context, audience profile, STP, decision context, and 3+ pain-point/solution/aha mappings before design/render work. It must avoid over-designed visuals, carry detail through hierarchy, spacing, alignment, thin rules, source clarity, and subtle accents, preserve editable source HTML under `source-html/`, record `ppt-parallel-report.json`, and clean PPT-only temporary build files before completion. `$Image-UX-Review` / `$UX-Review` is the imagegen/gpt-image-2 UI/UX review route: source screenshots must become generated annotated review images, those generated images must be extracted into issue ledgers, and text-only critique cannot pass the route gate. `$Answer`, `$Help`, and `$Wiki` stay lightweight.\n- For code work, surface route/guard/write scopes first, split independent worker scopes when available, and keep parent-owned integration and verification.\n- Design work reads `design.md` as the only design decision SSOT. If missing, create it through `design-system-builder` from `docs/Design-Sys-Prompt.md`; getdesign.md, getdesign-reference, and curated DESIGN.md examples from https://github.com/VoltAgent/awesome-design-md are source inputs to fuse into that SSOT or route-local style tokens, not parallel design authorities. Image/logo/raster assets use `imagegen`, which must prefer official Codex App built-in image generation via `$imagegen` / `gpt-image-2`; for newest-model image requests prompt explicitly for ChatGPT Images 2.0 / GPT Image 2.0 with `gpt-image-2`. Do not replace required raster evidence with placeholder SVG/HTML/CSS, prose-only reviews, or fabricated files.\n- Research, AutoResearch, performance, token, accuracy, SEO/GEO, or workflow-improvement claims need experiment/eval evidence. Do not claim live model accuracy without a scored dataset.\n- Treat handwritten files above 3000 lines as split-review risks. Run `sks code-structure scan` and prefer extraction before adding substantial logic.\n- Skill dreaming stays lightweight: route use records JSON counters in `.sneakoscope/skills/dream-state.json`, and full skill inventory/recommendation runs only after the configured 10-route-event threshold and cooldown. Reports are recommendation-only; deleting or merging skills needs explicit user approval.\n\n## Evidence And Context\n\n- Context7 is required for external libraries, APIs, MCPs, package managers, SDKs, and generated docs: resolve-library-id then query-docs.\n- When tech stack, framework, package, runtime, or deployment-platform versions change, use Context7 or official vendor web docs, record current syntax/security/limit guidance as high-priority TriWiki claims, then refresh and validate before coding.\n- TriWiki is the context-tracking SSOT for long-running missions, Team handoffs, and context-pressure recovery. Read `.sneakoscope/wiki/context-pack.json` before each stage, use `attention.use_first` for compact high-trust recall, hydrate `attention.hydrate_first` from source before risky or lower-trust decisions, refresh after findings or artifact changes, and validate before handoffs/final claims.\n- Source priority: current code/tests/config, decision contract, vgraph, beta, GX render/snapshot metadata, LLM Wiki coordinate index, then model knowledge only if allowed.\n- Final response before stop: summarize what was done, what changed for the user/repo, what was verified, and what remains unverified or blocked; then run Honest Mode. Say what passed and what was not verified.\n- `$From-Chat-IMG` uses forensic visual effort, not ordinary Team effort. Completion is blocked until source inventory, visual mapping, work-order coverage, scoped dogfood/QA, and post-fix verification artifacts are present and valid.\n\n## Safety\n\n- Database access is high risk. Use read-only inspection by default; live data mutation is out of scope unless a sealed contract allows local or branch-only migration files.\n- MAD and MAD-SKS widen only explicit scoped permissions; they still do not authorize unrequested fallback implementation code.\n- Task completion requires relevant tests or justification, zero unsupported critical claims, accepted visual/wiki drift, and final evidence.\n\n## Codex App\n\nUse `.codex/SNEAKOSCOPE.md`, generated `.agents/skills`, `.codex/hooks.json`, and SKS dollar commands (`$sks`, `$team`, `$computer-use`, `$cu`, `$ppt`, `$image-ux-review`, `$ux-review`, `$goal`, `$dfix`, `$qa-loop`, etc.) as the app control surface.\n";
|
|
241
241
|
function agentsBlockText() {
|
|
242
242
|
return AGENTS_BLOCK;
|
|
243
243
|
}
|
|
@@ -1011,7 +1011,7 @@ function codexAppQuickReference(scope, commandPrefix) {
|
|
|
1011
1011
|
stackCurrentDocsPolicyText(commandPrefix),
|
|
1012
1012
|
`Team review: ${MIN_TEAM_REVIEW_POLICY_TEXT}`,
|
|
1013
1013
|
`Team Zellij view: ${commandPrefix} team "task" prepares live watch/lane commands and reconciles managed Team panes inside the current SKS-owned Zellij session when available; add --no-open-zellij for artifact-only creation. Existing hook-created Team missions can be opened later with ${commandPrefix} team open-zellij latest. The view keeps the main Codex pane alive, adds an overview watch pane plus color-coded split per-agent lanes, and closes only SKS-managed Team panes as agent lanes finish or cleanup is requested; ${commandPrefix} team lane latest --agent native_agent_1 --follow shows one agent's status, assigned runtime tasks, recent agent events, direct messages, and fallback global tail; ${commandPrefix} team message latest --from native_agent_1 --to executor_1 --message "handoff note" mirrors bounded agent communication into transcript/lane panes; ${commandPrefix} team cleanup-zellij latest marks the SKS session record complete and asks managed panes/follow loops to close or show a cleanup summary.`,
|
|
1014
|
-
`Runtime: open Codex App once, then run ${commandPrefix} bootstrap and ${commandPrefix} deps check. Zellij is the interactive lane runtime for ${commandPrefix} --mad and Team lane UI; ${commandPrefix} bootstrap --yes, ${commandPrefix} deps check --yes, and ${commandPrefix} --mad --yes can install or repair Codex CLI/Zellij on macOS/Homebrew. npm postinstall reports missing CLI tools but does not mutate Homebrew/npm globals unless SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS=1 is set.
|
|
1014
|
+
`Runtime: open Codex App once, then run ${commandPrefix} bootstrap and ${commandPrefix} deps check. Zellij is the interactive lane runtime for ${commandPrefix} --mad and Team lane UI; ${commandPrefix} bootstrap --yes, ${commandPrefix} deps check --yes, and ${commandPrefix} --mad --yes can install or repair Codex CLI/Zellij on macOS/Homebrew. npm postinstall reports missing CLI tools but does not mutate Homebrew/npm globals unless SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS=1 is set. Launch paths do not run sneakoscope npm update checks; use ${commandPrefix} update-check or ${commandPrefix} update now explicitly when you want that. Codex CLI latest checks remain dependency-readiness guidance and prompt Y/n only when the installed Codex CLI is missing or outdated. ${commandPrefix} doctor --fix repairs the local SKS/Codex setup without running a global SKS package update. ${commandPrefix} codex-app remote-control wraps the Codex CLI 0.130.0+ headless remote-control entrypoint. ${commandPrefix} team open-zellij latest is the explicit Team lane view command.`,
|
|
1015
1015
|
`Guard: generated harness files are immutable outside the engine source repo; check ${commandPrefix} guard check; conflicts use ${commandPrefix} conflicts prompt with human approval.`
|
|
1016
1016
|
].join('\n') + '\n';
|
|
1017
1017
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { extractNarutoTaskHints, pathPrefix } from './naruto-task-hints.js';
|
|
2
|
+
export function chooseNarutoTaskOwner(task, workers, currentAssignments = [], leaseState = {}) {
|
|
3
|
+
if (!workers.length)
|
|
4
|
+
throw new Error('at least one Naruto worker is required');
|
|
5
|
+
const hints = extractNarutoTaskHints(task);
|
|
6
|
+
const activeWritePaths = new Set((leaseState.active_write_paths || []).map(String));
|
|
7
|
+
const completedTaskIds = new Set((leaseState.completed_task_ids || []).map(String));
|
|
8
|
+
const writeConflict = hints.writePaths.some((file) => activeWritePaths.has(file));
|
|
9
|
+
const dependencyIncomplete = task.dependencies.some((dep) => !completedTaskIds.has(dep));
|
|
10
|
+
const ranked = workers.map((worker, index) => {
|
|
11
|
+
const assigned = currentAssignments.filter((row) => row.owner === worker.id);
|
|
12
|
+
const assignedHints = assigned.map((row) => ({
|
|
13
|
+
role: row.role || null,
|
|
14
|
+
paths: row.paths || [],
|
|
15
|
+
domains: row.domains || [],
|
|
16
|
+
writePaths: row.write_paths || []
|
|
17
|
+
}));
|
|
18
|
+
const primaryRole = worker.primary_role || worker.role || null;
|
|
19
|
+
const declaredRoles = new Set([worker.role, ...(worker.declared_roles || [])].filter(Boolean).map(String));
|
|
20
|
+
const primaryRoleMatches = Boolean(hints.role && primaryRole === hints.role);
|
|
21
|
+
const declaredRoleMatches = Boolean(hints.role && declaredRoles.has(hints.role));
|
|
22
|
+
const assignmentRoleMatches = Boolean(hints.role && assigned.some((row) => row.role === hints.role));
|
|
23
|
+
const sameLane = samePathLane(hints.paths, assignedHints.flatMap((row) => row.paths));
|
|
24
|
+
const overlap = overlapCount(hints.paths, assignedHints.flatMap((row) => row.paths))
|
|
25
|
+
+ overlapCount(hints.domains, assignedHints.flatMap((row) => row.domains));
|
|
26
|
+
const laneMatches = Boolean(worker.lane && hints.paths.some((file) => pathLaneMatches(file, String(worker.lane))));
|
|
27
|
+
const score = dependencyIncomplete
|
|
28
|
+
? Number.NEGATIVE_INFINITY
|
|
29
|
+
: (primaryRoleMatches ? 18 : 0)
|
|
30
|
+
+ (declaredRoleMatches ? 12 : 0)
|
|
31
|
+
+ (sameLane || laneMatches ? 12 : 0)
|
|
32
|
+
+ (overlap * 4)
|
|
33
|
+
- (assigned.length * 4)
|
|
34
|
+
- (writeConflict ? 20 : 0);
|
|
35
|
+
return { worker, index, assigned, score, overlap, primaryRoleMatches, declaredRoleMatches, assignmentRoleMatches, sameLane: sameLane || laneMatches, writeConflict, dependencyIncomplete };
|
|
36
|
+
}).sort((left, right) => {
|
|
37
|
+
if (right.score !== left.score)
|
|
38
|
+
return right.score - left.score;
|
|
39
|
+
if (right.overlap !== left.overlap)
|
|
40
|
+
return right.overlap - left.overlap;
|
|
41
|
+
if (left.assigned.length !== right.assigned.length)
|
|
42
|
+
return left.assigned.length - right.assigned.length;
|
|
43
|
+
return left.index - right.index;
|
|
44
|
+
});
|
|
45
|
+
const selected = ranked[0];
|
|
46
|
+
const reasons = [
|
|
47
|
+
selected.primaryRoleMatches ? 'same primary role' : null,
|
|
48
|
+
selected.declaredRoleMatches ? 'same declared role' : null,
|
|
49
|
+
selected.assignmentRoleMatches ? 'same assigned role history' : null,
|
|
50
|
+
selected.sameLane ? 'same path/domain lane' : null,
|
|
51
|
+
selected.overlap ? `overlap:${selected.overlap}` : null,
|
|
52
|
+
selected.writeConflict ? 'write lease conflict penalty applied' : null,
|
|
53
|
+
selected.dependencyIncomplete ? 'dependency incomplete' : null,
|
|
54
|
+
`load:${selected.assigned.length}`
|
|
55
|
+
].filter(Boolean);
|
|
56
|
+
return {
|
|
57
|
+
owner: selected.worker.id,
|
|
58
|
+
score: selected.score,
|
|
59
|
+
reason: reasons.join('; '),
|
|
60
|
+
hints
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function allocateNarutoTasksToWorkers(tasks, workers) {
|
|
64
|
+
const assignments = [];
|
|
65
|
+
for (const task of tasks) {
|
|
66
|
+
const decision = chooseNarutoTaskOwner(task, workers, assignments.map((row) => ({
|
|
67
|
+
task_id: row.id,
|
|
68
|
+
owner: row.owner,
|
|
69
|
+
role: row.required_role,
|
|
70
|
+
paths: row.hints.paths,
|
|
71
|
+
domains: row.hints.domains,
|
|
72
|
+
write_paths: row.hints.writePaths
|
|
73
|
+
})), {
|
|
74
|
+
active_write_paths: assignments.flatMap((row) => row.hints.writePaths)
|
|
75
|
+
});
|
|
76
|
+
assignments.push({
|
|
77
|
+
...task,
|
|
78
|
+
owner: decision.owner,
|
|
79
|
+
allocation_reason: decision.reason,
|
|
80
|
+
allocation_score: decision.score,
|
|
81
|
+
hints: decision.hints
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return assignments;
|
|
85
|
+
}
|
|
86
|
+
function samePathLane(left, right) {
|
|
87
|
+
const prefixes = new Set(right.map(pathPrefix).filter(Boolean));
|
|
88
|
+
return left.some((file) => prefixes.has(pathPrefix(file)));
|
|
89
|
+
}
|
|
90
|
+
function pathLaneMatches(file, lane) {
|
|
91
|
+
const normalizedLane = lane.replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
92
|
+
const normalizedFile = file.replace(/^\.\/+/, '');
|
|
93
|
+
return pathPrefix(normalizedFile) === normalizedLane || normalizedFile === normalizedLane || normalizedFile.startsWith(`${normalizedLane}/`);
|
|
94
|
+
}
|
|
95
|
+
function overlapCount(left, right) {
|
|
96
|
+
const rightSet = new Set(right);
|
|
97
|
+
return left.filter((item) => rightSet.has(item)).length;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=naruto-allocation-policy.js.map
|