iterate-plugin 3.11.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: e3baa31a1f6965343996a15a146b539f5c1d02547014ae0cf0b2f90809967b7e */
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",
@@ -2658,6 +2743,38 @@ var EN = {
2658
2743
  \u6E05\u9664\u7C7B\u578B\u7B5B\u9009: "Clear type filter",
2659
2744
  "\u9632\u7EBF\uFF1A{a0}": "Defense: {a0}",
2660
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.",
2661
2778
  // ── copy feedback ────────────────────────────────────────────────────────
2662
2779
  \u5DF2\u590D\u5236: "Copied",
2663
2780
  \u590D\u5236\u5931\u8D25: "Copy failed"
@@ -2907,6 +3024,7 @@ var ITERATE_CSS = `
2907
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); }
2908
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); }
2909
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); }
2910
3028
 
2911
3029
  /* v3.0: Experience bank hit highlight */
2912
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); }
@@ -4343,7 +4461,9 @@ var OBS_TABS = [
4343
4461
  // v3.0: Quality Command Center tabs
4344
4462
  { key: "f8", label: { zh: "\u8D28\u91CF\u95E8\u7981", en: "Quality gate" }, hotkey: "8" },
4345
4463
  { key: "f9", label: { zh: "\u7ECF\u9A8C\u94F6\u884C", en: "Experience bank" }, hotkey: "9" },
4346
- { 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: "" }
4347
4467
  ];
4348
4468
  var OBS_HOTKEY_TABS = {
4349
4469
  1: "f1",
@@ -4414,6 +4534,10 @@ function latestDefenseEvents(session) {
4414
4534
  if (!session) return null;
4415
4535
  return scanSessionForDefenseEvents(session);
4416
4536
  }
4537
+ function latestPreflight(session) {
4538
+ if (!session) return null;
4539
+ return scanSessionForPreflight(session);
4540
+ }
4417
4541
  function buildObsFixInstruction(f) {
4418
4542
  return buildFixInstruction(f);
4419
4543
  }
@@ -4426,6 +4550,7 @@ function ObservatoryPanel(props) {
4426
4550
  const qualityGate = latestQualityGate(session);
4427
4551
  const experienceBank = latestExperienceBank(session);
4428
4552
  const defenseEvents = latestDefenseEvents(session);
4553
+ const preflight = latestPreflight(session);
4429
4554
  const [open, setOpen] = React.useState(false);
4430
4555
  const [tab, setTab] = React.useState("live");
4431
4556
  const [expandedThreads, setExpandedThreads] = React.useState(/* @__PURE__ */ new Set());
@@ -5577,6 +5702,195 @@ function ObservatoryPanel(props) {
5577
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"))
5578
5703
  );
5579
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
+ };
5580
5894
  const renderLive = () => {
5581
5895
  const liveEntries = manifest.live || [];
5582
5896
  if (liveEntries.length === 0) {
@@ -5673,6 +5987,8 @@ function ObservatoryPanel(props) {
5673
5987
  return renderExperienceBank();
5674
5988
  case "f10":
5675
5989
  return renderDefenseEvents();
5990
+ case "f11":
5991
+ return renderPreflight();
5676
5992
  default:
5677
5993
  return null;
5678
5994
  }
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -446,11 +446,50 @@ export const EN: Record<string, string> = {
446
446
  '防线:{a0}': 'Defense: {a0}',
447
447
  没有匹配该类型的防御事件: 'No defense events of this type',
448
448
 
449
+ // ── F11 preflight ────────────────────────────────────────────────────────
450
+ 开跑预检: 'Preflight',
451
+ 待预检: 'Not run yet',
452
+ 未就绪: 'Not ready',
453
+ '复制 iterate_preflight 预检指令(含 JSON 参数)':
454
+ 'Copy the iterate_preflight readiness instruction (JSON arguments included)',
455
+ 运行预检: 'Run preflight',
456
+ 开跑预检清单: 'Preflight checklist',
457
+ 预检结果: 'Preflight result',
458
+ '开跑前先预检:确认验证命令、Git、工作区、陈旧门禁与中断断点都已就绪,避免开跑后才发现环境问题。':
459
+ '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.',
460
+ 复制预检指令: 'Copy preflight instruction',
461
+ 验证命令: 'Validation commands',
462
+ '已配置 {a0} 条验证命令': '{a0} validation command(s) configured',
463
+ '未配置 validation.commands —— 本轮无法证明修复,属阻断项':
464
+ 'validation.commands is not configured — the run cannot prove fixes (blocking)',
465
+ 'Git 仓库': 'Git repository',
466
+ '已检测到 Git 仓库': 'Git repository detected',
467
+ '不是 Git 仓库 —— changed-only 范围与 iterate_diff 需要它':
468
+ 'Not a git repository — changed-only scope and iterate_diff need one',
469
+ 工作区: 'Working tree',
470
+ '未提交改动:未知': 'Uncommitted changes: unknown',
471
+ '{a0} 个未提交/未跟踪文件 —— 建议先 commit 或 stash':
472
+ '{a0} uncommitted/untracked file(s) — commit or stash first',
473
+ 工作区干净: 'Working tree clean',
474
+ 陈旧门禁: 'Stale gate',
475
+ '磁盘上残留 FAIL 质量门禁 —— 开新轮前先清除':
476
+ 'A stale FAIL quality gate is persisted — clear it before a fresh run',
477
+ '质量门禁:{a0}': 'Quality gate: {a0}',
478
+ 无持久化质量门禁: 'No persisted quality gate',
479
+ 中断断点: 'Interrupted checkpoint',
480
+ '存在中断断点(已恢复 {a0} 次)—— 恢复或清除后再开跑':
481
+ 'An interrupted checkpoint is waiting (resumed {a0}x) — resume or clear it first',
482
+ 无中断断点: 'No interrupted checkpoint',
483
+ 配置验证命令: 'Configure validation',
484
+ 恢复断点: 'Resume checkpoint',
485
+ '阻断 {a0} · 警告 {a1}': 'Blocking {a0} · warnings {a1}',
486
+ '预检失败:{a0}': 'Preflight failed: {a0}',
487
+ '预检只读,不会修改配置或文件。': 'Preflight is read-only; it never modifies config or files.',
488
+
449
489
  // ── copy feedback ────────────────────────────────────────────────────────
450
490
  已复制: 'Copied',
451
491
  复制失败: 'Copy failed',
452
492
  }
453
-
454
493
  let current: Lang = 'zh'
455
494
  let storage: LangStorage | null = null
456
495
  const listeners = new Set<(lang: Lang) => void>()