sneakoscope 3.1.0 → 3.1.2
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/install-helpers.js +6 -7
- package/dist/commands/zellij-slot-column-anchor.js +3 -1
- package/dist/commands/zellij-slot-pane.js +19 -2
- package/dist/core/agents/agent-janitor.js +10 -1
- package/dist/core/agents/agent-orchestrator.js +8 -2
- package/dist/core/agents/agent-proof-evidence.js +20 -0
- package/dist/core/agents/agent-runner-ollama.js +11 -4
- package/dist/core/agents/fast-mode-policy.js +7 -5
- package/dist/core/agents/intelligent-work-graph.js +93 -14
- package/dist/core/agents/native-cli-session-swarm.js +115 -9
- package/dist/core/agents/no-subagent-scaling-policy.js +10 -1
- package/dist/core/agents/official-subagent-helper-policy.js +62 -0
- package/dist/core/codex-app.js +0 -2
- package/dist/core/codex-control/codex-task-runner.js +9 -0
- package/dist/core/commands/fast-mode-command.js +1 -1
- package/dist/core/commands/loop-command.js +86 -13
- package/dist/core/commands/naruto-command.js +34 -21
- package/dist/core/commands/team-command.js +1 -0
- package/dist/core/commands/wiki-command.js +35 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +1 -2
- package/dist/core/locks/file-lock.js +88 -0
- package/dist/core/loops/loop-artifacts.js +54 -2
- package/dist/core/loops/loop-checkpoint.js +22 -0
- package/dist/core/loops/loop-concurrency-budget.js +55 -0
- package/dist/core/loops/loop-final-arbiter-contract.js +28 -0
- package/dist/core/loops/loop-finalizer.js +55 -7
- package/dist/core/loops/loop-fixture-policy.js +58 -0
- package/dist/core/loops/loop-gate-registry.js +96 -0
- package/dist/core/loops/loop-gate-runner.js +206 -17
- package/dist/core/loops/loop-gpt-final-arbiter.js +81 -0
- package/dist/core/loops/loop-integration-merge.js +80 -0
- package/dist/core/loops/loop-interrupt-registry.js +118 -0
- package/dist/core/loops/loop-lease.js +35 -20
- package/dist/core/loops/loop-merge-strategy.js +105 -0
- package/dist/core/loops/loop-mutation-ledger.js +103 -0
- package/dist/core/loops/loop-planner.js +36 -5
- package/dist/core/loops/loop-runtime-control.js +27 -0
- package/dist/core/loops/loop-runtime.js +254 -96
- package/dist/core/loops/loop-scheduler.js +14 -5
- package/dist/core/loops/loop-side-effect-scanner.js +91 -0
- package/dist/core/loops/loop-worker-prompts.js +43 -0
- package/dist/core/loops/loop-worker-runtime.js +281 -0
- package/dist/core/loops/loop-worktree-runtime.js +92 -0
- package/dist/core/naruto/naruto-finalizer.js +7 -2
- package/dist/core/naruto/naruto-loop-mesh.js +10 -1
- package/dist/core/proof/auto-finalize.js +3 -2
- package/dist/core/proof/proof-schema.js +6 -0
- package/dist/core/proof/proof-writer.js +5 -2
- package/dist/core/proof/root-cause-policy.js +70 -0
- package/dist/core/proof/route-adapter.js +18 -1
- package/dist/core/proof/route-finalizer.js +71 -6
- package/dist/core/proof/route-proof-gate.js +4 -0
- package/dist/core/release/release-gate-batch-runner.js +56 -10
- package/dist/core/release/release-gate-cache-v2.js +18 -3
- package/dist/core/release/release-gate-dag.js +121 -18
- package/dist/core/release/release-gate-node.js +2 -1
- package/dist/core/release/release-gate-resource-governor.js +27 -6
- package/dist/core/skills/core-skill-meta-update.js +24 -0
- package/dist/core/skills/core-skill-reflection.js +94 -0
- package/dist/core/skills/core-skill-trainer.js +103 -0
- package/dist/core/trust-kernel/completion-contract.js +4 -0
- package/dist/core/trust-kernel/route-contract.js +4 -1
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-right-column-manager.js +13 -2
- package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
- package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
- package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
- package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
- package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +14 -2
- package/dist/scripts/loop-directive-check-lib.js +225 -2
- package/dist/scripts/loop-hardening-check-lib.js +289 -0
- package/dist/scripts/loop-worker-fixture-child.js +53 -0
- package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
- package/dist/scripts/prepublish-release-check-or-fast.js +38 -10
- package/dist/scripts/release-check-stamp.js +29 -4
- package/dist/scripts/release-gate-existence-audit.js +1 -0
- package/package.json +32 -2
|
@@ -6,7 +6,7 @@ import { readAgentProofEvidence } from '../agents/agent-proof-evidence.js';
|
|
|
6
6
|
import { wrongnessProofEvidence } from '../triwiki-wrongness/wrongness-proof-linker.js';
|
|
7
7
|
import { computerUseStatusReport } from '../computer-use-status.js';
|
|
8
8
|
import { readComputerUseLiveEvidence } from '../computer-use-live-evidence.js';
|
|
9
|
-
export async function finalizeRouteWithProof(root, { missionId, route, gateFile = null, gate = null, artifacts = [], visualEvidence = null, dbEvidence = null, madSksEvidence = null, testEvidence = null, commandEvidence = null, claims = [], unverified = [], blockers = [], statusHint = 'verified_partial', strict = false, mock = false, fixClaim = false, requireRelation = false, visualClaim = undefined, agents = undefined, allowActiveWrongnessPartial = false } = {}) {
|
|
9
|
+
export async function finalizeRouteWithProof(root, { missionId, route, gateFile = null, gate = null, artifacts = [], visualEvidence = null, dbEvidence = null, madSksEvidence = null, testEvidence = null, commandEvidence = null, claims = [], unverified = [], blockers = [], statusHint = 'verified_partial', strict = false, mock = false, fixClaim = false, requireRelation = false, visualClaim = undefined, agents = undefined, allowActiveWrongnessPartial = false, failureAnalysis = null } = {}) {
|
|
10
10
|
const policy = routeFinalizerPolicy(route, { strict, fixClaim, requireRelation, visualClaim });
|
|
11
11
|
const localBlockers = [...blockers];
|
|
12
12
|
const providedVisualEvidence = visualEvidence;
|
|
@@ -66,6 +66,24 @@ export async function finalizeRouteWithProof(root, { missionId, route, gateFile
|
|
|
66
66
|
? (strict ? 'blocked' : statusHint === 'verified' ? 'verified_partial' : statusHint)
|
|
67
67
|
: visualComputerUseDowngrade ? 'verified_partial'
|
|
68
68
|
: statusHint;
|
|
69
|
+
const finalUnverified = [
|
|
70
|
+
...unverified,
|
|
71
|
+
...(imageEvidence?.mock ? ['Image voxel evidence is mock fixture evidence and does not claim a real visual run.'] : []),
|
|
72
|
+
...(Number(wrongnessEvidence?.medium_severity_active || 0) > 0 ? ['Active medium-severity wrongness memory remains and prevents full verification claims.'] : [])
|
|
73
|
+
];
|
|
74
|
+
const resolvedFailureAnalysis = failureAnalysis || inferRouteFailureAnalysis({
|
|
75
|
+
missionId,
|
|
76
|
+
route: policy.route,
|
|
77
|
+
status,
|
|
78
|
+
blockers: localBlockers,
|
|
79
|
+
unverified: finalUnverified,
|
|
80
|
+
wrongnessEvidence,
|
|
81
|
+
imageEvidence,
|
|
82
|
+
agentEvidence,
|
|
83
|
+
computerUse,
|
|
84
|
+
computerUseLive,
|
|
85
|
+
visualComputerUseDowngrade
|
|
86
|
+
});
|
|
69
87
|
const evidence = {
|
|
70
88
|
...collected,
|
|
71
89
|
...(dbEvidence ? { db: dbEvidence } : {}),
|
|
@@ -109,12 +127,9 @@ export async function finalizeRouteWithProof(root, { missionId, route, gateFile
|
|
|
109
127
|
artifacts,
|
|
110
128
|
evidence,
|
|
111
129
|
claims,
|
|
112
|
-
unverified:
|
|
113
|
-
...unverified,
|
|
114
|
-
...(imageEvidence?.mock ? ['Image voxel evidence is mock fixture evidence and does not claim a real visual run.'] : []),
|
|
115
|
-
...(Number(wrongnessEvidence?.medium_severity_active || 0) > 0 ? ['Active medium-severity wrongness memory remains and prevents full verification claims.'] : [])
|
|
116
|
-
],
|
|
130
|
+
unverified: finalUnverified,
|
|
117
131
|
blockers: localBlockers,
|
|
132
|
+
failureAnalysis: resolvedFailureAnalysis,
|
|
118
133
|
summary: {
|
|
119
134
|
files_changed: collected.files?.length || 0,
|
|
120
135
|
commands_run: evidence.commands?.length || 0,
|
|
@@ -124,4 +139,54 @@ export async function finalizeRouteWithProof(root, { missionId, route, gateFile
|
|
|
124
139
|
}
|
|
125
140
|
});
|
|
126
141
|
}
|
|
142
|
+
function inferRouteFailureAnalysis({ missionId, route, status, blockers, unverified, wrongnessEvidence, imageEvidence, agentEvidence, computerUse, computerUseLive, visualComputerUseDowngrade } = {}) {
|
|
143
|
+
if (status === 'verified' && !blockers?.length && !unverified?.length)
|
|
144
|
+
return null;
|
|
145
|
+
const evidence = [
|
|
146
|
+
missionId ? `.sneakoscope/missions/${missionId}/completion-proof.json#unverified` : 'completion-proof.json#unverified',
|
|
147
|
+
...(wrongnessEvidence?.mission_ledger ? [wrongnessEvidence.mission_ledger] : []),
|
|
148
|
+
...(agentEvidence ? [missionId ? `.sneakoscope/missions/${missionId}/agents/agent-proof-evidence.json` : 'agents/agent-proof-evidence.json'] : []),
|
|
149
|
+
...(imageEvidence?.ledger ? ['image_voxels'] : []),
|
|
150
|
+
...(computerUse ? ['computer_use_status'] : []),
|
|
151
|
+
...(computerUseLive?.path ? [computerUseLive.path] : [])
|
|
152
|
+
];
|
|
153
|
+
if (blockers?.length) {
|
|
154
|
+
return {
|
|
155
|
+
status: 'complete',
|
|
156
|
+
root_cause: `Route ${route || 'unknown'} could not be fully verified because finalization recorded blocking conditions: ${blockers.join(', ')}.`,
|
|
157
|
+
corrective_action: 'Preserved the non-verified completion status, recorded blockers in Completion Proof, and linked the available route evidence instead of claiming full completion.',
|
|
158
|
+
evidence
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (Number(wrongnessEvidence?.medium_severity_active || 0) > 0) {
|
|
162
|
+
return {
|
|
163
|
+
status: 'complete',
|
|
164
|
+
root_cause: `Route ${route || 'unknown'} remains verified_partial because active medium-severity wrongness memory is still present even though no high-severity blocker remains.`,
|
|
165
|
+
corrective_action: 'Kept the Completion Proof at verified_partial, recorded the wrongness caveat in unverified evidence, and avoided a full verified claim until the memory is resolved or explicitly accepted.',
|
|
166
|
+
evidence
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (visualComputerUseDowngrade) {
|
|
170
|
+
return {
|
|
171
|
+
status: 'complete',
|
|
172
|
+
root_cause: 'Native Computer Use visual confidence was downgraded because live capture or Image Voxel linkage was unavailable or incomplete.',
|
|
173
|
+
corrective_action: 'Kept the Completion Proof at verified_partial and recorded the missing native visual evidence instead of claiming high-confidence visual verification.',
|
|
174
|
+
evidence
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (imageEvidence?.mock) {
|
|
178
|
+
return {
|
|
179
|
+
status: 'complete',
|
|
180
|
+
root_cause: 'Visual evidence was produced from a mock fixture, which cannot support a real fully verified visual route claim.',
|
|
181
|
+
corrective_action: 'Kept the Completion Proof at verified_partial and recorded the mock-evidence caveat in unverified evidence.',
|
|
182
|
+
evidence
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
status: 'complete',
|
|
187
|
+
root_cause: `Route ${route || 'unknown'} generated a non-verified completion status because unresolved caveats remained in final unverified evidence.`,
|
|
188
|
+
corrective_action: 'Recorded those caveats in Completion Proof and preserved the partial status instead of upgrading the route to verified.',
|
|
189
|
+
evidence
|
|
190
|
+
};
|
|
191
|
+
}
|
|
127
192
|
//# sourceMappingURL=route-finalizer.js.map
|
|
@@ -3,6 +3,7 @@ import { readRouteProof } from './proof-reader.js';
|
|
|
3
3
|
import { validateCompletionProof } from './validation.js';
|
|
4
4
|
import { normalizeProofRoute, proofStatusBlocks, routeRequiresCompletionProof, routeRequiresImageVoxelAnchors } from './route-proof-policy.js';
|
|
5
5
|
import { routeRequiresAgentIntake } from '../agents/agent-plan.js';
|
|
6
|
+
import { rootCauseAnalysisIssue } from './root-cause-policy.js';
|
|
6
7
|
export async function validateRouteCompletionProof(root, { missionId = null, route = null, state = {}, visualClaim = undefined } = {}) {
|
|
7
8
|
const proofRequired = state.proof_required === true || routeRequiresCompletionProof(route);
|
|
8
9
|
if (!proofRequired)
|
|
@@ -61,6 +62,9 @@ export async function validateRouteCompletionProof(root, { missionId = null, rou
|
|
|
61
62
|
issues.push('active_wrongness_high');
|
|
62
63
|
if (proof.status === 'verified' && Number(wrongness?.active_count || 0) > 0)
|
|
63
64
|
issues.push('active_wrongness_requires_partial');
|
|
65
|
+
const rootCauseIssue = rootCauseAnalysisIssue(proof, issues);
|
|
66
|
+
if (rootCauseIssue)
|
|
67
|
+
issues.push(rootCauseIssue);
|
|
64
68
|
return {
|
|
65
69
|
ok: issues.length === 0,
|
|
66
70
|
required: true,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createReleaseGateHermeticEnv } from './release-gate-hermetic-env.js';
|
|
3
4
|
import { writeReleaseGateJson } from './release-gate-report.js';
|
|
4
|
-
|
|
5
|
+
import { guardedProcessKill, guardContextForRoute } from '../safety/mutation-guard.js';
|
|
6
|
+
import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
|
|
7
|
+
const DISALLOWED_BATCH_RESOURCES = new Set(['zellij-real', 'git-worktree', 'local-llm-real', 'remote-model-real', 'publish', 'global-config', 'timing-sensitive']);
|
|
5
8
|
export function isReleaseGateBatchable(gate) {
|
|
6
9
|
if (gate.side_effect !== 'hermetic')
|
|
7
10
|
return false;
|
|
@@ -11,6 +14,8 @@ export function isReleaseGateBatchable(gate) {
|
|
|
11
14
|
}
|
|
12
15
|
export async function runReleaseGateBatch(root, gates, input = {}) {
|
|
13
16
|
const concurrency = Math.max(1, Math.floor(Number(input.concurrency || process.env.SKS_RELEASE_BATCH_CONCURRENCY || 4)));
|
|
17
|
+
const runId = `rgb-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`;
|
|
18
|
+
const reportRoot = input.reportRoot || path.join(root, '.sneakoscope', 'reports', 'release-gate-batches', runId);
|
|
14
19
|
const nonBatchable = gates.filter((gate) => !isReleaseGateBatchable(gate));
|
|
15
20
|
if (nonBatchable.length) {
|
|
16
21
|
return {
|
|
@@ -19,7 +24,7 @@ export async function runReleaseGateBatch(root, gates, input = {}) {
|
|
|
19
24
|
batch_size: gates.length,
|
|
20
25
|
completed: 0,
|
|
21
26
|
failed: nonBatchable.length,
|
|
22
|
-
results: nonBatchable.map((gate) => ({ id: gate.id, ok: false, exit_code: null, duration_ms: 0 }))
|
|
27
|
+
results: nonBatchable.map((gate) => ({ id: gate.id, ok: false, exit_code: null, signal: null, timed_out: false, duration_ms: 0 }))
|
|
23
28
|
};
|
|
24
29
|
}
|
|
25
30
|
const queue = [...gates];
|
|
@@ -29,7 +34,7 @@ export async function runReleaseGateBatch(root, gates, input = {}) {
|
|
|
29
34
|
const gate = queue.shift();
|
|
30
35
|
if (!gate)
|
|
31
36
|
continue;
|
|
32
|
-
const result = await runOne(root, gate);
|
|
37
|
+
const result = await runOne(root, runId, reportRoot, gate);
|
|
33
38
|
results.push(result);
|
|
34
39
|
if (input.reportRoot)
|
|
35
40
|
writeChildResult(input.reportRoot, result);
|
|
@@ -46,19 +51,60 @@ export async function runReleaseGateBatch(root, gates, input = {}) {
|
|
|
46
51
|
results
|
|
47
52
|
};
|
|
48
53
|
}
|
|
49
|
-
function runOne(root, gate) {
|
|
54
|
+
function runOne(root, runId, reportRoot, gate) {
|
|
50
55
|
const started = Date.now();
|
|
56
|
+
const hermetic = createReleaseGateHermeticEnv({ root, runId, gate, reportRoot });
|
|
51
57
|
return new Promise((resolve) => {
|
|
52
|
-
const child = spawn(gate.command, { cwd: root, shell: true, stdio: ['ignore', 'ignore', 'ignore'] });
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const child = spawn(gate.command, { cwd: root, shell: true, env: hermetic.env, stdio: ['ignore', 'ignore', 'ignore'], detached: process.platform !== 'win32' });
|
|
59
|
+
let timedOut = false;
|
|
60
|
+
let timeoutCleanup = null;
|
|
61
|
+
const timer = setTimeout(() => {
|
|
62
|
+
timedOut = true;
|
|
63
|
+
timeoutCleanup = cleanupTimedOutGateProcessTree(root, child);
|
|
64
|
+
}, gate.timeout_ms);
|
|
65
|
+
timer.unref?.();
|
|
66
|
+
child.on('close', (code, signal) => {
|
|
67
|
+
void (async () => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
if (timeoutCleanup)
|
|
70
|
+
await timeoutCleanup;
|
|
71
|
+
const exitCode = timedOut ? 124 : code;
|
|
72
|
+
resolve({ id: gate.id, ok: exitCode === 0, exit_code: exitCode, signal, timed_out: timedOut, duration_ms: Date.now() - started, report_dir: hermetic.report_dir });
|
|
73
|
+
})();
|
|
57
74
|
});
|
|
58
75
|
});
|
|
59
76
|
}
|
|
77
|
+
async function cleanupTimedOutGateProcessTree(root, child) {
|
|
78
|
+
await killGateProcessTree(root, child, 'SIGTERM');
|
|
79
|
+
await sleep(1500);
|
|
80
|
+
await killGateProcessTree(root, child, 'SIGKILL');
|
|
81
|
+
await sleep(100);
|
|
82
|
+
}
|
|
83
|
+
async function killGateProcessTree(root, child, signal) {
|
|
84
|
+
if (!child.pid)
|
|
85
|
+
return;
|
|
86
|
+
const pid = process.platform !== 'win32' ? -child.pid : child.pid;
|
|
87
|
+
const contract = createRequestedScopeContract({
|
|
88
|
+
route: 'release:gate-batch-runner',
|
|
89
|
+
userRequest: 'Terminate only the batched release gate child process tree after its configured timeout.',
|
|
90
|
+
projectRoot: root,
|
|
91
|
+
overrides: { codex_app_process: true }
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
await guardedProcessKill(guardContextForRoute(root, contract, 'release gate batch timeout cleanup'), pid, { signal, confirmed: true });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
try {
|
|
98
|
+
child.kill(signal);
|
|
99
|
+
}
|
|
100
|
+
catch { }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function sleep(ms) {
|
|
104
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
105
|
+
}
|
|
60
106
|
function writeChildResult(reportRoot, result) {
|
|
61
|
-
const dir = path.join(reportRoot, result.id.replace(/[^A-Za-z0-9_.:-]/g, '_'));
|
|
107
|
+
const dir = result.report_dir || path.join(reportRoot, result.id.replace(/[^A-Za-z0-9_.:-]/g, '_'));
|
|
62
108
|
writeReleaseGateJson(path.join(dir, 'result.json'), {
|
|
63
109
|
schema: 'sks.release-gate-batch-child-result.v1',
|
|
64
110
|
...result
|
|
@@ -114,15 +114,29 @@ export function hashDirectoryRecursive(dir) {
|
|
|
114
114
|
return out.sort();
|
|
115
115
|
}
|
|
116
116
|
export function readReleaseGateCacheHit(root, gate) {
|
|
117
|
+
return Boolean(readReleaseGateCacheRecord(root, gate));
|
|
118
|
+
}
|
|
119
|
+
export function readReleaseGateCacheRecord(root, gate) {
|
|
117
120
|
try {
|
|
118
121
|
const parsed = JSON.parse(fs.readFileSync(releaseGateCacheFile(root), 'utf8'));
|
|
119
|
-
|
|
122
|
+
const record = parsed.schema === RELEASE_GATE_CACHE_V2_SCHEMA ? parsed.records?.[releaseGateCacheKey(root, gate)] : null;
|
|
123
|
+
if (record?.ok !== true)
|
|
124
|
+
return null;
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
gate_id: String(record.gate_id || gate.id),
|
|
128
|
+
command: String(record.command || gate.command),
|
|
129
|
+
resource: Array.isArray(record.resource) ? record.resource.map(String) : gate.resource,
|
|
130
|
+
preset: Array.isArray(record.preset) ? record.preset.map(String) : gate.preset,
|
|
131
|
+
duration_ms: Math.max(0, Math.floor(Number(record.duration_ms) || 0)),
|
|
132
|
+
recorded_at: String(record.recorded_at || '')
|
|
133
|
+
};
|
|
120
134
|
}
|
|
121
135
|
catch {
|
|
122
|
-
return
|
|
136
|
+
return null;
|
|
123
137
|
}
|
|
124
138
|
}
|
|
125
|
-
export function writeReleaseGateCacheHit(root, gate) {
|
|
139
|
+
export function writeReleaseGateCacheHit(root, gate, durationMs = 0) {
|
|
126
140
|
const file = releaseGateCacheFile(root);
|
|
127
141
|
let parsed = { schema: RELEASE_GATE_CACHE_V2_SCHEMA, records: {} };
|
|
128
142
|
try {
|
|
@@ -137,6 +151,7 @@ export function writeReleaseGateCacheHit(root, gate) {
|
|
|
137
151
|
command: gate.command,
|
|
138
152
|
resource: gate.resource,
|
|
139
153
|
preset: gate.preset,
|
|
154
|
+
duration_ms: Math.max(0, Math.floor(Number(durationMs) || 0)),
|
|
140
155
|
recorded_at: new Date().toISOString()
|
|
141
156
|
};
|
|
142
157
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -4,10 +4,13 @@ import { spawn } from 'node:child_process';
|
|
|
4
4
|
import { createReleaseGateHermeticEnv } from './release-gate-hermetic-env.js';
|
|
5
5
|
import { appendReleaseGateJsonl, writeReleaseGateJson } from './release-gate-report.js';
|
|
6
6
|
import { findReadyReleaseGateNodes, findReleaseGatesBlockedByFailedDeps, pickReadyLaunchableReleaseGates } from './release-gate-scheduler.js';
|
|
7
|
-
import {
|
|
7
|
+
import { readReleaseGateCacheRecord, writeReleaseGateCacheHit } from './release-gate-cache-v2.js';
|
|
8
8
|
import { RELEASE_GATE_NODE_SCHEMA, validateReleaseGateManifest } from './release-gate-node.js';
|
|
9
9
|
import { countReleaseGateResources, defaultReleaseGateBudget, summarizeReleaseGateBudget } from './release-gate-resource-governor.js';
|
|
10
10
|
import { selectAffectedReleaseGates } from './release-gate-affected-selector.js';
|
|
11
|
+
import { guardedProcessKill, guardContextForRoute } from '../safety/mutation-guard.js';
|
|
12
|
+
import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
|
|
13
|
+
import { rmrf } from '../fsx.js';
|
|
11
14
|
export function loadReleaseGateManifest(root, file = 'release-gates.v2.json') {
|
|
12
15
|
const manifestPath = path.join(root, file);
|
|
13
16
|
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
@@ -31,9 +34,11 @@ export async function runReleaseGateDag(input) {
|
|
|
31
34
|
? new Set(selected.flatMap((gate) => gate.deps || []).filter((dep) => !selectedIds.has(dep)))
|
|
32
35
|
: new Set();
|
|
33
36
|
const runId = `rg-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`;
|
|
37
|
+
const retentionBefore = await pruneOldReleaseGateRunDirs(root);
|
|
34
38
|
const reportDir = path.join(root, '.sneakoscope', 'reports', 'release-gates', runId);
|
|
35
39
|
fs.mkdirSync(reportDir, { recursive: true });
|
|
36
40
|
const timeline = path.join(reportDir, 'timeline.jsonl');
|
|
41
|
+
appendReleaseGateJsonl(timeline, { event: 'retention', phase: 'before_run', ...retentionBefore, at: new Date().toISOString() });
|
|
37
42
|
const started = Date.now();
|
|
38
43
|
const pending = new Map(selected.map((gate) => [gate.id, gate]));
|
|
39
44
|
const running = new Map();
|
|
@@ -48,7 +53,7 @@ export async function runReleaseGateDag(input) {
|
|
|
48
53
|
let peakRunning = 0;
|
|
49
54
|
const writeSummarySnapshot = (finished = false) => {
|
|
50
55
|
const wallMs = Date.now() - started;
|
|
51
|
-
const failures = [...failed.values()].map((row) => ({ id: row.id, exit_code: row.exit_code, stderr_tail: row.stderr_tail }));
|
|
56
|
+
const failures = [...failed.values()].map((row) => ({ id: row.id, exit_code: row.exit_code, stderr_tail: row.stderr_tail, timed_out: row.timed_out, signal: row.signal }));
|
|
52
57
|
const snapshot = {
|
|
53
58
|
schema: 'sks.release-gate-dag-run.v1',
|
|
54
59
|
ok: failures.length === 0,
|
|
@@ -97,14 +102,15 @@ export async function runReleaseGateDag(input) {
|
|
|
97
102
|
let progressed = false;
|
|
98
103
|
for (const gate of launchable) {
|
|
99
104
|
pending.delete(gate.id);
|
|
100
|
-
const cacheHit = !input.noCache && gate.cache.enabled
|
|
105
|
+
const cacheHit = !input.noCache && gate.cache.enabled ? readReleaseGateCacheRecord(root, gate) : null;
|
|
101
106
|
if (cacheHit) {
|
|
102
|
-
const result = { id: gate.id, ok: true, exit_code: 0, duration_ms:
|
|
107
|
+
const result = { id: gate.id, ok: true, exit_code: 0, signal: null, timed_out: false, duration_ms: cacheHit.duration_ms, cached: true, stderr_tail: '' };
|
|
103
108
|
completed.set(gate.id, result);
|
|
104
109
|
cached += 1;
|
|
105
110
|
cachedGates.push(gate.id);
|
|
111
|
+
sumGateMs += result.duration_ms;
|
|
106
112
|
progressed = true;
|
|
107
|
-
appendReleaseGateJsonl(timeline, { event: 'cache_hit', gate_id: gate.id, at: new Date().toISOString() });
|
|
113
|
+
appendReleaseGateJsonl(timeline, { event: 'cache_hit', gate_id: gate.id, duration_ms: result.duration_ms, at: new Date().toISOString() });
|
|
108
114
|
writeSummarySnapshot(false);
|
|
109
115
|
continue;
|
|
110
116
|
}
|
|
@@ -127,6 +133,8 @@ export async function runReleaseGateDag(input) {
|
|
|
127
133
|
id: gate.id,
|
|
128
134
|
ok: false,
|
|
129
135
|
exit_code: null,
|
|
136
|
+
signal: null,
|
|
137
|
+
timed_out: false,
|
|
130
138
|
duration_ms: 0,
|
|
131
139
|
cached: false,
|
|
132
140
|
stderr_tail: `blocked by failed dependency: ${gate.deps.filter((dep) => failed.has(dep)).join(', ')}`
|
|
@@ -148,7 +156,7 @@ export async function runReleaseGateDag(input) {
|
|
|
148
156
|
completed.set(result.id, result);
|
|
149
157
|
const gate = selected.find((row) => row.id === result.id);
|
|
150
158
|
if (gate?.cache.enabled && !input.noCache)
|
|
151
|
-
writeReleaseGateCacheHit(root, gate);
|
|
159
|
+
writeReleaseGateCacheHit(root, gate, result.duration_ms);
|
|
152
160
|
}
|
|
153
161
|
else {
|
|
154
162
|
failed.set(result.id, result);
|
|
@@ -161,7 +169,11 @@ export async function runReleaseGateDag(input) {
|
|
|
161
169
|
writeSummarySnapshot(false);
|
|
162
170
|
}
|
|
163
171
|
const result = writeSummarySnapshot(true);
|
|
164
|
-
|
|
172
|
+
const retentionAfter = await pruneOldReleaseGateRunDirs(root, { preserveRunId: runId });
|
|
173
|
+
const finalResult = { ...result, retention: mergeReleaseGateRetention(retentionBefore, retentionAfter) };
|
|
174
|
+
appendReleaseGateJsonl(timeline, { event: 'retention', phase: 'after_run', ...retentionAfter, at: new Date().toISOString() });
|
|
175
|
+
writeReleaseGateJson(path.join(reportDir, 'summary.json'), finalResult);
|
|
176
|
+
return finalResult;
|
|
165
177
|
}
|
|
166
178
|
export function selectReleaseGatePreset(manifest, preset) {
|
|
167
179
|
const effectivePreset = preset === 'affected' || preset === 'fast' ? 'release' : preset;
|
|
@@ -175,22 +187,65 @@ function runGate(root, runId, reportRoot, gate) {
|
|
|
175
187
|
const out = fs.createWriteStream(stdoutFile);
|
|
176
188
|
const err = fs.createWriteStream(stderrFile);
|
|
177
189
|
return new Promise((resolve) => {
|
|
178
|
-
const child = spawn(gate.command, { cwd: root, shell: true, env: hermetic.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
179
|
-
|
|
190
|
+
const child = spawn(gate.command, { cwd: root, shell: true, env: hermetic.env, stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32' });
|
|
191
|
+
let timedOut = false;
|
|
192
|
+
let timeoutCleanup = null;
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
timedOut = true;
|
|
195
|
+
timeoutCleanup = cleanupTimedOutGateProcessTree(root, child);
|
|
196
|
+
}, gate.timeout_ms);
|
|
197
|
+
timer.unref?.();
|
|
180
198
|
child.stdout.pipe(out);
|
|
181
199
|
child.stderr.pipe(err);
|
|
182
|
-
child.on('close', (code) => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
child.on('close', (code, signal) => {
|
|
201
|
+
void (async () => {
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
if (timeoutCleanup)
|
|
204
|
+
await timeoutCleanup;
|
|
205
|
+
out.end();
|
|
206
|
+
err.end();
|
|
207
|
+
const durationMs = Date.now() - started;
|
|
208
|
+
const stderrText = fs.existsSync(stderrFile) ? fs.readFileSync(stderrFile, 'utf8') : '';
|
|
209
|
+
const timeoutTail = timedOut ? `release_gate_timeout:${gate.id}:${gate.timeout_ms}ms` : '';
|
|
210
|
+
const signalTail = !timedOut && signal ? `release_gate_signal:${gate.id}:${signal}` : '';
|
|
211
|
+
const stderrTail = tail([stderrText, timeoutTail, signalTail].filter(Boolean).join('\n'));
|
|
212
|
+
const exitCode = timedOut ? 124 : code;
|
|
213
|
+
const result = { id: gate.id, ok: exitCode === 0, exit_code: exitCode, signal, timed_out: timedOut, duration_ms: durationMs, cached: false, stderr_tail: stderrTail };
|
|
214
|
+
writeReleaseGateJson(path.join(hermetic.report_dir, 'result.json'), { schema: 'sks.release-gate-result.v1', ...result, stdout_log: stdoutFile, stderr_log: stderrFile });
|
|
215
|
+
resolve(result);
|
|
216
|
+
})();
|
|
191
217
|
});
|
|
192
218
|
});
|
|
193
219
|
}
|
|
220
|
+
async function cleanupTimedOutGateProcessTree(root, child) {
|
|
221
|
+
await killGateProcessTree(root, child, 'SIGTERM');
|
|
222
|
+
await sleep(1500);
|
|
223
|
+
await killGateProcessTree(root, child, 'SIGKILL');
|
|
224
|
+
await sleep(100);
|
|
225
|
+
}
|
|
226
|
+
async function killGateProcessTree(root, child, signal) {
|
|
227
|
+
if (!child.pid)
|
|
228
|
+
return;
|
|
229
|
+
const pid = process.platform !== 'win32' ? -child.pid : child.pid;
|
|
230
|
+
const contract = createRequestedScopeContract({
|
|
231
|
+
route: 'release:gate-runner',
|
|
232
|
+
userRequest: 'Terminate only the release gate child process tree after its configured timeout.',
|
|
233
|
+
projectRoot: root,
|
|
234
|
+
overrides: { codex_app_process: true }
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
await guardedProcessKill(guardContextForRoute(root, contract, 'release gate timeout cleanup'), pid, { signal, confirmed: true });
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
try {
|
|
241
|
+
child.kill(signal);
|
|
242
|
+
}
|
|
243
|
+
catch { }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function sleep(ms) {
|
|
247
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
248
|
+
}
|
|
194
249
|
function estimateCriticalPath(gates, completed) {
|
|
195
250
|
const byId = new Map(gates.map((gate) => [gate.id, gate]));
|
|
196
251
|
const memo = new Map();
|
|
@@ -210,4 +265,52 @@ function estimateCriticalPath(gates, completed) {
|
|
|
210
265
|
function tail(value, limit = 1200) {
|
|
211
266
|
return value.length > limit ? value.slice(-limit) : value;
|
|
212
267
|
}
|
|
268
|
+
export async function pruneOldReleaseGateRunDirs(root, opts = {}) {
|
|
269
|
+
const keep = Math.max(1, Math.floor(Number(opts.keep ?? process.env.SKS_RELEASE_GATE_RUN_RETENTION ?? 20) || 20));
|
|
270
|
+
const preserveRunId = opts.preserveRunId || null;
|
|
271
|
+
const base = path.join(root, '.sneakoscope', 'reports', 'release-gates');
|
|
272
|
+
const report = {
|
|
273
|
+
schema: 'sks.release-gate-run-retention.v1',
|
|
274
|
+
keep,
|
|
275
|
+
scanned: 0,
|
|
276
|
+
kept: 0,
|
|
277
|
+
removed: 0,
|
|
278
|
+
preserve_run_id: preserveRunId,
|
|
279
|
+
removed_run_ids: []
|
|
280
|
+
};
|
|
281
|
+
if (!fs.existsSync(base))
|
|
282
|
+
return report;
|
|
283
|
+
const runs = fs.readdirSync(base, { withFileTypes: true })
|
|
284
|
+
.filter((entry) => entry.isDirectory() && /^rg-\d{4}-/.test(entry.name))
|
|
285
|
+
.map((entry) => {
|
|
286
|
+
const dir = path.join(base, entry.name);
|
|
287
|
+
const summary = path.join(dir, 'summary.json');
|
|
288
|
+
const stat = fs.statSync(fs.existsSync(summary) ? summary : dir);
|
|
289
|
+
return { id: entry.name, dir, mtimeMs: stat.mtimeMs };
|
|
290
|
+
})
|
|
291
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
292
|
+
report.scanned = runs.length;
|
|
293
|
+
const keepIds = new Set(runs.slice(0, keep).map((run) => run.id));
|
|
294
|
+
if (preserveRunId)
|
|
295
|
+
keepIds.add(preserveRunId);
|
|
296
|
+
for (const run of runs) {
|
|
297
|
+
if (keepIds.has(run.id)) {
|
|
298
|
+
report.kept += 1;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
await rmrf(run.dir);
|
|
302
|
+
report.removed += 1;
|
|
303
|
+
report.removed_run_ids.push(run.id);
|
|
304
|
+
}
|
|
305
|
+
return report;
|
|
306
|
+
}
|
|
307
|
+
function mergeReleaseGateRetention(before, after) {
|
|
308
|
+
return {
|
|
309
|
+
...after,
|
|
310
|
+
scanned: Math.max(before.scanned, after.scanned),
|
|
311
|
+
kept: after.kept,
|
|
312
|
+
removed: before.removed + after.removed,
|
|
313
|
+
removed_run_ids: [...before.removed_run_ids, ...after.removed_run_ids]
|
|
314
|
+
};
|
|
315
|
+
}
|
|
213
316
|
//# sourceMappingURL=release-gate-dag.js.map
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
+
const EXCLUSIVE_RESOURCES = new Set(['timing-sensitive']);
|
|
2
3
|
export function defaultReleaseGateBudget() {
|
|
3
4
|
const cores = Math.max(1, os.cpus().length || 1);
|
|
4
5
|
const base = {
|
|
5
|
-
'cpu-light': Math.min(
|
|
6
|
+
'cpu-light': Math.min(24, cores * 3),
|
|
6
7
|
'cpu-heavy': Math.max(1, cores),
|
|
7
8
|
'io-light': Math.min(96, cores * 10),
|
|
8
9
|
'io-heavy': Math.min(12, Math.max(1, cores)),
|
|
@@ -15,11 +16,12 @@ export function defaultReleaseGateBudget() {
|
|
|
15
16
|
'remote-model-real': 6,
|
|
16
17
|
'global-config': 1,
|
|
17
18
|
publish: 1,
|
|
18
|
-
'fs-read': Math.min(96, cores * 10)
|
|
19
|
+
'fs-read': Math.min(96, cores * 10),
|
|
20
|
+
'timing-sensitive': 1
|
|
19
21
|
};
|
|
20
22
|
for (const key of Object.keys(base)) {
|
|
21
23
|
const envName = `SKS_RELEASE_MAX_${key.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}`;
|
|
22
|
-
base[key] = envInt(envName, base[key]);
|
|
24
|
+
base[key] = envInt(envName, base[key], { max: base[key] });
|
|
23
25
|
}
|
|
24
26
|
return base;
|
|
25
27
|
}
|
|
@@ -34,23 +36,39 @@ export function countReleaseGateResources(running) {
|
|
|
34
36
|
}
|
|
35
37
|
export function pickLaunchableReleaseGates(input) {
|
|
36
38
|
const budget = input.budget || defaultReleaseGateBudget();
|
|
39
|
+
if (input.running.some(isExclusiveGate))
|
|
40
|
+
return [];
|
|
37
41
|
const used = usedResources(input.running);
|
|
38
42
|
const launchable = [];
|
|
39
|
-
const maxTotal = envInt('SKS_RELEASE_MAX_TOTAL',
|
|
43
|
+
const maxTotal = envInt('SKS_RELEASE_MAX_TOTAL', defaultReleaseGateMaxTotal(), { max: defaultReleaseGateMaxTotal() });
|
|
40
44
|
for (const gate of input.ready) {
|
|
41
45
|
if (input.running.length + launchable.length >= maxTotal)
|
|
42
46
|
break;
|
|
47
|
+
const exclusive = isExclusiveGate(gate);
|
|
48
|
+
if (exclusive && (input.running.length > 0 || launchable.length > 0))
|
|
49
|
+
continue;
|
|
50
|
+
if (!exclusive && launchable.some(isExclusiveGate))
|
|
51
|
+
continue;
|
|
43
52
|
if (fits(gate, used, budget)) {
|
|
44
53
|
launchable.push(gate);
|
|
45
54
|
for (const resource of gate.resource)
|
|
46
55
|
used[resource] = (used[resource] || 0) + 1;
|
|
56
|
+
if (exclusive)
|
|
57
|
+
break;
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
60
|
return launchable;
|
|
50
61
|
}
|
|
51
|
-
function
|
|
62
|
+
export function defaultReleaseGateMaxTotal() {
|
|
63
|
+
const cores = Math.max(1, os.cpus().length || 1);
|
|
64
|
+
return Math.max(8, Math.min(32, cores * 3));
|
|
65
|
+
}
|
|
66
|
+
function envInt(name, fallback, opts = {}) {
|
|
52
67
|
const parsed = Number(process.env[name]);
|
|
53
|
-
|
|
68
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
69
|
+
return fallback;
|
|
70
|
+
const value = Math.floor(parsed);
|
|
71
|
+
return typeof opts.max === 'number' ? Math.min(value, opts.max) : value;
|
|
54
72
|
}
|
|
55
73
|
function usedResources(running) {
|
|
56
74
|
const used = {};
|
|
@@ -63,4 +81,7 @@ function usedResources(running) {
|
|
|
63
81
|
function fits(gate, used, budget) {
|
|
64
82
|
return gate.resource.every((resource) => (used[resource] || 0) < budget[resource]);
|
|
65
83
|
}
|
|
84
|
+
function isExclusiveGate(gate) {
|
|
85
|
+
return gate.resource.some((resource) => EXCLUSIVE_RESOURCES.has(resource));
|
|
86
|
+
}
|
|
66
87
|
//# sourceMappingURL=release-gate-resource-governor.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// SKS Core Skill Engine — SkillOpt-style epoch-wise meta-update.
|
|
2
|
+
//
|
|
3
|
+
// SkillOpt adjusts its textual learning rate between epochs: rejected edits decay
|
|
4
|
+
// the budget (smaller, safer proposals), accepted edits regrow it toward the
|
|
5
|
+
// default ceiling. Deterministic, bounded, never exceeds the configured maximum.
|
|
6
|
+
import { DEFAULT_TEXTUAL_LEARNING_RATE } from './core-skill-epoch.js';
|
|
7
|
+
export const MIN_TEXTUAL_LEARNING_RATE = { max_added_chars: 100, max_deleted_chars: 50, max_replaced_chars: 75 };
|
|
8
|
+
export const META_UPDATE_DECAY = 0.5;
|
|
9
|
+
export const META_UPDATE_GROWTH = 1.25;
|
|
10
|
+
/** Epoch-wise textual learning-rate adjustment: decay on rejection, bounded regrowth on acceptance. */
|
|
11
|
+
export function metaUpdateLearningRate(rate, outcome, opts = {}) {
|
|
12
|
+
const decay = opts.decay ?? META_UPDATE_DECAY;
|
|
13
|
+
const growth = opts.growth ?? META_UPDATE_GROWTH;
|
|
14
|
+
const min = opts.min ?? MIN_TEXTUAL_LEARNING_RATE;
|
|
15
|
+
const max = opts.max ?? DEFAULT_TEXTUAL_LEARNING_RATE;
|
|
16
|
+
const factor = outcome === 'rejected' ? decay : growth;
|
|
17
|
+
const adjust = (value, lo, hi) => Math.min(hi, Math.max(lo, Math.round(value * factor)));
|
|
18
|
+
return {
|
|
19
|
+
max_added_chars: adjust(rate.max_added_chars, min.max_added_chars, max.max_added_chars),
|
|
20
|
+
max_deleted_chars: adjust(rate.max_deleted_chars, min.max_deleted_chars, max.max_deleted_chars),
|
|
21
|
+
max_replaced_chars: adjust(rate.max_replaced_chars, min.max_replaced_chars, max.max_replaced_chars)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=core-skill-meta-update.js.map
|