iterate-plugin 3.10.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1,4 +1,4 @@
1
- /* iterate-plugin client source-digest: 2819f2e6f5f182bb31b9e47c356f60e0b8d97fcdbd232c31b89438619623cf93 */
1
+ /* iterate-plugin client source-digest: 408ff45e22a5041e34cdeb7ccd786225658f6c1fe52a7bb5233fc35450546683 */
2
2
  window.__ModuleLoader__.load({ id: "iterate-plugin", factory: (require) => {
3
3
  var module = { exports: {} };
4
4
  var exports = module.exports;
@@ -854,6 +854,62 @@ function scanSessionForDefenseEvents(session) {
854
854
  const loose = findFirstInObject(session, isDefenseEventsResult);
855
855
  return loose ? normalizeDefenseEventsResult(loose) : null;
856
856
  }
857
+ function isPreflightReport(obj) {
858
+ if (!obj || typeof obj !== "object") return false;
859
+ const o = (
860
+ /** @type {Record<string, unknown>} */
861
+ obj
862
+ );
863
+ const ready = safeGet(o, "ready");
864
+ const checks = safeGet(o, "checks");
865
+ const blocking = safeGet(o, "blocking");
866
+ const warnings = safeGet(o, "warnings");
867
+ return typeof ready === "boolean" && Array.isArray(checks) && (typeof blocking === "number" || typeof warnings === "number");
868
+ }
869
+ function findPreflightReportInObject(obj) {
870
+ return findFirstInObject(obj, (o) => isPreflightReport(o));
871
+ }
872
+ function normalizePreflightReport(raw) {
873
+ const src = raw && typeof raw === "object" ? (
874
+ /** @type {Record<string, unknown>} */
875
+ raw
876
+ ) : {};
877
+ const asNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
878
+ const asStr = (v) => typeof v === "string" ? v : "";
879
+ const checks = Array.isArray(safeGet(src, "checks")) ? safeGet(src, "checks") : [];
880
+ return {
881
+ ok: safeGet(src, "ok") !== false,
882
+ ready: safeGet(src, "ready") === true,
883
+ blocking: asNum(safeGet(src, "blocking")),
884
+ warnings: asNum(safeGet(src, "warnings")),
885
+ error: asStr(safeGet(src, "error")) || null,
886
+ checks: (
887
+ /** @type {unknown[]} */
888
+ checks.map((c) => {
889
+ const rec = (
890
+ /** @type {Record<string, unknown>} */
891
+ c && typeof c === "object" ? c : {}
892
+ );
893
+ const status = safeGet(rec, "status");
894
+ const detail = safeGet(rec, "detail");
895
+ return {
896
+ id: asStr(safeGet(rec, "id")),
897
+ status: status === "pass" || status === "warn" || status === "fail" ? status : "warn",
898
+ detail: detail && typeof detail === "object" ? detail : {}
899
+ };
900
+ })
901
+ )
902
+ };
903
+ }
904
+ function scanSessionForPreflight(session) {
905
+ const node = latestToolResultNode(session, "iterate_preflight", findPreflightReportInObject);
906
+ if (node !== null) {
907
+ const found = findPreflightReportInObject(node);
908
+ if (found) return normalizePreflightReport(found);
909
+ }
910
+ const loose = findFirstInObject(session, isPreflightReport);
911
+ return loose ? normalizePreflightReport(loose) : null;
912
+ }
857
913
  function isRunSummary(obj) {
858
914
  if (!obj || typeof obj !== "object") return false;
859
915
  const o = (
@@ -1280,6 +1336,13 @@ function buildQualityGateClearInstruction() {
1280
1336
  ${JSON.stringify({ operation: "clear" }, null, 2)}
1281
1337
  \`\`\``;
1282
1338
  }
1339
+ function buildPreflightInstruction() {
1340
+ return `\u8BF7\u8C03\u7528 \`iterate_preflight\` \u8FD0\u884C\u5F00\u8DD1\u524D\u9884\u68C0\uFF08\u53EA\u8BFB\uFF1A\u6821\u9A8C validation.commands\u3001git\u3001\u810F\u5DE5\u4F5C\u533A\u3001\u9648\u65E7\u8D28\u91CF\u95E8\u7981\u4E0E\u4E2D\u65AD\u65AD\u70B9\uFF09\uFF1A
1341
+
1342
+ \`\`\`json
1343
+ ${JSON.stringify({}, null, 2)}
1344
+ \`\`\``;
1345
+ }
1283
1346
  function buildRollbackInstruction(id) {
1284
1347
  return `\u8BF7\u8C03\u7528 \`iterate_rollback\` \u56DE\u6EDA\u4EE5\u4E0B\u4FEE\u590D\uFF1A
1285
1348
 
@@ -1555,13 +1618,35 @@ function buildCompletionSummary(report, lang = "zh") {
1555
1618
  const stats = severityStats(report);
1556
1619
  const converged = conv.converged === true;
1557
1620
  const totalFindings = stats.critical + stats.high + stats.medium + stats.low;
1621
+ const stopped = typeof conv.stoppedReason === "string" ? conv.stoppedReason : typeof report.stoppedReason === "string" ? report.stoppedReason : "";
1622
+ const reason = stopped ? completionReason(stopped, lang, total) : converged ? lang === "en" ? "converged" : "\u5DF2\u6536\u655B" : lang === "en" ? `reached the round limit ${total}` : `\u5DF2\u8FBE\u6700\u5927\u8F6E\u6570 ${total}`;
1558
1623
  if (lang === "en") {
1559
- const reason2 = converged ? "converged" : `reached the round limit ${total}`;
1560
- return `iterate review complete \xB7 ${rounds}/${total} rounds \xB7 ${totalFindings} findings \xB7 ${reason2}`;
1624
+ return `iterate review complete \xB7 ${rounds}/${total} rounds \xB7 ${totalFindings} findings \xB7 ${reason}`;
1561
1625
  }
1562
- const reason = converged ? "\u5DF2\u6536\u655B" : `\u5DF2\u8FBE\u6700\u5927\u8F6E\u6570 ${total}`;
1563
1626
  return `iterate \u8BC4\u5BA1\u5B8C\u6210 \xB7 ${rounds}/${total} \u8F6E \xB7 ${totalFindings} \u9879\u53D1\u73B0 \xB7 ${reason}`;
1564
1627
  }
1628
+ function completionReason(reason, lang, total) {
1629
+ const zh = {
1630
+ converged: "\u5DF2\u6536\u655B",
1631
+ max_rounds_reached: `\u5DF2\u8FBE\u6700\u5927\u8F6E\u6570 ${total}`,
1632
+ aborted_by_validation: "\u9A8C\u8BC1\u5931\u8D25\u540E\u56DE\u6EDA",
1633
+ aborted_by_config: "\u9A8C\u8BC1\u547D\u4EE4\u672A\u5728\u767D\u540D\u5355",
1634
+ inconclusive: "\u8F6E\u6B21\u7ED3\u8BBA\u4E0D\u660E",
1635
+ schema_invalid: "\u5BA1\u67E5\u8F93\u51FA schema \u6821\u9A8C\u5931\u8D25",
1636
+ no_usable_reviewer_output: "\u65E0\u53EF\u7528\u5BA1\u67E5\u8F93\u51FA"
1637
+ };
1638
+ const en = {
1639
+ converged: "converged",
1640
+ max_rounds_reached: `reached the round limit ${total}`,
1641
+ aborted_by_validation: "rolled back after a validation failure",
1642
+ aborted_by_config: "validation command not allow-listed",
1643
+ inconclusive: "inconclusive rounds",
1644
+ schema_invalid: "review output failed schema validation",
1645
+ no_usable_reviewer_output: "no usable review output"
1646
+ };
1647
+ const table = lang === "en" ? en : zh;
1648
+ return Object.hasOwn(table, reason) ? table[reason] : reason;
1649
+ }
1565
1650
  var CONFIG_EDIT_FIELDS = [
1566
1651
  {
1567
1652
  key: "goal",
@@ -2311,8 +2396,31 @@ function renderRunReportSarif(manifest, extra, opts) {
2311
2396
  ]
2312
2397
  };
2313
2398
  }
2399
+ function renderRunReportJson(manifest, extra) {
2400
+ const data = buildRunReportData(manifest, extra);
2401
+ return {
2402
+ $schema: "iterate-run-report/1",
2403
+ project: data.project,
2404
+ goal: data.goal,
2405
+ mode: data.mode,
2406
+ taskMode: data.taskMode,
2407
+ round: data.round,
2408
+ maxRounds: data.maxRounds,
2409
+ active: data.active,
2410
+ stoppedReason: data.stoppedReason,
2411
+ updatedAt: data.updatedAt,
2412
+ convergence: data.convergence,
2413
+ severityCounts: data.severityCounts,
2414
+ findings: data.findings,
2415
+ fixes: data.fixes,
2416
+ validations: data.validations,
2417
+ qualityGate: data.qualityGate,
2418
+ experienceBank: data.experienceBank,
2419
+ defenseEvents: data.defenseEvents
2420
+ };
2421
+ }
2314
2422
  function runReportFilename(format, opts) {
2315
- const ext = format === "sarif" ? "sarif" : "md";
2423
+ const ext = format === "sarif" ? "sarif" : format === "json" ? "json" : "md";
2316
2424
  const stamp = opts?.now && opts.now ? opts.now.replace(/[:.]/g, "-") : (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2317
2425
  return `iterate-report-${stamp}.${ext}`;
2318
2426
  }
@@ -2484,6 +2592,8 @@ var EN = {
2484
2592
  "\u5BFC\u51FA\u53EF\u7C98\u8D34\u8FDB PR \u7684 Markdown \u8FD0\u884C\u62A5\u544A\uFF08\u4F18\u5148\u4E0B\u8F7D\uFF0C\u5931\u8D25\u5219\u590D\u5236\uFF09": "Export a PR-pasteable Markdown run report (download first, copy on failure)",
2485
2593
  "\u5BFC\u51FA SARIF": "Export SARIF",
2486
2594
  "\u5BFC\u51FA SARIF 2.1.0\uFF08\u4F9B CI / GitHub Code Scanning \u6D88\u8D39\uFF09": "Export SARIF 2.1.0 for CI / GitHub Code Scanning",
2595
+ "\u5BFC\u51FA JSON \u62A5\u544A": "Export JSON report",
2596
+ "\u5BFC\u51FA\u5B8C\u6574\u8FD0\u884C\u6A21\u578B\u7684 JSON\uFF08\u4F9B CI \u4EEA\u8868\u76D8 / PR \u673A\u5668\u4EBA\u6D88\u8D39\uFF0C\u65E0\u635F\u683C\u5F0F\uFF09": "Export the complete run model as JSON (lossless, for CI dashboards / PR bots)",
2487
2597
  "\u89C2\u6D4B\u53F0\u9875\u7B7E\uFF08\u5FEB\u6377\u952E 1-9 / 0 / -\uFF09": "Observatory tabs (keys 1-9 / 0 / -)",
2488
2598
  "{a0}\uFF08\u6309 {a1} \u5207\u6362\uFF09": "{a0} (press {a1})",
2489
2599
  "{a0}\uFF08\u6309 - \u5207\u6362\uFF09": "{a0} (press -)",
@@ -2633,6 +2743,38 @@ var EN = {
2633
2743
  \u6E05\u9664\u7C7B\u578B\u7B5B\u9009: "Clear type filter",
2634
2744
  "\u9632\u7EBF\uFF1A{a0}": "Defense: {a0}",
2635
2745
  \u6CA1\u6709\u5339\u914D\u8BE5\u7C7B\u578B\u7684\u9632\u5FA1\u4E8B\u4EF6: "No defense events of this type",
2746
+ // ── F11 preflight ────────────────────────────────────────────────────────
2747
+ \u5F00\u8DD1\u9884\u68C0: "Preflight",
2748
+ \u5F85\u9884\u68C0: "Not run yet",
2749
+ \u672A\u5C31\u7EEA: "Not ready",
2750
+ "\u590D\u5236 iterate_preflight \u9884\u68C0\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09": "Copy the iterate_preflight readiness instruction (JSON arguments included)",
2751
+ \u8FD0\u884C\u9884\u68C0: "Run preflight",
2752
+ \u5F00\u8DD1\u9884\u68C0\u6E05\u5355: "Preflight checklist",
2753
+ \u9884\u68C0\u7ED3\u679C: "Preflight result",
2754
+ "\u5F00\u8DD1\u524D\u5148\u9884\u68C0\uFF1A\u786E\u8BA4\u9A8C\u8BC1\u547D\u4EE4\u3001Git\u3001\u5DE5\u4F5C\u533A\u3001\u9648\u65E7\u95E8\u7981\u4E0E\u4E2D\u65AD\u65AD\u70B9\u90FD\u5DF2\u5C31\u7EEA\uFF0C\u907F\u514D\u5F00\u8DD1\u540E\u624D\u53D1\u73B0\u73AF\u5883\u95EE\u9898\u3002": "Run preflight first: confirm validation commands, git, the working tree, a stale gate and an interrupted checkpoint are ready, so you do not discover an environment problem mid-run.",
2755
+ \u590D\u5236\u9884\u68C0\u6307\u4EE4: "Copy preflight instruction",
2756
+ \u9A8C\u8BC1\u547D\u4EE4: "Validation commands",
2757
+ "\u5DF2\u914D\u7F6E {a0} \u6761\u9A8C\u8BC1\u547D\u4EE4": "{a0} validation command(s) configured",
2758
+ "\u672A\u914D\u7F6E validation.commands \u2014\u2014 \u672C\u8F6E\u65E0\u6CD5\u8BC1\u660E\u4FEE\u590D\uFF0C\u5C5E\u963B\u65AD\u9879": "validation.commands is not configured \u2014 the run cannot prove fixes (blocking)",
2759
+ "Git \u4ED3\u5E93": "Git repository",
2760
+ "\u5DF2\u68C0\u6D4B\u5230 Git \u4ED3\u5E93": "Git repository detected",
2761
+ "\u4E0D\u662F Git \u4ED3\u5E93 \u2014\u2014 changed-only \u8303\u56F4\u4E0E iterate_diff \u9700\u8981\u5B83": "Not a git repository \u2014 changed-only scope and iterate_diff need one",
2762
+ \u5DE5\u4F5C\u533A: "Working tree",
2763
+ "\u672A\u63D0\u4EA4\u6539\u52A8\uFF1A\u672A\u77E5": "Uncommitted changes: unknown",
2764
+ "{a0} \u4E2A\u672A\u63D0\u4EA4/\u672A\u8DDF\u8E2A\u6587\u4EF6 \u2014\u2014 \u5EFA\u8BAE\u5148 commit \u6216 stash": "{a0} uncommitted/untracked file(s) \u2014 commit or stash first",
2765
+ \u5DE5\u4F5C\u533A\u5E72\u51C0: "Working tree clean",
2766
+ \u9648\u65E7\u95E8\u7981: "Stale gate",
2767
+ "\u78C1\u76D8\u4E0A\u6B8B\u7559 FAIL \u8D28\u91CF\u95E8\u7981 \u2014\u2014 \u5F00\u65B0\u8F6E\u524D\u5148\u6E05\u9664": "A stale FAIL quality gate is persisted \u2014 clear it before a fresh run",
2768
+ "\u8D28\u91CF\u95E8\u7981\uFF1A{a0}": "Quality gate: {a0}",
2769
+ \u65E0\u6301\u4E45\u5316\u8D28\u91CF\u95E8\u7981: "No persisted quality gate",
2770
+ \u4E2D\u65AD\u65AD\u70B9: "Interrupted checkpoint",
2771
+ "\u5B58\u5728\u4E2D\u65AD\u65AD\u70B9\uFF08\u5DF2\u6062\u590D {a0} \u6B21\uFF09\u2014\u2014 \u6062\u590D\u6216\u6E05\u9664\u540E\u518D\u5F00\u8DD1": "An interrupted checkpoint is waiting (resumed {a0}x) \u2014 resume or clear it first",
2772
+ \u65E0\u4E2D\u65AD\u65AD\u70B9: "No interrupted checkpoint",
2773
+ \u914D\u7F6E\u9A8C\u8BC1\u547D\u4EE4: "Configure validation",
2774
+ \u6062\u590D\u65AD\u70B9: "Resume checkpoint",
2775
+ "\u963B\u65AD {a0} \xB7 \u8B66\u544A {a1}": "Blocking {a0} \xB7 warnings {a1}",
2776
+ "\u9884\u68C0\u5931\u8D25\uFF1A{a0}": "Preflight failed: {a0}",
2777
+ "\u9884\u68C0\u53EA\u8BFB\uFF0C\u4E0D\u4F1A\u4FEE\u6539\u914D\u7F6E\u6216\u6587\u4EF6\u3002": "Preflight is read-only; it never modifies config or files.",
2636
2778
  // ── copy feedback ────────────────────────────────────────────────────────
2637
2779
  \u5DF2\u590D\u5236: "Copied",
2638
2780
  \u590D\u5236\u5931\u8D25: "Copy failed"
@@ -2882,6 +3024,7 @@ var ITERATE_CSS = `
2882
3024
  .iterate-gate[data-status="pass"] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
2883
3025
  .iterate-gate[data-status="fail"] { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent); }
2884
3026
  .iterate-gate[data-status="pending"] { color: var(--dsw-alias-label-secondary); background: color-mix(in srgb, var(--dsw-alias-label-secondary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-label-secondary) 28%, transparent); }
3027
+ .iterate-gate[data-status="warn"] { color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 28%, transparent); }
2885
3028
 
2886
3029
  /* v3.0: Experience bank hit highlight */
2887
3030
  .iterate-exp-hit { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); font-size: 11px; color: var(--dsw-alias-state-success-primary); }
@@ -4318,7 +4461,9 @@ var OBS_TABS = [
4318
4461
  // v3.0: Quality Command Center tabs
4319
4462
  { key: "f8", label: { zh: "\u8D28\u91CF\u95E8\u7981", en: "Quality gate" }, hotkey: "8" },
4320
4463
  { key: "f9", label: { zh: "\u7ECF\u9A8C\u94F6\u884C", en: "Experience bank" }, hotkey: "9" },
4321
- { key: "f10", label: { zh: "\u9632\u5FA1\u4E8B\u4EF6", en: "Defense events" }, hotkey: "0" }
4464
+ { key: "f10", label: { zh: "\u9632\u5FA1\u4E8B\u4EF6", en: "Defense events" }, hotkey: "0" },
4465
+ // v3.12: pre-run readiness checklist (validation/git/dirty-tree/stale-gate/checkpoint)
4466
+ { key: "f11", label: { zh: "\u5F00\u8DD1\u9884\u68C0", en: "Preflight" }, hotkey: "" }
4322
4467
  ];
4323
4468
  var OBS_HOTKEY_TABS = {
4324
4469
  1: "f1",
@@ -4389,6 +4534,10 @@ function latestDefenseEvents(session) {
4389
4534
  if (!session) return null;
4390
4535
  return scanSessionForDefenseEvents(session);
4391
4536
  }
4537
+ function latestPreflight(session) {
4538
+ if (!session) return null;
4539
+ return scanSessionForPreflight(session);
4540
+ }
4392
4541
  function buildObsFixInstruction(f) {
4393
4542
  return buildFixInstruction(f);
4394
4543
  }
@@ -4401,6 +4550,7 @@ function ObservatoryPanel(props) {
4401
4550
  const qualityGate = latestQualityGate(session);
4402
4551
  const experienceBank = latestExperienceBank(session);
4403
4552
  const defenseEvents = latestDefenseEvents(session);
4553
+ const preflight = latestPreflight(session);
4404
4554
  const [open, setOpen] = React.useState(false);
4405
4555
  const [tab, setTab] = React.useState("live");
4406
4556
  const [expandedThreads, setExpandedThreads] = React.useState(/* @__PURE__ */ new Set());
@@ -4495,7 +4645,7 @@ function ObservatoryPanel(props) {
4495
4645
  const exportReport = (format) => {
4496
4646
  if (!manifest) return;
4497
4647
  const extra = { qualityGate, experienceBank, defenseEvents };
4498
- const content = format === "sarif" ? JSON.stringify(renderRunReportSarif(manifest, extra), null, 2) : renderRunReportMarkdown(manifest, extra, { lang: getLang() });
4648
+ const content = format === "sarif" ? JSON.stringify(renderRunReportSarif(manifest, extra), null, 2) : format === "json" ? JSON.stringify(renderRunReportJson(manifest, extra), null, 2) : renderRunReportMarkdown(manifest, extra, { lang: getLang() });
4499
4649
  const filename = runReportFilename(format);
4500
4650
  const key = `report-${format}`;
4501
4651
  const flash = () => {
@@ -5552,6 +5702,195 @@ function ObservatoryPanel(props) {
5552
5702
  eventRows.length > 0 ? React.createElement("div", {}, ...eventRows) : React.createElement("div", { className: "iterate-obs-empty" }, t("\u6CA1\u6709\u5339\u914D\u8BE5\u7C7B\u578B\u7684\u9632\u5FA1\u4E8B\u4EF6"))
5553
5703
  );
5554
5704
  };
5705
+ const renderPreflight = () => {
5706
+ const preflightInstruction = buildPreflightInstruction();
5707
+ const report = preflight;
5708
+ const hasReport = report !== null && Array.isArray(report.checks);
5709
+ const ready = report !== null && report.ready === true;
5710
+ const blocking = Number(report && report.blocking != null ? report.blocking : 0);
5711
+ const warnings = Number(report && report.warnings != null ? report.warnings : 0);
5712
+ const statusLabel = !hasReport ? t("\u5F85\u9884\u68C0") : ready ? t("\u5C31\u7EEA") : t("\u672A\u5C31\u7EEA");
5713
+ const statusTone = !hasReport ? "pending" : ready ? "pass" : "fail";
5714
+ const headerBar = React.createElement(
5715
+ "div",
5716
+ { className: "iterate-obs-bar", style: { marginBottom: 8 } },
5717
+ React.createElement("b", {}, t("\u5F00\u8DD1\u9884\u68C0")),
5718
+ React.createElement("span", { className: "iterate-gate", "data-status": statusTone }, statusLabel),
5719
+ React.createElement("button", {
5720
+ className: "iterate-btn",
5721
+ "data-primary": "",
5722
+ "data-copied": copiedKey === "preflight" ? "" : void 0,
5723
+ onClick: () => copyInstruction("preflight", preflightInstruction),
5724
+ title: t("\u590D\u5236 iterate_preflight \u9884\u68C0\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09")
5725
+ }, copiedKey === "preflight" ? t("\u5DF2\u590D\u5236") : t("\u8FD0\u884C\u9884\u68C0"))
5726
+ );
5727
+ if (!hasReport) {
5728
+ return React.createElement(
5729
+ "div",
5730
+ {},
5731
+ headerBar,
5732
+ React.createElement(
5733
+ "div",
5734
+ { className: "iterate-obs-block" },
5735
+ React.createElement(
5736
+ "div",
5737
+ { className: "iterate-obs-block-head" },
5738
+ React.createElement("span", {}, t("\u5F00\u8DD1\u9884\u68C0\u6E05\u5355")),
5739
+ React.createElement("span", { className: "iterate-obs-badge" }, "v3.12")
5740
+ ),
5741
+ React.createElement(
5742
+ "div",
5743
+ { className: "iterate-obs-block-body" },
5744
+ React.createElement(
5745
+ "div",
5746
+ { className: "iterate-obs-msg" },
5747
+ // Gap #4: separate "pulled and empty" from "never pulled".
5748
+ diskEmptyStateText(toolCalledInSession(session, "iterate_preflight"), t("\u9884\u68C0\u7ED3\u679C"), getLang())
5749
+ ),
5750
+ React.createElement(
5751
+ "div",
5752
+ { className: "iterate-obs-msg", style: { marginTop: 6 } },
5753
+ t("\u5F00\u8DD1\u524D\u5148\u9884\u68C0\uFF1A\u786E\u8BA4\u9A8C\u8BC1\u547D\u4EE4\u3001Git\u3001\u5DE5\u4F5C\u533A\u3001\u9648\u65E7\u95E8\u7981\u4E0E\u4E2D\u65AD\u65AD\u70B9\u90FD\u5DF2\u5C31\u7EEA\uFF0C\u907F\u514D\u5F00\u8DD1\u540E\u624D\u53D1\u73B0\u73AF\u5883\u95EE\u9898\u3002")
5754
+ ),
5755
+ React.createElement(
5756
+ "div",
5757
+ { className: "iterate-obs-bar", style: { marginTop: 8 } },
5758
+ React.createElement("button", {
5759
+ className: "iterate-btn",
5760
+ "data-copied": copiedKey === "preflight-instr" ? "" : void 0,
5761
+ onClick: () => copyInstruction("preflight-instr", preflightInstruction),
5762
+ title: t("\u590D\u5236\u9884\u68C0\u6307\u4EE4")
5763
+ }, copiedKey === "preflight-instr" ? t("\u5DF2\u590D\u5236") : t("\u590D\u5236\u9884\u68C0\u6307\u4EE4"))
5764
+ )
5765
+ )
5766
+ )
5767
+ );
5768
+ }
5769
+ if (report && report.ok === false) {
5770
+ return React.createElement(
5771
+ "div",
5772
+ {},
5773
+ headerBar,
5774
+ React.createElement(
5775
+ "div",
5776
+ { className: "iterate-obs-block" },
5777
+ React.createElement(
5778
+ "div",
5779
+ { className: "iterate-obs-block-head" },
5780
+ React.createElement("span", {}, t("\u5F00\u8DD1\u9884\u68C0\u6E05\u5355")),
5781
+ React.createElement("span", { className: "iterate-obs-badge" }, "v3.12")
5782
+ ),
5783
+ React.createElement(
5784
+ "div",
5785
+ { className: "iterate-obs-block-body" },
5786
+ React.createElement(
5787
+ "div",
5788
+ { className: "iterate-obs-msg" },
5789
+ tf("\u9884\u68C0\u5931\u8D25\uFF1A{a0}", { a0: String(report.error || "") })
5790
+ )
5791
+ )
5792
+ )
5793
+ );
5794
+ }
5795
+ const CHECK_META = {
5796
+ validation: {
5797
+ label: t("\u9A8C\u8BC1\u547D\u4EE4"),
5798
+ detail: (d) => {
5799
+ const n = Number(d.commandCount ?? 0);
5800
+ return n > 0 ? tf("\u5DF2\u914D\u7F6E {a0} \u6761\u9A8C\u8BC1\u547D\u4EE4", { a0: n }) : t("\u672A\u914D\u7F6E validation.commands \u2014\u2014 \u672C\u8F6E\u65E0\u6CD5\u8BC1\u660E\u4FEE\u590D\uFF0C\u5C5E\u963B\u65AD\u9879");
5801
+ }
5802
+ },
5803
+ git: {
5804
+ label: t("Git \u4ED3\u5E93"),
5805
+ detail: (d) => d.isRepo === true ? t("\u5DF2\u68C0\u6D4B\u5230 Git \u4ED3\u5E93") : t("\u4E0D\u662F Git \u4ED3\u5E93 \u2014\u2014 changed-only \u8303\u56F4\u4E0E iterate_diff \u9700\u8981\u5B83")
5806
+ },
5807
+ "dirty-tree": {
5808
+ label: t("\u5DE5\u4F5C\u533A"),
5809
+ detail: (d) => {
5810
+ const n = d.changedFiles;
5811
+ if (n === null || n === void 0) return t("\u672A\u63D0\u4EA4\u6539\u52A8\uFF1A\u672A\u77E5");
5812
+ const num = Number(n);
5813
+ return num > 0 ? tf("{a0} \u4E2A\u672A\u63D0\u4EA4/\u672A\u8DDF\u8E2A\u6587\u4EF6 \u2014\u2014 \u5EFA\u8BAE\u5148 commit \u6216 stash", { a0: num }) : t("\u5DE5\u4F5C\u533A\u5E72\u51C0");
5814
+ }
5815
+ },
5816
+ "stale-gate": {
5817
+ label: t("\u9648\u65E7\u95E8\u7981"),
5818
+ detail: (d) => {
5819
+ const s = d.overallStatus;
5820
+ if (s === "fail") return t("\u78C1\u76D8\u4E0A\u6B8B\u7559 FAIL \u8D28\u91CF\u95E8\u7981 \u2014\u2014 \u5F00\u65B0\u8F6E\u524D\u5148\u6E05\u9664");
5821
+ return s ? tf("\u8D28\u91CF\u95E8\u7981\uFF1A{a0}", { a0: String(s) }) : t("\u65E0\u6301\u4E45\u5316\u8D28\u91CF\u95E8\u7981");
5822
+ }
5823
+ },
5824
+ "interrupted-checkpoint": {
5825
+ label: t("\u4E2D\u65AD\u65AD\u70B9"),
5826
+ detail: (d) => {
5827
+ if (d.interrupted === true) {
5828
+ const rc = Number(d.resumeCount ?? 0);
5829
+ return tf("\u5B58\u5728\u4E2D\u65AD\u65AD\u70B9\uFF08\u5DF2\u6062\u590D {a0} \u6B21\uFF09\u2014\u2014 \u6062\u590D\u6216\u6E05\u9664\u540E\u518D\u5F00\u8DD1", { a0: rc });
5830
+ }
5831
+ return t("\u65E0\u4E2D\u65AD\u65AD\u70B9");
5832
+ }
5833
+ }
5834
+ };
5835
+ const fixFor = (id) => {
5836
+ switch (id) {
5837
+ case "validation":
5838
+ return { key: "preflight-fix-validation", label: t("\u914D\u7F6E\u9A8C\u8BC1\u547D\u4EE4"), instruction: buildConfigFieldInstruction("validation.commands") };
5839
+ case "stale-gate":
5840
+ return { key: "preflight-fix-gate", label: t("\u6E05\u9664\u95E8\u7981"), instruction: buildQualityGateClearInstruction() };
5841
+ case "interrupted-checkpoint":
5842
+ return { key: "preflight-fix-checkpoint", label: t("\u6062\u590D\u65AD\u70B9"), instruction: buildCheckpointResumeInstruction() };
5843
+ default:
5844
+ return null;
5845
+ }
5846
+ };
5847
+ const rows = (report.checks || []).map((c, i) => {
5848
+ const id = String(c.id || "");
5849
+ const status = String(c.status || "warn");
5850
+ const meta = CHECK_META[id];
5851
+ const label = meta ? meta.label : id || "?";
5852
+ const detail = meta ? meta.detail(c.detail || {}) : "";
5853
+ const fix = status !== "pass" ? fixFor(id) : null;
5854
+ return React.createElement(
5855
+ "div",
5856
+ { key: `pf-${i}`, className: "iterate-obs-row" },
5857
+ React.createElement("span", { className: "iterate-gate", "data-status": status }, status.toUpperCase()),
5858
+ React.createElement("b", {}, label),
5859
+ React.createElement("span", { className: "iterate-obs-msg" }, detail),
5860
+ fix ? React.createElement("button", {
5861
+ className: "iterate-btn",
5862
+ "data-copied": copiedKey === fix.key ? "" : void 0,
5863
+ onClick: () => copyInstruction(fix.key, fix.instruction),
5864
+ title: fix.instruction
5865
+ }, copiedKey === fix.key ? t("\u5DF2\u590D\u5236") : fix.label) : null
5866
+ );
5867
+ });
5868
+ return React.createElement(
5869
+ "div",
5870
+ {},
5871
+ headerBar,
5872
+ React.createElement(
5873
+ "div",
5874
+ { className: "iterate-obs-block" },
5875
+ React.createElement(
5876
+ "div",
5877
+ { className: "iterate-obs-block-head" },
5878
+ React.createElement("span", {}, t("\u5F00\u8DD1\u9884\u68C0\u6E05\u5355")),
5879
+ React.createElement(
5880
+ "span",
5881
+ { className: "iterate-obs-head-meta" },
5882
+ tf("\u963B\u65AD {a0} \xB7 \u8B66\u544A {a1}", { a0: blocking, a1: warnings })
5883
+ )
5884
+ ),
5885
+ React.createElement(
5886
+ "div",
5887
+ { className: "iterate-obs-block-body" },
5888
+ React.createElement("div", { className: "iterate-obs-msg" }, t("\u9884\u68C0\u53EA\u8BFB\uFF0C\u4E0D\u4F1A\u4FEE\u6539\u914D\u7F6E\u6216\u6587\u4EF6\u3002")),
5889
+ ...rows
5890
+ )
5891
+ )
5892
+ );
5893
+ };
5555
5894
  const renderLive = () => {
5556
5895
  const liveEntries = manifest.live || [];
5557
5896
  if (liveEntries.length === 0) {
@@ -5648,6 +5987,8 @@ function ObservatoryPanel(props) {
5648
5987
  return renderExperienceBank();
5649
5988
  case "f10":
5650
5989
  return renderDefenseEvents();
5990
+ case "f11":
5991
+ return renderPreflight();
5651
5992
  default:
5652
5993
  return null;
5653
5994
  }
@@ -5711,8 +6052,8 @@ function ObservatoryPanel(props) {
5711
6052
  title: t("\u5C06\u89C2\u6D4B\u53F0\u5168\u90E8\u6570\u636E\u5BFC\u51FA\u4E3A JSON\uFF08\u4F18\u5148\u4E0B\u8F7D\uFF0C\u5931\u8D25\u5219\u590D\u5236\uFF09")
5712
6053
  }, copiedKey === "export" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA JSON")),
5713
6054
  // v3.9: portable report export — Markdown for a PR body, SARIF for
5714
- // CI / GitHub Code Scanning. Rendered from the same data model as
5715
- // the `iterate_report` tool.
6055
+ // CI / GitHub Code Scanning, JSON for CI dashboards / PR bots.
6056
+ // Rendered from the same data model as the `iterate_report` tool.
5716
6057
  React.createElement("button", {
5717
6058
  className: "iterate-btn",
5718
6059
  "data-copied": copiedKey === "report-markdown" ? "" : void 0,
@@ -5725,6 +6066,15 @@ function ObservatoryPanel(props) {
5725
6066
  onClick: () => exportReport("sarif"),
5726
6067
  title: t("\u5BFC\u51FA SARIF 2.1.0\uFF08\u4F9B CI / GitHub Code Scanning \u6D88\u8D39\uFF09")
5727
6068
  }, copiedKey === "report-sarif" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA SARIF")),
6069
+ // v3.11: lossless JSON export — the complete normalized run model
6070
+ // (findings + fixes + validations + quality gate + experience bank
6071
+ // + defense events) for CI dashboards and PR bots.
6072
+ React.createElement("button", {
6073
+ className: "iterate-btn",
6074
+ "data-copied": copiedKey === "report-json" ? "" : void 0,
6075
+ onClick: () => exportReport("json"),
6076
+ title: t("\u5BFC\u51FA\u5B8C\u6574\u8FD0\u884C\u6A21\u578B\u7684 JSON\uFF08\u4F9B CI \u4EEA\u8868\u76D8 / PR \u673A\u5668\u4EBA\u6D88\u8D39\uFF0C\u65E0\u635F\u683C\u5F0F\uFF09")
6077
+ }, copiedKey === "report-json" ? t("\u5DF2\u5BFC\u51FA") : t("\u5BFC\u51FA JSON \u62A5\u544A")),
5728
6078
  // Gap #4: `.iterate/` artifacts are unreachable from the browser;
5729
6079
  // this copies the one instruction that makes the model echo them
5730
6080
  // into the session stream (which is what every panel scans).
package/lib/parse.js CHANGED
@@ -1124,6 +1124,88 @@ export function scanSessionForDefenseEvents(session) {
1124
1124
  return loose ? normalizeDefenseEventsResult(loose) : null
1125
1125
  }
1126
1126
 
1127
+ // ─── Preflight readiness report detection ────────────────────────────────────
1128
+
1129
+ /**
1130
+ * Check whether `obj` is an `iterate_preflight` report.
1131
+ *
1132
+ * Discriminator: a boolean `ready`, an array `checks`, and a numeric
1133
+ * `blocking`/`warnings` counter. The `ok:false` error shape (empty checks)
1134
+ * still matches so the panel can render the failure instead of claiming
1135
+ * "not pulled yet".
1136
+ *
1137
+ * @param {unknown} obj
1138
+ * @returns {boolean}
1139
+ */
1140
+ export function isPreflightReport(obj) {
1141
+ if (!obj || typeof obj !== 'object') return false
1142
+ const o = /** @type {Record<string, unknown>} */ (obj)
1143
+ const ready = safeGet(o, 'ready')
1144
+ const checks = safeGet(o, 'checks')
1145
+ const blocking = safeGet(o, 'blocking')
1146
+ const warnings = safeGet(o, 'warnings')
1147
+ return (
1148
+ typeof ready === 'boolean' &&
1149
+ Array.isArray(checks) &&
1150
+ (typeof blocking === 'number' || typeof warnings === 'number')
1151
+ )
1152
+ }
1153
+
1154
+ /** Deep-find the first `iterate_preflight` report node inside `obj`. */
1155
+ function findPreflightReportInObject(obj) {
1156
+ return findFirstInObject(obj, (o) => isPreflightReport(o))
1157
+ }
1158
+
1159
+ /**
1160
+ * Normalize an `iterate_preflight` report into a JSON-safe object.
1161
+ *
1162
+ * @param {Record<string, unknown> | null | undefined} raw
1163
+ * @returns {Record<string, unknown>}
1164
+ */
1165
+ export function normalizePreflightReport(raw) {
1166
+ const src = raw && typeof raw === 'object'
1167
+ ? /** @type {Record<string, unknown>} */ (raw)
1168
+ : {}
1169
+ const asNum = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
1170
+ const asStr = (v) => (typeof v === 'string' ? v : '')
1171
+ const checks = Array.isArray(safeGet(src, 'checks')) ? safeGet(src, 'checks') : []
1172
+ return {
1173
+ ok: safeGet(src, 'ok') !== false,
1174
+ ready: safeGet(src, 'ready') === true,
1175
+ blocking: asNum(safeGet(src, 'blocking')),
1176
+ warnings: asNum(safeGet(src, 'warnings')),
1177
+ error: asStr(safeGet(src, 'error')) || null,
1178
+ checks: (/** @type {unknown[]} */ (checks)).map((c) => {
1179
+ const rec = /** @type {Record<string, unknown>} */ (c && typeof c === 'object' ? c : {})
1180
+ const status = safeGet(rec, 'status')
1181
+ const detail = safeGet(rec, 'detail')
1182
+ return {
1183
+ id: asStr(safeGet(rec, 'id')),
1184
+ status: status === 'pass' || status === 'warn' || status === 'fail' ? status : 'warn',
1185
+ detail: detail && typeof detail === 'object' ? detail : {},
1186
+ }
1187
+ }),
1188
+ }
1189
+ }
1190
+
1191
+ /**
1192
+ * Scan a session snapshot for the latest `iterate_preflight` result and return
1193
+ * its normalized report (or null).
1194
+ *
1195
+ * @param {unknown} session
1196
+ * @returns {Record<string, unknown> | null}
1197
+ */
1198
+ export function scanSessionForPreflight(session) {
1199
+ const node = latestToolResultNode(session, 'iterate_preflight', findPreflightReportInObject)
1200
+ if (node !== null) {
1201
+ const found = findPreflightReportInObject(node)
1202
+ if (found) return normalizePreflightReport(found)
1203
+ }
1204
+ // Same deep-scan fallback as the quality-gate scanner (see above).
1205
+ const loose = findFirstInObject(session, isPreflightReport)
1206
+ return loose ? normalizePreflightReport(loose) : null
1207
+ }
1208
+
1127
1209
  // ─── Run-summary / meta-review verdict detection ─────────────────────────────
1128
1210
 
1129
1211
  /**
@@ -1857,6 +1939,21 @@ export function buildQualityGateClearInstruction() {
1857
1939
  )
1858
1940
  }
1859
1941
 
1942
+ /**
1943
+ * `iterate_preflight` readiness-check payload (runs the pre-run checklist).
1944
+ *
1945
+ * @returns {string}
1946
+ */
1947
+ export function buildPreflightInstruction() {
1948
+ return (
1949
+ '请调用 `iterate_preflight` 运行开跑前预检(只读:校验 validation.commands、git、脏工作区、' +
1950
+ '陈旧质量门禁与中断断点):\n\n' +
1951
+ '```json\n' +
1952
+ `${JSON.stringify({}, null, 2)}\n` +
1953
+ '```'
1954
+ )
1955
+ }
1956
+
1860
1957
  /**
1861
1958
  * `iterate_rollback` payload for one applied fix id.
1862
1959
  *
@@ -2310,14 +2407,63 @@ export function buildCompletionSummary(report, lang = 'zh') {
2310
2407
  const stats = severityStats(report)
2311
2408
  const converged = conv.converged === true
2312
2409
  const totalFindings = stats.critical + stats.high + stats.medium + stats.low
2410
+ // The persisted `stoppedReason` is the authoritative "why" — a validation
2411
+ // abort or a schema failure must NOT be reported as "reached the round
2412
+ // limit". A normalized report nests it under `convergence.stoppedReason`
2413
+ // (normalizeReport); a raw harness report may carry it at the top level.
2414
+ // Fall back to the convergence flag only when no reason was stored (older
2415
+ // transcripts predate the field).
2416
+ const stopped = typeof conv.stoppedReason === 'string'
2417
+ ? conv.stoppedReason
2418
+ : typeof report.stoppedReason === 'string'
2419
+ ? report.stoppedReason
2420
+ : ''
2421
+ const reason = stopped
2422
+ ? completionReason(stopped, lang, total)
2423
+ : converged
2424
+ ? (lang === 'en' ? 'converged' : '已收敛')
2425
+ : (lang === 'en' ? `reached the round limit ${total}` : `已达最大轮数 ${total}`)
2313
2426
  if (lang === 'en') {
2314
- const reason = converged ? 'converged' : `reached the round limit ${total}`
2315
2427
  return `iterate review complete \u00b7 ${rounds}/${total} rounds \u00b7 ${totalFindings} findings \u00b7 ${reason}`
2316
2428
  }
2317
- const reason = converged ? '已收敛' : `已达最大轮数 ${total}`
2318
2429
  return `iterate 评审完成 · ${rounds}/${total} 轮 · ${totalFindings} 项发现 · ${reason}`
2319
2430
  }
2320
2431
 
2432
+ /**
2433
+ * Compact "why the run ended" phrase for the completion summary.
2434
+ * Mirrors `stoppedReasonLabel` but drops the "已结束 · " prefix so it reads
2435
+ * inside the one-line summary. Unknown reasons degrade to the raw value.
2436
+ *
2437
+ * @param {string} reason
2438
+ * @param {string} lang
2439
+ * @param {number} total
2440
+ * @returns {string}
2441
+ */
2442
+ function completionReason(reason, lang, total) {
2443
+ const zh = {
2444
+ converged: '已收敛',
2445
+ max_rounds_reached: `已达最大轮数 ${total}`,
2446
+ aborted_by_validation: '验证失败后回滚',
2447
+ aborted_by_config: '验证命令未在白名单',
2448
+ inconclusive: '轮次结论不明',
2449
+ schema_invalid: '审查输出 schema 校验失败',
2450
+ no_usable_reviewer_output: '无可用审查输出',
2451
+ }
2452
+ const en = {
2453
+ converged: 'converged',
2454
+ max_rounds_reached: `reached the round limit ${total}`,
2455
+ aborted_by_validation: 'rolled back after a validation failure',
2456
+ aborted_by_config: 'validation command not allow-listed',
2457
+ inconclusive: 'inconclusive rounds',
2458
+ schema_invalid: 'review output failed schema validation',
2459
+ no_usable_reviewer_output: 'no usable review output',
2460
+ }
2461
+ const table = lang === 'en' ? en : zh
2462
+ // Object.hasOwn, NOT `table[reason]`: a hand-edited `__proto__` reason must
2463
+ // not resolve through the prototype chain to a non-string.
2464
+ return Object.hasOwn(table, reason) ? table[reason] : reason
2465
+ }
2466
+
2321
2467
  // ─── Config edit guidance ───────────────────────────────────────────────────
2322
2468
 
2323
2469
  /**