iterate-plugin 3.5.2 → 3.5.4

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.
Files changed (43) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +3 -3
  3. package/dist/session-hooks.js +48 -4
  4. package/dist/skill-prompt.js +63 -29
  5. package/dist/tools/checkpoint.js +39 -5
  6. package/dist/tools/config.js +11 -2
  7. package/dist/tools/context.js +5 -1
  8. package/dist/tools/defense-events.js +48 -9
  9. package/dist/tools/defense-store.js +63 -5
  10. package/dist/tools/experience-bank.js +14 -11
  11. package/dist/tools/experience-store.js +79 -14
  12. package/dist/tools/fix.js +30 -10
  13. package/dist/tools/history.js +5 -3
  14. package/dist/tools/prune.js +197 -29
  15. package/dist/tools/quality-gate.js +3 -0
  16. package/dist/tools/quality-store.js +8 -2
  17. package/dist/tools/review.js +17 -3
  18. package/dist/tools/transcript.js +72 -15
  19. package/dist/tools/triage.js +35 -2
  20. package/dist/transcript.js +30 -7
  21. package/lib/client.js +89 -21
  22. package/lib/parse.js +27 -2
  23. package/package.json +9 -7
  24. package/src/client/index.ts +53 -8
  25. package/src/session-hooks.ts +50 -4
  26. package/src/skill-prompt.ts +63 -29
  27. package/src/tools/checkpoint.ts +45 -6
  28. package/src/tools/config.ts +11 -2
  29. package/src/tools/context.ts +5 -1
  30. package/src/tools/defense-events.ts +50 -9
  31. package/src/tools/defense-store.ts +64 -8
  32. package/src/tools/experience-bank.ts +14 -11
  33. package/src/tools/experience-store.ts +88 -23
  34. package/src/tools/fix.ts +32 -12
  35. package/src/tools/history.ts +5 -3
  36. package/src/tools/prune.ts +204 -28
  37. package/src/tools/quality-gate.ts +4 -0
  38. package/src/tools/quality-store.ts +7 -2
  39. package/src/tools/review.ts +16 -3
  40. package/src/tools/transcript.ts +71 -14
  41. package/src/tools/triage.ts +36 -2
  42. package/src/transcript.ts +32 -7
  43. package/src/types.ts +2 -0
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import { iterateDir } from "../paths.js";
10
10
  import { writeJsonAtomic } from "../atomic-fs.js";
11
+ import { hashString } from "./fix.js";
11
12
  const DEFENSE_EVENTS_FILE = 'defense-events.json';
12
13
  /** Valid defense event types (must stay in sync with DefenseEventType). */
13
14
  const VALID_EVENT_TYPES = new Set([
@@ -39,11 +40,45 @@ function emptyStream() {
39
40
  },
40
41
  };
41
42
  }
43
+ /** Valid severity values (kept in sync with DefenseEvent). */
44
+ const VALID_SEVERITIES = new Set(['critical', 'high', 'medium', 'low']);
45
+ /**
46
+ * Normalize one persisted event. Hand-edited files can carry events missing
47
+ * `timestamp`/`round`/`description`/`defense`/`outcome`/`severity` — readers
48
+ * (list sort by timestamp, render label selection) must never crash or emit
49
+ * NaN for those. Returns null when the entry is not an object or has no usable
50
+ * `type`; otherwise fills every required field with a safe default. An event
51
+ * missing its `id` gets a DETERMINISTIC id derived from its content and
52
+ * position (never a random one) so a read→write cycle cannot silently churn
53
+ * ids on every mutation.
54
+ */
55
+ function normalizeEvent(raw, index) {
56
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
57
+ return null;
58
+ const e = raw;
59
+ if (typeof e.type !== 'string' || !VALID_EVENT_TYPES.has(e.type))
60
+ return null;
61
+ const derivedId = `def-${hashString(`${index}|${String(e.timestamp ?? '')}|${e.type}|${String(e.description ?? '')}|${String(e.defense ?? '')}`)}`;
62
+ return {
63
+ id: typeof e.id === 'string' && e.id ? e.id : derivedId,
64
+ timestamp: typeof e.timestamp === 'string' && e.timestamp ? e.timestamp : new Date().toISOString(),
65
+ round: typeof e.round === 'number' && Number.isFinite(e.round) ? Math.floor(e.round) : 0,
66
+ type: e.type,
67
+ description: typeof e.description === 'string' ? e.description : '',
68
+ defense: typeof e.defense === 'string' ? e.defense : '',
69
+ outcome: typeof e.outcome === 'string' ? e.outcome : '',
70
+ ...(typeof e.file === 'string' && e.file.length > 0 ? { file: e.file } : {}),
71
+ ...(typeof e.line === 'number' && Number.isFinite(e.line) && e.line >= 0 ? { line: e.line } : {}),
72
+ severity: VALID_SEVERITIES.has(String(e.severity)) ? e.severity : 'low',
73
+ };
74
+ }
42
75
  /**
43
76
  * Read the defense events stream from disk.
44
77
  * Normalizes the persisted stream so a hand-edited / partial file can never
45
78
  * produce NaN counts: `counts` is recomputed from the events when missing or
46
- * malformed, and every type key is guaranteed present.
79
+ * malformed, every type key is guaranteed present, and every surviving event is
80
+ * shape-normalized so consumers (timestamp sort, render label selection) cannot
81
+ * throw on missing fields.
47
82
  */
