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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/build-manifest.json +24 -8
  8. package/dist/cli/command-registry.js +1 -0
  9. package/dist/cli/install-helpers.js +8 -20
  10. package/dist/commands/doctor.js +5 -9
  11. package/dist/commands/zellij-slot-column-anchor.js +23 -0
  12. package/dist/core/agents/agent-orchestrator.js +59 -11
  13. package/dist/core/agents/agent-patch-schema.js +8 -1
  14. package/dist/core/commands/naruto-command.js +118 -19
  15. package/dist/core/fsx.js +1 -1
  16. package/dist/core/git/git-worktree-checkpoint.js +52 -0
  17. package/dist/core/git/git-worktree-cross-rebase.js +54 -0
  18. package/dist/core/git/git-worktree-merge-queue.js +69 -0
  19. package/dist/core/git/git-worktree-patch-envelope.js +8 -1
  20. package/dist/core/init.js +2 -2
  21. package/dist/core/naruto/naruto-allocation-policy.js +99 -0
  22. package/dist/core/naruto/naruto-real-worker-child.js +110 -11
  23. package/dist/core/naruto/naruto-rebalance-policy.js +36 -0
  24. package/dist/core/naruto/naruto-task-hints.js +71 -0
  25. package/dist/core/pipeline/finalize-pipeline-result.js +3 -1
  26. package/dist/core/pipeline/gpt-final-required.js +22 -2
  27. package/dist/core/version.js +1 -1
  28. package/dist/core/zellij/zellij-right-column-manager.js +45 -2
  29. package/dist/core/zellij/zellij-slot-column-anchor.js +59 -0
  30. package/dist/core/zellij/zellij-worker-pane-manager.js +70 -9
  31. package/dist/scripts/git-worktree-checkpoint-check.js +20 -0
  32. package/dist/scripts/git-worktree-cross-rebase-check.js +27 -0
  33. package/dist/scripts/naruto-actual-worker-control-plane-check.js +29 -0
  34. package/dist/scripts/naruto-allocation-policy-check.js +33 -0
  35. package/dist/scripts/naruto-extreme-parallelism-real-check.js +5 -4
  36. package/dist/scripts/naruto-orchestrator-runtime-source-check.js +11 -0
  37. package/dist/scripts/naruto-real-active-pool-runtime-check.js +4 -2
  38. package/dist/scripts/naruto-rebalance-policy-check.js +28 -0
  39. package/dist/scripts/naruto-shadow-clone-swarm-check.js +7 -3
  40. package/dist/scripts/release-dag-full-coverage-check.js +15 -1
  41. package/dist/scripts/release-readiness-report.js +1 -1
  42. package/dist/scripts/zellij-first-slot-down-stack-check.js +20 -0
  43. package/dist/scripts/zellij-first-slot-down-stack-real-check.js +16 -0
  44. package/dist/scripts/zellij-right-column-manager-check.js +7 -2
  45. package/dist/scripts/zellij-slot-column-anchor-check.js +24 -0
  46. package/dist/scripts/zellij-slot-only-ui-check.js +4 -2
  47. package/dist/scripts/zellij-worker-pane-manager-single-owner-check.js +11 -4
  48. 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 realRuntimeWorktreePolicy = schedulerBackend === 'fake'
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: 'fake_backend_fixture_skips_real_worktree_cleanup'
175
+ fallback_reason: 'pre_run_smoke_never_owns_production_runtime'
123
176
  }
124
- : worktreePolicy;
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: workGraph,
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: schedulerBackend,
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: realActivePool.ok,
278
- active_cap: realActivePool.active_cap,
279
- max_observed_active_workers: realActivePool.max_observed_active_workers,
280
- average_active_workers: realActivePool.average_active_workers,
281
- active_pool_utilization: realActivePool.active_pool_utilization,
282
- refill_latency_ms_p95: realActivePool.refill_latency_ms_p95,
283
- visible_workers: realActivePool.visible_workers,
284
- headless_workers: realActivePool.headless_workers,
285
- worker_lifecycle_count: realActivePool.worker_lifecycle.length,
286
- worker_lifecycle_sample: realActivePool.worker_lifecycle.slice(0, 5)
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.10';
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 launch notices) show latest-version guidance, and `sks doctor --fix` runs the guarded global SKS update path.\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";
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. Before launch SKS prints non-blocking latest-version notices for sneakoscope and checks npm @openai/codex@latest, prompting Y/n only when the installed Codex CLI is missing or outdated. ${commandPrefix} doctor --fix runs the guarded global SKS update path before repair. ${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.`,
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