sneakoscope 3.0.3 → 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/codex-control/codex-0139-capability.js +8 -3
- package/dist/core/codex-control/codex-0139-doctor-real-probe.js +64 -0
- package/dist/core/codex-control/codex-0139-image-path-real-probe.js +94 -0
- package/dist/core/codex-control/codex-0139-multi-agent-real-probe.js +107 -0
- package/dist/core/codex-control/codex-0139-plugin-real-probes.js +119 -0
- package/dist/core/codex-control/codex-0139-probe-runner.js +117 -0
- package/dist/core/codex-control/codex-0139-real-probe-summary.js +37 -0
- package/dist/core/codex-control/codex-0139-real-probes.js +74 -0
- package/dist/core/codex-control/codex-0139-rich-schema-real-probe.js +43 -0
- package/dist/core/codex-control/codex-0139-sandbox-real-probe.js +79 -0
- package/dist/core/codex-control/codex-0139-web-search-probe.js +72 -0
- package/dist/core/commands/goal-command.js +19 -1
- package/dist/core/commands/loop-command.js +135 -0
- package/dist/core/doctor/codex-0139-doctor.js +16 -0
- package/dist/core/doctor/doctor-readiness-matrix.js +6 -0
- package/dist/core/fsx.js +25 -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/github-release-body-helper.js +3 -1
- package/dist/scripts/loop-directive-check-lib.js +165 -0
- package/package.json +47 -3
- package/schemas/codex/codex-0139-real-probe-result.schema.json +85 -0
- 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,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
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function defaultLoopBudget(overrides = {}) {
|
|
2
|
+
return {
|
|
3
|
+
max_iterations: 2,
|
|
4
|
+
max_wall_ms: 15 * 60 * 1000,
|
|
5
|
+
max_model_calls: 8,
|
|
6
|
+
max_subagents: 4,
|
|
7
|
+
max_tokens_estimate: 120000,
|
|
8
|
+
max_changed_files: 16,
|
|
9
|
+
max_patch_bytes: 256000,
|
|
10
|
+
...overrides
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function validateLoopPlan(plan) {
|
|
14
|
+
const blockers = [
|
|
15
|
+
...(plan.schema !== 'sks.loop-plan.v1' ? ['loop_plan_schema_invalid'] : []),
|
|
16
|
+
...(!plan.mission_id ? ['loop_plan_mission_id_missing'] : []),
|
|
17
|
+
...(!plan.request ? ['loop_plan_request_missing'] : []),
|
|
18
|
+
...(!plan.integration_loop_id ? ['loop_plan_integration_loop_missing'] : []),
|
|
19
|
+
...(plan.graph.nodes.length === 0 ? ['loop_plan_nodes_missing'] : []),
|
|
20
|
+
...(!plan.graph.nodes.some((node) => node.loop_id === plan.integration_loop_id) ? ['loop_plan_integration_node_missing'] : [])
|
|
21
|
+
];
|
|
22
|
+
const ids = new Set();
|
|
23
|
+
for (const node of plan.graph.nodes) {
|
|
24
|
+
const result = validateLoopNode(node);
|
|
25
|
+
blockers.push(...result.blockers);
|
|
26
|
+
if (ids.has(node.loop_id))
|
|
27
|
+
blockers.push(`loop_node_duplicate:${node.loop_id}`);
|
|
28
|
+
ids.add(node.loop_id);
|
|
29
|
+
}
|
|
30
|
+
for (const edge of plan.graph.edges) {
|
|
31
|
+
if (!ids.has(edge.from) || !ids.has(edge.to))
|
|
32
|
+
blockers.push(`loop_edge_unknown:${edge.from}->${edge.to}`);
|
|
33
|
+
}
|
|
34
|
+
return { ok: blockers.length === 0, blockers };
|
|
35
|
+
}
|
|
36
|
+
export function validateLoopNode(node) {
|
|
37
|
+
const blockers = [
|
|
38
|
+
...(node.schema !== 'sks.loop-node.v1' ? [`loop_node_schema_invalid:${node.loop_id}`] : []),
|
|
39
|
+
...(!node.loop_id ? ['loop_node_id_missing'] : []),
|
|
40
|
+
...(!node.mission_id ? [`loop_node_mission_missing:${node.loop_id}`] : []),
|
|
41
|
+
...(!node.state_file ? [`loop_state_file_missing:${node.loop_id}`] : []),
|
|
42
|
+
...(!node.run_log_file ? [`loop_run_log_file_missing:${node.loop_id}`] : []),
|
|
43
|
+
...(!node.owner_scope ? [`loop_owner_scope_missing:${node.loop_id}`] : []),
|
|
44
|
+
...validateLoopBudget(node.budget).blockers.map((blocker) => `${node.loop_id}:${blocker}`),
|
|
45
|
+
...(node.level === 'L3-unattended' && ['high', 'critical'].includes(node.risk.level) ? [`loop_l3_risk_blocked:${node.loop_id}`] : []),
|
|
46
|
+
...(node.level === 'L2-action' && !node.checker.required_before_next_iteration ? [`loop_action_checker_missing:${node.loop_id}`] : [])
|
|
47
|
+
];
|
|
48
|
+
return { ok: blockers.length === 0, blockers };
|
|
49
|
+
}
|
|
50
|
+
export function validateLoopBudget(budget) {
|
|
51
|
+
const blockers = [];
|
|
52
|
+
for (const [key, value] of Object.entries(budget)) {
|
|
53
|
+
if (!Number.isFinite(value) || value < 0)
|
|
54
|
+
blockers.push(`loop_budget_invalid:${key}`);
|
|
55
|
+
}
|
|
56
|
+
if (budget.max_iterations < 1)
|
|
57
|
+
blockers.push('loop_budget_iterations_missing');
|
|
58
|
+
return { ok: blockers.length === 0, blockers };
|
|
59
|
+
}
|
|
60
|
+
export function allGateIds(gates) {
|
|
61
|
+
return [...new Set([...gates.triage, ...gates.local, ...gates.checker, ...gates.integration, ...gates.final])];
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=loop-schema.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { appendJsonl, readJson, writeJsonAtomic } from '../fsx.js';
|
|
2
|
+
import { loopRunLogPath, loopStatePath } from './loop-artifacts.js';
|
|
3
|
+
export async function readLoopState(root, missionId, loopId) {
|
|
4
|
+
return readJson(loopStatePath(root, missionId, loopId), null);
|
|
5
|
+
}
|
|
6
|
+
export async function writeLoopState(root, state) {
|
|
7
|
+
await writeJsonAtomic(loopStatePath(root, state.mission_id, state.loop_id), state);
|
|
8
|
+
return state;
|
|
9
|
+
}
|
|
10
|
+
export async function appendLoopRunLog(root, missionId, loopId, event) {
|
|
11
|
+
await appendJsonl(loopRunLogPath(root, missionId, loopId), { ts: event.ts || new Date().toISOString(), ...event });
|
|
12
|
+
}
|
|
13
|
+
export async function updateLoopState(root, missionId, loopId, patch) {
|
|
14
|
+
const current = await readLoopState(root, missionId, loopId);
|
|
15
|
+
if (!current)
|
|
16
|
+
throw new Error(`loop_state_missing:${loopId}`);
|
|
17
|
+
const next = {
|
|
18
|
+
...current,
|
|
19
|
+
...patch,
|
|
20
|
+
acting_on: { ...current.acting_on, ...(patch.acting_on || {}) },
|
|
21
|
+
handoff: { ...current.handoff, ...(patch.handoff || {}) },
|
|
22
|
+
budget_used: { ...current.budget_used, ...(patch.budget_used || {}) },
|
|
23
|
+
updated_at: new Date().toISOString()
|
|
24
|
+
};
|
|
25
|
+
await writeLoopState(root, next);
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
export function initialLoopState(input) {
|
|
29
|
+
return {
|
|
30
|
+
schema: 'sks.loop-state.v1',
|
|
31
|
+
mission_id: input.missionId,
|
|
32
|
+
loop_id: input.loopId,
|
|
33
|
+
status: 'planned',
|
|
34
|
+
iteration: 0,
|
|
35
|
+
acting_on: {
|
|
36
|
+
files: input.files,
|
|
37
|
+
worktree_id: input.worktreeId || null,
|
|
38
|
+
branch: input.branch || null
|
|
39
|
+
},
|
|
40
|
+
current_phase: 'triage',
|
|
41
|
+
last_action: null,
|
|
42
|
+
last_gate_result: null,
|
|
43
|
+
last_checker_result: null,
|
|
44
|
+
blockers: [],
|
|
45
|
+
handoff: {
|
|
46
|
+
required: false,
|
|
47
|
+
reason: null,
|
|
48
|
+
artifact: null
|
|
49
|
+
},
|
|
50
|
+
budget_used: {
|
|
51
|
+
wall_ms: 0,
|
|
52
|
+
model_calls: 0,
|
|
53
|
+
subagents: 0,
|
|
54
|
+
iterations: 0,
|
|
55
|
+
changed_files: 0,
|
|
56
|
+
patch_bytes: 0
|
|
57
|
+
},
|
|
58
|
+
updated_at: new Date().toISOString()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=loop-state.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
2
|
+
import { loopRoot } from '../loops/loop-artifacts.js';
|
|
3
|
+
import { runLoopPlan } from '../loops/loop-runtime.js';
|
|
4
|
+
import { routeNarutoLoopWorker } from './naruto-loop-worker-router.js';
|
|
5
|
+
export async function runNarutoLoopMesh(input) {
|
|
6
|
+
const routes = input.plan.graph.nodes.flatMap((node) => [routeNarutoLoopWorker(node, 'maker'), routeNarutoLoopWorker(node, 'checker')]);
|
|
7
|
+
const activeWorkerBudget = splitActiveWorkerBudget(input.plan, input.parallelism);
|
|
8
|
+
await writeJsonAtomic(`${loopRoot(input.root, input.plan.mission_id)}/naruto-loop-worker-routes.json`, {
|
|
9
|
+
schema: 'sks.naruto-loop-worker-routes.v1',
|
|
10
|
+
mission_id: input.plan.mission_id,
|
|
11
|
+
active_worker_budget: activeWorkerBudget,
|
|
12
|
+
routes
|
|
13
|
+
});
|
|
14
|
+
return runLoopPlan({ root: input.root, plan: input.plan, parallelism: input.parallelism });
|
|
15
|
+
}
|
|
16
|
+
export function splitActiveWorkerBudget(plan, parallelism) {
|
|
17
|
+
const cap = parallelism === 'safe' ? 8 : parallelism === 'extreme' ? 32 : 16;
|
|
18
|
+
const integrationReserved = 2;
|
|
19
|
+
const nonIntegration = plan.graph.nodes.filter((node) => node.route !== '$Integration');
|
|
20
|
+
const perLoopCap = Math.max(2, Math.floor((cap - integrationReserved) / Math.max(1, nonIntegration.length)));
|
|
21
|
+
const perLoop = nonIntegration.map((node) => ({
|
|
22
|
+
loop_id: node.loop_id,
|
|
23
|
+
maker_checker_workers: Math.min(perLoopCap, node.maker.worker_count + node.checker.worker_count)
|
|
24
|
+
}));
|
|
25
|
+
const used = perLoop.reduce((sum, row) => sum + row.maker_checker_workers, integrationReserved);
|
|
26
|
+
return {
|
|
27
|
+
global_active_workers: cap,
|
|
28
|
+
integration_reserved: integrationReserved,
|
|
29
|
+
per_loop: perLoop,
|
|
30
|
+
headroom: Math.max(0, cap - used)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=naruto-loop-mesh.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function routeNarutoLoopWorker(node, role) {
|
|
2
|
+
const domain = node.loop_id.replace(/^loop-/, '');
|
|
3
|
+
const roles = roleLabels(domain);
|
|
4
|
+
const gates = [...node.gates.triage, ...node.gates.local, ...node.gates.checker, ...node.gates.integration, ...node.gates.final];
|
|
5
|
+
return {
|
|
6
|
+
schema: 'sks.naruto-loop-worker-route.v1',
|
|
7
|
+
loop_id: node.loop_id,
|
|
8
|
+
maker_role: roles.maker,
|
|
9
|
+
checker_role: roles.checker,
|
|
10
|
+
prompt: [
|
|
11
|
+
`loop purpose: ${node.purpose}`,
|
|
12
|
+
`role: ${role === 'maker' ? roles.maker : roles.checker}`,
|
|
13
|
+
`owner files: ${node.owner_scope.files.join(', ') || '-'}`,
|
|
14
|
+
`owner directories: ${node.owner_scope.directories.join(', ') || '-'}`,
|
|
15
|
+
`gates: ${gates.join(', ') || '-'}`,
|
|
16
|
+
`state file: ${node.state_file}`,
|
|
17
|
+
`budget: ${JSON.stringify(node.budget)}`,
|
|
18
|
+
`collision policy: ${node.owner_scope.collision_policy}`,
|
|
19
|
+
'Do not mutate outside owner scope.'
|
|
20
|
+
].join('\n'),
|
|
21
|
+
allowed_files: node.owner_scope.files,
|
|
22
|
+
allowed_directories: node.owner_scope.directories,
|
|
23
|
+
gates,
|
|
24
|
+
mutation_outside_owner_scope_allowed: false
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function roleLabels(domain) {
|
|
28
|
+
if (domain.includes('zellij'))
|
|
29
|
+
return { maker: 'zellij implementer', checker: 'zellij QA/verifier' };
|
|
30
|
+
if (domain.includes('release'))
|
|
31
|
+
return { maker: 'release optimizer', checker: 'release gate verifier' };
|
|
32
|
+
if (domain.includes('research'))
|
|
33
|
+
return { maker: 'source shard/synthesis', checker: 'final reviewer' };
|
|
34
|
+
if (domain.includes('codex'))
|
|
35
|
+
return { maker: 'capability/probe implementer', checker: 'real probe verifier' };
|
|
36
|
+
return { maker: `${domain} implementer`, checker: `${domain} checker` };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=naruto-loop-worker-router.js.map
|
|
@@ -1213,9 +1213,17 @@ export async function recordContext7Evidence(root, state, payload) {
|
|
|
1213
1213
|
return null;
|
|
1214
1214
|
if (!await shouldWritePipelineEvidence(root, state))
|
|
1215
1215
|
return null;
|
|
1216
|
-
const record = { ts: nowIso(), stage, tool: context7ToolName(payload), payload_keys: Object.keys(payload || {}).sort() };
|
|
1217
1216
|
const id = state?.mission_id;
|
|
1218
1217
|
const file = id ? path.join(missionDir(root, id), 'context7-evidence.jsonl') : path.join(root, '.sneakoscope', 'state', 'context7-evidence.jsonl');
|
|
1218
|
+
const record = {
|
|
1219
|
+
ts: nowIso(),
|
|
1220
|
+
stage,
|
|
1221
|
+
tool: context7ToolName(payload),
|
|
1222
|
+
payload_keys: Object.keys(payload || {}).sort(),
|
|
1223
|
+
dedupe_key: context7DedupeKey(stage, payload)
|
|
1224
|
+
};
|
|
1225
|
+
if (await hasContext7EvidenceRecord(file, record.dedupe_key))
|
|
1226
|
+
return null;
|
|
1219
1227
|
await appendJsonl(file, record);
|
|
1220
1228
|
if (id) {
|
|
1221
1229
|
const evidence = await context7Evidence(root, state);
|
|
@@ -1297,7 +1305,11 @@ function context7ToolName(payload) {
|
|
|
1297
1305
|
return String(obj.tool_name || obj.name || obj.tool?.name || obj.mcp_tool || obj.command || obj.type || '');
|
|
1298
1306
|
}
|
|
1299
1307
|
function context7Stage(payload) {
|
|
1300
|
-
const
|
|
1308
|
+
const tool = context7ToolName(payload);
|
|
1309
|
+
const direct = context7DirectSignal(payload);
|
|
1310
|
+
if (!direct && !context7ToolLooksRelevant(tool))
|
|
1311
|
+
return null;
|
|
1312
|
+
const hay = [tool, direct].filter(Boolean).join('\n');
|
|
1301
1313
|
if (!/(context7|resolve[-_]?library[-_]?id|get[-_]?library[-_]?docs|query[-_]?docs)/i.test(hay))
|
|
1302
1314
|
return null;
|
|
1303
1315
|
if (/resolve[-_]?library[-_]?id/i.test(hay))
|
|
@@ -1306,6 +1318,74 @@ function context7Stage(payload) {
|
|
|
1306
1318
|
return 'get-library-docs';
|
|
1307
1319
|
return 'context7';
|
|
1308
1320
|
}
|
|
1321
|
+
function context7ToolLooksRelevant(tool) {
|
|
1322
|
+
return /(^|[_:/.-])(context7|resolve[-_]?library[-_]?id|get[-_]?library[-_]?docs|query[-_]?docs)($|[_:/.-])/i.test(String(tool || ''));
|
|
1323
|
+
}
|
|
1324
|
+
function context7DirectSignal(payload = {}) {
|
|
1325
|
+
const source = String(payload.source || '');
|
|
1326
|
+
const tool = context7ToolName(payload);
|
|
1327
|
+
if (/^sks context7 evidence/i.test(source)) {
|
|
1328
|
+
return [
|
|
1329
|
+
tool,
|
|
1330
|
+
source,
|
|
1331
|
+
payload.library,
|
|
1332
|
+
payload.library_id,
|
|
1333
|
+
payload.docs_tool
|
|
1334
|
+
].filter(Boolean).join('\n');
|
|
1335
|
+
}
|
|
1336
|
+
if (context7ToolLooksRelevant(tool)) {
|
|
1337
|
+
const input = payload.tool_input || payload.toolInput || payload.input || payload.tool?.input || {};
|
|
1338
|
+
return JSON.stringify({
|
|
1339
|
+
tool,
|
|
1340
|
+
library: payload.library,
|
|
1341
|
+
library_id: payload.library_id,
|
|
1342
|
+
docs_tool: payload.docs_tool,
|
|
1343
|
+
input: context7SafeInput(input)
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
return '';
|
|
1347
|
+
}
|
|
1348
|
+
function context7SafeInput(input) {
|
|
1349
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
1350
|
+
return input ?? null;
|
|
1351
|
+
const out = {};
|
|
1352
|
+
for (const key of ['name', 'tool', 'library', 'libraryName', 'library_id', 'libraryId', 'context7CompatibleLibraryID', 'query', 'topic', 'tokens']) {
|
|
1353
|
+
if (Object.prototype.hasOwnProperty.call(input, key))
|
|
1354
|
+
out[key] = input[key];
|
|
1355
|
+
}
|
|
1356
|
+
return out;
|
|
1357
|
+
}
|
|
1358
|
+
function context7DedupeKey(stage, payload = {}) {
|
|
1359
|
+
const input = payload.tool_input || payload.toolInput || payload.input || payload.tool?.input || {};
|
|
1360
|
+
const library = payload.library_id
|
|
1361
|
+
|| payload.library
|
|
1362
|
+
|| input.libraryId
|
|
1363
|
+
|| input.context7CompatibleLibraryID
|
|
1364
|
+
|| input.libraryName
|
|
1365
|
+
|| input.library
|
|
1366
|
+
|| '';
|
|
1367
|
+
const query = input.query || input.topic || payload.query || payload.topic || '';
|
|
1368
|
+
return [
|
|
1369
|
+
stage,
|
|
1370
|
+
context7ToolName(payload),
|
|
1371
|
+
String(library).trim().toLowerCase(),
|
|
1372
|
+
String(query).trim().toLowerCase()
|
|
1373
|
+
].join('|');
|
|
1374
|
+
}
|
|
1375
|
+
async function hasContext7EvidenceRecord(file, key) {
|
|
1376
|
+
const text = await readText(file, '');
|
|
1377
|
+
for (const line of text.split(/\n/)) {
|
|
1378
|
+
if (!line.trim())
|
|
1379
|
+
continue;
|
|
1380
|
+
try {
|
|
1381
|
+
const entry = JSON.parse(line);
|
|
1382
|
+
if (entry.dedupe_key === key)
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
catch { }
|
|
1386
|
+
}
|
|
1387
|
+
return false;
|
|
1388
|
+
}
|
|
1309
1389
|
export async function context7Evidence(root, state) {
|
|
1310
1390
|
const id = state?.mission_id;
|
|
1311
1391
|
if (!id)
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '3.0
|
|
1
|
+
export const PACKAGE_VERSION = '3.1.0';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -11,9 +11,12 @@ export function renderZellijSlotColumnAnchor(input = {}) {
|
|
|
11
11
|
const update = input.updateAvailableVersion ? ` · update ${trimInline(input.updateAvailableVersion, 18)} available` : '';
|
|
12
12
|
const madDb = input.madDbActive ? ' · MAD-DB ACTIVE' : '';
|
|
13
13
|
const appHandoff = input.qaAppHandoffPending ? ' · QA /app handoff pending' : '';
|
|
14
|
-
const
|
|
14
|
+
const loopHeader = input.loopsTotal != null
|
|
15
|
+
? `LOOPS ${nonNegativeInt(input.loopsTotal, 0)} · running ${nonNegativeInt(input.loopsRunning, 0)} · blocked ${nonNegativeInt(input.loopsBlocked, 0)} · done ${nonNegativeInt(input.loopsCompleted, 0)} · workers ${active}`
|
|
16
|
+
: null;
|
|
17
|
+
const header = loopHeader || (done || fail
|
|
15
18
|
? `SLOTS active ${active} · headless ${headless} · done ${done} · fail ${fail} · q ${queue}${update}${madDb}${appHandoff}`
|
|
16
|
-
: `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}${update}${madDb}${appHandoff}
|
|
19
|
+
: `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}${update}${madDb}${appHandoff}`);
|
|
17
20
|
const workers = Array.isArray(input.workerRows) ? input.workerRows : [];
|
|
18
21
|
const handoffLine = input.qaAppHandoffPending ? `QA app handoff pending · ${trimInline(input.qaAppHandoffArtifact || 'qa-loop/app-handoff.json', 64)}` : null;
|
|
19
22
|
if (!workers.length)
|
|
@@ -15,6 +15,7 @@ export function renderZellijSlotPane(input) {
|
|
|
15
15
|
const stdout = (input.stdoutTail || []).filter(Boolean).slice(-2);
|
|
16
16
|
const stderr = (input.stderrTail || []).filter(Boolean).slice(-1);
|
|
17
17
|
const rows = [
|
|
18
|
+
input.loopId ? `${trimInline(input.loopId, 28)} · ${trimInline(input.loopRole || input.role || 'worker', 14)} · ${input.slotId}` : null,
|
|
18
19
|
`slot: ${input.slotId} / gen-${Math.max(1, Math.floor(Number(input.generationIndex) || 1))} / ${trimInline(input.status || 'running', 18)}`,
|
|
19
20
|
`role: ${trimInline(input.role || 'worker', 18)} backend: ${trimInline(input.backend || 'codex-sdk', 20)} worktree: ${trimInline(input.worktreeId || '-', 18)}`,
|
|
20
21
|
`runtime: fast ${formatFastMode(input.fastMode, input.serviceTier)} tier: ${trimInline(input.serviceTier || 'unknown', 12)} provider: ${trimInline(input.provider || 'unknown', 18)}`,
|
|
@@ -22,6 +23,7 @@ export function renderZellijSlotPane(input) {
|
|
|
22
23
|
input.sessionId ? `session: ${trimInline(input.sessionId, 62)}` : null,
|
|
23
24
|
`heartbeat: ${heartbeat}${input.heartbeatEvent ? ` event: ${trimInline(input.heartbeatEvent, 40)}` : ''}`,
|
|
24
25
|
`doing: ${task}`,
|
|
26
|
+
input.loopGate ? `gate: ${trimInline(input.loopGate, 68)}` : null,
|
|
25
27
|
`files: ${trimInline(files.length ? files.join(', ') : 'no changed file yet', 78)}`,
|
|
26
28
|
`patch: ${trimInline(input.patchStatus || 'queued', 24)} verify: ${trimInline(input.verifyStatus || 'queued', 24)}`,
|
|
27
29
|
input.qaAppHandoffPending ? `QA app handoff pending: ${trimInline(input.qaAppHandoffArtifact || 'qa-loop/app-handoff.json', 55)}` : null,
|
|
@@ -7,6 +7,7 @@ const args = process.argv.slice(2);
|
|
|
7
7
|
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
8
8
|
const truth = readJson('.sneakoscope/release-proof-truth.json') || readJson('dist/release-proof-truth.json');
|
|
9
9
|
const codex0139 = readJson('.sneakoscope/codex-0139-capability.json');
|
|
10
|
+
const codex0139Real = readJson('.sneakoscope/codex-0139-real-probe-summary.json');
|
|
10
11
|
const zellij = readJson('.sneakoscope/reports/zellij-worker-pane-summary.json');
|
|
11
12
|
const changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8');
|
|
12
13
|
const latest = latestChangelogSection(changelog);
|
|
@@ -24,6 +25,7 @@ const body = [
|
|
|
24
25
|
`Release proof truth: ${truth ? '.sneakoscope/release-proof-truth.json' : 'missing'}`,
|
|
25
26
|
`Codex SDK package: ${sdkVersion || 'unknown'}`,
|
|
26
27
|
`External Codex CLI 0.139 capability: ${codex0139 ? `${codex0139.ok ? 'ok' : 'blocked'}${codex0139.parsed_version ? ` (${codex0139.parsed_version})` : ''}` : 'not recorded'}`,
|
|
28
|
+
`Codex 0.139 real probes: ${codex0139Real ? `${codex0139Real.ok ? 'ok' : 'blocked'} (${codex0139Real.actual_cli_probe_count || 0} actual CLI, ${codex0139Real.skipped_count || 0} skipped, ${codex0139Real.failed_count || 0} failed)` : 'not run in this release environment. Hermetic fixture gates: passed.'}`,
|
|
27
29
|
`Zellij stacked panes: ${zellij ? `${zellij.stacked_applied_count || 0}/${zellij.stacked_requested_count || 0} applied, fallback ${zellij.stacked_fallback_count || 0}, SLOTS anchors ${zellij.duplicate_slot_anchor_count || 0}` : 'not recorded'}`,
|
|
28
30
|
`Packlist: ${truth?.npm_packlist_count ?? 'unknown'} files / ${truth?.npm_packlist_bytes ?? 'unknown'} bytes`,
|
|
29
31
|
...(warnings.length ? [`Warnings: ${warnings.join('; ')}`] : ['Warnings: none']),
|
|
@@ -34,7 +36,7 @@ const body = [
|
|
|
34
36
|
if (args.includes('--check')) {
|
|
35
37
|
assertGate(Boolean(truth && truth.schema === 'sks.release-proof-truth.v1'), 'release proof truth missing; run npm run release:proof-truth first');
|
|
36
38
|
assertGate(latest.version === pkg.version, 'latest changelog section must match package version', { latest: latest.version, package: pkg.version });
|
|
37
|
-
assertGate(body.includes(`Version: ${pkg.version}`) && body.includes('Commit:') && body.includes('Dirty:') && body.includes('Release proof truth:') && body.includes('Codex SDK package:') && body.includes('External Codex CLI 0.139 capability:') && body.includes('Zellij stacked panes:') && body.includes('Packlist:'), 'github release body helper missing source truth fields', { body });
|
|
39
|
+
assertGate(body.includes(`Version: ${pkg.version}`) && body.includes('Commit:') && body.includes('Dirty:') && body.includes('Release proof truth:') && body.includes('Codex SDK package:') && body.includes('External Codex CLI 0.139 capability:') && body.includes('Codex 0.139 real probes:') && body.includes('Zellij stacked panes:') && body.includes('Packlist:'), 'github release body helper missing source truth fields', { body });
|
|
38
40
|
assertGate(!warnings.some((warning) => warning.includes('package version') || warning.includes('changelog latest') || warning.includes('release proof truth missing')), 'github release body helper source truth warnings must not include version/proof mismatch', { warnings, body });
|
|
39
41
|
}
|
|
40
42
|
console.log(body);
|