48
83
  export function readDefenseEvents(projectRoot) {
49
84
  const filePath = path.join(projectRoot, '.iterate', DEFENSE_EVENTS_FILE);
@@ -51,7 +86,9 @@ export function readDefenseEvents(projectRoot) {
51
86
  const content = fs.readFileSync(filePath, 'utf-8');
52
87
  const parsed = JSON.parse(content);
53
88
  if (parsed && Array.isArray(parsed.events)) {
54
- const events = parsed.events.filter((e) => !!e && typeof e === 'object' && typeof e.type === 'string');
89
+ const events = parsed.events
90
+ .map((raw, i) => normalizeEvent(raw, i))
91
+ .filter((e) => e !== null);
55
92
  const counts = computeCounts(events);
56
93
  return {
57
94
  events,
@@ -87,13 +124,34 @@ export function writeDefenseEvents(projectRoot, stream) {
87
124
  }
88
125
  return { ok: true };
89
126
  }
127
+ /**
128
+ * Clear the persisted defense-event stream (`.iterate/defense-events.json`).
129
+ * Mirrors `clearQualityGate` in quality-store.ts: a stale event stream from a
130
+ * previous iteration must be resettable before a fresh run. Returns whether a
131
+ * file existed and was removed, or a structured error when removal fails.
132
+ */
133
+ export function clearDefenseEvents(projectRoot) {
134
+ const filePath = path.join(iterateDir(projectRoot), DEFENSE_EVENTS_FILE);
135
+ const existed = fs.existsSync(filePath);
136
+ if (!existed)
137
+ return { ok: true, existed: false };
138
+ try {
139
+ fs.rmSync(filePath, { force: true });
140
+ }
141
+ catch (err) {
142
+ return { ok: false, error: `unable to remove ${filePath}: ${String(err)}` };
143
+ }
144
+ return { ok: true, existed: true };
145
+ }
90
146
  /** Add a defense event to the stream. */
91
147
  export function addDefenseEvent(stream, event) {
92
- const id = `def-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
148
+ // Spread the caller-shaped event FIRST so the store-generated `id` and
149
+ // `timestamp` always win — a caller-supplied id/timestamp must never
150
+ // override the stream's own identity fields.
93
151
  const newEvent = {
94
- id,
95
- timestamp: new Date().toISOString(),
96
152
  ...event,
153
+ id: `def-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
154
+ timestamp: new Date().toISOString(),
97
155
  };
98
156
  // Always recompute from the events array instead of mutating a possibly
99
157
  // stale/malformed persisted `counts` object — guarantees the stream counts
@@ -67,9 +67,12 @@ function normalizeExperienceInput(raw) {
67
67
  export function registerExperienceBankTool(ctx) {
68
68
  ctx.tools.register(defineTool({
69
69
  name: 'iterate_experience',
70
- // List/search/get never write; only `add` upserts the bank → read shapes
71
- // join a parallel dispatch group.
72
- isConcurrencySafe: (args) => args.operation !== 'add',
70
+ // List/search/get never write; `add` upserts the bank and `remove`
71
+ // rewrites it → only the read shapes may join a parallel dispatch group.
72
+ isConcurrencySafe: (args) => {
73
+ const op = args.operation;
74
+ return op !== 'add' && op !== 'remove';
75
+ },
73
76
  description: 'Query or extend the experience bank: browse/search historical fixes and patterns, ' +
74
77
  'or record a new verified fix (operation:"add"). ' +
75
78
  'List/search/get return matching entries with hit counts, verified fixes, and related context. ' +
@@ -144,8 +147,8 @@ export function registerExperienceBankTool(ctx) {
144
147
  `Dimension: ${entry.dimension}`,
145
148
  `Description: ${entry.description}`,
146
149
  `Fix: ${entry.verifiedFix}`,
147
- `Files: ${entry.files.join(', ')}`,
148
- `Tags: ${entry.tags.join(', ')}`,
150
+ `Files: ${Array.isArray(entry.files) ? entry.files.join(', ') : ''}`,
151
+ `Tags: ${Array.isArray(entry.tags) ? entry.tags.join(', ') : ''}`,
149
152
  ].join('\n') }];
150
153
  }
151
154
  if (value.operation === 'remove') {
@@ -158,9 +161,9 @@ export function registerExperienceBankTool(ctx) {
158
161
  `Pattern: ${entry.pattern}`,
159
162
  `Description: ${entry.description}`,
160
163
  `Fix: ${entry.verifiedFix}`,
161
- `Files: ${entry.files.join(', ')}`,
164
+ `Files: ${Array.isArray(entry.files) ? entry.files.join(', ') : ''}`,
162
165
  `Hits: ${entry.hitCount}`,
163
- `Tags: ${entry.tags.join(', ')}`,
166
+ `Tags: ${Array.isArray(entry.tags) ? entry.tags.join(', ') : ''}`,
164
167
  ].join('\n') }];
165
168
  }
166
169
  const entries = value.entries ?? [];
@@ -252,7 +255,7 @@ export function registerExperienceBankTool(ctx) {
252
255
  operation: 'get',
253
256
  count: 1,
254
257
  entry: entry,
255
- totalHits: bank.totalHits,
258
+ totalHits: bank.totalHits ?? 0,
256
259
  };
257
260
  }
258
261
  if (operation === 'search' && typeof args.query === 'string') {
@@ -266,7 +269,7 @@ export function registerExperienceBankTool(ctx) {
266
269
  operation: 'search',
267
270
  count: entries.length,
268
271
  entries: entries,
269
- totalHits: bank.totalHits,
272
+ totalHits: bank.totalHits ?? 0,
270
273
  };
271
274
  }
272
275
  // Default: list with optional filters
@@ -275,7 +278,7 @@ export function registerExperienceBankTool(ctx) {
275
278
  entries = entries.filter((e) => e.dimension === args.dimension);
276
279
  }
277
280
  if (Array.isArray(args.tags) && args.tags.length > 0) {
278
- entries = entries.filter((e) => args.tags.every((t) => e.tags.includes(t)));
281
+ entries = entries.filter((e) => args.tags.every((t) => Array.isArray(e.tags) && e.tags.includes(t)));
279
282
  }
280
283
  return {
281
284
  ok: true,
@@ -283,7 +286,7 @@ export function registerExperienceBankTool(ctx) {
283
286
  operation: 'list',
284
287
  count: Math.min(entries.length, limit),
285
288
  entries: entries.slice(0, limit),
286
- totalHits: bank.totalHits,
289
+ totalHits: bank.totalHits ?? 0,
287
290
  };
288
291
  },
289
292
  }));
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import { iterateDir } from "../paths.js";
10
10
  import { writeJsonAtomic } from "../atomic-fs.js";
