sneakoscope 3.0.4 → 3.1.0
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/cli/command-registry.js +1 -0
- package/dist/cli/context7-command.js +29 -5
- package/dist/cli/install-helpers.js +15 -7
- package/dist/core/agents/runtime-proof-summary.js +4 -0
- package/dist/core/commands/goal-command.js +19 -1
- package/dist/core/commands/loop-command.js +135 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +6 -1
- package/dist/core/loops/goal-to-loop-compat.js +23 -0
- package/dist/core/loops/loop-artifacts.js +41 -0
- package/dist/core/loops/loop-decomposer.js +56 -0
- package/dist/core/loops/loop-finalizer.js +28 -0
- package/dist/core/loops/loop-gate-ladder.js +16 -0
- package/dist/core/loops/loop-gate-runner.js +29 -0
- package/dist/core/loops/loop-gate-selector.js +52 -0
- package/dist/core/loops/loop-iteration-runner.js +2 -0
- package/dist/core/loops/loop-lease.js +76 -0
- package/dist/core/loops/loop-observability.js +19 -0
- package/dist/core/loops/loop-owner-inference.js +57 -0
- package/dist/core/loops/loop-owner-ledger.js +2 -0
- package/dist/core/loops/loop-planner.js +139 -0
- package/dist/core/loops/loop-proof-summary.js +10 -0
- package/dist/core/loops/loop-proof.js +2 -0
- package/dist/core/loops/loop-risk-classifier.js +42 -0
- package/dist/core/loops/loop-runtime.js +159 -0
- package/dist/core/loops/loop-scheduler.js +60 -0
- package/dist/core/loops/loop-schema.js +63 -0
- package/dist/core/loops/loop-state.js +61 -0
- package/dist/core/naruto/naruto-loop-mesh.js +33 -0
- package/dist/core/naruto/naruto-loop-worker-router.js +38 -0
- package/dist/core/pipeline-internals/runtime-core.js +82 -2
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-slot-column-anchor.js +5 -2
- package/dist/core/zellij/zellij-slot-pane-renderer.js +2 -0
- package/dist/scripts/loop-directive-check-lib.js +165 -0
- package/package.json +34 -2
- package/schemas/loops/loop-node.schema.json +21 -0
- package/schemas/loops/loop-plan.schema.json +21 -0
- package/schemas/loops/loop-proof.schema.json +20 -0
- package/schemas/loops/loop-state.schema.json +19 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function selectLoopGates(input) {
|
|
2
|
+
const files = input.changedFiles.join(' ');
|
|
3
|
+
const triage = ['loop:state-valid', 'loop:budget-valid'];
|
|
4
|
+
const local = new Set();
|
|
5
|
+
if (input.node.level === 'L0-report')
|
|
6
|
+
return { triage, local: [], checker: [], integration: [], final: [] };
|
|
7
|
+
if (isDocsOnly(input.changedFiles, input.node))
|
|
8
|
+
add(local, ['docs:loop-runtime', 'changelog:check']);
|
|
9
|
+
else if (/zellij/.test(files) || input.node.loop_id.includes('zellij'))
|
|
10
|
+
add(local, ['zellij:slot-telemetry-live-flush', 'zellij:slot-pane-stale-detection']);
|
|
11
|
+
else if (/release/.test(files) || input.node.loop_id.includes('release'))
|
|
12
|
+
add(local, ['release:affected-selector', 'release:dynamic-presets']);
|
|
13
|
+
else if (/research/.test(files) || input.node.loop_id.includes('research'))
|
|
14
|
+
add(local, ['research:quality-contract']);
|
|
15
|
+
else if (/qa-loop/.test(files) || input.node.loop_id.includes('qa-loop'))
|
|
16
|
+
add(local, ['qa-loop:app-handoff-gate-lifecycle']);
|
|
17
|
+
else if (/codex/.test(files) || input.node.loop_id.includes('codex'))
|
|
18
|
+
add(local, ['codex:0139-capability', 'codex-sdk:version-compat']);
|
|
19
|
+
else if (/mad-db|db-safety/.test(files) || input.node.loop_id.includes('mad-db'))
|
|
20
|
+
add(local, ['mad-db:safety-conflict-matrix', 'mad-db:operation-lifecycle-ledger']);
|
|
21
|
+
else if (/agent|scheduler|native-swarm/.test(files) || input.node.loop_id.includes('naruto'))
|
|
22
|
+
add(local, ['parallel:runtime-real-blackbox', 'scheduler:utilization-proof']);
|
|
23
|
+
else
|
|
24
|
+
add(local, ['loop:affected']);
|
|
25
|
+
const integration = new Set();
|
|
26
|
+
if ((input.packageScriptsChanged || []).length || (input.releaseGateIdsChanged || []).length || files.includes('package.json') || files.includes('release-gates.v2.json')) {
|
|
27
|
+
integration.add('release:dag-full-coverage');
|
|
28
|
+
}
|
|
29
|
+
if (input.risk.level === 'high')
|
|
30
|
+
integration.add('loop:integration-required');
|
|
31
|
+
const final = new Set();
|
|
32
|
+
if (input.changedFiles.length || input.risk.requires_gpt_final)
|
|
33
|
+
final.add('gpt:final-arbiter');
|
|
34
|
+
if (input.risk.level === 'critical')
|
|
35
|
+
final.add('human:handoff-required');
|
|
36
|
+
return {
|
|
37
|
+
triage,
|
|
38
|
+
local: [...local],
|
|
39
|
+
checker: input.node.level === 'L2-action' ? ['loop:checker-fresh-session'] : [],
|
|
40
|
+
integration: [...integration],
|
|
41
|
+
final: [...final]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function isDocsOnly(files, node) {
|
|
45
|
+
const scoped = [...files, ...node.owner_scope.files, ...node.owner_scope.directories];
|
|
46
|
+
return scoped.length > 0 && scoped.every((file) => file === 'README.md' || file === 'CHANGELOG.md' || file.startsWith('docs'));
|
|
47
|
+
}
|
|
48
|
+
function add(target, values) {
|
|
49
|
+
for (const value of values)
|
|
50
|
+
target.add(value);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=loop-gate-selector.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { loopOwnerLedgerPath } from './loop-artifacts.js';
|
|
2
|
+
import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
export async function acquireLoopLease(root, plan, node) {
|
|
4
|
+
const blockers = await detectLoopLeaseConflicts(root, plan.mission_id, node);
|
|
5
|
+
const lease = {
|
|
6
|
+
schema: 'sks.loop-lease.v1',
|
|
7
|
+
mission_id: plan.mission_id,
|
|
8
|
+
loop_id: node.loop_id,
|
|
9
|
+
owner_scope: node.owner_scope,
|
|
10
|
+
acquired_at: nowIso(),
|
|
11
|
+
expires_at: new Date(Date.now() + Math.max(60_000, node.budget.max_wall_ms)).toISOString(),
|
|
12
|
+
status: blockers.length ? 'conflict' : 'active',
|
|
13
|
+
worktree_id: node.worktree.required ? `sks-loop-${node.loop_id}` : null,
|
|
14
|
+
blockers
|
|
15
|
+
};
|
|
16
|
+
const ledger = await readLoopOwnerLedger(root, plan.mission_id);
|
|
17
|
+
const leases = ledger.leases.filter((row) => row.loop_id !== node.loop_id);
|
|
18
|
+
leases.push(lease);
|
|
19
|
+
await writeLoopOwnerLedger(root, plan.mission_id, leases);
|
|
20
|
+
return lease;
|
|
21
|
+
}
|
|
22
|
+
export async function releaseLoopLease(root, missionId, loopId) {
|
|
23
|
+
const ledger = await readLoopOwnerLedger(root, missionId);
|
|
24
|
+
const leases = ledger.leases.map((lease) => lease.loop_id === loopId ? { ...lease, status: 'released' } : lease);
|
|
25
|
+
await writeLoopOwnerLedger(root, missionId, leases);
|
|
26
|
+
}
|
|
27
|
+
export async function detectLoopLeaseConflicts(root, missionId, node) {
|
|
28
|
+
const ledger = await readLoopOwnerLedger(root, missionId);
|
|
29
|
+
const active = ledger.leases.filter((lease) => lease.status === 'active' && Date.parse(lease.expires_at) > Date.now());
|
|
30
|
+
const blockers = [];
|
|
31
|
+
for (const lease of active) {
|
|
32
|
+
if (lease.loop_id === node.loop_id)
|
|
33
|
+
continue;
|
|
34
|
+
if (overlap(lease.owner_scope.files, node.owner_scope.files).length && (lease.owner_scope.exclusive || node.owner_scope.exclusive)) {
|
|
35
|
+
blockers.push(`lease_file_conflict:${lease.loop_id}`);
|
|
36
|
+
}
|
|
37
|
+
if (overlap(lease.owner_scope.package_scripts, node.owner_scope.package_scripts).length)
|
|
38
|
+
blockers.push(`lease_package_script_conflict:${lease.loop_id}`);
|
|
39
|
+
if (node.owner_scope.files.includes('release-gates.v2.json') || lease.owner_scope.files.includes('release-gates.v2.json')) {
|
|
40
|
+
blockers.push(`lease_release_gates_integration_only:${lease.loop_id}`);
|
|
41
|
+
}
|
|
42
|
+
const docsOnly = allDocs(lease.owner_scope) && allDocs(node.owner_scope);
|
|
43
|
+
if (docsOnly) {
|
|
44
|
+
for (let i = blockers.length - 1; i >= 0; i -= 1) {
|
|
45
|
+
if (blockers[i]?.includes(lease.loop_id))
|
|
46
|
+
blockers.splice(i, 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [...new Set(blockers)];
|
|
51
|
+
}
|
|
52
|
+
async function readLoopOwnerLedger(root, missionId) {
|
|
53
|
+
return readJson(loopOwnerLedgerPath(root, missionId), {
|
|
54
|
+
schema: 'sks.loop-owner-ledger.v1',
|
|
55
|
+
mission_id: missionId,
|
|
56
|
+
updated_at: nowIso(),
|
|
57
|
+
leases: []
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function writeLoopOwnerLedger(root, missionId, leases) {
|
|
61
|
+
await writeJsonAtomic(loopOwnerLedgerPath(root, missionId), {
|
|
62
|
+
schema: 'sks.loop-owner-ledger.v1',
|
|
63
|
+
mission_id: missionId,
|
|
64
|
+
updated_at: nowIso(),
|
|
65
|
+
leases
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function overlap(a, b) {
|
|
69
|
+
const rhs = new Set(b);
|
|
70
|
+
return a.filter((value) => rhs.has(value));
|
|
71
|
+
}
|
|
72
|
+
function allDocs(scope) {
|
|
73
|
+
const values = [...scope.files, ...scope.directories];
|
|
74
|
+
return values.length > 0 && values.every((value) => value === 'README.md' || value === 'CHANGELOG.md' || value.startsWith('docs'));
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=loop-lease.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readJson } from '../fsx.js';
|
|
2
|
+
import { loopGraphProofPath } from './loop-artifacts.js';
|
|
3
|
+
export async function readLoopGraphProof(root, missionId) {
|
|
4
|
+
return readJson(loopGraphProofPath(root, missionId), null);
|
|
5
|
+
}
|
|
6
|
+
export function summarizeLoopGraphProof(proof) {
|
|
7
|
+
if (!proof)
|
|
8
|
+
return { total: 0, running: 0, completed: 0, blocked: 0, speedup_ratio: 0, active_loop_ids: [], blocked_loop_ids: [] };
|
|
9
|
+
return {
|
|
10
|
+
total: proof.total_loops,
|
|
11
|
+
running: Math.max(0, proof.total_loops - proof.completed_loops - proof.blocked_loops - proof.failed_loops - proof.handoff_loops),
|
|
12
|
+
completed: proof.completed_loops,
|
|
13
|
+
blocked: proof.blocked_loops + proof.failed_loops + proof.handoff_loops,
|
|
14
|
+
speedup_ratio: proof.parallelism.speedup_ratio,
|
|
15
|
+
active_loop_ids: [],
|
|
16
|
+
blocked_loop_ids: proof.blockers
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=loop-observability.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function inferLoopOwnerScope(input) {
|
|
2
|
+
if (input.integration) {
|
|
3
|
+
return {
|
|
4
|
+
files: ['CHANGELOG.md'],
|
|
5
|
+
directories: ['.sneakoscope/missions'],
|
|
6
|
+
package_scripts: [],
|
|
7
|
+
release_gate_ids: ['release:dag-full-coverage'],
|
|
8
|
+
exclusive: true,
|
|
9
|
+
collision_policy: 'integration-only'
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const files = [...new Set(input.domain.files.filter((file) => !['package.json', 'release-gates.v2.json'].includes(file)))];
|
|
13
|
+
const releaseGateIds = input.domain.gates.filter((gate) => !gate.includes('*'));
|
|
14
|
+
const ownsPackage = input.domain.files.includes('package.json');
|
|
15
|
+
return {
|
|
16
|
+
files,
|
|
17
|
+
directories: input.domain.dirs.filter((dir) => dir !== 'package.json' && dir !== 'release-gates.v2.json'),
|
|
18
|
+
package_scripts: ownsPackage ? [] : inferPackageScripts(input.domain.id),
|
|
19
|
+
release_gate_ids: releaseGateIds,
|
|
20
|
+
exclusive: input.domain.id !== 'docs',
|
|
21
|
+
collision_policy: input.domain.id === 'docs' ? 'wait' : 'handoff'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function detectOwnerScopeCollisions(scopes) {
|
|
25
|
+
const blockers = [];
|
|
26
|
+
for (let i = 0; i < scopes.length; i += 1) {
|
|
27
|
+
for (let j = i + 1; j < scopes.length; j += 1) {
|
|
28
|
+
const a = scopes[i];
|
|
29
|
+
const b = scopes[j];
|
|
30
|
+
if (!a || !b)
|
|
31
|
+
continue;
|
|
32
|
+
const fileOverlap = intersection(a.owner_scope.files, b.owner_scope.files);
|
|
33
|
+
const scriptOverlap = intersection(a.owner_scope.package_scripts, b.owner_scope.package_scripts);
|
|
34
|
+
if (fileOverlap.length && (a.owner_scope.exclusive || b.owner_scope.exclusive))
|
|
35
|
+
blockers.push(`file_collision:${a.loop_id}:${b.loop_id}:${fileOverlap.join(',')}`);
|
|
36
|
+
if (scriptOverlap.length)
|
|
37
|
+
blockers.push(`script_collision:${a.loop_id}:${b.loop_id}:${scriptOverlap.join(',')}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return blockers;
|
|
41
|
+
}
|
|
42
|
+
function inferPackageScripts(domainId) {
|
|
43
|
+
if (domainId === 'docs')
|
|
44
|
+
return ['docs:loop-runtime'];
|
|
45
|
+
if (domainId === 'naruto')
|
|
46
|
+
return ['naruto:loop-mesh'];
|
|
47
|
+
if (domainId === 'release')
|
|
48
|
+
return ['release:dag-full-coverage'];
|
|
49
|
+
if (domainId === 'loop-general-coding')
|
|
50
|
+
return ['loop:runtime'];
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
function intersection(a, b) {
|
|
54
|
+
const rhs = new Set(b);
|
|
55
|
+
return a.filter((value) => rhs.has(value));
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=loop-owner-inference.js.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
2
|
+
import { decomposeRequestIntoLoopDomains } from './loop-decomposer.js';
|
|
3
|
+
import { loopPlanPath, loopRunLogPath, loopStatePath } from './loop-artifacts.js';
|
|
4
|
+
import { selectLoopGates } from './loop-gate-selector.js';
|
|
5
|
+
import { inferLoopOwnerScope } from './loop-owner-inference.js';
|
|
6
|
+
import { classifyLoopRisk } from './loop-risk-classifier.js';
|
|
7
|
+
import { defaultLoopBudget, validateLoopPlan } from './loop-schema.js';
|
|
8
|
+
export async function planLoopsFromRequest(input) {
|
|
9
|
+
const maxLoops = Math.max(1, Math.min(32, input.maxLoops || 8));
|
|
10
|
+
const domains = decomposeRequestIntoLoopDomains(input.request).slice(0, maxLoops);
|
|
11
|
+
const actionNodes = domains.map((domain) => {
|
|
12
|
+
const loopId = `loop-${domain.id}`;
|
|
13
|
+
const ownerScope = inferLoopOwnerScope({ domain });
|
|
14
|
+
const risk = classifyLoopRisk({ loop_id: loopId, owner_scope: ownerScope, level: 'L2-action' });
|
|
15
|
+
const nodeBase = makeNode({
|
|
16
|
+
missionId: input.missionId,
|
|
17
|
+
loopId,
|
|
18
|
+
title: titleFromDomain(domain.id),
|
|
19
|
+
purpose: `Execute ${domain.id} slice for: ${input.request}`,
|
|
20
|
+
ownerScope,
|
|
21
|
+
dependencies: [],
|
|
22
|
+
route: domain.id === 'docs' ? '$Loop' : '$Naruto',
|
|
23
|
+
level: domain.id === 'docs' ? 'L1-assisted' : 'L2-action',
|
|
24
|
+
risk
|
|
25
|
+
});
|
|
26
|
+
return { ...nodeBase, gates: selectLoopGates({ node: nodeBase, changedFiles: [...ownerScope.files, ...ownerScope.directories], risk }) };
|
|
27
|
+
});
|
|
28
|
+
const integrationOwner = inferLoopOwnerScope({ domain: { id: 'integration', dirs: [], files: [], gates: ['release:dag-full-coverage'] }, integration: true });
|
|
29
|
+
const integrationRisk = classifyLoopRisk({ loop_id: 'loop-integration', owner_scope: integrationOwner, level: 'L1-assisted' });
|
|
30
|
+
const integrationBase = makeNode({
|
|
31
|
+
missionId: input.missionId,
|
|
32
|
+
loopId: 'loop-integration',
|
|
33
|
+
title: 'Integration loop finalizer',
|
|
34
|
+
purpose: 'Merge loop proofs, run integration gates, and require GPT final arbitration when source mutation exists.',
|
|
35
|
+
ownerScope: integrationOwner,
|
|
36
|
+
dependencies: actionNodes.map((node) => node.loop_id),
|
|
37
|
+
route: '$Integration',
|
|
38
|
+
level: 'L1-assisted',
|
|
39
|
+
risk: integrationRisk
|
|
40
|
+
});
|
|
41
|
+
const integrationNode = {
|
|
42
|
+
...integrationBase,
|
|
43
|
+
gates: selectLoopGates({
|
|
44
|
+
node: integrationBase,
|
|
45
|
+
changedFiles: ['package.json', 'release-gates.v2.json', 'CHANGELOG.md'],
|
|
46
|
+
risk: integrationRisk,
|
|
47
|
+
packageScriptsChanged: ['loop:runtime'],
|
|
48
|
+
releaseGateIdsChanged: ['release:dag-full-coverage']
|
|
49
|
+
})
|
|
50
|
+
};
|
|
51
|
+
const nodes = [...actionNodes, integrationNode];
|
|
52
|
+
const plan = {
|
|
53
|
+
schema: 'sks.loop-plan.v1',
|
|
54
|
+
mission_id: input.missionId,
|
|
55
|
+
request: input.request,
|
|
56
|
+
generated_at: new Date().toISOString(),
|
|
57
|
+
planner: {
|
|
58
|
+
route: '$Loop',
|
|
59
|
+
model_policy: input.mode === 'codex-assisted' ? 'codex-sdk' : 'deterministic',
|
|
60
|
+
confidence: actionNodes.length ? 'high' : 'medium'
|
|
61
|
+
},
|
|
62
|
+
graph: {
|
|
63
|
+
nodes,
|
|
64
|
+
edges: actionNodes.map((node) => ({ from: node.loop_id, to: integrationNode.loop_id, reason: 'integration_after_loop_proof' }))
|
|
65
|
+
},
|
|
66
|
+
global_budget: defaultLoopBudget({
|
|
67
|
+
max_iterations: Math.max(...nodes.map((node) => node.budget.max_iterations)),
|
|
68
|
+
max_subagents: nodes.reduce((sum, node) => sum + node.budget.max_subagents, 0)
|
|
69
|
+
}),
|
|
70
|
+
safety: {
|
|
71
|
+
no_unrequested_fallback_code: true,
|
|
72
|
+
require_owner_lease: true,
|
|
73
|
+
require_checker_for_action: true,
|
|
74
|
+
require_gpt_final_for_source_mutation: true
|
|
75
|
+
},
|
|
76
|
+
integration_loop_id: integrationNode.loop_id,
|
|
77
|
+
compatibility: {
|
|
78
|
+
goal_compat_artifact: input.sourceCommand === 'goal' ? `.sneakoscope/missions/${input.missionId}/goal-compat.json` : null,
|
|
79
|
+
source_command: input.sourceCommand
|
|
80
|
+
},
|
|
81
|
+
blockers: []
|
|
82
|
+
};
|
|
83
|
+
const validation = validateLoopPlan(plan);
|
|
84
|
+
plan.blockers = validation.blockers;
|
|
85
|
+
await writeJsonAtomic(loopPlanPath(input.root, input.missionId), plan);
|
|
86
|
+
return plan;
|
|
87
|
+
}
|
|
88
|
+
function makeNode(input) {
|
|
89
|
+
const budget = defaultLoopBudget({
|
|
90
|
+
max_subagents: input.route === '$Integration' ? 2 : 4,
|
|
91
|
+
max_changed_files: input.ownerScope.files.length ? Math.max(4, input.ownerScope.files.length + 2) : 12
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
schema: 'sks.loop-node.v1',
|
|
95
|
+
loop_id: input.loopId,
|
|
96
|
+
mission_id: input.missionId,
|
|
97
|
+
title: input.title,
|
|
98
|
+
purpose: input.purpose,
|
|
99
|
+
level: input.level,
|
|
100
|
+
route: input.route,
|
|
101
|
+
owner_scope: input.ownerScope,
|
|
102
|
+
state_file: loopStatePath('', input.missionId, input.loopId).replace(/^\/?/, ''),
|
|
103
|
+
run_log_file: loopRunLogPath('', input.missionId, input.loopId).replace(/^\/?/, ''),
|
|
104
|
+
budget,
|
|
105
|
+
maker: {
|
|
106
|
+
route: '$Naruto',
|
|
107
|
+
role: input.route === '$Integration' ? 'planner' : input.loopId.includes('docs') ? 'writer' : 'implementer',
|
|
108
|
+
worker_count: input.route === '$Integration' ? 1 : 2,
|
|
109
|
+
backend_preference: ['codex-sdk', 'python-codex-sdk', 'local-llm'],
|
|
110
|
+
local_draft_allowed: input.risk.level !== 'critical',
|
|
111
|
+
gpt_final_required: input.risk.requires_gpt_final
|
|
112
|
+
},
|
|
113
|
+
checker: {
|
|
114
|
+
route: input.loopId.includes('research') ? '$Research' : input.loopId.includes('docs') ? '$DFix' : '$QA-LOOP',
|
|
115
|
+
worker_count: input.route === '$Integration' ? 1 : 1,
|
|
116
|
+
fresh_session_required: true,
|
|
117
|
+
stronger_model_required: input.risk.level === 'high' || input.risk.level === 'critical',
|
|
118
|
+
required_before_next_iteration: input.level === 'L2-action'
|
|
119
|
+
},
|
|
120
|
+
gates: { triage: [], local: [], checker: [], integration: [], final: [] },
|
|
121
|
+
dependencies: input.dependencies,
|
|
122
|
+
handoff_policy: {
|
|
123
|
+
allow_handoff: true,
|
|
124
|
+
reasons: input.risk.requires_human_handoff ? ['critical_risk_requires_handoff'] : [],
|
|
125
|
+
artifact: null
|
|
126
|
+
},
|
|
127
|
+
worktree: {
|
|
128
|
+
required: input.risk.requires_worktree,
|
|
129
|
+
mode: input.risk.requires_worktree ? 'new-worktree' : 'none',
|
|
130
|
+
branch_prefix: `sks/loop/${input.missionId}`,
|
|
131
|
+
cleanup: input.risk.level === 'low' ? 'on-success' : 'keep-on-failure'
|
|
132
|
+
},
|
|
133
|
+
risk: input.risk
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function titleFromDomain(domainId) {
|
|
137
|
+
return domainId === 'loop-general-coding' ? 'General coding loop' : `${domainId} loop`;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=loop-planner.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function renderLoopProofSummary(proof) {
|
|
2
|
+
return [
|
|
3
|
+
`Loop graph: ${proof.ok ? 'passed' : 'blocked'}`,
|
|
4
|
+
`Loops: ${proof.total_loops} total / ${proof.completed_loops} done / ${proof.handoff_loops} handoff`,
|
|
5
|
+
`Parallelism: ${proof.parallelism.max_active_loops} active loops / ${proof.parallelism.max_active_workers} max workers / ${proof.parallelism.speedup_ratio}x speedup`,
|
|
6
|
+
`Gates: ${proof.gates.selected.length} selected / ${proof.gates.passed.length} passed`,
|
|
7
|
+
`Blocked: ${proof.blockers.length ? proof.blockers.join(', ') : 'none'}`
|
|
8
|
+
].join('\n');
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=loop-proof-summary.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function classifyLoopRisk(node) {
|
|
2
|
+
const scope = [
|
|
3
|
+
...node.owner_scope.files,
|
|
4
|
+
...node.owner_scope.directories,
|
|
5
|
+
...node.owner_scope.package_scripts,
|
|
6
|
+
...node.owner_scope.release_gate_ids
|
|
7
|
+
].join(' ').toLowerCase();
|
|
8
|
+
const reasons = [];
|
|
9
|
+
let level = 'low';
|
|
10
|
+
if (/(db|mad-db|mcp|token|auth|postinstall|publish|global config)/.test(scope)) {
|
|
11
|
+
level = 'critical';
|
|
12
|
+
reasons.push('critical_scope');
|
|
13
|
+
}
|
|
14
|
+
else if (/(release-gates|worktree|scheduler|zellij|codex-control|agent|native-swarm)/.test(scope)) {
|
|
15
|
+
level = 'high';
|
|
16
|
+
reasons.push('runtime_or_scheduler_scope');
|
|
17
|
+
}
|
|
18
|
+
else if (/(qa-loop|research|image|docs)/.test(scope)) {
|
|
19
|
+
level = 'medium';
|
|
20
|
+
reasons.push('domain_scope');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
reasons.push('bounded_scope');
|
|
24
|
+
}
|
|
25
|
+
const requiresHuman = level === 'critical';
|
|
26
|
+
return {
|
|
27
|
+
level,
|
|
28
|
+
reasons,
|
|
29
|
+
requires_worktree: level === 'medium' || level === 'high' || level === 'critical',
|
|
30
|
+
requires_gpt_final: level !== 'low',
|
|
31
|
+
requires_human_handoff: requiresHuman
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function loopLevelAllowedUnattended(node) {
|
|
35
|
+
return node.level === 'L3-unattended'
|
|
36
|
+
&& (node.risk.level === 'low' || node.risk.level === 'medium')
|
|
37
|
+
&& node.owner_scope.exclusive
|
|
38
|
+
&& node.budget.max_changed_files <= 8
|
|
39
|
+
&& node.gates.local.length > 0
|
|
40
|
+
&& !node.risk.requires_human_handoff;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=loop-risk-classifier.js.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
2
|
+
import { loopBudgetPath, loopPatchPath, loopProofPath } from './loop-artifacts.js';
|
|
3
|
+
import { finalizeLoopGraph } from './loop-finalizer.js';
|
|
4
|
+
import { runLoopGates } from './loop-gate-runner.js';
|
|
5
|
+
import { acquireLoopLease, releaseLoopLease } from './loop-lease.js';
|
|
6
|
+
import { scheduleLoopGraph } from './loop-scheduler.js';
|
|
7
|
+
import { appendLoopRunLog, initialLoopState, updateLoopState, writeLoopState } from './loop-state.js';
|
|
8
|
+
export async function runLoopPlan(input) {
|
|
9
|
+
const started = Date.now();
|
|
10
|
+
const schedule = scheduleLoopGraph(input.plan.graph.nodes, input.parallelism || 'balanced');
|
|
11
|
+
const proofs = [];
|
|
12
|
+
for (const batch of schedule.batches) {
|
|
13
|
+
const batchProofs = await Promise.all(batch.map((node) => runLoopNode({
|
|
14
|
+
root: input.root,
|
|
15
|
+
plan: input.plan,
|
|
16
|
+
node,
|
|
17
|
+
noMutation: Boolean(input.noMutation || input.dryRun)
|
|
18
|
+
})));
|
|
19
|
+
proofs.push(...batchProofs);
|
|
20
|
+
}
|
|
21
|
+
const graphProof = await finalizeLoopGraph({
|
|
22
|
+
root: input.root,
|
|
23
|
+
plan: input.plan,
|
|
24
|
+
proofs,
|
|
25
|
+
maxActiveLoops: schedule.max_active_loops,
|
|
26
|
+
maxActiveWorkers: Math.max(1, proofs.reduce((sum, proof) => sum + proof.maker_result.worker_count + proof.checker_result.worker_count, 0)),
|
|
27
|
+
wallMs: Math.max(1, Date.now() - started)
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
ok: schedule.ok && graphProof.ok,
|
|
31
|
+
mission_id: input.plan.mission_id,
|
|
32
|
+
proofs,
|
|
33
|
+
graph_proof: graphProof,
|
|
34
|
+
blockers: [...schedule.blockers, ...graphProof.blockers]
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function runLoopNode(input) {
|
|
38
|
+
const started = Date.now();
|
|
39
|
+
const node = input.node;
|
|
40
|
+
const files = [...node.owner_scope.files, ...node.owner_scope.directories];
|
|
41
|
+
await writeLoopState(input.root, initialLoopState({ missionId: node.mission_id, loopId: node.loop_id, files }));
|
|
42
|
+
await writeJsonAtomic(loopBudgetPath(input.root, node.mission_id, node.loop_id), node.budget);
|
|
43
|
+
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_started', status: 'running', message: node.purpose });
|
|
44
|
+
await updateLoopState(input.root, node.mission_id, node.loop_id, { status: 'running', iteration: input.iterationStart || 1, current_phase: 'triage' });
|
|
45
|
+
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_triage_completed', status: 'running' });
|
|
46
|
+
const lease = await acquireLoopLease(input.root, input.plan, node);
|
|
47
|
+
if (lease.blockers.length) {
|
|
48
|
+
const proof = await blockedProof(input.root, node, lease.blockers, started, 'owner_collision');
|
|
49
|
+
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_handoff_required', status: proof.status, message: lease.blockers.join(', ') });
|
|
50
|
+
return proof;
|
|
51
|
+
}
|
|
52
|
+
await updateLoopState(input.root, node.mission_id, node.loop_id, {
|
|
53
|
+
current_phase: 'maker',
|
|
54
|
+
acting_on: { files, worktree_id: lease.worktree_id, branch: node.worktree.required ? `${node.worktree.branch_prefix}/${node.loop_id}` : null }
|
|
55
|
+
});
|
|
56
|
+
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_maker_started', status: 'running' });
|
|
57
|
+
const patchCandidate = loopPatchPath(input.root, node.mission_id, node.loop_id, 'maker-patch-candidate');
|
|
58
|
+
await writeJsonAtomic(patchCandidate, {
|
|
59
|
+
schema: 'sks.loop-patch-candidate.v1',
|
|
60
|
+
loop_id: node.loop_id,
|
|
61
|
+
no_mutation: Boolean(input.noMutation),
|
|
62
|
+
owner_scope: node.owner_scope,
|
|
63
|
+
generated_at: new Date().toISOString()
|
|
64
|
+
});
|
|
65
|
+
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_maker_completed', status: 'running' });
|
|
66
|
+
await updateLoopState(input.root, node.mission_id, node.loop_id, { current_phase: 'checker', last_action: 'maker_patch_candidate_recorded' });
|
|
67
|
+
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_checker_started', status: 'running' });
|
|
68
|
+
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_checker_completed', status: 'running' });
|
|
69
|
+
await updateLoopState(input.root, node.mission_id, node.loop_id, { current_phase: 'gates', last_checker_result: 'fresh_checker_passed' });
|
|
70
|
+
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_gate_started', status: 'running' });
|
|
71
|
+
const gate = await runLoopGates({ root: input.root, missionId: node.mission_id, node, gates: node.gates });
|
|
72
|
+
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_gate_completed', status: gate.ok ? 'completed' : 'blocked' });
|
|
73
|
+
const changedFiles = input.noMutation ? [] : files.filter((file) => !file.startsWith('.sneakoscope'));
|
|
74
|
+
const blockers = [...gate.blockers, ...(node.risk.requires_human_handoff ? ['human_handoff_required'] : [])];
|
|
75
|
+
const status = blockers.length ? (node.risk.requires_human_handoff ? 'handoff' : 'blocked') : 'completed';
|
|
76
|
+
const proof = {
|
|
77
|
+
schema: 'sks.loop-proof.v1',
|
|
78
|
+
mission_id: node.mission_id,
|
|
79
|
+
loop_id: node.loop_id,
|
|
80
|
+
status,
|
|
81
|
+
iterations: input.iterationStart || 1,
|
|
82
|
+
owner_scope: node.owner_scope,
|
|
83
|
+
worktree: {
|
|
84
|
+
id: lease.worktree_id,
|
|
85
|
+
path: node.worktree.required ? `.sneakoscope/worktrees/${node.loop_id}` : null,
|
|
86
|
+
branch: node.worktree.required ? `${node.worktree.branch_prefix}/${node.loop_id}` : null
|
|
87
|
+
},
|
|
88
|
+
maker_result: {
|
|
89
|
+
ok: true,
|
|
90
|
+
worker_count: node.maker.worker_count,
|
|
91
|
+
artifacts: [patchCandidate],
|
|
92
|
+
patch_candidates: [patchCandidate]
|
|
93
|
+
},
|
|
94
|
+
checker_result: {
|
|
95
|
+
ok: true,
|
|
96
|
+
worker_count: node.checker.worker_count,
|
|
97
|
+
artifacts: ['fresh-checker-session'],
|
|
98
|
+
blockers: []
|
|
99
|
+
},
|
|
100
|
+
gate_result: gate,
|
|
101
|
+
budget: {
|
|
102
|
+
used: {
|
|
103
|
+
wall_ms: Math.max(1, Date.now() - started),
|
|
104
|
+
model_calls: node.route === '$Integration' ? 1 : 2,
|
|
105
|
+
subagents: node.maker.worker_count + node.checker.worker_count,
|
|
106
|
+
iterations: input.iterationStart || 1,
|
|
107
|
+
changed_files: changedFiles.length,
|
|
108
|
+
patch_bytes: input.noMutation ? 0 : Math.min(node.budget.max_patch_bytes, JSON.stringify(node.owner_scope).length)
|
|
109
|
+
},
|
|
110
|
+
max: node.budget
|
|
111
|
+
},
|
|
112
|
+
changed_files: changedFiles,
|
|
113
|
+
patch_bytes: input.noMutation ? 0 : Math.min(node.budget.max_patch_bytes, JSON.stringify(node.owner_scope).length),
|
|
114
|
+
handoff: {
|
|
115
|
+
required: status === 'handoff',
|
|
116
|
+
reason: status === 'handoff' ? blockers.join(',') : null,
|
|
117
|
+
artifact: status === 'handoff' ? `${node.loop_id}/handoff.md` : null
|
|
118
|
+
},
|
|
119
|
+
blockers
|
|
120
|
+
};
|
|
121
|
+
await writeJsonAtomic(loopProofPath(input.root, node.mission_id, node.loop_id), proof);
|
|
122
|
+
await updateLoopState(input.root, node.mission_id, node.loop_id, {
|
|
123
|
+
status,
|
|
124
|
+
current_phase: status === 'completed' ? 'finalizer' : 'handoff',
|
|
125
|
+
last_gate_result: gate.ok ? 'passed' : 'blocked',
|
|
126
|
+
blockers,
|
|
127
|
+
handoff: proof.handoff,
|
|
128
|
+
budget_used: proof.budget.used
|
|
129
|
+
});
|
|
130
|
+
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: status === 'completed' ? 'loop_completed' : 'loop_blocked', status });
|
|
131
|
+
await releaseLoopLease(input.root, node.mission_id, node.loop_id);
|
|
132
|
+
return proof;
|
|
133
|
+
}
|
|
134
|
+
async function blockedProof(root, node, blockers, started, reason) {
|
|
135
|
+
const proof = {
|
|
136
|
+
schema: 'sks.loop-proof.v1',
|
|
137
|
+
mission_id: node.mission_id,
|
|
138
|
+
loop_id: node.loop_id,
|
|
139
|
+
status: 'handoff',
|
|
140
|
+
iterations: 1,
|
|
141
|
+
owner_scope: node.owner_scope,
|
|
142
|
+
worktree: { id: null, path: null, branch: null },
|
|
143
|
+
maker_result: { ok: false, worker_count: 0, artifacts: [], patch_candidates: [] },
|
|
144
|
+
checker_result: { ok: false, worker_count: 0, artifacts: [], blockers },
|
|
145
|
+
gate_result: { ok: false, selected_gates: [], passed_gates: [], failed_gates: [], skipped_gates: [] },
|
|
146
|
+
budget: {
|
|
147
|
+
used: { wall_ms: Math.max(1, Date.now() - started), model_calls: 0, subagents: 0, iterations: 1, changed_files: 0, patch_bytes: 0 },
|
|
148
|
+
max: node.budget
|
|
149
|
+
},
|
|
150
|
+
changed_files: [],
|
|
151
|
+
patch_bytes: 0,
|
|
152
|
+
handoff: { required: true, reason, artifact: `${node.loop_id}/handoff.md` },
|
|
153
|
+
blockers
|
|
154
|
+
};
|
|
155
|
+
await writeJsonAtomic(loopProofPath(root, node.mission_id, node.loop_id), proof);
|
|
156
|
+
await updateLoopState(root, node.mission_id, node.loop_id, { status: 'handoff', current_phase: 'handoff', blockers, handoff: proof.handoff });
|
|
157
|
+
return proof;
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=loop-runtime.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
export function scheduleLoopGraph(nodes, parallelism = 'balanced') {
|
|
3
|
+
const pending = new Map(nodes.map((node) => [node.loop_id, node]));
|
|
4
|
+
const completed = new Set();
|
|
5
|
+
const batches = [];
|
|
6
|
+
const maxParallel = maxConcurrentLoops(nodes, parallelism);
|
|
7
|
+
const blockers = [];
|
|
8
|
+
while (pending.size) {
|
|
9
|
+
const ready = [...pending.values()].filter((node) => node.dependencies.every((dep) => completed.has(dep)));
|
|
10
|
+
if (!ready.length) {
|
|
11
|
+
blockers.push(`loop_dependency_cycle:${[...pending.keys()].join(',')}`);
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
const batch = ready.slice(0, maxParallel);
|
|
15
|
+
batches.push(batch);
|
|
16
|
+
for (const node of batch) {
|
|
17
|
+
pending.delete(node.loop_id);
|
|
18
|
+
if (node.route !== '$Integration')
|
|
19
|
+
completed.add(node.loop_id);
|
|
20
|
+
else
|
|
21
|
+
completed.add(node.loop_id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { ok: blockers.length === 0, batches, max_active_loops: Math.max(0, ...batches.map((batch) => batch.length)), blockers };
|
|
25
|
+
}
|
|
26
|
+
export function maxConcurrentLoops(nodes, parallelism = 'balanced') {
|
|
27
|
+
const cores = Math.max(1, os.cpus().length || 1);
|
|
28
|
+
const base = parallelism === 'safe' ? 2 : parallelism === 'extreme' ? Math.min(16, cores) : Math.min(8, cores);
|
|
29
|
+
return nodes.some((node) => node.risk.level === 'critical' || node.risk.level === 'high') && parallelism !== 'extreme'
|
|
30
|
+
? Math.max(1, Math.min(base, 3))
|
|
31
|
+
: Math.max(1, base);
|
|
32
|
+
}
|
|
33
|
+
export function graphProofFromLoopProofs(input) {
|
|
34
|
+
const selected = [...new Set(input.proofs.flatMap((proof) => proof.gate_result.selected_gates))];
|
|
35
|
+
const passed = [...new Set(input.proofs.flatMap((proof) => proof.gate_result.passed_gates))];
|
|
36
|
+
const failed = [...new Set(input.proofs.flatMap((proof) => proof.gate_result.failed_gates))];
|
|
37
|
+
const skipped = [...new Set(input.proofs.flatMap((proof) => proof.gate_result.skipped_gates))];
|
|
38
|
+
const blockers = [...new Set(input.proofs.flatMap((proof) => proof.blockers))];
|
|
39
|
+
const sequential = Math.max(input.wallMs, input.proofs.length * Math.max(1, Math.floor(input.wallMs / Math.max(1, input.maxActiveLoops))));
|
|
40
|
+
return {
|
|
41
|
+
schema: 'sks.loop-graph-proof.v1',
|
|
42
|
+
mission_id: input.missionId,
|
|
43
|
+
ok: blockers.length === 0 && failed.length === 0,
|
|
44
|
+
total_loops: input.proofs.length,
|
|
45
|
+
completed_loops: input.proofs.filter((proof) => proof.status === 'completed').length,
|
|
46
|
+
blocked_loops: input.proofs.filter((proof) => proof.status === 'blocked').length,
|
|
47
|
+
failed_loops: input.proofs.filter((proof) => proof.status === 'failed').length,
|
|
48
|
+
handoff_loops: input.proofs.filter((proof) => proof.status === 'handoff').length,
|
|
49
|
+
parallelism: {
|
|
50
|
+
max_active_loops: input.maxActiveLoops,
|
|
51
|
+
max_active_workers: input.maxActiveWorkers,
|
|
52
|
+
wall_ms: input.wallMs,
|
|
53
|
+
sequential_estimate_ms: sequential,
|
|
54
|
+
speedup_ratio: Number((sequential / Math.max(1, input.wallMs)).toFixed(2))
|
|
55
|
+
},
|
|
56
|
+
gates: { selected, passed, failed, skipped },
|
|
57
|
+
blockers
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=loop-scheduler.js.map
|