iterate-plugin 3.5.7 → 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.
Files changed (75) hide show
  1. package/README.md +64 -20
  2. package/README.zh-CN.md +53 -17
  3. package/dist/approval-gate.js +143 -23
  4. package/dist/atomic-fs.js +148 -17
  5. package/dist/config-loader.js +509 -34
  6. package/dist/config-write.js +83 -22
  7. package/dist/evidence.js +141 -87
  8. package/dist/file-lock.js +165 -0
  9. package/dist/index.js +54 -28
  10. package/dist/jobs.js +56 -11
  11. package/dist/live.js +52 -13
  12. package/dist/meta-review.js +132 -28
  13. package/dist/method-scope.js +31 -8
  14. package/dist/review-scope.js +32 -9
  15. package/dist/review.js +96 -17
  16. package/dist/session-hooks.js +55 -13
  17. package/dist/skill-prompt.js +396 -66
  18. package/dist/tools/checkpoint.js +85 -19
  19. package/dist/tools/config.js +44 -2
  20. package/dist/tools/context.js +22 -5
  21. package/dist/tools/decision-log.js +111 -27
  22. package/dist/tools/defense-events.js +90 -31
  23. package/dist/tools/defense-store.js +28 -3
  24. package/dist/tools/experience-bank.js +108 -43
  25. package/dist/tools/experience-store.js +54 -4
  26. package/dist/tools/fix.js +347 -119
  27. package/dist/tools/history.js +68 -14
  28. package/dist/tools/present.js +80 -0
  29. package/dist/tools/prune.js +150 -53
  30. package/dist/tools/quality-gate.js +52 -2
  31. package/dist/tools/quality-store.js +17 -1
  32. package/dist/tools/review.js +158 -7
  33. package/dist/tools/transcript.js +173 -36
  34. package/dist/tools/triage.js +67 -12
  35. package/dist/tools/validate.js +115 -24
  36. package/dist/transcript.js +267 -4
  37. package/lib/client.js +945 -181
  38. package/lib/parse.js +760 -57
  39. package/package.json +14 -12
  40. package/src/approval-gate.ts +160 -23
  41. package/src/atomic-fs.ts +152 -17
  42. package/src/client/index.ts +600 -129
  43. package/src/config-loader.ts +542 -35
  44. package/src/config-write.ts +108 -26
  45. package/src/evidence.ts +194 -92
  46. package/src/file-lock.ts +184 -0
  47. package/src/index.ts +53 -29
  48. package/src/jobs.ts +63 -12
  49. package/src/live.ts +48 -11
  50. package/src/meta-review.ts +151 -34
  51. package/src/method-scope.ts +31 -8
  52. package/src/review-scope.ts +33 -10
  53. package/src/review.ts +96 -17
  54. package/src/session-hooks.ts +54 -12
  55. package/src/skill-prompt.ts +396 -66
  56. package/src/tools/checkpoint.ts +81 -19
  57. package/src/tools/config.ts +44 -2
  58. package/src/tools/context.ts +25 -5
  59. package/src/tools/decision-log.ts +115 -22
  60. package/src/tools/defense-events.ts +87 -31
  61. package/src/tools/defense-store.ts +30 -3
  62. package/src/tools/experience-bank.ts +112 -44
  63. package/src/tools/experience-store.ts +54 -5
  64. package/src/tools/fix.ts +258 -20
  65. package/src/tools/history.ts +74 -15
  66. package/src/tools/present.ts +89 -0
  67. package/src/tools/prune.ts +154 -51
  68. package/src/tools/quality-gate.ts +57 -2
  69. package/src/tools/quality-store.ts +18 -1
  70. package/src/tools/review.ts +155 -6
  71. package/src/tools/transcript.ts +178 -36
  72. package/src/tools/triage.ts +70 -13
  73. package/src/tools/validate.ts +138 -32
  74. package/src/transcript.ts +279 -4
  75. package/src/types.ts +29 -1
package/lib/client.js CHANGED
@@ -60,6 +60,16 @@ function safeKeys(o) {
60
60
  return [];
61
61
  }
62
62
  }
