@kognai/orchestrator-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +175 -0
- package/dist/lib/aar-middleware.d.ts +6 -0
- package/dist/lib/aar-middleware.js +70 -0
- package/dist/lib/aar-types.d.ts +34 -0
- package/dist/lib/aar-types.js +4 -0
- package/dist/lib/acp-engine.d.ts +68 -0
- package/dist/lib/acp-engine.js +123 -0
- package/dist/lib/acp.d.ts +61 -0
- package/dist/lib/acp.js +425 -0
- package/dist/lib/agent-registry.d.ts +50 -0
- package/dist/lib/agent-registry.js +137 -0
- package/dist/lib/anthropic-direct.d.ts +27 -0
- package/dist/lib/anthropic-direct.js +109 -0
- package/dist/lib/asmr-extractor.d.ts +40 -0
- package/dist/lib/asmr-extractor.js +151 -0
- package/dist/lib/asmr-retrieval.d.ts +76 -0
- package/dist/lib/asmr-retrieval.js +311 -0
- package/dist/lib/asmr.d.ts +8 -0
- package/dist/lib/asmr.js +24 -0
- package/dist/lib/brainx-client.d.ts +72 -0
- package/dist/lib/brainx-client.js +200 -0
- package/dist/lib/brainx-embed.d.ts +14 -0
- package/dist/lib/brainx-embed.js +139 -0
- package/dist/lib/brainx-swarm-bridge.d.ts +93 -0
- package/dist/lib/brainx-swarm-bridge.js +242 -0
- package/dist/lib/byterover-client.d.ts +19 -0
- package/dist/lib/byterover-client.js +59 -0
- package/dist/lib/ceo-wallet.d.ts +37 -0
- package/dist/lib/ceo-wallet.js +176 -0
- package/dist/lib/chomsky-gate.d.ts +24 -0
- package/dist/lib/chomsky-gate.js +178 -0
- package/dist/lib/chomsky-runner.d.ts +29 -0
- package/dist/lib/chomsky-runner.js +157 -0
- package/dist/lib/citizen-score-contract.d.ts +72 -0
- package/dist/lib/citizen-score-contract.js +16 -0
- package/dist/lib/citizen-score-registry.d.ts +25 -0
- package/dist/lib/citizen-score-registry.js +65 -0
- package/dist/lib/citizenship.d.ts +103 -0
- package/dist/lib/citizenship.js +272 -0
- package/dist/lib/clawrouter-client.d.ts +37 -0
- package/dist/lib/clawrouter-client.js +148 -0
- package/dist/lib/code-asset-crystalliser.d.ts +41 -0
- package/dist/lib/code-asset-crystalliser.js +181 -0
- package/dist/lib/code-failure-logger.d.ts +27 -0
- package/dist/lib/code-failure-logger.js +42 -0
- package/dist/lib/cto-approval-gate.d.ts +45 -0
- package/dist/lib/cto-approval-gate.js +478 -0
- package/dist/lib/cto-gate-types.d.ts +28 -0
- package/dist/lib/cto-gate-types.js +8 -0
- package/dist/lib/decomposer-feedback.d.ts +54 -0
- package/dist/lib/decomposer-feedback.js +115 -0
- package/dist/lib/emotional-safety-gate.d.ts +48 -0
- package/dist/lib/emotional-safety-gate.js +97 -0
- package/dist/lib/engine-paths.d.ts +13 -0
- package/dist/lib/engine-paths.js +32 -0
- package/dist/lib/event-bus-listener.d.ts +8 -0
- package/dist/lib/event-bus-listener.js +144 -0
- package/dist/lib/event-bus-publisher.d.ts +25 -0
- package/dist/lib/event-bus-publisher.js +188 -0
- package/dist/lib/event-bus-types.d.ts +73 -0
- package/dist/lib/event-bus-types.js +23 -0
- package/dist/lib/failure-library.d.ts +178 -0
- package/dist/lib/failure-library.js +349 -0
- package/dist/lib/ksl/error-log.d.ts +28 -0
- package/dist/lib/ksl/error-log.js +43 -0
- package/dist/lib/ksl/index.d.ts +9 -0
- package/dist/lib/ksl/index.js +25 -0
- package/dist/lib/ksl/orchestrator-tap.d.ts +16 -0
- package/dist/lib/ksl/orchestrator-tap.js +85 -0
- package/dist/lib/ksl/record-writer.d.ts +46 -0
- package/dist/lib/ksl/record-writer.js +45 -0
- package/dist/lib/llm-cost-table.d.ts +36 -0
- package/dist/lib/llm-cost-table.js +90 -0
- package/dist/lib/local-model-router.d.ts +27 -0
- package/dist/lib/local-model-router.js +61 -0
- package/dist/lib/mc-client.d.ts +51 -0
- package/dist/lib/mc-client.js +249 -0
- package/dist/lib/model-router-contract.d.ts +91 -0
- package/dist/lib/model-router-contract.js +19 -0
- package/dist/lib/model-router-registry.d.ts +24 -0
- package/dist/lib/model-router-registry.js +52 -0
- package/dist/lib/model-router.d.ts +20 -0
- package/dist/lib/model-router.js +79 -0
- package/dist/lib/monotask-state-machine.d.ts +19 -0
- package/dist/lib/monotask-state-machine.js +131 -0
- package/dist/lib/neutral-prompt-checker.d.ts +22 -0
- package/dist/lib/neutral-prompt-checker.js +130 -0
- package/dist/lib/notion-direct.d.ts +92 -0
- package/dist/lib/notion-direct.js +381 -0
- package/dist/lib/ollama-client.d.ts +37 -0
- package/dist/lib/ollama-client.js +158 -0
- package/dist/lib/omel/credential-vault.d.ts +57 -0
- package/dist/lib/omel/credential-vault.js +324 -0
- package/dist/lib/omel/human-brake.d.ts +32 -0
- package/dist/lib/omel/human-brake.js +289 -0
- package/dist/lib/omel/index.d.ts +10 -0
- package/dist/lib/omel/index.js +26 -0
- package/dist/lib/omel/phantom-workspace.d.ts +31 -0
- package/dist/lib/omel/phantom-workspace.js +256 -0
- package/dist/lib/omel/wipe-witness.d.ts +75 -0
- package/dist/lib/omel/wipe-witness.js +398 -0
- package/dist/lib/orchestrate-engine.d.ts +25 -0
- package/dist/lib/orchestrate-engine.js +4436 -0
- package/dist/lib/perm-judge.d.ts +46 -0
- package/dist/lib/perm-judge.js +173 -0
- package/dist/lib/plumber/conformance.d.ts +54 -0
- package/dist/lib/plumber/conformance.js +121 -0
- package/dist/lib/plumber/index.d.ts +9 -0
- package/dist/lib/plumber/index.js +25 -0
- package/dist/lib/plumber/observer.d.ts +52 -0
- package/dist/lib/plumber/observer.js +180 -0
- package/dist/lib/plumber/types.d.ts +78 -0
- package/dist/lib/plumber/types.js +29 -0
- package/dist/lib/research-impl-gate.d.ts +16 -0
- package/dist/lib/research-impl-gate.js +105 -0
- package/dist/lib/sherlock-memory.d.ts +29 -0
- package/dist/lib/sherlock-memory.js +105 -0
- package/dist/lib/skill-crystalliser.d.ts +44 -0
- package/dist/lib/skill-crystalliser.js +60 -0
- package/dist/lib/sprint-runner-engine.d.ts +27 -0
- package/dist/lib/sprint-runner-engine.js +1042 -0
- package/dist/lib/sprint-state.d.ts +71 -0
- package/dist/lib/sprint-state.js +202 -0
- package/dist/lib/stuck-handler.d.ts +17 -0
- package/dist/lib/stuck-handler.js +249 -0
- package/dist/lib/task-contract-checker.d.ts +17 -0
- package/dist/lib/task-contract-checker.js +29 -0
- package/dist/lib/task-router/index.d.ts +17 -0
- package/dist/lib/task-router/index.js +52 -0
- package/dist/lib/task-router/router/generate-execution-id.d.ts +10 -0
- package/dist/lib/task-router/router/generate-execution-id.js +24 -0
- package/dist/lib/task-router/router/resolve-route.d.ts +2 -0
- package/dist/lib/task-router/router/resolve-route.js +49 -0
- package/dist/lib/task-router/types.d.ts +79 -0
- package/dist/lib/task-router/types.js +39 -0
- package/dist/lib/token-budget-validator.d.ts +44 -0
- package/dist/lib/token-budget-validator.js +84 -0
- package/dist/lib/trust-score-updater.d.ts +30 -0
- package/dist/lib/trust-score-updater.js +107 -0
- package/dist/lib/wallet-state.d.ts +26 -0
- package/dist/lib/wallet-state.js +85 -0
- package/package.json +27 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Failure Library Consumer — TICKET-152 Gap 2.
|
|
4
|
+
*
|
|
5
|
+
* `code-failure-logger.ts` WRITES rejection records to
|
|
6
|
+
* `<root>/data/failure-library/code/fail-*.json`. Until now nothing READ them
|
|
7
|
+
* back, so a task could be rejected for the same reason on every attempt and the
|
|
8
|
+
* agent never saw its own history — e.g. `ksl_batch_runner` accumulated 75 failure
|
|
9
|
+
* records and still never converged. This module closes that loop.
|
|
10
|
+
*
|
|
11
|
+
* Three surfaces:
|
|
12
|
+
* 1. aggregateCodeFailures() — collapse the raw log into per-task summaries
|
|
13
|
+
* (how deep it stuck, what kind of failure, the
|
|
14
|
+
* recurring theme). The analytics view.
|
|
15
|
+
* 2. promoteLessons() — write a human/agent-readable LESSONS.md to the
|
|
16
|
+
* failure-library root. The promotion view; this
|
|
17
|
+
* is the `learnings_ref` decomposer-feedback points at.
|
|
18
|
+
* 3. retrieveTaskFailures() — retry-aware retrieval for ONE task: a compact
|
|
19
|
+
* "here is what your last N attempts were rejected
|
|
20
|
+
* for, do not repeat it" brief, ready to inject
|
|
21
|
+
* into the next attempt's prompt. The feedback view.
|
|
22
|
+
*
|
|
23
|
+
* Stdlib only: fs, path. Read failures degrade to empty results — a missing or
|
|
24
|
+
* corrupt library must never throw into a running sprint.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.DEFAULT_THRESHOLDS = void 0;
|
|
28
|
+
exports.projectCodeFailureToBase = projectCodeFailureToBase;
|
|
29
|
+
exports.readAllCodeFailures = readAllCodeFailures;
|
|
30
|
+
exports.failureSignature = failureSignature;
|
|
31
|
+
exports.aggregateCodeFailures = aggregateCodeFailures;
|
|
32
|
+
exports.retrieveTaskFailures = retrieveTaskFailures;
|
|
33
|
+
exports.retrieveAgentFailures = retrieveAgentFailures;
|
|
34
|
+
exports.renderLessons = renderLessons;
|
|
35
|
+
exports.promoteLessons = promoteLessons;
|
|
36
|
+
const fs_1 = require("fs");
|
|
37
|
+
const path_1 = require("path");
|
|
38
|
+
const engine_paths_1 = require("./engine-paths");
|
|
39
|
+
exports.DEFAULT_THRESHOLDS = {
|
|
40
|
+
recurring: 3,
|
|
41
|
+
noConvergeAttempt: 3,
|
|
42
|
+
noConvergeVolume: 10,
|
|
43
|
+
};
|
|
44
|
+
function codeFailureDir(root) {
|
|
45
|
+
return (0, path_1.join)((0, engine_paths_1.resolveEnginePaths)(root).failureLibrary, 'code');
|
|
46
|
+
}
|
|
47
|
+
/** Project a raw code-failure record onto FailureEntryBase (see contract conformance). */
|
|
48
|
+
function projectCodeFailureToBase(e) {
|
|
49
|
+
return {
|
|
50
|
+
entry_id: e.entry_id,
|
|
51
|
+
filed_at: e.filed_at,
|
|
52
|
+
subject_ref: e.task_id,
|
|
53
|
+
failure_class: e.fail_type,
|
|
54
|
+
score: typeof e.score === 'number' ? e.score : null,
|
|
55
|
+
filed_by: e.agent_id,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read every fail-*.json under data/failure-library/code/. Corrupt or partial
|
|
60
|
+
* files are skipped, never thrown — the writer and readers run concurrently, so a
|
|
61
|
+
* half-written file is expected and must not break a read.
|
|
62
|
+
*/
|
|
63
|
+
function readAllCodeFailures(root) {
|
|
64
|
+
const dir = codeFailureDir(root);
|
|
65
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
66
|
+
return [];
|
|
67
|
+
let names;
|
|
68
|
+
try {
|
|
69
|
+
names = (0, fs_1.readdirSync)(dir).filter((n) => n.startsWith('fail-') && n.endsWith('.json'));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const name of names) {
|
|
76
|
+
try {
|
|
77
|
+
const raw = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(dir, name), 'utf8'));
|
|
78
|
+
if (raw && typeof raw.task_id === 'string')
|
|
79
|
+
out.push(raw);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Skip unreadable/half-written entry.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Reduce a rejection to a coarse, deterministic signature so recurring themes
|
|
89
|
+
* surface across attempts. No LLM — keyword heuristics over the reason + issues.
|
|
90
|
+
*/
|
|
91
|
+
function failureSignature(entry) {
|
|
92
|
+
const text = `${entry.rejection_reason || ''} ${(entry.issues || [])
|
|
93
|
+
.map((i) => i.description)
|
|
94
|
+
.join(' ')}`.toLowerCase();
|
|
95
|
+
if (/truncat/.test(text))
|
|
96
|
+
return 'truncation';
|
|
97
|
+
if (/destructive|wholesale rewrite|deleted (the|existing)/.test(text))
|
|
98
|
+
return 'destructive rewrite';
|
|
99
|
+
if (/typecheck failed|tsc|ts\d{3,5}|is not assignable|does not exist on type/.test(text))
|
|
100
|
+
return 'typecheck / compile error';
|
|
101
|
+
if (/unused import|no-unused-vars|ts6133|imported but never/.test(text))
|
|
102
|
+
return 'unused / dangling import';
|
|
103
|
+
if (/partial|half-applied|skipped entirely|edit \(2\)|never rendered|not performed/.test(text))
|
|
104
|
+
return 'partial / incomplete edit';
|
|
105
|
+
if (/no files|empty diff|nothing changed/.test(text))
|
|
106
|
+
return 'no files written';
|
|
107
|
+
if (/meta-?commentary|self-justifying|comment claiming|refusal/.test(text))
|
|
108
|
+
return 'commentary instead of implementation';
|
|
109
|
+
if (/spec non-compliance|task spec|did not follow|out of scope/.test(text))
|
|
110
|
+
return 'spec non-compliance';
|
|
111
|
+
return entry.fail_type || 'unknown';
|
|
112
|
+
}
|
|
113
|
+
function bump(map, key) {
|
|
114
|
+
map[key] = (map[key] || 0) + 1;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Collapse the raw log into one summary per task_id, sorted most-failed first.
|
|
118
|
+
*/
|
|
119
|
+
function aggregateCodeFailures(root, thresholds = exports.DEFAULT_THRESHOLDS) {
|
|
120
|
+
const byTask = new Map();
|
|
121
|
+
for (const e of readAllCodeFailures(root)) {
|
|
122
|
+
const list = byTask.get(e.task_id) || [];
|
|
123
|
+
list.push(e);
|
|
124
|
+
byTask.set(e.task_id, list);
|
|
125
|
+
}
|
|
126
|
+
const summaries = [];
|
|
127
|
+
for (const [taskId, entries] of byTask) {
|
|
128
|
+
entries.sort((a, b) => String(a.filed_at).localeCompare(String(b.filed_at)));
|
|
129
|
+
const latest = entries[entries.length - 1];
|
|
130
|
+
const fail_types = {};
|
|
131
|
+
const models = {};
|
|
132
|
+
const signatures = {};
|
|
133
|
+
const sprints = new Set();
|
|
134
|
+
let maxAttempt = 0;
|
|
135
|
+
for (const e of entries) {
|
|
136
|
+
bump(fail_types, e.fail_type || 'unknown');
|
|
137
|
+
bump(models, e.model || 'unknown');
|
|
138
|
+
bump(signatures, failureSignature(e));
|
|
139
|
+
if (e.sprint_id)
|
|
140
|
+
sprints.add(e.sprint_id);
|
|
141
|
+
if (typeof e.attempt_num === 'number' && e.attempt_num > maxAttempt)
|
|
142
|
+
maxAttempt = e.attempt_num;
|
|
143
|
+
}
|
|
144
|
+
summaries.push({
|
|
145
|
+
task_id: taskId,
|
|
146
|
+
total_failures: entries.length,
|
|
147
|
+
max_attempt: maxAttempt,
|
|
148
|
+
fail_types,
|
|
149
|
+
models,
|
|
150
|
+
signatures,
|
|
151
|
+
sprint_ids: [...sprints],
|
|
152
|
+
latest_score: latest.score ?? 0,
|
|
153
|
+
latest_reason: latest.rejection_reason || '',
|
|
154
|
+
latest_filed_at: latest.filed_at || '',
|
|
155
|
+
recurring: entries.length >= thresholds.recurring,
|
|
156
|
+
no_converge: maxAttempt >= thresholds.noConvergeAttempt || entries.length >= thresholds.noConvergeVolume,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
summaries.sort((a, b) => b.total_failures - a.total_failures || b.max_attempt - a.max_attempt);
|
|
160
|
+
return summaries;
|
|
161
|
+
}
|
|
162
|
+
function truncate(s, n) {
|
|
163
|
+
if (!s)
|
|
164
|
+
return '';
|
|
165
|
+
const clean = s.replace(/\s+/g, ' ').trim();
|
|
166
|
+
return clean.length <= n ? clean : `${clean.slice(0, n - 1)}…`;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Retrieve everything the library knows about ONE task and synthesize a compact
|
|
170
|
+
* avoidance brief. This is the piece that was missing: call it before re-running a
|
|
171
|
+
* rejected task and prepend `.brief` to the agent's prompt so attempt N+1 can see
|
|
172
|
+
* why attempts 1..N were rejected.
|
|
173
|
+
*/
|
|
174
|
+
function retrieveTaskFailures(taskId, opts = {}) {
|
|
175
|
+
const maxAttempts = opts.maxAttempts ?? 5;
|
|
176
|
+
const maxReasonChars = opts.maxReasonChars ?? 240;
|
|
177
|
+
const maxIssues = opts.maxIssuesPerAttempt ?? 3;
|
|
178
|
+
const all = readAllCodeFailures(opts.root)
|
|
179
|
+
.filter((e) => e.task_id === taskId)
|
|
180
|
+
.sort((a, b) => String(a.filed_at).localeCompare(String(b.filed_at)));
|
|
181
|
+
if (all.length === 0) {
|
|
182
|
+
return { task_id: taskId, prior_attempts: 0, entries: [], themes: [], brief: '' };
|
|
183
|
+
}
|
|
184
|
+
// Theme ranking across the FULL history (not just the windowed entries).
|
|
185
|
+
const sigCounts = {};
|
|
186
|
+
for (const e of all)
|
|
187
|
+
bump(sigCounts, failureSignature(e));
|
|
188
|
+
const themes = Object.entries(sigCounts)
|
|
189
|
+
.sort((a, b) => b[1] - a[1])
|
|
190
|
+
.map(([sig]) => sig);
|
|
191
|
+
// Most-recent window for the per-attempt detail.
|
|
192
|
+
const window = all.slice(-maxAttempts);
|
|
193
|
+
const lines = [];
|
|
194
|
+
lines.push(`⚠️ PRIOR FAILURES for "${taskId}" — ${all.length} rejected attempt(s) on record. ` +
|
|
195
|
+
`Do NOT repeat these mistakes:`);
|
|
196
|
+
for (const e of window) {
|
|
197
|
+
const score = typeof e.score === 'number' ? `${e.score}/100` : 'n/a';
|
|
198
|
+
lines.push(` • attempt ${e.attempt_num} (${e.fail_type}, ${score}): ${truncate(e.rejection_reason, maxReasonChars)}`);
|
|
199
|
+
const issues = (e.issues || []).filter((i) => i && i.description).slice(0, maxIssues);
|
|
200
|
+
for (const i of issues) {
|
|
201
|
+
lines.push(` - ${i.severity}: ${truncate(`${i.file ? i.file + ' — ' : ''}${i.description}`, maxReasonChars)}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (all.length > window.length) {
|
|
205
|
+
lines.push(` • (…${all.length - window.length} earlier attempt(s) omitted)`);
|
|
206
|
+
}
|
|
207
|
+
lines.push(`Recurring theme${themes.length > 1 ? 's' : ''}: ${themes.slice(0, 3).join('; ')}.`);
|
|
208
|
+
return {
|
|
209
|
+
task_id: taskId,
|
|
210
|
+
prior_attempts: all.length,
|
|
211
|
+
entries: window,
|
|
212
|
+
themes,
|
|
213
|
+
brief: lines.join('\n'),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Match an entry's stored agent id against a queried id, tolerating the Gap-1
|
|
218
|
+
* transition: new entries carry `did:kognai:coder`, historical ones the bare
|
|
219
|
+
* role `coder`. A query for either form matches both.
|
|
220
|
+
*/
|
|
221
|
+
function agentMatches(entryAgentId, queryAgentId) {
|
|
222
|
+
if (!entryAgentId)
|
|
223
|
+
return false;
|
|
224
|
+
if (entryAgentId === queryAgentId)
|
|
225
|
+
return true;
|
|
226
|
+
const strip = (s) => s.replace(/^did:kognai:(?:[^:]+:)?/, '');
|
|
227
|
+
return strip(entryAgentId) === strip(queryAgentId);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* "Failures attributed to agent_id X over the last N days." Powers SOP-GEN-013
|
|
231
|
+
* Improve-mode and the per-agent AIC curve in TICKET-110. `nowISO` is injected
|
|
232
|
+
* (callers pass new Date().toISOString()) to keep this deterministic/testable.
|
|
233
|
+
*/
|
|
234
|
+
function retrieveAgentFailures(agentId, nowISO, opts = {}) {
|
|
235
|
+
const sinceDays = opts.sinceDays ?? 14;
|
|
236
|
+
const maxEntries = opts.maxEntries ?? 20;
|
|
237
|
+
const cutoff = Date.parse(nowISO) - sinceDays * 86_400_000;
|
|
238
|
+
const matched = readAllCodeFailures(opts.root)
|
|
239
|
+
.filter((e) => agentMatches(e.agent_id, agentId))
|
|
240
|
+
.filter((e) => {
|
|
241
|
+
const t = Date.parse(e.filed_at);
|
|
242
|
+
return Number.isNaN(t) ? true : t >= cutoff; // keep undated rather than drop
|
|
243
|
+
})
|
|
244
|
+
.sort((a, b) => String(b.filed_at).localeCompare(String(a.filed_at)));
|
|
245
|
+
const by_signature = {};
|
|
246
|
+
const taskCounts = {};
|
|
247
|
+
for (const e of matched) {
|
|
248
|
+
bump(by_signature, failureSignature(e));
|
|
249
|
+
bump(taskCounts, e.task_id);
|
|
250
|
+
}
|
|
251
|
+
const by_task = Object.entries(taskCounts)
|
|
252
|
+
.sort((a, b) => b[1] - a[1])
|
|
253
|
+
.map(([task_id, count]) => ({ task_id, count }));
|
|
254
|
+
const weakest = Object.entries(by_signature).sort((a, b) => b[1] - a[1])[0];
|
|
255
|
+
const lines = [];
|
|
256
|
+
if (matched.length === 0) {
|
|
257
|
+
lines.push(`No failures attributed to "${agentId}" in the last ${sinceDays} day(s).`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
lines.push(`📉 IMPROVE-MODE: "${agentId}" — ${matched.length} failure(s) in the last ${sinceDays} day(s).`);
|
|
261
|
+
lines.push(`Weakest area: ${weakest[0]} (${weakest[1]}). Recurring across:`);
|
|
262
|
+
for (const t of by_task.slice(0, 8))
|
|
263
|
+
lines.push(` • ${t.task_id} ×${t.count}`);
|
|
264
|
+
const themeList = Object.entries(by_signature)
|
|
265
|
+
.sort((a, b) => b[1] - a[1])
|
|
266
|
+
.map(([s, n]) => `${s} (${n})`)
|
|
267
|
+
.join(', ');
|
|
268
|
+
lines.push(`Themes: ${themeList}.`);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
agent_id: agentId,
|
|
272
|
+
window_days: sinceDays,
|
|
273
|
+
total: matched.length,
|
|
274
|
+
by_signature,
|
|
275
|
+
by_task,
|
|
276
|
+
weakest_signature: weakest ? weakest[0] : '',
|
|
277
|
+
entries: matched.slice(0, maxEntries),
|
|
278
|
+
brief: lines.join('\n'),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function topEntries(map, n = 3) {
|
|
282
|
+
return Object.entries(map)
|
|
283
|
+
.sort((a, b) => b[1] - a[1])
|
|
284
|
+
.slice(0, n)
|
|
285
|
+
.map(([k, v]) => `${k} (${v})`)
|
|
286
|
+
.join(', ');
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Render LESSONS.md content from the aggregate. Pure (no I/O) so it's testable.
|
|
290
|
+
*/
|
|
291
|
+
function renderLessons(summaries, generatedAt) {
|
|
292
|
+
const promoted = summaries.filter((s) => s.recurring || s.no_converge);
|
|
293
|
+
const lines = [];
|
|
294
|
+
lines.push('# Failure Library — LESSONS');
|
|
295
|
+
lines.push('');
|
|
296
|
+
lines.push('> Auto-generated by `failure-library.ts` (TICKET-152 Gap 2) from ' +
|
|
297
|
+
'`data/failure-library/code/`. Do not edit by hand — re-run the promoter.');
|
|
298
|
+
lines.push(`> Generated: ${generatedAt} · ${summaries.length} task(s) with failures · ${promoted.length} promoted.`);
|
|
299
|
+
lines.push('');
|
|
300
|
+
if (promoted.length === 0) {
|
|
301
|
+
lines.push('_No recurring or non-converging tasks above threshold._');
|
|
302
|
+
lines.push('');
|
|
303
|
+
return lines.join('\n');
|
|
304
|
+
}
|
|
305
|
+
lines.push('## Chronic tasks (recurring or never converged)');
|
|
306
|
+
lines.push('');
|
|
307
|
+
lines.push('| Task | Fails | Max attempt | Converged? | Recurring theme |');
|
|
308
|
+
lines.push('|------|------:|------------:|:----------:|-----------------|');
|
|
309
|
+
for (const s of promoted) {
|
|
310
|
+
const theme = topEntries(s.signatures, 1) || '—';
|
|
311
|
+
const converged = s.no_converge ? '❌' : '—';
|
|
312
|
+
lines.push(`| \`${s.task_id}\` | ${s.total_failures} | ${s.max_attempt} | ${converged} | ${theme} |`);
|
|
313
|
+
}
|
|
314
|
+
lines.push('');
|
|
315
|
+
lines.push('## Details');
|
|
316
|
+
lines.push('');
|
|
317
|
+
for (const s of promoted) {
|
|
318
|
+
lines.push(`### \`${s.task_id}\``);
|
|
319
|
+
lines.push('');
|
|
320
|
+
lines.push(`- Total failures: **${s.total_failures}** · max attempt: **${s.max_attempt}**`);
|
|
321
|
+
lines.push(`- Fail types: ${topEntries(s.fail_types)}`);
|
|
322
|
+
lines.push(`- Themes: ${topEntries(s.signatures)}`);
|
|
323
|
+
lines.push(`- Models: ${topEntries(s.models)}`);
|
|
324
|
+
lines.push(`- Sprints: ${s.sprint_ids.join(', ') || 'n/a'}`);
|
|
325
|
+
lines.push(`- Latest (${s.latest_filed_at}, ${s.latest_score}/100): ${truncate(s.latest_reason, 280)}`);
|
|
326
|
+
lines.push('');
|
|
327
|
+
}
|
|
328
|
+
return lines.join('\n');
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Build the aggregate and write LESSONS.md to the failure-library root.
|
|
332
|
+
* `generatedAt` is injected (callers pass new Date().toISOString()) so this stays
|
|
333
|
+
* deterministic and testable.
|
|
334
|
+
*/
|
|
335
|
+
function promoteLessons(generatedAt, opts = {}) {
|
|
336
|
+
const thresholds = opts.thresholds ?? exports.DEFAULT_THRESHOLDS;
|
|
337
|
+
const summaries = aggregateCodeFailures(opts.root, thresholds);
|
|
338
|
+
const content = renderLessons(summaries, generatedAt);
|
|
339
|
+
const dir = (0, engine_paths_1.resolveEnginePaths)(opts.root).failureLibrary;
|
|
340
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
341
|
+
const path = (0, path_1.join)(dir, 'LESSONS.md');
|
|
342
|
+
(0, fs_1.writeFileSync)(path, content);
|
|
343
|
+
return {
|
|
344
|
+
path,
|
|
345
|
+
promoted: summaries.filter((s) => s.recurring || s.no_converge).length,
|
|
346
|
+
totalTasks: summaries.length,
|
|
347
|
+
content,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KSL structured error log — appends typed error entries to
|
|
3
|
+
* <root>/workspace/ksl/errors/<month>.jsonl. Capture is best-effort and
|
|
4
|
+
* silent on disk failure so it can never break sprint execution.
|
|
5
|
+
*
|
|
6
|
+
* TICKET-215 Phase 3b-3 (Wave B): ERROR_ROOT now resolves via engine-paths
|
|
7
|
+
* (KOGNAI_ROOT / cwd) instead of process.cwd() directly, so the KSL capture
|
|
8
|
+
* layer is location-independent and ships in @kognai/orchestrator-core.
|
|
9
|
+
* resolveEnginePaths().root === the old cwd when KOGNAI_ROOT is unset.
|
|
10
|
+
*/
|
|
11
|
+
export type ErrorKind = 'llm_error' | 'tool_failure' | 'budget_exceeded' | 'guardrail_triggered' | 'timeout' | 'agent_disagreement' | 'compile_error' | 'test_failure' | 'cerberus_block' | 'unknown';
|
|
12
|
+
export interface ErrorContext {
|
|
13
|
+
sprint_id: string;
|
|
14
|
+
task_id: string;
|
|
15
|
+
attempt: number;
|
|
16
|
+
agent: string;
|
|
17
|
+
recovered?: boolean;
|
|
18
|
+
retry_count?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ErrorLogEntry extends ErrorContext {
|
|
21
|
+
ts: string;
|
|
22
|
+
kind: ErrorKind;
|
|
23
|
+
message: string;
|
|
24
|
+
stack?: string;
|
|
25
|
+
recovered: boolean;
|
|
26
|
+
retry_count: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function record(err: unknown, ctx: ErrorContext, kind?: ErrorKind): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* KSL structured error log — appends typed error entries to
|
|
4
|
+
* <root>/workspace/ksl/errors/<month>.jsonl. Capture is best-effort and
|
|
5
|
+
* silent on disk failure so it can never break sprint execution.
|
|
6
|
+
*
|
|
7
|
+
* TICKET-215 Phase 3b-3 (Wave B): ERROR_ROOT now resolves via engine-paths
|
|
8
|
+
* (KOGNAI_ROOT / cwd) instead of process.cwd() directly, so the KSL capture
|
|
9
|
+
* layer is location-independent and ships in @kognai/orchestrator-core.
|
|
10
|
+
* resolveEnginePaths().root === the old cwd when KOGNAI_ROOT is unset.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.record = record;
|
|
14
|
+
const fs_1 = require("fs");
|
|
15
|
+
const path_1 = require("path");
|
|
16
|
+
const engine_paths_1 = require("../engine-paths");
|
|
17
|
+
const ERROR_ROOT = (0, path_1.join)((0, engine_paths_1.resolveEnginePaths)().root, 'workspace/ksl/errors');
|
|
18
|
+
function record(err, ctx, kind = 'unknown') {
|
|
19
|
+
try {
|
|
20
|
+
const ts = new Date().toISOString();
|
|
21
|
+
const month = ts.slice(0, 7);
|
|
22
|
+
(0, fs_1.mkdirSync)(ERROR_ROOT, { recursive: true });
|
|
23
|
+
const e = err;
|
|
24
|
+
const message = (e && e.message) ? String(e.message).slice(0, 1000) : String(err).slice(0, 1000);
|
|
25
|
+
const stack = (e && e.stack) ? String(e.stack).slice(0, 4000) : undefined;
|
|
26
|
+
const entry = {
|
|
27
|
+
ts,
|
|
28
|
+
sprint_id: ctx.sprint_id,
|
|
29
|
+
task_id: ctx.task_id,
|
|
30
|
+
attempt: ctx.attempt,
|
|
31
|
+
agent: ctx.agent,
|
|
32
|
+
kind,
|
|
33
|
+
message,
|
|
34
|
+
...(stack ? { stack } : {}),
|
|
35
|
+
recovered: ctx.recovered ?? false,
|
|
36
|
+
retry_count: ctx.retry_count ?? 0,
|
|
37
|
+
};
|
|
38
|
+
(0, fs_1.appendFileSync)((0, path_1.join)(ERROR_ROOT, `${month}.jsonl`), JSON.stringify(entry) + '\n');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Silent on disk failure.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KSL (Knowledge/Session Ledger) capture cluster — Phase 3b-3 Wave B.
|
|
3
|
+
* Structured per-attempt session records + typed error log + the tap that
|
|
4
|
+
* the orchestrator calls on every agent attempt. Best-effort, kill-switchable
|
|
5
|
+
* (SPRINT_NO_KSL_CAPTURE=1), never breaks sprint execution.
|
|
6
|
+
*/
|
|
7
|
+
export * from './record-writer';
|
|
8
|
+
export * from './error-log';
|
|
9
|
+
export * from './orchestrator-tap';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* KSL (Knowledge/Session Ledger) capture cluster — Phase 3b-3 Wave B.
|
|
19
|
+
* Structured per-attempt session records + typed error log + the tap that
|
|
20
|
+
* the orchestrator calls on every agent attempt. Best-effort, kill-switchable
|
|
21
|
+
* (SPRINT_NO_KSL_CAPTURE=1), never breaks sprint execution.
|
|
22
|
+
*/
|
|
23
|
+
__exportStar(require("./record-writer"), exports);
|
|
24
|
+
__exportStar(require("./error-log"), exports);
|
|
25
|
+
__exportStar(require("./orchestrator-tap"), exports);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type KSLError, type KSLToolUse } from './record-writer';
|
|
2
|
+
export interface TapInput {
|
|
3
|
+
sprint_id: string;
|
|
4
|
+
task_id: string;
|
|
5
|
+
attempt: number;
|
|
6
|
+
agent: string;
|
|
7
|
+
model: string;
|
|
8
|
+
prompt: string;
|
|
9
|
+
reply: string;
|
|
10
|
+
tools_used?: KSLToolUse[];
|
|
11
|
+
errors?: KSLError[];
|
|
12
|
+
cost_usd?: number;
|
|
13
|
+
duration_ms: number;
|
|
14
|
+
signal_strength?: 'low' | 'medium' | 'high';
|
|
15
|
+
}
|
|
16
|
+
export declare function tapAttempt(input: TapInput): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.tapAttempt = tapAttempt;
|
|
37
|
+
const record_writer_1 = require("./record-writer");
|
|
38
|
+
const errorLog = __importStar(require("./error-log"));
|
|
39
|
+
const KILL = () => process.env.SPRINT_NO_KSL_CAPTURE === '1';
|
|
40
|
+
function pickSignal(input) {
|
|
41
|
+
if (input.signal_strength)
|
|
42
|
+
return input.signal_strength;
|
|
43
|
+
// Heuristic: long reply + no errors → high; errors → low; else medium.
|
|
44
|
+
if ((input.errors?.length ?? 0) > 0)
|
|
45
|
+
return 'low';
|
|
46
|
+
if (input.reply && input.reply.length > 4000)
|
|
47
|
+
return 'high';
|
|
48
|
+
return 'medium';
|
|
49
|
+
}
|
|
50
|
+
function tapAttempt(input) {
|
|
51
|
+
if (KILL())
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
const record = {
|
|
55
|
+
run_id: (0, record_writer_1.makeRunId)(input.sprint_id, input.task_id, input.attempt),
|
|
56
|
+
sprint_id: input.sprint_id,
|
|
57
|
+
task_id: input.task_id,
|
|
58
|
+
attempt: input.attempt,
|
|
59
|
+
agent: input.agent,
|
|
60
|
+
candidate_id: input.model || 'unknown',
|
|
61
|
+
resolution: null,
|
|
62
|
+
constitutional_score: 0,
|
|
63
|
+
pact_score: null,
|
|
64
|
+
signal_strength: pickSignal(input),
|
|
65
|
+
prompt: input.prompt || '',
|
|
66
|
+
reply: input.reply || '',
|
|
67
|
+
tools_used: input.tools_used || [],
|
|
68
|
+
errors: input.errors || [],
|
|
69
|
+
cost_usd: input.cost_usd ?? 0,
|
|
70
|
+
duration_ms: input.duration_ms,
|
|
71
|
+
justification: null,
|
|
72
|
+
judged_at: null,
|
|
73
|
+
ts: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
(0, record_writer_1.writeRecord)(record);
|
|
76
|
+
// Mirror errors into the structured error log.
|
|
77
|
+
for (const e of record.errors) {
|
|
78
|
+
const kind = e.kind && e.kind.length > 0 ? e.kind : 'unknown';
|
|
79
|
+
errorLog.record(new Error(e.message), { sprint_id: input.sprint_id, task_id: input.task_id, attempt: input.attempt, agent: input.agent }, kind);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Capture must not break sprint execution. Swallow.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KSL session record writer — persists one JSON record per task attempt to
|
|
3
|
+
* <root>/workspace/ksl/sessions/<sprint>/<task>.<attempt>.json (atomic,
|
|
4
|
+
* idempotent by run_id).
|
|
5
|
+
*
|
|
6
|
+
* TICKET-215 Phase 3b-3 (Wave B): KSL_ROOT now resolves via engine-paths
|
|
7
|
+
* (KOGNAI_ROOT / cwd) instead of process.cwd() directly, so KSL capture is
|
|
8
|
+
* location-independent and ships in @kognai/orchestrator-core.
|
|
9
|
+
* resolveEnginePaths().root === the old cwd when KOGNAI_ROOT is unset.
|
|
10
|
+
*/
|
|
11
|
+
export type Resolution = 'A' | 'B' | 'C';
|
|
12
|
+
export interface KSLToolUse {
|
|
13
|
+
skill_id: string;
|
|
14
|
+
args_hash: string;
|
|
15
|
+
ok: boolean;
|
|
16
|
+
ms: number;
|
|
17
|
+
}
|
|
18
|
+
export interface KSLError {
|
|
19
|
+
kind: string;
|
|
20
|
+
message: string;
|
|
21
|
+
stack?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface KSLRecord {
|
|
24
|
+
run_id: string;
|
|
25
|
+
sprint_id: string;
|
|
26
|
+
task_id: string;
|
|
27
|
+
attempt: number;
|
|
28
|
+
agent: string;
|
|
29
|
+
candidate_id: string;
|
|
30
|
+
resolution: Resolution | null;
|
|
31
|
+
constitutional_score: number;
|
|
32
|
+
pact_score: number | null;
|
|
33
|
+
signal_strength: 'low' | 'medium' | 'high';
|
|
34
|
+
prompt: string;
|
|
35
|
+
reply: string;
|
|
36
|
+
tools_used: KSLToolUse[];
|
|
37
|
+
errors: KSLError[];
|
|
38
|
+
cost_usd: number;
|
|
39
|
+
duration_ms: number;
|
|
40
|
+
justification: string | null;
|
|
41
|
+
judged_at: string | null;
|
|
42
|
+
ts: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function pathForRecord(r: Pick<KSLRecord, 'sprint_id' | 'task_id' | 'attempt'>): string;
|
|
45
|
+
export declare function writeRecord(r: KSLRecord): string;
|
|
46
|
+
export declare function makeRunId(sprintId: string, taskId: string, attempt: number): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* KSL session record writer — persists one JSON record per task attempt to
|
|
4
|
+
* <root>/workspace/ksl/sessions/<sprint>/<task>.<attempt>.json (atomic,
|
|
5
|
+
* idempotent by run_id).
|
|
6
|
+
*
|
|
7
|
+
* TICKET-215 Phase 3b-3 (Wave B): KSL_ROOT now resolves via engine-paths
|
|
8
|
+
* (KOGNAI_ROOT / cwd) instead of process.cwd() directly, so KSL capture is
|
|
9
|
+
* location-independent and ships in @kognai/orchestrator-core.
|
|
10
|
+
* resolveEnginePaths().root === the old cwd when KOGNAI_ROOT is unset.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.pathForRecord = pathForRecord;
|
|
14
|
+
exports.writeRecord = writeRecord;
|
|
15
|
+
exports.makeRunId = makeRunId;
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const path_1 = require("path");
|
|
18
|
+
const crypto_1 = require("crypto");
|
|
19
|
+
const engine_paths_1 = require("../engine-paths");
|
|
20
|
+
const KSL_ROOT = (0, path_1.join)((0, engine_paths_1.resolveEnginePaths)().root, 'workspace/ksl/sessions');
|
|
21
|
+
function pathForRecord(r) {
|
|
22
|
+
return (0, path_1.join)(KSL_ROOT, r.sprint_id, `${r.task_id}.${r.attempt}.json`);
|
|
23
|
+
}
|
|
24
|
+
function writeRecord(r) {
|
|
25
|
+
if (!r.run_id || !r.sprint_id || !r.task_id || typeof r.attempt !== 'number') {
|
|
26
|
+
throw new Error('writeRecord: missing required fields run_id|sprint_id|task_id|attempt');
|
|
27
|
+
}
|
|
28
|
+
const out = pathForRecord(r);
|
|
29
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(out), { recursive: true });
|
|
30
|
+
if ((0, fs_1.existsSync)(out)) {
|
|
31
|
+
try {
|
|
32
|
+
const existing = JSON.parse((0, fs_1.readFileSync)(out, 'utf-8'));
|
|
33
|
+
if (existing.run_id === r.run_id)
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
catch { /* corrupt — overwrite */ }
|
|
37
|
+
}
|
|
38
|
+
const tmp = `${out}.${(0, crypto_1.randomBytes)(6).toString('hex')}.tmp`;
|
|
39
|
+
(0, fs_1.writeFileSync)(tmp, JSON.stringify(r, null, 2));
|
|
40
|
+
(0, fs_1.renameSync)(tmp, out);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function makeRunId(sprintId, taskId, attempt) {
|
|
44
|
+
return `${sprintId}:${taskId}:attempt-${attempt}`;
|
|
45
|
+
}
|