parallel-codex-tui 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -11
- package/dist/cli.js +3 -1
- package/dist/core/collaboration-timeline.js +3 -1
- package/dist/core/role-configuration.js +47 -8
- package/dist/core/session-index.js +123 -10
- package/dist/core/session-manager.js +15 -4
- package/dist/core/task-report.js +510 -0
- package/dist/core/task-search.js +142 -0
- package/dist/domain/schemas.js +9 -0
- package/dist/orchestrator/collaboration-channel.js +11 -3
- package/dist/orchestrator/orchestrator.js +117 -38
- package/dist/tui/App.js +125 -14
- package/dist/tui/FeatureBoardView.js +4 -1
- package/dist/tui/InputBar.js +22 -7
- package/dist/tui/StatusDetailView.js +27 -6
- package/dist/tui/TaskSessionsView.js +30 -9
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, readdir, readlink } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { RouteDecisionSchema, TurnMetaSchema } from "../domain/schemas.js";
|
|
5
|
+
import { loadCollaborationTimeline } from "./collaboration-timeline.js";
|
|
6
|
+
import { pathExists, readJson, readTextIfExists } from "./file-store.js";
|
|
7
|
+
import { loadTaskSessionDetails } from "./task-session-details.js";
|
|
8
|
+
export async function buildTaskReport(input) {
|
|
9
|
+
const taskSummary = {
|
|
10
|
+
...input.task,
|
|
11
|
+
turnCount: 0,
|
|
12
|
+
workerCount: 0,
|
|
13
|
+
nativeSessionCount: 0
|
|
14
|
+
};
|
|
15
|
+
const [turns, timeline, details, integrations, reconciliation] = await Promise.all([
|
|
16
|
+
readTaskReportTurns(input.taskDir),
|
|
17
|
+
loadCollaborationTimeline(input.task.id, input.taskDir),
|
|
18
|
+
loadTaskSessionDetails({ task: taskSummary, taskDir: input.taskDir }),
|
|
19
|
+
readTaskReportIntegrations(input.taskDir),
|
|
20
|
+
reconcileTaskWorkspace(input.taskDir, input.workspaceRoot)
|
|
21
|
+
]);
|
|
22
|
+
const report = {
|
|
23
|
+
format: "parallel-codex-task-report-v1",
|
|
24
|
+
generated_at: input.generatedAt,
|
|
25
|
+
task: input.task,
|
|
26
|
+
workspace: {
|
|
27
|
+
path: input.workspaceRoot,
|
|
28
|
+
reconciliation
|
|
29
|
+
},
|
|
30
|
+
turns,
|
|
31
|
+
features: timeline.features.map((feature) => ({
|
|
32
|
+
id: feature.id,
|
|
33
|
+
turn_id: feature.turnId,
|
|
34
|
+
title: feature.title,
|
|
35
|
+
description: feature.description,
|
|
36
|
+
depends_on: feature.dependsOn,
|
|
37
|
+
state: feature.state,
|
|
38
|
+
updated_at: feature.updatedAt,
|
|
39
|
+
...(feature.actorEngine ? { actor_engine: feature.actorEngine } : {}),
|
|
40
|
+
...(feature.actorModel ? { actor_model: feature.actorModel } : {}),
|
|
41
|
+
...(feature.criticEngine ? { critic_engine: feature.criticEngine } : {}),
|
|
42
|
+
...(feature.criticModel ? { critic_model: feature.criticModel } : {}),
|
|
43
|
+
findings: feature.findings,
|
|
44
|
+
replies: feature.replies,
|
|
45
|
+
...(typeof feature.resolvedFindings === "number"
|
|
46
|
+
? { resolved_findings: feature.resolvedFindings }
|
|
47
|
+
: {}),
|
|
48
|
+
...(typeof feature.unresolvedFindings === "number"
|
|
49
|
+
? { unresolved_findings: feature.unresolvedFindings }
|
|
50
|
+
: {}),
|
|
51
|
+
...(feature.latestFinding ? { latest_finding: feature.latestFinding } : {}),
|
|
52
|
+
...(feature.latestReply ? { latest_reply: feature.latestReply } : {})
|
|
53
|
+
})),
|
|
54
|
+
workers: details.workers.map((worker) => ({
|
|
55
|
+
id: worker.id,
|
|
56
|
+
turn_id: worker.turnId,
|
|
57
|
+
...(worker.featureId ? { feature_id: worker.featureId } : {}),
|
|
58
|
+
...(worker.featureTitle ? { feature_title: worker.featureTitle } : {}),
|
|
59
|
+
role: worker.role,
|
|
60
|
+
engine: worker.engine,
|
|
61
|
+
model: worker.model,
|
|
62
|
+
...(worker.modelProvider ? { model_provider: worker.modelProvider } : {}),
|
|
63
|
+
state: worker.state,
|
|
64
|
+
phase: worker.phase,
|
|
65
|
+
summary: worker.summary,
|
|
66
|
+
last_activity_at: worker.lastActivityAt,
|
|
67
|
+
...(worker.nativeSession ? {
|
|
68
|
+
native_session: {
|
|
69
|
+
id: worker.nativeSession.sessionId,
|
|
70
|
+
cwd: worker.nativeSession.cwd,
|
|
71
|
+
writable_dirs: worker.nativeSession.writableDirs,
|
|
72
|
+
created_at: worker.nativeSession.createdAt,
|
|
73
|
+
last_used_at: worker.nativeSession.lastUsedAt,
|
|
74
|
+
source: worker.nativeSession.source
|
|
75
|
+
}
|
|
76
|
+
} : {})
|
|
77
|
+
})),
|
|
78
|
+
integrations
|
|
79
|
+
};
|
|
80
|
+
return { report, markdown: renderTaskReportMarkdown(report) };
|
|
81
|
+
}
|
|
82
|
+
export async function reconcileTaskWorkspace(taskDir, workspaceRoot) {
|
|
83
|
+
const { waves, issues, sawIntegrationRecord } = await readIntegratedWaves(taskDir);
|
|
84
|
+
const authoritative = new Map();
|
|
85
|
+
for (const wave of waves) {
|
|
86
|
+
for (const changedPath of wave.changedPaths) {
|
|
87
|
+
authoritative.set(changedPath, wave);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const paths = await Promise.all([...authoritative.entries()]
|
|
91
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
92
|
+
.map(async ([changedPath, wave]) => {
|
|
93
|
+
const expected = await inspectReportEntry(wave.integrationDir, changedPath);
|
|
94
|
+
const current = await inspectReportEntry(workspaceRoot, changedPath);
|
|
95
|
+
return {
|
|
96
|
+
path: changedPath,
|
|
97
|
+
state: compareTaskReportEntries(expected, current),
|
|
98
|
+
source: {
|
|
99
|
+
turn_id: wave.turnId,
|
|
100
|
+
wave: wave.wave,
|
|
101
|
+
feature_ids: wave.featureIds
|
|
102
|
+
},
|
|
103
|
+
expected,
|
|
104
|
+
current
|
|
105
|
+
};
|
|
106
|
+
}));
|
|
107
|
+
const counts = emptyComparisonCounts();
|
|
108
|
+
for (const path of paths) {
|
|
109
|
+
counts[path.state] += 1;
|
|
110
|
+
}
|
|
111
|
+
const state = paths.length === 0
|
|
112
|
+
? sawIntegrationRecord || issues.length > 0 ? "unavailable" : "no-integrations"
|
|
113
|
+
: counts.unavailable > 0
|
|
114
|
+
? "unavailable"
|
|
115
|
+
: counts.drift + counts.missing + counts.unexpected > 0
|
|
116
|
+
? "drifted"
|
|
117
|
+
: "clean";
|
|
118
|
+
return {
|
|
119
|
+
state,
|
|
120
|
+
integrated_waves: waves.length,
|
|
121
|
+
changed_paths: paths.length,
|
|
122
|
+
counts,
|
|
123
|
+
paths,
|
|
124
|
+
issues
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function readTaskReportTurns(taskDir) {
|
|
128
|
+
const turnsRoot = join(taskDir, "turns");
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = await readdir(turnsRoot, { withFileTypes: true });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
const turns = await Promise.all(entries
|
|
137
|
+
.filter((entry) => entry.isDirectory() && /^\d{4}$/.test(entry.name))
|
|
138
|
+
.map(async (entry) => {
|
|
139
|
+
const dir = join(turnsRoot, entry.name);
|
|
140
|
+
try {
|
|
141
|
+
const meta = await readJson(join(dir, "turn.json"), TurnMetaSchema);
|
|
142
|
+
if (meta.turn_id !== entry.name) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const [request, route, requirements, plan, acceptanceCriteria, summary, featurePlan, judgeValidation, contract, acceptance, validation] = await Promise.all([
|
|
146
|
+
readTextIfExists(join(dir, "user.md")),
|
|
147
|
+
readRouteDecision(join(dir, "route.json")),
|
|
148
|
+
readTextIfExists(join(dir, "requirements.md")),
|
|
149
|
+
readTextIfExists(join(dir, "plan.md")),
|
|
150
|
+
readTextIfExists(join(dir, "acceptance.md")),
|
|
151
|
+
readTextIfExists(join(dir, "supervisor-summary.md")),
|
|
152
|
+
readOptionalJson(join(dir, "feature-plan.json")),
|
|
153
|
+
readOptionalJson(join(dir, "judge-validation.json")),
|
|
154
|
+
readOptionalJson(join(dir, "completion-contract.json")),
|
|
155
|
+
readOptionalJson(join(dir, "final-acceptance.json")),
|
|
156
|
+
readOptionalJson(join(dir, "final-acceptance-validation.json"))
|
|
157
|
+
]);
|
|
158
|
+
return {
|
|
159
|
+
turn_id: meta.turn_id,
|
|
160
|
+
created_at: meta.created_at,
|
|
161
|
+
request: request.trimEnd(),
|
|
162
|
+
...(route ? { route } : {}),
|
|
163
|
+
...(requirements.trim() ? { requirements: requirements.trimEnd() } : {}),
|
|
164
|
+
...(plan.trim() ? { plan: plan.trimEnd() } : {}),
|
|
165
|
+
...(acceptanceCriteria.trim() ? { acceptance_criteria: acceptanceCriteria.trimEnd() } : {}),
|
|
166
|
+
...(summary.trim() ? { supervisor_summary: summary.trimEnd() } : {}),
|
|
167
|
+
...(featurePlan !== undefined ? { feature_plan: featurePlan } : {}),
|
|
168
|
+
...(judgeValidation !== undefined ? { judge_validation: judgeValidation } : {}),
|
|
169
|
+
...(contract !== undefined ? { completion_contract: contract } : {}),
|
|
170
|
+
...(acceptance !== undefined ? { final_acceptance: acceptance } : {}),
|
|
171
|
+
...(validation !== undefined ? { final_acceptance_validation: validation } : {})
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}));
|
|
178
|
+
return turns
|
|
179
|
+
.filter((turn) => turn !== null)
|
|
180
|
+
.sort((left, right) => left.turn_id.localeCompare(right.turn_id));
|
|
181
|
+
}
|
|
182
|
+
async function readTaskReportIntegrations(taskDir) {
|
|
183
|
+
const { waves } = await readIntegratedWaves(taskDir);
|
|
184
|
+
return Promise.all(waves.map(async (wave) => {
|
|
185
|
+
const [verification, verificationReview] = await Promise.all([
|
|
186
|
+
readOptionalJson(join(wave.rootDir, "verification.json")),
|
|
187
|
+
readTextIfExists(join(wave.rootDir, "verification-review.md"))
|
|
188
|
+
]);
|
|
189
|
+
return {
|
|
190
|
+
turn_id: wave.turnId,
|
|
191
|
+
wave: wave.wave,
|
|
192
|
+
feature_ids: wave.featureIds,
|
|
193
|
+
changed_paths: wave.changedPaths,
|
|
194
|
+
...(verification !== undefined ? { verification } : {}),
|
|
195
|
+
...(verificationReview.trim() ? { verification_review: verificationReview.trimEnd() } : {})
|
|
196
|
+
};
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
async function readRouteDecision(path) {
|
|
200
|
+
if (!(await pathExists(path))) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
return await readJson(path, RouteDecisionSchema);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function readOptionalJson(path) {
|
|
211
|
+
if (!(await pathExists(path))) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(await readTextIfExists(path));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function readIntegratedWaves(taskDir) {
|
|
222
|
+
const workspacesRoot = join(taskDir, "workspaces");
|
|
223
|
+
const waves = [];
|
|
224
|
+
const issues = [];
|
|
225
|
+
let sawIntegrationRecord = false;
|
|
226
|
+
let turnEntries;
|
|
227
|
+
try {
|
|
228
|
+
turnEntries = await readdir(workspacesRoot, { withFileTypes: true });
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return { waves, issues, sawIntegrationRecord };
|
|
232
|
+
}
|
|
233
|
+
for (const turnEntry of turnEntries
|
|
234
|
+
.filter((entry) => entry.isDirectory() && /^turn-\d{4,}$/.test(entry.name))
|
|
235
|
+
.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
236
|
+
const turnId = turnEntry.name.slice("turn-".length);
|
|
237
|
+
const turnRoot = join(workspacesRoot, turnEntry.name);
|
|
238
|
+
let waveEntries;
|
|
239
|
+
try {
|
|
240
|
+
waveEntries = await readdir(turnRoot, { withFileTypes: true });
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
issues.push(`${turnEntry.name}: ${errorMessage(error)}`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
for (const waveEntry of waveEntries
|
|
247
|
+
.filter((entry) => entry.isDirectory() && /^wave-\d{4,}$/.test(entry.name))
|
|
248
|
+
.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
249
|
+
const path = join(turnRoot, waveEntry.name, "integration.json");
|
|
250
|
+
if (!(await pathExists(path))) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
sawIntegrationRecord = true;
|
|
254
|
+
const value = await readOptionalJson(path);
|
|
255
|
+
if (!isRecord(value)) {
|
|
256
|
+
issues.push(`${turnEntry.name}/${waveEntry.name}: malformed integration.json`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (value.state !== "integrated") {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const wave = Number(value.wave);
|
|
263
|
+
const featureIds = stringArray(value.feature_ids);
|
|
264
|
+
const changedPaths = stringArray(value.changed_paths);
|
|
265
|
+
if (!Number.isInteger(wave) || wave < 1 || !featureIds || !changedPaths) {
|
|
266
|
+
issues.push(`${turnEntry.name}/${waveEntry.name}: invalid integrated checkpoint`);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
waves.push({
|
|
270
|
+
turnId: typeof value.turn_id === "string" ? value.turn_id : turnId,
|
|
271
|
+
wave,
|
|
272
|
+
featureIds,
|
|
273
|
+
changedPaths,
|
|
274
|
+
rootDir: join(turnRoot, waveEntry.name),
|
|
275
|
+
integrationDir: join(turnRoot, waveEntry.name, "integration")
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
waves.sort((left, right) => (left.turnId.localeCompare(right.turnId)
|
|
280
|
+
|| left.wave - right.wave));
|
|
281
|
+
return { waves, issues, sawIntegrationRecord };
|
|
282
|
+
}
|
|
283
|
+
async function inspectReportEntry(root, changedPath) {
|
|
284
|
+
const safe = safeWorkspacePath(root, changedPath);
|
|
285
|
+
if (!safe) {
|
|
286
|
+
return { type: "unavailable", detail: "unsafe relative path" };
|
|
287
|
+
}
|
|
288
|
+
const segments = relative(resolve(root), safe).split(sep);
|
|
289
|
+
let parent = resolve(root);
|
|
290
|
+
for (const segment of segments.slice(0, -1)) {
|
|
291
|
+
parent = join(parent, segment);
|
|
292
|
+
try {
|
|
293
|
+
const stat = await lstat(parent);
|
|
294
|
+
if (stat.isSymbolicLink()) {
|
|
295
|
+
return { type: "unavailable", detail: "parent path is a symbolic link" };
|
|
296
|
+
}
|
|
297
|
+
if (!stat.isDirectory()) {
|
|
298
|
+
return { type: "missing" };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (isMissingError(error)) {
|
|
303
|
+
return { type: "missing" };
|
|
304
|
+
}
|
|
305
|
+
return { type: "unavailable", detail: errorMessage(error) };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const stat = await lstat(safe);
|
|
310
|
+
if (stat.isSymbolicLink()) {
|
|
311
|
+
return { type: "symlink", target: await readlink(safe) };
|
|
312
|
+
}
|
|
313
|
+
if (stat.isFile()) {
|
|
314
|
+
const content = await readFile(safe);
|
|
315
|
+
return {
|
|
316
|
+
type: "file",
|
|
317
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
318
|
+
mode: stat.mode & 0o777,
|
|
319
|
+
size: stat.size
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return { type: "other", detail: stat.isDirectory() ? "directory" : "special entry" };
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
return isMissingError(error)
|
|
326
|
+
? { type: "missing" }
|
|
327
|
+
: { type: "unavailable", detail: errorMessage(error) };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function safeWorkspacePath(root, changedPath) {
|
|
331
|
+
if (!changedPath || changedPath.includes("\0") || isAbsolute(changedPath)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const base = resolve(root);
|
|
335
|
+
const candidate = resolve(base, changedPath);
|
|
336
|
+
const relativePath = relative(base, candidate);
|
|
337
|
+
return relativePath
|
|
338
|
+
&& relativePath !== ".."
|
|
339
|
+
&& !relativePath.startsWith(`..${sep}`)
|
|
340
|
+
&& !isAbsolute(relativePath)
|
|
341
|
+
? candidate
|
|
342
|
+
: null;
|
|
343
|
+
}
|
|
344
|
+
function compareTaskReportEntries(expected, current) {
|
|
345
|
+
if (expected.type === "unavailable" || current.type === "unavailable") {
|
|
346
|
+
return "unavailable";
|
|
347
|
+
}
|
|
348
|
+
if (expected.type === "missing") {
|
|
349
|
+
return current.type === "missing" ? "match" : "unexpected";
|
|
350
|
+
}
|
|
351
|
+
if (current.type === "missing") {
|
|
352
|
+
return "missing";
|
|
353
|
+
}
|
|
354
|
+
if (expected.type !== current.type) {
|
|
355
|
+
return "drift";
|
|
356
|
+
}
|
|
357
|
+
if (expected.type === "file" && current.type === "file") {
|
|
358
|
+
return expected.sha256 === current.sha256 && expected.mode === current.mode ? "match" : "drift";
|
|
359
|
+
}
|
|
360
|
+
if (expected.type === "symlink" && current.type === "symlink") {
|
|
361
|
+
return expected.target === current.target ? "match" : "drift";
|
|
362
|
+
}
|
|
363
|
+
return expected.detail === current.detail ? "match" : "drift";
|
|
364
|
+
}
|
|
365
|
+
function emptyComparisonCounts() {
|
|
366
|
+
return { match: 0, drift: 0, missing: 0, unexpected: 0, unavailable: 0 };
|
|
367
|
+
}
|
|
368
|
+
function renderTaskReportMarkdown(report) {
|
|
369
|
+
const lines = [
|
|
370
|
+
`# Task Report: ${escapeMarkdownText(report.task.title)}`,
|
|
371
|
+
"",
|
|
372
|
+
`- Task: \`${escapeCode(report.task.id)}\``,
|
|
373
|
+
`- Status: \`${report.task.status}\``,
|
|
374
|
+
`- Mode: \`${report.task.mode}\``,
|
|
375
|
+
`- Created: ${report.task.created_at}`,
|
|
376
|
+
`- Generated: ${report.generated_at}`,
|
|
377
|
+
`- Workspace: \`${escapeCode(report.workspace.path)}\``,
|
|
378
|
+
"",
|
|
379
|
+
"## Workspace Reconciliation",
|
|
380
|
+
"",
|
|
381
|
+
`State: **${report.workspace.reconciliation.state}**. `
|
|
382
|
+
+ `${report.workspace.reconciliation.integrated_waves} integrated wave(s), `
|
|
383
|
+
+ `${report.workspace.reconciliation.changed_paths} changed path(s).`,
|
|
384
|
+
""
|
|
385
|
+
];
|
|
386
|
+
if (report.workspace.reconciliation.paths.length > 0) {
|
|
387
|
+
lines.push("| State | Path | Source | Expected | Current |", "| --- | --- | --- | --- | --- |", ...report.workspace.reconciliation.paths.map((entry) => (`| ${entry.state} | \`${escapeCode(entry.path)}\` | turn ${entry.source.turn_id}, wave ${entry.source.wave} | `
|
|
388
|
+
+ `${describeReportEntry(entry.expected)} | ${describeReportEntry(entry.current)} |`)), "");
|
|
389
|
+
}
|
|
390
|
+
if (report.workspace.reconciliation.issues.length > 0) {
|
|
391
|
+
lines.push("### Reconciliation Issues", "", ...report.workspace.reconciliation.issues.map((issue) => `- ${escapeMarkdownText(issue)}`), "");
|
|
392
|
+
}
|
|
393
|
+
lines.push("## Turns", "");
|
|
394
|
+
if (report.turns.length === 0) {
|
|
395
|
+
lines.push("No valid turn records were found.", "");
|
|
396
|
+
}
|
|
397
|
+
for (const turn of report.turns) {
|
|
398
|
+
lines.push(`### Turn ${turn.turn_id}`, "", `- Created: ${turn.created_at}`, ...(turn.route ? [
|
|
399
|
+
`- Route: \`${turn.route.mode}\` via \`${turn.route.source ?? "configured"}\``,
|
|
400
|
+
`- Reason: ${escapeMarkdownText(turn.route.reason)}`
|
|
401
|
+
] : []), "", "**Request**", "", ...(turn.request ? quoteMarkdown(turn.request) : ["> (empty)"]), "");
|
|
402
|
+
if (turn.supervisor_summary) {
|
|
403
|
+
lines.push("**Supervisor summary**", "", turn.supervisor_summary, "");
|
|
404
|
+
}
|
|
405
|
+
for (const [label, value] of [
|
|
406
|
+
["Requirements", turn.requirements],
|
|
407
|
+
["Plan", turn.plan],
|
|
408
|
+
["Acceptance criteria", turn.acceptance_criteria]
|
|
409
|
+
]) {
|
|
410
|
+
if (value) {
|
|
411
|
+
lines.push(`**${label}**`, "", value, "");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const [label, value] of [
|
|
415
|
+
["Feature plan", turn.feature_plan],
|
|
416
|
+
["Judge validation", turn.judge_validation],
|
|
417
|
+
["Completion contract", turn.completion_contract],
|
|
418
|
+
["Final acceptance", turn.final_acceptance],
|
|
419
|
+
["Final acceptance validation", turn.final_acceptance_validation]
|
|
420
|
+
]) {
|
|
421
|
+
if (value !== undefined) {
|
|
422
|
+
lines.push(`**${label}**`, "", fencedJson(value), "");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
lines.push("## Features", "");
|
|
427
|
+
if (report.features.length === 0) {
|
|
428
|
+
lines.push("No Feature records were found.", "");
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
lines.push("| State | Turn | Feature | Actor | Critic | Findings | Replies | Latest review evidence |", "| --- | --- | --- | --- | --- | ---: | ---: | --- |", ...report.features.map((feature) => (`| ${feature.state} | ${feature.turn_id} | ${escapeTableText(feature.title)} (\`${escapeCode(feature.id)}\`) | `
|
|
432
|
+
+ `${escapeTableText(workerAssignment(feature.actor_engine, feature.actor_model))} | `
|
|
433
|
+
+ `${escapeTableText(workerAssignment(feature.critic_engine, feature.critic_model))} | `
|
|
434
|
+
+ `${feature.findings} | ${feature.replies} | `
|
|
435
|
+
+ `${escapeTableText(feature.latest_finding ?? feature.latest_reply ?? "-")} |`)), "");
|
|
436
|
+
}
|
|
437
|
+
lines.push("## Integration And Verification", "");
|
|
438
|
+
if (report.integrations.length === 0) {
|
|
439
|
+
lines.push("No integrated Wave records were found.", "");
|
|
440
|
+
}
|
|
441
|
+
for (const integration of report.integrations) {
|
|
442
|
+
lines.push(`### Turn ${integration.turn_id}, Wave ${integration.wave}`, "", `- Features: ${integration.feature_ids.map((id) => `\`${escapeCode(id)}\``).join(", ") || "none"}`, `- Changed paths: ${integration.changed_paths.length}`, ...integration.changed_paths.map((path) => ` - \`${escapeCode(path)}\``), "");
|
|
443
|
+
if (integration.verification_review) {
|
|
444
|
+
lines.push("**Verification review**", "", integration.verification_review, "");
|
|
445
|
+
}
|
|
446
|
+
if (integration.verification !== undefined) {
|
|
447
|
+
lines.push("**Verification evidence**", "", fencedJson(integration.verification), "");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
lines.push("## Workers", "");
|
|
451
|
+
if (report.workers.length === 0) {
|
|
452
|
+
lines.push("No Worker records were found.", "");
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
lines.push("| State | Turn | Role | Engine / model | Feature | Native session | Worker | Summary |", "| --- | --- | --- | --- | --- | --- | --- | --- |", ...report.workers.map((worker) => (`| ${worker.state} | ${worker.turn_id} | ${worker.role} | `
|
|
456
|
+
+ `${escapeTableText(workerAssignment(worker.engine, worker.model, worker.model_provider))} | `
|
|
457
|
+
+ `${escapeTableText(worker.feature_title ?? worker.feature_id ?? "-")} | `
|
|
458
|
+
+ `${escapeTableText(worker.native_session?.id ?? "-")} | \`${escapeCode(worker.id)}\` | `
|
|
459
|
+
+ `${escapeTableText(worker.summary || "-")} |`)), "");
|
|
460
|
+
}
|
|
461
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
462
|
+
}
|
|
463
|
+
function quoteMarkdown(value) {
|
|
464
|
+
return value.split(/\r?\n/).map((line) => `> ${line}`);
|
|
465
|
+
}
|
|
466
|
+
function fencedJson(value) {
|
|
467
|
+
const json = JSON.stringify(value, null, 2);
|
|
468
|
+
const longestRun = Math.max(0, ...Array.from(json.matchAll(/`+/g), (match) => match[0].length));
|
|
469
|
+
const fence = "`".repeat(Math.max(3, longestRun + 1));
|
|
470
|
+
return `${fence}json\n${json}\n${fence}`;
|
|
471
|
+
}
|
|
472
|
+
function describeReportEntry(entry) {
|
|
473
|
+
if (entry.type === "file") {
|
|
474
|
+
return `file ${entry.sha256?.slice(0, 12) ?? "?"} mode ${entry.mode?.toString(8) ?? "?"}`;
|
|
475
|
+
}
|
|
476
|
+
if (entry.type === "symlink") {
|
|
477
|
+
return `symlink \`${escapeCode(entry.target ?? "")}\``;
|
|
478
|
+
}
|
|
479
|
+
return entry.detail ? `${entry.type}: ${escapeTableText(entry.detail)}` : entry.type;
|
|
480
|
+
}
|
|
481
|
+
function workerAssignment(engine, model, provider) {
|
|
482
|
+
if (!engine) {
|
|
483
|
+
return "-";
|
|
484
|
+
}
|
|
485
|
+
const configured = [provider, model].filter(Boolean).join("/");
|
|
486
|
+
return configured ? `${engine} (${configured})` : engine;
|
|
487
|
+
}
|
|
488
|
+
function escapeMarkdownText(value) {
|
|
489
|
+
return value.replace(/[\\`*_{}[\]()#+.!|>-]/g, "\\$&").replace(/\r?\n/g, " ");
|
|
490
|
+
}
|
|
491
|
+
function escapeTableText(value) {
|
|
492
|
+
return escapeMarkdownText(value).replace(/\r?\n/g, " ");
|
|
493
|
+
}
|
|
494
|
+
function escapeCode(value) {
|
|
495
|
+
return value.replaceAll("`", "\\`").replace(/\r?\n/g, " ");
|
|
496
|
+
}
|
|
497
|
+
function isRecord(value) {
|
|
498
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
499
|
+
}
|
|
500
|
+
function stringArray(value) {
|
|
501
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
502
|
+
? [...value]
|
|
503
|
+
: null;
|
|
504
|
+
}
|
|
505
|
+
function isMissingError(error) {
|
|
506
|
+
return ["ENOENT", "ENOTDIR"].includes(error.code ?? "");
|
|
507
|
+
}
|
|
508
|
+
function errorMessage(error) {
|
|
509
|
+
return error instanceof Error ? error.message : String(error);
|
|
510
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export const TASK_SEARCH_FIELDS = [
|
|
2
|
+
"task",
|
|
3
|
+
"turn",
|
|
4
|
+
"feature",
|
|
5
|
+
"role",
|
|
6
|
+
"provider",
|
|
7
|
+
"model",
|
|
8
|
+
"state"
|
|
9
|
+
];
|
|
10
|
+
const TASK_SEARCH_FIELD_SET = new Set(TASK_SEARCH_FIELDS);
|
|
11
|
+
export function parseTaskSearchQuery(query) {
|
|
12
|
+
return tokenizeTaskSearchQuery(query)
|
|
13
|
+
.map((token) => {
|
|
14
|
+
const separator = token.indexOf(":");
|
|
15
|
+
if (separator > 0) {
|
|
16
|
+
const field = normalizeTaskSearchValue(token.slice(0, separator));
|
|
17
|
+
const value = normalizeTaskSearchValue(token.slice(separator + 1));
|
|
18
|
+
if (TASK_SEARCH_FIELD_SET.has(field) && value) {
|
|
19
|
+
return { field: field, value };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const value = normalizeTaskSearchValue(token);
|
|
23
|
+
return value ? { field: "any", value } : null;
|
|
24
|
+
})
|
|
25
|
+
.filter((term) => term !== null);
|
|
26
|
+
}
|
|
27
|
+
export function matchTaskSearchDocument(query, document) {
|
|
28
|
+
const terms = typeof query === "string" ? parseTaskSearchQuery(query) : [...query];
|
|
29
|
+
if (terms.length === 0) {
|
|
30
|
+
return { fields: [], summary: "" };
|
|
31
|
+
}
|
|
32
|
+
const candidates = taskSearchCandidates(document);
|
|
33
|
+
const matches = [];
|
|
34
|
+
for (const term of terms) {
|
|
35
|
+
const match = candidates.find((candidate) => ((term.field === "any" || candidate.field === term.field)
|
|
36
|
+
&& normalizeTaskSearchValue(candidate.value).includes(term.value)));
|
|
37
|
+
if (!match) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
matches.push(match);
|
|
41
|
+
}
|
|
42
|
+
const fields = [...new Set(matches
|
|
43
|
+
.map((match) => match.field)
|
|
44
|
+
.filter((field) => field !== "any"))];
|
|
45
|
+
const summaries = [...new Set(matches.map((match) => match.summary).filter(Boolean))].slice(0, 2);
|
|
46
|
+
return {
|
|
47
|
+
fields,
|
|
48
|
+
summary: summaries.length > 0 ? `match · ${summaries.join(" · ")}` : "match"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function taskSearchCandidates(document) {
|
|
52
|
+
const candidates = [
|
|
53
|
+
candidate("task", document.task.id, `task ${compactSearchEvidence(document.task.id)}`),
|
|
54
|
+
candidate("task", document.task.title, `task ${compactSearchEvidence(document.task.title)}`),
|
|
55
|
+
candidate("task", document.task.cwd, `task ${compactSearchEvidence(document.task.cwd)}`),
|
|
56
|
+
candidate("task", document.task.mode, `task ${compactSearchEvidence(document.task.mode)}`),
|
|
57
|
+
candidate("state", document.task.state, `state ${compactSearchEvidence(document.task.state)}`)
|
|
58
|
+
];
|
|
59
|
+
for (const turn of document.turns) {
|
|
60
|
+
const label = numericTurnLabel(turn.turnId);
|
|
61
|
+
candidates.push(candidate("turn", turn.turnId, label));
|
|
62
|
+
candidates.push(candidate("turn", String(Number(turn.turnId)), label));
|
|
63
|
+
if (turn.request.trim()) {
|
|
64
|
+
candidates.push(candidate("turn", turn.request, `${label} ${compactSearchEvidence(turn.request, 72)}`));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const worker of document.workers) {
|
|
68
|
+
if (worker.featureId) {
|
|
69
|
+
candidates.push(candidate("feature", worker.featureId, `feature ${compactSearchEvidence(worker.featureTitle || worker.featureId)}`));
|
|
70
|
+
}
|
|
71
|
+
if (worker.featureTitle) {
|
|
72
|
+
candidates.push(candidate("feature", worker.featureTitle, `feature ${compactSearchEvidence(worker.featureTitle)}`));
|
|
73
|
+
}
|
|
74
|
+
candidates.push(candidate("role", worker.role, `role ${compactSearchEvidence(worker.role)}`));
|
|
75
|
+
candidates.push(candidate("provider", worker.provider, `provider ${compactSearchEvidence(worker.provider)}`));
|
|
76
|
+
if (worker.modelProvider) {
|
|
77
|
+
candidates.push(candidate("provider", worker.modelProvider, `provider ${compactSearchEvidence(worker.modelProvider)}`));
|
|
78
|
+
}
|
|
79
|
+
if (worker.model) {
|
|
80
|
+
candidates.push(candidate("model", worker.model, `model ${compactSearchEvidence(worker.model)}`));
|
|
81
|
+
}
|
|
82
|
+
candidates.push(candidate("state", worker.state, `state ${compactSearchEvidence(worker.state)}`));
|
|
83
|
+
candidates.push(candidate("any", worker.id, `worker ${compactSearchEvidence(worker.id)}`));
|
|
84
|
+
candidates.push(candidate("any", worker.phase, `phase ${compactSearchEvidence(worker.phase)}`));
|
|
85
|
+
if (worker.summary) {
|
|
86
|
+
candidates.push(candidate("any", worker.summary, `worker ${compactSearchEvidence(worker.summary, 72)}`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const session of document.nativeSessions) {
|
|
90
|
+
candidates.push(candidate("any", session.sessionId, `session ${compactSearchEvidence(session.sessionId)}`));
|
|
91
|
+
candidates.push(candidate("provider", session.provider, `provider ${compactSearchEvidence(session.provider)}`));
|
|
92
|
+
}
|
|
93
|
+
return candidates.filter((item) => item.value.trim());
|
|
94
|
+
}
|
|
95
|
+
function candidate(field, value, summary) {
|
|
96
|
+
return { field, value, summary };
|
|
97
|
+
}
|
|
98
|
+
function tokenizeTaskSearchQuery(query) {
|
|
99
|
+
const tokens = [];
|
|
100
|
+
let current = "";
|
|
101
|
+
let quoted = false;
|
|
102
|
+
for (const character of Array.from(query).slice(0, 1000)) {
|
|
103
|
+
if (character === '"') {
|
|
104
|
+
quoted = !quoted;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (/\s/u.test(character) && !quoted) {
|
|
108
|
+
if (current) {
|
|
109
|
+
tokens.push(current);
|
|
110
|
+
current = "";
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
current += character;
|
|
115
|
+
}
|
|
116
|
+
if (current) {
|
|
117
|
+
tokens.push(current);
|
|
118
|
+
}
|
|
119
|
+
return tokens;
|
|
120
|
+
}
|
|
121
|
+
function normalizeTaskSearchValue(value) {
|
|
122
|
+
return value
|
|
123
|
+
.normalize("NFKC")
|
|
124
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
125
|
+
.replace(/\s+/g, " ")
|
|
126
|
+
.trim()
|
|
127
|
+
.toLowerCase();
|
|
128
|
+
}
|
|
129
|
+
function numericTurnLabel(turnId) {
|
|
130
|
+
const numeric = Number(turnId);
|
|
131
|
+
return `turn ${Number.isInteger(numeric) ? numeric : compactSearchEvidence(turnId)}`;
|
|
132
|
+
}
|
|
133
|
+
function compactSearchEvidence(value, maxLength = 48) {
|
|
134
|
+
const clean = value
|
|
135
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
136
|
+
.replace(/\s+/g, " ")
|
|
137
|
+
.trim();
|
|
138
|
+
const points = Array.from(clean);
|
|
139
|
+
return points.length > maxLength
|
|
140
|
+
? `${points.slice(0, Math.max(1, maxLength - 3)).join("")}...`
|
|
141
|
+
: clean;
|
|
142
|
+
}
|
package/dist/domain/schemas.js
CHANGED
|
@@ -170,10 +170,19 @@ export const FeatureStatusSchema = z.object({
|
|
|
170
170
|
state: FeatureStateSchema,
|
|
171
171
|
updated_at: z.string().datetime()
|
|
172
172
|
});
|
|
173
|
+
export const WorkerModelNameSchema = z.string()
|
|
174
|
+
.max(200)
|
|
175
|
+
.refine((value) => !/[\u0000-\u001f\u007f]/.test(value), {
|
|
176
|
+
message: "Model name cannot contain control characters"
|
|
177
|
+
});
|
|
173
178
|
export const FeatureAssignmentSchema = z.object({
|
|
174
179
|
version: z.literal(1),
|
|
175
180
|
actor_engine: EngineNameSchema,
|
|
176
181
|
critic_engine: EngineNameSchema,
|
|
182
|
+
actor_model: WorkerModelNameSchema.default(""),
|
|
183
|
+
critic_model: WorkerModelNameSchema.default(""),
|
|
184
|
+
actor_override: z.boolean().default(false),
|
|
185
|
+
critic_override: z.boolean().default(false),
|
|
177
186
|
updated_at: z.string().datetime()
|
|
178
187
|
});
|
|
179
188
|
export const TurnMetaSchema = z.object({
|