11
+ import { hashString } from "./fix.js";
11
12
  const EXPERIENCE_FILE = 'experience.json';
12
13
  /** Default empty experience bank. */
13
14
  function emptyBank() {
@@ -17,6 +18,49 @@ function emptyBank() {
17
18
  totalHits: 0,
18
19
  };
19
20
  }
21
+ /** Valid severity values (kept in sync with ExperienceEntry). */
22
+ const VALID_SEVERITIES = new Set(['critical', 'high', 'medium', 'low']);
23
+ /** String array guard for fields that must be arrays (`files`, `tags`). */
24
+ function stringArray(v) {
25
+ if (!Array.isArray(v))
26
+ return [];
27
+ return v.filter((x) => typeof x === 'string');
28
+ }
29
+ /**
30
+ * Normalize one persisted experience entry. A hand-edited bank entry can be
31
+ * missing `files`/`tags` arrays (or `hitCount`) — consumers rendering/searching
32
+ * entries (`render` `.join(', ')`, `searchExperienceEntries` spread) must never
33
+ * throw or emit NaN. Returns null for non-object entries; every required field
34
+ * gets a safe default.
35
+ */
36
+ function normalizeEntry(raw, index) {
37
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
38
+ return null;
39
+ const e = raw;
40
+ const pattern = typeof e.pattern === 'string' ? e.pattern : '';
41
+ const dimension = typeof e.dimension === 'string' ? e.dimension : '';
42
+ // Deterministic id (content + position based) so a read→write cycle never
43
+ // churns random ids on entries that were hand-edited without one.
44
+ const derivedId = `exp-${hashString(`${index}|${pattern}|${dimension}`)}`;
45
+ const id = typeof e.id === 'string' && e.id ? e.id : derivedId;
46
+ if (!pattern && !dimension)
47
+ return null;
48
+ const hitCount = typeof e.hitCount === 'number' && Number.isFinite(e.hitCount) ? e.hitCount : 0;
49
+ return {
50
+ id,
51
+ timestamp: typeof e.timestamp === 'string' ? e.timestamp : new Date().toISOString(),
52
+ dimension,
53
+ pattern,
54
+ description: typeof e.description === 'string' ? e.description : '',
55
+ verifiedFix: typeof e.verifiedFix === 'string' ? e.verifiedFix : '',
56
+ findingSummary: typeof e.findingSummary === 'string' ? e.findingSummary : '',
57
+ files: stringArray(e.files),
58
+ hitCount,
59
+ ...(typeof e.lastHitAt === 'string' ? { lastHitAt: e.lastHitAt } : {}),
60
+ tags: stringArray(e.tags),
61
+ severity: VALID_SEVERITIES.has(String(e.severity)) ? e.severity : 'low',
62
+ };
63
+ }
20
64
  /** Read the experience bank from disk. Returns empty bank if not found. */
