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/skill-prompt.ts
CHANGED
|
@@ -23,7 +23,7 @@ You have the iterate plugin installed, which registers these tools:
|
|
|
23
23
|
- \`iterate_status\` — summarize the current run: mode, round, fixes applied, architectural remaining, decision-log size, checkpoint presence, and whether the run was interrupted (a checkpoint on disk with no newer decision-log activity means the previous run was interrupted and can be resumed)
|
|
24
24
|
- \`iterate_history\` — inspect the runtime state in detail: decision-log entries and applied fixes (optionally scoped to a round or a fixed file)
|
|
25
25
|
- \`iterate_prune\` — remove stale runtime artifacts (\`.iterate/\` entries). Defaults to a read-only dry-run that reports what WOULD be removed; pass \`dryRun:false\` to actually prune.
|
|
26
|
-
- \`iterate_transcript\` — runtime observatory file (\`.iterate/transcript.json\`). \`read\` fetches the persisted manifest including any steering \`nudge\` for this run's reviewers; \`capture\` (call once after the final report) persists the per-reviewer threads, convergence trend, findings, fixes, checkpoint, and timeline so the client observatory panel reflects the run; \`nudge\` sets/clears steering text the next round's reviewers read. Purely local, never touches source files.
|
|
26
|
+
- \`iterate_transcript\` — runtime observatory file (\`.iterate/transcript.json\`). \`read\` fetches the persisted manifest including any steering \`nudge\` for this run's reviewers; \`capture\` (call once after the final report) persists the per-reviewer threads, convergence trend, findings, fixes, checkpoint, per-round validation results (\`validations\`: command + exitCode + allowed + rejectReason), and timeline so the client observatory panel reflects the run; \`nudge\` sets/clears steering text the next round's reviewers read. Purely local, never touches source files.
|
|
27
27
|
- \`iterate_experience\` — experience bank (\`.iterate/experience.json\`): \`list\`/\`search\`/\`get\` recall verified fixes and patterns from past sessions (read the bank before fixing so proven fixes are applied first); \`add\` records a new verified fix — re-adding the same pattern+dimension bumps its hit count instead of duplicating it; \`remove\` deletes a stale or incorrect entry by \`id\` so bad experiences never resurface.
|
|
28
28
|
- \`iterate_quality_gate\` — quality certificate: \`read\` loads the persisted dimension convergence rates / verification pass rate / PASS-FAIL status; \`compute\` recomputes a fresh snapshot from this round's findings + validation results (supply \`findingsByRound\` for real convergence) and persists it to \`.iterate/quality-gate.json\`; \`clear\` removes the persisted certificate to reset a stale gate before a fresh iteration.
|
|
29
29
|
- \`iterate_defense_events\` — defense event stream (\`.iterate/defense-events.json\`): \`list\`/\`counts\` review precondition failures, rollbacks, invariant violations, and falsified assumptions; \`record\` logs a new event when a defense fires. Human-readable labels follow the project \`language\` (en/zh).
|
|
@@ -63,6 +63,7 @@ final review report. NO file writes, NO git, NO branches, NO worktree.
|
|
|
63
63
|
Canonical script — reproduce this structure exactly (adjust dims via the plan):
|
|
64
64
|
|
|
65
65
|
\`\`\`js
|
|
66
|
+
// args = { mode: "dry-run", maxRounds?, subagentProvider?, subagentModel?, attachments? }
|
|
66
67
|
phase('plan')
|
|
67
68
|
// Optional per-run backend override for sub-agents (omit to inherit the session backend).
|
|
68
69
|
const subAgentProvider = (args && args.subagentProvider) || undefined
|
|
@@ -71,7 +72,7 @@ const backend = Object.assign({}, subAgentProvider ? { provider: subAgentProvide
|
|
|
71
72
|
// User-attached image evidence relayed into reviewer prompts (metadata only).
|
|
72
73
|
const attachments = (args && Array.isArray(args.attachments)) ? args.attachments : []
|
|
73
74
|
const planRes = await agent(
|
|
74
|
-
'Call iterate_review({operation:"plan", mode:"dry-run"}) and return the plan JSON.',
|
|
75
|
+
'Call iterate_review({operation:"plan", mode:"dry-run"' + (args.maxRounds ? ', maxReviewRounds:' + args.maxRounds : '') + '}) and return the plan JSON.',
|
|
75
76
|
Object.assign({ label: 'review:plan' }, backend)
|
|
76
77
|
)
|
|
77
78
|
const plan = (planRes && planRes.plan) ? planRes.plan : null
|
|
@@ -85,21 +86,35 @@ const rounds = [] // raw per-round findings
|
|
|
85
86
|
// round where every reviewer subagent failed (or output stayed schema-invalid
|
|
86
87
|
// after retries) is INCONCLUSIVE — it must never make the run look converged.
|
|
87
88
|
let lastRoundOk = true
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const transRead = await agent(
|
|
92
|
-
'Call iterate_transcript({operation:"read"}) and return {nudge:<transcript.nudge ? transcript.nudge.text : null>}.',
|
|
93
|
-
Object.assign({ label: 'transcript:read' }, backend)
|
|
94
|
-
)
|
|
95
|
-
const steering = transRead && typeof transRead.nudge === 'string' && transRead.nudge ? transRead.nudge : null
|
|
89
|
+
// Slot index of the current round within this run's rounds array. A resumed
|
|
90
|
+
// run starts at startRound > 1, so the round NUMBER is not a valid array index.
|
|
91
|
+
let roundSlot = -1
|
|
96
92
|
|
|
97
93
|
phase('review')
|
|
98
94
|
for (let r = 1; r <= maxRounds; r++) {
|
|
95
|
+
// Re-read the steering nudge at the start of EVERY round (not once before
|
|
96
|
+
// the loop): iterate_transcript nudge promises to steer "the next round's
|
|
97
|
+
// reviewers", so a nudge written mid-run must reach round 2+ — a single
|
|
98
|
+
// pre-loop read froze the round-1 nudge for the whole run.
|
|
99
|
+
const transRead = await agent(
|
|
100
|
+
'Call iterate_transcript({operation:"read"}) and return {nudge:<transcript.nudge ? transcript.nudge.text : null>}.',
|
|
101
|
+
Object.assign({ label: 'transcript:read:r' + r }, backend)
|
|
102
|
+
)
|
|
103
|
+
const steering = transRead && typeof transRead.nudge === 'string' && transRead.nudge ? transRead.nudge : null
|
|
99
104
|
log('round ' + r + ' of ' + maxRounds + ' — finding NEW issues only')
|
|
105
|
+
roundSlot = rounds.length
|
|
100
106
|
let agg = null
|
|
101
107
|
let schemaInvalid = false
|
|
102
108
|
let attempts = 0
|
|
109
|
+
// Declared OUTSIDE the do-block: both are read after the retry loop exits,
|
|
110
|
+
// and block-scoped consts here raised ReferenceError at runtime (crashing
|
|
111
|
+
// both canonical workflows after the first round) whenever the aggregate
|
|
112
|
+
// subagent resolved null — or an object without a report — and the
|
|
113
|
+
// convergence fallback below reached thisRound.findings.
|
|
114
|
+
let reviewersOk = true
|
|
115
|
+
// Safe empty shape: the do-block always overwrites it, but the post-loop
|
|
116
|
+
// read must never depend on block scope again.
|
|
117
|
+
let thisRound = { round: r, findings: [], readFiles: [] }
|
|
103
118
|
do {
|
|
104
119
|
attempts += 1
|
|
105
120
|
// Schema validation retry: on the 2nd+ pass, nudge reviewers toward strict JSON.
|
|
@@ -127,15 +142,15 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
127
142
|
// the convergence math would then read "0 new findings". An all-failed
|
|
128
143
|
// round is NOT a clean pass — it is inconclusive, so the convergence gate
|
|
129
144
|
// below refuses it before any "converged" signal fires.
|
|
130
|
-
|
|
131
|
-
|
|
145
|
+
reviewersOk = raw.length > 0 ? raw.some(x => x !== null && typeof x === 'object') : true
|
|
146
|
+
thisRound = {
|
|
132
147
|
round: r,
|
|
133
148
|
findings: [].concat(...raw.map(x => x && x.findings ? x.findings : [])),
|
|
134
149
|
// readFiles are threaded through so the aggregate/meta-review coverage
|
|
135
150
|
// gate can compare self-reported reads against the assigned inventory.
|
|
136
151
|
readFiles: [].concat(...raw.map(x => x && Array.isArray(x.readFiles) ? x.readFiles : [])),
|
|
137
152
|
}
|
|
138
|
-
if (rounds.length
|
|
153
|
+
if (roundSlot < rounds.length) rounds[roundSlot] = thisRound; else rounds.push(thisRound)
|
|
139
154
|
// Deterministic aggregate: cross-round dedupe + known_intentional filter + severity sort.
|
|
140
155
|
agg = await agent(
|
|
141
156
|
'Call iterate_review({operation:"aggregate", mode:"dry-run", rounds:' + JSON.stringify(rounds) + ', maxReviewRounds:' + maxRounds + ', knownIntentional:' + JSON.stringify(knownIntentional) + '}) and return the report JSON.',
|
|
@@ -203,10 +218,45 @@ const metaRes = await agent(
|
|
|
203
218
|
const finalReport = metaRes && metaRes.finalReport ? metaRes.finalReport : null
|
|
204
219
|
const metaAudit = finalReport && finalReport.metaReview ? finalReport.metaReview : null
|
|
205
220
|
|
|
221
|
+
// Compute the final flags ONCE, before the capture: the capture must persist
|
|
222
|
+
// the SAME stoppedReason the caller receives. Previously the capture omitted
|
|
223
|
+
// the computed reason, so the transcript tool derived a wrong one from the
|
|
224
|
+
// trend (an inconclusive run was stored as "converged"/"max_rounds_reached").
|
|
225
|
+
const convergedFinal = report.convergence.converged && lastRoundOk
|
|
226
|
+
// The aggregate's convergence flag is only trustworthy when the last round
|
|
227
|
+
// was usable — an all-failed/schema-invalid final round looks like "0 new"
|
|
228
|
+
// to the convergence math but is an inconclusive run, never a clean pass.
|
|
229
|
+
const stoppedReason = report.convergence.converged && !lastRoundOk
|
|
230
|
+
? 'inconclusive'
|
|
231
|
+
: report.convergence.stoppedReason
|
|
232
|
+
|
|
233
|
+
// F8 (quality gate): persist a certificate for the read-only review too, so
|
|
234
|
+
// the quality tab has data after a dry-run as well. A dry-run never fixes
|
|
235
|
+
// anything (fixedByDimension omitted → 0) and never executes a validation
|
|
236
|
+
// command (validationResults omitted → verification 0/0, never a fabricated
|
|
237
|
+
// check) — the snapshot reflects review findings + convergence only.
|
|
238
|
+
const drySeries = Object.create(null)
|
|
239
|
+
for (const dim of dims) drySeries[dim] = new Array(rounds.length).fill(0)
|
|
240
|
+
for (let i = 0; i < rounds.length; i++) {
|
|
241
|
+
for (const f of (rounds[i].findings || [])) {
|
|
242
|
+
if (f && typeof f === 'object') {
|
|
243
|
+
const s = drySeries[String(f.dimension)]
|
|
244
|
+
if (Array.isArray(s) && i < s.length) s[i] += 1
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
await agent(
|
|
249
|
+
'Call iterate_quality_gate({operation:"compute", dimensions:' + JSON.stringify(dims) +
|
|
250
|
+
', findings:' + JSON.stringify(Array.isArray(report.findings) ? report.findings : known) +
|
|
251
|
+
', findingsByRound:' + JSON.stringify(drySeries) + '}) and return {ok, snapshot, warnings}.',
|
|
252
|
+
Object.assign({ label: 'quality-gate:compute' }, backend)
|
|
253
|
+
)
|
|
254
|
+
|
|
206
255
|
// Persist the run's observatory transcript (reviewer threads, trend, findings)
|
|
207
256
|
// so the client observatory panel reflects this review. Writes ONLY .iterate/transcript.json.
|
|
257
|
+
// A dry-run executes no validation commands, so its \`validations\` is always [].
|
|
208
258
|
await agent(
|
|
209
|
-
'Call iterate_transcript({operation:"capture", mode:"dry-run", goal:' + JSON.stringify(report.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' + report.convergence.totalRounds + ', findingsByRound:' + JSON.stringify(report.convergence.findingsByRound || []) + ', rounds:' + JSON.stringify(rounds.map(rr => ({ round: rr.round, findings: rr.findings, readFiles: rr.readFiles }))) + '}). Return {operation:"ok"}.',
|
|
259
|
+
'Call iterate_transcript({operation:"capture", mode:"dry-run", goal:' + JSON.stringify(report.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' + report.convergence.totalRounds + ', findingsByRound:' + JSON.stringify(report.convergence.findingsByRound || []) + ', stoppedReason:"' + stoppedReason + '", validations:[], rounds:' + JSON.stringify(rounds.map(rr => ({ round: rr.round, findings: rr.findings, readFiles: rr.readFiles }))) + '}). Return {operation:"ok"}.',
|
|
210
260
|
Object.assign({ label: 'transcript:capture' }, backend)
|
|
211
261
|
)
|
|
212
262
|
|
|
@@ -214,13 +264,8 @@ return {
|
|
|
214
264
|
mode: 'dry-run',
|
|
215
265
|
goal: report.goal,
|
|
216
266
|
rounds: rounds.length,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// to the convergence math but is an inconclusive run, never a clean pass.
|
|
220
|
-
converged: report.convergence.converged && lastRoundOk,
|
|
221
|
-
stoppedReason: report.convergence.converged && !lastRoundOk
|
|
222
|
-
? 'inconclusive'
|
|
223
|
-
: report.convergence.stoppedReason,
|
|
267
|
+
converged: convergedFinal,
|
|
268
|
+
stoppedReason: stoppedReason,
|
|
224
269
|
findingsByRound: report.convergence.findingsByRound,
|
|
225
270
|
totalFindings: report.summary.totalFindings,
|
|
226
271
|
bySeverity: { critical: report.summary.critical, high: report.summary.high, medium: report.summary.medium, low: report.summary.low },
|
|
@@ -239,7 +284,7 @@ Key rules for dry-run:
|
|
|
239
284
|
- Stop when a round reports 0 new findings (converged) or maxReviewRounds is reached.
|
|
240
285
|
- The report (with per-round convergence stats + suggested fix priorities) is the deliverable.
|
|
241
286
|
- **Meta-review**: after building the report, audit it with \`iterate_review({operation:"meta-review"})\` for internal consistency (counts, severity buckets, dimension sums, sort order, convergence math). The meta-review ALSO runs the hard code-evidence gate (default on): every finding's file/line is validated against real files on disk, so any fabricated location surfaces as a critical \`EVIDENCE_VIOLATION\` and flips the verdict to \`needs_revision\`. The \`finalReport.verdict\` is \`approved\` only when the report passes every check AND every finding anchors to real, read code; otherwise \`needs_revision\`. Surface the final report and its verdict as the closing deliverable.
|
|
242
|
-
- Only a single \`report\` entry may be appended to the decision log; nothing else is written to source files. The final \`iterate_transcript capture\` writes ONLY the observatory file (\`.iterate/transcript.json\`) so the client panel reflects the run — it is not a source-code write.
|
|
287
|
+
- Only a single \`report\` entry may be appended to the decision log; nothing else is written to source files. The final \`iterate_transcript capture\` writes ONLY the observatory file (\`.iterate/transcript.json\`) so the client panel reflects the run — it is not a source-code write. The capture carries the computed \`stoppedReason\` (e.g. \`inconclusive\` for a run whose last round was unusable) so the panel shows why the run actually ended instead of the tool guessing from the trend.
|
|
243
288
|
|
|
244
289
|
### Normal-mode workflow (autonomous closed loop)
|
|
245
290
|
Set \`args.mode = "normal"\`. Loop: resume → plan → parallel review ×N → atomic fixes via \`iterate_fix\` → validate → rollback on failure → checkpoint → loop → auto-stop when zero findings remain.
|
|
@@ -253,18 +298,60 @@ const subAgentModel = (args && args.subagentModel) || undefined
|
|
|
253
298
|
const backend = Object.assign({}, subAgentProvider ? { provider: subAgentProvider } : {}, subAgentModel ? { model: subAgentModel } : {})
|
|
254
299
|
// User-attached image evidence relayed into reviewer prompts (metadata only).
|
|
255
300
|
const attachments = (args && Array.isArray(args.attachments)) ? args.attachments : []
|
|
301
|
+
phase('preflight')
|
|
302
|
+
// PREFLIGHT (#2): with NO configured validation.commands this loop would run
|
|
303
|
+
// zero validations every round and still report green (fails OPEN). Read the
|
|
304
|
+
// config BEFORE any work and stop with an actionable message — unless the
|
|
305
|
+
// caller explicitly opted into an unverified run (args.unverified === true).
|
|
306
|
+
const preCfgRes = await agent(
|
|
307
|
+
'Call iterate_config({}) and return the config JSON.',
|
|
308
|
+
Object.assign({ label: 'config:preflight' }, backend)
|
|
309
|
+
)
|
|
310
|
+
const preCfg = (preCfgRes && preCfgRes.config) ? preCfgRes.config : null
|
|
311
|
+
const validationCmds = []
|
|
312
|
+
const cfgCommands = preCfg && preCfg.validation && preCfg.validation.commands
|
|
313
|
+
if (cfgCommands && typeof cfgCommands === 'object') {
|
|
314
|
+
for (const group of Object.values(cfgCommands)) {
|
|
315
|
+
if (Array.isArray(group)) {
|
|
316
|
+
for (const c of group) { if (typeof c === 'string' && c.trim()) validationCmds.push(c) }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Explicit USER opt-in: proceed knowing nothing will be verified (UNVERIFIED).
|
|
321
|
+
const unverified = !!(args && args.unverified === true)
|
|
322
|
+
if (validationCmds.length === 0 && !unverified) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
'preflight failed: iterate.config.yaml has NO validation.commands — this iteration would be protected by NO test. ' +
|
|
325
|
+
'Configure at least one trusted command with iterate_config({operation:"write", updates:{validation:{commands:{test:["<command>"]}}}}) ' +
|
|
326
|
+
'(e.g. "npm test"), then re-run; or pass args.unverified:true to proceed WITHOUT verification — the run will then be reported as UNVERIFIED (zero-validation), never as a verified pass.'
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
if (validationCmds.length === 0) {
|
|
330
|
+
log('preflight: no validation.commands configured — proceeding as an EXPLICIT unverified run (args.unverified); every round is UNVERIFIED')
|
|
331
|
+
}
|
|
256
332
|
phase('resume')
|
|
257
333
|
// If a previous run was interrupted, resume from its checkpoint instead of restarting.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
334
|
+
// Use operation:"resume" (NOT plain "load"): the tool loads, bumps AND PERSISTS
|
|
335
|
+
// resumeCount itself, so a run that is interrupted again before its first save
|
|
336
|
+
// still counts this resumption — and re-adding +1 here on top of the tool's
|
|
337
|
+
// bump would double-count. A failed resume (fresh run with no checkpoint, or a
|
|
338
|
+
// persist error on a real one) falls back to a plain "load" so the state is
|
|
339
|
+
// never lost.
|
|
340
|
+
const resumeRes = await agent(
|
|
341
|
+
'Call iterate_checkpoint({ operation: "resume" }) and return {ok, checkpoint, error}.',
|
|
342
|
+
Object.assign({ label: 'checkpoint:resume' }, backend)
|
|
261
343
|
)
|
|
262
|
-
|
|
344
|
+
let checkpoint = (resumeRes && resumeRes.ok !== false && resumeRes.checkpoint) ? resumeRes.checkpoint : null
|
|
345
|
+
if (!checkpoint) {
|
|
346
|
+
const loadRes = await agent(
|
|
347
|
+
'Call iterate_checkpoint({ operation: "load" }) and return the checkpoint JSON.',
|
|
348
|
+
Object.assign({ label: 'checkpoint:load' }, backend)
|
|
349
|
+
)
|
|
350
|
+
checkpoint = (loadRes && loadRes.checkpoint) ? loadRes.checkpoint : null
|
|
351
|
+
}
|
|
263
352
|
const startRound = (checkpoint && typeof checkpoint.round === 'number') ? checkpoint.round + 1 : 1
|
|
264
|
-
//
|
|
265
|
-
const
|
|
266
|
-
// The effective count AFTER this recovery: this run counts as one more resume.
|
|
267
|
-
const effectiveResumeCount = checkpoint ? resumeCount + 1 : 0
|
|
353
|
+
// Already-bumped count returned by the tool's resume (0 when there was nothing to resume).
|
|
354
|
+
const effectiveResumeCount = (checkpoint && typeof checkpoint.resumeCount === 'number') ? checkpoint.resumeCount : 0
|
|
268
355
|
if (checkpoint) {
|
|
269
356
|
// A previous run left a checkpoint — record the recovery so the decision log
|
|
270
357
|
// shows the resume, then continue where it left off.
|
|
@@ -291,26 +378,58 @@ const knownIntentional = (plan.knownIntentional || []) // config personalizati
|
|
|
291
378
|
const dims = plan.dimensions.map(d => d.id)
|
|
292
379
|
const maxRounds = plan.maxReviewRounds
|
|
293
380
|
const rounds = [] // findings per review round (each on the then-current code state)
|
|
381
|
+
// Per-round AGGREGATE findings (deduped / known_intentional-filtered /
|
|
382
|
+
// schema-validated — the same set \`atomic\`/\`remaining\` act on). F8's quality
|
|
383
|
+
// certificate is computed from these, never from raw reviewer output.
|
|
384
|
+
const roundReports = []
|
|
294
385
|
// Restore previously-unfixed architectural findings when resuming an interrupted run.
|
|
295
386
|
const architectural = (checkpoint && Array.isArray(checkpoint.findings)) ? checkpoint.findings : [] // findings deliberately left unfixed (reported at the end)
|
|
296
387
|
let fixedCount = (checkpoint && typeof checkpoint.fixedCount === 'number') ? checkpoint.fixedCount : 0
|
|
297
388
|
let converged = false
|
|
298
389
|
let abortedByValidation = false
|
|
299
390
|
let schemaFailed = false // last round produced no usable reviewer output (schema-invalid after retries / all reviewers failed)
|
|
391
|
+
let validationUnavailable = false // the validate subagent itself failed (null / no results) — validation could not run, so the round is UNVERIFIED
|
|
300
392
|
let failedCommands = []
|
|
301
393
|
let configErrors = [] // validation commands NOT in validation.commands (config gap, no rollback)
|
|
302
394
|
const fixRecords = [] // observatory fix records collected round by round
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
395
|
+
// Every validation outcome captured for the transcript (round + command +
|
|
396
|
+
// exitCode + allowed) — persisted via \`iterate_transcript capture\` so the run
|
|
397
|
+
// console can show WHICH commands ran and whether they passed (#6).
|
|
398
|
+
const validationRecords = []
|
|
399
|
+
// F10 (defense events): rollbacks / precondition failures fire during the
|
|
400
|
+
// loop; \`recordDefense\` logs each one to \`.iterate/defense-events.json\`.
|
|
401
|
+
// Labels follow the project \`language\` inside the tool.
|
|
402
|
+
const recordDefense = (type, roundNo, description, defense, outcome) =>
|
|
403
|
+
agent(
|
|
404
|
+
'Call iterate_defense_events({operation:"record", type:' + JSON.stringify(type) +
|
|
405
|
+
', round:' + roundNo +
|
|
406
|
+
', severity:' + (type === 'rollback' ? '"high"' : '"medium"') +
|
|
407
|
+
', description:' + JSON.stringify(description) +
|
|
408
|
+
', defense:' + JSON.stringify(defense) +
|
|
409
|
+
', outcome:' + JSON.stringify(outcome) + '}). Return {operation:"ok"}.',
|
|
410
|
+
Object.assign({ label: 'defense:' + type + ':r' + roundNo }, backend)
|
|
411
|
+
)
|
|
412
|
+
// Slot index of the current round within this run's rounds array. A resumed
|
|
413
|
+
// run starts at startRound > 1, so the round NUMBER is not a valid array index.
|
|
414
|
+
let roundSlot = -1
|
|
415
|
+
// Highest round number actually executed by THIS run (carried so a resumed run
|
|
416
|
+
// reports its true round, not the count of rounds it happened to run).
|
|
417
|
+
let lastRound = startRound - 1
|
|
310
418
|
|
|
311
419
|
phase('loop')
|
|
312
420
|
for (let r = startRound; r <= maxRounds; r++) {
|
|
421
|
+
// Re-read the steering nudge at the start of EVERY round (not once before
|
|
422
|
+
// the loop): iterate_transcript nudge promises to steer "the next round's
|
|
423
|
+
// reviewers", so a nudge written mid-run must reach round 2+ — a single
|
|
424
|
+
// pre-loop read froze the round-1 nudge for the whole run.
|
|
425
|
+
const transRead = await agent(
|
|
426
|
+
'Call iterate_transcript({operation:"read"}) and return {nudge:<transcript.nudge ? transcript.nudge.text : null>}.',
|
|
427
|
+
Object.assign({ label: 'transcript:read:r' + r }, backend)
|
|
428
|
+
)
|
|
429
|
+
const steering = transRead && typeof transRead.nudge === 'string' && transRead.nudge ? transRead.nudge : null
|
|
313
430
|
log('round ' + r + ' of ' + maxRounds + ' — review current state, fix atomics via iterate_fix, validate')
|
|
431
|
+
roundSlot = rounds.length
|
|
432
|
+
lastRound = r
|
|
314
433
|
// Audit-trail: record the round start (SKILL.md Phase 4 requires per-round records).
|
|
315
434
|
await agent(
|
|
316
435
|
'Call iterate_decision_log({operation:"append", type:"round_start", round:' + r + ', data:{maxRounds:' + maxRounds + ', fixedSoFar:' + fixedCount + '}})',
|
|
@@ -319,6 +438,14 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
319
438
|
let agg = null
|
|
320
439
|
let schemaInvalid = false
|
|
321
440
|
let attempts = 0
|
|
441
|
+
// Declared OUTSIDE the do-block: both are read after the retry loop exits,
|
|
442
|
+
// and block-scoped consts here raised ReferenceError at runtime (crashing
|
|
443
|
+
// both canonical workflows after the first round) whenever the aggregate
|
|
444
|
+
// subagent resolved null — the findings fallback below read thisRound.
|
|
445
|
+
let reviewersOk = true
|
|
446
|
+
// Safe empty shape: the do-block always overwrites it, but the post-loop
|
|
447
|
+
// read must never depend on block scope again.
|
|
448
|
+
let thisRound = { round: r, findings: [], readFiles: [] }
|
|
322
449
|
do {
|
|
323
450
|
attempts += 1
|
|
324
451
|
// Schema validation retry: on the 2nd+ pass, nudge reviewers toward strict JSON.
|
|
@@ -342,13 +469,13 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
342
469
|
// ALL reviewers returning null (child failures) means this round produced
|
|
343
470
|
// no usable output — an empty aggregate must not read like a clean
|
|
344
471
|
// "nothing to fix" convergence. Gate the loop exit + convergence on it.
|
|
345
|
-
|
|
346
|
-
|
|
472
|
+
reviewersOk = raw.length > 0 ? raw.some(x => x !== null && typeof x === 'object') : true
|
|
473
|
+
thisRound = {
|
|
347
474
|
round: r,
|
|
348
475
|
findings: [].concat(...raw.map(x => x && x.findings ? x.findings : [])),
|
|
349
476
|
readFiles: [].concat(...raw.map(x => x && Array.isArray(x.readFiles) ? x.readFiles : [])),
|
|
350
477
|
}
|
|
351
|
-
if (rounds.length
|
|
478
|
+
if (roundSlot < rounds.length) rounds[roundSlot] = thisRound; else rounds.push(thisRound)
|
|
352
479
|
|
|
353
480
|
// Deterministic dedupe / known_intentional filter / severity sort for this round.
|
|
354
481
|
// \`fixedCount\` is threaded into the report summary so the client dashboard can
|
|
@@ -366,6 +493,11 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
366
493
|
log('retry ' + attempts + ': round ' + r + ' output failed schema validation — re-running reviewers with strict-JSON emphasis')
|
|
367
494
|
}
|
|
368
495
|
} while (schemaInvalid && attempts < 3)
|
|
496
|
+
// Freeze the round's aggregate findings for the final F8 gate — the raw
|
|
497
|
+
// reviewer output above may still hold duplicates the aggregate dropped.
|
|
498
|
+
roundReports[roundSlot] = (agg && agg.report && Array.isArray(agg.report.findings))
|
|
499
|
+
? agg.report.findings
|
|
500
|
+
: thisRound.findings
|
|
369
501
|
if (schemaInvalid || !reviewersOk) {
|
|
370
502
|
// Bounded exit: never loop forever, and never let an all-failed/empty round
|
|
371
503
|
// read like a clean convergence. Stop the run WITHOUT applying fixes for
|
|
@@ -378,6 +510,8 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
378
510
|
'Call iterate_decision_log({operation:"append", type:"round_failed", round:' + r + ', data:{reason:"' + (schemaInvalid ? 'schema_invalid' : 'no_usable_reviewer_output') + '", rolledBack:0}})',
|
|
379
511
|
Object.assign({ label: 'log:schemaFailed:r' + r }, backend)
|
|
380
512
|
)
|
|
513
|
+
// F10: an unusable round is a precondition failure the gate could not clear.
|
|
514
|
+
await recordDefense('precondition_failed', r, 'reviewer output unusable: ' + (schemaInvalid ? 'schema-invalid after 3 attempts' : 'all reviewers failed'), 'reviewer output schema gate', 'round inconclusive; run stopped WITHOUT fixes (never reported as converged)')
|
|
381
515
|
break
|
|
382
516
|
}
|
|
383
517
|
const findings = (agg && agg.report && agg.report.findings) ? agg.report.findings : thisRound.findings
|
|
@@ -390,7 +524,13 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
390
524
|
// calling iterate_fix per finding (the ONLY sanctioned writer), then
|
|
391
525
|
// iterate_diff to verify — so the same file is never edited concurrently;
|
|
392
526
|
// different files still run in parallel.
|
|
393
|
-
|
|
527
|
+
// Null-prototype map: f.file is MODEL-CONTROLLED, and on a plain {} a
|
|
528
|
+
// finding with file "__proto__" or "constructor" resolves to
|
|
529
|
+
// Object.prototype/Object — the "|| []" keeps that inherited object and
|
|
530
|
+
// ".push(f)" then throws TypeError, killing the whole run. With
|
|
531
|
+
// Object.create(null) every key is an own property, and Object.keys(byFile)
|
|
532
|
+
// (the only downstream usage) works unchanged.
|
|
533
|
+
const byFile = Object.create(null)
|
|
394
534
|
atomic.forEach(f => { (byFile[f.file] = byFile[f.file] || []).push(f) })
|
|
395
535
|
const fixRes = await parallel(Object.keys(byFile).map(file => () => agent(
|
|
396
536
|
'Apply the fixes for ' + JSON.stringify(file) + ' using iterate_fix. For EACH finding in this list, ' +
|
|
@@ -425,6 +565,14 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
425
565
|
}
|
|
426
566
|
}
|
|
427
567
|
}
|
|
568
|
+
// F10: a fix iterate_fix REFUSED (at this point in the round success:false
|
|
569
|
+
// can only mean the tool refused it — rollbacks flip records later) means
|
|
570
|
+
// the reviewer's \`is_atomic\` assumption was wrong: the change did not fit
|
|
571
|
+
// the atomic limit. That is a falsified assumption, not a code failure.
|
|
572
|
+
const refusedFixes = fixRecords.filter(fr => fr.round === r && fr.success === false)
|
|
573
|
+
if (refusedFixes.length > 0) {
|
|
574
|
+
await recordDefense('assumption_falsified', r, refusedFixes.length + ' finding(s) marked atomic could not be applied by iterate_fix', 'finding judged is_atomic (change ≤ ' + atomicMaxLines + ' lines in one file)', 'fix refused — the atomicity assumption was wrong for these findings; they stay open for a follow-up round')
|
|
575
|
+
}
|
|
428
576
|
}
|
|
429
577
|
|
|
430
578
|
// Cross-round dedupe of architectural findings before accumulating.
|
|
@@ -439,17 +587,73 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
439
587
|
// when a command is NOT in validation.commands (a config gap, NOT a code
|
|
440
588
|
// failure) — those abort the run WITHOUT rolling back (the code changes are
|
|
441
589
|
// fine; the trust list just needs fixing).
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
590
|
+
let valResults = []
|
|
591
|
+
if (unverified) {
|
|
592
|
+
// Explicit user opt-in chosen at the preflight above: NOTHING is configured
|
|
593
|
+
// to validate, so validation is skipped DELIBERATELY — never silently.
|
|
594
|
+
// The round runs UNVERIFIED and the final report/stoppedReason carry
|
|
595
|
+
// zeroValidation so it can never read as a verified pass.
|
|
596
|
+
log('round ' + r + ' UNVERIFIED (args.unverified, no validation.commands) — skipping validation for this round')
|
|
597
|
+
} else {
|
|
598
|
+
const valRes = await agent(
|
|
599
|
+
'Read iterate.config.yaml validation.commands, then call iterate_validate({ command: <cmd> }) for EACH configured command ' +
|
|
600
|
+
'(one tool call per command). Return all results as {command, exitCode, allowed, rejectReason} entries — include \`allowed\` and \`rejectReason\` exactly as returned by iterate_validate (allowed:false means the command is not in validation.commands).',
|
|
601
|
+
Object.assign({ label: 'validate:r' + r, phase: 'validate', schema: {
|
|
602
|
+
type: 'object', additionalProperties: false,
|
|
603
|
+
properties: {
|
|
604
|
+
results: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { command: { type: 'string' }, exitCode: { type: 'integer' }, allowed: { type: 'boolean' }, rejectReason: { type: 'string' } }, required: ['command', 'exitCode'] } }
|
|
605
|
+
},
|
|
606
|
+
required: ['results'] } }, backend)
|
|
607
|
+
)
|
|
608
|
+
if (!valRes || !Array.isArray(valRes.results)) {
|
|
609
|
+
// A FAILED validate subagent (null / missing results) must abort the run
|
|
610
|
+
// exactly like a validation failure: collapsing it to [] would mark every
|
|
611
|
+
// command as passed, skip the failure branch below, and continue as if the
|
|
612
|
+
// round were verified (fails OPEN). Stop here — before the checkpoint save
|
|
613
|
+
// and the next round — keeping the round's fixes on disk (they may be
|
|
614
|
+
// fine) but UNVERIFIED, with the checkpoint left in place for a resume.
|
|
615
|
+
validationUnavailable = true
|
|
616
|
+
abortedByValidation = true
|
|
617
|
+
log('round ' + r + ' validation could NOT run (iterate_validate subagent failed) — stopping run; fixes from this round are kept but UNVERIFIED')
|
|
618
|
+
await agent(
|
|
619
|
+
'Call iterate_decision_log({operation:"append", type:"round_failed", round:' + r + ', data:{reason:"validation_unavailable", rolledBack:0}})',
|
|
620
|
+
Object.assign({ label: 'log:valUnavailable:r' + r }, backend)
|
|
621
|
+
)
|
|
622
|
+
await recordDefense('precondition_failed', r, 'validation subagent failed — no command could run', 'validate subagent unavailable', 'round stopped with fixes kept UNVERIFIED (no rollback)')
|
|
623
|
+
break
|
|
624
|
+
}
|
|
625
|
+
valResults = valRes.results
|
|
626
|
+
if (valResults.length === 0) {
|
|
627
|
+
// ZERO results (#2): the validator returned an EMPTY set, so NO
|
|
628
|
+
// configured command was actually executed this round. An empty result
|
|
629
|
+
// is NOT a pass — treat it exactly like a failed validator (fail CLOSED)
|
|
630
|
+
// instead of letting a zero-verification round look green. The preflight
|
|
631
|
+
// above already blocks the "no commands configured" case, so reaching
|
|
632
|
+
// here means the run simply verified nothing.
|
|
633
|
+
validationUnavailable = true
|
|
634
|
+
abortedByValidation = true
|
|
635
|
+
log('round ' + r + ' validation produced ZERO results — stopping run (a round with no executed validation is never a pass)')
|
|
636
|
+
await agent(
|
|
637
|
+
'Call iterate_decision_log({operation:"append", type:"round_failed", round:' + r + ', data:{reason:"zero_validation_results", rolledBack:0}})',
|
|
638
|
+
Object.assign({ label: 'log:zeroValidation:r' + r }, backend)
|
|
639
|
+
)
|
|
640
|
+
await recordDefense('precondition_failed', r, 'validation produced zero results — no command ran', 'validation result gate', 'round stopped with fixes kept UNVERIFIED (no rollback)')
|
|
641
|
+
break
|
|
642
|
+
}
|
|
643
|
+
// Persist every outcome for the transcript console (#6): command, exit
|
|
644
|
+
// code, allow-list verdict, and the reject reason when the list refused it.
|
|
645
|
+
for (const v of valResults) {
|
|
646
|
+
if (v && typeof v === 'object') {
|
|
647
|
+
validationRecords.push({
|
|
648
|
+
round: r,
|
|
649
|
+
command: typeof v.command === 'string' ? v.command : '',
|
|
650
|
+
exitCode: typeof v.exitCode === 'number' && isFinite(v.exitCode) ? Math.floor(v.exitCode) : null,
|
|
651
|
+
allowed: v.allowed === true,
|
|
652
|
+
rejectReason: (typeof v.rejectReason === 'string' && v.rejectReason) ? v.rejectReason : undefined
|
|
653
|
+
})
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
453
657
|
configErrors = valResults.filter(v => v.allowed === false).map(v => v.rejectReason || (v.command + ' was rejected'))
|
|
454
658
|
failedCommands = valResults.filter(v => !(v.allowed === false) && v.exitCode !== 0).map(v => v.command)
|
|
455
659
|
if (configErrors.length > 0) {
|
|
@@ -459,6 +663,7 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
459
663
|
'Call iterate_decision_log({operation:"append", type:"round_failed", round:' + r + ', data:{configErrors:' + JSON.stringify(configErrors) + ', rolledBack:0, reason:"command not in validation.commands"}})',
|
|
460
664
|
Object.assign({ label: 'log:configError:r' + r }, backend)
|
|
461
665
|
)
|
|
666
|
+
await recordDefense('precondition_failed', r, 'command not in validation.commands: ' + configErrors.join(' | '), 'validation command allow-list', 'run aborted WITHOUT rollback; the trust list needs updating')
|
|
462
667
|
break
|
|
463
668
|
}
|
|
464
669
|
if (failedCommands.length > 0) {
|
|
@@ -474,14 +679,69 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
474
679
|
},
|
|
475
680
|
required: ['results'] } }, backend)
|
|
476
681
|
)
|
|
682
|
+
// The round's fixes were reverted on disk — the observatory records and
|
|
683
|
+
// the running fixedCount must stop counting them as applied, otherwise
|
|
684
|
+
// findingsFixed / the final capture would report rolled-back work as
|
|
685
|
+
// done. Each roundFixId was pushed with success:true exactly once, so
|
|
686
|
+
// the guard keeps the decrement single-shot per fix.
|
|
687
|
+
for (const rec of fixRecords) {
|
|
688
|
+
if (rec.round === r && rec.success && roundFixIds.indexOf(rec.id) >= 0) {
|
|
689
|
+
rec.success = false
|
|
690
|
+
fixedCount -= 1
|
|
691
|
+
}
|
|
692
|
+
}
|
|
477
693
|
}
|
|
478
694
|
await agent(
|
|
479
695
|
'Call iterate_decision_log({operation:"append", type:"round_failed", round:' + r + ', data:{failedCommands:' + JSON.stringify(failedCommands) + ', rolledBack:' + roundFixIds.length + '}})',
|
|
480
696
|
Object.assign({ label: 'log:failed:r' + r }, backend)
|
|
481
697
|
)
|
|
698
|
+
// F10: every rollback is a defense event the F10 tab surfaces as a count.
|
|
699
|
+
await recordDefense('rollback', r, 'validation failed on: ' + failedCommands.join(', '), 'iterate_rollback reverted the round-' + r + ' fixes', roundFixIds.length + ' fix(es) reverted; run stopped (checkpoint kept for resume)')
|
|
482
700
|
break
|
|
483
701
|
}
|
|
484
702
|
|
|
703
|
+
// F9 (experience bank): validation PASSED for this round, so these fixes are
|
|
704
|
+
// VERIFIED — bank them before moving on. An UNVERIFIED run banks NOTHING:
|
|
705
|
+
// with no validation command executed there is no proof the fix worked.
|
|
706
|
+
// \`iterate_experience add\` dedupes on
|
|
707
|
+
// pattern+dimension (repeat = hit count bump), so a re-discovered finding
|
|
708
|
+
// never floods the bank. Each successful fix record is paired with the next
|
|
709
|
+
// unused atomic finding of the same file (fixes are per-file, in order).
|
|
710
|
+
if (roundFixIds.length > 0 && !unverified) {
|
|
711
|
+
const expByFile = Object.create(null)
|
|
712
|
+
for (const f of atomic) { (expByFile[f.file] = expByFile[f.file] || []).push(f) }
|
|
713
|
+
const expUsed = Object.create(null)
|
|
714
|
+
const expEntries = []
|
|
715
|
+
for (const rec of fixRecords) {
|
|
716
|
+
if (rec.round !== r || !rec.success || roundFixIds.indexOf(rec.id) < 0) continue
|
|
717
|
+
const pool = expByFile[rec.file] || []
|
|
718
|
+
let finding = null
|
|
719
|
+
for (let i = 0; i < pool.length; i++) {
|
|
720
|
+
const key = rec.file + '#' + i
|
|
721
|
+
if (!expUsed[key]) { expUsed[key] = true; finding = pool[i]; break }
|
|
722
|
+
}
|
|
723
|
+
if (!finding) continue
|
|
724
|
+
expEntries.push({
|
|
725
|
+
pattern: String(finding.summary || finding.dimension || 'atomic fix'),
|
|
726
|
+
dimension: String(finding.dimension || 'general'),
|
|
727
|
+
description: String(finding.failure_scenario || finding.summary || 'Verified atomic fix from this iteration.'),
|
|
728
|
+
verifiedFix: String(finding.suggested_fix || finding.summary || 'Applied via iterate_fix.'),
|
|
729
|
+
findingSummary: String(finding.summary || ''),
|
|
730
|
+
severity: (finding.severity === 'critical' || finding.severity === 'high' || finding.severity === 'medium' || finding.severity === 'low') ? finding.severity : 'medium',
|
|
731
|
+
files: [rec.file],
|
|
732
|
+
tags: ['iterate', 'round-' + r],
|
|
733
|
+
})
|
|
734
|
+
}
|
|
735
|
+
if (expEntries.length > 0) {
|
|
736
|
+
await agent(
|
|
737
|
+
'Fixes from round ' + r + ' PASSED validation. For EACH entry below call ' +
|
|
738
|
+
'iterate_experience({operation:"add", entry:<that entry>}) (the tool dedupes on pattern+dimension — a repeat bumps its hit count). ' +
|
|
739
|
+
'Entries: ' + JSON.stringify(expEntries) + '. Return {added:<number of entries the tool reported as new>}.',
|
|
740
|
+
Object.assign({ label: 'experience:add:r' + r }, backend)
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
485
745
|
await agent(
|
|
486
746
|
'Call iterate_decision_log({operation:"append", type:"review_result", round:' + r +
|
|
487
747
|
', data:{atomic:' + atomic.length + ', architectural:' + remaining.length + ', fixedSoFar:' + fixedCount + '}})',
|
|
@@ -503,20 +763,95 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
503
763
|
|
|
504
764
|
phase('report')
|
|
505
765
|
await agent(
|
|
506
|
-
'Call iterate_decision_log({operation:"append", type:"report", round:' +
|
|
766
|
+
'Call iterate_decision_log({operation:"append", type:"report", round:' + lastRound +
|
|
507
767
|
', data:{mode:"normal", fixed:' + fixedCount + ', architectural:' + architectural.length + '}})',
|
|
508
|
-
{ label: 'report:log' }
|
|
768
|
+
Object.assign({ label: 'report:log' }, backend)
|
|
509
769
|
)
|
|
510
770
|
const statusRes = await agent(
|
|
511
771
|
'Call iterate_status() and return the status JSON.',
|
|
512
|
-
{ label: 'status:final' }
|
|
772
|
+
Object.assign({ label: 'status:final' }, backend)
|
|
513
773
|
)
|
|
514
774
|
const status = (statusRes && statusRes.ok) ? statusRes : null
|
|
515
|
-
|
|
775
|
+
// F8 (quality gate): persist the run's quality certificate so the command
|
|
776
|
+
// panel shows PASS/FAIL + per-dimension scores instead of an empty tab.
|
|
777
|
+
// Inputs are assembled defensively:
|
|
778
|
+
// - \`gateFindings\` holds only what is still OPEN on disk: architectural
|
|
779
|
+
// findings (never auto-fixed) plus round findings no successful fix closed
|
|
780
|
+
// (same round + file, paired in order). A finding that was fixed and stayed
|
|
781
|
+
// fixed must NOT fail the final gate.
|
|
782
|
+
// - \`gateSeries\` is the per-dimension NEW-finding series, pre-zeroed to the
|
|
783
|
+
// number of executed rounds so a round that found nothing in a dimension
|
|
784
|
+
// still advances its convergence reading.
|
|
785
|
+
// - \`gateValidation\` maps a null exitCode (command never produced one) to 1 —
|
|
786
|
+
// fail CLOSED, never as a pass.
|
|
787
|
+
const gateAll = []
|
|
788
|
+
for (let si = 0; si < rounds.length; si++) {
|
|
789
|
+
const roundNo = rounds[si] && typeof rounds[si].round === 'number' ? rounds[si].round : si + 1
|
|
790
|
+
const perFile = Object.create(null)
|
|
791
|
+
for (const f of (roundReports[si] || [])) {
|
|
792
|
+
if (!f || typeof f !== 'object') continue
|
|
793
|
+
const fk = (typeof f.file === 'string') ? f.file : ''
|
|
794
|
+
const idx = perFile[fk] || 0
|
|
795
|
+
perFile[fk] = idx + 1
|
|
796
|
+
gateAll.push({ f: f, key: roundNo + '#' + fk + '#' + idx })
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const gateClosed = Object.create(null)
|
|
800
|
+
for (const rec of fixRecords) {
|
|
801
|
+
if (!rec.success) continue
|
|
802
|
+
const prefix = rec.round + '#' + rec.file + '#'
|
|
803
|
+
for (let i = 0; i < gateAll.length; i++) {
|
|
804
|
+
if (!gateClosed[gateAll[i].key] && gateAll[i].key.indexOf(prefix) === 0) { gateClosed[gateAll[i].key] = true; break }
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const gateFindings = []
|
|
808
|
+
const gateSeen = Object.create(null)
|
|
809
|
+
const gatePush = (f) => {
|
|
810
|
+
if (!f || typeof f !== 'object') return
|
|
811
|
+
const key = String(f.file || '') + '|' + String(f.dimension || '') + '|' + String(f.summary || '')
|
|
812
|
+
if (gateSeen[key]) return
|
|
813
|
+
gateSeen[key] = true
|
|
814
|
+
gateFindings.push(f)
|
|
815
|
+
}
|
|
816
|
+
for (const a of architectural) gatePush(a)
|
|
817
|
+
for (const e of gateAll) if (!gateClosed[e.key]) gatePush(e.f)
|
|
818
|
+
const gateSeries = Object.create(null)
|
|
819
|
+
for (const dim of dims) gateSeries[dim] = new Array(rounds.length).fill(0)
|
|
820
|
+
for (let i = 0; i < rounds.length; i++) {
|
|
821
|
+
for (const f of (roundReports[i] || [])) {
|
|
822
|
+
if (f && typeof f === 'object') {
|
|
823
|
+
const s = gateSeries[String(f.dimension)]
|
|
824
|
+
if (Array.isArray(s) && i < s.length) s[i] += 1
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const gateFixed = Object.create(null)
|
|
829
|
+
const gateFixedSeen = Object.create(null)
|
|
830
|
+
for (const e of gateAll) {
|
|
831
|
+
if (!gateClosed[e.key]) continue
|
|
832
|
+
const key = String(e.f.file || '') + '|' + String(e.f.dimension || '') + '|' + String(e.f.summary || '')
|
|
833
|
+
if (gateFixedSeen[key]) continue
|
|
834
|
+
gateFixedSeen[key] = true
|
|
835
|
+
const d = String(e.f.dimension || '')
|
|
836
|
+
if (d) gateFixed[d] = (gateFixed[d] || 0) + 1
|
|
837
|
+
}
|
|
838
|
+
const gateValidation = validationRecords.map(v => ({
|
|
839
|
+
command: v.command,
|
|
840
|
+
exitCode: (typeof v.exitCode === 'number' && isFinite(v.exitCode)) ? Math.floor(v.exitCode) : 1,
|
|
841
|
+
}))
|
|
842
|
+
await agent(
|
|
843
|
+
'Call iterate_quality_gate({operation:"compute", dimensions:' + JSON.stringify(dims) +
|
|
844
|
+
', findings:' + JSON.stringify(gateFindings) +
|
|
845
|
+
', findingsByRound:' + JSON.stringify(gateSeries) +
|
|
846
|
+
', fixedByDimension:' + JSON.stringify(gateFixed) +
|
|
847
|
+
', validationResults:' + JSON.stringify(gateValidation) + '}) and return {ok, snapshot, warnings}.',
|
|
848
|
+
Object.assign({ label: 'quality-gate:compute' }, backend)
|
|
849
|
+
)
|
|
850
|
+
if (!abortedByValidation && !schemaFailed && !validationUnavailable) {
|
|
516
851
|
// Iteration finished cleanly → clear the checkpoint so the next run starts fresh.
|
|
517
852
|
await agent(
|
|
518
853
|
'Call iterate_checkpoint({ operation: "clear" }) and return {ok, existed}.',
|
|
519
|
-
{ label: 'checkpoint:clear' }
|
|
854
|
+
Object.assign({ label: 'checkpoint:clear' }, backend)
|
|
520
855
|
)
|
|
521
856
|
}
|
|
522
857
|
// Persist the run's observatory transcript (threads, trend, fixes, checkpoint)
|
|
@@ -526,7 +861,7 @@ if (!abortedByValidation && !schemaFailed) {
|
|
|
526
861
|
// would hide the very thing F5 is meant to resume. Reflect what is on disk.
|
|
527
862
|
const obsCheckpoint = {
|
|
528
863
|
mode: 'normal',
|
|
529
|
-
round:
|
|
864
|
+
round: lastRound,
|
|
530
865
|
maxRounds: maxRounds,
|
|
531
866
|
fixedCount: fixedCount,
|
|
532
867
|
resumeCount: effectiveResumeCount,
|
|
@@ -535,22 +870,35 @@ if (!abortedByValidation && !schemaFailed) {
|
|
|
535
870
|
? 'schema_invalid'
|
|
536
871
|
: abortedByValidation
|
|
537
872
|
? (configErrors.length > 0 ? 'aborted_by_config' : 'aborted_by_validation')
|
|
538
|
-
|
|
873
|
+
// Clean stop with ZERO recorded validations = an UNVERIFIED run (the
|
|
874
|
+
// explicit args.unverified opt-in from preflight). Never report it as a
|
|
875
|
+
// verified \`converged\`/\`max_rounds_reached\` — the suffix marks that no
|
|
876
|
+
// test ever ran (#2).
|
|
877
|
+
: (validationRecords.length === 0
|
|
878
|
+
? (converged ? 'converged_unverified' : 'max_rounds_unverified')
|
|
879
|
+
: (converged ? 'converged' : 'max_rounds_reached'))
|
|
539
880
|
await agent(
|
|
540
|
-
'Call iterate_transcript({operation:"capture", mode:"normal", goal:' + JSON.stringify(plan.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' +
|
|
541
|
-
{ label: 'transcript:capture' }
|
|
881
|
+
'Call iterate_transcript({operation:"capture", mode:"normal", goal:' + JSON.stringify(plan.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' + lastRound + ', findingsByRound:' + JSON.stringify(rounds.map(rr => (rr.findings && rr.findings.length) ? rr.findings.length : 0)) + ', stoppedReason:"' + stopReason + '", fixes:' + JSON.stringify(fixRecords) + ', checkpoint:' + JSON.stringify(obsCheckpoint) + ', validations:' + JSON.stringify(validationRecords) + ', rounds:' + JSON.stringify(rounds.map(rr => ({ round: rr.round, findings: rr.findings, readFiles: rr.readFiles }))) + '}). Return {operation:"ok"}.',
|
|
882
|
+
Object.assign({ label: 'transcript:capture' }, backend)
|
|
542
883
|
)
|
|
543
884
|
return {
|
|
544
885
|
mode: 'normal',
|
|
545
886
|
goal: plan.goal,
|
|
546
|
-
roundsExecuted:
|
|
887
|
+
roundsExecuted: lastRound,
|
|
547
888
|
maxRounds: maxRounds,
|
|
548
889
|
converged: converged,
|
|
549
890
|
abortedByValidation: abortedByValidation,
|
|
550
891
|
schemaFailed: schemaFailed,
|
|
892
|
+
validationUnavailable: validationUnavailable,
|
|
551
893
|
failedCommands: failedCommands,
|
|
552
894
|
configErrors: configErrors,
|
|
553
895
|
stoppedReason: stopReason,
|
|
896
|
+
// #2 close-out: the caller/UI can tell a verified run from one where no
|
|
897
|
+
// validation command ever executed, and which commands were in play.
|
|
898
|
+
zeroValidation: validationRecords.length === 0,
|
|
899
|
+
unverified: unverified,
|
|
900
|
+
validationCommands: validationCmds,
|
|
901
|
+
validations: validationRecords,
|
|
554
902
|
findingsFixed: fixedCount,
|
|
555
903
|
remainingArchitecturalCount: architectural.length,
|
|
556
904
|
remainingArchitectural: architectural,
|
|
@@ -566,16 +914,18 @@ return {
|
|
|
566
914
|
\`\`\`
|
|
567
915
|
|
|
568
916
|
Key rules for normal mode:
|
|
917
|
+
- **Preflight before ANY work**: read \`validation.commands\` via \`iterate_config({})\` and abort with the actionable message when it is empty/missing (it points at \`iterate_config({operation:"write"...})\`) — unless the caller explicitly passed \`args.unverified === true\`. An unverified run is labelled everywhere (log line, \`unverified:true\`/\`zeroValidation:true\` in the return, \`*_unverified\` stoppedReason). "No commands configured" must NEVER read as a green, converged run.
|
|
569
918
|
- Fixers are the ONLY agents allowed to write files, and they must go through \`iterate_fix\` — never edit files directly. That is what gives every change a backup, a diff, and a rollback path. Reviewers read only. Architectural findings are reported, never auto-fixed.
|
|
570
919
|
- Aggregate the current round deterministically (\`report.findings\`) before fixing, so fixes act on deduped/filtered/sorted findings.
|
|
571
920
|
- **Schema validation & retry**: when \`reviewer.output_schema_validation\` is on (default), retry the round's reviewers up to 2 times (3 attempts total) when \`aggregate\` reports \`schemaValidation\` valid=false for it, then re-aggregate. Never forward schema-invalid findings into \`iterate_fix\`. If the output is still schema-invalid after the 3rd attempt — or every reviewer subagent failed outright (no usable output at all) — the round is INCONCLUSIVE: \`break\` the loop, log a \`round_failed\` entry, set \`schemaFailed = true\`, and report \`stoppedReason:"schema_invalid"\`/\`"no_usable_reviewer_output"\`. Never report an inconclusive round as a clean convergence, and keep the checkpoint so the run can be resumed after the reviewer configuration is fixed.
|
|
572
921
|
- Apply atomic fixes **per file**: one fixer agent handles all findings for a given file serially (so the same file is never edited concurrently); different files are fixed in parallel.
|
|
573
|
-
- **Resume**:
|
|
574
|
-
- **Validate after every round** of fixes; on ANY validation failure, roll back the round's fixes via \`iterate_rollback\` and stop (the checkpoint is left in place so the run can be resumed). EXCEPTION: \`iterate_validate\` returning \`allowed:false\` means the command is simply not in \`validation.commands\` — a config gap, not broken code. In that case abort WITHOUT rolling back the round's fixes and surface the missing command + file so the user can fix the trust list.
|
|
575
|
-
- **Checkpoint after every round**; clear it only when the iteration completes cleanly.
|
|
576
|
-
- Stop when a round produces nothing to fix (converged), maxReviewRounds is reached, validation aborts the run, or a round yields no usable reviewer output (\`stoppedReason:"schema_invalid"\`).
|
|
922
|
+
- **Resume**: read the checkpoint with \`operation:"resume"\` (the tool loads it, bumps \`resumeCount\` AND persists it back — that is what keeps a run interrupted again before its first save counted; never add +1 yourself on top of the tool's bump); fall back to a plain \`operation:"load"\` only when there is no checkpoint to resume. If a previous run left one, continue from \`checkpoint.round + 1\` (its \`fixedCount\` and deduped \`findings\` are carried forward).
|
|
923
|
+
- **Validate after every round** of fixes; on ANY validation failure, roll back the round's fixes via \`iterate_rollback\` and stop (the checkpoint is left in place so the run can be resumed). A rollback must also flip that round's fix records to \`success:false\` and decrement \`fixedCount\`, so \`findingsFixed\` and the final capture never count rolled-back work as applied. EXCEPTION: \`iterate_validate\` returning \`allowed:false\` means the command is simply not in \`validation.commands\` — a config gap, not broken code. In that case abort WITHOUT rolling back the round's fixes and surface the missing command + file so the user can fix the trust list. And if the validate SUBAGENT itself fails (resolves null / no \`results\`), validation did NOT run: stop with \`validationUnavailable = true\` (and \`abortedByValidation = true\`) BEFORE the checkpoint save — never collapse a failed validator into an empty, apparently-passing result. The SAME fail-closed rule applies to an EMPTY result set (\`results: []\`): zero commands executed means zero verification, so log \`reason:"zero_validation_results"\`, set \`validationUnavailable = true\`, record the defense event, and stop — an empty result is not a pass.
|
|
924
|
+
- **Checkpoint after every round**; clear it only when the iteration completes cleanly (never after a validation, schema, or unavailable-validator abort — the checkpoint must stay for a resume).
|
|
925
|
+
- Stop when a round produces nothing to fix (converged), maxReviewRounds is reached, validation aborts the run (including an unavailable validator: \`stoppedReason:"aborted_by_validation"\` with \`validationUnavailable:true\`), or a round yields no usable reviewer output (\`stoppedReason:"schema_invalid"\`). A run that ends cleanly having executed ZERO validation commands reports \`stoppedReason:"converged_unverified"\`/\`"max_rounds_unverified"\` — never a plain \`converged\`/\`"max_rounds_reached"\`.
|
|
577
926
|
- Every round, every rollback, and the final report go to the append-only decision log.
|
|
578
|
-
- Close with \`
|
|
927
|
+
- **Close-out for the command-center tabs (F8/F9/F10)**: after the report compute the quality certificate with \`iterate_quality_gate({operation:"compute", dimensions, findings, findingsByRound, fixedByDimension, validationResults})\` (label \`quality-gate:compute\`) — pass only findings still OPEN (architectural + findings no successful fix closed), the per-dimension series, and the recorded validations (a null exitCode counts as failed). During the loop: every validation-passed fix batch is banked via \`iterate_experience add\` (the tool dedupes pattern+dimension into hit counts), and every rollback / precondition failure (validator failed, zero results, allow-list gap, schema-invalid round) or falsified assumption (a fix \`iterate_fix\` refused) is recorded with \`recordDefense\`. Dry-run computes ONLY the quality gate — it applies no fixes (no F9) and triggers no defenses (no F10), because it writes nothing outside the transcript.
|
|
928
|
+
- Close with \`iterate_status\` metrics and surface the convergence indicators (fixed count, remaining architectural count, abort reason, verified vs UNVERIFIED) in the final summary.
|
|
579
929
|
|
|
580
930
|
### Finding schema (for reviewer agents)
|
|
581
931
|
{ "dimension": string, "file": string (relative path), "line": number (REQUIRED for line-targeted issues — the exact line you READ; use 0 for whole-file/module-level issues),
|