agent-threader 2.0.5
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/LICENSE +21 -0
- package/README.md +126 -0
- package/compiled/claude/agent-threader/SKILL.md +361 -0
- package/compiled/codex/agent-threader/SKILL.md +361 -0
- package/compiled/cursor/rules/agent-threader.mdc +367 -0
- package/compiled/cursor/skills/agent-threader/SKILL.md +361 -0
- package/compiled/opencode/agent-threader.md +361 -0
- package/compiled/windsurf/rules/agent-threader.md +361 -0
- package/compiled/windsurf/skills/agent-threader/SKILL.md +361 -0
- package/dist/cli/commands/doctor.d.ts +6 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +7 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/explain-error.d.ts +12 -0
- package/dist/cli/commands/explain-error.d.ts.map +1 -0
- package/dist/cli/commands/explain-error.js +23 -0
- package/dist/cli/commands/explain-error.js.map +1 -0
- package/dist/cli/commands/init-state.d.ts +6 -0
- package/dist/cli/commands/init-state.d.ts.map +1 -0
- package/dist/cli/commands/init-state.js +10 -0
- package/dist/cli/commands/init-state.js.map +1 -0
- package/dist/cli/commands/logs.d.ts +6 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/logs.js +9 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/commands/parse-heal.d.ts +6 -0
- package/dist/cli/commands/parse-heal.d.ts.map +1 -0
- package/dist/cli/commands/parse-heal.js +5 -0
- package/dist/cli/commands/parse-heal.js.map +1 -0
- package/dist/cli/commands/parse-result.d.ts +6 -0
- package/dist/cli/commands/parse-result.d.ts.map +1 -0
- package/dist/cli/commands/parse-result.js +5 -0
- package/dist/cli/commands/parse-result.js.map +1 -0
- package/dist/cli/commands/status.d.ts +6 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +5 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/validate-manifest.d.ts +6 -0
- package/dist/cli/commands/validate-manifest.d.ts.map +1 -0
- package/dist/cli/commands/validate-manifest.js +5 -0
- package/dist/cli/commands/validate-manifest.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +360 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output-formatter.d.ts +6 -0
- package/dist/cli/output-formatter.d.ts.map +1 -0
- package/dist/cli/output-formatter.js +19 -0
- package/dist/cli/output-formatter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/adapters/types.d.ts +40 -0
- package/dist/lib/adapters/types.d.ts.map +1 -0
- package/dist/lib/adapters/types.js +3 -0
- package/dist/lib/adapters/types.js.map +1 -0
- package/dist/lib/contracts/schema-validator.d.ts +15 -0
- package/dist/lib/contracts/schema-validator.d.ts.map +1 -0
- package/dist/lib/contracts/schema-validator.js +63 -0
- package/dist/lib/contracts/schema-validator.js.map +1 -0
- package/dist/lib/contracts/types.d.ts +91 -0
- package/dist/lib/contracts/types.d.ts.map +1 -0
- package/dist/lib/contracts/types.js +15 -0
- package/dist/lib/contracts/types.js.map +1 -0
- package/dist/lib/contracts/validate-manifest.d.ts +16 -0
- package/dist/lib/contracts/validate-manifest.d.ts.map +1 -0
- package/dist/lib/contracts/validate-manifest.js +123 -0
- package/dist/lib/contracts/validate-manifest.js.map +1 -0
- package/dist/lib/diagnostics/doctor.d.ts +17 -0
- package/dist/lib/diagnostics/doctor.d.ts.map +1 -0
- package/dist/lib/diagnostics/doctor.js +131 -0
- package/dist/lib/diagnostics/doctor.js.map +1 -0
- package/dist/lib/errors/explain-error.d.ts +10 -0
- package/dist/lib/errors/explain-error.d.ts.map +1 -0
- package/dist/lib/errors/explain-error.js +73 -0
- package/dist/lib/errors/explain-error.js.map +1 -0
- package/dist/lib/errors/types.d.ts +16 -0
- package/dist/lib/errors/types.d.ts.map +1 -0
- package/dist/lib/errors/types.js +50 -0
- package/dist/lib/errors/types.js.map +1 -0
- package/dist/lib/index.d.ts +29 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +25 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/orchestrator/batch-strategy.d.ts +9 -0
- package/dist/lib/orchestrator/batch-strategy.d.ts.map +1 -0
- package/dist/lib/orchestrator/batch-strategy.js +34 -0
- package/dist/lib/orchestrator/batch-strategy.js.map +1 -0
- package/dist/lib/orchestrator/healing-policy.d.ts +47 -0
- package/dist/lib/orchestrator/healing-policy.d.ts.map +1 -0
- package/dist/lib/orchestrator/healing-policy.js +104 -0
- package/dist/lib/orchestrator/healing-policy.js.map +1 -0
- package/dist/lib/orchestrator/index.d.ts +11 -0
- package/dist/lib/orchestrator/index.d.ts.map +1 -0
- package/dist/lib/orchestrator/index.js +11 -0
- package/dist/lib/orchestrator/index.js.map +1 -0
- package/dist/lib/orchestrator/patch-validation.d.ts +9 -0
- package/dist/lib/orchestrator/patch-validation.d.ts.map +1 -0
- package/dist/lib/orchestrator/patch-validation.js +58 -0
- package/dist/lib/orchestrator/patch-validation.js.map +1 -0
- package/dist/lib/orchestrator/scheduling.d.ts +12 -0
- package/dist/lib/orchestrator/scheduling.d.ts.map +1 -0
- package/dist/lib/orchestrator/scheduling.js +74 -0
- package/dist/lib/orchestrator/scheduling.js.map +1 -0
- package/dist/lib/orchestrator/write-safety.d.ts +14 -0
- package/dist/lib/orchestrator/write-safety.d.ts.map +1 -0
- package/dist/lib/orchestrator/write-safety.js +44 -0
- package/dist/lib/orchestrator/write-safety.js.map +1 -0
- package/dist/lib/parser/parse-heal.d.ts +12 -0
- package/dist/lib/parser/parse-heal.d.ts.map +1 -0
- package/dist/lib/parser/parse-heal.js +9 -0
- package/dist/lib/parser/parse-heal.js.map +1 -0
- package/dist/lib/parser/parse-result.d.ts +12 -0
- package/dist/lib/parser/parse-result.d.ts.map +1 -0
- package/dist/lib/parser/parse-result.js +9 -0
- package/dist/lib/parser/parse-result.js.map +1 -0
- package/dist/lib/parser/parser.d.ts +8 -0
- package/dist/lib/parser/parser.d.ts.map +1 -0
- package/dist/lib/parser/parser.js +167 -0
- package/dist/lib/parser/parser.js.map +1 -0
- package/dist/lib/state/init-state.d.ts +15 -0
- package/dist/lib/state/init-state.d.ts.map +1 -0
- package/dist/lib/state/init-state.js +50 -0
- package/dist/lib/state/init-state.js.map +1 -0
- package/dist/lib/state/logs.d.ts +19 -0
- package/dist/lib/state/logs.d.ts.map +1 -0
- package/dist/lib/state/logs.js +25 -0
- package/dist/lib/state/logs.js.map +1 -0
- package/dist/lib/state/state.d.ts +7 -0
- package/dist/lib/state/state.d.ts.map +1 -0
- package/dist/lib/state/state.js +72 -0
- package/dist/lib/state/state.js.map +1 -0
- package/dist/lib/state/status.d.ts +22 -0
- package/dist/lib/state/status.d.ts.map +1 -0
- package/dist/lib/state/status.js +34 -0
- package/dist/lib/state/status.js.map +1 -0
- package/dist/lib/state/types.d.ts +55 -0
- package/dist/lib/state/types.d.ts.map +1 -0
- package/dist/lib/state/types.js +14 -0
- package/dist/lib/state/types.js.map +1 -0
- package/install-local.sh +239 -0
- package/install.sh +36 -0
- package/package.json +55 -0
- package/site/CNAME +1 -0
- package/site/index.html +141 -0
- package/site/install.sh +36 -0
- package/site/style.css +319 -0
- package/skill/SKILL.md +127 -0
- package/skill/SPEC.md +1189 -0
- package/skill/build/compile.mjs +237 -0
- package/skill/build/manifest.json +21 -0
- package/skill/fragments/common/model-selection.md +11 -0
- package/skill/fragments/common/portability-rules.md +16 -0
- package/skill/fragments/common/workflow.md +12 -0
- package/skill/fragments/domain/adapter-model.md +42 -0
- package/skill/fragments/domain/architecture-overview.md +36 -0
- package/skill/fragments/domain/contracts.md +31 -0
- package/skill/fragments/domain/pbh-healing.md +47 -0
- package/skill/fragments/domain/state-resume.md +34 -0
- package/skill/fragments/domain/verification-safety.md +33 -0
- package/skill/fragments/meta/schemas-reference.md +13 -0
- package/skill/fragments/meta/templates-reference.md +11 -0
- package/skill/schemas/heal_decision.v2.json +100 -0
- package/skill/schemas/manifest.v2.json +91 -0
- package/skill/schemas/state.v2.json +183 -0
- package/skill/schemas/task_result.v2.json +104 -0
- package/skill/schemas/verify_profile.v2.json +61 -0
- package/skill/skills/agent-threader/agent-threader.md +85 -0
- package/skill/templates/orchestrator.ts +38 -0
- package/skill/templates/parser.ts +384 -0
- package/skill/templates/types.ts +282 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentThreader v2 — Shared Parser and Validator Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module is the ONLY authority for interpreting worker and healer contracts.
|
|
5
|
+
* Adapters MUST delegate to these functions rather than implementing their own parsing.
|
|
6
|
+
*
|
|
7
|
+
* Key behaviors:
|
|
8
|
+
* - Extracts fenced JSON blocks using custom sentinels
|
|
9
|
+
* - Uses "last block wins" to defeat prompt echo contamination
|
|
10
|
+
* - Applies conservative JSON repair before validation
|
|
11
|
+
* - Returns deterministic ParserFailure on invalid output
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import type {
|
|
16
|
+
TaskResultV2,
|
|
17
|
+
HealDecisionV2,
|
|
18
|
+
ParserFailure,
|
|
19
|
+
ParserErrorCode,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
|
|
22
|
+
// ─── Sentinels ───────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const TASK_RESULT_START = "<<<TASK_RESULT_V2>>>";
|
|
25
|
+
const TASK_RESULT_END = "<<<END_TASK_RESULT_V2>>>";
|
|
26
|
+
const HEAL_DECISION_START = "<<<HEAL_DECISION_V2>>>";
|
|
27
|
+
const HEAL_DECISION_END = "<<<END_HEAL_DECISION_V2>>>";
|
|
28
|
+
|
|
29
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract and validate a TaskResultV2 from a worker log file.
|
|
33
|
+
* Uses the LAST matching fenced block to defeat prompt echo contamination.
|
|
34
|
+
*/
|
|
35
|
+
export function parseTaskResult(
|
|
36
|
+
logPath: string,
|
|
37
|
+
): TaskResultV2 | ParserFailure {
|
|
38
|
+
const raw = readFileSync(logPath, "utf8");
|
|
39
|
+
return parseTaskResultFromString(raw);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract and validate a HealDecisionV2 from a healer log file.
|
|
44
|
+
* Uses the LAST matching fenced block to defeat prompt echo contamination.
|
|
45
|
+
*/
|
|
46
|
+
export function parseHealDecision(
|
|
47
|
+
logPath: string,
|
|
48
|
+
): HealDecisionV2 | ParserFailure {
|
|
49
|
+
const raw = readFileSync(logPath, "utf8");
|
|
50
|
+
return parseHealDecisionFromString(raw);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse TaskResultV2 from a raw string (useful for testing).
|
|
55
|
+
*/
|
|
56
|
+
export function parseTaskResultFromString(
|
|
57
|
+
raw: string,
|
|
58
|
+
): TaskResultV2 | ParserFailure {
|
|
59
|
+
const extracted = extractLastFencedBlock(
|
|
60
|
+
raw,
|
|
61
|
+
TASK_RESULT_START,
|
|
62
|
+
TASK_RESULT_END,
|
|
63
|
+
);
|
|
64
|
+
if (!extracted) {
|
|
65
|
+
return fail("NO_SENTINEL", "No <<<TASK_RESULT_V2>>> block found in output");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const repaired = repairJson(extracted);
|
|
69
|
+
|
|
70
|
+
let parsed: unknown;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(repaired);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return fail(
|
|
75
|
+
"INVALID_JSON",
|
|
76
|
+
`Invalid JSON in TASK_RESULT_V2 block: ${e instanceof Error ? e.message : String(e)}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return validateTaskResult(parsed);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse HealDecisionV2 from a raw string (useful for testing).
|
|
85
|
+
*/
|
|
86
|
+
export function parseHealDecisionFromString(
|
|
87
|
+
raw: string,
|
|
88
|
+
): HealDecisionV2 | ParserFailure {
|
|
89
|
+
const extracted = extractLastFencedBlock(
|
|
90
|
+
raw,
|
|
91
|
+
HEAL_DECISION_START,
|
|
92
|
+
HEAL_DECISION_END,
|
|
93
|
+
);
|
|
94
|
+
if (!extracted) {
|
|
95
|
+
return fail(
|
|
96
|
+
"NO_SENTINEL",
|
|
97
|
+
"No <<<HEAL_DECISION_V2>>> block found in output",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const repaired = repairJson(extracted);
|
|
102
|
+
|
|
103
|
+
let parsed: unknown;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(repaired);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return fail(
|
|
108
|
+
"INVALID_JSON",
|
|
109
|
+
`Invalid JSON in HEAL_DECISION_V2 block: ${e instanceof Error ? e.message : String(e)}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return validateHealDecision(parsed);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Sentinel Extraction ─────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract the LAST matching fenced block between start and end sentinels.
|
|
120
|
+
* "Last block wins" defeats prompt echo contamination.
|
|
121
|
+
*/
|
|
122
|
+
function extractLastFencedBlock(
|
|
123
|
+
text: string,
|
|
124
|
+
startSentinel: string,
|
|
125
|
+
endSentinel: string,
|
|
126
|
+
): string | null {
|
|
127
|
+
let lastMatch: string | null = null;
|
|
128
|
+
let searchFrom = 0;
|
|
129
|
+
|
|
130
|
+
while (true) {
|
|
131
|
+
const startIdx = text.indexOf(startSentinel, searchFrom);
|
|
132
|
+
if (startIdx === -1) break;
|
|
133
|
+
|
|
134
|
+
const contentStart = startIdx + startSentinel.length;
|
|
135
|
+
const endIdx = text.indexOf(endSentinel, contentStart);
|
|
136
|
+
if (endIdx === -1) break;
|
|
137
|
+
|
|
138
|
+
lastMatch = text.slice(contentStart, endIdx).trim();
|
|
139
|
+
searchFrom = endIdx + endSentinel.length;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return lastMatch;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── JSON Repair ─────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Conservative JSON repair pass. Handles common LLM JSON mistakes:
|
|
149
|
+
* - Strips outer markdown fences (```json ... ```)
|
|
150
|
+
* - Removes trailing commas before } or ]
|
|
151
|
+
* - Removes JavaScript-style single-line and multi-line comments
|
|
152
|
+
*/
|
|
153
|
+
function repairJson(raw: string): string {
|
|
154
|
+
let text = raw.trim();
|
|
155
|
+
|
|
156
|
+
// Strip outer markdown fences
|
|
157
|
+
if (text.startsWith("```")) {
|
|
158
|
+
const lines = text.split("\n");
|
|
159
|
+
if (lines[lines.length - 1].trim() === "```") {
|
|
160
|
+
// Remove first line (```json or ```) and last line (```)
|
|
161
|
+
lines.shift();
|
|
162
|
+
lines.pop();
|
|
163
|
+
text = lines.join("\n").trim();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Remove single-line comments (// ...) — but not inside strings
|
|
168
|
+
text = removeComments(text);
|
|
169
|
+
|
|
170
|
+
// Remove trailing commas before } or ]
|
|
171
|
+
text = text.replace(/,\s*([\]}])/g, "$1");
|
|
172
|
+
|
|
173
|
+
return text;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Remove JS-style comments from JSON text while preserving string contents.
|
|
178
|
+
* This is a simple state machine that tracks whether we're inside a string.
|
|
179
|
+
*/
|
|
180
|
+
function removeComments(text: string): string {
|
|
181
|
+
let result = "";
|
|
182
|
+
let i = 0;
|
|
183
|
+
let inString = false;
|
|
184
|
+
|
|
185
|
+
while (i < text.length) {
|
|
186
|
+
if (inString) {
|
|
187
|
+
if (text[i] === "\\" && i + 1 < text.length) {
|
|
188
|
+
result += text[i] + text[i + 1];
|
|
189
|
+
i += 2;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (text[i] === '"') {
|
|
193
|
+
inString = false;
|
|
194
|
+
}
|
|
195
|
+
result += text[i];
|
|
196
|
+
i++;
|
|
197
|
+
} else {
|
|
198
|
+
// Check for string start
|
|
199
|
+
if (text[i] === '"') {
|
|
200
|
+
inString = true;
|
|
201
|
+
result += text[i];
|
|
202
|
+
i++;
|
|
203
|
+
}
|
|
204
|
+
// Check for single-line comment
|
|
205
|
+
else if (text[i] === "/" && i + 1 < text.length && text[i + 1] === "/") {
|
|
206
|
+
// Skip to end of line
|
|
207
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
208
|
+
}
|
|
209
|
+
// Check for multi-line comment
|
|
210
|
+
else if (text[i] === "/" && i + 1 < text.length && text[i + 1] === "*") {
|
|
211
|
+
i += 2;
|
|
212
|
+
while (
|
|
213
|
+
i < text.length &&
|
|
214
|
+
!(text[i] === "*" && i + 1 < text.length && text[i + 1] === "/")
|
|
215
|
+
) {
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
if (i < text.length) i += 2; // skip */
|
|
219
|
+
} else {
|
|
220
|
+
result += text[i];
|
|
221
|
+
i++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── Validation ──────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
const VALID_TASK_STATUSES = new Set([
|
|
232
|
+
"DONE",
|
|
233
|
+
"BLOCKED",
|
|
234
|
+
"FAILED",
|
|
235
|
+
"CONTRACT_ERROR",
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
function validateTaskResult(data: unknown): TaskResultV2 | ParserFailure {
|
|
239
|
+
if (typeof data !== "object" || data === null) {
|
|
240
|
+
return fail("SCHEMA_VIOLATION", "Task result must be a JSON object");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const obj = data as Record<string, unknown>;
|
|
244
|
+
|
|
245
|
+
if (obj.contract_version !== "2.0") {
|
|
246
|
+
return fail(
|
|
247
|
+
"UNSUPPORTED_VERSION",
|
|
248
|
+
`Expected contract_version "2.0", got "${String(obj.contract_version)}"`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const field of ["task_id", "status", "summary"] as const) {
|
|
253
|
+
if (typeof obj[field] !== "string" || (obj[field] as string).length === 0) {
|
|
254
|
+
return fail("MISSING_REQUIRED_FIELD", `Missing or empty required field: ${field}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!VALID_TASK_STATUSES.has(obj.status as string)) {
|
|
259
|
+
return fail(
|
|
260
|
+
"SCHEMA_VIOLATION",
|
|
261
|
+
`Invalid status "${String(obj.status)}". Must be one of: ${[...VALID_TASK_STATUSES].join(", ")}`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Validate writes[] if present
|
|
266
|
+
if (obj.writes !== undefined) {
|
|
267
|
+
if (!Array.isArray(obj.writes)) {
|
|
268
|
+
return fail("SCHEMA_VIOLATION", "writes must be an array");
|
|
269
|
+
}
|
|
270
|
+
for (const w of obj.writes as Array<Record<string, unknown>>) {
|
|
271
|
+
if (!w.content && !w.content_ref) {
|
|
272
|
+
return fail(
|
|
273
|
+
"SCHEMA_VIOLATION",
|
|
274
|
+
`Write entry for "${String(w.path)}" must have content or content_ref`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return obj as unknown as TaskResultV2;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const VALID_HEAL_DECISIONS = new Set(["RETRY", "ESCALATE", "NOT_FIXABLE"]);
|
|
284
|
+
const VALID_HEAL_SCOPES = new Set(["task", "batch", "epoch"]);
|
|
285
|
+
|
|
286
|
+
function validateHealDecision(data: unknown): HealDecisionV2 | ParserFailure {
|
|
287
|
+
if (typeof data !== "object" || data === null) {
|
|
288
|
+
return fail("SCHEMA_VIOLATION", "Heal decision must be a JSON object");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const obj = data as Record<string, unknown>;
|
|
292
|
+
|
|
293
|
+
if (obj.contract_version !== "2.0") {
|
|
294
|
+
return fail(
|
|
295
|
+
"UNSUPPORTED_VERSION",
|
|
296
|
+
`Expected contract_version "2.0", got "${String(obj.contract_version)}"`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const field of [
|
|
301
|
+
"scope",
|
|
302
|
+
"decision",
|
|
303
|
+
"failure_class",
|
|
304
|
+
"root_cause",
|
|
305
|
+
] as const) {
|
|
306
|
+
if (typeof obj[field] !== "string" || (obj[field] as string).length === 0) {
|
|
307
|
+
return fail("MISSING_REQUIRED_FIELD", `Missing or empty required field: ${field}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!VALID_HEAL_SCOPES.has(obj.scope as string)) {
|
|
312
|
+
return fail(
|
|
313
|
+
"SCHEMA_VIOLATION",
|
|
314
|
+
`Invalid scope "${String(obj.scope)}". Must be one of: ${[...VALID_HEAL_SCOPES].join(", ")}`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!VALID_HEAL_DECISIONS.has(obj.decision as string)) {
|
|
319
|
+
return fail(
|
|
320
|
+
"SCHEMA_VIOLATION",
|
|
321
|
+
`Invalid decision "${String(obj.decision)}". Must be one of: ${[...VALID_HEAL_DECISIONS].join(", ")}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!Array.isArray(obj.patches)) {
|
|
326
|
+
return fail("MISSING_REQUIRED_FIELD", "Missing required field: patches");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return obj as unknown as HealDecisionV2;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─── Failure Signature Generation ────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Generate a stable failure signature from a failure class and signal.
|
|
336
|
+
*
|
|
337
|
+
* Algorithm (per SPEC.md §10):
|
|
338
|
+
* 1. Start with the normalized failure class
|
|
339
|
+
* 2. Extract the primary stable signal
|
|
340
|
+
* 3. Remove timestamps, absolute paths, task IDs, and unstable numbers
|
|
341
|
+
* 4. Lowercase and collapse whitespace
|
|
342
|
+
* 5. Truncate to a stable maximum length
|
|
343
|
+
*/
|
|
344
|
+
export function generateFailureSignature(
|
|
345
|
+
failureClass: string,
|
|
346
|
+
primarySignal: string,
|
|
347
|
+
maxLength = 120,
|
|
348
|
+
): string {
|
|
349
|
+
let sig = primarySignal;
|
|
350
|
+
|
|
351
|
+
// Remove absolute paths
|
|
352
|
+
sig = sig.replace(/\/[\w./-]+/g, "<path>");
|
|
353
|
+
|
|
354
|
+
// Remove ISO timestamps
|
|
355
|
+
sig = sig.replace(/\d{4}-\d{2}-\d{2}T[\d:.Z+-]+/g, "<ts>");
|
|
356
|
+
|
|
357
|
+
// Remove standalone large numbers (likely line numbers, PIDs, etc.)
|
|
358
|
+
sig = sig.replace(/\b\d{4,}\b/g, "<n>");
|
|
359
|
+
|
|
360
|
+
// Lowercase and collapse whitespace
|
|
361
|
+
sig = sig.toLowerCase().replace(/\s+/g, " ").trim();
|
|
362
|
+
|
|
363
|
+
// Truncate
|
|
364
|
+
if (sig.length > maxLength - failureClass.length - 1) {
|
|
365
|
+
sig = sig.slice(0, maxLength - failureClass.length - 1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return `${failureClass}:${sig}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
function fail(code: ParserErrorCode, message: string): ParserFailure {
|
|
374
|
+
return { ok: false, code, message };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Type guard to check if a parse result is a ParserFailure.
|
|
379
|
+
*/
|
|
380
|
+
export function isParserFailure(
|
|
381
|
+
result: TaskResultV2 | HealDecisionV2 | ParserFailure,
|
|
382
|
+
): result is ParserFailure {
|
|
383
|
+
return "ok" in result && result.ok === false;
|
|
384
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentThreader v2 — Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* These types correspond 1:1 to the JSON schemas in ../schemas/.
|
|
5
|
+
* They are the canonical TypeScript representation of the v2 contract stack.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── Manifest ────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface ManifestV2 {
|
|
11
|
+
manifest_version: "2.0";
|
|
12
|
+
run_id: string;
|
|
13
|
+
tasks: ManifestTaskV2[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ManifestTaskV2 {
|
|
17
|
+
id: string;
|
|
18
|
+
prompt_ref: string;
|
|
19
|
+
depends_on: string[];
|
|
20
|
+
timeout_sec: number;
|
|
21
|
+
verify_profile: string;
|
|
22
|
+
context_refs?: string[];
|
|
23
|
+
priority?: number;
|
|
24
|
+
retry_policy?: RetryPolicy;
|
|
25
|
+
metadata?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RetryPolicy {
|
|
29
|
+
max_attempts?: number;
|
|
30
|
+
retry_on?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Verify Profile Registry ─────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export interface VerifyProfileRegistry {
|
|
36
|
+
profiles: Record<string, VerifyProfile>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface VerifyProfile {
|
|
40
|
+
steps: VerifyStep[];
|
|
41
|
+
rollback_on_failure: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface VerifyStep {
|
|
45
|
+
name: string;
|
|
46
|
+
cmd: string;
|
|
47
|
+
cwd: string;
|
|
48
|
+
timeout_sec: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Task Result (Worker Output) ─────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export interface TaskResultV2 {
|
|
54
|
+
contract_version: "2.0";
|
|
55
|
+
task_id: string;
|
|
56
|
+
status: "DONE" | "BLOCKED" | "FAILED" | "CONTRACT_ERROR";
|
|
57
|
+
summary: string;
|
|
58
|
+
changed_files?: string[];
|
|
59
|
+
writes?: WriteEntry[];
|
|
60
|
+
evidence?: Evidence;
|
|
61
|
+
failure_class?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface WriteEntry {
|
|
65
|
+
path: string;
|
|
66
|
+
op: "create" | "replace" | "append";
|
|
67
|
+
encoding: "utf8";
|
|
68
|
+
content?: string;
|
|
69
|
+
content_ref?: string;
|
|
70
|
+
sha256_before?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Evidence {
|
|
74
|
+
commands?: string[];
|
|
75
|
+
log_refs?: string[];
|
|
76
|
+
notes?: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Heal Decision (Healer Output) ──────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export interface HealDecisionV2 {
|
|
82
|
+
contract_version: "2.0";
|
|
83
|
+
scope: "task" | "batch" | "epoch";
|
|
84
|
+
decision: "RETRY" | "ESCALATE" | "NOT_FIXABLE";
|
|
85
|
+
failure_class: string;
|
|
86
|
+
root_cause: string;
|
|
87
|
+
patches: HealPatch[];
|
|
88
|
+
learned_rule?: string;
|
|
89
|
+
escalations?: Array<{ task_id: string; reason: string }>;
|
|
90
|
+
retry_policy?: HealRetryPolicy;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface HealPatch {
|
|
94
|
+
target: "shared_context" | "task_prompt" | "runtime_patch" | "contract_hint";
|
|
95
|
+
operation: "replace" | "append" | "merge";
|
|
96
|
+
path?: string;
|
|
97
|
+
task_id?: string;
|
|
98
|
+
content?: string | Record<string, unknown>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface HealRetryPolicy {
|
|
102
|
+
reset_tasks?: string[];
|
|
103
|
+
retry_window?: "same_window" | "shrink_window" | "next_epoch";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Run State ───────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export interface StateV2 {
|
|
109
|
+
state_version: "2.0";
|
|
110
|
+
run_id: string;
|
|
111
|
+
run_status: "RUNNING" | "COMPLETED" | "ABORTED";
|
|
112
|
+
abort_reason: string | null;
|
|
113
|
+
manifest_digest: string;
|
|
114
|
+
policy: RunPolicy;
|
|
115
|
+
tasks: Record<string, TaskState>;
|
|
116
|
+
healing_rounds: HealingRound[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface RunPolicy {
|
|
120
|
+
heal_schedule: "auto" | "off" | "task" | "batch" | "epoch";
|
|
121
|
+
batch_strategy: "fibonacci" | "fixed";
|
|
122
|
+
current_batch_size: number;
|
|
123
|
+
failure_threshold: number;
|
|
124
|
+
max_worker_attempts_per_task: number;
|
|
125
|
+
max_heal_rounds_per_window: number;
|
|
126
|
+
max_total_heal_rounds: number;
|
|
127
|
+
signature_repeat_limit: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type TaskStatus =
|
|
131
|
+
| "PENDING"
|
|
132
|
+
| "RUNNING"
|
|
133
|
+
| "DONE"
|
|
134
|
+
| "BLOCKED"
|
|
135
|
+
| "FAILED"
|
|
136
|
+
| "ESCALATED";
|
|
137
|
+
|
|
138
|
+
export interface TaskState {
|
|
139
|
+
status: TaskStatus;
|
|
140
|
+
worker_attempts: number;
|
|
141
|
+
healer_attempts: number;
|
|
142
|
+
last_failure_class: string | null;
|
|
143
|
+
last_failure_signature: string | null;
|
|
144
|
+
applied_patch_ids: string[];
|
|
145
|
+
history: HistoryEntry[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface HistoryEntry {
|
|
149
|
+
task_id: string;
|
|
150
|
+
phase: "worker" | "verify" | "healer" | "rollback";
|
|
151
|
+
attempt_number: number;
|
|
152
|
+
log_path: string;
|
|
153
|
+
verify_log_path?: string | null;
|
|
154
|
+
exit_code?: number | null;
|
|
155
|
+
failure_class?: string | null;
|
|
156
|
+
failure_signature?: string | null;
|
|
157
|
+
applied_patch_ids: string[];
|
|
158
|
+
duration_sec?: number | null;
|
|
159
|
+
timestamp: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface HealingRound {
|
|
163
|
+
round_number: number;
|
|
164
|
+
scope: "task" | "batch" | "epoch";
|
|
165
|
+
window_task_ids: string[];
|
|
166
|
+
failed_task_ids: string[];
|
|
167
|
+
decision: "RETRY" | "ESCALATE" | "NOT_FIXABLE";
|
|
168
|
+
applied_patch_ids: string[];
|
|
169
|
+
timestamp: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Adapter Interface ───────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
export type ParserErrorCode =
|
|
175
|
+
| "NO_SENTINEL"
|
|
176
|
+
| "INVALID_JSON"
|
|
177
|
+
| "SCHEMA_VIOLATION"
|
|
178
|
+
| "MISSING_REQUIRED_FIELD"
|
|
179
|
+
| "UNSUPPORTED_VERSION";
|
|
180
|
+
|
|
181
|
+
export interface PreparedInvocation {
|
|
182
|
+
cwd: string;
|
|
183
|
+
argv: string[];
|
|
184
|
+
env?: Record<string, string>;
|
|
185
|
+
stdin?: string | null;
|
|
186
|
+
timeoutSec: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ExecutionArtifact {
|
|
190
|
+
logPath: string;
|
|
191
|
+
exitCode: number | null;
|
|
192
|
+
startedAt: string;
|
|
193
|
+
finishedAt: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface ParserFailure {
|
|
197
|
+
ok: false;
|
|
198
|
+
code: ParserErrorCode;
|
|
199
|
+
message: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface AdapterHealth {
|
|
203
|
+
ready: boolean;
|
|
204
|
+
details: string[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface RunContext {
|
|
208
|
+
repoRoot: string;
|
|
209
|
+
logsDir: string;
|
|
210
|
+
sharedContextPaths: string[];
|
|
211
|
+
contractHints: Map<string, string[]>;
|
|
212
|
+
policy: RunPolicy;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface CliAdapter {
|
|
216
|
+
id: string;
|
|
217
|
+
capabilities: {
|
|
218
|
+
stdinPrompt: boolean;
|
|
219
|
+
argPrompt: boolean;
|
|
220
|
+
pty: boolean;
|
|
221
|
+
interactive: boolean;
|
|
222
|
+
};
|
|
223
|
+
prepare(task: ManifestTaskV2, ctx: RunContext): PreparedInvocation;
|
|
224
|
+
execute(
|
|
225
|
+
invocation: PreparedInvocation,
|
|
226
|
+
ctx: RunContext,
|
|
227
|
+
): Promise<ExecutionArtifact>;
|
|
228
|
+
extractResult(
|
|
229
|
+
artifact: ExecutionArtifact,
|
|
230
|
+
ctx: RunContext,
|
|
231
|
+
): Promise<TaskResultV2 | ParserFailure>;
|
|
232
|
+
healthcheck(ctx: RunContext): Promise<AdapterHealth>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Failure Classification ──────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
export type FailureClass =
|
|
238
|
+
| "prompt_gap"
|
|
239
|
+
| "missing_paths"
|
|
240
|
+
| "weak_contract"
|
|
241
|
+
| "contract_error"
|
|
242
|
+
| "output_format"
|
|
243
|
+
| "build_error"
|
|
244
|
+
| "test_error"
|
|
245
|
+
| "smoke_error"
|
|
246
|
+
| "timeout"
|
|
247
|
+
| "transient_infra"
|
|
248
|
+
| "blocked_external"
|
|
249
|
+
| "real_bug"
|
|
250
|
+
| "unknown";
|
|
251
|
+
|
|
252
|
+
export const HEALABLE_FAILURE_CLASSES: ReadonlySet<string> = new Set([
|
|
253
|
+
"prompt_gap",
|
|
254
|
+
"missing_paths",
|
|
255
|
+
"weak_contract",
|
|
256
|
+
"contract_error",
|
|
257
|
+
"output_format",
|
|
258
|
+
"timeout",
|
|
259
|
+
"transient_infra",
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
export const NON_HEALABLE_FAILURE_CLASSES: ReadonlySet<string> = new Set([
|
|
263
|
+
"blocked_external",
|
|
264
|
+
"real_bug",
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
// ─── PBH Fibonacci Sequence ─────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
export const FIBONACCI_BATCH_SEQUENCE = [1, 2, 3, 5, 8, 13, 21, 34] as const;
|
|
270
|
+
|
|
271
|
+
// ─── Default Policy ──────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
export const DEFAULT_POLICY: RunPolicy = {
|
|
274
|
+
heal_schedule: "auto",
|
|
275
|
+
batch_strategy: "fibonacci",
|
|
276
|
+
current_batch_size: 1,
|
|
277
|
+
failure_threshold: 0.2,
|
|
278
|
+
max_worker_attempts_per_task: 2,
|
|
279
|
+
max_heal_rounds_per_window: 2,
|
|
280
|
+
max_total_heal_rounds: 8,
|
|
281
|
+
signature_repeat_limit: 2,
|
|
282
|
+
};
|