63
+ function coerceJsonNode(v) {
64
+ if (typeof v !== "string") return v;
65
+ const t = v.trim();
66
+ if (!t.startsWith("{") && !t.startsWith("[")) return v;
67
+ try {
68
+ return JSON.parse(t);
69
+ } catch {
70
+ return v;
71
+ }
72
+ }
63
73
  function scanSessionForResume(obj, seen, maxDepth = 20) {
64
74
  if (maxDepth <= 0) return 0;
65
75
  if (!obj || typeof obj !== "object") return 0;
@@ -213,19 +223,16 @@ function scanSessionForReport(session) {
213
223
  const call = calls[i];
214
224
  if (!call) continue;
215
225
  if (safeGet(call, "tool") === "iterate_review" || String(safeGet(call, "tool") ?? "").endsWith("iterate_review")) {
216
- const result = safeGet(call, "result");
226
+ const result = coerceJsonNode(safeGet(call, "result"));
217
227
  if (result && typeof result === "object") {
218
228
  const r = (
219
229
  /** @type {Record<string, unknown>} */
220
230
  result
221
231
  );
222
232
  const report = safeGet(r, "report");
223
- if (report && typeof report === "object") {
224
- return (
225
- /** @type {Record<string, unknown>} */
226
- report
227
- );
228
- }
233
+ if (isReviewReport(report)) return report;
234
+ const found = findReportInObject(result);
235
+ if (found) return found;
229
236
  }
230
237
  }
231
238
  }
@@ -244,8 +251,24 @@ function scanSessionForReport(session) {
244
251
  /** @type {Array<Record<string, unknown>>} */
245
252
  msgCalls
246
253
  );
247
- for (const call of calls) {
254
+ for (let j = calls.length - 1; j >= 0; j--) {
255
+ const call = calls[j];
248
256
  if (!call) continue;
257
+ const callName = String(safeGet(call, "name") ?? safeGet(call, "tool") ?? "");
258
+ if (callName === "iterate_review" || callName.endsWith("iterate_review")) {
259
+ for (const key of ["result", "response", "message"]) {
260
+ const node = coerceJsonNode(safeGet(call, key));
261
+ if (!node || typeof node !== "object") continue;
262
+ const r = (
263
+ /** @type {Record<string, unknown>} */
264
+ node
265
+ );
266
+ const report = safeGet(r, "report");
267
+ if (isReviewReport(report)) return report;
268
+ const found = findReportInObject(node);
269
+ if (found) return found;
270
+ }
271
+ }
249
272
  const fn = safeGet(call, "function");
250
273
  if (fn && typeof fn === "object") {
251
274
  const f = (
@@ -402,10 +425,29 @@ function scanSessionForTranscript(session) {
402
425
  if (!msg) continue;
403
426
  const calls = safeGet(msg, "tool_calls");
404
427
  if (Array.isArray(calls)) {
405
- for (const call of calls) {
428
+ const callList = (
429
+ /** @type {Array<Record<string, unknown>>} */
430
+ calls
431
+ );
432
+ for (let j = callList.length - 1; j >= 0; j--) {
433
+ const call = callList[j];
406
434
  if (!call) continue;
407
- const args = safeGet(call, "arguments");
408
- if (typeof args === "string") {
435
+ const fn = safeGet(call, "function");
436
+ const candidates = [
437
+ // Result surface first: `{ name|tool, result }` message tool calls
438
+ // (the same shape scanSessionForQualityGate already accepts).
439
+ safeGet(call, "result"),
440
+ safeGet(call, "response"),
441
+ safeGet(call, "message"),
442
+ safeGet(call, "arguments"),
443
+ fn && typeof fn === "object" ? safeGet(
444
+ /** @type {Record<string, unknown>} */
445
+ fn,
446
+ "arguments"
447
+ ) : void 0
448
+ ];
449
+ for (const args of candidates) {
450
+ if (args === void 0 || args === null) continue;
409
451
  const found2 = extractTranscript(args);
410
452
  if (found2) return found2;
411
453
  }
@@ -417,6 +459,34 @@ function scanSessionForTranscript(session) {
417
459
  }
418
460
  return null;
419
461
  }
462
+ function normalizeValidations(raw) {
463
+ if (!Array.isArray(raw)) return [];
464
+ const out = [];
465
+ for (
466
+ const v of
467
+ /** @type {unknown[]} */
468
+ raw
469
+ ) {
470
+ if (!v || typeof v !== "object") continue;
471
+ const rec = (
472
+ /** @type {Record<string, unknown>} */
473
+ v
474
+ );
475
+ const roundRaw = Number(rec.round);
476
+ const exitRaw = rec.exitCode;
477
+ const command = typeof rec.command === "string" ? rec.command : String(rec.command ?? "");
478
+ if (!command) continue;
479
+ const rejectReason = typeof rec.rejectReason === "string" ? rec.rejectReason : "";
480
+ out.push({
481
+ round: Number.isFinite(roundRaw) && roundRaw > 0 ? Math.floor(roundRaw) : 0,
482
+ command,
483
+ exitCode: typeof exitRaw === "number" && Number.isFinite(exitRaw) ? exitRaw : null,
484
+ allowed: rec.allowed === true,
485
+ ...rejectReason ? { rejectReason } : {}
486
+ });
487
+ }
488
+ return out;
489
+ }
420
490
  function normalizeTranscript(manifest) {
421
491
  const src = manifest && typeof manifest === "object" ? (
422
492
  /** @type {Record<string, unknown>} */
@@ -481,6 +551,9 @@ function normalizeTranscript(manifest) {
481
551
  })),
482
552
  checkpoint,
483
553
  timeline: asArray(safeGet(src, "timeline")).map((t) => ({ ...t })),
554
+ // #6: per-round validation rows (command / exitCode / allowed), captured
555
+ // by the workflow and persisted through `iterate_transcript capture`.
556
+ validations: normalizeValidations(safeGet(src, "validations")),
484
557
  nudge,
485
558
  approval: ap && typeof ap === "object" ? { active: asBool(safeGet(ap, "active")), policy: asStr(safeGet(ap, "policy")) || "ask" } : { active: false, policy: "ask" }
486
559
  };
@@ -515,6 +588,11 @@ function isDefenseEventsResult(obj) {
515
588
  }
516
589
  function findFirstInObject(obj, predicate, seen, maxDepth = 20) {
517
590
  if (maxDepth <= 0) return null;
591
+ if (typeof obj === "string") {
592
+ const parsed = coerceJsonNode(obj);
593
+ if (parsed === obj) return null;
594
+ return findFirstInObject(parsed, predicate, seen, maxDepth - 1);
595
+ }
518
596
  if (!obj || typeof obj !== "object") return null;
519
597
  const s = seen || /* @__PURE__ */ new Set();
520
598
  if (s.has(obj)) return null;
@@ -551,7 +629,7 @@ function findExperienceResultInObject(obj) {
551
629
  function findDefenseResultInObject(obj) {
552
630
  return findFirstInObject(obj, (o) => isDefenseEventsResult(o));
553
631
  }
554
- function latestToolResultNode(session, toolName) {
632
+ function latestToolResultNode(session, toolName, find) {
555
633
  if (!session || typeof session !== "object") return null;
556
634
  const s = (
557
635
  /** @type {Record<string, unknown>} */
@@ -569,9 +647,9 @@ function latestToolResultNode(session, toolName) {
569
647
  const tool = String(safeGet(call, "tool") ?? "");
570
648
  if (tool !== toolName && !tool.endsWith(toolName)) continue;
571
649
  const result = safeGet(call, "result");
572
- if (result !== void 0 && result !== null) return result;
650
+ if (result !== void 0 && result !== null && find(result)) return result;
573
651
  const message = safeGet(call, "message");
574
- if (message !== void 0 && message !== null) return message;
652
+ if (message !== void 0 && message !== null && find(message)) return message;
575
653
  }
576
654
  }
577
655
  const messages = safeGet(s, "messages");
@@ -595,14 +673,15 @@ function latestToolResultNode(session, toolName) {
595
673
  const name2 = String(safeGet(call, "name") ?? safeGet(call, "tool") ?? "");
596
674
  if (name2 !== toolName && !name2.endsWith(toolName)) continue;
597
675
  const result = safeGet(call, "result") ?? safeGet(call, "response") ?? safeGet(call, "message");
598
- if (result !== void 0 && result !== null) return result;
676
+ if (result !== void 0 && result !== null && find(result)) return result;
599
677
  }
600
678
  }
601
679
  for (let i = msgs.length - 1; i >= 0; i--) {
602
680
  const msg = msgs[i];
603
681
  if (!msg) continue;
604
682
  const content = safeGet(msg, "content");
605
- if (content !== void 0 && content !== null) return content;
683
+ if (content === void 0 || content === null) continue;
684
+ if (find(content)) return content;
606
685
  }
607
686
  }
608
687
  return null;
@@ -651,7 +730,7 @@ function normalizeQualityGateSnapshot(raw) {
651
730
  };
652
731
  }
653
732
  function scanSessionForQualityGate(session) {
654
- const node = latestToolResultNode(session, "iterate_quality_gate");
733
+ const node = latestToolResultNode(session, "iterate_quality_gate", findQualityGateInObject);
655
734
  if (node === null) return null;
656
735
  const found = findQualityGateInObject(node);
657
736
  if (!found) return null;
@@ -701,7 +780,7 @@ function normalizeExperienceBankResult(raw) {
701
780
  };
702
781
  }
703
782
  function scanSessionForExperienceBank(session) {
704
- const node = latestToolResultNode(session, "iterate_experience");
783
+ const node = latestToolResultNode(session, "iterate_experience", findExperienceResultInObject);
705
784
  if (node === null) return null;
706
785
  const found = findExperienceResultInObject(node);
707
786
  if (!found) return null;
@@ -757,7 +836,7 @@ function normalizeDefenseEventsResult(raw) {
757
836
  };
758
837
  }
759
838
  function scanSessionForDefenseEvents(session) {
760
- const node = latestToolResultNode(session, "iterate_defense_events");
839
+ const node = latestToolResultNode(session, "iterate_defense_events", findDefenseResultInObject);
761
840
  if (node === null) return null;
762
841
  const found = findDefenseResultInObject(node);
763
842
  if (!found) return null;
@@ -871,13 +950,14 @@ function normalizeReport(report) {
871
950
  );
872
951
  const rounds = (
873
952
  /** @type {Array<unknown>} */
874
- report.rounds ?? []
953
+ Array.isArray(report.rounds) ? report.rounds : []
875
954
  );
876
955
  const findings = (
877
956
  /** @type {Array<Record<string, unknown>>} */
878
- report.findings ?? []
957
+ Array.isArray(report.findings) ? report.findings : []
879
958
  );
880
959
  const totalRounds = typeof convergence.totalRounds === "number" ? convergence.totalRounds : rounds.length;
960
+ const explicitReason = typeof convergence.stoppedReason === "string" && convergence.stoppedReason.trim() ? convergence.stoppedReason : null;
881
961
  const normalizedConvergence = {
882
962
  totalRounds,
883
963
  findingsByRound: Array.isArray(convergence.findingsByRound) ? convergence.findingsByRound : rounds.map((r) => {
@@ -888,7 +968,7 @@ function normalizeReport(report) {
888
968
  return Array.isArray(rr?.findings) ? rr.findings.length : 0;
889
969
  }),
890
970
  converged: convergence.converged === true,
891
- stoppedReason: convergence.stoppedReason ?? (rounds.length < totalRounds ? "converged" : "max_rounds_reached")
971
+ stoppedReason: explicitReason ?? (convergence.converged === true ? "converged" : rounds.length >= totalRounds ? "max_rounds_reached" : null)
892
972
  };
893
973
  let summary = report.summary;
894
974
  if (!summary || typeof summary !== "object") {
@@ -921,17 +1001,18 @@ function normalizeReport(report) {
921
1001
  };
922
1002
  }
923
1003
  function computeSummaryFromFindings(findings) {
1004
+ const list = Array.isArray(findings) ? findings : [];
924
1005
  const counts = { critical: 0, high: 0, medium: 0, low: 0 };
925
- const byDimension = {};
926
- for (const f of findings) {
1006
+ const byDimension = /* @__PURE__ */ Object.create(null);
1007
+ for (const f of list) {
927
1008
  if (!f || typeof f !== "object") continue;
928
1009
  const sev = String(f.severity ?? "low");
929
- if (sev in counts) counts[sev]++;
1010
+ if (Object.prototype.hasOwnProperty.call(counts, sev)) counts[sev]++;
930
1011
  const dim = String(f.dimension ?? "unknown");
931
1012
  byDimension[dim] = (byDimension[dim] ?? 0) + 1;
932
1013
  }
933
1014
  return {
934
- totalFindings: findings.length,
1015
+ totalFindings: list.length,
935
1016
  critical: counts.critical,
936
1017
  high: counts.high,
937
1018
  medium: counts.medium,
@@ -952,7 +1033,7 @@ function computeConvergenceProgress(report) {
952
1033
  function currentRoundNumber(report) {
953
1034
  const rounds = (
954
1035
  /** @type {Array<Record<string, unknown>>} */
955
- report.rounds ?? []
1036
+ Array.isArray(report.rounds) ? report.rounds : []
956
1037
  );
957
1038
  let max = 0;
958
1039
  for (const r of rounds) {
@@ -975,34 +1056,38 @@ function getTotalRounds(report) {
975
1056
  function severityStats(report) {
976
1057
  const findings = (
977
1058
  /** @type {Array<Record<string, unknown>>} */
978
- report.findings ?? []
1059
+ Array.isArray(report.findings) ? report.findings : []
979
1060
  );
980
1061
  const counts = { critical: 0, high: 0, medium: 0, low: 0 };
981
1062
  for (const f of findings) {
982
1063
  if (!f || typeof f !== "object") continue;
983
1064
  const sev = String(f.severity ?? "low");
984
- if (sev in counts) counts[sev]++;
1065
+ if (Object.prototype.hasOwnProperty.call(counts, sev)) counts[sev]++;
985
1066
  }
986
1067
  return counts;
987
1068
  }
988
1069
  function groupByDimension(report) {
989
1070
  const findings = (
990
1071
  /** @type {Array<Record<string, unknown>>} */
991
- report.findings ?? []
1072
+ Array.isArray(report.findings) ? report.findings : []
992
1073
  );
993
- const groups = {};
1074
+ const groups = /* @__PURE__ */ Object.create(null);
994
1075
  for (const f of findings) {
995
1076
  if (!f || typeof f !== "object") continue;
996
1077
  const dim = String(f.dimension ?? "unknown");
997
- if (!groups[dim]) groups[dim] = [];
1078
+ if (!Object.prototype.hasOwnProperty.call(groups, dim)) groups[dim] = [];
998
1079
  groups[dim].push(f);
999
1080
  }
1000
1081
  return groups;
1001
1082
  }
1083
+ var TRIAGE_VERDICTS = (
1084
+ /** @type {const} */
1085
+ ["keep", "skip", "ignore"]
1086
+ );
1002
1087
  function buildTriageState(report) {
1003
1088
  const findings = (
1004
1089
  /** @type {Array<unknown>} */
1005
- report.findings ?? []
1090
+ Array.isArray(report.findings) ? report.findings : []
1006
1091
  );
1007
1092
  const state = {};
1008
1093
  for (let i = 0; i < findings.length; i++) {
@@ -1013,7 +1098,7 @@ function buildTriageState(report) {
1013
1098
  function hashReport(report) {
1014
1099
  const findings = (
1015
1100
  /** @type {Array<Record<string, unknown>>} */
1016
- report.findings ?? []
1101
+ Array.isArray(report.findings) ? report.findings : []
1017
1102
  );
1018
1103
  let h = 2166136261;
1019
1104
  const mix = (s) => {
@@ -1058,17 +1143,27 @@ function buildApplyInstruction(entries) {
1058
1143
  null,
1059
1144
  2
1060
1145
  );
1061
- return `Please call \`iterate_triage\` with the following payload to apply the triage verdicts:
1146
+ return `\u8BF7\u8C03\u7528 \`iterate_triage\` \u5E94\u7528\u4E0B\u9762\u7684 known_intentional \u6761\u76EE\uFF08\u7C98\u8D34\u540E\u7531\u6A21\u578B\u8C03\u7528 \`iterate_triage\` \u7684 \`operation: "apply"\` \u5199\u5165\u914D\u7F6E\uFF1A\u81EA\u52A8\u53BB\u91CD\u3001\u5199\u5165\u524D\u5907\u4EFD\u3001\u5931\u8D25\u81EA\u52A8\u56DE\u6EDA\uFF09\uFF1A
1062
1147
 
1063
1148
  \`\`\`json
1064
1149
  ${payload}
1150
+ \`\`\`
1151
+
1152
+ \u5199\u5165\u5B8C\u6210\u540E\u8BF7\u590D\u5236\u9762\u677F\u7684\u300C\u56DE\u8BFB\u5DF2\u5199\u5165\u6761\u76EE\u300D\u6307\u4EE4\uFF08\`iterate_triage\` \`operation: "list"\`\uFF09\u6838\u5BF9\u7ED3\u679C\u3002`;
1153
+ }
1154
+ function buildTriageReadbackInstruction() {
1155
+ return `\u8BF7\u8C03\u7528 \`iterate_triage\` \u56DE\u8BFB\u5F53\u524D\u5DF2\u5199\u5165\u914D\u7F6E\u7684 known_intentional \u6761\u76EE\uFF0C\u5E76\u628A\u6BCF\u6761\u7684 file / line / dimension / reason \u539F\u6837\u5217\u51FA\u6765\u4F9B\u6211\u6838\u5BF9\uFF1A
1156
+
1157
+ \`\`\`json
1158
+ ${JSON.stringify({ operation: "list" }, null, 2)}
1065
1159
  \`\`\``;
1066
1160
  }
1067
1161
  function collectIgnoredEntries(triageState, findings) {
1162
+ const list = Array.isArray(findings) ? findings : [];
1068
1163
  const entries = [];
1069
1164
  for (const [idx, verdict] of Object.entries(triageState)) {
1070
1165
  if (verdict !== "ignore") continue;
1071
- const finding = findings[Number(idx)];
1166
+ const finding = list[Number(idx)];
1072
1167
  if (!finding) continue;
1073
1168
  entries.push({
1074
1169
  file: String(finding.file ?? ""),
@@ -1079,6 +1174,204 @@ function collectIgnoredEntries(triageState, findings) {
1079
1174
  }
1080
1175
  return entries;
1081
1176
  }
1177
+ var CONTENT_PLACEHOLDER = "<\u7531\u6A21\u578B\u586B\u5199\uFF1A\u4FEE\u590D\u540E\u7684\u5B8C\u6574\u6587\u4EF6\u5185\u5BB9>";
1178
+ var ROUND_PLACEHOLDER = "<\u7531\u6A21\u578B\u586B\u5199\uFF1A\u5F53\u524D\u8FED\u4EE3\u8F6E\u6B21\uFF08\u22651 \u7684\u6574\u6570\uFF09>";
1179
+ var START_INSTRUCTION_FULL = '\u5F00\u59CB\u4E00\u6B21\u5B8C\u6574\u8FED\u4EE3\uFF08\u5BA1\u67E5 \u2192 \u4FEE\u590D \u2192 \u9A8C\u8BC1 \u2192 \u590D\u76D8\uFF09\uFF1A\u8BF7\u8C03\u7528 `workflow` \u5DE5\u5177\u8FD0\u884C iterate \u5DE5\u4F5C\u6D41\uFF0Cmode: "normal"\uFF0C\u6309\u7CFB\u7EDF\u63D0\u793A\u7684 Iterate Workflow \u6267\u884C plan \u2192 \u591A\u7EBF\u7A0B\u5E76\u884C\u8BC4\u5BA1 \u2192 \u5206\u8BCA \u2192 \u539F\u5B50\u4FEE\u590D \u2192 \u6BCF\u8F6E\u9A8C\u8BC1 \u2192 \u5FAA\u73AF\u76F4\u81F3\u6536\u655B\u6216\u8FBE\u5230 max_rounds \u2192 \u7EC8\u62A5\u5E76 `iterate_transcript` capture\u3002\u5F00\u8DD1\u524D\u5148 `iterate_config({operation:"read"})` \u68C0\u67E5 validation.commands\uFF1A\u82E5\u4E3A\u7A7A\uFF0C\u5148\u505C\u4E0B\u6765\u63D0\u793A\u6211\u7528 `iterate_config` \u5199\u5165\u9A8C\u8BC1\u547D\u4EE4\uFF0C\u5426\u5219\u672C\u8F6E\u8FED\u4EE3\u4E0D\u53D7\u4EFB\u4F55\u6D4B\u8BD5\u4FDD\u62A4\u3002';
1180
+ var START_INSTRUCTION_REVIEW_ONLY = '\u5F00\u59CB\u4E00\u6B21\u4EC5\u8BC4\u5BA1\uFF08dry-run\uFF0C\u4E0D\u4FEE\u6539\u4EFB\u4F55\u6587\u4EF6\uFF09\uFF1A\u8BF7\u8C03\u7528 `workflow` \u5DE5\u5177\u8FD0\u884C iterate \u5DE5\u4F5C\u6D41\uFF0Cmode: "dry-run"\uFF0C\u6309\u7CFB\u7EDF\u63D0\u793A\u7684 Iterate Workflow \u6267\u884C plan \u2192 \u591A\u7EBF\u7A0B\u5E76\u884C\u8BC4\u5BA1 \u2192 \u5206\u8BCA \u2192 `iterate_review aggregate` / `meta-review` \u7EC8\u62A5\u5E76 `iterate_transcript` capture\uFF1B\u5168\u7A0B\u4E0D\u8981\u8C03\u7528 `iterate_fix` / `iterate_rollback`\u3002';
1181
+ var START_INSTRUCTIONS = {
1182
+ full: START_INSTRUCTION_FULL,
1183
+ reviewOnly: START_INSTRUCTION_REVIEW_ONLY
1184
+ };
1185
+ function fixFindingPayload(finding) {
1186
+ const f = finding && typeof finding === "object" ? finding : {};
1187
+ const file = String(f.file ?? "");
1188
+ return {
1189
+ file,
1190
+ ...typeof f.line === "number" && f.line > 0 ? { line: f.line } : {},
1191
+ dimension: String(f.dimension ?? ""),
1192
+ severity: String(f.severity ?? ""),
1193
+ summary: String(f.summary ?? ""),
1194
+ ...f.failure_scenario ? { failure_scenario: String(f.failure_scenario) } : {},
1195
+ ...f.suggested_fix ? { suggested_fix: String(f.suggested_fix) } : {}
1196
+ };
1197
+ }
1198
+ function buildFixInstruction(finding, opts) {
1199
+ const payload = fixFindingPayload(finding);
1200
+ if (!payload.file) return "";
1201
+ const round = opts && Number.isInteger(opts.round) ? opts.round : null;
1202
+ const call = {
1203
+ file: payload.file,
1204
+ content: CONTENT_PLACEHOLDER,
1205
+ finding: payload,
1206
+ round: round !== null ? round : ROUND_PLACEHOLDER
1207
+ };
1208
+ return [
1209
+ "\u8BF7\u4FEE\u590D\u4E0B\u9762\u8FD9\u4E00\u4E2A finding\u3002`iterate_fix` \u4E00\u6B21\u53EA\u63A5\u53D7\u4E00\u4E2A finding\uFF0C\u4E14 file / content / finding / round \u56DB\u4E2A\u53C2\u6570\u5168\u90E8\u5FC5\u586B\uFF1A",
1210
+ `\u6B65\u9AA4 1\uFF1A\u8BFB\u53D6 \`${payload.file}\`\uFF0C\u6309 finding \u751F\u6210\u4FEE\u590D\u540E\u7684\u3010\u5B8C\u6574\u6587\u4EF6\u5185\u5BB9\u3011\uFF08content \u5360\u4F4D\u5FC5\u987B\u6362\u6210\u771F\u5B9E\u6587\u4EF6\u5185\u5BB9\uFF09\u3002`,
1211
+ `\u6B65\u9AA4 2\uFF1A\u8C03\u7528 \`iterate_fix\`\uFF0C\u628A content \u4E0E round \u586B\u6210\u771F\u5B9E\u503C${round !== null ? `\uFF08\u672C\u8F6E round = ${round}\uFF09` : ""}\uFF1A`,
1212
+ "```json",
1213
+ JSON.stringify(call, null, 2),
1214
+ "```",
1215
+ '\u9700\u8981\u8D85\u8FC7 atomic \u884C\u6570\u4E0A\u9650\u65F6\uFF0C\u53E6\u52A0 "force": true\u3002'
1216
+ ].join("\n");
1217
+ }
1218
+ function buildAssignFixesInstruction(findings) {
1219
+ const list = (Array.isArray(findings) ? findings : []).filter((f) => f && typeof f === "object");
1220
+ if (list.length === 0) return "";
1221
+ const payload = list.map((f) => fixFindingPayload(f)).filter((p) => p.file);
1222
+ if (payload.length === 0) return "";
1223
+ return [
1224
+ `\u8BF7\u4FEE\u590D\u4E0B\u9762\u8FD9 ${payload.length} \u4E2A findings\u3002\u6CE8\u610F\uFF1A\`iterate_fix\` \u6BCF\u6B21\u53EA\u5904\u7406\u3010\u4E00\u4E2A\u3011finding\uFF08\u6CA1\u6709\u6570\u7EC4\u5165\u53C2\uFF09\uFF0C\u8BF7\u9010\u4E2A\u8C03\u7528 iterate_fix\u2014\u2014\u4E00\u6B21\u4E00\u4E2A finding\uFF0C\u5904\u7406\u5B8C\u4E00\u4E2A\u518D\u53D1\u8D77\u4E0B\u4E00\u4E2A\uFF0C\u7EDD\u4E0D\u8981\u628A\u6574\u6279\u6570\u7EC4\u5F53\u4F5C\u53C2\u6570\u4F20\u5165\u3002`,
1225
+ "\u6BCF\u4E2A finding \u90FD\u8D70\u540C\u6837\u7684\u4E24\u6B65\uFF1A\u2460 \u8BFB\u53D6\u6587\u4EF6\u751F\u6210\u4FEE\u590D\u540E\u7684\u5B8C\u6574 content\uFF1B\u2461 \u8C03\u7528 iterate_fix\uFF08file / content / finding / round \u5FC5\u586B\uFF0Ccontent \u4E0E round \u7531\u4F60\u586B\u5199\uFF09\u3002",
1226
+ "\u5F85\u4FEE\u590D\u6E05\u5355\uFF1A",
1227
+ "```json",
1228
+ JSON.stringify(payload, null, 2),
1229
+ "```"
1230
+ ].join("\n");
1231
+ }
1232
+ function buildArchitecturalFixInstruction(findings) {
1233
+ const list = (Array.isArray(findings) ? findings : []).filter((f) => f && typeof f === "object");
1234
+ const lines = [
1235
+ '\u8BF7\u6279\u51C6\u5E76\u6267\u884C\u67B6\u6784\u578B\u4FEE\u590D\uFF08\u5141\u8BB8\u8D85\u8FC7 atomic \u884C\u6570\u4E0A\u9650\uFF09\uFF1A\u5BF9\u6BCF\u4E2A\u67B6\u6784\u578B finding \u9010\u4E2A\u8C03\u7528 `iterate_fix`\uFF0C\u4E00\u6B21\u4E00\u4E2A finding\uFF0C\u5E76\u663E\u5F0F\u8BBE\u7F6E `"force": true`\uFF1B',
1236
+ "file / content / finding / round \u56DB\u4E2A\u5FC5\u586B\u53C2\u6570\u4E00\u4E2A\u90FD\u4E0D\u80FD\u5C11\uFF08content = \u4FEE\u590D\u540E\u7684\u5B8C\u6574\u6587\u4EF6\u5185\u5BB9\uFF0Cround = \u5F53\u524D\u8FED\u4EE3\u8F6E\u6B21\uFF0C\u4E24\u8005\u7531\u4F60\u586B\u5199\uFF09\u3002"
1237
+ ];
1238
+ if (list.length > 0) {
1239
+ lines.push("\u672C\u9879\u76EE\u5269\u4F59\u67B6\u6784\u578B finding\uFF1A", "```json", JSON.stringify(list.map((f) => fixFindingPayload(f)), null, 2), "```");
1240
+ }
1241
+ return lines.join("\n");
1242
+ }
1243
+ function buildCheckpointResumeInstruction() {
1244
+ return `\u8BF7\u8C03\u7528 \`iterate_checkpoint\` \u4ECE\u65AD\u70B9\u6062\u590D\u8FED\u4EE3\uFF08\u52A0\u8F7D\u65AD\u70B9\u5E76\u628A resumeCount +1\uFF09\uFF1A
1245
+
1246
+ \`\`\`json
1247
+ ${JSON.stringify({ operation: "resume" }, null, 2)}
1248
+ \`\`\``;
1249
+ }
1250
+ function buildCheckpointClearInstruction() {
1251
+ return `\u8BF7\u8C03\u7528 \`iterate_checkpoint\` \u6E05\u9664\u5F53\u524D\u65AD\u70B9\uFF1A
1252
+
1253
+ \`\`\`json
1254
+ ${JSON.stringify({ operation: "clear" }, null, 2)}
1255
+ \`\`\``;
1256
+ }
1257
+ function buildQualityGateQueryInstruction() {
1258
+ return `\u8BF7\u8C03\u7528 \`iterate_quality_gate\` \u67E5\u8BE2\u5F53\u524D\u8D28\u91CF\u95E8\u7981\u72B6\u6001\uFF08\u8BFB\u53D6\u78C1\u76D8\u4E0A\u6301\u4E45\u5316\u7684\u8BC1\u4E66\uFF09\uFF1A
1259
+
1260
+ \`\`\`json
1261
+ ${JSON.stringify({ operation: "read" }, null, 2)}
1262
+ \`\`\``;
1263
+ }
1264
+ function buildQualityGateClearInstruction() {
1265
+ return `\u8BF7\u8C03\u7528 \`iterate_quality_gate\` \u6E05\u9664\u5F53\u524D\u8D28\u91CF\u95E8\u7981\u8BC1\u4E66\uFF1A
1266
+
1267
+ \`\`\`json
1268
+ ${JSON.stringify({ operation: "clear" }, null, 2)}
1269
+ \`\`\``;
1270
+ }
1271
+ function buildRollbackInstruction(id) {
1272
+ return `\u8BF7\u8C03\u7528 \`iterate_rollback\` \u56DE\u6EDA\u4EE5\u4E0B\u4FEE\u590D\uFF1A
1273
+
1274
+ \`\`\`json
1275
+ ${JSON.stringify({ id: String(id || "") }, null, 2)}
1276
+ \`\`\``;
1277
+ }
1278
+ function buildExperienceListInstruction() {
1279
+ return `\u8BF7\u8C03\u7528 \`iterate_experience\` \u5217\u51FA\u7ECF\u9A8C\u94F6\u884C\u4E2D\u7684\u6240\u6709\u7ECF\u9A8C\uFF0C\u5E76\u628A\u5B8C\u6574\u8FD4\u56DE\u7ED3\u679C\u56DE\u663E\u5230\u4F1A\u8BDD\u91CC\uFF1A
1280
+
1281
+ \`\`\`json
1282
+ ${JSON.stringify({ operation: "list" }, null, 2)}
1283
+ \`\`\``;
1284
+ }
1285
+ function buildDefenseEventsListInstruction() {
1286
+ return `\u8BF7\u8C03\u7528 \`iterate_defense_events\` \u5217\u51FA\u5168\u90E8\u9632\u5FA1\u4E8B\u4EF6\uFF0C\u5E76\u628A\u5B8C\u6574\u8FD4\u56DE\u7ED3\u679C\u56DE\u663E\u5230\u4F1A\u8BDD\u91CC\uFF1A
1287
+
1288
+ \`\`\`json
1289
+ ${JSON.stringify({ operation: "list" }, null, 2)}
1290
+ \`\`\``;
1291
+ }
1292
+ function buildDiskSnapshotInstruction() {
1293
+ const lines = [
1294
+ "\u8BF7\u4ECE\u78C1\u76D8\u62C9\u53D6 iterate \u5FEB\u7167\uFF1A\u9010\u4E2A\u8C03\u7528\u4E0B\u9762\u7684\u53EA\u8BFB\u5DE5\u5177\uFF0C\u5E76\u628A\u6BCF\u4E2A\u5DE5\u5177\u7684\u3010\u5B8C\u6574\u8FD4\u56DE\u7ED3\u679C\u3011\u539F\u6837\u56DE\u663E\u5230\u4F1A\u8BDD\u91CC\uFF08\u4E0D\u8981\u53EA\u7ED9\u6458\u8981\u2014\u2014\u63D2\u4EF6\u9762\u677F\u9760\u8FD9\u4E9B\u56DE\u663E\u586B\u5145\uFF09\uFF1A"
1295
+ ];
1296
+ DISK_SNAPSHOT_SOURCES.forEach((s, i) => {
1297
+ lines.push(`${i + 1}. \`${s.tool}\`\uFF08${s.label}\uFF09\uFF1A${JSON.stringify(s.args)}`);
1298
+ });
1299
+ lines.push("\u56DE\u663E\u540E\uFF0C\u89C2\u6D4B\u53F0\u5B9E\u65F6\u6D41 / F2 \u8DE8\u8F6E\u5BF9\u6BD4 / F5 \u65AD\u70B9 / F8 \u8D28\u91CF\u95E8\u7981 / F9 \u7ECF\u9A8C\u94F6\u884C / F10 \u9632\u5FA1\u4E8B\u4EF6\u4F1A\u81EA\u52A8\u8BFB\u53D6\u8FD9\u4E9B\u7ED3\u679C\u3002");
1300
+ return lines.join("\n");
1301
+ }
1302
+ var DISK_SNAPSHOT_SOURCES = [
1303
+ { tool: "iterate_status", args: {}, label: "\u8FD0\u884C\u72B6\u6001\u6C47\u603B" },
1304
+ { tool: "iterate_transcript", args: { operation: "read" }, label: "transcript \u89C2\u6D4B\u6E05\u5355" },
1305
+ { tool: "iterate_quality_gate", args: { operation: "read" }, label: "\u8D28\u91CF\u95E8\u7981\u8BC1\u4E66" },
1306
+ { tool: "iterate_experience", args: { operation: "list" }, label: "\u7ECF\u9A8C\u94F6\u884C" },
1307
+ { tool: "iterate_defense_events", args: { operation: "list" }, label: "\u9632\u5FA1\u4E8B\u4EF6\u6D41" },
1308
+ { tool: "iterate_history", args: {}, label: "\u51B3\u7B56\u65E5\u5FD7\u4E0E\u4FEE\u590D\u6CE8\u518C\u8868\uFF08\u8DE8\u8F6E/\u8DE8\u4F1A\u8BDD\u5BF9\u6BD4\uFF09" }
1309
+ ];
1310
+ function toolCalledInSession(session, toolName) {
1311
+ if (!session || typeof session !== "object" || typeof toolName !== "string" || !toolName) return false;
1312
+ const s = (
1313
+ /** @type {Record<string, unknown>} */
1314
+ session
1315
+ );
1316
+ const toolCalls = safeGet(s, "toolCalls");
1317
+ if (Array.isArray(toolCalls)) {
1318
+ for (
1319
+ const call of
1320
+ /** @type {unknown[]} */
1321
+ toolCalls
1322
+ ) {
1323
+ if (!call || typeof call !== "object") continue;
1324
+ const t = String(safeGet(
1325
+ /** @type {Record<string, unknown>} */
1326
+ call,
1327
+ "tool"
1328
+ ) ?? "");
1329
+ if (t === toolName || t.endsWith(toolName)) return true;
1330
+ }
1331
+ }
1332
+ const messages = safeGet(s, "messages");
1333
+ if (Array.isArray(messages)) {
1334
+ for (
1335
+ const msg of
1336
+ /** @type {unknown[]} */
1337
+ messages
1338
+ ) {
1339
+ if (!msg || typeof msg !== "object") continue;
1340
+ const calls = safeGet(
1341
+ /** @type {Record<string, unknown>} */
1342
+ msg,
1343
+ "tool_calls"
1344
+ );
1345
+ if (!Array.isArray(calls)) continue;
1346
+ for (
1347
+ const call of
1348
+ /** @type {unknown[]} */
1349
+ calls
1350
+ ) {
1351
+ if (!call || typeof call !== "object") continue;
1352
+ const c = (
1353
+ /** @type {Record<string, unknown>} */
1354
+ call
1355
+ );
1356
+ const name2 = String(safeGet(c, "name") ?? safeGet(c, "tool") ?? "");
1357
+ if (name2 === toolName || name2.endsWith(toolName)) return true;
1358
+ const fn = safeGet(c, "function");
1359
+ if (fn && typeof fn === "object" && String(safeGet(
1360
+ /** @type {Record<string, unknown>} */
1361
+ fn,
1362
+ "name"
1363
+ ) ?? "") === toolName) {
1364
+ return true;
1365
+ }
1366
+ }
1367
+ }
1368
+ }
1369
+ return false;
1370
+ }
1371
+ function diskEmptyStateText(pulled, topic) {
1372
+ const subject = topic || "\u6570\u636E";
1373
+ return pulled ? `\u5DF2\u4ECE\u78C1\u76D8\u62C9\u53D6\uFF1A${subject}\u6682\u65E0\u8BB0\u5F55\uFF08\u78C1\u76D8\u65E0\u6570\u636E\uFF09\u3002` : `\u5C1A\u672A\u4ECE\u78C1\u76D8\u62C9\u53D6${subject}\uFF1A\u6570\u636E\u5B58\u653E\u5728\u9879\u76EE .iterate/ \u4E0B\uFF0C\u70B9\u300C\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u300D\u590D\u5236\u6307\u4EE4\u53D1\u56DE\u4F1A\u8BDD\u5373\u53EF\u56DE\u586B\u3002`;
1374
+ }
1082
1375
  function normalizeFindingFilter(filter) {
1083
1376
  const f = filter && typeof filter === "object" ? filter : {};
1084
1377
  const severities = Array.isArray(f.severities) ? f.severities.filter((s) => SEVERITY_ORDER.includes(String(s))) : [];
@@ -1122,6 +1415,7 @@ function buildFilterOptions(findings) {
1122
1415
  const severities = SEVERITY_ORDER.map((value) => ({ value, count: 0 }));
1123
1416
  const dimCounts = {};
1124
1417
  for (const f of list) {
1418
+ if (!f || typeof f !== "object") continue;
1125
1419
  const sev = String(f.severity ?? "low");
1126
1420
  const sv = severities.find((s) => s.value === sev);
1127
1421
  if (sv) sv.count++;
@@ -1141,7 +1435,7 @@ function countVerdicts(triageState) {
1141
1435
  return counts;
1142
1436
  }
1143
1437
  function batchSetVerdict(triageState, indices, verdict) {
1144
- if (verdict !== "keep" && verdict !== "skip" && verdict !== "ignore") return triageState;
1438
+ if (!TRIAGE_VERDICTS.includes(verdict)) return triageState;
1145
1439
  if (!Array.isArray(indices) || indices.length === 0) return triageState;
1146
1440
  const next = { ...triageState };
1147
1441
  for (const idx of indices) {
@@ -1250,6 +1544,21 @@ var CONFIG_EDIT_FIELDS = [
1250
1544
  { key: "max_rounds", label: "\u6700\u5927\u8F6E\u6570", hint: "\u6B63\u6574\u6570" },
1251
1545
  { key: "review.scope", label: "\u5BA1\u67E5\u8303\u56F4", hint: '"full" \u6216 "changed-only"' },
1252
1546
  { key: "reasoning_effort", label: "\u5BA1\u67E5\u63A8\u7406\u5F3A\u5EA6", hint: '"low" / "medium" / "high"\uFF0C\u7F3A\u7701\u8DDF\u968F\u6A21\u578B\u9ED8\u8BA4' },
1547
+ {
1548
+ key: "validation.commands",
1549
+ label: "\u9A8C\u8BC1\u547D\u4EE4",
1550
+ hint: '\u6570\u7EC4\uFF0C\u6BCF\u9879\u662F\u4E00\u6761\u767D\u540D\u5355\u547D\u4EE4\u5B57\u7B26\u4E32\uFF0C\u5982 ["npm test","npm run lint"]\uFF1B\u6BCF\u8F6E\u9A8C\u8BC1\u9010\u6761\u6267\u884C\u3002\u7559\u7A7A\u6570\u7EC4 = \u4E0D\u9A8C\u8BC1\uFF08\u672C\u8F6E\u8FED\u4EE3\u4E0D\u53D7\u4EFB\u4F55\u6D4B\u8BD5\u4FDD\u62A4\uFF09'
1551
+ },
1552
+ {
1553
+ key: "language",
1554
+ label: "\u754C\u9762\u8BED\u8A00",
1555
+ hint: '"zh" \u6216 "en"\uFF08\u9ED8\u8BA4 "en"\uFF09\uFF1A\u63A7\u5236\u5BA1\u6279\u7406\u7531\u3001\u9632\u5FA1\u4E8B\u4EF6\u6807\u7B7E\u7B49\u9762\u5411\u4EBA\u6587\u6848\u7684\u8BED\u8A00'
1556
+ },
1557
+ {
1558
+ key: "personalization.known_intentional",
1559
+ label: "\u5DF2\u77E5\u6709\u610F\u95EE\u9898",
1560
+ hint: "\u6570\u7EC4\uFF0C\u6BCF\u9879 {file, line?, dimension, reason}\uFF1B\u547D\u4E2D\u6761\u76EE\u5728\u8BC4\u5BA1\u4E2D\u88AB\u8DF3\u8FC7\u3002\u53EF\u7528\u5206\u8BCA\u9762\u677F\u7684\u300C\u56DE\u8BFB\u5DF2\u5199\u5165\u6761\u76EE\u300D\u6838\u5BF9"
1561
+ },
1253
1562
  { key: "atomic.max_lines", label: "\u539F\u5B50\u4FEE\u590D\u4E0A\u9650\u884C\u6570", hint: "\u6B63\u6574\u6570" },
1254
1563
  { key: "git.push_per_round", label: "\u6BCF\u8F6E\u63A8\u9001", hint: "true / false" }
1255
1564
  ];
@@ -1259,16 +1568,50 @@ function buildConfigEditGuide() {
1259
1568
  "---------------------",
1260
1569
  "\u914D\u7F6E\u6587\u4EF6\uFF1A\u9879\u76EE\u6839\u76EE\u5F55 iterate.config.yaml\u3002",
1261
1570
  "",
1571
+ "\u26A0 \u672A\u914D\u7F6E validation.commands \u65F6\u8FED\u4EE3\u4E0D\u88AB\u4FDD\u62A4\uFF1A\u6BCF\u8F6E validate \u62FF\u5230\u7A7A\u7ED3\u679C\u4F1A\u88AB\u5F53\u4F5C",
1572
+ " \u201C\u9A8C\u8BC1\u901A\u8FC7\u201D\uFF0C\u7B49\u4E8E\u5728\u6CA1\u6709\u4EFB\u4F55\u6D4B\u8BD5\u4FDD\u62A4\u7684\u60C5\u51B5\u4E0B\u7EE7\u7EED\u6536\u655B\u3002\u7B2C\u4E00\u6B21\u542F\u52A8\u8FED\u4EE3\u524D\uFF0C",
1573
+ " \u8BF7\u5148\u7ED9 validation.commands \u914D\u4E0A\u81F3\u5C11\u4E00\u6761\u771F\u5B9E\u547D\u4EE4\uFF08\u5982 npm test\uFF09\u3002",
1574
+ "",
1262
1575
  "\u53EF\u7F16\u8F91\u5B57\u6BB5\uFF1A",
1263
1576
  ...CONFIG_EDIT_FIELDS.map((f) => `- ${f.key}\uFF08${f.label}\uFF09\uFF1A${f.hint}`),
1264
1577
  "",
1265
1578
  "\u8BA9\u6A21\u578B\u5E2E\u4F60\u6539\uFF1A",
1266
1579
  '1. \u8C03\u7528 iterate_config({ operation: "read" }) \u67E5\u770B\u5F53\u524D\u914D\u7F6E\uFF1B',
1267
- "2. \u8BF4\u660E\u60F3\u6539\u7684\u5B57\u6BB5\uFF0C\u4F8B\u5982\u300C\u628A max_rounds \u6539\u6210 5\uFF0Cdimensions \u53EA\u4FDD\u7559 correctness \u548C security\u300D\uFF1B",
1268
- '3. \u6A21\u578B\u4F1A\u8C03\u7528 iterate_config({ operation: "write", updates: {...} }) \u5199\u5165\uFF0C\u5199\u5165\u524D\u81EA\u52A8\u5907\u4EFD\uFF0C\u5931\u8D25\u81EA\u52A8\u56DE\u6EDA\u3002'
1580
+ "2. \u8BF4\u660E\u60F3\u6539\u7684\u5B57\u6BB5\uFF0C\u4F8B\u5982\u300C\u628A max_rounds \u6539\u6210 5\uFF0Cvalidation.commands \u52A0\u4E0A npm test\u300D\uFF1B",
1581
+ '3. \u6A21\u578B\u4F1A\u8C03\u7528 iterate_config({ operation: "write", updates: {...} }) \u5199\u5165\uFF0C\u5199\u5165\u524D\u81EA\u52A8\u5907\u4EFD\uFF0C\u5931\u8D25\u81EA\u52A8\u56DE\u6EDA\u3002',
1582
+ "",
1583
+ "\u4E5F\u53EF\u4EE5\u7528\u8BBE\u7F6E\u9875\u7684\u201C\u9009\u5B57\u6BB5 \u2192 \u751F\u6210\u5199\u5165\u6307\u4EE4\u201D\u76F4\u63A5\u590D\u5236\u7B2C 3 \u6B65\u7684\u6307\u4EE4\u7C98\u8D34\u7ED9\u6A21\u578B\u3002"
1269
1584
  ];
1270
1585
  return lines.join("\n");
1271
1586
  }
1587
+ function buildConfigEditInstruction(desiredChanges) {
1588
+ const payload = JSON.stringify({ operation: "write", updates: desiredChanges }, null, 2);
1589
+ return `\u8BF7\u8C03\u7528 \`iterate_config\` \u5199\u5165\u4EE5\u4E0B\u914D\u7F6E\u66F4\u65B0\uFF1A
1590
+
1591
+ \`\`\`json
1592
+ ${payload}
1593
+ \`\`\``;
1594
+ }
1595
+ var CONFIG_VALUE_PLACEHOLDER = "<\u7531\u6A21\u578B\u586B\u5199\uFF1A\u8BE5\u5B57\u6BB5\u7684\u65B0\u503C>";
1596
+ function configFieldByKey(key) {
1597
+ if (typeof key !== "string" || !key) return null;
1598
+ return CONFIG_EDIT_FIELDS.find((f) => f.key === key) ?? null;
1599
+ }
1600
+ function buildConfigFieldInstruction(key, value) {
1601
+ const field = configFieldByKey(key);
1602
+ const label = field ? field.label : key;
1603
+ const hint = field ? field.hint : "\u89C1 iterate.config.yaml";
1604
+ const updates = { [key]: value === void 0 ? CONFIG_VALUE_PLACEHOLDER : value };
1605
+ const payload = buildConfigEditInstruction(updates);
1606
+ return [
1607
+ `\u914D\u7F6E\u5B57\u6BB5 \xB7 ${label}\uFF08${key}\uFF09`,
1608
+ `\u5B57\u6BB5\u8BF4\u660E\uFF1A${hint}`,
1609
+ "",
1610
+ payload,
1611
+ "",
1612
+ '\u5199\u5165\u524D\u8BF7\u5148 `operation: "read"` \u5C55\u793A\u5F53\u524D\u503C\uFF1B\u5199\u5165\u7531\u5DE5\u5177\u81EA\u52A8\u5907\u4EFD\uFF0C\u5931\u8D25\u81EA\u52A8\u56DE\u6EDA\u3002'
1613
+ ].join("\n");
1614
+ }
1272
1615
  var VERDICT_SHORTCUTS = {
1273
1616
  y: "keep",
1274
1617
  Y: "keep",
@@ -1278,7 +1621,8 @@ var VERDICT_SHORTCUTS = {
1278
1621
  A: "ignore"
1279
1622
  };
1280
1623
  function keyToVerdict(key) {
1281
- return VERDICT_SHORTCUTS[key] ?? null;
1624
+ const verdict = VERDICT_SHORTCUTS[key];
1625
+ return verdict !== void 0 && TRIAGE_VERDICTS.includes(verdict) ? verdict : null;
1282
1626
  }
1283
1627
  function allVerdictKeys(triageState) {
1284
1628
  const state = triageState && typeof triageState === "object" ? triageState : {};
@@ -1352,11 +1696,12 @@ function filterTimelineEntries(entries, opts) {
1352
1696
  });
1353
1697
  return filtered.slice().sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
1354
1698
  }
1355
- function serializeObservatoryExport(manifest, live) {
1699
+ function serializeObservatoryExport(manifest, live, extra) {
1356
1700
  const payload = {
1357
1701
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1358
1702
  manifest: manifest && typeof manifest === "object" ? manifest : null,
1359
- live: Array.isArray(live) ? live : []
1703
+ live: Array.isArray(live) ? live : [],
1704
+ ...pickExportExtras(extra)
1360
1705
  };
1361
1706
  try {
1362
1707
  return JSON.stringify(payload, null, 2);
@@ -1364,6 +1709,62 @@ function serializeObservatoryExport(manifest, live) {
1364
1709
  return JSON.stringify({ exportedAt: payload.exportedAt, manifest: null, live: [] }, null, 2);
1365
1710
  }
1366
1711
  }
1712
+ var EXPORT_EXTRA_KEYS = ["qualityGate", "experienceBank", "defenseEvents", "report"];
1713
+ function pickExportExtras(extra) {
1714
+ const out = {};
1715
+ if (!extra || typeof extra !== "object") return out;
1716
+ for (const key of EXPORT_EXTRA_KEYS) {
1717
+ if (!Object.prototype.hasOwnProperty.call(extra, key)) continue;
1718
+ const value = extra[key];
1719
+ if (value === void 0) continue;
1720
+ out[key] = value;
1721
+ }
1722
+ return out;
1723
+ }
1724
+ function buildRoundComparison(report, validations) {
1725
+ const rounds = /* @__PURE__ */ new Map();
1726
+ const rowsFor = (round) => {
1727
+ const n = Number(round);
1728
+ const key = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
1729
+ if (!rounds.has(key)) {
1730
+ rounds.set(key, { round: key, findings: 0, fixed: 0, severities: /* @__PURE__ */ Object.create(null), validations: [] });
1731
+ }
1732
+ return rounds.get(key);
1733
+ };
1734
+ const list = report && Array.isArray(report.findings) ? report.findings : [];
1735
+ for (const f of list) {
1736
+ if (!f || typeof f !== "object") continue;
1737
+ const row = rowsFor(f.round);
1738
+ row.findings += 1;
1739
+ const sev = typeof f.severity === "string" && f.severity ? f.severity : "unknown";
1740
+ row.severities[sev] = (row.severities[sev] || 0) + 1;
1741
+ if (f.status === "fixed") row.fixed += 1;
1742
+ }
1743
+ const vals = Array.isArray(validations) ? validations : [];
1744
+ for (const v of vals) {
1745
+ if (!v || typeof v !== "object") continue;
1746
+ const row = rowsFor(v.round);
1747
+ row.validations.push({
1748
+ command: typeof v.command === "string" ? v.command : "",
1749
+ exitCode: typeof v.exitCode === "number" ? v.exitCode : null,
1750
+ allowed: v.allowed === true
1751
+ });
1752
+ }
1753
+ return [...rounds.values()].filter((r) => r.round > 0).sort((a, b) => a.round - b.round).map((r) => ({ ...r, severities: { ...r.severities } }));
1754
+ }
1755
+ function dashboardRunState(transcript) {
1756
+ const t = transcript && typeof transcript === "object" ? transcript : null;
1757
+ if (!t) return { state: "empty", round: 0, phase: "", stoppedReason: "" };
1758
+ const rawRound = Number(t.round);
1759
+ const round = Number.isFinite(rawRound) && rawRound > 0 ? Math.floor(rawRound) : 1;
1760
+ const stoppedReason = typeof t.stoppedReason === "string" ? t.stoppedReason.trim() : "";
1761
+ return {
1762
+ state: t.active === true ? "running" : "done",
1763
+ round,
1764
+ phase: latestPhase(t.phases),
1765
+ stoppedReason
1766
+ };
1767
+ }
1367
1768
  function latestPhase(phases) {
1368
1769
  const list = Array.isArray(phases) ? phases : [];
1369
1770
  let latest = "";
@@ -1453,6 +1854,7 @@ var ITERATE_CSS = `
1453
1854
  }
1454
1855
  .iterate-btn[data-primary] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
1455
1856
  .iterate-btn[data-copied] { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); }
1857
+ .iterate-btn[data-failed], .iterate-cmd[data-failed] { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); }
1456
1858
  .iterate-payload { width: 100%; margin-top: 8px; padding: 8px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 11px; white-space: pre-wrap; }
1457
1859
 
1458
1860
  .iterate-stats { margin: 10px 0; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); padding: 12px 14px; }
@@ -1637,6 +2039,16 @@ var ITERATE_CSS = `
1637
2039
  .iterate-obs-block-head[data-click] { cursor: pointer; }
1638
2040
  .iterate-obs-block-body { padding: 8px 10px; }
1639
2041
  .iterate-obs-chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-secondary); white-space: nowrap; }
2042
+ /* Gap #6: validation / gate status colors (pass=green, fail=red). */
2043
+ .iterate-obs-chip[data-status="pass"], .iterate-obs-badge[data-status="pass"] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
2044
+ .iterate-obs-chip[data-status="fail"], .iterate-obs-badge[data-status="fail"] { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent); }
2045
+ .iterate-obs-row[data-status="fail"] { color: var(--dsw-alias-state-error-primary); }
2046
+ .iterate-obs-row[data-highlight] { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent); border-radius: 6px; padding-left: 6px; padding-right: 6px; border-bottom-color: transparent; font-weight: 600; }
2047
+ /* Gap #13: cross-round comparison table. */
2048
+ .iterate-obs-table { width: 100%; border-collapse: collapse; font-size: 11px; }
2049
+ .iterate-obs-table th, .iterate-obs-table td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--dsw-alias-border-l1); white-space: nowrap; }
2050
+ .iterate-obs-table th { color: var(--dsw-alias-label-secondary); font-weight: 600; }
2051
+ .iterate-obs-table td { color: var(--dsw-alias-label-primary); }
1640
2052
  .iterate-obs-msg { padding: 2px 0; color: var(--dsw-alias-label-secondary); line-height: 1.5; font-size: 11px; word-break: break-word; }
1641
2053
  .iterate-obs-file { font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 10.5px; color: var(--dsw-alias-label-secondary); word-break: break-all; }
1642
2054
  .iterate-obs-bar { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); flex-wrap: wrap; }
@@ -1853,11 +2265,25 @@ function TrendChart({ points }) {
1853
2265
  }
1854
2266
  function StartIterationButton() {
1855
2267
  const [copied, setCopied] = React.useState(null);
2268
+ const [failed, setFailed] = React.useState(null);
2269
+ const timers = React.useRef([]);
2270
+ React.useEffect(
2271
+ () => () => {
2272
+ for (const t of timers.current) clearTimeout(t);
2273
+ timers.current = [];
2274
+ },
2275
+ []
2276
+ );
1856
2277
  const copy = (key, text) => {
1857
2278
  copyText(text).then((ok) => {
1858
2279
  if (ok) {
2280
+ setFailed(null);
1859
2281
  setCopied(key);
1860
- setTimeout(() => setCopied((cur) => cur === key ? null : cur), 1600);
2282
+ const t = window.setTimeout(() => setCopied((cur) => cur === key ? null : cur), 1600);
2283
+ timers.current.push(t);
2284
+ } else {
2285
+ setCopied(null);
2286
+ setFailed(key);
1861
2287
  }
1862
2288
  });
1863
2289
  };
@@ -1866,14 +2292,15 @@ function StartIterationButton() {
1866
2292
  className: "iterate-cmd",
1867
2293
  "data-primary": primary ? "" : void 0,
1868
2294
  "data-copied": copied === key ? "" : void 0,
2295
+ "data-failed": failed === key ? "" : void 0,
1869
2296
  onClick: () => copy(key, command),
1870
2297
  title
1871
- }, copied === key ? "\u5DF2\u590D\u5236" : label);
2298
+ }, copied === key ? "\u5DF2\u590D\u5236" : failed === key ? "\u590D\u5236\u5931\u8D25" : label);
1872
2299
  return React.createElement(
1873
2300
  "span",
1874
2301
  { className: "iterate-dashboard-launch" },
1875
- btn("start-full", "\u5B8C\u6574\u8FED\u4EE3", "/iterate", true, "\u590D\u5236\u542F\u52A8\u547D\u4EE4\uFF1A\u5B8C\u6574\u300C\u5BA1\u67E5 \u2192 \u4FEE\u590D \u2192 \u9A8C\u8BC1\u300D\u95ED\u73AF"),
1876
- btn("start-review", "\u4EC5\u8BC4\u5BA1", "/iterate review-only", false, "\u590D\u5236\u542F\u52A8\u547D\u4EE4\uFF1A\u53EA\u5BA1\u67E5\u4E0D\u4FEE\u6539\uFF08dry-run\uFF09")
2302
+ btn("start-full", "\u5B8C\u6574\u8FED\u4EE3", START_INSTRUCTIONS.full, true, "\u590D\u5236\u542F\u52A8\u6307\u4EE4\uFF1A\u5B8C\u6574\u300C\u5BA1\u67E5 \u2192 \u4FEE\u590D \u2192 \u9A8C\u8BC1 \u2192 \u590D\u76D8\u300D\u95ED\u73AF\uFF08\u7C98\u8D34\u7ED9\u6A21\u578B\u5373\u53EF\uFF09"),
2303
+ btn("start-review", "\u4EC5\u8BC4\u5BA1", START_INSTRUCTIONS.reviewOnly, false, "\u590D\u5236\u542F\u52A8\u6307\u4EE4\uFF1A\u53EA\u5BA1\u67E5\u4E0D\u4FEE\u6539\uFF08dry-run\uFF09\uFF08\u7C98\u8D34\u7ED9\u6A21\u578B\u5373\u53EF\uFF09")
1877
2304
  );
1878
2305
  }
1879
2306
  function ConvergenceDashboard(props) {
@@ -1888,9 +2315,36 @@ function ConvergenceDashboard(props) {
1888
2315
  setPulseKey((k) => k + 1);
1889
2316
  }, [report && hashReport(report) + ":" + getCurrentRound(report)]);
1890
2317
  if (!report) {
2318
+ const runState = dashboardRunState(latestTranscript(session));
2319
+ if (runState.state === "running") {
2320
+ return React.createElement(
2321
+ "div",
2322
+ { "data-iterate-root": "", "data-iterate": "dashboard", "data-run-state": "running", className: "iterate-dashboard iterate-dashboard-empty" },
2323
+ React.createElement("span", { className: "iterate-round-badge", "data-live": "" }, "\u8FD0\u884C\u4E2D"),
2324
+ React.createElement(
2325
+ "span",
2326
+ { className: "iterate-empty-hint" },
2327
+ `\u7B2C ${String(runState.round)} \u8F6E\u8FDB\u884C\u4E2D${runState.phase ? ` \xB7 \u9636\u6BB5\u300C${runState.phase}\u300D` : ""}\u2014\u2014\u9996\u8F6E\u6C47\u603B\u62A5\u544A\u843D\u5730\u540E\uFF0C\u8FD9\u91CC\u4F1A\u663E\u793A\u6536\u655B\u8FDB\u5EA6\u4E0E\u53D1\u73B0\u7EDF\u8BA1\u3002`
2328
+ )
2329
+ // No start button while a run is active.
2330
+ );
2331
+ }
2332
+ if (runState.state === "done") {
2333
+ return React.createElement(
2334
+ "div",
2335
+ { "data-iterate-root": "", "data-iterate": "dashboard", "data-run-state": "done", className: "iterate-dashboard iterate-dashboard-empty" },
2336
+ React.createElement("span", { className: "iterate-round-badge" }, "\u5DF2\u7ED3\u675F"),
2337
+ React.createElement(
2338
+ "span",
2339
+ { className: "iterate-empty-hint" },
2340
+ `\u4E0A\u6B21\u8FED\u4EE3\u5728\u7B2C ${String(runState.round)} \u8F6E\u540E\u505C\u6B62${runState.stoppedReason ? `\uFF08${stoppedReasonLabel(runState.stoppedReason)}\uFF09` : ""}\uFF0C\u4F46\u6CA1\u6709\u53EF\u5C55\u793A\u7684\u6C47\u603B\u62A5\u544A\u3002`
2341
+ ),
2342
+ React.createElement(StartIterationButton, null)
2343
+ );
2344
+ }
1891
2345
  return React.createElement(
1892
2346
  "div",
1893
- { "data-iterate-root": "", "data-iterate": "dashboard", className: "iterate-dashboard iterate-dashboard-empty" },
2347
+ { "data-iterate-root": "", "data-iterate": "dashboard", "data-run-state": "empty", className: "iterate-dashboard iterate-dashboard-empty" },
1894
2348
  React.createElement("span", { className: "iterate-round-badge" }, "iterate"),
1895
2349
  React.createElement("span", { className: "iterate-empty-hint" }, "\u8FD0\u884C\u4E00\u6B21\u8BC4\u5BA1\u540E\uFF0C\u8FD9\u91CC\u4F1A\u663E\u793A\u6536\u655B\u8FDB\u5EA6\u4E0E\u53D1\u73B0\u7EDF\u8BA1\u3002"),
1896
2350
  React.createElement(StartIterationButton, null)
@@ -2116,6 +2570,25 @@ function TriagePanel(props) {
2116
2570
  const [selected, setSelected] = React.useState(null);
2117
2571
  const [selectAll, setSelectAll] = React.useState(false);
2118
2572
  const [cmdCopied, setCmdCopied] = React.useState(null);
2573
+ const [cmdFailed, setCmdFailed] = React.useState(null);
2574
+ const cmdTimers = React.useRef([]);
2575
+ React.useEffect(() => () => {
2576
+ for (const t of cmdTimers.current) clearTimeout(t);
2577
+ cmdTimers.current = [];
2578
+ }, []);
2579
+ const copyWithFeedback = (text, key) => {
2580
+ copyText(text).then((ok) => {
2581
+ if (ok) {
2582
+ setCmdFailed(null);
2583
+ setCmdCopied(key);
2584
+ cmdTimers.current.push(window.setTimeout(() => setCmdCopied((cur) => cur === key ? null : cur), 1600));
2585
+ } else {
2586
+ setCmdCopied(null);
2587
+ setCmdFailed(key);
2588
+ setPayload(text);
2589
+ }
2590
+ });
2591
+ };
2119
2592
  React.useEffect(() => {
2120
2593
  if (storage) storage.set(storageKey, JSON.stringify(verdicts));
2121
2594
  }, [storageKey, verdicts]);
@@ -2180,55 +2653,26 @@ function TriagePanel(props) {
2180
2653
  const ignoredCount = ignored.length;
2181
2654
  const counts = countVerdicts(verdicts);
2182
2655
  const doApproveArchitecturalFix = () => {
2183
- const cmd = "\u8BF7\u8C03\u7528 `iterate_fix`\uFF0C\u5BF9\u672C\u9879\u76EE\u5269\u4F59\u67B6\u6784\u578B finding \u751F\u6210\u4FEE\u590D\u5185\u5BB9\u5E76\u8BBE\u7F6E force: true \u5E94\u7528\uFF08\u67B6\u6784\u4FEE\u590D\u5141\u8BB8\u8D85\u8FC7 atomic \u9608\u503C\uFF09";
2184
- copyText(cmd).then((ok) => {
2185
- if (ok) {
2186
- setCmdCopied("approve-arch");
2187
- setTimeout(() => setCmdCopied(null), 1600);
2188
- }
2189
- });
2656
+ const cmd = buildArchitecturalFixInstruction(findings);
2657
+ if (!cmd) return;
2658
+ copyWithFeedback(cmd, "approve-arch");
2190
2659
  };
2191
2660
  const doTriggerNewRound = () => {
2192
2661
  const cmd = "\u8BF7\u7EE7\u7EED\u6267\u884C\u4E0B\u4E00\u8F6E\u8FED\u4EE3\u5BA1\u67E5";
2193
- copyText(cmd).then((ok) => {
2194
- if (ok) {
2195
- setCmdCopied("new-round");
2196
- setTimeout(() => setCmdCopied(null), 1600);
2197
- }
2198
- });
2662
+ copyWithFeedback(cmd, "new-round");
2199
2663
  };
2200
2664
  const doRollbackToCheckpoint = () => {
2201
2665
  const cmd = "\u8BF7\u56DE\u6EDA\u4E0A\u4E00\u8F6E\u8FED\u4EE3\u7684\u4FEE\u590D\uFF1A\u8C03\u7528 `iterate_history` \u67E5\u770B\u672C\u8F6E fix id\uFF0C\u518D\u7528 `iterate_rollback` \u9010\u4E2A\u64A4\u9500\u8FD9\u4E9B\u4FEE\u590D";
2202
- copyText(cmd).then((ok) => {
2203
- if (ok) {
2204
- setCmdCopied("rollback");
2205
- setTimeout(() => setCmdCopied(null), 1600);
2206
- }
2207
- });
2666
+ copyWithFeedback(cmd, "rollback");
2208
2667
  };
2209
2668
  const doAssignFindings = () => {
2210
2669
  const targetAll = selectAll;
2211
2670
  const scopeIndices = targetAll ? allIndices : indices;
2212
- const scopeFindings = findings.filter((f, i) => scopeIndices.includes(i)).map((f) => ({
2213
- file: String(f.file || ""),
2214
- ...typeof f.line === "number" && f.line > 0 ? { line: f.line } : {},
2215
- dimension: String(f.dimension || ""),
2216
- severity: String(f.severity || ""),
2217
- summary: String(f.summary || ""),
2218
- ...f.suggested_fix ? { suggested_fix: String(f.suggested_fix) } : {}
2219
- }));
2671
+ const scopeFindings = findings.filter((f, i) => scopeIndices.includes(i));
2220
2672
  if (scopeFindings.length === 0) return;
2221
- const cmd = `\u8BF7\u8C03\u7528 \`iterate_fix\` \u6307\u6D3E\u5E76\u4FEE\u590D\u4EE5\u4E0B findings\uFF1A
2222
-
2223
- \`\`\`json
2224
- ${JSON.stringify(scopeFindings, null, 2)}
2225
- \`\`\``;
2226
- copyText(cmd).then((ok) => {
2227
- if (ok) {
2228
- setCmdCopied("assign");
2229
- setTimeout(() => setCmdCopied(null), 1600);
2230
- }
2231
- });
2673
+ const cmd = buildAssignFixesInstruction(scopeFindings);
2674
+ if (!cmd) return;
2675
+ copyWithFeedback(cmd, "assign");
2232
2676
  };
2233
2677
  const doCopyYaml = () => {
2234
2678
  const yaml = toKnownIntentionalYaml(ignored);
@@ -2236,19 +2680,31 @@ ${JSON.stringify(scopeFindings, null, 2)}
2236
2680
  copyText(yaml).then((ok) => {
2237
2681
  if (ok) {
2238
2682
  setCopied(true);
2239
- setTimeout(() => setCopied(false), 1600);
2683
+ cmdTimers.current.push(window.setTimeout(() => setCopied(false), 1600));
2240
2684
  } else {
2241
2685
  setPayload(yaml);
2686
+ setCmdFailed("yaml");
2242
2687
  }
2243
2688
  });
2244
2689
  };
2245
2690
  const doBuildInstruction = () => {
2246
2691
  const text = buildApplyInstruction(ignored);
2692
+ if (!text) return;
2247
2693
  setPayload(text);
2248
- if (text) copyText(text);
2694
+ copyWithFeedback(text, "apply");
2249
2695
  };
2250
- const rows = filtered.map((finding, i) => {
2251
- const index = indices[i];
2696
+ const doReadback = () => {
2697
+ const text = buildTriageReadbackInstruction();
2698
+ setPayload(text);
2699
+ copyWithFeedback(text, "readback");
2700
+ };
2701
+ const rowPairs = [];
2702
+ for (let i = 0; i < filtered.length; i++) {
2703
+ const idx = indices[i];
2704
+ const f = filtered[i];
2705
+ if (typeof idx === "number" && f) rowPairs.push({ index: idx, finding: f });
2706
+ }
2707
+ const rows = rowPairs.map(({ index, finding }) => {
2252
2708
  const severity = finding.severity || "low";
2253
2709
  const verdict = verdicts[String(index)] || "keep";
2254
2710
  const isSelected = selected === index;
@@ -2368,22 +2824,35 @@ ${JSON.stringify(scopeFindings, null, 2)}
2368
2824
  React.createElement("span", {}, `y ${counts.keep} \xB7 n ${counts.skip} \xB7 a ${counts.ignore} \xB7 \u5F85\u5199\u56DE known_intentional\uFF1A${ignoredCount} \u6761`),
2369
2825
  React.createElement(
2370
2826
  "span",
2371
- { style: { display: "flex", gap: 6 } },
2827
+ { style: { display: "flex", gap: 6, flexWrap: "wrap" } },
2828
+ // Gap #7: the apply instruction (safe path) is PRIMARY; the raw YAML
2829
+ // is demoted to an explicit "just the snippet" escape hatch.
2372
2830
  React.createElement("button", {
2373
2831
  className: "iterate-btn",
2374
2832
  "data-primary": "",
2833
+ "data-copied": cmdCopied === "apply" ? "" : void 0,
2834
+ "data-failed": cmdFailed === "apply" ? "" : void 0,
2835
+ onClick: doBuildInstruction,
2836
+ disabled: ignoredCount === 0,
2837
+ title: ignoredCount === 0 ? "\u5F53\u524D\u6CA1\u6709\u6807\u8BB0\u4E3A\u300C\u5DF2\u77E5\u6709\u610F\u300D\u7684 finding" : "\u590D\u5236\u5E94\u7528\u6307\u4EE4\uFF1A\u7C98\u8D34\u540E\u6A21\u578B\u4F1A\u8C03\u7528 iterate_triage apply\uFF08\u6821\u9A8C\u3001\u53BB\u91CD\u3001\u5199\u5165\u524D\u5907\u4EFD\u3001\u5931\u8D25\u81EA\u52A8\u56DE\u6EDA\uFF09"
2838
+ }, cmdCopied === "apply" ? "\u5DF2\u590D\u5236" : cmdFailed === "apply" ? "\u590D\u5236\u5931\u8D25" : `\u751F\u6210\u5E94\u7528\u6307\u4EE4${ignoredCount > 0 ? `\uFF08${ignoredCount}\uFF09` : ""}`),
2839
+ React.createElement("button", {
2840
+ className: "iterate-btn",
2375
2841
  "data-copied": copied ? "" : void 0,
2842
+ "data-failed": cmdFailed === "yaml" ? "" : void 0,
2376
2843
  onClick: doCopyYaml,
2377
2844
  disabled: ignoredCount === 0,
2378
- title: ignoredCount === 0 ? "\u5F53\u524D\u6CA1\u6709\u6807\u8BB0\u4E3A\u300C\u5DF2\u77E5\u6709\u610F\u300D\u7684 finding" : "\u590D\u5236 known_intentional YAML"
2379
- }, copied ? "\u5DF2\u590D\u5236" : `\u590D\u5236 known_intentional${ignoredCount > 0 ? `\uFF08${ignoredCount}\uFF09` : ""}`),
2845
+ title: ignoredCount === 0 ? "\u5F53\u524D\u6CA1\u6709\u6807\u8BB0\u4E3A\u300C\u5DF2\u77E5\u6709\u610F\u300D\u7684 finding" : "\u53EA\u590D\u5236 known_intentional YAML \u7247\u6BB5\uFF08\u9700\u81EA\u884C\u7C98\u5230 iterate.config.yaml \u7684 personalization.known_intentional \u4E0B\uFF09"
2846
+ }, copied ? "\u5DF2\u590D\u5236" : cmdFailed === "yaml" ? "\u590D\u5236\u5931\u8D25" : "\u4EC5\u590D\u5236 YAML \u7247\u6BB5"),
2380
2847
  React.createElement("button", {
2381
2848
  className: "iterate-btn",
2382
- onClick: doBuildInstruction,
2383
- disabled: ignoredCount === 0,
2384
- title: ignoredCount === 0 ? "\u5F53\u524D\u6CA1\u6709\u6807\u8BB0\u4E3A\u300C\u5DF2\u77E5\u6709\u610F\u300D\u7684 finding" : "\u751F\u6210 iterate_triage \u5E94\u7528\u6307\u4EE4"
2385
- }, "\u751F\u6210\u5E94\u7528\u6307\u4EE4")
2386
- )
2849
+ "data-copied": cmdCopied === "readback" ? "" : void 0,
2850
+ "data-failed": cmdFailed === "readback" ? "" : void 0,
2851
+ onClick: doReadback,
2852
+ title: "\u590D\u5236 iterate_triage list \u6307\u4EE4\uFF0C\u6838\u5BF9\u5DF2\u7ECF\u5199\u5165\u7684 known_intentional \u6761\u76EE\uFF08\u5199\u56DE\u95ED\u73AF\u786E\u8BA4\uFF09"
2853
+ }, cmdCopied === "readback" ? "\u5DF2\u590D\u5236" : cmdFailed === "readback" ? "\u590D\u5236\u5931\u8D25" : "\u56DE\u8BFB\u5DF2\u5199\u5165\u6761\u76EE")
2854
+ ),
2855
+ cmdCopied === "apply" ? React.createElement("span", { className: "iterate-triage-hint", role: "status" }, "\u5DF2\u590D\u5236\u5E94\u7528\u6307\u4EE4\u2014\u2014\u7C98\u8D34\u56DE\u4F1A\u8BDD\uFF0C\u6A21\u578B\u5C06\u8C03\u7528 iterate_triage apply \u5E76\u56DE\u62A5\u5199\u5165\u6761\u6570\u3002") : cmdCopied === "readback" ? React.createElement("span", { className: "iterate-triage-hint", role: "status" }, "\u5DF2\u590D\u5236\u56DE\u8BFB\u6307\u4EE4\u2014\u2014\u7C98\u8D34\u56DE\u4F1A\u8BDD\u53EF\u5217\u51FA\u5F53\u524D\u5DF2\u5199\u5165\u7684 known_intentional\u3002") : null
2387
2856
  ),
2388
2857
  // v3.0: Command buttons for native actions (§8)
2389
2858
  React.createElement(
@@ -2392,35 +2861,39 @@ ${JSON.stringify(scopeFindings, null, 2)}
2392
2861
  React.createElement("span", { style: { fontSize: 11, fontWeight: 600, color: "var(--dsw-alias-label-secondary)" } }, "\u6307\u6325\u64CD\u4F5C"),
2393
2862
  React.createElement(
2394
2863
  "span",
2395
- { style: { display: "flex", gap: 6 } },
2864
+ { style: { display: "flex", gap: 6, flexWrap: "wrap" } },
2396
2865
  React.createElement("button", {
2397
2866
  className: "iterate-cmd",
2398
2867
  "data-primary": "",
2399
2868
  "data-copied": cmdCopied === "approve-arch" ? "" : void 0,
2869
+ "data-failed": cmdFailed === "approve-arch" ? "" : void 0,
2400
2870
  onClick: doApproveArchitecturalFix,
2401
- title: "\u6279\u51C6\u67B6\u6784\u4FEE\u590D\uFF08\u590D\u5236\u6307\u4EE4\u6587\u672C\uFF09"
2402
- }, cmdCopied === "approve-arch" ? "\u5DF2\u590D\u5236" : "\u6279\u51C6\u67B6\u6784\u4FEE\u590D"),
2871
+ title: "\u6279\u51C6\u67B6\u6784\u4FEE\u590D\uFF08\u590D\u5236\u6307\u4EE4\u6587\u672C\uFF1Aiterate_fix force:true\uFF0C\u542B\u5168\u90E8\u5FC5\u586B\u53C2\u6570\uFF09"
2872
+ }, cmdCopied === "approve-arch" ? "\u5DF2\u590D\u5236" : cmdFailed === "approve-arch" ? "\u590D\u5236\u5931\u8D25" : "\u6279\u51C6\u67B6\u6784\u4FEE\u590D"),
2403
2873
  React.createElement("button", {
2404
2874
  className: "iterate-cmd",
2405
2875
  "data-copied": cmdCopied === "new-round" ? "" : void 0,
2876
+ "data-failed": cmdFailed === "new-round" ? "" : void 0,
2406
2877
  onClick: doTriggerNewRound,
2407
2878
  title: "\u89E6\u53D1\u65B0\u4E00\u8F6E\u8FED\u4EE3\u5BA1\u67E5"
2408
- }, cmdCopied === "new-round" ? "\u5DF2\u590D\u5236" : "\u89E6\u53D1\u65B0\u4E00\u8F6E"),
2879
+ }, cmdCopied === "new-round" ? "\u5DF2\u590D\u5236" : cmdFailed === "new-round" ? "\u590D\u5236\u5931\u8D25" : "\u89E6\u53D1\u65B0\u4E00\u8F6E"),
2409
2880
  React.createElement("button", {
2410
2881
  className: "iterate-cmd",
2411
2882
  "data-primary": "",
2412
2883
  "data-copied": cmdCopied === "assign" ? "" : void 0,
2884
+ "data-failed": cmdFailed === "assign" ? "" : void 0,
2413
2885
  onClick: doAssignFindings,
2414
2886
  disabled: (selectAll ? allIndices : indices).length === 0,
2415
- title: selectAll ? `\u6307\u6D3E\u5168\u90E8 ${allIndices.length} \u4E2A findings \u4FEE\u590D` : `\u6307\u6D3E\u5F53\u524D\u53EF\u89C1 ${indices.length} \u4E2A findings \u4FEE\u590D`
2416
- }, cmdCopied === "assign" ? "\u5DF2\u590D\u5236" : "\u6307\u6D3E\u4FEE\u590D"),
2887
+ title: selectAll ? `\u6307\u6D3E\u5168\u90E8 ${allIndices.length} \u4E2A findings \u4FEE\u590D\uFF08\u9010\u4E2A iterate_fix\uFF09` : `\u6307\u6D3E\u5F53\u524D\u53EF\u89C1 ${indices.length} \u4E2A findings \u4FEE\u590D\uFF08\u9010\u4E2A iterate_fix\uFF09`
2888
+ }, cmdCopied === "assign" ? "\u5DF2\u590D\u5236" : cmdFailed === "assign" ? "\u590D\u5236\u5931\u8D25" : "\u6307\u6D3E\u4FEE\u590D"),
2417
2889
  React.createElement("button", {
2418
2890
  className: "iterate-cmd",
2419
2891
  "data-danger": "",
2420
2892
  "data-copied": cmdCopied === "rollback" ? "" : void 0,
2893
+ "data-failed": cmdFailed === "rollback" ? "" : void 0,
2421
2894
  onClick: doRollbackToCheckpoint,
2422
2895
  title: "\u56DE\u6EDA\u5230\u4E0A\u4E00\u4E2A\u68C0\u67E5\u70B9"
2423
- }, cmdCopied === "rollback" ? "\u5DF2\u590D\u5236" : "\u56DE\u6EDA\u68C0\u67E5\u70B9")
2896
+ }, cmdCopied === "rollback" ? "\u5DF2\u590D\u5236" : cmdFailed === "rollback" ? "\u590D\u5236\u5931\u8D25" : "\u56DE\u6EDA\u68C0\u67E5\u70B9")
2424
2897
  )
2425
2898
  ),
2426
2899
  payload ? React.createElement("div", { className: "iterate-payload" }, payload) : null
@@ -2522,33 +2995,57 @@ function SettingsPanel(_props) {
2522
2995
  const [showStatus, setShowStatus] = React.useState(false);
2523
2996
  const [confirming, setConfirming] = React.useState(false);
2524
2997
  const [clearedInfo, setClearedInfo] = React.useState(null);
2998
+ const [cfgField, setCfgField] = React.useState(
2999
+ () => CONFIG_EDIT_FIELDS.some((f) => f.key === "validation.commands") ? "validation.commands" : CONFIG_EDIT_FIELDS[0]?.key ?? "goal"
3000
+ );
3001
+ const [cfgCopied, setCfgCopied] = React.useState(false);
3002
+ const [cfgFailed, setCfgFailed] = React.useState(false);
3003
+ const [cfgText, setCfgText] = React.useState(null);
3004
+ const [pullCopied, setPullCopied] = React.useState(false);
3005
+ const [copyError, setCopyError] = React.useState(null);
2525
3006
  const guide = buildConfigEditGuide();
2526
3007
  const statusGuide = buildRuntimeStatusGuide();
3008
+ const cfgTimers = React.useRef([]);
3009
+ React.useEffect(() => () => {
3010
+ for (const t of cfgTimers.current) clearTimeout(t);
3011
+ cfgTimers.current = [];
3012
+ }, []);
2527
3013
  const toggleTheme = () => {
2528
3014
  const next = !enabled;
2529
3015
  setEnabled(next);
2530
3016
  setThemeEnabled(next);
2531
3017
  };
2532
3018
  const flashCopied = (slot) => {
2533
- const setter = slot === "guide" ? setGuideCopied : setStatusCopied;
3019
+ const setter = slot === "guide" ? setGuideCopied : slot === "status" ? setStatusCopied : setPullCopied;
2534
3020
  setter(true);
2535
- setTimeout(() => setter(false), 1600);
3021
+ cfgTimers.current.push(window.setTimeout(() => setter(false), 1600));
2536
3022
  };
2537
3023
  const doCopy = (text, slot) => {
2538
3024
  copyText(text).then((ok) => {
3025
+ setCopyError(ok ? null : slot);
2539
3026
  if (ok) flashCopied(slot);
2540
3027
  });
2541
3028
  };
3029
+ const doBuildConfigInstruction = () => {
3030
+ const text = buildConfigFieldInstruction(cfgField);
3031
+ setCfgText(text);
3032
+ copyText(text).then((ok) => {
3033
+ setCfgFailed(!ok);
3034
+ setCfgCopied(ok);
3035
+ cfgTimers.current.push(window.setTimeout(() => setCfgCopied(false), 1600));
3036
+ if (!ok) return;
3037
+ });
3038
+ };
2542
3039
  const requestClear = () => {
2543
3040
  if (confirming) {
2544
3041
  const count = removeStorageByPrefix(TRIAGE_STORAGE_PREFIX);
2545
3042
  setClearedInfo(count);
2546
3043
  setConfirming(false);
2547
- setTimeout(() => setClearedInfo(null), 3e3);
3044
+ cfgTimers.current.push(window.setTimeout(() => setClearedInfo(null), 3e3));
2548
3045
  return;
2549
3046
  }
2550
3047
  setConfirming(true);
2551
- setTimeout(() => setConfirming(false), 3e3);
3048
+ cfgTimers.current.push(window.setTimeout(() => setConfirming(false), 3e3));
2552
3049
  };
2553
3050
  const clearButton = clearedInfo !== null ? React.createElement("button", { className: "iterate-btn", "data-copied": "", disabled: true }, `\u5DF2\u6E05\u9664 ${clearedInfo} \u6761`) : React.createElement("button", {
2554
3051
  className: "iterate-btn",
@@ -2614,7 +3111,7 @@ function SettingsPanel(_props) {
2614
3111
  "div",
2615
3112
  {},
2616
3113
  React.createElement("div", { className: "iterate-settings-title" }, "\u914D\u7F6E\u7BA1\u7406"),
2617
- React.createElement("div", { className: "iterate-settings-desc" }, "\u76EE\u6807 / \u7EF4\u5EA6 / \u6700\u5927\u8F6E\u6570\u5199\u5728\u9879\u76EE\u7684 iterate.config.yaml\uFF0C\u590D\u5236\u6307\u5F15\u53EF\u8BA9\u6A21\u578B\u6309\u9700\u8C03\u6574\u3002")
3114
+ React.createElement("div", { className: "iterate-settings-desc" }, "\u76EE\u6807 / \u7EF4\u5EA6 / \u6700\u5927\u8F6E\u6570 / validation.commands \u5199\u5728\u9879\u76EE\u7684 iterate.config.yaml\uFF0C\u590D\u5236\u6307\u5F15\u53EF\u8BA9\u6A21\u578B\u6309\u9700\u8C03\u6574\u3002")
2618
3115
  ),
2619
3116
  React.createElement(
2620
3117
  "div",
@@ -2623,6 +3120,50 @@ function SettingsPanel(_props) {
2623
3120
  React.createElement("button", { className: "iterate-btn", "data-ghost": "", onClick: () => setShowGuide((v) => !v) }, showGuide ? "\u6536\u8D77" : "\u5C55\u5F00")
2624
3121
  )
2625
3122
  ),
3123
+ // Gap #8: wire up the previously-dead `buildConfigFieldInstruction` — pick
3124
+ // a field, generate the `iterate_config` write payload for it, paste it to
3125
+ // the model. `validation.commands` is the default selection.
3126
+ React.createElement(
3127
+ "div",
3128
+ { className: "iterate-guide-bar", style: { marginTop: 8, flexWrap: "wrap" } },
3129
+ React.createElement(
3130
+ "select",
3131
+ {
3132
+ className: "iterate-filter-select",
3133
+ value: cfgField,
3134
+ "aria-label": "\u9009\u62E9\u8981\u4FEE\u6539\u7684\u914D\u7F6E\u5B57\u6BB5",
3135
+ onChange: (e) => setCfgField(e.target.value)
3136
+ },
3137
+ ...CONFIG_EDIT_FIELDS.map(
3138
+ (f) => React.createElement("option", { key: f.key, value: f.key }, `${f.label}\uFF08${f.key}\uFF09`)
3139
+ )
3140
+ ),
3141
+ React.createElement(
3142
+ "span",
3143
+ { className: "iterate-settings-desc" },
3144
+ configFieldByKey(cfgField)?.hint ?? ""
3145
+ ),
3146
+ React.createElement("button", {
3147
+ className: "iterate-btn",
3148
+ "data-primary": "",
3149
+ "data-copied": cfgCopied ? "" : void 0,
3150
+ "data-failed": cfgFailed ? "" : void 0,
3151
+ onClick: doBuildConfigInstruction,
3152
+ title: "\u751F\u6210\u5E76\u590D\u5236\u8BE5\u5B57\u6BB5\u7684 iterate_config \u5199\u5165\u6307\u4EE4\uFF08\u5199\u5165\u524D\u81EA\u52A8\u5907\u4EFD\u3001\u5931\u8D25\u81EA\u52A8\u56DE\u6EDA\uFF09"
3153
+ }, cfgCopied ? "\u5DF2\u590D\u5236" : cfgFailed ? "\u590D\u5236\u5931\u8D25" : "\u751F\u6210\u5199\u5165\u6307\u4EE4"),
3154
+ cfgFailed ? React.createElement("span", { className: "iterate-settings-desc" }, "\u526A\u8D34\u677F\u88AB\u62D2\u7EDD\u2014\u2014\u6307\u4EE4\u5DF2\u5C55\u793A\u5728\u4E0B\u65B9\uFF0C\u8BF7\u624B\u52A8\u590D\u5236\u3002") : null
3155
+ ),
3156
+ cfgText ? React.createElement(
3157
+ "div",
3158
+ { className: "iterate-guide" },
3159
+ React.createElement(
3160
+ "div",
3161
+ { className: "iterate-guide-bar" },
3162
+ React.createElement("span", {}, "\u5199\u5165\u6307\u4EE4\uFF08\u7C98\u8D34\u7ED9\u6A21\u578B\uFF09"),
3163
+ React.createElement("button", { className: "iterate-btn", "data-ghost": "", onClick: () => doCopy(cfgText, "guide") }, "\u590D\u5236")
3164
+ ),
3165
+ React.createElement("div", { className: "iterate-guide-body" }, cfgText)
3166
+ ) : null,
2626
3167
  showGuide ? React.createElement(
2627
3168
  "div",
2628
3169
  { className: "iterate-guide" },
@@ -2651,6 +3192,14 @@ function SettingsPanel(_props) {
2651
3192
  "div",
2652
3193
  { className: "iterate-scard-actions" },
2653
3194
  React.createElement("button", { className: "iterate-btn", "data-primary": "", "data-copied": statusCopied ? "" : void 0, onClick: () => doCopy(statusGuide, "status") }, statusCopied ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u6307\u5F15"),
3195
+ // Gap #4: RUNTIME_ARTIFACTS stops being documentation-only — this
3196
+ // copies the instruction that makes the model echo those artifacts
3197
+ // into the session so the observatory panels can read them.
3198
+ React.createElement("button", {
3199
+ className: "iterate-btn",
3200
+ "data-copied": pullCopied ? "" : void 0,
3201
+ onClick: () => doCopy(buildDiskSnapshotInstruction(), "pull")
3202
+ }, pullCopied ? "\u5DF2\u590D\u5236" : "\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167"),
2654
3203
  React.createElement("button", { className: "iterate-btn", "data-ghost": "", onClick: () => setShowStatus((v) => !v) }, showStatus ? "\u6536\u8D77" : "\u5C55\u5F00")
2655
3204
  )
2656
3205
  ),
@@ -2691,6 +3240,15 @@ function SettingsPanel(_props) {
2691
3240
  { "data-iterate-root": "", "data-iterate": "settings", className: "iterate-settings" },
2692
3241
  React.createElement("div", { className: "iterate-settings-title", style: { fontSize: 15, fontWeight: 700 } }, "iterate \u8BBE\u7F6E"),
2693
3242
  banner,
3243
+ copyError ? React.createElement(
3244
+ "div",
3245
+ { key: "copy-err", className: "iterate-scard", role: "alert" },
3246
+ React.createElement(
3247
+ "div",
3248
+ { className: "iterate-settings-desc" },
3249
+ "\u526A\u8D34\u677F\u5199\u5165\u88AB\u62D2\u7EDD\uFF0C\u590D\u5236\u672A\u751F\u6548\u2014\u2014\u8BF7\u5C55\u5F00\u5BF9\u5E94\u533A\u5757\u624B\u52A8\u9009\u4E2D\u6587\u672C\u590D\u5236\u3002"
3250
+ )
3251
+ ) : null,
2694
3252
  themeCard,
2695
3253
  dataCard,
2696
3254
  guideCard,
@@ -2698,19 +3256,37 @@ function SettingsPanel(_props) {
2698
3256
  );
2699
3257
  }
2700
3258
  var OBS_TABS = [
2701
- { key: "live", label: "\u5B9E\u65F6\u6D3B\u52A8\u6D41" },
2702
- { key: "f1", label: "\u5BA1\u67E5\u7EBF\u7A0B" },
2703
- { key: "f2", label: "\u6536\u655B\u8D8B\u52BF" },
2704
- { key: "f3", label: "\u53D1\u73B0\u5B9A\u4F4D" },
2705
- { key: "f4", label: "\u4FEE\u590D\u4E0E\u56DE\u6EDA" },
2706
- { key: "f5", label: "\u65AD\u70B9\u6062\u590D" },
2707
- { key: "f6", label: "\u8FD0\u884C\u63A7\u5236\u53F0" },
2708
- { key: "f7", label: "\u51B3\u7B56\u65F6\u95F4\u7EBF" },
3259
+ { key: "live", label: "\u5B9E\u65F6\u6D3B\u52A8\u6D41", hotkey: "" },
3260
+ { key: "f1", label: "\u5BA1\u67E5\u7EBF\u7A0B", hotkey: "1" },
3261
+ { key: "f2", label: "\u6536\u655B\u8D8B\u52BF", hotkey: "2" },
3262
+ { key: "f3", label: "\u53D1\u73B0\u5B9A\u4F4D", hotkey: "3" },
3263
+ { key: "f4", label: "\u4FEE\u590D\u4E0E\u56DE\u6EDA", hotkey: "4" },
3264
+ { key: "f5", label: "\u65AD\u70B9\u6062\u590D", hotkey: "5" },
3265
+ { key: "f6", label: "\u8FD0\u884C\u63A7\u5236\u53F0", hotkey: "6" },
3266
+ { key: "f7", label: "\u51B3\u7B56\u65F6\u95F4\u7EBF", hotkey: "7" },
2709
3267
  // v3.0: Quality Command Center tabs
2710
- { key: "f8", label: "\u8D28\u91CF\u95E8\u7981" },
2711
- { key: "f9", label: "\u7ECF\u9A8C\u94F6\u884C" },
2712
- { key: "f10", label: "\u9632\u5FA1\u4E8B\u4EF6" }
3268
+ { key: "f8", label: "\u8D28\u91CF\u95E8\u7981", hotkey: "8" },
3269
+ { key: "f9", label: "\u7ECF\u9A8C\u94F6\u884C", hotkey: "9" },
3270
+ { key: "f10", label: "\u9632\u5FA1\u4E8B\u4EF6", hotkey: "0" }
2713
3271
  ];
3272
+ var OBS_HOTKEY_TABS = {
3273
+ 1: "f1",
3274
+ 2: "f2",
3275
+ 3: "f3",
3276
+ 4: "f4",
3277
+ 5: "f5",
3278
+ 6: "f6",
3279
+ 7: "f7",
3280
+ 8: "f8",
3281
+ 9: "f9",
3282
+ 0: "f10",
3283
+ "-": "live"
3284
+ };
3285
+ function obsTabLabel(t) {
3286
+ if (t.key === "live") return t.label;
3287
+ return `${t.key.toUpperCase()} ${t.label}`;
3288
+ }
3289
+ var OBS_HOTKEY_HINT = `${String(OBS_TABS.length)} \u9875 \xB7 \u6309 1-9/0 \u5207 Fn\u3001- \u56DE\u5B9E\u65F6\u6D41`;
2714
3290
  var OBS_LIVE_META = {
2715
3291
  read: { label: "\u9605\u8BFB", color: "var(--dsw-alias-brand-primary)" },
2716
3292
  fix: { label: "\u4FEE\u590D", color: "var(--dsw-alias-state-success-primary)" },
@@ -2760,18 +3336,7 @@ function latestDefenseEvents(session) {
2760
3336
  return scanSessionForDefenseEvents(session);
2761
3337
  }
2762
3338
  function buildObsFixInstruction(f) {
2763
- const payload = JSON.stringify({
2764
- file: String(f.file || ""),
2765
- ...typeof f.line === "number" && f.line > 0 ? { line: f.line } : {},
2766
- dimension: String(f.dimension || ""),
2767
- summary: String(f.summary || ""),
2768
- ...f.suggested_fix ? { suggested_fix: String(f.suggested_fix) } : {}
2769
- }, null, 2);
2770
- return `\u8BF7\u8C03\u7528 \`iterate_fix\` \u4FEE\u590D\u4EE5\u4E0B finding\uFF1A
2771
-
2772
- \`\`\`json
2773
- ${payload}
2774
- \`\`\``;
3339
+ return buildFixInstruction(f);
2775
3340
  }
2776
3341
  function ObservatoryPanel(props) {
2777
3342
  const session = props && props.session ? props.session : null;
@@ -2799,6 +3364,28 @@ function ObservatoryPanel(props) {
2799
3364
  React.useEffect(() => () => {
2800
3365
  if (copyTimer.current) clearTimeout(copyTimer.current);
2801
3366
  }, []);
3367
+ React.useEffect(() => {
3368
+ const doc = typeof document !== "undefined" ? document : null;
3369
+ if (!doc || !open) return;
3370
+ const onKeyDown = (ev) => {
3371
+ if (ev.metaKey || ev.ctrlKey || ev.altKey || ev.shiftKey) return;
3372
+ const t = ev.target;
3373
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT")) return;
3374
+ if (t && typeof t.isContentEditable === "boolean" && t.isContentEditable) return;
3375
+ const rootEl = doc.querySelector('[data-iterate="obs"]');
3376
+ if (!rootEl) return;
3377
+ const activeEl = doc.activeElement;
3378
+ const inPanel = activeEl === rootEl || activeEl !== null && typeof rootEl.contains === "function" && rootEl.contains(activeEl);
3379
+ const idle = activeEl === null || activeEl === doc.body;
3380
+ if (!inPanel && !idle) return;
3381
+ const next = OBS_HOTKEY_TABS[ev.key];
3382
+ if (!next) return;
3383
+ ev.preventDefault();
3384
+ setTab(next);
3385
+ };
3386
+ doc.addEventListener("keydown", onKeyDown);
3387
+ return () => doc.removeEventListener("keydown", onKeyDown);
3388
+ }, [open]);
2802
3389
  const copyInstruction = (key, text) => {
2803
3390
  if (!text) return;
2804
3391
  copyText(text).then((ok) => {
@@ -2811,6 +3398,8 @@ function ObservatoryPanel(props) {
2811
3398
  copyTimer.current = setTimeout(() => setCopiedKey((cur) => cur === key ? null : cur), 1600);
2812
3399
  });
2813
3400
  };
3401
+ const pullInstruction = buildDiskSnapshotInstruction();
3402
+ const pullTitle = `\u590D\u5236\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u6307\u4EE4\uFF1A\u8BA9\u6A21\u578B\u9010\u4E2A\u8C03\u7528 ${DISK_SNAPSHOT_SOURCES.map((s) => s.tool).join(" / ")} \u5E76\u56DE\u663E\u5B8C\u6574\u7ED3\u679C`;
2814
3403
  const toggleThread = (key) => {
2815
3404
  setThreadMode("auto");
2816
3405
  setExpandedThreads((prev) => {
@@ -2822,7 +3411,12 @@ function ObservatoryPanel(props) {
2822
3411
  };
2823
3412
  const exportObservatory = () => {
2824
3413
  if (!manifest) return;
2825
- const json = serializeObservatoryExport(manifest, manifest.live || []);
3414
+ const json = serializeObservatoryExport(manifest, manifest.live || [], {
3415
+ qualityGate,
3416
+ experienceBank,
3417
+ defenseEvents,
3418
+ report: latestReport(session)
3419
+ });
2826
3420
  const filename = `iterate-observatory-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
2827
3421
  const flash = () => {
2828
3422
  setCopiedKey("export");
@@ -2858,7 +3452,16 @@ function ObservatoryPanel(props) {
2858
3452
  "div",
2859
3453
  { className: "iterate-obs-head", "data-closed": "" },
2860
3454
  React.createElement("span", { className: "iterate-obs-title" }, "iterate \u89C2\u6D4B\u53F0"),
2861
- React.createElement("span", { className: "iterate-obs-head-meta" }, "\u6682\u65E0\u8FD0\u884C\u65F6\u89C2\u6D4B\u6570\u636E")
3455
+ React.createElement("span", { className: "iterate-obs-head-meta" }, `\u6682\u65E0\u8FD0\u884C\u65F6\u89C2\u6D4B\u6570\u636E \xB7 ${OBS_HOTKEY_HINT}`),
3456
+ React.createElement("button", {
3457
+ className: "iterate-btn",
3458
+ "data-copied": copiedKey === "pull-snapshot" ? "" : void 0,
3459
+ onClick: (e) => {
3460
+ e.stopPropagation();
3461
+ copyInstruction("pull-snapshot", pullInstruction);
3462
+ },
3463
+ title: "\u590D\u5236\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u6307\u4EE4\uFF1A\u78C1\u76D8\u4E0A\u53EF\u80FD\u5DF2\u6709\u5386\u53F2\u4EA7\u7269\uFF0C\u56DE\u663E\u8FDB\u4F1A\u8BDD\u540E\u89C2\u6D4B\u53F0\u5373\u53EF\u586B\u5145"
3464
+ }, copiedKey === "pull-snapshot" ? "\u5DF2\u590D\u5236" : "\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167")
2862
3465
  )
2863
3466
  );
2864
3467
  }
@@ -2968,6 +3571,45 @@ function ObservatoryPanel(props) {
2968
3571
  return React.createElement("div", { className: "iterate-obs-empty" }, "\u6682\u65E0\u6536\u655B\u6570\u636E");
2969
3572
  }
2970
3573
  const points = conv.map((n, i) => ({ round: i + 1, count: n }));
3574
+ const report = latestReport(session);
3575
+ const comparison = buildRoundComparison(report, manifest.validations || []);
3576
+ const comparisonTable = comparison.length === 0 ? null : React.createElement(
3577
+ "table",
3578
+ { className: "iterate-obs-table" },
3579
+ React.createElement(
3580
+ "thead",
3581
+ {},
3582
+ React.createElement(
3583
+ "tr",
3584
+ {},
3585
+ React.createElement("th", {}, "\u8F6E\u6B21"),
3586
+ React.createElement("th", {}, "\u53D1\u73B0"),
3587
+ React.createElement("th", {}, "\u5DF2\u4FEE\u590D"),
3588
+ React.createElement("th", {}, "\u4E25\u91CD\u5EA6\u5206\u5E03"),
3589
+ React.createElement("th", {}, "\u9A8C\u8BC1")
3590
+ )
3591
+ ),
3592
+ React.createElement(
3593
+ "tbody",
3594
+ {},
3595
+ ...comparison.map((row) => {
3596
+ const sevText = Object.entries(row.severities).map(([k, v]) => `${k}:${v}`).join(" \xB7 ");
3597
+ const anyFailed = row.validations.some((v) => v.allowed !== true);
3598
+ const valText = row.validations.length === 0 ? "\u2014" : `${row.validations.filter((v) => v.allowed === true).length}/${row.validations.length} \u901A\u8FC7${anyFailed ? "\uFF08\u542B\u5931\u8D25\uFF09" : ""}`;
3599
+ return React.createElement(
3600
+ "tr",
3601
+ { key: `cmp-${row.round}` },
3602
+ React.createElement("td", {}, `Round ${row.round}`),
3603
+ React.createElement("td", {}, String(row.findings)),
3604
+ React.createElement("td", {}, String(row.fixed)),
3605
+ React.createElement("td", {}, sevText || "\u2014"),
3606
+ React.createElement("td", {
3607
+ style: anyFailed ? { color: "var(--dsw-alias-state-error-primary)", fontWeight: 600 } : row.validations.length > 0 ? { color: "var(--dsw-alias-state-success-primary)" } : void 0
3608
+ }, valText)
3609
+ );
3610
+ })
3611
+ )
3612
+ );
2971
3613
  return React.createElement(
2972
3614
  "div",
2973
3615
  {},
@@ -2976,7 +3618,22 @@ function ObservatoryPanel(props) {
2976
3618
  "div",
2977
3619
  { className: "iterate-obs-bar", style: { marginTop: 6 } },
2978
3620
  `\u5404\u8F6E\u53D1\u73B0\u6570\u91CF\uFF1A${conv.join(" \u2192 ")}${typeof manifest.round === "number" ? ` \xB7 \u5F53\u524D Round ${manifest.round}` : ""}`
2979
- )
3621
+ ),
3622
+ comparisonTable ? React.createElement(
3623
+ "div",
3624
+ { className: "iterate-obs-block", style: { marginTop: 8 } },
3625
+ React.createElement(
3626
+ "div",
3627
+ { className: "iterate-obs-block-head" },
3628
+ React.createElement("span", {}, "\u8DE8\u8F6E\u5BF9\u6BD4\uFF08\u672C\u4F1A\u8BDD\uFF09"),
3629
+ React.createElement(
3630
+ "span",
3631
+ { className: "iterate-obs-head-meta" },
3632
+ "\u8DE8\u4F1A\u8BDD\u5BF9\u6BD4\uFF1A\u672C\u8868\u53EA\u8986\u76D6\u5F53\u524D\u4F1A\u8BDD\uFF1B\u70B9\u300C\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u300D\u8BA9\u6A21\u578B\u56DE\u663E iterate_history\uFF0C\u518D\u7528\u300C\u5BFC\u51FA JSON\u300D\u5E76\u6392\u6BD4\u8F83\u4E24\u6B21\u8FD0\u884C"
3633
+ )
3634
+ ),
3635
+ React.createElement("div", { className: "iterate-obs-block-body" }, comparisonTable)
3636
+ ) : null
2980
3637
  );
2981
3638
  };
2982
3639
  const renderFindings = () => {
@@ -3109,11 +3766,7 @@ function ObservatoryPanel(props) {
3109
3766
  return React.createElement("div", {}, ...fixes.map((f, i) => {
3110
3767
  const k = `f4-${i}`;
3111
3768
  const id = String(f.id || `fix#${i + 1}`);
3112
- const rollbackText = `\u8BF7\u8C03\u7528 \`iterate_rollback\` \u56DE\u6EDA\u4EE5\u4E0B\u4FEE\u590D\uFF1A
3113
-
3114
- \`\`\`json
3115
- ${JSON.stringify({ id }, null, 2)}
3116
- \`\`\``;
3769
+ const rollbackText = buildRollbackInstruction(id);
3117
3770
  const added = typeof f.linesAdded === "number" ? f.linesAdded : 0;
3118
3771
  const removed = typeof f.linesRemoved === "number" ? f.linesRemoved : 0;
3119
3772
  return React.createElement(
@@ -3151,22 +3804,14 @@ ${JSON.stringify({ id }, null, 2)}
3151
3804
  const renderCheckpoint = () => {
3152
3805
  const cp = manifest.checkpoint;
3153
3806
  if (!cp) {
3154
- return React.createElement("div", { className: "iterate-obs-empty" }, "\u6682\u65E0\u65AD\u70B9");
3807
+ return React.createElement(
3808
+ "div",
3809
+ { className: "iterate-obs-empty" },
3810
+ diskEmptyStateText(toolCalledInSession(session, "iterate_transcript"), "\u65AD\u70B9")
3811
+ );
3155
3812
  }
3156
- const resumeText = `\u8BF7\u8C03\u7528 \`iterate_checkpoint\` \u4ECE\u65AD\u70B9\u6062\u590D\u8FED\u4EE3\uFF1A
3157
-
3158
- \`\`\`json
3159
- ${JSON.stringify({
3160
- operation: "resume",
3161
- mode: String(cp.mode || "normal"),
3162
- maxRounds: typeof cp.maxRounds === "number" ? cp.maxRounds : null
3163
- }, null, 2)}
3164
- \`\`\``;
3165
- const clearText = `\u8BF7\u8C03\u7528 \`iterate_checkpoint\` \u6E05\u9664\u5F53\u524D\u65AD\u70B9\uFF1A
3166
-
3167
- \`\`\`json
3168
- ${JSON.stringify({ operation: "clear" }, null, 2)}
3169
- \`\`\``;
3813
+ const resumeText = buildCheckpointResumeInstruction();
3814
+ const clearText = buildCheckpointClearInstruction();
3170
3815
  const item = (label, value) => React.createElement("span", { className: "iterate-obs-chip" }, `${label} ${String(value ?? "?")}`);
3171
3816
  return React.createElement(
3172
3817
  "div",
@@ -3227,6 +3872,53 @@ ${JSON.stringify({ operation: "nudge", text: nudgeText }, null, 2)}
3227
3872
  \`\`\`json
3228
3873
  ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
3229
3874
  \`\`\``;
3875
+ const validations = Array.isArray(manifest.validations) ? manifest.validations : [];
3876
+ const abortedByValidation = String(manifest.stoppedReason || "") === "aborted_by_validation";
3877
+ const validationRows = validations.map((v, i) => {
3878
+ const allowed = v.allowed === true;
3879
+ const failed = !allowed;
3880
+ const highlight = failed && abortedByValidation;
3881
+ const exit = typeof v.exitCode === "number" ? `exit ${v.exitCode}` : "exit \u2014";
3882
+ return React.createElement(
3883
+ "div",
3884
+ {
3885
+ key: `val-${i}`,
3886
+ className: "iterate-obs-row",
3887
+ "data-status": allowed ? "pass" : "fail",
3888
+ "data-highlight": highlight ? "" : void 0,
3889
+ title: v.rejectReason ? String(v.rejectReason) : void 0
3890
+ },
3891
+ React.createElement("span", { className: "iterate-obs-head-meta" }, `Round ${typeof v.round === "number" ? v.round : "?"}`),
3892
+ React.createElement("code", { className: "iterate-obs-mono" }, String(v.command || "")),
3893
+ React.createElement(
3894
+ "span",
3895
+ { className: "iterate-obs-chip", "data-status": allowed ? "pass" : "fail" },
3896
+ allowed ? "\u901A\u8FC7" : "\u5931\u8D25"
3897
+ ),
3898
+ React.createElement("span", { className: "iterate-obs-head-meta" }, exit),
3899
+ highlight ? React.createElement("span", { className: "iterate-obs-chip", "data-status": "fail" }, "\u5BFC\u81F4\u4E2D\u6B62") : null
3900
+ );
3901
+ });
3902
+ const validationsBlock = React.createElement(
3903
+ "div",
3904
+ { className: "iterate-obs-block", style: { marginTop: 8 } },
3905
+ React.createElement(
3906
+ "div",
3907
+ { className: "iterate-obs-block-head" },
3908
+ React.createElement("span", {}, "\u9A8C\u8BC1\u7ED3\u679C"),
3909
+ validations.length === 0 ? React.createElement("span", { className: "iterate-obs-head-meta" }, "\u672C\u8F6E\u5C1A\u65E0\u9A8C\u8BC1\u8BB0\u5F55") : React.createElement("span", { className: "iterate-obs-head-meta" }, `${validations.length} \u6761\u547D\u4EE4`),
3910
+ abortedByValidation ? React.createElement("span", { className: "iterate-obs-badge", "data-status": "fail" }, "\u56E0\u9A8C\u8BC1\u5931\u8D25\u4E2D\u6B62") : null
3911
+ ),
3912
+ React.createElement(
3913
+ "div",
3914
+ { className: "iterate-obs-block-body" },
3915
+ validations.length === 0 ? React.createElement(
3916
+ "div",
3917
+ { className: "iterate-obs-empty" },
3918
+ "\u5C1A\u672A\u62C9\u53D6\u9A8C\u8BC1\u7ED3\u679C\uFF1A\u6570\u636E\u5728\u78C1\u76D8 transcript \u91CC\uFF0C\u70B9\u300C\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u300D\u590D\u5236\u6307\u4EE4\u53D1\u56DE\u4F1A\u8BDD\u5373\u53EF\u56DE\u586B\u3002"
3919
+ ) : React.createElement(React.Fragment, null, ...validationRows)
3920
+ )
3921
+ );
3230
3922
  return React.createElement(
3231
3923
  "div",
3232
3924
  { className: "iterate-obs-block" },
@@ -3271,7 +3963,8 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
3271
3963
  onClick: () => copyInstruction("nudge", nudgeInstruction),
3272
3964
  title: "\u590D\u5236 iterate_transcript nudge \u6307\u4EE4\u6587\u672C"
3273
3965
  }, copiedKey === "nudge" ? "\u5DF2\u590D\u5236" : "\u590D\u5236 nudge \u6307\u4EE4")
3274
- )
3966
+ ),
3967
+ validationsBlock
3275
3968
  )
3276
3969
  );
3277
3970
  };
@@ -3374,18 +4067,15 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
3374
4067
  return React.createElement("div", {}, filterBar, ...rows);
3375
4068
  };
3376
4069
  const renderQualityGate = () => {
3377
- const gateInstruction = "\u8BF7\u8C03\u7528 `iterate_quality_gate` \u67E5\u8BE2\u5F53\u524D\u8D28\u91CF\u95E8\u7981\u72B6\u6001";
3378
- const gateClearInstruction = `\u8BF7\u8C03\u7528 \`iterate_quality_gate\` \u6E05\u9664\u5F53\u524D\u8D28\u91CF\u95E8\u7981\u8BC1\u4E66\uFF1A
3379
-
3380
- \`\`\`json
3381
- ${JSON.stringify({ operation: "clear" }, null, 2)}
3382
- \`\`\``;
4070
+ const gateInstruction = buildQualityGateQueryInstruction();
4071
+ const gateClearInstruction = buildQualityGateClearInstruction();
3383
4072
  const gate = qualityGate;
3384
4073
  const dims = gate && gate.dimensions ? gate.dimensions : [];
3385
4074
  const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0);
3386
4075
  const status = gate && gate.overallStatus ? String(gate.overallStatus) : "pending";
3387
4076
  const statusLabel = status === "pass" ? "PASS" : status === "fail" ? "FAIL" : "PENDING";
3388
4077
  const num = (v) => Number(v != null ? v : 0);
4078
+ const pulled = toolCalledInSession(session, "iterate_quality_gate");
3389
4079
  const headerBar = React.createElement(
3390
4080
  "div",
3391
4081
  { className: "iterate-obs-bar", style: { marginBottom: 8 } },
@@ -3395,7 +4085,7 @@ ${JSON.stringify({ operation: "clear" }, null, 2)}
3395
4085
  "data-primary": "",
3396
4086
  "data-copied": copiedKey === "qgate" ? "" : void 0,
3397
4087
  onClick: () => copyInstruction("qgate", gateInstruction),
3398
- title: "\u590D\u5236 iterate_quality_gate \u67E5\u8BE2\u6307\u4EE4"
4088
+ title: "\u590D\u5236 iterate_quality_gate \u67E5\u8BE2\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09"
3399
4089
  }, copiedKey === "qgate" ? "\u5DF2\u590D\u5236" : "\u67E5\u8BE2\u95E8\u7981"),
3400
4090
  React.createElement("button", {
3401
4091
  className: "iterate-btn",
@@ -3403,7 +4093,15 @@ ${JSON.stringify({ operation: "clear" }, null, 2)}
3403
4093
  "data-copied": copiedKey === "qgate-clear" ? "" : void 0,
3404
4094
  onClick: () => copyInstruction("qgate-clear", gateClearInstruction),
3405
4095
  title: "\u590D\u5236 iterate_quality_gate clear \u6307\u4EE4\u6587\u672C\uFF08\u91CD\u7F6E\u9648\u65E7\u7684 FAIL \u8BC1\u4E66\uFF09"
3406
- }, copiedKey === "qgate-clear" ? "\u5DF2\u590D\u5236" : "\u6E05\u9664\u95E8\u7981")
4096
+ }, copiedKey === "qgate-clear" ? "\u5DF2\u590D\u5236" : "\u6E05\u9664\u95E8\u7981"),
4097
+ // Gap #4: the browser cannot read `.iterate/` directly — one click
4098
+ // copies the pull instruction that makes the model echo it into the stream.
4099
+ React.createElement("button", {
4100
+ className: "iterate-btn",
4101
+ "data-copied": copiedKey === "disk-snapshot" ? "" : void 0,
4102
+ onClick: () => copyInstruction("disk-snapshot", pullInstruction),
4103
+ title: "\u590D\u5236\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u6307\u4EE4\uFF1A\u8BA9\u6A21\u578B\u56DE\u663E .iterate/ \u4E0B\u7684\u95E8\u7981\u3001\u7ECF\u9A8C\u3001\u9632\u5FA1\u4E8B\u4EF6\u4E0E transcript"
4104
+ }, copiedKey === "disk-snapshot" ? "\u5DF2\u590D\u5236" : "\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167")
3407
4105
  );
3408
4106
  if (!hasGate) {
3409
4107
  return React.createElement(
@@ -3422,7 +4120,18 @@ ${JSON.stringify({ operation: "clear" }, null, 2)}
3422
4120
  React.createElement(
3423
4121
  "div",
3424
4122
  { className: "iterate-obs-block-body" },
3425
- React.createElement("div", { className: "iterate-obs-msg" }, "\u8D28\u91CF\u95E8\u7981\u663E\u793A\u5404\u7EF4\u5EA6\u6536\u655B\u5EA6\u3001\u9A8C\u8BC1\u901A\u8FC7\u7387\u548C\u6574\u4F53 PASS/FAIL \u72B6\u6001\u3002"),
4123
+ React.createElement(
4124
+ "div",
4125
+ { className: "iterate-obs-msg" },
4126
+ // Gap #4: distinguish "we pulled it and it is empty" from "we
4127
+ // never asked the model to read the disk".
4128
+ diskEmptyStateText(pulled, "\u8D28\u91CF\u95E8\u7981\u8BC1\u4E66")
4129
+ ),
4130
+ React.createElement(
4131
+ "div",
4132
+ { className: "iterate-obs-msg", style: { marginTop: 6 } },
4133
+ "\u8D28\u91CF\u95E8\u7981\u663E\u793A\u5404\u7EF4\u5EA6\u6536\u655B\u5EA6\u3001\u9A8C\u8BC1\u901A\u8FC7\u7387\u548C\u6574\u4F53 PASS/FAIL \u72B6\u6001\u3002"
4134
+ ),
3426
4135
  React.createElement(
3427
4136
  "div",
3428
4137
  { className: "iterate-obs-bar", style: { marginTop: 8 } },
@@ -3431,7 +4140,13 @@ ${JSON.stringify({ operation: "clear" }, null, 2)}
3431
4140
  "data-copied": copiedKey === "qgate-instr" ? "" : void 0,
3432
4141
  onClick: () => copyInstruction("qgate-instr", gateInstruction),
3433
4142
  title: "\u590D\u5236\u67E5\u8BE2\u6307\u4EE4"
3434
- }, copiedKey === "qgate-instr" ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u67E5\u8BE2\u6307\u4EE4")
4143
+ }, copiedKey === "qgate-instr" ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u67E5\u8BE2\u6307\u4EE4"),
4144
+ React.createElement("button", {
4145
+ className: "iterate-btn",
4146
+ "data-copied": copiedKey === "disk-snapshot-2" ? "" : void 0,
4147
+ onClick: () => copyInstruction("disk-snapshot-2", pullInstruction),
4148
+ title: "\u590D\u5236\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u6307\u4EE4"
4149
+ }, copiedKey === "disk-snapshot-2" ? "\u5DF2\u590D\u5236" : "\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167")
3435
4150
  )
3436
4151
  )
3437
4152
  )
@@ -3500,12 +4215,13 @@ ${JSON.stringify({ operation: "clear" }, null, 2)}
3500
4215
 
3501
4216
  \`\`\`json
3502
4217
  ${JSON.stringify({ operation: "search", query: expSearch }, null, 2)}
3503
- \`\`\`` : "\u8BF7\u8C03\u7528 `iterate_experience` \u5217\u51FA\u6240\u6709\u7ECF\u9A8C";
4218
+ \`\`\`` : buildExperienceListInstruction();
3504
4219
  const expEntries = experienceBank && experienceBank.entries ? experienceBank.entries : [];
3505
4220
  const query = expSearch.trim().toLowerCase();
3506
4221
  const visible = query ? expEntries.filter(
3507
4222
  (e) => [e.pattern, e.description, e.id, e.dimension, e.severity, ...e.tags || []].filter(Boolean).some((t) => String(t).toLowerCase().includes(query))
3508
4223
  ) : expEntries;
4224
+ const pulled = toolCalledInSession(session, "iterate_experience");
3509
4225
  const resultRows = visible.map((e, i) => {
3510
4226
  const adoptInstruction = `\u8BF7\u8C03\u7528 \`iterate_experience\` \u91C7\u7EB3\u7ECF\u9A8C\u5E76\u5E94\u7528\u5DF2\u9A8C\u8BC1\u4FEE\u6CD5\uFF1A
3511
4227
 
@@ -3552,9 +4268,15 @@ ${JSON.stringify({ operation: "get", id: e.id }, null, 2)}
3552
4268
  className: "iterate-btn",
3553
4269
  "data-primary": "",
3554
4270
  "data-copied": copiedKey === "exp-list" ? "" : void 0,
3555
- onClick: () => copyInstruction("exp-list", "\u8BF7\u8C03\u7528 `iterate_experience` \u5217\u51FA\u6240\u6709\u7ECF\u9A8C"),
3556
- title: "\u590D\u5236\u5217\u51FA\u7ECF\u9A8C\u6307\u4EE4"
3557
- }, copiedKey === "exp-list" ? "\u5DF2\u590D\u5236" : "\u5217\u51FA\u7ECF\u9A8C")
4271
+ onClick: () => copyInstruction("exp-list", buildExperienceListInstruction()),
4272
+ title: "\u590D\u5236\u5217\u51FA\u7ECF\u9A8C\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09"
4273
+ }, copiedKey === "exp-list" ? "\u5DF2\u590D\u5236" : "\u5217\u51FA\u7ECF\u9A8C"),
4274
+ React.createElement("button", {
4275
+ className: "iterate-btn",
4276
+ "data-copied": copiedKey === "exp-snapshot" ? "" : void 0,
4277
+ onClick: () => copyInstruction("exp-snapshot", pullInstruction),
4278
+ title: "\u590D\u5236\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u6307\u4EE4\uFF1A\u628A .iterate/ \u4E0B\u7684\u7ECF\u9A8C\u94F6\u884C\u7B49\u4EA7\u7269\u56DE\u663E\u8FDB\u4F1A\u8BDD"
4279
+ }, copiedKey === "exp-snapshot" ? "\u5DF2\u590D\u5236" : "\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167")
3558
4280
  ),
3559
4281
  React.createElement(
3560
4282
  "div",
@@ -3575,7 +4297,13 @@ ${JSON.stringify({ operation: "get", id: e.id }, null, 2)}
3575
4297
  title: "\u590D\u5236\u641C\u7D22\u6307\u4EE4"
3576
4298
  }, copiedKey === "exp-search" ? "\u5DF2\u590D\u5236" : "\u641C\u7D22")
3577
4299
  ),
3578
- resultRows.length > 0 ? React.createElement("div", {}, ...resultRows) : expEntries.length > 0 ? React.createElement("div", { className: "iterate-obs-empty" }, "\u6CA1\u6709\u5339\u914D\u300C" + expSearch + "\u300D\u7684\u7ECF\u9A8C") : React.createElement("div", { className: "iterate-obs-empty" }, "\u672C\u6B21\u4F1A\u8BDD\u6682\u65E0\u7ECF\u9A8C\u8BB0\u5F55\uFF0C\u53EF\u70B9\u51FB\u300C\u5217\u51FA\u7ECF\u9A8C\u300D\u67E5\u8BE2\u5386\u53F2\u7ECF\u9A8C")
4300
+ resultRows.length > 0 ? React.createElement("div", {}, ...resultRows) : expEntries.length > 0 ? React.createElement("div", { className: "iterate-obs-empty" }, "\u6CA1\u6709\u5339\u914D\u300C" + expSearch + "\u300D\u7684\u7ECF\u9A8C") : React.createElement(
4301
+ "div",
4302
+ {},
4303
+ // Gap #4: separate "pulled and empty" from "never pulled".
4304
+ React.createElement("div", { className: "iterate-obs-empty" }, diskEmptyStateText(pulled, "\u7ECF\u9A8C\u94F6\u884C")),
4305
+ React.createElement("div", { className: "iterate-obs-empty" }, "\u4E5F\u53EF\u70B9\u300C\u5217\u51FA\u7ECF\u9A8C\u300D\u67E5\u8BE2\u78C1\u76D8\u4E0A\u7684\u5168\u90E8\u5386\u53F2\u7ECF\u9A8C\u3002")
4306
+ )
3579
4307
  );
3580
4308
  };
3581
4309
  const renderDefenseEvents = () => {
@@ -3583,7 +4311,7 @@ ${JSON.stringify({ operation: "get", id: e.id }, null, 2)}
3583
4311
 
3584
4312
  \`\`\`json
3585
4313
  ${JSON.stringify({ operation: "list", type: defenseFilter || void 0 }, null, 2)}
3586
- \`\`\`` : "\u8BF7\u8C03\u7528 `iterate_defense_events` \u5217\u51FA\u6240\u6709\u9632\u5FA1\u4E8B\u4EF6";
4314
+ \`\`\`` : buildDefenseEventsListInstruction();
3587
4315
  const typeOptions = [
3588
4316
  { value: "", label: "\u5168\u90E8\u7C7B\u578B" },
3589
4317
  { value: "precondition_failed", label: "\u524D\u7F6E\u6821\u9A8C\u5931\u8D25" },
@@ -3610,14 +4338,24 @@ ${JSON.stringify({ operation: "list", type: defenseFilter || void 0 }, null, 2)}
3610
4338
  className: "iterate-btn",
3611
4339
  "data-primary": "",
3612
4340
  "data-copied": copiedKey === "defense-list" ? "" : void 0,
3613
- onClick: () => copyInstruction("defense-list", "\u8BF7\u8C03\u7528 `iterate_defense_events` \u5217\u51FA\u6240\u6709\u9632\u5FA1\u4E8B\u4EF6"),
3614
- title: "\u590D\u5236\u5217\u51FA\u9632\u5FA1\u4E8B\u4EF6\u6307\u4EE4"
4341
+ onClick: () => copyInstruction("defense-list", buildDefenseEventsListInstruction()),
4342
+ title: "\u590D\u5236\u5217\u51FA\u9632\u5FA1\u4E8B\u4EF6\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09"
3615
4343
  }, copiedKey === "defense-list" ? "\u5DF2\u590D\u5236" : "\u5217\u51FA\u4E8B\u4EF6"),
4344
+ React.createElement("button", {
4345
+ className: "iterate-btn",
4346
+ "data-copied": copiedKey === "defense-snapshot" ? "" : void 0,
4347
+ onClick: () => copyInstruction("defense-snapshot", pullInstruction),
4348
+ title: "\u590D\u5236\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u6307\u4EE4\uFF1A\u628A .iterate/ \u4E0B\u7684\u9632\u5FA1\u4E8B\u4EF6\u6D41\u56DE\u663E\u8FDB\u4F1A\u8BDD"
4349
+ }, copiedKey === "defense-snapshot" ? "\u5DF2\u590D\u5236" : "\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167"),
3616
4350
  React.createElement("button", {
3617
4351
  className: "iterate-btn",
3618
4352
  "data-copied": copiedKey === "defense-counts" ? "" : void 0,
3619
- onClick: () => copyInstruction("defense-counts", "\u8BF7\u8C03\u7528 `iterate_defense_events` \u67E5\u8BE2\u4E8B\u4EF6\u7EDF\u8BA1"),
3620
- title: "\u590D\u5236\u7EDF\u8BA1\u6307\u4EE4"
4353
+ onClick: () => copyInstruction("defense-counts", `\u8BF7\u8C03\u7528 \`iterate_defense_events\` \u67E5\u8BE2\u4E8B\u4EF6\u7EDF\u8BA1\uFF1A
4354
+
4355
+ \`\`\`json
4356
+ ${JSON.stringify({ operation: "counts" }, null, 2)}
4357
+ \`\`\``),
4358
+ title: "\u590D\u5236\u7EDF\u8BA1\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09"
3621
4359
  }, copiedKey === "defense-counts" ? "\u5DF2\u590D\u5236" : "\u7EDF\u8BA1"),
3622
4360
  React.createElement("button", {
3623
4361
  className: "iterate-btn",
@@ -3648,7 +4386,16 @@ ${JSON.stringify({ operation: "clear" }, null, 2)}
3648
4386
  React.createElement(
3649
4387
  "div",
3650
4388
  { className: "iterate-obs-block-body" },
3651
- React.createElement("div", { className: "iterate-obs-msg" }, "\u9632\u5FA1\u4E8B\u4EF6\u5305\u62EC\uFF1A\u524D\u7F6E\u6821\u9A8C\u5931\u8D25\u3001\u56DE\u6EDA\u3001\u4E0D\u53D8\u91CF\u8FDD\u53CD\u3001\u5047\u8BBE\u88AB\u8BC1\u4F2A\u3002"),
4389
+ React.createElement(
4390
+ "div",
4391
+ { className: "iterate-obs-msg" },
4392
+ diskEmptyStateText(toolCalledInSession(session, "iterate_defense_events"), "\u9632\u5FA1\u4E8B\u4EF6\u6D41")
4393
+ ),
4394
+ React.createElement(
4395
+ "div",
4396
+ { className: "iterate-obs-msg", style: { marginTop: 6 } },
4397
+ "\u9632\u5FA1\u4E8B\u4EF6\u5305\u62EC\uFF1A\u524D\u7F6E\u6821\u9A8C\u5931\u8D25\u3001\u56DE\u6EDA\u3001\u4E0D\u53D8\u91CF\u8FDD\u53CD\u3001\u5047\u8BBE\u88AB\u8BC1\u4F2A\u3002"
4398
+ ),
3652
4399
  React.createElement(
3653
4400
  "div",
3654
4401
  { className: "iterate-obs-bar", style: { marginTop: 8 } },
@@ -3832,6 +4579,11 @@ ${JSON.stringify({ operation: "clear" }, null, 2)}
3832
4579
  React.createElement("span", { className: "iterate-obs-title" }, "iterate \u89C2\u6D4B\u53F0"),
3833
4580
  React.createElement("span", { className: "iterate-obs-badge", "data-live": live ? "" : void 0 }, runStatusText(live, manifest)),
3834
4581
  React.createElement("span", { className: "iterate-obs-head-meta" }, headMeta || "runtime"),
4582
+ // Gap #14: make the tab system discoverable without expanding it.
4583
+ React.createElement("span", {
4584
+ className: "iterate-obs-head-meta",
4585
+ title: OBS_HOTKEY_HINT
4586
+ }, OBS_HOTKEY_HINT),
3835
4587
  React.createElement("button", {
3836
4588
  className: "iterate-btn",
3837
4589
  "data-ghost": "",
@@ -3875,18 +4627,30 @@ ${JSON.stringify({ operation: "clear" }, null, 2)}
3875
4627
  "data-copied": copiedKey === "export" ? "" : void 0,
3876
4628
  onClick: exportObservatory,
3877
4629
  title: "\u5C06\u89C2\u6D4B\u53F0\u5168\u90E8\u6570\u636E\u5BFC\u51FA\u4E3A JSON\uFF08\u4F18\u5148\u4E0B\u8F7D\uFF0C\u5931\u8D25\u5219\u590D\u5236\uFF09"
3878
- }, copiedKey === "export" ? "\u5DF2\u5BFC\u51FA" : "\u5BFC\u51FA JSON")
4630
+ }, copiedKey === "export" ? "\u5DF2\u5BFC\u51FA" : "\u5BFC\u51FA JSON"),
4631
+ // Gap #4: `.iterate/` artifacts are unreachable from the browser;
4632
+ // this copies the one instruction that makes the model echo them
4633
+ // into the session stream (which is what every panel scans).
4634
+ React.createElement("button", {
4635
+ className: "iterate-btn",
4636
+ "data-copied": copiedKey === "pull-snapshot" ? "" : void 0,
4637
+ onClick: () => copyInstruction("pull-snapshot", pullInstruction),
4638
+ title: pullTitle
4639
+ }, copiedKey === "pull-snapshot" ? "\u5DF2\u590D\u5236" : "\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167")
3879
4640
  ),
3880
4641
  React.createElement(
3881
4642
  "div",
3882
- { className: "iterate-obs-tabs" },
4643
+ { className: "iterate-obs-tabs", role: "tablist", "aria-label": "\u89C2\u6D4B\u53F0\u9875\u7B7E\uFF08\u5FEB\u6377\u952E 1-9 / 0 / -\uFF09" },
3883
4644
  ...OBS_TABS.map(
3884
4645
  (t) => React.createElement("button", {
3885
4646
  key: t.key,
3886
4647
  className: "iterate-obs-tab",
4648
+ role: "tab",
4649
+ "aria-selected": tab === t.key,
3887
4650
  "data-active": tab === t.key ? "" : void 0,
3888
- onClick: () => setTab(t.key)
3889
- }, t.label)
4651
+ onClick: () => setTab(t.key),
4652
+ title: t.hotkey ? `${obsTabLabel(t)}\uFF08\u6309 ${t.hotkey} \u5207\u6362\uFF09` : `${obsTabLabel(t)}\uFF08\u6309 - \u5207\u6362\uFF09`
4653
+ }, obsTabLabel(t))
3890
4654
  )
3891
4655
  ),
3892
4656
  React.createElement("div", { className: "iterate-obs-body" }, renderBody())