21
65
  export function readExperienceBank(projectRoot) {
22
66
  const filePath = path.join(projectRoot, '.iterate', EXPERIENCE_FILE);
@@ -24,7 +68,14 @@ export function readExperienceBank(projectRoot) {
24
68
  const content = fs.readFileSync(filePath, 'utf-8');
25
69
  const parsed = JSON.parse(content);
26
70
  if (parsed && Array.isArray(parsed.entries)) {
27
- return parsed;
71
+ const entries = parsed.entries
72
+ .map((raw, i) => normalizeEntry(raw, i))
73
+ .filter((e) => e !== null);
74
+ return {
75
+ entries,
76
+ lastUpdated: typeof parsed.lastUpdated === 'string' ? parsed.lastUpdated : emptyBank().lastUpdated,
77
+ totalHits: typeof parsed.totalHits === 'number' && Number.isFinite(parsed.totalHits) ? parsed.totalHits : 0,
78
+ };
28
79
  }
29
80
  }
30
81
  catch {
@@ -58,17 +109,23 @@ export function writeExperienceBank(projectRoot, bank) {
58
109
  export function searchExperienceEntries(entries, query, opts = {}) {
59
110
  const lowerQuery = query.toLowerCase();
60
111
  return entries.filter((entry) => {
112
+ if (!entry || typeof entry !== 'object')
113
+ return false;
114
+ const rawEntry = entry;
115
+ const tags = stringArray(rawEntry.tags);
61
116
  // Dimension filter
62
117
  if (opts.dimension && entry.dimension !== opts.dimension) {
63
118
  return false;
64
119
  }
65
120
  // Tags filter (AND logic)
66
121
  if (opts.tags && opts.tags.length > 0) {
67
- if (!opts.tags.every((t) => entry.tags.includes(t))) {
122
+ if (!opts.tags.every((t) => tags.includes(t))) {
68
123
  return false;
69
124
  }
70
125
  }
71
- // Text search across multiple fields
126
+ // Text search across multiple fields. Guards against a hand-edited bank entry
127
+ // whose `files`/`tags` are missing or non-array (the spread below would
128
+ // otherwise throw a TypeError on a non-iterable).
72
129
  if (query) {
73
130
  const searchableText = [
74
131
  entry.pattern,
@@ -76,8 +133,8 @@ export function searchExperienceEntries(entries, query, opts = {}) {
76
133
  entry.verifiedFix,
77
134
  entry.findingSummary,
78
135
  entry.dimension,
79
- ...entry.files,
80
- ...entry.tags,
136
+ ...stringArray(entry.files),
137
+ ...stringArray(entry.tags),
81
138
  ].join(' ').toLowerCase();
82
139
  if (!searchableText.includes(lowerQuery)) {
83
140
  return false;
@@ -89,8 +146,11 @@ export function searchExperienceEntries(entries, query, opts = {}) {
89
146
  /**
90
147
  * Add or update an experience entry.
91
148
  *
92
- * An entry with an `id` that already exists, OR a new entry whose
93
- * `pattern`+`dimension` pair matches an existing entry, is treated as a HIT:
149
+ * An entry with an `id` that already exists is treated as an UPDATE + HIT:
150
+ * the caller-supplied fields replace the stored ones and the hitCount is
151
+ * incremented (lastHitAt refreshed) — this fulfills the documented
152
+ * "update a specific entry via add" contract. A NEW entry whose
153
+ * `pattern`+`dimension` pair matches an existing entry is treated as a HIT:
94
154
  * the matching entry's hitCount is incremented (lastHitAt refreshed) so
95
155
  * repeated encounters of the same pattern do not create duplicates. Otherwise
96
156
  * a fresh entry is appended with hitCount 1. Never mutates the input bank.
@@ -104,11 +164,14 @@ export function upsertExperience(bank, entry) {
104
164
  ? bank.entries.find((e) => e.id === entry.id)
105
165
  : bank.entries.find((e) => e.pattern === entry.pattern && e.dimension === entry.dimension);
106
166
  if (existing) {
107
- const updated = {
108
- ...existing,
109
- hitCount: (existing.hitCount ?? 0) + 1,
110
- lastHitAt: lastUpdated,
111
- };
167
+ // Explicit-id updates REPLACE the editable fields (fulfilling the
168
+ // documented "update a specific entry via add" contract); a pattern+
169
+ // dimension HIT only bumps the hit metadata so a repeat encounter never
170
+ // overwrites the curated entry.
171
+ const byId = typeof entry.id === 'string' && entry.id.length > 0;
172
+ const updated = byId
173
+ ? { ...existing, ...entry, id: existing.id, hitCount: (existing.hitCount ?? 0) + 1, lastHitAt: lastUpdated }
174
+ : { ...existing, hitCount: (existing.hitCount ?? 0) + 1, lastHitAt: lastUpdated };
112
175
  return {
113
176
  bank: {
114
177
  ...bank,
@@ -120,14 +183,16 @@ export function upsertExperience(bank, entry) {
120
183
  entryId: existing.id,
121
184
  };
122
185
  }
123
- // Add new entry
186
+ // Add new entry. Spread the caller input FIRST so the store-generated
187
+ // `id`/`timestamp`/`hitCount`/`lastHitAt` always win — a hostile or
188
+ // malformed input can never forge its own identity or hit count.
124
189
  const id = entry.id || `exp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
125
190
  const newEntry = {
191
+ ...entry,
126
192
  id,
127
193
  timestamp: lastUpdated,
128
194
  hitCount: 1,
129
195
  lastHitAt: lastUpdated,
130
- ...entry,
131
196
  };
132
197
  return {
133
198
  bank: {
package/dist/tools/fix.js CHANGED
@@ -120,9 +120,22 @@ export function readRegistry(projectRoot) {
120
120
  const parsed = JSON.parse(readFileSync(file, 'utf-8'));
121
121
  if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.rounds))
122
122
  return emptyRegistry();
123
- // Defensive: a hand-edited or partially-written registry may contain a
124
- // round without a `records` array — normalize instead of crashing readers.
125
- parsed.rounds = parsed.rounds.filter((r) => r && typeof r === 'object' && Array.isArray(r.records));
123
+ // Defensive normalization: a hand-edited or partially-written registry may
124
+ // contain a round without a `records` array, or records that are missing
125
+ // their id / finding object — all of which would make readers
126
+ // (findFixRecord / recordsForFile / iterate_diff) throw or sum NaN.
127
+ parsed.rounds = parsed.rounds
128
+ .filter((r) => r && typeof r === 'object' && Array.isArray(r.records))
129
+ .map((r) => ({
130
+ ...r,
131
+ round: typeof r.round === 'number' && Number.isFinite(r.round) ? Math.floor(r.round) : 0,
132
+ records: r.records.filter((rec) => !!rec &&
133
+ typeof rec === 'object' &&
134
+ typeof rec.id === 'string' &&
135
+ !!rec.finding &&
136
+ typeof rec.finding === 'object'),
137
+ }))
138
+ .filter((r) => typeof r.round === 'number' && Number.isFinite(r.round));
126
139
  return parsed;
127
140
  }
128
141
  catch {
@@ -611,20 +624,27 @@ export function registerDiffTool(ctx) {
611
624
  for (const r of round.records) {
612
625
  if (!r.success)
613
626
  continue;
614
- const existing = files.find((f) => f.file === r.finding.file);
627
+ const file = typeof r.finding.file === 'string' ? r.finding.file : '';
628
+ if (!file)
629
+ continue;
630
+ // Coerce defensively: a hand-edited registry record missing the
631
+ // numeric fields must not produce NaN in the accumulated summary.
632
+ const added = Number(r.linesAdded) || 0;
633
+ const removed = Number(r.linesRemoved) || 0;
634
+ const existing = files.find((f) => f.file === file);
615
635
  if (existing) {
616
- existing.linesAdded += r.linesAdded;
617
- existing.linesRemoved += r.linesRemoved;
636
+ existing.linesAdded += added;
637
+ existing.linesRemoved += removed;
618
638
  // Recompute the summary from the summed counts so a multi-fix
619
639
  // file's text does not contradict its accumulated numbers.
620
640
  existing.diffSummary = `+${existing.linesAdded}/-${existing.linesRemoved} lines`;
621
641
  }
622
642
  else {
623
643
  files.push({
624
- file: r.finding.file,
625
- diffSummary: r.diffSummary,
626
- linesAdded: r.linesAdded,
627
- linesRemoved: r.linesRemoved,
644
+ file,
645
+ diffSummary: typeof r.diffSummary === 'string' ? r.diffSummary : `+${added}/-${removed} lines`,
646
+ linesAdded: added,
647
+ linesRemoved: removed,
628
648
  });
629
649
  }
630
650
  }
@@ -46,8 +46,10 @@ export function filterDecisionEntries(entries, opts) {
46
46
  export function summarizeFixRegistry(registry) {
47
47
  const rounds = (registry.rounds ?? []).map((r) => ({
48
48
  round: r.round,
49
- fixedCount: r.fixedCount,
50
- failedCount: r.failedCount,
49
+ // Coerce defensively so a hand-edited registry round missing either count
50
+ // can never propagate NaN into the integer output fields.
51
+ fixedCount: Number(r.fixedCount) || 0,
52
+ failedCount: Number(r.failedCount) || 0,
51
53
  }));
52
54
  return {
53
55
  totalFixed: rounds.reduce((s, r) => s + r.fixedCount, 0),
@@ -74,7 +76,7 @@ export function registerHistoryTool(ctx) {
74
76
  type: {
75
77
  type: 'string',
76
78
  description: 'Optional entry-type filter: round_start, review_result, atomic_fix, architectural_fix, ' +
77
- 'revert, validation, decision, report.',
79
+ 'revert, round_failed, validation, decision, report, resume.',
78
80
  },
79
81
  since: {
80
82
  type: 'string',