sneakoscope 3.0.4 → 3.1.1
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/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 +1 -0
- package/dist/core/agents/agent-runner-ollama.js +11 -4
- package/dist/core/agents/native-cli-session-swarm.js +69 -9
- package/dist/core/agents/runtime-proof-summary.js +4 -0
- package/dist/core/codex-control/codex-task-runner.js +9 -0
- package/dist/core/commands/goal-command.js +19 -1
- package/dist/core/commands/loop-command.js +176 -0
- package/dist/core/commands/naruto-command.js +26 -17
- package/dist/core/commands/team-command.js +1 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +6 -1
- package/dist/core/locks/file-lock.js +88 -0
- package/dist/core/loops/goal-to-loop-compat.js +23 -0
- package/dist/core/loops/loop-artifacts.js +72 -0
- package/dist/core/loops/loop-checkpoint.js +22 -0
- package/dist/core/loops/loop-decomposer.js +56 -0
- package/dist/core/loops/loop-finalizer.js +54 -0
- package/dist/core/loops/loop-gate-ladder.js +16 -0
- package/dist/core/loops/loop-gate-registry.js +96 -0
- package/dist/core/loops/loop-gate-runner.js +177 -0
- package/dist/core/loops/loop-gate-selector.js +52 -0
- package/dist/core/loops/loop-gpt-final-arbiter.js +61 -0
- package/dist/core/loops/loop-integration-merge.js +75 -0
- package/dist/core/loops/loop-iteration-runner.js +2 -0
- package/dist/core/loops/loop-lease.js +91 -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 +170 -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-control.js +25 -0
- package/dist/core/loops/loop-runtime.js +314 -0
- package/dist/core/loops/loop-scheduler.js +69 -0
- package/dist/core/loops/loop-schema.js +63 -0
- package/dist/core/loops/loop-state.js +61 -0
- package/dist/core/loops/loop-worker-prompts.js +43 -0
- package/dist/core/loops/loop-worker-runtime.js +275 -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 +39 -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/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-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 +65 -17
- 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 +45 -5
- package/dist/core/zellij/zellij-slot-pane-renderer.js +37 -10
- package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
- package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
- package/dist/scripts/loop-directive-check-lib.js +388 -0
- package/dist/scripts/loop-worker-fixture-child.js +53 -0
- package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
- package/package.json +38 -3
- 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,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
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SKS Core Skill Engine — SkillOpt-style reflection stage.
|
|
2
|
+
//
|
|
3
|
+
// SkillOpt's training loop is rollout → reflection → aggregation → selection →
|
|
4
|
+
// validation-gated update. This module implements the reflect/aggregate/select
|
|
5
|
+
// stages deterministically (no model call): each scored rollout yields reflection
|
|
6
|
+
// records for its deficient score dimensions, reflections are aggregated and
|
|
7
|
+
// ranked across the batch, and the top-ranked deficiencies are mapped to bounded
|
|
8
|
+
// add-operations on the single skill document.
|
|
9
|
+
import { scoreRollout } from './core-skill-scorer.js';
|
|
10
|
+
// dimension → { instruction line, target section, regex proving the card already covers it }
|
|
11
|
+
const DIMENSION_LESSONS = {
|
|
12
|
+
proof_completeness: {
|
|
13
|
+
target: 'section:verification',
|
|
14
|
+
text: '- Always emit a proof artifact before reporting success.',
|
|
15
|
+
covered: /proof artifact/i
|
|
16
|
+
},
|
|
17
|
+
rollback_ready: {
|
|
18
|
+
target: 'section:rollback',
|
|
19
|
+
text: '- Record a rollback-ready checkpoint before mutating anything.',
|
|
20
|
+
covered: /rollback/i
|
|
21
|
+
},
|
|
22
|
+
latency_budget: {
|
|
23
|
+
target: 'section:latency',
|
|
24
|
+
text: '- Prefer the smallest sufficient probe; stop expanding scope once the latency budget is at risk.',
|
|
25
|
+
covered: /latency budget/i
|
|
26
|
+
},
|
|
27
|
+
requested_scope_compliance: {
|
|
28
|
+
target: 'section:scope',
|
|
29
|
+
text: '- Touch only what the request names; treat any out-of-scope mutation as a hard stop.',
|
|
30
|
+
covered: /out-of-scope mutation/i
|
|
31
|
+
},
|
|
32
|
+
side_effect_zero: {
|
|
33
|
+
target: 'section:scope',
|
|
34
|
+
text: '- Leave zero side effects: never mutate outside the requested scope.',
|
|
35
|
+
covered: /zero side effects/i
|
|
36
|
+
},
|
|
37
|
+
task_success: {
|
|
38
|
+
target: 'section:method',
|
|
39
|
+
text: '- Re-read the recorded failure reason and address it directly before retrying.',
|
|
40
|
+
covered: /failure reason/i
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/** Reflect on one scored rollout: one reflection per deficient score dimension. */
|
|
44
|
+
export function reflectOnTrace(trace, opts = {}) {
|
|
45
|
+
const score = scoreRollout(trace, opts);
|
|
46
|
+
const reflections = [];
|
|
47
|
+
for (const dimension of Object.keys(score.components)) {
|
|
48
|
+
const value = score.components[dimension];
|
|
49
|
+
if (value >= 1)
|
|
50
|
+
continue;
|
|
51
|
+
reflections.push({
|
|
52
|
+
dimension,
|
|
53
|
+
severity: Number((1 - value).toFixed(6)),
|
|
54
|
+
note: trace.failure_reason ? `deficient ${dimension} (failure: ${trace.failure_reason})` : `deficient ${dimension}`
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return reflections;
|
|
58
|
+
}
|
|
59
|
+
/** Aggregate reflections across a rollout batch, ranked by total severity then count. */
|
|
60
|
+
export function aggregateReflections(reflections) {
|
|
61
|
+
const byDimension = new Map();
|
|
62
|
+
for (const reflection of reflections) {
|
|
63
|
+
const entry = byDimension.get(reflection.dimension) ?? { dimension: reflection.dimension, count: 0, severity: 0 };
|
|
64
|
+
entry.count += 1;
|
|
65
|
+
entry.severity = Number((entry.severity + reflection.severity).toFixed(6));
|
|
66
|
+
byDimension.set(reflection.dimension, entry);
|
|
67
|
+
}
|
|
68
|
+
return [...byDimension.values()].sort((a, b) => b.severity - a.severity || b.count - a.count || a.dimension.localeCompare(b.dimension));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Select bounded patch operations from aggregated reflections. Skips lessons the
|
|
72
|
+
* card body already covers, and truncates the selection so total added chars fit
|
|
73
|
+
* the textual learning-rate budget.
|
|
74
|
+
*/
|
|
75
|
+
export function selectPatchOperations(card, aggregated, learningRate, opts = {}) {
|
|
76
|
+
const maxOperations = Math.max(1, opts.maxOperations ?? 2);
|
|
77
|
+
const operations = [];
|
|
78
|
+
let addedChars = 0;
|
|
79
|
+
for (const entry of aggregated) {
|
|
80
|
+
if (operations.length >= maxOperations)
|
|
81
|
+
break;
|
|
82
|
+
const lesson = DIMENSION_LESSONS[entry.dimension];
|
|
83
|
+
if (!lesson || lesson.covered.test(card.body))
|
|
84
|
+
continue;
|
|
85
|
+
if (operations.some((op) => op.op === 'add' && op.text === lesson.text))
|
|
86
|
+
continue;
|
|
87
|
+
if (addedChars + lesson.text.length > learningRate.max_added_chars)
|
|
88
|
+
continue;
|
|
89
|
+
operations.push({ op: 'add', target: lesson.target, text: lesson.text });
|
|
90
|
+
addedChars += lesson.text.length;
|
|
91
|
+
}
|
|
92
|
+
return operations;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=core-skill-reflection.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SKS Core Skill Engine — SkillOpt-style multi-epoch trainer.
|
|
2
|
+
//
|
|
3
|
+
// Closes the SkillOpt training loop over the existing primitives: per epoch a
|
|
4
|
+
// rollout batch is reflected on (reflect → aggregate → select), the selected
|
|
5
|
+
// bounded edit goes through the strict held-out gate (runSkillEpoch), and the
|
|
6
|
+
// textual learning rate is meta-updated from the outcome. The best held-out
|
|
7
|
+
// accepted card is exported as the deployable best-skill artifact (SkillOpt's
|
|
8
|
+
// best_skill.md analogue). Pure/deterministic: the caller supplies the held-out
|
|
9
|
+
// evaluator; no model call is made here. Forbidden in deployment context.
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { ensureDir, nowIso, writeJsonAtomic } from '../fsx.js';
|
|
12
|
+
import { assertNotInDeployment } from './core-skill-deployment.js';
|
|
13
|
+
import { DEFAULT_TEXTUAL_LEARNING_RATE, runSkillEpoch } from './core-skill-epoch.js';
|
|
14
|
+
import { metaUpdateLearningRate } from './core-skill-meta-update.js';
|
|
15
|
+
import { applyPatch } from './core-skill-patch-apply.js';
|
|
16
|
+
import { aggregateReflections, reflectOnTrace, selectPatchOperations } from './core-skill-reflection.js';
|
|
17
|
+
import { CORE_SKILL_PATCH_SCHEMA } from './core-skill-types.js';
|
|
18
|
+
export const CORE_SKILL_TRAINING_REPORT_SCHEMA = 'sks.core-skill-training-report.v1';
|
|
19
|
+
/** Run the SkillOpt training loop in a TRAINING/EVALUATION context. */
|
|
20
|
+
export async function trainSkill(root, input) {
|
|
21
|
+
assertNotInDeployment('trainSkill');
|
|
22
|
+
let current = input.card;
|
|
23
|
+
let learningRate = { ...(input.learningRate ?? DEFAULT_TEXTUAL_LEARNING_RATE) };
|
|
24
|
+
let currentProbe = input.evaluateHeldout(current);
|
|
25
|
+
const baselineHeldout = currentProbe.heldout;
|
|
26
|
+
const records = [];
|
|
27
|
+
let acceptedCount = 0;
|
|
28
|
+
for (let epoch = 0; epoch < input.epochs.length; epoch += 1) {
|
|
29
|
+
const traces = input.epochs[epoch] ?? [];
|
|
30
|
+
const reflections = traces.flatMap((trace) => reflectOnTrace(trace, { ...(input.latencyBudgetMs === undefined ? {} : { latencyBudgetMs: input.latencyBudgetMs }) }));
|
|
31
|
+
const aggregated = aggregateReflections(reflections);
|
|
32
|
+
const operations = selectPatchOperations(current, aggregated, learningRate, { ...(input.maxOperationsPerEpoch === undefined ? {} : { maxOperations: input.maxOperationsPerEpoch }) });
|
|
33
|
+
if (!operations.length) {
|
|
34
|
+
records.push({ epoch, accepted: false, reason: 'no_proposal', patch_hash: null, score_delta: 0, learning_rate: { ...learningRate } });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const patch = {
|
|
38
|
+
schema: CORE_SKILL_PATCH_SCHEMA,
|
|
39
|
+
skill_id: current.skill_id,
|
|
40
|
+
base_version: current.version,
|
|
41
|
+
operations,
|
|
42
|
+
textual_learning_rate: { ...learningRate }
|
|
43
|
+
};
|
|
44
|
+
// Probe the candidate's held-out score BEFORE the gated update.
|
|
45
|
+
const preview = applyPatch(current, patch);
|
|
46
|
+
const candidateProbe = preview.ok && preview.candidate ? input.evaluateHeldout(preview.candidate) : currentProbe;
|
|
47
|
+
const result = await runSkillEpoch(root, {
|
|
48
|
+
card: current,
|
|
49
|
+
trainTraces: traces,
|
|
50
|
+
patch,
|
|
51
|
+
validation: {
|
|
52
|
+
baselineHeldout: currentProbe.heldout,
|
|
53
|
+
candidateHeldout: candidateProbe.heldout,
|
|
54
|
+
sideEffectZero: candidateProbe.sideEffectZero,
|
|
55
|
+
requestedScopeCompliant: candidateProbe.requestedScopeCompliant,
|
|
56
|
+
proofCompletenessBaseline: currentProbe.proofCompleteness,
|
|
57
|
+
proofCompletenessCandidate: candidateProbe.proofCompleteness,
|
|
58
|
+
rollbackReadyBaseline: currentProbe.rollbackReady,
|
|
59
|
+
rollbackReadyCandidate: candidateProbe.rollbackReady,
|
|
60
|
+
latencyBaselineMs: currentProbe.latencyMs,
|
|
61
|
+
latencyCandidateMs: candidateProbe.latencyMs
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
records.push({ epoch, accepted: result.accepted, reason: result.reason, patch_hash: result.patch_hash, score_delta: result.score_delta, learning_rate: { ...learningRate } });
|
|
65
|
+
if (result.accepted && result.candidate) {
|
|
66
|
+
current = result.candidate;
|
|
67
|
+
currentProbe = candidateProbe;
|
|
68
|
+
acceptedCount += 1;
|
|
69
|
+
learningRate = metaUpdateLearningRate(learningRate, 'accepted');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
learningRate = metaUpdateLearningRate(learningRate, 'rejected');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const reportsDir = path.join(path.resolve(root), '.sneakoscope', 'reports');
|
|
76
|
+
await ensureDir(reportsDir);
|
|
77
|
+
const reportPath = path.join(reportsDir, 'core-skill-training-report.json');
|
|
78
|
+
const skillDir = path.join(path.resolve(root), '.sneakoscope', 'skills', current.route, current.skill_id);
|
|
79
|
+
await ensureDir(skillDir);
|
|
80
|
+
const bestSkillPath = path.join(skillDir, 'best-skill.json');
|
|
81
|
+
await writeJsonAtomic(bestSkillPath, { ...current, exported_as: 'best_skill', exported_at: nowIso() });
|
|
82
|
+
await writeJsonAtomic(reportPath, {
|
|
83
|
+
schema: CORE_SKILL_TRAINING_REPORT_SCHEMA,
|
|
84
|
+
skill_id: input.card.skill_id,
|
|
85
|
+
route: input.card.route,
|
|
86
|
+
baseline_heldout: baselineHeldout,
|
|
87
|
+
best_heldout: currentProbe.heldout,
|
|
88
|
+
best_version: current.version,
|
|
89
|
+
accepted_count: acceptedCount,
|
|
90
|
+
epochs: records,
|
|
91
|
+
generated_at: nowIso()
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
epochs: records,
|
|
95
|
+
best: current,
|
|
96
|
+
best_heldout: currentProbe.heldout,
|
|
97
|
+
baseline_heldout: baselineHeldout,
|
|
98
|
+
accepted_count: acceptedCount,
|
|
99
|
+
report_path: reportPath,
|
|
100
|
+
best_skill_path: bestSkillPath
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=core-skill-trainer.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateCompletionProof } from '../proof/validation.js';
|
|
2
|
+
import { rootCauseAnalysisIssue } from '../proof/root-cause-policy.js';
|
|
2
3
|
function asRecord(value) {
|
|
3
4
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
4
5
|
}
|
|
@@ -44,6 +45,9 @@ export function validateCompletionContract(contract = {}, proof = {}, evidenceIn
|
|
|
44
45
|
issues.push('active_wrongness_requires_verified_partial');
|
|
45
46
|
if (claimLinksActiveWrongness(proofRecord, wrongness))
|
|
46
47
|
issues.push('claim_linked_to_active_wrongness');
|
|
48
|
+
const rootCauseIssue = rootCauseAnalysisIssue(proofRecord, issues);
|
|
49
|
+
if (rootCauseIssue)
|
|
50
|
+
issues.push(rootCauseIssue);
|
|
47
51
|
const status = typeof proofRecord.status === 'string'
|
|
48
52
|
? proofRecord.status
|
|
49
53
|
: typeof contractRecord.status === 'string'
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { writeJsonAtomic } from '../fsx.js';
|
|
3
3
|
import { missionDir } from '../mission.js';
|
|
4
4
|
import { routeRequiresCompletionProof, routeRequiresImageVoxelAnchors } from '../proof/route-proof-policy.js';
|
|
5
|
+
import { rootCauseAnalysisRequired } from '../proof/root-cause-policy.js';
|
|
5
6
|
import { ROUTE_COMPLETION_CONTRACT_SCHEMA, normalizeTrustStatus, trustKernelMetadata } from './trust-kernel-schema.js';
|
|
6
7
|
import { validateCompletionContract } from './completion-contract.js';
|
|
7
8
|
function asRecord(value) {
|
|
@@ -51,7 +52,8 @@ export function routeRequirements(route, proof = {}) {
|
|
|
51
52
|
image_voxels: routeRequiresImageVoxelAnchors(route),
|
|
52
53
|
db_safety: route === '$DB' || Boolean(evidence.db || evidence.db_safety),
|
|
53
54
|
tests: Boolean(evidence.tests),
|
|
54
|
-
blackbox: JSON.stringify(evidence).includes('blackbox')
|
|
55
|
+
blackbox: JSON.stringify(evidence).includes('blackbox'),
|
|
56
|
+
root_cause_analysis: rootCauseAnalysisRequired(proofRecord)
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
function evidencePathsForContract(missionId, proof, evidenceIndex, required) {
|
|
@@ -67,6 +69,7 @@ function evidencePathsForContract(missionId, proof, evidenceIndex, required) {
|
|
|
67
69
|
tests: pathFromEvidence(proofEvidence.tests),
|
|
68
70
|
db_safety: pathFromEvidence(proofEvidence.db || proofEvidence.db_safety),
|
|
69
71
|
blackbox: pathFromEvidence(proofEvidence.blackbox),
|
|
72
|
+
root_cause_analysis: required.root_cause_analysis && base ? `${base}/completion-proof.json#failure_analysis` : null,
|
|
70
73
|
evidence_index: base ? `${base}/evidence-index.json` : null,
|
|
71
74
|
trust_report: base ? `${base}/trust-report.json` : null,
|
|
72
75
|
evidence_records: asList(evidenceIndexRecord.records).map((entry) => {
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '3.
|
|
1
|
+
export const PACKAGE_VERSION = '3.1.1';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -135,6 +135,9 @@ async function prepareWorkerInRightColumnUnlocked(input) {
|
|
|
135
135
|
return { state: next, placement: 'zellij-pane', focusPaneId, yOrder };
|
|
136
136
|
}
|
|
137
137
|
export async function recordWorkerPaneInRightColumn(input) {
|
|
138
|
+
return withRightColumnLock(input.root, input.missionId, async () => recordWorkerPaneInRightColumnUnlocked(input));
|
|
139
|
+
}
|
|
140
|
+
async function recordWorkerPaneInRightColumnUnlocked(input) {
|
|
138
141
|
const paths = resolveRightColumnPaths(input.root, input.missionId, input.projectRoot);
|
|
139
142
|
const state = await readRightColumnState(input.root, input.missionId, input.projectRoot);
|
|
140
143
|
if (!state)
|
|
@@ -154,6 +157,7 @@ export async function recordWorkerPaneInRightColumn(input) {
|
|
|
154
157
|
...state,
|
|
155
158
|
updated_at: nowIso(),
|
|
156
159
|
right_anchor_pane_id: input.record.pane_id || state.right_anchor_pane_id,
|
|
160
|
+
slot_column_anchor_pane_id: input.record.slot_column_anchor_pane_id || state.slot_column_anchor_pane_id || null,
|
|
157
161
|
visible_worker_panes: rows.sort((a, b) => a.y_order - b.y_order),
|
|
158
162
|
blockers: [...new Set([...state.blockers, ...(input.record.blockers || [])])]
|
|
159
163
|
});
|
|
@@ -173,6 +177,9 @@ export async function recordWorkerPaneInRightColumn(input) {
|
|
|
173
177
|
return next;
|
|
174
178
|
}
|
|
175
179
|
export async function recordSlotColumnAnchorInRightColumn(input) {
|
|
180
|
+
return withRightColumnLock(input.root, input.missionId, async () => recordSlotColumnAnchorInRightColumnUnlocked(input));
|
|
181
|
+
}
|
|
182
|
+
async function recordSlotColumnAnchorInRightColumnUnlocked(input) {
|
|
176
183
|
const paths = resolveRightColumnPaths(input.root, input.missionId, input.projectRoot);
|
|
177
184
|
const state = await readRightColumnState(input.root, input.missionId, input.projectRoot) || {
|
|
178
185
|
schema: ZELLIJ_RIGHT_COLUMN_STATE_SCHEMA,
|
|
@@ -238,6 +245,9 @@ async function recordHeadlessWorkerInRightColumnUnlocked(input) {
|
|
|
238
245
|
return next;
|
|
239
246
|
}
|
|
240
247
|
export async function closeWorkerInRightColumn(input) {
|
|
248
|
+
return withRightColumnLock(input.root, input.missionId, async () => closeWorkerInRightColumnUnlocked(input));
|
|
249
|
+
}
|
|
250
|
+
async function closeWorkerInRightColumnUnlocked(input) {
|
|
241
251
|
const paths = resolveRightColumnPaths(input.root, input.missionId, input.projectRoot);
|
|
242
252
|
const state = await readRightColumnState(input.root, input.missionId, input.projectRoot);
|
|
243
253
|
if (!state)
|
|
@@ -333,14 +343,15 @@ async function withRightColumnLock(root, missionId, fn) {
|
|
|
333
343
|
const current = new Promise((resolve) => {
|
|
334
344
|
release = resolve;
|
|
335
345
|
});
|
|
336
|
-
|
|
346
|
+
const chained = previous.then(() => current, () => current);
|
|
347
|
+
rightColumnLocks.set(key, chained);
|
|
337
348
|
await previous.catch(() => undefined);
|
|
338
349
|
try {
|
|
339
350
|
return await fn();
|
|
340
351
|
}
|
|
341
352
|
finally {
|
|
342
353
|
release();
|
|
343
|
-
if (rightColumnLocks.get(key) ===
|
|
354
|
+
if (rightColumnLocks.get(key) === chained)
|
|
344
355
|
rightColumnLocks.delete(key);
|
|
345
356
|
}
|
|
346
357
|
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { packageRoot } from '../fsx.js';
|
|
3
4
|
import { readZellijSlotTelemetrySnapshot } from './zellij-slot-telemetry.js';
|
|
5
|
+
import { workerBackendTag } from './zellij-worker-pane-manager.js';
|
|
6
|
+
function readPackageVersion() {
|
|
7
|
+
try {
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot(), 'package.json'), 'utf8'));
|
|
9
|
+
return String(pkg?.version || '?');
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return '?';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
export function renderZellijSlotColumnAnchor(input = {}) {
|
|
5
16
|
const active = nonNegativeInt(input.activeWorkers, 0);
|
|
6
17
|
const visible = Math.max(1, nonNegativeInt(input.visiblePaneCap, active || 1));
|
|
@@ -11,9 +22,12 @@ export function renderZellijSlotColumnAnchor(input = {}) {
|
|
|
11
22
|
const update = input.updateAvailableVersion ? ` · update ${trimInline(input.updateAvailableVersion, 18)} available` : '';
|
|
12
23
|
const madDb = input.madDbActive ? ' · MAD-DB ACTIVE' : '';
|
|
13
24
|
const appHandoff = input.qaAppHandoffPending ? ' · QA /app handoff pending' : '';
|
|
14
|
-
const
|
|
25
|
+
const loopHeader = input.loopsTotal != null
|
|
26
|
+
? `LOOPS ${nonNegativeInt(input.loopsTotal, 0)} · running ${nonNegativeInt(input.loopsRunning, 0)} · blocked ${nonNegativeInt(input.loopsBlocked, 0)} · done ${nonNegativeInt(input.loopsCompleted, 0)} · workers ${active}`
|
|
27
|
+
: null;
|
|
28
|
+
const header = loopHeader || (done || fail
|
|
15
29
|
? `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}
|
|
30
|
+
: `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}${update}${madDb}${appHandoff}`);
|
|
17
31
|
const workers = Array.isArray(input.workerRows) ? input.workerRows : [];
|
|
18
32
|
const handoffLine = input.qaAppHandoffPending ? `QA app handoff pending · ${trimInline(input.qaAppHandoffArtifact || 'qa-loop/app-handoff.json', 64)}` : null;
|
|
19
33
|
if (!workers.length)
|
|
@@ -70,9 +84,35 @@ function renderTelemetryAnchor(snapshot, updateNotice = null, madDbCapability =
|
|
|
70
84
|
const update = updateNotice?.update_available && updateNotice?.latest_version ? ` · update ${trimInline(String(updateNotice.latest_version), 18)} available` : '';
|
|
71
85
|
const madDb = isMadDbActive(madDbCapability) ? ' · MAD-DB ACTIVE' : '';
|
|
72
86
|
const qaHandoff = ['pending', 'blocked_for_desktop_review'].includes(String(appHandoff?.status || '')) ? ' · QA /app handoff pending' : '';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
// Compact SKS branding header: package version + mission id so the SLOTS column self-identifies.
|
|
88
|
+
const brand = `SKS v${readPackageVersion()} · mission ${trimInline(String(snapshot.mission_id || '-'), 28)}`;
|
|
89
|
+
if (staleSeconds != null && staleSeconds > 60) {
|
|
90
|
+
return [brand, `SLOTS telemetry stale ${staleSeconds}s · active ?${update}${madDb}${qaHandoff}`].join('\n');
|
|
91
|
+
}
|
|
92
|
+
const countsLine = `SLOTS active ${active} · run ${Number(counts.running || 0)} · verify ${Number(counts.verifying || 0)} · headless ${Number(counts.headless || 0)} · done ${Number(counts.completed || 0)} · fail ${Number(counts.failed || 0)} · q ${Number(counts.queued || 0)}${update}${madDb}${qaHandoff}`;
|
|
93
|
+
const slotRows = renderTelemetrySlotRows(snapshot);
|
|
94
|
+
return [brand, countsLine, ...slotRows].join('\n');
|
|
95
|
+
}
|
|
96
|
+
function renderTelemetrySlotRows(snapshot) {
|
|
97
|
+
const slots = Object.values((snapshot?.slots || {}));
|
|
98
|
+
if (!slots.length)
|
|
99
|
+
return [];
|
|
100
|
+
const ordered = slots.sort((a, b) => {
|
|
101
|
+
const statusDelta = statusWeight(a?.status) - statusWeight(b?.status);
|
|
102
|
+
if (statusDelta)
|
|
103
|
+
return statusDelta;
|
|
104
|
+
return String(a?.slot_id || '').localeCompare(String(b?.slot_id || ''));
|
|
105
|
+
}).slice(0, 12);
|
|
106
|
+
return ordered.map((slot) => {
|
|
107
|
+
const id = `${trimInline(String(slot?.slot_id || 'slot-?'), 12)} g${Math.max(1, Math.floor(Number(slot?.generation_index) || 1))}`;
|
|
108
|
+
const task = trimInline(String(slot?.task_title || ''), 24);
|
|
109
|
+
const engine = workerBackendTag(slot?.backend, slot?.provider);
|
|
110
|
+
const role = trimInline(String(slot?.role || 'worker'), 10);
|
|
111
|
+
const status = trimInline(String(slot?.status || 'running'), 9);
|
|
112
|
+
const file = trimInline(String(slot?.current_file || '-'), 30);
|
|
113
|
+
const hb = slot?.latest_ts ? `${Math.max(0, Math.round((Date.now() - Date.parse(String(slot.latest_ts))) / 1000))}s` : '?';
|
|
114
|
+
return `${id}${task && task !== 'worker task' ? ` · ${task}` : ''} · ${engine} · ${role} · ${status} · ${file} · hb ${hb}`;
|
|
115
|
+
});
|
|
76
116
|
}
|
|
77
117
|
function isMadDbActive(capability) {
|
|
78
118
|
if (!capability || capability.enabled !== true || capability.consumed === true)
|
|
@@ -11,10 +11,12 @@ export function renderZellijSlotPane(input) {
|
|
|
11
11
|
? 'now'
|
|
12
12
|
: `${Math.max(1, Math.round(input.heartbeatAgeMs / 1000))}s ago`;
|
|
13
13
|
const files = firstNonEmptyList(input.changedFiles, input.patchFiles, input.plannedFiles, input.currentFile ? [input.currentFile] : []);
|
|
14
|
-
const events = (input.eventLines || []).filter(Boolean).slice(-
|
|
15
|
-
const stdout = (input.stdoutTail || []).filter(Boolean).slice(-
|
|
14
|
+
const events = (input.eventLines || []).filter(Boolean).slice(-10);
|
|
15
|
+
const stdout = (input.stdoutTail || []).filter(Boolean).slice(-6);
|
|
16
16
|
const stderr = (input.stderrTail || []).filter(Boolean).slice(-1);
|
|
17
|
+
const fixtureLoopProof = String(input.backend || '').includes('fixture') || String(input.patchStatus || '').includes('fixture');
|
|
17
18
|
const rows = [
|
|
19
|
+
input.loopId ? `${trimInline(input.loopId, 28)} · ${trimInline(input.loopRole || input.role || 'worker', 14)} · ${input.slotId}` : null,
|
|
18
20
|
`slot: ${input.slotId} / gen-${Math.max(1, Math.floor(Number(input.generationIndex) || 1))} / ${trimInline(input.status || 'running', 18)}`,
|
|
19
21
|
`role: ${trimInline(input.role || 'worker', 18)} backend: ${trimInline(input.backend || 'codex-sdk', 20)} worktree: ${trimInline(input.worktreeId || '-', 18)}`,
|
|
20
22
|
`runtime: fast ${formatFastMode(input.fastMode, input.serviceTier)} tier: ${trimInline(input.serviceTier || 'unknown', 12)} provider: ${trimInline(input.provider || 'unknown', 18)}`,
|
|
@@ -22,6 +24,8 @@ export function renderZellijSlotPane(input) {
|
|
|
22
24
|
input.sessionId ? `session: ${trimInline(input.sessionId, 62)}` : null,
|
|
23
25
|
`heartbeat: ${heartbeat}${input.heartbeatEvent ? ` event: ${trimInline(input.heartbeatEvent, 40)}` : ''}`,
|
|
24
26
|
`doing: ${task}`,
|
|
27
|
+
input.loopGate ? `gate: ${trimInline(input.loopGate, 68)}${input.verifyStatus ? ` · ${trimInline(input.verifyStatus, 18)}` : ''}` : null,
|
|
28
|
+
fixtureLoopProof ? 'fixture loop proof · not production execution' : null,
|
|
25
29
|
`files: ${trimInline(files.length ? files.join(', ') : 'no changed file yet', 78)}`,
|
|
26
30
|
`patch: ${trimInline(input.patchStatus || 'queued', 24)} verify: ${trimInline(input.verifyStatus || 'queued', 24)}`,
|
|
27
31
|
input.qaAppHandoffPending ? `QA app handoff pending: ${trimInline(input.qaAppHandoffArtifact || 'qa-loop/app-handoff.json', 55)}` : null,
|
|
@@ -89,7 +93,7 @@ async function renderZellijSlotPaneFromArtifactDir(input) {
|
|
|
89
93
|
path.join(artifactDir, 'python-codex-sdk-events.jsonl'),
|
|
90
94
|
path.join(artifactDir, 'local-llm-events.jsonl'),
|
|
91
95
|
path.join(artifactDir, 'zellij-worker-pane-events.jsonl')
|
|
92
|
-
],
|
|
96
|
+
], 10);
|
|
93
97
|
const patchFiles = patchPaths(patch || result);
|
|
94
98
|
const changedFiles = normalizeList(result?.changed_files);
|
|
95
99
|
const plannedFiles = normalizeList([
|
|
@@ -155,7 +159,7 @@ async function renderZellijSlotPaneFromArtifactDir(input) {
|
|
|
155
159
|
heartbeatEvent: heartbeatRows.length ? formatArtifactEvent(heartbeatRows[heartbeatRows.length - 1]) : null,
|
|
156
160
|
worktreeId: result?.worktree?.id || intake?.worktree?.id || null,
|
|
157
161
|
eventLines: eventRows.map(formatArtifactEvent).filter(Boolean),
|
|
158
|
-
stdoutTail: await readTextTailLines(path.join(artifactDir, 'worker.stdout.log'),
|
|
162
|
+
stdoutTail: await readTextTailLines(path.join(artifactDir, 'worker.stdout.log'), 6),
|
|
159
163
|
stderrTail: await readTextTailLines(path.join(artifactDir, 'worker.stderr.log'), 1),
|
|
160
164
|
qaAppHandoffPending: ['pending', 'blocked_for_desktop_review'].includes(String(qaAppHandoff?.status || '')),
|
|
161
165
|
qaAppHandoffArtifact: qaAppHandoff?.artifact_path || null,
|
|
@@ -190,6 +194,24 @@ export async function renderZellijSlotPaneStatusFromArtifacts(input) {
|
|
|
190
194
|
telemetry_age_ms: status.telemetry_age_ms
|
|
191
195
|
};
|
|
192
196
|
}
|
|
197
|
+
// Root-cause-3 fix: the watch loop never exited, so completed/failed panes lingered forever and
|
|
198
|
+
// kept re-reporting staleness. Resolve whether this slot has reached a terminal state (status is
|
|
199
|
+
// completed/failed/drained in telemetry) AND its worker-result.json exists, so the pane command
|
|
200
|
+
// can render one final frame and exit instead of looping indefinitely.
|
|
201
|
+
export async function resolveZellijSlotPaneExit(input) {
|
|
202
|
+
const resultExists = Boolean(await readJson(path.join(path.resolve(input.artifactDir), 'worker-result.json')));
|
|
203
|
+
if (!resultExists)
|
|
204
|
+
return false;
|
|
205
|
+
if (!input.missionId || input.missionId === 'latest')
|
|
206
|
+
return true;
|
|
207
|
+
const snapshot = await readZellijSlotTelemetrySnapshot(path.resolve(input.artifactRoot || input.artifactDir), input.missionId).catch(() => null);
|
|
208
|
+
if (!snapshot)
|
|
209
|
+
return true;
|
|
210
|
+
const slot = findTelemetrySlot(snapshot, input.slotId, input.generationIndex);
|
|
211
|
+
if (!slot)
|
|
212
|
+
return true;
|
|
213
|
+
return slot.status === 'completed' || slot.status === 'failed' || slot.status === 'drained';
|
|
214
|
+
}
|
|
193
215
|
export function buildZellijSlotPaneCommand(input) {
|
|
194
216
|
const args = [
|
|
195
217
|
input.cliPath,
|
|
@@ -257,7 +279,7 @@ function artifactFallbackRows(text) {
|
|
|
257
279
|
.map((line) => line.replace(/^\|\s?/, '').replace(/\s?\|$/, '').trim())
|
|
258
280
|
.filter((line) => /^(heartbeat|doing|files|event|out|err):\s+/i.test(line))
|
|
259
281
|
.filter((line) => !/unknown|waiting for worker intake|no changed file yet/i.test(line))
|
|
260
|
-
.slice(-
|
|
282
|
+
.slice(-12)
|
|
261
283
|
.map((line) => `live: ${trimInline(line, 72)}`);
|
|
262
284
|
}
|
|
263
285
|
function findTelemetrySlot(snapshot, slotId, generationIndex) {
|
|
@@ -273,16 +295,19 @@ function telemetryStatus(snapshot) {
|
|
|
273
295
|
const parsed = snapshot?.updated_at ? Date.parse(snapshot.updated_at) : NaN;
|
|
274
296
|
const telemetryAgeMs = Number.isFinite(parsed) ? Math.max(0, Date.now() - parsed) : Number.MAX_SAFE_INTEGER;
|
|
275
297
|
return {
|
|
276
|
-
|
|
298
|
+
// Root-cause-3 fix: with a 1000ms+jitter flush throttle the old 3000ms threshold flapped
|
|
299
|
+
// constantly. Raise to 15000ms so brief gaps between flushes don't read as "stale", and only
|
|
300
|
+
// claim the worker may be gone after 60000ms of true silence.
|
|
301
|
+
telemetry_stale: telemetryAgeMs > 15000,
|
|
277
302
|
telemetry_age_ms: telemetryAgeMs
|
|
278
303
|
};
|
|
279
304
|
}
|
|
280
305
|
function staleTelemetryRows(ageMs) {
|
|
281
306
|
if (!Number.isFinite(ageMs))
|
|
282
307
|
return ['telemetry stale; worker may still be running'];
|
|
283
|
-
if (ageMs >
|
|
308
|
+
if (ageMs > 60000)
|
|
284
309
|
return ['telemetry stale; worker may still be running'];
|
|
285
|
-
if (ageMs >
|
|
310
|
+
if (ageMs > 15000)
|
|
286
311
|
return [`telemetry stale ${(ageMs / 1000).toFixed(1)}s`];
|
|
287
312
|
return [];
|
|
288
313
|
}
|
|
@@ -392,15 +417,17 @@ function formatArtifactEvent(row) {
|
|
|
392
417
|
if (!row)
|
|
393
418
|
return '';
|
|
394
419
|
const status = trimInline(row.lane_status || row.status || row.event || row.event_type || row.type || row.sdk_event_type || 'event', 18);
|
|
420
|
+
// Prefer the latest LLM message text (message_tail/message) so the live pane shows what the
|
|
421
|
+
// model is actually saying, falling back to tool/file/blocker context when no message exists.
|
|
395
422
|
const detail = firstText([
|
|
423
|
+
row.message_tail,
|
|
424
|
+
row.message,
|
|
396
425
|
row.current_tool && row.current_file ? `tool ${row.current_tool} file ${row.current_file}` : null,
|
|
397
426
|
row.current_file ? `file ${row.current_file}` : null,
|
|
398
427
|
row.current_tool ? `tool ${row.current_tool}` : null,
|
|
399
|
-
row.message_tail,
|
|
400
428
|
row.blocker ? `blocker ${row.blocker}` : null,
|
|
401
429
|
row.request_id,
|
|
402
430
|
row.pane_id ? `pane ${row.pane_id}` : null,
|
|
403
|
-
row.message,
|
|
404
431
|
row.reason
|
|
405
432
|
]);
|
|
406
433
|
return trimInline(detail ? `${status}: ${detail}` : status, 96);
|