iterate-plugin 3.5.6 → 3.6.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 +64 -20
- package/README.zh-CN.md +53 -17
- package/dist/approval-gate.js +143 -23
- package/dist/atomic-fs.js +148 -17
- package/dist/config-loader.js +509 -34
- package/dist/config-write.js +83 -22
- package/dist/evidence.js +141 -87
- package/dist/file-lock.js +165 -0
- package/dist/index.js +54 -28
- package/dist/jobs.js +56 -11
- package/dist/live.js +52 -13
- package/dist/meta-review.js +132 -28
- package/dist/method-scope.js +31 -8
- package/dist/review-scope.js +32 -9
- package/dist/review.js +96 -17
- package/dist/session-hooks.js +55 -13
- package/dist/skill-prompt.js +417 -67
- package/dist/tools/checkpoint.js +85 -19
- package/dist/tools/config.js +44 -2
- package/dist/tools/context.js +22 -5
- package/dist/tools/decision-log.js +122 -27
- package/dist/tools/defense-events.js +90 -31
- package/dist/tools/defense-store.js +28 -3
- package/dist/tools/experience-bank.js +108 -43
- package/dist/tools/experience-store.js +54 -4
- package/dist/tools/fix.js +347 -119
- package/dist/tools/history.js +68 -14
- package/dist/tools/present.js +80 -0
- package/dist/tools/prune.js +156 -53
- package/dist/tools/quality-gate.js +52 -2
- package/dist/tools/quality-store.js +17 -1
- package/dist/tools/review.js +158 -7
- package/dist/tools/transcript.js +173 -36
- package/dist/tools/triage.js +67 -12
- package/dist/tools/validate.js +115 -24
- package/dist/transcript.js +267 -4
- package/lib/client.js +945 -181
- package/lib/parse.js +760 -57
- package/package.json +15 -12
- package/src/approval-gate.ts +160 -23
- package/src/atomic-fs.ts +152 -17
- package/src/client/index.ts +600 -129
- package/src/config-loader.ts +542 -35
- package/src/config-write.ts +108 -26
- package/src/evidence.ts +194 -92
- package/src/file-lock.ts +184 -0
- package/src/index.ts +53 -29
- package/src/jobs.ts +63 -12
- package/src/live.ts +48 -11
- package/src/meta-review.ts +151 -34
- package/src/method-scope.ts +31 -8
- package/src/review-scope.ts +33 -10
- package/src/review.ts +96 -17
- package/src/session-hooks.ts +54 -12
- package/src/skill-prompt.ts +417 -67
- package/src/tools/checkpoint.ts +81 -19
- package/src/tools/config.ts +44 -2
- package/src/tools/context.ts +25 -5
- package/src/tools/decision-log.ts +127 -22
- package/src/tools/defense-events.ts +87 -31
- package/src/tools/defense-store.ts +30 -3
- package/src/tools/experience-bank.ts +112 -44
- package/src/tools/experience-store.ts +54 -5
- package/src/tools/fix.ts +258 -20
- package/src/tools/history.ts +74 -15
- package/src/tools/present.ts +89 -0
- package/src/tools/prune.ts +160 -51
- package/src/tools/quality-gate.ts +57 -2
- package/src/tools/quality-store.ts +18 -1
- package/src/tools/review.ts +155 -6
- package/src/tools/transcript.ts +178 -36
- package/src/tools/triage.ts +70 -13
- package/src/tools/validate.ts +138 -32
- package/src/transcript.ts +279 -4
- package/src/types.ts +29 -1
package/src/transcript.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* - F5 — checkpoint summary (resume).
|
|
12
12
|
* - F6 — nudge channel (steer the next round).
|
|
13
13
|
* - F7 — append-only decision timeline.
|
|
14
|
+
* - validations — per-round validation outcomes (command / exitCode /
|
|
15
|
+
* allowed) for the run console; see {@link TranscriptValidation}.
|
|
14
16
|
*
|
|
15
17
|
* It performs NO I/O and NEVER touches the filesystem — persistence lives in
|
|
16
18
|
* the `iterate_transcript` tool. Inputs are defensively normalized so a
|
|
@@ -29,6 +31,8 @@ import type {
|
|
|
29
31
|
TranscriptManifest,
|
|
30
32
|
TranscriptNudge,
|
|
31
33
|
TranscriptRound,
|
|
34
|
+
TranscriptThread,
|
|
35
|
+
TranscriptValidation,
|
|
32
36
|
} from './types.ts'
|
|
33
37
|
|
|
34
38
|
/** Manifest schema version (bump on incompatible shape change). */
|
|
@@ -47,16 +51,26 @@ const MAX_FINDINGS_PER_THREAD = 100
|
|
|
47
51
|
const MAX_FINDINGS_TOTAL = 2000
|
|
48
52
|
|
|
49
53
|
/** Max timeline entries kept (newest wins). */
|
|
50
|
-
const MAX_TIMELINE = 500
|
|
54
|
+
export const MAX_TIMELINE = 500
|
|
51
55
|
|
|
52
56
|
/** Max applied-fix records kept (newest wins; bounded so a long run cannot
|
|
53
57
|
* grow the manifest payload without limit). */
|
|
54
58
|
const MAX_FIXES = 200
|
|
55
59
|
|
|
60
|
+
/** Max validation rows kept (newest wins; bounds a very long run's payload). */
|
|
61
|
+
const MAX_VALIDATIONS = 500
|
|
62
|
+
|
|
56
63
|
/** Max round number accepted by the builder. A model-authored/manifest-backed
|
|
57
64
|
* round value is attacker-influenced JSON: `roundStart(1e9)` / `snapshotConvergence`
|
|
58
|
-
* would otherwise preallocate arrays of that size and OOM the host.
|
|
59
|
-
|
|
65
|
+
* would otherwise preallocate arrays of that size and OOM the host.
|
|
66
|
+
* Exported so the capture/rehydrate FEED side can truncate before feeding
|
|
67
|
+
* (a value above the cap would otherwise be folded into the last slot). */
|
|
68
|
+
export const MAX_ROUNDS = 1000
|
|
69
|
+
|
|
70
|
+
/** Max threads restored per round from a persisted manifest (the live cap plus
|
|
71
|
+
* headroom for the single overflow thread). Shared by {@link ReviewTranscriptBuilder.restoreThread}
|
|
72
|
+
* and the read-side normalizer so both bound hostile manifests identically. */
|
|
73
|
+
export const MAX_THREADS_RESTORED = MAX_THREADS_PER_ROUND * 2 + 1
|
|
60
74
|
|
|
61
75
|
/** Thresholds applied when reducing a string list under a cap. */
|
|
62
76
|
function clampStringList(source: string[], cap: number): string[] {
|
|
@@ -115,6 +129,40 @@ function normalizeFinding(input: unknown): TranscriptFinding | null {
|
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Normalize one validation row (`capture` `validations` contract), dropping
|
|
134
|
+
* rows that cannot identify WHAT was run and clamping the rest — the same
|
|
135
|
+
* defensive style as the round/fix normalizers, because these values arrive
|
|
136
|
+
* from a `type:'json'` argument a model authored.
|
|
137
|
+
*
|
|
138
|
+
* - a non-object row, a missing/non-finite round (≤ 0 after flooring), or
|
|
139
|
+
* a missing/blank command → dropped (there is nothing to attribute);
|
|
140
|
+
* - `round` above the builder cap → clamped (never dropped: the row is real);
|
|
141
|
+
* - `exitCode` → integer, or `null` when absent/unknown/not a number;
|
|
142
|
+
* - `allowed` → strict boolean; an ABSENT flag degrades to `false` (fail
|
|
143
|
+
* closed: a row that cannot prove it was allow-listed reads as rejected);
|
|
144
|
+
* - `rejectReason` → trimmed string, omitted when blank.
|
|
145
|
+
*/
|
|
146
|
+
function normalizeValidation(input: unknown): TranscriptValidation | null {
|
|
147
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return null
|
|
148
|
+
const v = input as Record<string, unknown>
|
|
149
|
+
const rawRound = typeof v.round === 'number' && Number.isFinite(v.round) ? Math.floor(v.round) : 0
|
|
150
|
+
if (rawRound <= 0) return null
|
|
151
|
+
const command = typeof v.command === 'string' ? v.command.trim() : ''
|
|
152
|
+
if (!command) return null
|
|
153
|
+
const exitCode =
|
|
154
|
+
typeof v.exitCode === 'number' && Number.isFinite(v.exitCode) ? Math.floor(v.exitCode) : null
|
|
155
|
+
const rejectReason =
|
|
156
|
+
typeof v.rejectReason === 'string' && v.rejectReason.trim() ? v.rejectReason.trim() : undefined
|
|
157
|
+
return {
|
|
158
|
+
round: Math.min(rawRound, MAX_ROUNDS),
|
|
159
|
+
command,
|
|
160
|
+
exitCode,
|
|
161
|
+
allowed: v.allowed === true,
|
|
162
|
+
...(rejectReason ? { rejectReason } : {}),
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
118
166
|
/** Stage an in-progress thread so the builder can append messages/reads/findings. */
|
|
119
167
|
interface LiveThread {
|
|
120
168
|
dimension: string
|
|
@@ -177,6 +225,7 @@ export class ReviewTranscriptBuilder {
|
|
|
177
225
|
private readonly globalFindings: TranscriptFinding[] = []
|
|
178
226
|
private readonly globalSeenKeys = new Set<string>()
|
|
179
227
|
private readonly fixes: TranscriptFix[] = []
|
|
228
|
+
private readonly validations: TranscriptValidation[] = []
|
|
180
229
|
private checkpoint: TranscriptCheckpoint | null = null
|
|
181
230
|
private readonly timeline: TranscriptEntry[] = []
|
|
182
231
|
private nudge: TranscriptNudge | null = null
|
|
@@ -262,6 +311,30 @@ export class ReviewTranscriptBuilder {
|
|
|
262
311
|
this.touch()
|
|
263
312
|
}
|
|
264
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Advance the CURRENT-round marker to `round` WITHOUT creating round rows.
|
|
316
|
+
*
|
|
317
|
+
* `roundStart` pre-allocates empty rows up to `round` (legitimate while a
|
|
318
|
+
* round is opening — reviewers write into those rows). At capture time,
|
|
319
|
+
* though, `roundsExecuted` may legitimately exceed the rounds we actually
|
|
320
|
+
* captured data for: calling `roundStart(roundsExecuted)` there fabricated
|
|
321
|
+
* phantom EMPTY round rows the reviewers never produced. This setter only
|
|
322
|
+
* moves the marker — and only FORWARD (never rewinds past a row capture
|
|
323
|
+
* already recorded) — so `manifest.round` still reports how far the run got
|
|
324
|
+
* while `manifest.rounds` keeps only rounds with captured content.
|
|
325
|
+
*/
|
|
326
|
+
advanceRound(round: number, maxRounds?: number): void {
|
|
327
|
+
const r =
|
|
328
|
+
typeof round === 'number' && Number.isFinite(round) && round > 0
|
|
329
|
+
? Math.min(Math.floor(round), MAX_ROUNDS)
|
|
330
|
+
: 0
|
|
331
|
+
if (r > this.round) this.round = r
|
|
332
|
+
if (typeof maxRounds === 'number' && Number.isFinite(maxRounds) && maxRounds >= 0) {
|
|
333
|
+
this.maxRounds = Math.min(Math.floor(maxRounds), MAX_ROUNDS)
|
|
334
|
+
}
|
|
335
|
+
this.touch()
|
|
336
|
+
}
|
|
337
|
+
|
|
265
338
|
// ─── Reviewer threads (F1) ──────────────────────────────────────────────
|
|
266
339
|
|
|
267
340
|
/** Start a reviewer sub-agent's thread for the current round. */
|
|
@@ -330,7 +403,7 @@ export class ReviewTranscriptBuilder {
|
|
|
330
403
|
if (!thread || typeof thread !== 'object') return
|
|
331
404
|
// Hostile-manifest guard: keep the live hard bound (with headroom for the
|
|
332
405
|
// single overflow thread) and newest-first truncate beyond it.
|
|
333
|
-
if (live.threads.length >=
|
|
406
|
+
if (live.threads.length >= MAX_THREADS_RESTORED) return
|
|
334
407
|
const dim =
|
|
335
408
|
typeof thread.dimension === 'string' && thread.dimension.trim()
|
|
336
409
|
? thread.dimension.trim()
|
|
@@ -464,6 +537,24 @@ export class ReviewTranscriptBuilder {
|
|
|
464
537
|
this.touch()
|
|
465
538
|
}
|
|
466
539
|
|
|
540
|
+
// ─── Validations (F6/F8 console) ────────────────────────────────────────
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Record one validation command outcome. Normalizes defensively (bad rows
|
|
544
|
+
* are dropped, out-of-cap rounds clamped) and keeps the NEWEST rows under
|
|
545
|
+
* MAX_VALIDATIONS, mirroring {@link fix}. A malformed row can never crash
|
|
546
|
+
* the capture nor leak non-JSON state into the manifest.
|
|
547
|
+
*/
|
|
548
|
+
validation(input: unknown): void {
|
|
549
|
+
const record = normalizeValidation(input)
|
|
550
|
+
if (!record) return
|
|
551
|
+
this.validations.push(record)
|
|
552
|
+
if (this.validations.length > MAX_VALIDATIONS) {
|
|
553
|
+
this.validations.splice(0, this.validations.length - MAX_VALIDATIONS)
|
|
554
|
+
}
|
|
555
|
+
this.touch()
|
|
556
|
+
}
|
|
557
|
+
|
|
467
558
|
// ─── Checkpoint (F5) ────────────────────────────────────────────────────
|
|
468
559
|
|
|
469
560
|
/** Record the current checkpoint summary (null clears it). */
|
|
@@ -557,6 +648,7 @@ export class ReviewTranscriptBuilder {
|
|
|
557
648
|
// already ≤ MAX_FINDINGS_TOTAL — no post-hoc slice needed.
|
|
558
649
|
findings: this.globalFindings,
|
|
559
650
|
fixes: this.fixes,
|
|
651
|
+
validations: this.validations,
|
|
560
652
|
checkpoint: this.checkpoint,
|
|
561
653
|
timeline: this.timeline,
|
|
562
654
|
nudge: this.nudge,
|
|
@@ -628,4 +720,187 @@ function dedupePaths(paths: string[]): string[] {
|
|
|
628
720
|
/** ISO timestamp helper (kept injectable in tests via the builder's now). */
|
|
629
721
|
function isoNow(): string {
|
|
630
722
|
return new Date().toISOString()
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ─── Read-side normalization ────────────────────────────────────────────────
|
|
726
|
+
|
|
727
|
+
/** Clamp an unknown round marker into [0, MAX_ROUNDS] (0 = never recorded). */
|
|
728
|
+
function clampRoundValue(v: unknown): number {
|
|
729
|
+
return typeof v === 'number' && Number.isFinite(v)
|
|
730
|
+
? Math.min(Math.max(Math.floor(v), 0), MAX_ROUNDS)
|
|
731
|
+
: 0
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** Clamp an unknown non-negative counter (0 when absent/garbage). */
|
|
735
|
+
function clampCountValue(v: unknown): number {
|
|
736
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.floor(v) : 0
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** Re-run one persisted thread row through the builder's per-row bounds. */
|
|
740
|
+
function normalizeThreadRow(input: unknown): TranscriptThread {
|
|
741
|
+
const t = (input && typeof input === 'object' ? input : {}) as Record<string, unknown>
|
|
742
|
+
const dim = typeof t.dimension === 'string' && t.dimension.trim() ? t.dimension.trim() : 'review'
|
|
743
|
+
const att = typeof t.attempt === 'number' && Number.isFinite(t.attempt) ? Math.floor(t.attempt) : 1
|
|
744
|
+
const messages = Array.isArray(t.messages) ? (t.messages as unknown[]) : []
|
|
745
|
+
const readFiles = Array.isArray(t.readFiles) ? (t.readFiles as unknown[]) : []
|
|
746
|
+
const rawFindings = Array.isArray(t.findings) ? (t.findings as unknown[]) : []
|
|
747
|
+
const findings = rawFindings
|
|
748
|
+
.map((f) => normalizeFinding(f))
|
|
749
|
+
.filter((f): f is TranscriptFinding => f !== null)
|
|
750
|
+
return {
|
|
751
|
+
dimension: dim,
|
|
752
|
+
attempt: att > 0 ? att : 1,
|
|
753
|
+
// Same reductions serialize() applies: messages newest-capped + trimmed,
|
|
754
|
+
// readFiles deduped, per-thread findings newest-capped.
|
|
755
|
+
messages: clampStringList(messages as string[], MAX_MESSAGES_PER_THREAD),
|
|
756
|
+
readFiles: dedupePaths(readFiles as string[]),
|
|
757
|
+
findings:
|
|
758
|
+
findings.length > MAX_FINDINGS_PER_THREAD
|
|
759
|
+
? findings.slice(-MAX_FINDINGS_PER_THREAD)
|
|
760
|
+
: findings,
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** Re-run one persisted fix row through the builder's fix() requirements. */
|
|
765
|
+
function normalizeFixRow(input: unknown): TranscriptFix | null {
|
|
766
|
+
if (!input || typeof input !== 'object') return null
|
|
767
|
+
const f = input as Record<string, unknown>
|
|
768
|
+
const id = typeof f.id === 'string' ? f.id : ''
|
|
769
|
+
const file = typeof f.file === 'string' ? f.file : ''
|
|
770
|
+
if (!id || !file) return null
|
|
771
|
+
return {
|
|
772
|
+
id,
|
|
773
|
+
file,
|
|
774
|
+
// No fabricated "now" on read: a corrupt/absent timestamp stays blank
|
|
775
|
+
// instead of masquerading as a fix that just happened.
|
|
776
|
+
timestamp: typeof f.timestamp === 'string' ? f.timestamp : '',
|
|
777
|
+
round: clampRoundValue(f.round),
|
|
778
|
+
summary: typeof f.summary === 'string' ? f.summary : '',
|
|
779
|
+
linesAdded: clampCountValue(f.linesAdded),
|
|
780
|
+
linesRemoved: clampCountValue(f.linesRemoved),
|
|
781
|
+
success: f.success !== false,
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Re-run one persisted timeline row through the builder's decision() shape. */
|
|
786
|
+
function normalizeTimelineRow(input: unknown): TranscriptEntry | null {
|
|
787
|
+
if (!input || typeof input !== 'object') return null
|
|
788
|
+
const e = input as Record<string, unknown>
|
|
789
|
+
return {
|
|
790
|
+
timestamp: typeof e.timestamp === 'string' ? e.timestamp : '',
|
|
791
|
+
round: clampRoundValue(e.round),
|
|
792
|
+
type: typeof e.type === 'string' && e.type ? e.type : 'decision',
|
|
793
|
+
data:
|
|
794
|
+
e.data && typeof e.data === 'object' && !Array.isArray(e.data)
|
|
795
|
+
? (e.data as Record<string, unknown>)
|
|
796
|
+
: {},
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** Re-run a persisted checkpoint through the builder's recordCheckpoint rules. */
|
|
801
|
+
function normalizeCheckpointRow(input: unknown): TranscriptCheckpoint | null {
|
|
802
|
+
if (!input || typeof input !== 'object') return null
|
|
803
|
+
const c = input as Record<string, unknown>
|
|
804
|
+
const round = typeof c.round === 'number' && Number.isFinite(c.round) ? c.round : 0
|
|
805
|
+
if (round <= 0) return null
|
|
806
|
+
return {
|
|
807
|
+
mode: c.mode === 'dry-run' || c.mode === 'normal' ? c.mode : 'normal',
|
|
808
|
+
round: clampRoundValue(c.round),
|
|
809
|
+
maxRounds: clampCountValue(c.maxRounds),
|
|
810
|
+
fixedCount: clampCountValue(c.fixedCount),
|
|
811
|
+
resumeCount: clampCountValue(c.resumeCount),
|
|
812
|
+
updatedAt: typeof c.updatedAt === 'string' ? c.updatedAt : '',
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** Re-run a persisted nudge through the builder's setNudge rules. */
|
|
817
|
+
function normalizeNudgeRow(input: unknown): TranscriptNudge | null {
|
|
818
|
+
if (!input || typeof input !== 'object') return null
|
|
819
|
+
const n = input as Record<string, unknown>
|
|
820
|
+
const text = typeof n.text === 'string' ? n.text.trim() : ''
|
|
821
|
+
if (!text) return null
|
|
822
|
+
return { timestamp: typeof n.timestamp === 'string' ? n.timestamp : '', text }
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Defensive read-side normalization of a persisted manifest.
|
|
827
|
+
*
|
|
828
|
+
* `.iterate/transcript.json` is hand-editable JSON handed straight to the
|
|
829
|
+
* client/model by `iterate_transcript.read`: the tool gates the ROOT shape,
|
|
830
|
+
* but row-level bounds (threads, messages, findings, timeline, fixes …) used
|
|
831
|
+
* to pass through untouched, so a hostile or corrupted file could leak an
|
|
832
|
+
* unbounded payload or junk field values (fractional lines, non-string
|
|
833
|
+
* phases, negative counters) that a capture could never have written.
|
|
834
|
+
*
|
|
835
|
+
* Every field is re-run through the SAME normalizers and caps the builder
|
|
836
|
+
* uses at capture time. Deliberately NOT implemented as
|
|
837
|
+
* `rehydrateBuilder(...).serialize()`: that drops fields the builder cannot
|
|
838
|
+
* rebuild (phases, the run identity, the round marker), so a plain read would
|
|
839
|
+
* silently rewrite history. Unknown inputs degrade instead of throwing — this
|
|
840
|
+
* runs after the tool's structural gate but must stay safe on its own.
|
|
841
|
+
*/
|
|
842
|
+
export function normalizeManifestBounds(value: TranscriptManifest): TranscriptManifest {
|
|
843
|
+
const m = (value && typeof value === 'object' ? value : {}) as Partial<TranscriptManifest>
|
|
844
|
+
|
|
845
|
+
const rounds: TranscriptRound[] = (Array.isArray(m.rounds) ? m.rounds : [])
|
|
846
|
+
.slice(0, MAX_ROUNDS)
|
|
847
|
+
.map((r) => {
|
|
848
|
+
const row = (r && typeof r === 'object' ? r : {}) as Partial<TranscriptRound>
|
|
849
|
+
const threads = (Array.isArray(row.threads) ? row.threads : [])
|
|
850
|
+
.slice(0, MAX_THREADS_RESTORED)
|
|
851
|
+
.map((t) => normalizeThreadRow(t))
|
|
852
|
+
return { round: Math.max(clampRoundValue(row.round), 1), threads }
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
const fixes = (Array.isArray(m.fixes) ? m.fixes : [])
|
|
856
|
+
.map((f) => normalizeFixRow(f))
|
|
857
|
+
.filter((f): f is TranscriptFix => f !== null)
|
|
858
|
+
const timeline = (Array.isArray(m.timeline) ? m.timeline : [])
|
|
859
|
+
.map((e) => normalizeTimelineRow(e))
|
|
860
|
+
.filter((e): e is TranscriptEntry => e !== null)
|
|
861
|
+
const globalFindings = (Array.isArray(m.findings) ? m.findings : [])
|
|
862
|
+
.map((f) => normalizeFinding(f))
|
|
863
|
+
.filter((f): f is TranscriptFinding => f !== null)
|
|
864
|
+
const convergence = (Array.isArray(m.convergence) ? m.convergence : [])
|
|
865
|
+
.slice(0, MAX_ROUNDS)
|
|
866
|
+
.map((n) => (typeof n === 'number' && Number.isFinite(n) ? n : -1))
|
|
867
|
+
const validations = Array.isArray(m.validations)
|
|
868
|
+
? m.validations
|
|
869
|
+
.map((v) => normalizeValidation(v))
|
|
870
|
+
.filter((v): v is TranscriptValidation => v !== null)
|
|
871
|
+
.slice(-MAX_VALIDATIONS)
|
|
872
|
+
: undefined
|
|
873
|
+
|
|
874
|
+
const policy =
|
|
875
|
+
m.approval?.policy === 'deny' || m.approval?.policy === 'allow' ? m.approval.policy : 'ask'
|
|
876
|
+
const out: TranscriptManifest = {
|
|
877
|
+
version: typeof m.version === 'number' && Number.isFinite(m.version) ? m.version : TRANSCRIPT_VERSION,
|
|
878
|
+
project: typeof m.project === 'string' ? m.project : '',
|
|
879
|
+
updatedAt: typeof m.updatedAt === 'string' ? m.updatedAt : '',
|
|
880
|
+
active: m.active === true,
|
|
881
|
+
mode: m.mode === 'dry-run' || m.mode === 'normal' ? m.mode : null,
|
|
882
|
+
goal: typeof m.goal === 'string' ? m.goal : '',
|
|
883
|
+
// A phase list is free-form narration names — strings only, bounded.
|
|
884
|
+
phases: clampStringList(Array.isArray(m.phases) ? (m.phases as string[]) : [], 100),
|
|
885
|
+
round: clampRoundValue(m.round),
|
|
886
|
+
maxRounds: clampCountValue(m.maxRounds),
|
|
887
|
+
rounds,
|
|
888
|
+
convergence,
|
|
889
|
+
findings:
|
|
890
|
+
globalFindings.length > MAX_FINDINGS_TOTAL
|
|
891
|
+
? globalFindings.slice(-MAX_FINDINGS_TOTAL)
|
|
892
|
+
: globalFindings,
|
|
893
|
+
fixes: fixes.slice(-MAX_FIXES),
|
|
894
|
+
checkpoint: normalizeCheckpointRow(m.checkpoint),
|
|
895
|
+
timeline: timeline.slice(-MAX_TIMELINE),
|
|
896
|
+
nudge: normalizeNudgeRow(m.nudge),
|
|
897
|
+
approval: {
|
|
898
|
+
active: typeof m.approval?.active === 'boolean' ? m.approval.active : policy !== 'allow',
|
|
899
|
+
policy,
|
|
900
|
+
},
|
|
901
|
+
}
|
|
902
|
+
if (validations !== undefined) out.validations = validations
|
|
903
|
+
if (m.taskMode === 'code' || m.taskMode === 'iterate' || m.taskMode === null) out.taskMode = m.taskMode
|
|
904
|
+
if (typeof m.stoppedReason === 'string' || m.stoppedReason === null) out.stoppedReason = m.stoppedReason
|
|
905
|
+
return out
|
|
631
906
|
}
|
package/src/types.ts
CHANGED
|
@@ -41,7 +41,8 @@ export interface IterateConfig {
|
|
|
41
41
|
capture?: boolean
|
|
42
42
|
/**
|
|
43
43
|
* Approval policy for destructive iterate tools (iterate_fix /
|
|
44
|
-
* iterate_rollback / iterate_prune with dryRun:false
|
|
44
|
+
* iterate_rollback / iterate_prune with dryRun:false /
|
|
45
|
+
* iterate_config with operation:"write").
|
|
45
46
|
* - 'ask': prompt the human through the dsh approval service before running.
|
|
46
47
|
* - 'deny': refuse the call outright (fail-closed).
|
|
47
48
|
* - 'allow': always run (debug/trusted). Default 'ask'.
|
|
@@ -327,6 +328,28 @@ export interface TranscriptCheckpoint {
|
|
|
327
328
|
updatedAt: string
|
|
328
329
|
}
|
|
329
330
|
|
|
331
|
+
/**
|
|
332
|
+
* One validation command outcome captured for the observatory.
|
|
333
|
+
*
|
|
334
|
+
* Client/server contract (agreed with the client implementation — do not
|
|
335
|
+
* deviate): every row is `{round, command, exitCode, allowed}` with an
|
|
336
|
+
* optional `rejectReason`. `exitCode` is `null` when the run never produced
|
|
337
|
+
* an exit code; `allowed:false` marks a command that is NOT in
|
|
338
|
+
* `validation.commands` (a config gap, not a code failure).
|
|
339
|
+
*/
|
|
340
|
+
export interface TranscriptValidation {
|
|
341
|
+
/** 1-based round that ran the command. */
|
|
342
|
+
round: number
|
|
343
|
+
/** The exact command string passed to `iterate_validate`. */
|
|
344
|
+
command: string
|
|
345
|
+
/** Process exit code, or null when unknown/never started. */
|
|
346
|
+
exitCode: number | null
|
|
347
|
+
/** False when the command was rejected by the allow-list (`rejectReason`). */
|
|
348
|
+
allowed: boolean
|
|
349
|
+
/** Why the command was rejected (present only when `allowed` is false). */
|
|
350
|
+
rejectReason?: string
|
|
351
|
+
}
|
|
352
|
+
|
|
330
353
|
/**
|
|
331
354
|
* Serializable runtime-observatory manifest the client renders. Every field is
|
|
332
355
|
* `JsonValue`-safe; the builder caps growth so a long run cannot blow up memory.
|
|
@@ -348,6 +371,11 @@ export interface TranscriptManifest {
|
|
|
348
371
|
checkpoint: TranscriptCheckpoint | null
|
|
349
372
|
timeline: TranscriptEntry[]
|
|
350
373
|
nudge: TranscriptNudge | null
|
|
374
|
+
/**
|
|
375
|
+
* Per-round validation outcomes (F6/F8 console). Absent on manifests
|
|
376
|
+
* captured before the field existed; always an array on new captures.
|
|
377
|
+
*/
|
|
378
|
+
validations?: TranscriptValidation[]
|
|
351
379
|
approval: {
|
|
352
380
|
active: boolean
|
|
353
381
|
policy: 'ask' | 'deny' | 'allow'
|