sneakoscope 2.0.6 → 2.0.8
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 +6 -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 +56 -8
- package/dist/core/agents/agent-command-surface.js +4 -2
- package/dist/core/agents/agent-orchestrator.js +140 -4
- package/dist/core/agents/agent-patch-schema.js +20 -4
- package/dist/core/agents/agent-proof-evidence.js +3 -0
- package/dist/core/agents/native-cli-session-swarm.js +31 -5
- package/dist/core/agents/native-cli-worker.js +28 -1
- package/dist/core/codex-control/python-codex-sdk-adapter.js +28 -4
- package/dist/core/commands/mad-sks-command.js +25 -0
- package/dist/core/commands/naruto-command.js +68 -10
- package/dist/core/feature-registry.js +2 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/git/git-integration-worktree.js +15 -0
- package/dist/core/git/git-repo-detection.js +79 -0
- package/dist/core/git/git-worktree-cache-policy.js +36 -0
- package/dist/core/git/git-worktree-capability.js +54 -0
- package/dist/core/git/git-worktree-cleanup.js +62 -0
- package/dist/core/git/git-worktree-conflict-resolver.js +13 -0
- package/dist/core/git/git-worktree-diff.js +55 -0
- package/dist/core/git/git-worktree-manager.js +93 -0
- package/dist/core/git/git-worktree-merge-queue.js +55 -0
- package/dist/core/git/git-worktree-patch-envelope.js +35 -0
- package/dist/core/git/git-worktree-pool.js +23 -0
- package/dist/core/git/git-worktree-root.js +52 -0
- package/dist/core/git/git-worktree-runner.js +40 -0
- package/dist/core/naruto/naruto-active-pool.js +35 -0
- package/dist/core/naruto/naruto-gpt-final-pack.js +2 -0
- package/dist/core/naruto/naruto-work-graph.js +16 -1
- package/dist/core/release/release-gate-cache-v2.js +63 -0
- package/dist/core/release/release-gate-dag.js +179 -0
- package/dist/core/release/release-gate-hermetic-env.js +32 -0
- package/dist/core/release/release-gate-node.js +62 -0
- package/dist/core/release/release-gate-report.js +11 -0
- package/dist/core/release/release-gate-resource-governor.js +54 -0
- package/dist/core/release/release-gate-scheduler.js +15 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-dashboard-pane.js +71 -0
- package/dist/core/zellij/zellij-dashboard-renderer.js +42 -0
- package/dist/core/zellij/zellij-naruto-dashboard.js +10 -1
- package/dist/core/zellij/zellij-worker-pane-manager.js +68 -6
- package/dist/scripts/git-worktree-cache-performance-check.js +25 -0
- package/dist/scripts/git-worktree-capability-check.js +27 -0
- package/dist/scripts/git-worktree-cleanup-check.js +27 -0
- package/dist/scripts/git-worktree-diff-envelope-check.js +17 -0
- package/dist/scripts/git-worktree-diff-export-check.js +43 -0
- package/dist/scripts/git-worktree-dirty-lock-check.js +17 -0
- package/dist/scripts/git-worktree-dirty-main-detection-check.js +14 -0
- package/dist/scripts/git-worktree-integration-primary-check.js +22 -0
- package/dist/scripts/git-worktree-manager-check.js +37 -0
- package/dist/scripts/git-worktree-manifest-append-check.js +18 -0
- package/dist/scripts/git-worktree-merge-queue-check.js +30 -0
- package/dist/scripts/git-worktree-pool-performance-check.js +20 -0
- package/dist/scripts/git-worktree-untracked-diff-check.js +18 -0
- package/dist/scripts/lib/git-worktree-fixture.js +33 -0
- package/dist/scripts/naruto-shadow-clone-swarm-check.js +9 -5
- package/dist/scripts/naruto-worktree-coding-blackbox.js +29 -0
- package/dist/scripts/naruto-worktree-coding-check.js +44 -0
- package/dist/scripts/naruto-worktree-gpt-final-check.js +45 -0
- package/dist/scripts/naruto-worktree-zellij-ui-check.js +28 -0
- package/dist/scripts/release-gate-dag-runner-check.js +17 -0
- package/dist/scripts/release-gate-dag-runner.js +32 -0
- package/dist/scripts/release-gate-worker.js +10 -0
- package/dist/scripts/release-metadata-1-19-check.js +8 -2
- package/dist/scripts/release-parallel-check.js +1 -1
- package/dist/scripts/release-parallel-speed-budget-check.js +25 -0
- package/dist/scripts/release-stability-report-check.js +99 -0
- package/dist/scripts/zellij-dashboard-pane-check.js +68 -0
- package/dist/scripts/zellij-dashboard-watch.js +41 -0
- package/dist/scripts/zellij-worker-pane-real-ui-blackbox.js +185 -0
- package/package.json +33 -5
- package/schemas/git/git-worktree-capability.schema.json +19 -0
- package/schemas/git/git-worktree-manifest.schema.json +36 -0
- package/schemas/release/release-gate-node.schema.json +52 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { runGitCommand } from './git-worktree-runner.js';
|
|
4
|
+
export async function cleanupGitWorktree(input) {
|
|
5
|
+
const repoRoot = path.resolve(input.repoRoot);
|
|
6
|
+
const worktreePath = path.resolve(input.worktreePath);
|
|
7
|
+
const status = await runGitCommand(worktreePath, ['status', '--porcelain=v1', '--untracked-files=all']);
|
|
8
|
+
const clean = status.ok && status.stdout.trim().length === 0;
|
|
9
|
+
if (!clean) {
|
|
10
|
+
const reason = `SKS retained dirty failed worker ${path.basename(worktreePath)}`;
|
|
11
|
+
const lock = await runGitCommand(repoRoot, ['worktree', 'lock', '--reason', reason, worktreePath]);
|
|
12
|
+
const lockPath = `${worktreePath}.retained.json`;
|
|
13
|
+
await writeJsonAtomic(lockPath, {
|
|
14
|
+
schema: 'sks.git-worktree-retention-lock.v1',
|
|
15
|
+
generated_at: nowIso(),
|
|
16
|
+
repo_root: repoRoot,
|
|
17
|
+
worktree_path: worktreePath,
|
|
18
|
+
branch: input.branch || null,
|
|
19
|
+
reason: status.ok ? 'dirty_worktree_retained' : 'status_failed_retained',
|
|
20
|
+
status_porcelain: status.stdout || null,
|
|
21
|
+
git_locked: lock.ok,
|
|
22
|
+
unlock_command: `git worktree unlock ${JSON.stringify(worktreePath)}`,
|
|
23
|
+
cleanup_command: 'sks worktree cleanup --mission <id>'
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
schema: 'sks.git-worktree-cleanup.v1',
|
|
27
|
+
ok: true,
|
|
28
|
+
generated_at: nowIso(),
|
|
29
|
+
repo_root: repoRoot,
|
|
30
|
+
worktree_path: worktreePath,
|
|
31
|
+
branch: input.branch || null,
|
|
32
|
+
clean: false,
|
|
33
|
+
action: 'retained_dirty',
|
|
34
|
+
retention_lock_path: lockPath,
|
|
35
|
+
blockers: lock.ok ? [] : ['git_worktree_lock_failed'],
|
|
36
|
+
git_locked: lock.ok,
|
|
37
|
+
unlock_command: `git worktree unlock ${JSON.stringify(worktreePath)}`,
|
|
38
|
+
cleanup_command: 'sks worktree cleanup --mission <id>'
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const remove = await runGitCommand(repoRoot, ['worktree', 'remove', worktreePath]);
|
|
42
|
+
const blockers = remove.ok ? [] : ['git_worktree_remove_failed'];
|
|
43
|
+
if (remove.ok && input.deleteBranch === true && input.branch) {
|
|
44
|
+
await runGitCommand(repoRoot, ['branch', '-D', input.branch]);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
schema: 'sks.git-worktree-cleanup.v1',
|
|
48
|
+
ok: blockers.length === 0,
|
|
49
|
+
generated_at: nowIso(),
|
|
50
|
+
repo_root: repoRoot,
|
|
51
|
+
worktree_path: worktreePath,
|
|
52
|
+
branch: input.branch || null,
|
|
53
|
+
clean: true,
|
|
54
|
+
action: remove.ok ? 'removed' : 'remove_failed',
|
|
55
|
+
retention_lock_path: null,
|
|
56
|
+
blockers,
|
|
57
|
+
git_locked: false,
|
|
58
|
+
unlock_command: null,
|
|
59
|
+
cleanup_command: null
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=git-worktree-cleanup.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function summarizeGitWorktreeConflict(input) {
|
|
2
|
+
const stderr = String(input.stderr || '');
|
|
3
|
+
return {
|
|
4
|
+
schema: 'sks.git-worktree-conflict.v1',
|
|
5
|
+
ok: false,
|
|
6
|
+
worker_id: input.workerId,
|
|
7
|
+
changed_files: input.changedFiles,
|
|
8
|
+
stderr_tail: stderr.slice(-4000),
|
|
9
|
+
conflict_markers_possible: /conflict|patch failed|does not apply/i.test(stderr),
|
|
10
|
+
blockers: ['git_worktree_diff_apply_failed']
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=git-worktree-conflict-resolver.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso } from '../fsx.js';
|
|
3
|
+
import { gitOutputLine, runGitCommand } from './git-worktree-runner.js';
|
|
4
|
+
export async function exportGitWorktreeDiff(input) {
|
|
5
|
+
const worktreePath = path.resolve(input.worktreePath);
|
|
6
|
+
const blockers = [];
|
|
7
|
+
const branch = await runGitCommand(worktreePath, ['branch', '--show-current']);
|
|
8
|
+
const head = await runGitCommand(worktreePath, ['rev-parse', 'HEAD']);
|
|
9
|
+
const status = await runGitCommand(worktreePath, ['status', '--porcelain=v1', '--untracked-files=all']);
|
|
10
|
+
const untracked = await runGitCommand(worktreePath, ['ls-files', '--others', '--exclude-standard']);
|
|
11
|
+
const untrackedFiles = lines(untracked.stdout);
|
|
12
|
+
if (untrackedFiles.length) {
|
|
13
|
+
const addIntent = await runGitCommand(worktreePath, ['add', '-N', '--', ...untrackedFiles]);
|
|
14
|
+
if (!addIntent.ok)
|
|
15
|
+
blockers.push('git_worktree_untracked_intent_to_add_failed');
|
|
16
|
+
}
|
|
17
|
+
const diff = await runGitCommand(worktreePath, ['diff', '--binary', '--full-index', 'HEAD']);
|
|
18
|
+
const names = await runGitCommand(worktreePath, ['diff', '--name-only', 'HEAD']);
|
|
19
|
+
if (!status.ok)
|
|
20
|
+
blockers.push('git_worktree_status_failed');
|
|
21
|
+
if (!diff.ok)
|
|
22
|
+
blockers.push('git_worktree_diff_failed');
|
|
23
|
+
const trackedChanged = lines(names.stdout);
|
|
24
|
+
const changedFiles = [...new Set([...trackedChanged, ...untrackedFiles, ...statusFiles(status.stdout)])];
|
|
25
|
+
return {
|
|
26
|
+
schema: 'sks.git-worktree-diff.v1',
|
|
27
|
+
ok: blockers.length === 0,
|
|
28
|
+
generated_at: nowIso(),
|
|
29
|
+
mission_id: input.missionId,
|
|
30
|
+
worker_id: input.workerId,
|
|
31
|
+
main_repo_root: path.resolve(input.mainRepoRoot),
|
|
32
|
+
worktree_path: worktreePath,
|
|
33
|
+
branch: gitOutputLine(branch) || null,
|
|
34
|
+
base_head: null,
|
|
35
|
+
worktree_head: gitOutputLine(head) || null,
|
|
36
|
+
status_porcelain: status.stdout,
|
|
37
|
+
changed_files: changedFiles,
|
|
38
|
+
untracked_files: untrackedFiles,
|
|
39
|
+
diff: diff.stdout,
|
|
40
|
+
diff_bytes: Buffer.byteLength(diff.stdout),
|
|
41
|
+
clean: changedFiles.length === 0 && status.stdout.trim().length === 0,
|
|
42
|
+
blockers
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function lines(text) {
|
|
46
|
+
return String(text || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
function statusFiles(text) {
|
|
49
|
+
return lines(text).map((line) => {
|
|
50
|
+
const match = line.match(/^.{2}\s+(.*)$/) || line.match(/^\S+\s+(.*)$/);
|
|
51
|
+
const file = (match?.[1] || line).trim();
|
|
52
|
+
return file.includes(' -> ') ? file.split(' -> ').pop()?.trim() || file : file;
|
|
53
|
+
}).filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=git-worktree-diff.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import { ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
|
|
4
|
+
import { evaluateGitWorktreeCapability } from './git-worktree-capability.js';
|
|
5
|
+
import { gitBlocker, gitOutputLine, runGitCommand } from './git-worktree-runner.js';
|
|
6
|
+
import { sanitizePathPart } from './git-worktree-root.js';
|
|
7
|
+
export async function allocateWorkerWorktree(input) {
|
|
8
|
+
const capability = await evaluateGitWorktreeCapability({
|
|
9
|
+
root: input.repoRoot || process.cwd(),
|
|
10
|
+
missionId: input.missionId,
|
|
11
|
+
requireGitWorktree: true
|
|
12
|
+
});
|
|
13
|
+
const repoRoot = capability.detection.root || path.resolve(input.repoRoot || process.cwd());
|
|
14
|
+
const root = capability.root_resolution?.root || path.join(repoRoot, '.sneakoscope', 'blocked-worktrees');
|
|
15
|
+
const workerId = sanitizePathPart(input.workerId);
|
|
16
|
+
const slotId = sanitizePathPart(input.slotId || workerId);
|
|
17
|
+
const generationIndex = Math.max(1, Math.floor(Number(input.generationIndex || 1)));
|
|
18
|
+
const baseRef = input.baseRef || capability.detection.head || 'HEAD';
|
|
19
|
+
const branchPrefix = sanitizeBranchPart(input.branchPrefix || 'sks');
|
|
20
|
+
const branch = `${branchPrefix}/${sanitizeBranchPart(input.missionId)}/${sanitizeBranchPart(slotId)}-gen-${generationIndex}-${workerId}`;
|
|
21
|
+
const worktreePath = path.join(root, `${slotId}-gen-${generationIndex}-${workerId}`);
|
|
22
|
+
const blockers = [...capability.blockers];
|
|
23
|
+
let baseHead = capability.detection.head;
|
|
24
|
+
if (capability.ok) {
|
|
25
|
+
await ensureDir(root);
|
|
26
|
+
let add = null;
|
|
27
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
28
|
+
if (attempt > 1) {
|
|
29
|
+
await sleep(250 * attempt);
|
|
30
|
+
await runGitCommand(repoRoot, ['worktree', 'prune'], { timeoutMs: 30000 }).catch(() => null);
|
|
31
|
+
await fsp.rm(worktreePath, { recursive: true, force: true }).catch(() => null);
|
|
32
|
+
}
|
|
33
|
+
const existingBranch = await runGitCommand(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]);
|
|
34
|
+
const args = existingBranch.ok
|
|
35
|
+
? ['worktree', 'add', worktreePath, branch]
|
|
36
|
+
: ['worktree', 'add', '-b', branch, worktreePath, baseRef];
|
|
37
|
+
add = await runGitCommand(repoRoot, args, { timeoutMs: 120000 });
|
|
38
|
+
if (add.ok)
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (!add?.ok)
|
|
42
|
+
blockers.push(gitBlocker('git_worktree_add_failed', add));
|
|
43
|
+
if (add?.ok) {
|
|
44
|
+
const head = await runGitCommand(worktreePath, ['rev-parse', 'HEAD']);
|
|
45
|
+
baseHead = head.ok ? gitOutputLine(head) : baseHead;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const allocation = {
|
|
49
|
+
schema: 'sks.git-worktree-allocation.v1',
|
|
50
|
+
ok: blockers.length === 0,
|
|
51
|
+
created_at: nowIso(),
|
|
52
|
+
mission_id: input.missionId,
|
|
53
|
+
worker_id: workerId,
|
|
54
|
+
slot_id: slotId,
|
|
55
|
+
generation_index: generationIndex,
|
|
56
|
+
repo_root: repoRoot,
|
|
57
|
+
main_repo_root: repoRoot,
|
|
58
|
+
worktree_path: worktreePath,
|
|
59
|
+
branch,
|
|
60
|
+
base_ref: baseRef,
|
|
61
|
+
base_head: baseHead,
|
|
62
|
+
manifest_path: path.join(root, 'git-worktree-manifest.json'),
|
|
63
|
+
capability,
|
|
64
|
+
blockers: [...new Set(blockers)]
|
|
65
|
+
};
|
|
66
|
+
await appendWorktreeManifest(allocation);
|
|
67
|
+
return allocation;
|
|
68
|
+
}
|
|
69
|
+
async function appendWorktreeManifest(allocation) {
|
|
70
|
+
const current = await readJson(allocation.manifest_path, null).catch(() => null);
|
|
71
|
+
const allocations = Array.isArray(current?.allocations) ? current.allocations : [];
|
|
72
|
+
const key = (row) => `${row.mission_id}:${row.slot_id}:${row.generation_index}:${row.worker_id}`;
|
|
73
|
+
const nextAllocations = [
|
|
74
|
+
...allocations.filter((row) => key(row) !== key(allocation)),
|
|
75
|
+
allocation
|
|
76
|
+
];
|
|
77
|
+
const manifest = {
|
|
78
|
+
schema: 'sks.git-worktree-manifest.v1',
|
|
79
|
+
updated_at: nowIso(),
|
|
80
|
+
mission_id: allocation.mission_id,
|
|
81
|
+
repo_root: allocation.repo_root,
|
|
82
|
+
root: path.dirname(allocation.worktree_path),
|
|
83
|
+
allocations: nextAllocations
|
|
84
|
+
};
|
|
85
|
+
await writeJsonAtomic(allocation.manifest_path, manifest);
|
|
86
|
+
}
|
|
87
|
+
function sanitizeBranchPart(value) {
|
|
88
|
+
return sanitizePathPart(value).replace(/\./g, '-').slice(0, 48) || 'item';
|
|
89
|
+
}
|
|
90
|
+
function sleep(ms) {
|
|
91
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=git-worktree-manager.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { nowIso } from '../fsx.js';
|
|
2
|
+
import { runGitCommand } from './git-worktree-runner.js';
|
|
3
|
+
import { summarizeGitWorktreeConflict } from './git-worktree-conflict-resolver.js';
|
|
4
|
+
export async function applyGitWorktreeMergeQueue(input) {
|
|
5
|
+
const conflicts = [];
|
|
6
|
+
const changedFiles = new Set();
|
|
7
|
+
let appliedCount = 0;
|
|
8
|
+
let skippedCleanCount = 0;
|
|
9
|
+
for (const diff of input.diffs) {
|
|
10
|
+
for (const file of diff.changed_files)
|
|
11
|
+
changedFiles.add(file);
|
|
12
|
+
if (diff.clean || !diff.diff.trim()) {
|
|
13
|
+
skippedCleanCount += 1;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const check = await runGitCommand(input.integrationWorktreePath, ['apply', '--3way', '--check', '-'], {
|
|
17
|
+
input: diff.diff,
|
|
18
|
+
timeoutMs: 30000
|
|
19
|
+
});
|
|
20
|
+
if (!check.ok) {
|
|
21
|
+
conflicts.push(summarizeGitWorktreeConflict({
|
|
22
|
+
workerId: diff.worker_id,
|
|
23
|
+
changedFiles: diff.changed_files,
|
|
24
|
+
stderr: check.stderr || check.stdout
|
|
25
|
+
}));
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const apply = await runGitCommand(input.integrationWorktreePath, ['apply', '--3way', '-'], {
|
|
29
|
+
input: diff.diff,
|
|
30
|
+
timeoutMs: 30000
|
|
31
|
+
});
|
|
32
|
+
if (apply.ok)
|
|
33
|
+
appliedCount += 1;
|
|
34
|
+
else {
|
|
35
|
+
conflicts.push(summarizeGitWorktreeConflict({
|
|
36
|
+
workerId: diff.worker_id,
|
|
37
|
+
changedFiles: diff.changed_files,
|
|
38
|
+
stderr: apply.stderr || apply.stdout
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const blockers = conflicts.length ? ['git_worktree_merge_queue_conflicts'] : [];
|
|
43
|
+
return {
|
|
44
|
+
schema: 'sks.git-worktree-merge-queue.v1',
|
|
45
|
+
ok: blockers.length === 0,
|
|
46
|
+
generated_at: nowIso(),
|
|
47
|
+
integration_worktree_path: input.integrationWorktreePath,
|
|
48
|
+
applied_count: appliedCount,
|
|
49
|
+
skipped_clean_count: skippedCleanCount,
|
|
50
|
+
conflicts,
|
|
51
|
+
changed_files: [...changedFiles],
|
|
52
|
+
blockers
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=git-worktree-merge-queue.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function buildGitWorktreePatchEnvelope(input) {
|
|
2
|
+
const changedFiles = input.diff.changed_files.length ? input.diff.changed_files : ['git-worktree.diff'];
|
|
3
|
+
return {
|
|
4
|
+
schema: 'sks.agent-patch-envelope.v1',
|
|
5
|
+
source: 'git-worktree-diff',
|
|
6
|
+
mission_id: input.diff.mission_id,
|
|
7
|
+
route: '$Naruto',
|
|
8
|
+
agent_id: input.agentId,
|
|
9
|
+
session_id: input.sessionId,
|
|
10
|
+
slot_id: input.slotId,
|
|
11
|
+
generation_index: input.generationIndex,
|
|
12
|
+
lease_id: `git-worktree:${input.diff.worker_id}`,
|
|
13
|
+
allowed_paths: changedFiles,
|
|
14
|
+
git_worktree: {
|
|
15
|
+
main_repo_root: input.diff.main_repo_root,
|
|
16
|
+
worktree_path: input.diff.worktree_path,
|
|
17
|
+
branch: input.diff.branch,
|
|
18
|
+
base_head: input.diff.base_head,
|
|
19
|
+
worktree_head: input.diff.worktree_head,
|
|
20
|
+
changed_files: changedFiles,
|
|
21
|
+
diff_bytes: input.diff.diff_bytes
|
|
22
|
+
},
|
|
23
|
+
operations: [{
|
|
24
|
+
op: 'git_apply_patch',
|
|
25
|
+
path: '.',
|
|
26
|
+
diff: input.diff.diff
|
|
27
|
+
}],
|
|
28
|
+
rationale: 'Process-generated patch envelope exported from an isolated Git worktree diff.',
|
|
29
|
+
verification_hint: {
|
|
30
|
+
command: 'git apply --3way --check <diff>',
|
|
31
|
+
notes: 'Apply inside an integration worktree based on the recorded base head.'
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=git-worktree-patch-envelope.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { nowIso } from '../fsx.js';
|
|
2
|
+
export function planGitWorktreePool(input) {
|
|
3
|
+
const reusable = [...(input.reusableWorktrees || [])];
|
|
4
|
+
const assignments = input.workerIds.map((workerId) => {
|
|
5
|
+
const worktree = reusable.shift() || null;
|
|
6
|
+
return {
|
|
7
|
+
worker_id: workerId,
|
|
8
|
+
action: worktree ? 'reuse' : 'allocate',
|
|
9
|
+
worktree_path: worktree
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
schema: 'sks.git-worktree-pool.v1',
|
|
14
|
+
ok: true,
|
|
15
|
+
generated_at: nowIso(),
|
|
16
|
+
requested_workers: input.workerIds.length,
|
|
17
|
+
reusable_count: (input.reusableWorktrees || []).length,
|
|
18
|
+
allocate_count: assignments.filter((row) => row.action === 'allocate').length,
|
|
19
|
+
assignments,
|
|
20
|
+
blockers: []
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=git-worktree-pool.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { sha256 } from '../fsx.js';
|
|
5
|
+
export function resolveGitWorktreeRoot(input) {
|
|
6
|
+
const env = input.env || process.env;
|
|
7
|
+
const repoRoot = path.resolve(input.repoRoot);
|
|
8
|
+
const missionId = sanitizePathPart(input.missionId || 'mission');
|
|
9
|
+
const repoHash = sha256(repoRoot).slice(0, 16);
|
|
10
|
+
const explicitRoot = env.SKS_WORKTREE_ROOT ? path.resolve(env.SKS_WORKTREE_ROOT) : null;
|
|
11
|
+
const source = explicitRoot ? 'SKS_WORKTREE_ROOT' : env.XDG_CACHE_HOME ? 'XDG_CACHE_HOME' : 'HOME_CACHE';
|
|
12
|
+
const cacheBase = explicitRoot || path.join(env.XDG_CACHE_HOME || path.join(env.HOME || os.homedir(), '.cache'), 'sks', 'worktrees');
|
|
13
|
+
const root = explicitRoot ? path.join(cacheBase, repoHash, missionId) : path.join(cacheBase, repoHash, missionId);
|
|
14
|
+
const inRepo = isPathInside(root, repoRoot);
|
|
15
|
+
const allowInRepo = env.SKS_ALLOW_IN_REPO_WORKTREES === '1';
|
|
16
|
+
const blockers = inRepo && !allowInRepo ? ['git_worktree_root_inside_repo_blocked'] : [];
|
|
17
|
+
return {
|
|
18
|
+
schema: 'sks.git-worktree-root.v1',
|
|
19
|
+
ok: blockers.length === 0,
|
|
20
|
+
repo_root: repoRoot,
|
|
21
|
+
mission_id: missionId,
|
|
22
|
+
root,
|
|
23
|
+
source,
|
|
24
|
+
repo_hash: repoHash,
|
|
25
|
+
in_repo: inRepo,
|
|
26
|
+
allow_in_repo: allowInRepo,
|
|
27
|
+
blockers
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function sanitizePathPart(value) {
|
|
31
|
+
return String(value || 'item').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'item';
|
|
32
|
+
}
|
|
33
|
+
export function isPathInside(candidate, parent) {
|
|
34
|
+
const canonicalParent = canonicalPath(parent);
|
|
35
|
+
const canonicalCandidate = canonicalPath(candidate);
|
|
36
|
+
const rel = path.relative(canonicalParent, canonicalCandidate);
|
|
37
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
38
|
+
}
|
|
39
|
+
function canonicalPath(value) {
|
|
40
|
+
const resolved = path.resolve(value);
|
|
41
|
+
try {
|
|
42
|
+
return fs.realpathSync.native(resolved);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
const parent = path.dirname(resolved);
|
|
46
|
+
if (parent === resolved)
|
|
47
|
+
return resolved;
|
|
48
|
+
const base = canonicalPath(parent);
|
|
49
|
+
return path.join(base, path.basename(resolved));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=git-worktree-root.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { runProcess } from '../fsx.js';
|
|
2
|
+
export async function runGitCommand(cwd, args, opts = {}) {
|
|
3
|
+
const processOptions = {
|
|
4
|
+
cwd,
|
|
5
|
+
timeoutMs: opts.timeoutMs ?? 30000,
|
|
6
|
+
maxOutputBytes: 512 * 1024
|
|
7
|
+
};
|
|
8
|
+
if (opts.input !== undefined)
|
|
9
|
+
processOptions.input = opts.input;
|
|
10
|
+
const result = await runProcess('git', args, processOptions);
|
|
11
|
+
return normalizeGitResult(cwd, args, result);
|
|
12
|
+
}
|
|
13
|
+
export function normalizeGitResult(cwd, args, result) {
|
|
14
|
+
const stdout = result.stdout || '';
|
|
15
|
+
const stderr = result.stderr || '';
|
|
16
|
+
return {
|
|
17
|
+
ok: result.code === 0,
|
|
18
|
+
code: result.code,
|
|
19
|
+
args,
|
|
20
|
+
cwd,
|
|
21
|
+
stdout,
|
|
22
|
+
stderr,
|
|
23
|
+
stdout_tail: stdout.slice(-4000),
|
|
24
|
+
stderr_tail: stderr.slice(-4000),
|
|
25
|
+
timed_out: result.timedOut
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function gitOutputLine(result) {
|
|
29
|
+
return String(result.stdout || '').split(/\r?\n/).find((line) => line.trim())?.trim() || '';
|
|
30
|
+
}
|
|
31
|
+
export function gitBlocker(prefix, result) {
|
|
32
|
+
const combined = [result.stderr_tail || result.stderr, result.stdout_tail || result.stdout]
|
|
33
|
+
.map((value) => String(value || '').trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join('\n');
|
|
36
|
+
const detail = combined.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-3).join(' | ');
|
|
37
|
+
const meta = `code=${result.code ?? 'null'} timed_out=${result.timed_out ? '1' : '0'} args=${result.args.join(' ')}`;
|
|
38
|
+
return detail ? `${prefix}:${meta}:${detail.slice(0, 320)}` : `${prefix}:${meta}`;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=git-worktree-runner.js.map
|
|
@@ -1,4 +1,39 @@
|
|
|
1
1
|
import { createNarutoGeneration, completeNarutoGeneration } from './naruto-generation-scheduler.js';
|
|
2
|
+
export async function runNarutoActivePool(input) {
|
|
3
|
+
const base = simulateNarutoActivePool(input);
|
|
4
|
+
const allocations = [];
|
|
5
|
+
for (const item of input.graph.work_items) {
|
|
6
|
+
if (!item.write_allowed)
|
|
7
|
+
continue;
|
|
8
|
+
const mode = item.worktree?.mode || input.graph.worktree_policy.mode;
|
|
9
|
+
if (mode !== 'git-worktree') {
|
|
10
|
+
allocations.push({ work_item_id: item.id, mode, allocation_status: 'skipped', blockers: [] });
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (!input.allocateWorktree) {
|
|
14
|
+
allocations.push({ work_item_id: item.id, mode, allocation_status: 'planned', blockers: [] });
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const allocated = await input.allocateWorktree(item);
|
|
18
|
+
allocations.push({
|
|
19
|
+
work_item_id: item.id,
|
|
20
|
+
mode,
|
|
21
|
+
allocation_status: allocated.ok ? 'allocated' : 'planned',
|
|
22
|
+
...(allocated.worktree_path ? { worktree_path: allocated.worktree_path } : {}),
|
|
23
|
+
...(allocated.branch ? { branch: allocated.branch } : {}),
|
|
24
|
+
blockers: allocated.blockers || []
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const allocationBlockers = allocations.flatMap((row) => row.blockers);
|
|
28
|
+
return {
|
|
29
|
+
...base,
|
|
30
|
+
ok: base.ok && allocationBlockers.length === 0,
|
|
31
|
+
worktree_mode: input.graph.worktree_policy.mode,
|
|
32
|
+
worktree_allocation_required_count: allocations.filter((row) => row.mode === 'git-worktree').length,
|
|
33
|
+
worktree_allocations: allocations,
|
|
34
|
+
blockers: [...base.blockers, ...allocationBlockers]
|
|
35
|
+
};
|
|
36
|
+
}
|
|
2
37
|
export function simulateNarutoActivePool(input) {
|
|
3
38
|
const safeActiveWorkers = Math.max(1, input.governor.safe_active_workers);
|
|
4
39
|
const retryLimit = Math.max(0, Math.floor(Number(input.retryLimit ?? 1)));
|
|
@@ -12,6 +12,8 @@ export function buildNarutoGptFinalPack(input) {
|
|
|
12
12
|
},
|
|
13
13
|
role_distribution: input.roleDistribution,
|
|
14
14
|
changed_files: [...new Set((input.changedFiles || []).map(String))],
|
|
15
|
+
worktree_policy: input.worktreePolicy || input.graph.worktree_policy,
|
|
16
|
+
worktree_diffs: (input.worktreeDiffs || []).slice(0, maxPatchEnvelopes).map(redactSecrets),
|
|
15
17
|
patch_envelopes: (input.patchEnvelopes || []).slice(0, maxPatchEnvelopes).map(redactSecrets),
|
|
16
18
|
verification_results: (input.verificationResults || []).slice(0, 200).map(redactSecrets),
|
|
17
19
|
failed_shards: (input.failedShards || []).slice(0, 100).map(redactSecrets),
|
|
@@ -33,6 +33,13 @@ export function buildNarutoWorkGraph(input = {}) {
|
|
|
33
33
|
const basePath = normalizeNarutoPath(input.leaseBasePath || '.sneakoscope/naruto/patch-envelopes');
|
|
34
34
|
const targetPaths = normalizePaths(input.targetPaths || []);
|
|
35
35
|
const readonlyPaths = normalizePaths(input.readonlyPaths || []);
|
|
36
|
+
const worktreePolicy = input.worktreePolicy || {
|
|
37
|
+
mode: 'patch-envelope-only',
|
|
38
|
+
required: false,
|
|
39
|
+
main_repo_root: null,
|
|
40
|
+
worktree_root: null,
|
|
41
|
+
fallback_reason: writeCapable ? 'git_capability_not_evaluated' : 'readonly_or_write_disabled'
|
|
42
|
+
};
|
|
36
43
|
const workItems = [];
|
|
37
44
|
for (let index = 0; index < totalWorkItems; index += 1) {
|
|
38
45
|
const id = `NW-${String(index + 1).padStart(6, '0')}`;
|
|
@@ -64,7 +71,14 @@ export function buildNarutoWorkGraph(input = {}) {
|
|
|
64
71
|
requires_patch_envelope: writePaths.length > 0,
|
|
65
72
|
requires_verification: kind !== 'research' && kind !== 'final_review_input_pack',
|
|
66
73
|
requires_gpt_final: writePaths.length > 0 || kind === 'final_review_input_pack'
|
|
67
|
-
}
|
|
74
|
+
},
|
|
75
|
+
...(writePaths.length > 0 ? {
|
|
76
|
+
worktree: {
|
|
77
|
+
mode: worktreePolicy.mode,
|
|
78
|
+
required: worktreePolicy.required,
|
|
79
|
+
allocation_required: worktreePolicy.mode === 'git-worktree'
|
|
80
|
+
}
|
|
81
|
+
} : {})
|
|
68
82
|
});
|
|
69
83
|
}
|
|
70
84
|
const activeWaves = planNarutoWorkWaves(workItems, Math.max(1, normalizePositiveInt(input.maxActiveWorkers, requestedClones)));
|
|
@@ -88,6 +102,7 @@ export function buildNarutoWorkGraph(input = {}) {
|
|
|
88
102
|
active_waves: activeWaves,
|
|
89
103
|
mixed_work_kinds: mixedWorkKinds,
|
|
90
104
|
write_allowed_count: writeAllowedCount,
|
|
105
|
+
worktree_policy: worktreePolicy,
|
|
91
106
|
ok: blockers.length === 0,
|
|
92
107
|
blockers
|
|
93
108
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
export const RELEASE_GATE_CACHE_V2_SCHEMA = 'sks.release-gate-cache.v2';
|
|
5
|
+
export function releaseGateCacheFile(root) {
|
|
6
|
+
return path.join(root, '.sneakoscope', 'reports', 'release-gates', 'cache-v2.json');
|
|
7
|
+
}
|
|
8
|
+
export function releaseGateCacheKey(root, gate) {
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
10
|
+
const hash = crypto.createHash('sha256');
|
|
11
|
+
hash.update(gate.id);
|
|
12
|
+
hash.update(gate.command);
|
|
13
|
+
hash.update(String(pkg.version || ''));
|
|
14
|
+
hash.update(process.version);
|
|
15
|
+
hash.update(String(process.env.npm_config_user_agent || ''));
|
|
16
|
+
hash.update(JSON.stringify(gate.resource || []));
|
|
17
|
+
hash.update(JSON.stringify(gate.preset || []));
|
|
18
|
+
hashFileIfPresent(hash, path.join(root, 'release-gates.v2.json'));
|
|
19
|
+
hashFileIfPresent(hash, path.join(root, 'package.json'));
|
|
20
|
+
hashFileIfPresent(hash, path.join(root, 'dist', 'build-manifest.json'));
|
|
21
|
+
for (const input of gate.cache.inputs) {
|
|
22
|
+
const file = path.join(root, input);
|
|
23
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile())
|
|
24
|
+
hashFileIfPresent(hash, file);
|
|
25
|
+
else
|
|
26
|
+
hash.update(input);
|
|
27
|
+
}
|
|
28
|
+
return hash.digest('hex');
|
|
29
|
+
}
|
|
30
|
+
export function readReleaseGateCacheHit(root, gate) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(fs.readFileSync(releaseGateCacheFile(root), 'utf8'));
|
|
33
|
+
return parsed.schema === RELEASE_GATE_CACHE_V2_SCHEMA && parsed.records?.[releaseGateCacheKey(root, gate)]?.ok === true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function writeReleaseGateCacheHit(root, gate) {
|
|
40
|
+
const file = releaseGateCacheFile(root);
|
|
41
|
+
let parsed = { schema: RELEASE_GATE_CACHE_V2_SCHEMA, records: {} };
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
parsed.schema = RELEASE_GATE_CACHE_V2_SCHEMA;
|
|
47
|
+
parsed.records ||= {};
|
|
48
|
+
parsed.records[releaseGateCacheKey(root, gate)] = {
|
|
49
|
+
ok: true,
|
|
50
|
+
gate_id: gate.id,
|
|
51
|
+
command: gate.command,
|
|
52
|
+
resource: gate.resource,
|
|
53
|
+
preset: gate.preset,
|
|
54
|
+
recorded_at: new Date().toISOString()
|
|
55
|
+
};
|
|
56
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
57
|
+
fs.writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
58
|
+
}
|
|
59
|
+
function hashFileIfPresent(hash, file) {
|
|
60
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile())
|
|
61
|
+
hash.update(fs.readFileSync(file));
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=release-gate-cache-v2.js.map
|