opencode-feature-factory 0.2.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/LICENSE +21 -0
- package/README.md +877 -0
- package/assets/agent/backend-builder.md +73 -0
- package/assets/agent/codebase-researcher.md +56 -0
- package/assets/agent/design-interpreter.md +37 -0
- package/assets/agent/frontend-builder.md +74 -0
- package/assets/agent/implementation-validator.md +73 -0
- package/assets/agent/security-reviewer.md +60 -0
- package/assets/agent/spec-writer.md +94 -0
- package/assets/agent/story-reader.md +38 -0
- package/assets/agent/story-writer.md +39 -0
- package/assets/agent/test-verifier.md +73 -0
- package/assets/agent/work-decomposer.md +102 -0
- package/assets/agent/work-reviewer.md +77 -0
- package/assets/command/feature.md +68 -0
- package/assets/skills/feature/SCHEMA.md +990 -0
- package/assets/skills/feature/SKILL.md +620 -0
- package/dist/tui.js +3641 -0
- package/package.json +65 -0
- package/src/cleanup-sweep-command.js +75 -0
- package/src/cleanup-sweep-eligibility.js +581 -0
- package/src/cleanup-sweep-output.js +139 -0
- package/src/cleanup-sweep-report.js +548 -0
- package/src/cleanup-sweep.js +546 -0
- package/src/cli-output.js +251 -0
- package/src/cli.js +1152 -0
- package/src/config.js +231 -0
- package/src/cost-attribution.js +327 -0
- package/src/cost-report.js +185 -0
- package/src/detached-log-supervisor.js +178 -0
- package/src/doctor.js +598 -0
- package/src/env-snapshot.js +211 -0
- package/src/factory-diagnostics.js +429 -0
- package/src/factory-paths.js +40 -0
- package/src/factory.js +4769 -0
- package/src/feature-command-payload.js +378 -0
- package/src/git.js +110 -0
- package/src/github.js +252 -0
- package/src/hardening/atomic-write.js +954 -0
- package/src/hardening/line-output.js +139 -0
- package/src/hardening/output-policy.js +365 -0
- package/src/hardening/process-verification.js +542 -0
- package/src/hardening/sensitive-data.js +341 -0
- package/src/hardening/terminal-encoding.js +224 -0
- package/src/plugin.js +246 -0
- package/src/post-pr-ci.js +754 -0
- package/src/process-evidence.js +1139 -0
- package/src/refs.js +144 -0
- package/src/run-state.js +2411 -0
- package/src/steering-conflicts.js +77 -0
- package/src/telemetry.js +419 -0
- package/src/tui-data.js +499 -0
- package/src/tui-rendering.js +148 -0
- package/src/utils.js +35 -0
- package/src/validate.js +1655 -0
- package/src/worktrees.js +56 -0
package/src/tui-data.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { publicCostAttributionSummary } from "./cost-attribution.js";
|
|
4
|
+
import {
|
|
5
|
+
DIAGNOSTIC_CLASSIFICATIONS,
|
|
6
|
+
DIAGNOSTIC_CONDITIONS,
|
|
7
|
+
DIAGNOSTIC_SEVERITIES,
|
|
8
|
+
DIAGNOSTIC_STATUSES,
|
|
9
|
+
diagnoseRunFile,
|
|
10
|
+
diagnoseRunObject,
|
|
11
|
+
diagnosticEnvelope,
|
|
12
|
+
diagnosticItem,
|
|
13
|
+
} from "./factory-diagnostics.js";
|
|
14
|
+
import {
|
|
15
|
+
freeformSegment,
|
|
16
|
+
projectDiagnosticData,
|
|
17
|
+
projectFreeformData,
|
|
18
|
+
renderTerminalSegmentsOrFallback,
|
|
19
|
+
} from "./hardening/output-policy.js";
|
|
20
|
+
|
|
21
|
+
const SKIP_DIRS = new Set([".git", "node_modules", "dist", "coverage", ".cache", ".next"]);
|
|
22
|
+
const MAX_SCAN_DIRS = 2000;
|
|
23
|
+
const MAX_DIAGNOSTIC_SUMMARY = 180;
|
|
24
|
+
const ROOT_CACHE_TTL_MS = 30000;
|
|
25
|
+
const FAIL_CLOSED_CONDITIONS = new Set(["invalid-run-state"]);
|
|
26
|
+
const DIAGNOSTIC_IDENTITIES = Object.freeze({
|
|
27
|
+
condition: new Set(DIAGNOSTIC_CONDITIONS),
|
|
28
|
+
classification: new Set(DIAGNOSTIC_CLASSIFICATIONS),
|
|
29
|
+
severity: new Set(DIAGNOSTIC_SEVERITIES),
|
|
30
|
+
status: new Set(DIAGNOSTIC_STATUSES),
|
|
31
|
+
});
|
|
32
|
+
const rootCache = new Map();
|
|
33
|
+
|
|
34
|
+
export function factoryRoots(api, options = {}) {
|
|
35
|
+
const starts = tuiStartPaths(api);
|
|
36
|
+
const cacheKey = starts.map((start) => resolve(start)).join("\0");
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const cached = rootCache.get(cacheKey);
|
|
39
|
+
if (!options.noCache && !options.notes && cached && cached.expiresAt > now && cached.roots.some((root) => existsSync(root))) return cached.roots;
|
|
40
|
+
|
|
41
|
+
const roots = new Set();
|
|
42
|
+
for (const start of starts) {
|
|
43
|
+
for (const root of findFactoryRoots(start, options)) roots.add(root);
|
|
44
|
+
}
|
|
45
|
+
const sorted = [...roots].sort();
|
|
46
|
+
if (!options.noCache) rootCache.set(cacheKey, { expiresAt: now + ROOT_CACHE_TTL_MS, roots: sorted });
|
|
47
|
+
return sorted;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function tuiStartPaths(api) {
|
|
51
|
+
const path = api?.state?.path;
|
|
52
|
+
const starts = [path?.worktree, path?.directory].filter(isUsablePath);
|
|
53
|
+
if (starts.length) return [...new Set(starts)];
|
|
54
|
+
const cwd = typeof process.cwd === "function" ? process.cwd() : null;
|
|
55
|
+
return isUsablePath(cwd) ? [cwd] : [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isUsablePath(value) {
|
|
59
|
+
if (typeof value !== "string" || value.trim().length === 0) return false;
|
|
60
|
+
const resolved = resolve(value);
|
|
61
|
+
return dirname(resolved) !== resolved;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function readRuns(roots, options = {}) {
|
|
65
|
+
return roots
|
|
66
|
+
.flatMap((root) => readRootRuns(root, options))
|
|
67
|
+
.sort(compareRunRows);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function runHasNonOkDiagnostic(run) {
|
|
71
|
+
return Boolean(run?.diagnostic_status && run.diagnostic_status !== "ok");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Sidebar visibility rule, extracted so the documented contract is testable:
|
|
75
|
+
// non-completed runs are always listed; a completed run stays listed while it
|
|
76
|
+
// carries a non-ok diagnostic; and the most recent completed run is appended
|
|
77
|
+
// even when healthy. Multiple completed runs can therefore appear at once when
|
|
78
|
+
// older completed runs still have diagnostics that need attention.
|
|
79
|
+
export function selectVisibleRuns(runs) {
|
|
80
|
+
const rows = Array.isArray(runs) ? runs : [];
|
|
81
|
+
const list = rows.filter((run) => run?.status !== "completed" || runHasNonOkDiagnostic(run));
|
|
82
|
+
const latestCompleted = rows.find((run) => run?.status === "completed");
|
|
83
|
+
if (latestCompleted && !list.some((run) => run.run_id === latestCompleted.run_id)) return [...list, latestCompleted];
|
|
84
|
+
return list;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function findFactoryRoots(start, options = {}) {
|
|
88
|
+
if (!isUsablePath(start)) return [];
|
|
89
|
+
const dir = resolve(start);
|
|
90
|
+
const roots = new Set();
|
|
91
|
+
const nearest = findNearestFactoryRoot(dir);
|
|
92
|
+
if (nearest) roots.add(nearest);
|
|
93
|
+
for (const root of findNestedFactoryRoots(dir, options)) roots.add(root);
|
|
94
|
+
if (!roots.size) roots.add(join(dir, ".opencode", "factory"));
|
|
95
|
+
return [...roots];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findNearestFactoryRoot(start) {
|
|
99
|
+
let dir = resolve(start);
|
|
100
|
+
while (true) {
|
|
101
|
+
const candidate = join(dir, ".opencode", "factory");
|
|
102
|
+
if (existsSync(candidate)) return candidate;
|
|
103
|
+
const parent = dirname(dir);
|
|
104
|
+
if (parent === dir) return null;
|
|
105
|
+
dir = parent;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function findNestedFactoryRoots(start, options = {}) {
|
|
110
|
+
const roots = [];
|
|
111
|
+
const queue = [resolve(start)];
|
|
112
|
+
const maxScanDirs = Number.isInteger(options.maxScanDirs) && options.maxScanDirs > 0 ? options.maxScanDirs : MAX_SCAN_DIRS;
|
|
113
|
+
let scanned = 0;
|
|
114
|
+
while (queue.length && scanned < maxScanDirs) {
|
|
115
|
+
const dir = queue.shift();
|
|
116
|
+
scanned += 1;
|
|
117
|
+
const candidate = join(dir, ".opencode", "factory");
|
|
118
|
+
if (existsSync(candidate)) roots.push(candidate);
|
|
119
|
+
for (const child of safeReadDir(dir)) {
|
|
120
|
+
if (SKIP_DIRS.has(child.name)) continue;
|
|
121
|
+
const path = join(dir, child.name);
|
|
122
|
+
if (child.isDirectory()) queue.push(path);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (queue.length && Array.isArray(options.notes)) {
|
|
126
|
+
options.notes.push({ type: "scan-truncated", start: resolve(start), scanned, remaining: queue.length, max_scan_dirs: maxScanDirs });
|
|
127
|
+
}
|
|
128
|
+
return roots;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function compareRunRows(a, b) {
|
|
132
|
+
return Number(isInvalidRow(b)) - Number(isInvalidRow(a)) || String(b.updated_at || "").localeCompare(String(a.updated_at || "")) || String(b.run_id || "").localeCompare(String(a.run_id || ""));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isInvalidRow(row) {
|
|
136
|
+
return row?.status === "invalid" || row?.diagnostic_classification === "invalid";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function safeReadDir(dir) {
|
|
140
|
+
try {
|
|
141
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
142
|
+
} catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readRootRuns(root, options = {}) {
|
|
148
|
+
if (!root || !existsSync(root)) return [];
|
|
149
|
+
const repoRoot = dirname(dirname(root));
|
|
150
|
+
return readdirSync(root)
|
|
151
|
+
.flatMap((runID) => {
|
|
152
|
+
const file = join(root, runID, "run.json");
|
|
153
|
+
if (!existsSync(file) || !statSync(file).isFile()) return [];
|
|
154
|
+
try {
|
|
155
|
+
const run = JSON.parse(readFileSync(file, "utf8"));
|
|
156
|
+
const diagnostics = options.diagnostics === false ? healthyDiagnostics() : safeDiagnoseRunObject(run, file, { repoRoot });
|
|
157
|
+
if (shouldUseFallbackRow(diagnostics)) return [fallbackRun(runID, file, diagnostics)];
|
|
158
|
+
return [summarize(run, runID, file, diagnostics)];
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const diagnostics = options.diagnostics === false ? parseErrorDiagnostics(file, error) : safeDiagnoseRunFile(file, { repoRoot });
|
|
161
|
+
return [fallbackRun(runID, file, diagnostics)];
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseErrorDiagnostics(file, error) {
|
|
167
|
+
const checkedAt = new Date().toISOString();
|
|
168
|
+
return diagnosticEnvelope([
|
|
169
|
+
diagnosticItem("invalid-run-state", {
|
|
170
|
+
checkedAt,
|
|
171
|
+
authoritative: false,
|
|
172
|
+
message: `Factory run JSON could not be parsed: ${error.message}`,
|
|
173
|
+
evidence: { source: "tui-data", run_path: file, error: error.message },
|
|
174
|
+
}),
|
|
175
|
+
], { checkedAt, authoritative: false });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function summarize(run, fallbackID, file, diagnostics = healthyDiagnostics()) {
|
|
179
|
+
return {
|
|
180
|
+
run_id: projectFreeformText(run.run_id || fallbackID),
|
|
181
|
+
status: projectFreeformText(run.status || "unknown"),
|
|
182
|
+
mode: projectOptionalFreeformText(run.mode),
|
|
183
|
+
gate: projectOptionalFreeformText(pendingGate(run)),
|
|
184
|
+
branch: projectOptionalFreeformText(run.branch),
|
|
185
|
+
pr_url: projectOptionalFreeformText(run.pr_url),
|
|
186
|
+
review_tier: projectOptionalFreeformText(stringOrNull(run.review_tier)),
|
|
187
|
+
review_tier_source: null,
|
|
188
|
+
updated_at: projectOptionalFreeformText(run.updated_at),
|
|
189
|
+
current: projectOptionalFreeformText(currentSummary(run)),
|
|
190
|
+
steering: steeringSummary(run),
|
|
191
|
+
cost: costSummary(run.cost_attribution),
|
|
192
|
+
slices: sliceSummary(run),
|
|
193
|
+
panel: projectOptionalFreeformText(panelSummary(run)),
|
|
194
|
+
terminal_reason: projectOptionalFreeformText(run.terminal_result?.reason),
|
|
195
|
+
file: projectFreeformText(file),
|
|
196
|
+
run_dir: projectFreeformText(dirname(file)),
|
|
197
|
+
...diagnosticSummary(diagnostics),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function fallbackRun(fallbackID, file, diagnostics) {
|
|
202
|
+
return {
|
|
203
|
+
run_id: projectFreeformText(fallbackID),
|
|
204
|
+
status: "invalid",
|
|
205
|
+
mode: null,
|
|
206
|
+
gate: null,
|
|
207
|
+
branch: null,
|
|
208
|
+
pr_url: null,
|
|
209
|
+
review_tier: null,
|
|
210
|
+
review_tier_source: null,
|
|
211
|
+
updated_at: null,
|
|
212
|
+
current: null,
|
|
213
|
+
steering: null,
|
|
214
|
+
cost: null,
|
|
215
|
+
slices: null,
|
|
216
|
+
panel: null,
|
|
217
|
+
terminal_reason: null,
|
|
218
|
+
file: projectFreeformText(file),
|
|
219
|
+
run_dir: projectFreeformText(dirname(file)),
|
|
220
|
+
...diagnosticSummary(diagnostics),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function shouldUseFallbackRow(diagnostics) {
|
|
225
|
+
return Array.isArray(diagnostics?.items) && diagnostics.items.some((item) => FAIL_CLOSED_CONDITIONS.has(item?.condition));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function safeDiagnoseRunFile(file, options) {
|
|
229
|
+
try {
|
|
230
|
+
return diagnoseRunFile(file, options);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const checkedAt = new Date().toISOString();
|
|
233
|
+
return diagnosticEnvelope([
|
|
234
|
+
diagnosticItem("invalid-run-state", {
|
|
235
|
+
checkedAt,
|
|
236
|
+
authoritative: false,
|
|
237
|
+
message: `Factory diagnostics failed: ${error.message}`,
|
|
238
|
+
evidence: { source: "tui-data", run_path: file, error: error.message },
|
|
239
|
+
}),
|
|
240
|
+
], { checkedAt, authoritative: false });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function safeDiagnoseRunObject(run, file, options) {
|
|
245
|
+
try {
|
|
246
|
+
return diagnoseRunObject(run, { ...options, runDir: dirname(file), runFile: file });
|
|
247
|
+
} catch (error) {
|
|
248
|
+
const checkedAt = new Date().toISOString();
|
|
249
|
+
return diagnosticEnvelope([
|
|
250
|
+
diagnosticItem("invalid-run-state", {
|
|
251
|
+
checkedAt,
|
|
252
|
+
authoritative: false,
|
|
253
|
+
message: `Factory diagnostics failed: ${error.message}`,
|
|
254
|
+
evidence: { source: "tui-data", run_path: file, error: error.message },
|
|
255
|
+
}),
|
|
256
|
+
], { checkedAt, authoritative: false });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function diagnosticSummary(diagnostics) {
|
|
261
|
+
const envelope = projectTuiDiagnosticData(diagnostics || healthyDiagnostics());
|
|
262
|
+
return {
|
|
263
|
+
diagnostics: envelope,
|
|
264
|
+
diagnostic_status: stringOrDefault(envelope.status, "ok"),
|
|
265
|
+
diagnostic_severity: stringOrDefault(envelope.severity, "info"),
|
|
266
|
+
diagnostic_classification: stringOrDefault(envelope.classification, "healthy"),
|
|
267
|
+
diagnostic_summary: truncateDiagnosticSummary(stringOrDefault(envelope.summary, "No diagnostics")),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function projectTuiDiagnosticData(diagnostics) {
|
|
272
|
+
return terminalSafeProjection(plainProjection(projectDiagnosticData(diagnostics, {
|
|
273
|
+
validatedIdentityPaths: validatedDiagnosticIdentityPaths(diagnostics),
|
|
274
|
+
})));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function healthyDiagnostics() {
|
|
278
|
+
return diagnosticEnvelope([], { authoritative: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function truncateDiagnosticSummary(value) {
|
|
282
|
+
const text = String(value || "");
|
|
283
|
+
if (text.length <= MAX_DIAGNOSTIC_SUMMARY) return text;
|
|
284
|
+
return `${text.slice(0, MAX_DIAGNOSTIC_SUMMARY - 3)}...`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function pendingGate(run) {
|
|
288
|
+
for (const [name, gate] of Object.entries(run.gates || {})) {
|
|
289
|
+
if (gate?.status === "pending") return name;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function sliceSummary(run) {
|
|
295
|
+
const slices = Array.isArray(run.slices) ? run.slices : [];
|
|
296
|
+
if (!slices.length) return null;
|
|
297
|
+
return {
|
|
298
|
+
merged: slices.filter((item) => item?.status === "merged").length,
|
|
299
|
+
blocked: slices.filter((item) => item?.status === "blocked").length,
|
|
300
|
+
total: slices.length,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function currentSummary(run) {
|
|
305
|
+
const activeSlice = firstByStatus(run.slices, ["running", "review"]);
|
|
306
|
+
if (activeSlice) return summarizeWorkItem(activeSlice.id, activeSlice.status, activeSlice.attempts);
|
|
307
|
+
const activeStep = firstByStatus(run.steps, ["running", "review"]);
|
|
308
|
+
if (activeStep) return summarizeWorkItem(activeStep.agent, activeStep.status, activeStep.attempts);
|
|
309
|
+
const blockedSlice = firstByStatus(run.slices, ["blocked"]);
|
|
310
|
+
if (blockedSlice) return summarizeWorkItem(blockedSlice.id, blockedSlice.status, blockedSlice.attempts);
|
|
311
|
+
const step = firstByStatus(run.steps, ["blocked", "pending"]);
|
|
312
|
+
if (step) return summarizeWorkItem(step.agent, step.status, step.attempts);
|
|
313
|
+
const panel = inferredPrePrPanelSummary(run);
|
|
314
|
+
if (panel) return panel;
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function steeringSummary(run) {
|
|
319
|
+
const steering = run?.steering;
|
|
320
|
+
if (!steering || typeof steering !== "object" || Array.isArray(steering)) return { pending: null, uncheckpointed: null, consumed_count: 0, latest_consumed: null, boundary: null, action_claim: null, last_action: null, pr_fence: null };
|
|
321
|
+
const pending = steering.pending && typeof steering.pending === "object" && !Array.isArray(steering.pending)
|
|
322
|
+
? {
|
|
323
|
+
id: projectOptionalFreeformText(stringOrNull(steering.pending.id)),
|
|
324
|
+
ref: projectOptionalFreeformText(stringOrNull(steering.pending.ref)),
|
|
325
|
+
hash: projectOptionalFreeformText(stringOrNull(steering.pending.hash)),
|
|
326
|
+
message_chars: Number.isInteger(steering.pending.message_chars) ? steering.pending.message_chars : null,
|
|
327
|
+
created_at: projectOptionalFreeformText(stringOrNull(steering.pending.created_at)),
|
|
328
|
+
}
|
|
329
|
+
: null;
|
|
330
|
+
const consumed = Array.isArray(steering.history) ? steering.history.filter((item) => item?.event === "consumed") : [];
|
|
331
|
+
const latest = consumed[consumed.length - 1];
|
|
332
|
+
const uncheckpointed = steering.uncheckpointed && typeof steering.uncheckpointed === "object" && !Array.isArray(steering.uncheckpointed)
|
|
333
|
+
? {
|
|
334
|
+
id: projectOptionalFreeformText(stringOrNull(steering.uncheckpointed.id)),
|
|
335
|
+
ref: projectOptionalFreeformText(stringOrNull(steering.uncheckpointed.ref)),
|
|
336
|
+
hash: projectOptionalFreeformText(stringOrNull(steering.uncheckpointed.hash)),
|
|
337
|
+
message_chars: Number.isInteger(steering.uncheckpointed.message_chars) ? steering.uncheckpointed.message_chars : null,
|
|
338
|
+
created_at: projectOptionalFreeformText(stringOrNull(steering.uncheckpointed.created_at)),
|
|
339
|
+
consumed_at: projectOptionalFreeformText(stringOrNull(steering.uncheckpointed.consumed_at)),
|
|
340
|
+
}
|
|
341
|
+
: null;
|
|
342
|
+
return {
|
|
343
|
+
pending,
|
|
344
|
+
uncheckpointed,
|
|
345
|
+
consumed_count: consumed.length,
|
|
346
|
+
latest_consumed: latest ? {
|
|
347
|
+
ref: projectOptionalFreeformText(stringOrNull(latest.ref)),
|
|
348
|
+
consumed_at: projectOptionalFreeformText(stringOrNull(latest.consumed_at)),
|
|
349
|
+
} : null,
|
|
350
|
+
boundary: steering.boundary && typeof steering.boundary === "object" ? {
|
|
351
|
+
kind: projectOptionalFreeformText(stringOrNull(steering.boundary.kind)),
|
|
352
|
+
token: projectOptionalFreeformText(stringOrNull(steering.boundary.token)),
|
|
353
|
+
generation: Number.isInteger(steering.boundary.generation) ? steering.boundary.generation : null,
|
|
354
|
+
created_at: projectOptionalFreeformText(stringOrNull(steering.boundary.created_at)),
|
|
355
|
+
} : null,
|
|
356
|
+
action_claim: steeringActionSummary(steering.action_claim),
|
|
357
|
+
last_action: steeringActionSummary(steering.last_action),
|
|
358
|
+
pr_fence: steering.pr_fence && typeof steering.pr_fence === "object" ? {
|
|
359
|
+
token: projectOptionalFreeformText(stringOrNull(steering.pr_fence.token)),
|
|
360
|
+
generation: Number.isInteger(steering.pr_fence.generation) ? steering.pr_fence.generation : null,
|
|
361
|
+
created_at: projectOptionalFreeformText(stringOrNull(steering.pr_fence.created_at)),
|
|
362
|
+
} : null,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function steeringActionSummary(action) {
|
|
367
|
+
if (!action || typeof action !== "object" || Array.isArray(action)) return null;
|
|
368
|
+
return {
|
|
369
|
+
kind: projectOptionalFreeformText(stringOrNull(action.kind)),
|
|
370
|
+
token: projectOptionalFreeformText(stringOrNull(action.token)),
|
|
371
|
+
generation: Number.isInteger(action.generation) ? action.generation : null,
|
|
372
|
+
claimed_at: projectOptionalFreeformText(stringOrNull(action.claimed_at)),
|
|
373
|
+
outcome: projectOptionalFreeformText(stringOrNull(action.outcome)),
|
|
374
|
+
resolved_at: projectOptionalFreeformText(stringOrNull(action.resolved_at)),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function costSummary(costAttribution) {
|
|
379
|
+
if (!costAttribution || typeof costAttribution !== "object" || Array.isArray(costAttribution)) return null;
|
|
380
|
+
const summary = projectPublicStrings(publicCostAttributionSummary(costAttribution));
|
|
381
|
+
return { ...summary, label: formatProjectedCostSummary(summary) };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function inferredPrePrPanelSummary(run) {
|
|
385
|
+
if (run?.status !== "running" || pendingGate(run)) return null;
|
|
386
|
+
const testAccepted = Array.isArray(run.steps) && run.steps.some((step) => step?.agent === "test-verifier" && step?.status === "accepted");
|
|
387
|
+
if (!testAccepted) return null;
|
|
388
|
+
const validatorVerdict = stringOrNull(run.validator?.verdict);
|
|
389
|
+
const securityVerdict = stringOrNull(run.security_review?.verdict);
|
|
390
|
+
if (!validatorVerdict && !securityVerdict) return "pre-PR panel running";
|
|
391
|
+
if (validatorVerdict === "NO-GO" || securityVerdict === "BLOCK") return "panel remediation running";
|
|
392
|
+
if (!validatorVerdict) return "implementation-validator running";
|
|
393
|
+
if (!securityVerdict) return "security-reviewer running";
|
|
394
|
+
if (!run.pr_url) return "PR pending";
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function firstByStatus(items, statuses) {
|
|
399
|
+
if (!Array.isArray(items)) return null;
|
|
400
|
+
for (const status of statuses) {
|
|
401
|
+
const item = items.find((candidate) => candidate?.status === status);
|
|
402
|
+
if (item) return item;
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function summarizeWorkItem(name, status, attempts) {
|
|
408
|
+
const label = projectOptionalFreeformData(stringOrNull(name));
|
|
409
|
+
if (!label || !status) return null;
|
|
410
|
+
const normalizedStatus = projectFreeformData(String(status));
|
|
411
|
+
const attempt = Number.isInteger(attempts) && attempts > 0 ? ` a${attempts}` : "";
|
|
412
|
+
return `${label} ${normalizedStatus}${attempt}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function panelSummary(run) {
|
|
416
|
+
const validator = projectOptionalFreeformData(run.validator?.verdict);
|
|
417
|
+
const security = projectOptionalFreeformData(run.security_review?.verdict);
|
|
418
|
+
if (!validator && !security) return null;
|
|
419
|
+
return [validator, security].filter(Boolean).join(" / ");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function validatedDiagnosticIdentityPaths(value) {
|
|
423
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
424
|
+
const paths = [];
|
|
425
|
+
for (const field of ["status", "severity", "classification"]) {
|
|
426
|
+
if (DIAGNOSTIC_IDENTITIES[field].has(value[field])) paths.push([field]);
|
|
427
|
+
}
|
|
428
|
+
if (!Array.isArray(value.items)) return paths;
|
|
429
|
+
for (let index = 0; index < value.items.length; index += 1) {
|
|
430
|
+
const item = value.items[index];
|
|
431
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
432
|
+
for (const field of ["condition", "status", "severity", "classification"]) {
|
|
433
|
+
if (DIAGNOSTIC_IDENTITIES[field].has(item[field])) paths.push(["items", String(index), field]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return paths;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function plainProjection(value) {
|
|
440
|
+
if (Array.isArray(value)) return value.map(plainProjection);
|
|
441
|
+
if (value && typeof value === "object") {
|
|
442
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plainProjection(item)]));
|
|
443
|
+
}
|
|
444
|
+
return value;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function terminalSafeProjection(value) {
|
|
448
|
+
if (typeof value === "string") return projectFreeformText(value);
|
|
449
|
+
if (Array.isArray(value)) return value.map(terminalSafeProjection);
|
|
450
|
+
if (value && typeof value === "object") {
|
|
451
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, terminalSafeProjection(item)]));
|
|
452
|
+
}
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function projectPublicStrings(value) {
|
|
457
|
+
if (typeof value === "string") return projectFreeformText(value);
|
|
458
|
+
if (Array.isArray(value)) return value.map(projectPublicStrings);
|
|
459
|
+
if (value && typeof value === "object") {
|
|
460
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, projectPublicStrings(item)]));
|
|
461
|
+
}
|
|
462
|
+
return value;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function projectFreeformText(value) {
|
|
466
|
+
return renderTerminalSegmentsOrFallback([freeformSegment(String(value))]);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function projectOptionalFreeformText(value) {
|
|
470
|
+
return value === null || value === undefined ? null : projectFreeformText(value);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function projectOptionalFreeformData(value) {
|
|
474
|
+
return value === null || value === undefined ? null : projectFreeformData(String(value));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function formatProjectedCostSummary(summary) {
|
|
478
|
+
const parts = [`cost ${summary.status}`, `${summary.entry_count} ${summary.entry_count === 1 ? "entry" : "entries"}`];
|
|
479
|
+
if (summary.total_tokens !== undefined) parts.push(`${summary.total_tokens} tokens`);
|
|
480
|
+
else if (summary.input_tokens !== undefined || summary.output_tokens !== undefined) {
|
|
481
|
+
parts.push(`${summary.input_tokens ?? "?"}/${summary.output_tokens ?? "?"} tokens`);
|
|
482
|
+
}
|
|
483
|
+
if (summary.mixed_currency) parts.push("mixed currency");
|
|
484
|
+
else if (summary.cost_total !== undefined) parts.push(`${formatCost(summary.cost_total)} ${summary.cost_currency || ""}`.trim());
|
|
485
|
+
if (summary.missing.length > 0) parts.push(`missing ${summary.missing.join(",")}`);
|
|
486
|
+
return parts.join(" · ");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function formatCost(value) {
|
|
490
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(6).replace(/0+$/u, "").replace(/\.$/u, "");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function stringOrNull(value) {
|
|
494
|
+
return typeof value === "string" ? value : null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function stringOrDefault(value, fallback) {
|
|
498
|
+
return typeof value === "string" ? value : fallback;
|
|
499
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DIAGNOSTIC_CLASSIFICATIONS,
|
|
3
|
+
DIAGNOSTIC_STATUSES,
|
|
4
|
+
} from "./factory-diagnostics.js";
|
|
5
|
+
import {
|
|
6
|
+
freeformSegment,
|
|
7
|
+
identitySegment,
|
|
8
|
+
renderTerminalSegmentsOrFallback,
|
|
9
|
+
TRUSTED_SEGMENTS,
|
|
10
|
+
} from "./hardening/output-policy.js";
|
|
11
|
+
|
|
12
|
+
const RUN_STATUSES = new Set(["running", "completed", "blocked", "partial", "needs-human", "invalid"]);
|
|
13
|
+
const RUN_MODES = new Set(["interactive", "headless", "autonomous"]);
|
|
14
|
+
const DIAGNOSTIC_CLASSIFICATION_SET = new Set(DIAGNOSTIC_CLASSIFICATIONS);
|
|
15
|
+
const DIAGNOSTIC_STATUS_SET = new Set(DIAGNOSTIC_STATUSES);
|
|
16
|
+
const COST_STATUSES = new Set(["available", "partial", "unavailable"]);
|
|
17
|
+
const COST_CURRENCY_PATTERN = /^[A-Z]{3,12}$/u;
|
|
18
|
+
|
|
19
|
+
export function renderFreeformText(value, max) {
|
|
20
|
+
return truncate(renderTerminalSegmentsOrFallback([freeformSegment(value)]), max);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderRunStatus(value) {
|
|
24
|
+
return renderValidatedIdentity(value, RUN_STATUSES);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderRunMode(value) {
|
|
28
|
+
return renderValidatedIdentity(value, RUN_MODES);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderDiagnosticStatus(value) {
|
|
32
|
+
return renderValidatedIdentity(value, DIAGNOSTIC_STATUS_SET);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function renderDiagnosticLine(run, max = 42) {
|
|
36
|
+
const segments = [];
|
|
37
|
+
if (run?.diagnostic_classification) {
|
|
38
|
+
segments.push(validatedIdentitySegment(run.diagnostic_classification, DIAGNOSTIC_CLASSIFICATION_SET));
|
|
39
|
+
segments.push(TRUSTED_SEGMENTS.COLON_SPACE);
|
|
40
|
+
}
|
|
41
|
+
segments.push(freeformSegment(run?.diagnostic_summary || "Diagnostics require attention"));
|
|
42
|
+
return truncate(renderTerminalSegmentsOrFallback(segments), max);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function renderRunTextFields(run) {
|
|
46
|
+
const status = renderRunStatus(run?.status);
|
|
47
|
+
const mode = run?.mode ? renderRunMode(run.mode) : null;
|
|
48
|
+
const steering = renderSteeringLine(run?.steering);
|
|
49
|
+
const slices = renderSliceLine(run?.slices);
|
|
50
|
+
const diagnostic = renderDiagnosticLine(run);
|
|
51
|
+
return {
|
|
52
|
+
run_id: renderFreeformText(run?.run_id, 31),
|
|
53
|
+
status_line: mode ? `${status} | ${mode}` : status,
|
|
54
|
+
gate_line: run?.gate ? `gate: ${renderFreeformText(run.gate)}` : null,
|
|
55
|
+
current_line: run?.current ? `current: ${renderFreeformText(run.current, 34)}` : null,
|
|
56
|
+
steering_line: steering,
|
|
57
|
+
slices_line: slices,
|
|
58
|
+
cost_line: renderCostLine(run?.cost),
|
|
59
|
+
panel_line: run?.panel ? `panel: ${renderFreeformText(run.panel)}` : null,
|
|
60
|
+
pr_line: run?.pr_url ? `PR: ${renderFreeformText(run.pr_url, 34)}` : null,
|
|
61
|
+
terminal_reason_line: run?.terminal_reason ? `reason: ${renderFreeformText(run.terminal_reason, 30)}` : null,
|
|
62
|
+
diagnostic_line: `diagnostic: ${diagnostic}`,
|
|
63
|
+
branch_line: run?.branch ? `branch: ${renderFreeformText(run.branch, 30)}` : null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function renderHiddenRunsLine(value) {
|
|
68
|
+
const count = Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
69
|
+
return `+ ${renderTerminalSegmentsOrFallback([identitySegment(count)])} more runs`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderSteeringLine(steering) {
|
|
73
|
+
if (!steering || typeof steering !== "object") return null;
|
|
74
|
+
if (steering.pending) {
|
|
75
|
+
return `steering pending: ${renderFreeformText(steering.pending.ref || "pending", 34)}`;
|
|
76
|
+
}
|
|
77
|
+
if (steering.latest_consumed) {
|
|
78
|
+
const count = Number.isSafeInteger(steering.consumed_count) && steering.consumed_count >= 0
|
|
79
|
+
? steering.consumed_count
|
|
80
|
+
: 0;
|
|
81
|
+
const renderedCount = renderTerminalSegmentsOrFallback([identitySegment(count)]);
|
|
82
|
+
const ref = renderFreeformText(steering.latest_consumed.ref || "consumed", 24);
|
|
83
|
+
return `steering consumed: ${renderedCount} latest ${ref}`;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderSliceLine(slices) {
|
|
89
|
+
if (!slices || typeof slices !== "object") return null;
|
|
90
|
+
const merged = nonNegativeInteger(slices.merged);
|
|
91
|
+
const total = nonNegativeInteger(slices.total);
|
|
92
|
+
const blocked = nonNegativeInteger(slices.blocked);
|
|
93
|
+
const base = `slices: ${renderNumber(merged)}/${renderNumber(total)}`;
|
|
94
|
+
return blocked > 0 ? `${base} | blocked ${renderNumber(blocked)}` : base;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderNumber(value) {
|
|
98
|
+
return renderTerminalSegmentsOrFallback([identitySegment(value)]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function nonNegativeInteger(value) {
|
|
102
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderCostLine(cost) {
|
|
106
|
+
if (!cost || typeof cost !== "object" || !COST_STATUSES.has(cost.status)) return null;
|
|
107
|
+
const entryCount = nonNegativeInteger(cost.entry_count);
|
|
108
|
+
const parts = [`cost ${cost.status}`, `${entryCount} ${entryCount === 1 ? "entry" : "entries"}`];
|
|
109
|
+
if (Number.isFinite(cost.total_tokens) && cost.total_tokens >= 0) parts.push(`${cost.total_tokens} tokens`);
|
|
110
|
+
else if ((Number.isFinite(cost.input_tokens) && cost.input_tokens >= 0)
|
|
111
|
+
|| (Number.isFinite(cost.output_tokens) && cost.output_tokens >= 0)) {
|
|
112
|
+
parts.push(`${validCostNumber(cost.input_tokens)}/${validCostNumber(cost.output_tokens)} tokens`);
|
|
113
|
+
}
|
|
114
|
+
if (cost.mixed_currency === true) parts.push("mixed currency");
|
|
115
|
+
else if (Number.isFinite(cost.cost_total) && cost.cost_total >= 0) {
|
|
116
|
+
const currency = typeof cost.cost_currency === "string" && COST_CURRENCY_PATTERN.test(cost.cost_currency)
|
|
117
|
+
? cost.cost_currency
|
|
118
|
+
: "";
|
|
119
|
+
parts.push(`${formatCost(cost.cost_total)} ${currency}`.trim());
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(cost.missing) && cost.missing.length > 0) {
|
|
122
|
+
parts.push(`missing ${cost.missing.map((item) => renderFreeformText(item)).join(",")}`);
|
|
123
|
+
}
|
|
124
|
+
return truncate(parts.join(" · "), 42);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validCostNumber(value) {
|
|
128
|
+
return Number.isFinite(value) && value >= 0 ? value : "?";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatCost(value) {
|
|
132
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(6).replace(/0+$/u, "").replace(/\.$/u, "");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderValidatedIdentity(value, allowed) {
|
|
136
|
+
return renderTerminalSegmentsOrFallback([validatedIdentitySegment(value, allowed)]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validatedIdentitySegment(value, allowed) {
|
|
140
|
+
return typeof value === "string" && allowed.has(value)
|
|
141
|
+
? identitySegment(value)
|
|
142
|
+
: freeformSegment(value ?? "unknown");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function truncate(value, max) {
|
|
146
|
+
if (!Number.isSafeInteger(max) || max < 0 || value.length <= max) return value;
|
|
147
|
+
return `${value.slice(0, Math.max(0, max - 3))}...`;
|
|
148
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function requireNonEmptyString(value, label) {
|
|
5
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error(`${label} must be a non-empty string`);
|
|
6
|
+
return value.trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function timestamp(value, label = "timestamp") {
|
|
10
|
+
if (value === undefined || value === null) return new Date().toISOString();
|
|
11
|
+
const parsed = typeof value === "number" ? value : value instanceof Date ? value.getTime() : Date.parse(value);
|
|
12
|
+
if (!Number.isFinite(parsed)) throw new Error(`invalid ${label}`);
|
|
13
|
+
return new Date(parsed).toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function physicalPath(path, label = "path", options = {}) {
|
|
17
|
+
const resolved = resolve(requireNonEmptyString(path, label));
|
|
18
|
+
if (!existsSync(resolved)) {
|
|
19
|
+
if (options.mustExist) throw new Error(`${label} is unresolvable: ${resolved}`);
|
|
20
|
+
return resolved;
|
|
21
|
+
}
|
|
22
|
+
return realpathSync.native(resolved);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isContainedPath(parent, child, options = {}) {
|
|
26
|
+
const allowEqual = options.allowEqual !== false;
|
|
27
|
+
const rel = relative(physicalPath(parent, "parent path"), physicalPath(child, "child path"));
|
|
28
|
+
if (rel === "") return allowEqual;
|
|
29
|
+
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function assertContainedPath(parent, child, label, options = {}) {
|
|
33
|
+
if (isContainedPath(parent, child, options)) return;
|
|
34
|
+
throw new Error(`${label} must stay under ${physicalPath(parent, "parent path")}`);
|
|
35
|
+
}
|