iterate-plugin 3.8.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/README.zh-CN.md +4 -3
- package/dist/index.js +9 -4
- package/dist/report-render.js +452 -0
- package/dist/session-record.js +246 -0
- package/dist/skill-prompt.js +2 -1
- package/dist/tools/report.js +239 -0
- package/dist/tools/transcript.js +186 -9
- package/lib/client.js +453 -13
- package/package.json +17 -10
- package/src/client/i18n.ts +6 -0
- package/src/client/index.ts +44 -0
- package/src/index.ts +9 -4
- package/src/report-render.ts +578 -0
- package/src/session-record.ts +333 -0
- package/src/skill-prompt.ts +2 -1
- package/src/tools/report.ts +244 -0
- package/src/tools/transcript.ts +199 -9
package/dist/tools/transcript.js
CHANGED
|
@@ -30,6 +30,76 @@ import { transcriptPath } from "../paths.js";
|
|
|
30
30
|
import { ReviewTranscriptBuilder, MAX_TIMELINE, MAX_ROUNDS, normalizeManifestBounds } from "../transcript.js";
|
|
31
31
|
import { readDecisionLogDetailed } from "./decision-log.js";
|
|
32
32
|
import { readLive } from "../live.js";
|
|
33
|
+
import { buildRunRecord, appendRunRecord, readRunRecords, sessionOf, } from "../session-record.js";
|
|
34
|
+
/** Severity tally over a manifest's (globally deduped) findings. */
|
|
35
|
+
function severityCounts(findings) {
|
|
36
|
+
const counts = { total: 0, critical: 0, high: 0, medium: 0, low: 0 };
|
|
37
|
+
if (!Array.isArray(findings))
|
|
38
|
+
return counts;
|
|
39
|
+
for (const f of findings) {
|
|
40
|
+
if (!f || typeof f !== 'object')
|
|
41
|
+
continue;
|
|
42
|
+
const sev = f.severity;
|
|
43
|
+
counts.total++;
|
|
44
|
+
if (sev === 'critical')
|
|
45
|
+
counts.critical++;
|
|
46
|
+
else if (sev === 'high')
|
|
47
|
+
counts.high++;
|
|
48
|
+
else if (sev === 'medium')
|
|
49
|
+
counts.medium++;
|
|
50
|
+
else
|
|
51
|
+
counts.low++;
|
|
52
|
+
}
|
|
53
|
+
return counts;
|
|
54
|
+
}
|
|
55
|
+
/** Build the compact session-linked record from a finished manifest. */
|
|
56
|
+
function runRecordFromManifest(manifest, qualityStatus = null) {
|
|
57
|
+
return buildRunRecord({
|
|
58
|
+
project: manifest.project,
|
|
59
|
+
goal: manifest.goal,
|
|
60
|
+
mode: manifest.mode,
|
|
61
|
+
taskMode: manifest.taskMode,
|
|
62
|
+
round: manifest.round,
|
|
63
|
+
maxRounds: manifest.maxRounds,
|
|
64
|
+
active: manifest.active,
|
|
65
|
+
stoppedReason: manifest.stoppedReason,
|
|
66
|
+
convergence: manifest.convergence,
|
|
67
|
+
counts: severityCounts(manifest.findings),
|
|
68
|
+
fixedCount: Array.isArray(manifest.fixes) ? manifest.fixes.filter((f) => f.success !== false).length : 0,
|
|
69
|
+
qualityStatus,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Rebuild a (summary-only) transcript manifest from a session-linked record.
|
|
74
|
+
*
|
|
75
|
+
* Degraded by design: the record carries the run SUMMARY (mode / goal / round /
|
|
76
|
+
* max-rounds / end state / convergence series), not the per-finding detail, so
|
|
77
|
+
* the recovered manifest has an empty `findings`/`fixes`/`timeline`. It exists
|
|
78
|
+
* so the observatory shows the real run header + trend after `.iterate/` is
|
|
79
|
+
* lost, instead of a blank panel. Round rows stay empty because the record has
|
|
80
|
+
* no per-thread narration.
|
|
81
|
+
*/
|
|
82
|
+
export function recoverManifestFromRecord(record, fallbackProject, approval) {
|
|
83
|
+
const builder = new ReviewTranscriptBuilder({
|
|
84
|
+
project: record.project || fallbackProject,
|
|
85
|
+
mode: record.mode,
|
|
86
|
+
taskMode: record.taskMode,
|
|
87
|
+
approval,
|
|
88
|
+
goal: record.goal,
|
|
89
|
+
maxRounds: record.maxRounds,
|
|
90
|
+
});
|
|
91
|
+
const series = Array.isArray(record.convergence) ? record.convergence : [];
|
|
92
|
+
for (let i = 0; i < Math.min(series.length, MAX_ROUNDS); i += 1) {
|
|
93
|
+
const n = series[i];
|
|
94
|
+
if (typeof n === 'number' && Number.isFinite(n) && n >= 0)
|
|
95
|
+
builder.snapshotConvergence(i + 1, n);
|
|
96
|
+
}
|
|
97
|
+
if (record.round > 0)
|
|
98
|
+
builder.advanceRound(record.round, record.maxRounds);
|
|
99
|
+
if (!record.active)
|
|
100
|
+
builder.finish(record.stoppedReason ?? undefined);
|
|
101
|
+
return builder.serialize();
|
|
102
|
+
}
|
|
33
103
|
/** Build per-dimension threads for one round from its (dimension-tagged) findings. */
|
|
34
104
|
function captureRound(builder, round) {
|
|
35
105
|
if (!round || typeof round !== 'object')
|
|
@@ -143,26 +213,36 @@ function isManifestShaped(value) {
|
|
|
143
213
|
export function registerTranscriptTool(ctx) {
|
|
144
214
|
ctx.tools.register(defineTool({
|
|
145
215
|
name: 'iterate_transcript',
|
|
146
|
-
// `read` never
|
|
147
|
-
// read
|
|
148
|
-
isConcurrencySafe: (args) =>
|
|
216
|
+
// `read`/`recall` never write; `capture`/`nudge` persist transcript state
|
|
217
|
+
// → only the read shapes join a parallel dispatch group.
|
|
218
|
+
isConcurrencySafe: (args) => {
|
|
219
|
+
const op = args.operation;
|
|
220
|
+
return op === 'read' || op === 'recall';
|
|
221
|
+
},
|
|
149
222
|
description: 'Runtime-observatory transcript for the iterate workflow. ' +
|
|
150
223
|
'`read` returns the current persisted transcript manifest (per-reviewer threads, ' +
|
|
151
224
|
'convergence series, findings, fixes, checkpoint, timeline, and any steering nudge ' +
|
|
152
225
|
'written for the next round) — or, when the file is missing/malformed, a structured ' +
|
|
153
|
-
'empty view (`found:false` plus a blank manifest). ' +
|
|
226
|
+
'empty view (`found:false` plus a blank manifest). When the on-disk file is gone but the ' +
|
|
227
|
+
'session carries a linked run record (dsh ≥ 0.2.1-rc.1), `read` RECOVERS a summary manifest ' +
|
|
228
|
+
'from that record and sets `recovered:true`. ' +
|
|
154
229
|
'`capture` builds a fresh transcript from the review `rounds` + `report`, folds in the ' +
|
|
155
230
|
'decision-log timeline, and persists it (call once after the final aggregate so the UI ' +
|
|
156
231
|
'reflects the run; skipped when `observatory.capture` is disabled). Round validation outcomes ' +
|
|
157
232
|
'can be carried on capture via `validations` ({round, command, exitCode, allowed, rejectReason?}). ' +
|
|
233
|
+
'On a successful capture the tool ALSO appends a compact run record onto the dsh session log ' +
|
|
234
|
+
'(best-effort; reported as `sessionRecord`) so the run survives `.iterate/` loss and travels with ' +
|
|
235
|
+
'the session. ' +
|
|
236
|
+
'`recall` lists the session-linked run records (newest first) — useful to audit a run after ' +
|
|
237
|
+
'restart or to confirm the write took. ' +
|
|
158
238
|
'`nudge` sets (text) or clears (text:null or an empty string) steering text the next round\'s reviewers read. ' +
|
|
159
239
|
'Purely local and deterministic — never touches source files.',
|
|
160
240
|
parameters: {
|
|
161
241
|
operation: {
|
|
162
242
|
type: 'string',
|
|
163
243
|
required: true,
|
|
164
|
-
description: '"read" to fetch the manifest, "capture" to persist one, "nudge" to set steering text.',
|
|
165
|
-
enum: ['read', 'capture', 'nudge'],
|
|
244
|
+
description: '"read" to fetch the manifest, "capture" to persist one, "recall" to list session-linked run records, "nudge" to set steering text.',
|
|
245
|
+
enum: ['read', 'capture', 'recall', 'nudge'],
|
|
166
246
|
},
|
|
167
247
|
rounds: {
|
|
168
248
|
type: 'json',
|
|
@@ -244,6 +324,19 @@ export function registerTranscriptTool(ctx) {
|
|
|
244
324
|
description: 'For `capture`: true when nothing was persisted because `observatory.capture` is disabled.',
|
|
245
325
|
},
|
|
246
326
|
reason: { type: 'string', description: 'For a skipped `capture`: why persistence was skipped.' },
|
|
327
|
+
recovered: {
|
|
328
|
+
type: 'boolean',
|
|
329
|
+
description: 'For `read`: true when the manifest was rebuilt from a session-linked run record because the on-disk file was missing or unreadable.',
|
|
330
|
+
},
|
|
331
|
+
supported: {
|
|
332
|
+
type: 'boolean',
|
|
333
|
+
description: 'For `recall`: whether the host exposes the dsh session state-record API (dsh ≥ 0.2.1-rc.1).',
|
|
334
|
+
},
|
|
335
|
+
records: { type: 'json', description: 'For `recall`: session-linked run records, newest first.' },
|
|
336
|
+
sessionRecord: {
|
|
337
|
+
type: 'json',
|
|
338
|
+
description: 'For `capture`/`read`: the session-link outcome/position (`{linked, seq}` on capture, `{seq, updatedAt}` on a recovery).',
|
|
339
|
+
},
|
|
247
340
|
error: { type: 'string' },
|
|
248
341
|
},
|
|
249
342
|
},
|
|
@@ -273,13 +366,59 @@ export function registerTranscriptTool(ctx) {
|
|
|
273
366
|
approval,
|
|
274
367
|
}).serialize(),
|
|
275
368
|
});
|
|
276
|
-
|
|
369
|
+
// Recovery (dsh ≥ 0.2.1-rc.1): when the on-disk manifest is gone
|
|
370
|
+
// but the SESSION that produced the run carries a linked run record,
|
|
371
|
+
// rebuild a summary manifest so the observatory shows the real run
|
|
372
|
+
// header + trend instead of a blank panel. Best-effort: no session,
|
|
373
|
+
// no API, or no record all fall back to `emptyView()`.
|
|
374
|
+
const recover = async () => {
|
|
375
|
+
const session = sessionOf(exec);
|
|
376
|
+
if (!session)
|
|
377
|
+
return null;
|
|
378
|
+
const { supported, records } = await readRunRecords(session);
|
|
379
|
+
if (!supported || records.length === 0)
|
|
380
|
+
return null;
|
|
381
|
+
const latest = records[records.length - 1];
|
|
382
|
+
return {
|
|
383
|
+
transcript: normalizeManifestBounds(recoverManifestFromRecord(latest, projectRoot, approval)),
|
|
384
|
+
sessionRecord: {
|
|
385
|
+
seq: latest.seq,
|
|
386
|
+
time: latest.time,
|
|
387
|
+
updatedAt: latest.updatedAt,
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
if (!existsSync(file)) {
|
|
392
|
+
const recovered = await recover();
|
|
393
|
+
if (recovered) {
|
|
394
|
+
return {
|
|
395
|
+
operation: 'read',
|
|
396
|
+
found: true,
|
|
397
|
+
recovered: true,
|
|
398
|
+
live: live,
|
|
399
|
+
transcript: recovered.transcript,
|
|
400
|
+
sessionRecord: recovered.sessionRecord,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
277
403
|
return emptyView();
|
|
404
|
+
}
|
|
278
405
|
let parsed;
|
|
279
406
|
try {
|
|
280
407
|
parsed = JSON.parse(await readFile(file, 'utf-8'));
|
|
281
408
|
}
|
|
282
409
|
catch (err) {
|
|
410
|
+
const recovered = await recover();
|
|
411
|
+
if (recovered) {
|
|
412
|
+
return {
|
|
413
|
+
operation: 'read',
|
|
414
|
+
found: true,
|
|
415
|
+
recovered: true,
|
|
416
|
+
live: live,
|
|
417
|
+
transcript: recovered.transcript,
|
|
418
|
+
sessionRecord: recovered.sessionRecord,
|
|
419
|
+
error: `Recovered a summary from the session log; on-disk transcript was unreadable (${err instanceof Error ? err.message : String(err)}).`,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
283
422
|
return {
|
|
284
423
|
...emptyView(),
|
|
285
424
|
error: `Failed to read transcript: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -289,6 +428,18 @@ export function registerTranscriptTool(ctx) {
|
|
|
289
428
|
// `JSON.parse` happily accepts `null`, `[]` and `{}` — values that
|
|
290
429
|
// are parseable but carry no manifest. Surfacing them as
|
|
291
430
|
// `found:true` made the observatory render junk panels.
|
|
431
|
+
const recovered = await recover();
|
|
432
|
+
if (recovered) {
|
|
433
|
+
return {
|
|
434
|
+
operation: 'read',
|
|
435
|
+
found: true,
|
|
436
|
+
recovered: true,
|
|
437
|
+
live: live,
|
|
438
|
+
transcript: recovered.transcript,
|
|
439
|
+
sessionRecord: recovered.sessionRecord,
|
|
440
|
+
error: 'Recovered a summary from the session log; on-disk file is not a valid manifest.',
|
|
441
|
+
};
|
|
442
|
+
}
|
|
292
443
|
return {
|
|
293
444
|
...emptyView(),
|
|
294
445
|
error: 'Transcript file is not a valid manifest; returning an empty view.',
|
|
@@ -304,6 +455,21 @@ export function registerTranscriptTool(ctx) {
|
|
|
304
455
|
transcript: normalizeManifestBounds(parsed),
|
|
305
456
|
};
|
|
306
457
|
}
|
|
458
|
+
if (args.operation === 'recall') {
|
|
459
|
+
// New capability: list the run records this plugin appended onto the
|
|
460
|
+
// dsh session log (dsh ≥ 0.2.1-rc.1). Read-only; degrades to
|
|
461
|
+
// `supported:false` on a host without the API.
|
|
462
|
+
const session = sessionOf(exec);
|
|
463
|
+
const { supported, records, error } = await readRunRecords(session);
|
|
464
|
+
const newestFirst = [...records].reverse();
|
|
465
|
+
return {
|
|
466
|
+
operation: 'recall',
|
|
467
|
+
found: newestFirst.length > 0,
|
|
468
|
+
supported,
|
|
469
|
+
records: newestFirst,
|
|
470
|
+
...(error ? { error } : {}),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
307
473
|
if (args.operation === 'nudge') {
|
|
308
474
|
// Read-modify-write across processes: a concurrent nudge in a sibling
|
|
309
475
|
// process would otherwise read the same manifest and write back over
|
|
@@ -480,17 +646,28 @@ export function registerTranscriptTool(ctx) {
|
|
|
480
646
|
builder.finish('converged');
|
|
481
647
|
else if (roundsExecuted > 0)
|
|
482
648
|
builder.finish('max_rounds_reached');
|
|
483
|
-
const
|
|
649
|
+
const manifest = builder.serialize();
|
|
650
|
+
const persisted = await persistChecked(file, manifest);
|
|
484
651
|
if (!persisted.ok) {
|
|
485
652
|
return { operation: 'capture', found: true, updated: false, error: persisted.error };
|
|
486
653
|
}
|
|
654
|
+
// Session link (dsh ≥ 0.2.1-rc.1): append a compact run record onto the
|
|
655
|
+
// session log so the run survives `.iterate/` loss and travels with the
|
|
656
|
+
// session. Best-effort — a link failure is reported, never fatal (the
|
|
657
|
+
// on-disk capture above already succeeded).
|
|
658
|
+
let sessionRecord;
|
|
659
|
+
const linkSession = await appendRunRecord(sessionOf(exec), runRecordFromManifest(manifest));
|
|
660
|
+
sessionRecord = (linkSession.ok
|
|
661
|
+
? { linked: true, seq: linkSession.seq }
|
|
662
|
+
: { linked: false, supported: linkSession.supported, reason: linkSession.error });
|
|
487
663
|
const live = await readLive(projectRoot);
|
|
488
664
|
return {
|
|
489
665
|
operation: 'capture',
|
|
490
666
|
found: true,
|
|
491
667
|
updated: true,
|
|
492
668
|
live: live,
|
|
493
|
-
transcript:
|
|
669
|
+
transcript: manifest,
|
|
670
|
+
sessionRecord,
|
|
494
671
|
};
|
|
495
672
|
},
|
|
496
673
|
}));
|