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.
Files changed (84) hide show
  1. package/README.md +1 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/cli/install-helpers.js +6 -7
  8. package/dist/commands/zellij-slot-column-anchor.js +3 -1
  9. package/dist/commands/zellij-slot-pane.js +19 -2
  10. package/dist/core/agents/agent-janitor.js +10 -1
  11. package/dist/core/agents/agent-orchestrator.js +8 -2
  12. package/dist/core/agents/agent-proof-evidence.js +20 -0
  13. package/dist/core/agents/agent-runner-ollama.js +11 -4
  14. package/dist/core/agents/fast-mode-policy.js +7 -5
  15. package/dist/core/agents/intelligent-work-graph.js +93 -14
  16. package/dist/core/agents/native-cli-session-swarm.js +115 -9
  17. package/dist/core/agents/no-subagent-scaling-policy.js +10 -1
  18. package/dist/core/agents/official-subagent-helper-policy.js +62 -0
  19. package/dist/core/codex-app.js +0 -2
  20. package/dist/core/codex-control/codex-task-runner.js +9 -0
  21. package/dist/core/commands/fast-mode-command.js +1 -1
  22. package/dist/core/commands/loop-command.js +86 -13
  23. package/dist/core/commands/naruto-command.js +34 -21
  24. package/dist/core/commands/team-command.js +1 -0
  25. package/dist/core/commands/wiki-command.js +35 -1
  26. package/dist/core/fsx.js +1 -1
  27. package/dist/core/init.js +1 -2
  28. package/dist/core/locks/file-lock.js +88 -0
  29. package/dist/core/loops/loop-artifacts.js +54 -2
  30. package/dist/core/loops/loop-checkpoint.js +22 -0
  31. package/dist/core/loops/loop-concurrency-budget.js +55 -0
  32. package/dist/core/loops/loop-final-arbiter-contract.js +28 -0
  33. package/dist/core/loops/loop-finalizer.js +55 -7
  34. package/dist/core/loops/loop-fixture-policy.js +58 -0
  35. package/dist/core/loops/loop-gate-registry.js +96 -0
  36. package/dist/core/loops/loop-gate-runner.js +206 -17
  37. package/dist/core/loops/loop-gpt-final-arbiter.js +81 -0
  38. package/dist/core/loops/loop-integration-merge.js +80 -0
  39. package/dist/core/loops/loop-interrupt-registry.js +118 -0
  40. package/dist/core/loops/loop-lease.js +35 -20
  41. package/dist/core/loops/loop-merge-strategy.js +105 -0
  42. package/dist/core/loops/loop-mutation-ledger.js +103 -0
  43. package/dist/core/loops/loop-planner.js +36 -5
  44. package/dist/core/loops/loop-runtime-control.js +27 -0
  45. package/dist/core/loops/loop-runtime.js +254 -96
  46. package/dist/core/loops/loop-scheduler.js +14 -5
  47. package/dist/core/loops/loop-side-effect-scanner.js +91 -0
  48. package/dist/core/loops/loop-worker-prompts.js +43 -0
  49. package/dist/core/loops/loop-worker-runtime.js +281 -0
  50. package/dist/core/loops/loop-worktree-runtime.js +92 -0
  51. package/dist/core/naruto/naruto-finalizer.js +7 -2
  52. package/dist/core/naruto/naruto-loop-mesh.js +10 -1
  53. package/dist/core/proof/auto-finalize.js +3 -2
  54. package/dist/core/proof/proof-schema.js +6 -0
  55. package/dist/core/proof/proof-writer.js +5 -2
  56. package/dist/core/proof/root-cause-policy.js +70 -0
  57. package/dist/core/proof/route-adapter.js +18 -1
  58. package/dist/core/proof/route-finalizer.js +71 -6
  59. package/dist/core/proof/route-proof-gate.js +4 -0
  60. package/dist/core/release/release-gate-batch-runner.js +56 -10
  61. package/dist/core/release/release-gate-cache-v2.js +18 -3
  62. package/dist/core/release/release-gate-dag.js +121 -18
  63. package/dist/core/release/release-gate-node.js +2 -1
  64. package/dist/core/release/release-gate-resource-governor.js +27 -6
  65. package/dist/core/skills/core-skill-meta-update.js +24 -0
  66. package/dist/core/skills/core-skill-reflection.js +94 -0
  67. package/dist/core/skills/core-skill-trainer.js +103 -0
  68. package/dist/core/trust-kernel/completion-contract.js +4 -0
  69. package/dist/core/trust-kernel/route-contract.js +4 -1
  70. package/dist/core/version.js +1 -1
  71. package/dist/core/zellij/zellij-right-column-manager.js +13 -2
  72. package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
  73. package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
  74. package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
  75. package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
  76. package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +14 -2
  77. package/dist/scripts/loop-directive-check-lib.js +225 -2
  78. package/dist/scripts/loop-hardening-check-lib.js +289 -0
  79. package/dist/scripts/loop-worker-fixture-child.js +53 -0
  80. package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
  81. package/dist/scripts/prepublish-release-check-or-fast.js +38 -10
  82. package/dist/scripts/release-check-stamp.js +29 -4
  83. package/dist/scripts/release-gate-existence-audit.js +1 -0
  84. 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
- const DISALLOWED_BATCH_RESOURCES = new Set(['zellij-real', 'git-worktree', 'local-llm-real', 'remote-model-real', 'publish', 'global-config']);
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
- const timer = setTimeout(() => child.kill('SIGTERM'), gate.timeout_ms);
54
- child.on('close', (code) => {
55
- clearTimeout(timer);
56
- resolve({ id: gate.id, ok: code === 0, exit_code: code, duration_ms: Date.now() - started });
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
- return parsed.schema === RELEASE_GATE_CACHE_V2_SCHEMA && parsed.records?.[releaseGateCacheKey(root, gate)]?.ok === true;
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 false;
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 { readReleaseGateCacheHit, writeReleaseGateCacheHit } from './release-gate-cache-v2.js';
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 && readReleaseGateCacheHit(root, gate);
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: 0, cached: true, stderr_tail: '' };
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
- return result;
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
- const timer = setTimeout(() => child.kill('SIGTERM'), gate.timeout_ms);
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
- clearTimeout(timer);
184
- out.end();
185
- err.end();
186
- const durationMs = Date.now() - started;
187
- const stderrTail = tail(fs.existsSync(stderrFile) ? fs.readFileSync(stderrFile, 'utf8') : '');
188
- const result = { id: gate.id, ok: code === 0, exit_code: code, duration_ms: durationMs, cached: false, stderr_tail: stderrTail };
189
- writeReleaseGateJson(path.join(hermetic.report_dir, 'result.json'), { schema: 'sks.release-gate-result.v1', ...result, stdout_log: stdoutFile, stderr_log: stderrFile });
190
- resolve(result);
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
@@ -13,7 +13,8 @@ export const RELEASE_GATE_RESOURCE_CLASSES = [
13
13
  'network',
14
14
  'global-config',
15
15
  'publish',
16
- 'fs-read'
16
+ 'fs-read',
17
+ 'timing-sensitive'
17
18
  ];
18
19
  export function validateReleaseGateManifest(input) {
19
20
  const errors = [];
@@ -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(48, cores * 6),
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', Number.POSITIVE_INFINITY);
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 envInt(name, fallback) {
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
- return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
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