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
@@ -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) => {
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '3.1.0';
1
+ export const PACKAGE_VERSION = '3.1.2';
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
- rightColumnLocks.set(key, previous.then(() => current, () => current));
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) === current)
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));
@@ -73,9 +84,35 @@ function renderTelemetryAnchor(snapshot, updateNotice = null, madDbCapability =
73
84
  const update = updateNotice?.update_available && updateNotice?.latest_version ? ` · update ${trimInline(String(updateNotice.latest_version), 18)} available` : '';
74
85
  const madDb = isMadDbActive(madDbCapability) ? ' · MAD-DB ACTIVE' : '';
75
86
  const qaHandoff = ['pending', 'blocked_for_desktop_review'].includes(String(appHandoff?.status || '')) ? ' · QA /app handoff pending' : '';
76
- if (staleSeconds != null && staleSeconds > 10)
77
- return `SLOTS telemetry stale ${staleSeconds}s · active ?${update}${madDb}${qaHandoff}`;
78
- return `SLOTS active ${active} · headless ${Number(counts.headless || 0)} · done ${Number(counts.completed || 0)} · fail ${Number(counts.failed || 0)} · q ${Number(counts.queued || 0)}${update}${madDb}${qaHandoff}`;
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
+ });
79
116
  }
80
117
  function isMadDbActive(capability) {
81
118
  if (!capability || capability.enabled !== true || capability.consumed === true)
@@ -11,9 +11,10 @@ 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(-3);
15
- const stdout = (input.stdoutTail || []).filter(Boolean).slice(-2);
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 = [
18
19
  input.loopId ? `${trimInline(input.loopId, 28)} · ${trimInline(input.loopRole || input.role || 'worker', 14)} · ${input.slotId}` : null,
19
20
  `slot: ${input.slotId} / gen-${Math.max(1, Math.floor(Number(input.generationIndex) || 1))} / ${trimInline(input.status || 'running', 18)}`,
@@ -23,7 +24,8 @@ export function renderZellijSlotPane(input) {
23
24
  input.sessionId ? `session: ${trimInline(input.sessionId, 62)}` : null,
24
25
  `heartbeat: ${heartbeat}${input.heartbeatEvent ? ` event: ${trimInline(input.heartbeatEvent, 40)}` : ''}`,
25
26
  `doing: ${task}`,
26
- input.loopGate ? `gate: ${trimInline(input.loopGate, 68)}` : null,
27
+ input.loopGate ? `gate: ${trimInline(input.loopGate, 68)}${input.verifyStatus ? ` · ${trimInline(input.verifyStatus, 18)}` : ''}` : null,
28
+ fixtureLoopProof ? 'fixture loop proof · not production execution' : null,
27
29
  `files: ${trimInline(files.length ? files.join(', ') : 'no changed file yet', 78)}`,
28
30
  `patch: ${trimInline(input.patchStatus || 'queued', 24)} verify: ${trimInline(input.verifyStatus || 'queued', 24)}`,
29
31
  input.qaAppHandoffPending ? `QA app handoff pending: ${trimInline(input.qaAppHandoffArtifact || 'qa-loop/app-handoff.json', 55)}` : null,
@@ -91,7 +93,7 @@ async function renderZellijSlotPaneFromArtifactDir(input) {
91
93
  path.join(artifactDir, 'python-codex-sdk-events.jsonl'),
92
94
  path.join(artifactDir, 'local-llm-events.jsonl'),
93
95
  path.join(artifactDir, 'zellij-worker-pane-events.jsonl')
94
- ], 6);
96
+ ], 10);
95
97
  const patchFiles = patchPaths(patch || result);
96
98
  const changedFiles = normalizeList(result?.changed_files);
97
99
  const plannedFiles = normalizeList([
@@ -157,7 +159,7 @@ async function renderZellijSlotPaneFromArtifactDir(input) {
157
159
  heartbeatEvent: heartbeatRows.length ? formatArtifactEvent(heartbeatRows[heartbeatRows.length - 1]) : null,
158
160
  worktreeId: result?.worktree?.id || intake?.worktree?.id || null,
159
161
  eventLines: eventRows.map(formatArtifactEvent).filter(Boolean),
160
- stdoutTail: await readTextTailLines(path.join(artifactDir, 'worker.stdout.log'), 2),
162
+ stdoutTail: await readTextTailLines(path.join(artifactDir, 'worker.stdout.log'), 6),
161
163
  stderrTail: await readTextTailLines(path.join(artifactDir, 'worker.stderr.log'), 1),
162
164
  qaAppHandoffPending: ['pending', 'blocked_for_desktop_review'].includes(String(qaAppHandoff?.status || '')),
163
165
  qaAppHandoffArtifact: qaAppHandoff?.artifact_path || null,
@@ -192,6 +194,24 @@ export async function renderZellijSlotPaneStatusFromArtifacts(input) {
192
194
  telemetry_age_ms: status.telemetry_age_ms
193
195
  };
194
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
+ }
195
215
  export function buildZellijSlotPaneCommand(input) {
196
216
  const args = [
197
217
  input.cliPath,
@@ -259,7 +279,7 @@ function artifactFallbackRows(text) {
259
279
  .map((line) => line.replace(/^\|\s?/, '').replace(/\s?\|$/, '').trim())
260
280
  .filter((line) => /^(heartbeat|doing|files|event|out|err):\s+/i.test(line))
261
281
  .filter((line) => !/unknown|waiting for worker intake|no changed file yet/i.test(line))
262
- .slice(-7)
282
+ .slice(-12)
263
283
  .map((line) => `live: ${trimInline(line, 72)}`);
264
284
  }
265
285
  function findTelemetrySlot(snapshot, slotId, generationIndex) {
@@ -275,16 +295,19 @@ function telemetryStatus(snapshot) {
275
295
  const parsed = snapshot?.updated_at ? Date.parse(snapshot.updated_at) : NaN;
276
296
  const telemetryAgeMs = Number.isFinite(parsed) ? Math.max(0, Date.now() - parsed) : Number.MAX_SAFE_INTEGER;
277
297
  return {
278
- telemetry_stale: telemetryAgeMs > 3000,
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,
279
302
  telemetry_age_ms: telemetryAgeMs
280
303
  };
281
304
  }
282
305
  function staleTelemetryRows(ageMs) {
283
306
  if (!Number.isFinite(ageMs))
284
307
  return ['telemetry stale; worker may still be running'];
285
- if (ageMs > 10000)
308
+ if (ageMs > 60000)
286
309
  return ['telemetry stale; worker may still be running'];
287
- if (ageMs > 3000)
310
+ if (ageMs > 15000)
288
311
  return [`telemetry stale ${(ageMs / 1000).toFixed(1)}s`];
289
312
  return [];
290
313
  }
@@ -394,15 +417,17 @@ function formatArtifactEvent(row) {
394
417
  if (!row)
395
418
  return '';
396
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.
397
422
  const detail = firstText([
423
+ row.message_tail,
424
+ row.message,
398
425
  row.current_tool && row.current_file ? `tool ${row.current_tool} file ${row.current_file}` : null,
399
426
  row.current_file ? `file ${row.current_file}` : null,
400
427
  row.current_tool ? `tool ${row.current_tool}` : null,
401
- row.message_tail,
402
428
  row.blocker ? `blocker ${row.blocker}` : null,
403
429
  row.request_id,
404
430
  row.pane_id ? `pane ${row.pane_id}` : null,
405
- row.message,
406
431
  row.reason
407
432
  ]);
408
433
  return trimInline(detail ? `${status}: ${detail}` : status, 96);