iterate-plugin 3.11.0 → 3.13.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: 909db487a440ae131a6de59f253a9d84b240e5fbd55bce9ae83a9876b711a9a2 */
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,25 @@ 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
+ }
1346
+ function buildValidationApplyInstruction(commands) {
1347
+ const list = Array.isArray(commands) ? commands.filter((c) => typeof c === "string" && c.length > 0) : [];
1348
+ const payload = {
1349
+ operation: "write",
1350
+ updates: { validation: { commands: { default: list } } }
1351
+ };
1352
+ return `\u8BF7\u8C03\u7528 \`iterate_config\` \u628A\u68C0\u6D4B\u5230\u7684\u9A8C\u8BC1\u547D\u4EE4\u5199\u5165 \`validation.commands.default\`\uFF08\u4F1A\u5148\u5907\u4EFD\u914D\u7F6E\uFF09\uFF1A
1353
+
1354
+ \`\`\`json
1355
+ ${JSON.stringify(payload, null, 2)}
1356
+ \`\`\``;
1357
+ }
1283
1358
  function buildRollbackInstruction(id) {
1284
1359
  return `\u8BF7\u8C03\u7528 \`iterate_rollback\` \u56DE\u6EDA\u4EE5\u4E0B\u4FEE\u590D\uFF1A
1285
1360
 
@@ -1555,13 +1630,35 @@ function buildCompletionSummary(report, lang = "zh") {
1555
1630
  const stats = severityStats(report);
1556
1631
  const converged = conv.converged === true;
1557
1632
  const totalFindings = stats.critical + stats.high + stats.medium + stats.low;
1633
+ const stopped = typeof conv.stoppedReason === "string" ? conv.stoppedReason : typeof report.stoppedReason === "string" ? report.stoppedReason : "";
1634
+ 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
1635
  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}`;
1636
+ return `iterate review complete \xB7 ${rounds}/${total} rounds \xB7 ${totalFindings} findings \xB7 ${reason}`;
1561
1637
  }
1562
- const reason = converged ? "\u5DF2\u6536\u655B" : `\u5DF2\u8FBE\u6700\u5927\u8F6E\u6570 ${total}`;
1563
1638
  return `iterate \u8BC4\u5BA1\u5B8C\u6210 \xB7 ${rounds}/${total} \u8F6E \xB7 ${totalFindings} \u9879\u53D1\u73B0 \xB7 ${reason}`;
1564
1639
  }
1640
+ function completionReason(reason, lang, total) {
1641
+ const zh = {
1642
+ converged: "\u5DF2\u6536\u655B",
1643
+ max_rounds_reached: `\u5DF2\u8FBE\u6700\u5927\u8F6E\u6570 ${total}`,
1644
+ aborted_by_validation: "\u9A8C\u8BC1\u5931\u8D25\u540E\u56DE\u6EDA",
1645
+ aborted_by_config: "\u9A8C\u8BC1\u547D\u4EE4\u672A\u5728\u767D\u540D\u5355",
1646
+ inconclusive: "\u8F6E\u6B21\u7ED3\u8BBA\u4E0D\u660E",
1647
+ schema_invalid: "\u5BA1\u67E5\u8F93\u51FA schema \u6821\u9A8C\u5931\u8D25",
1648
+ no_usable_reviewer_output: "\u65E0\u53EF\u7528\u5BA1\u67E5\u8F93\u51FA"
1649
+ };
1650
+ const en = {
1651
+ converged: "converged",
1652
+ max_rounds_reached: `reached the round limit ${total}`,
1653
+ aborted_by_validation: "rolled back after a validation failure",
1654
+ aborted_by_config: "validation command not allow-listed",
1655
+ inconclusive: "inconclusive rounds",
1656
+ schema_invalid: "review output failed schema validation",
1657
+ no_usable_reviewer_output: "no usable review output"
1658
+ };
1659
+ const table = lang === "en" ? en : zh;
1660
+ return Object.hasOwn(table, reason) ? table[reason] : reason;
1661
+ }
1565
1662
  var CONFIG_EDIT_FIELDS = [
1566
1663
  {
1567
1664
  key: "goal",
@@ -2658,6 +2755,40 @@ var EN = {
2658
2755
  \u6E05\u9664\u7C7B\u578B\u7B5B\u9009: "Clear type filter",
2659
2756
  "\u9632\u7EBF\uFF1A{a0}": "Defense: {a0}",
2660
2757
  \u6CA1\u6709\u5339\u914D\u8BE5\u7C7B\u578B\u7684\u9632\u5FA1\u4E8B\u4EF6: "No defense events of this type",
2758
+ // ── F11 preflight ────────────────────────────────────────────────────────
2759
+ \u5F00\u8DD1\u9884\u68C0: "Preflight",
2760
+ \u5F85\u9884\u68C0: "Not run yet",
2761
+ \u672A\u5C31\u7EEA: "Not ready",
2762
+ "\u590D\u5236 iterate_preflight \u9884\u68C0\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09": "Copy the iterate_preflight readiness instruction (JSON arguments included)",
2763
+ \u8FD0\u884C\u9884\u68C0: "Run preflight",
2764
+ \u5F00\u8DD1\u9884\u68C0\u6E05\u5355: "Preflight checklist",
2765
+ \u9884\u68C0\u7ED3\u679C: "Preflight result",
2766
+ "\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.",
2767
+ \u590D\u5236\u9884\u68C0\u6307\u4EE4: "Copy preflight instruction",
2768
+ \u9A8C\u8BC1\u547D\u4EE4: "Validation commands",
2769
+ "\u5DF2\u914D\u7F6E {a0} \u6761\u9A8C\u8BC1\u547D\u4EE4": "{a0} validation command(s) configured",
2770
+ "\u672A\u914D\u7F6E validation.commands\uFF08\u963B\u65AD\u9879\uFF09\u2014\u2014 \u68C0\u6D4B\u5230 {a0} \u9879\u76EE\uFF0C\u5EFA\u8BAE\uFF1A{a1}": "validation.commands is not configured (blocking) \u2014 detected {a0} project; suggested: {a1}",
2771
+ "\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)",
2772
+ "Git \u4ED3\u5E93": "Git repository",
2773
+ "\u5DF2\u68C0\u6D4B\u5230 Git \u4ED3\u5E93": "Git repository detected",
2774
+ "\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",
2775
+ \u5DE5\u4F5C\u533A: "Working tree",
2776
+ "\u672A\u63D0\u4EA4\u6539\u52A8\uFF1A\u672A\u77E5": "Uncommitted changes: unknown",
2777
+ "{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",
2778
+ \u5DE5\u4F5C\u533A\u5E72\u51C0: "Working tree clean",
2779
+ \u9648\u65E7\u95E8\u7981: "Stale gate",
2780
+ "\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",
2781
+ "\u8D28\u91CF\u95E8\u7981\uFF1A{a0}": "Quality gate: {a0}",
2782
+ \u65E0\u6301\u4E45\u5316\u8D28\u91CF\u95E8\u7981: "No persisted quality gate",
2783
+ \u4E2D\u65AD\u65AD\u70B9: "Interrupted checkpoint",
2784
+ "\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",
2785
+ \u65E0\u4E2D\u65AD\u65AD\u70B9: "No interrupted checkpoint",
2786
+ \u914D\u7F6E\u9A8C\u8BC1\u547D\u4EE4: "Configure validation",
2787
+ \u5E94\u7528\u5EFA\u8BAE: "Apply suggestion",
2788
+ \u6062\u590D\u65AD\u70B9: "Resume checkpoint",
2789
+ "\u963B\u65AD {a0} \xB7 \u8B66\u544A {a1}": "Blocking {a0} \xB7 warnings {a1}",
2790
+ "\u9884\u68C0\u5931\u8D25\uFF1A{a0}": "Preflight failed: {a0}",
2791
+ "\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
2792
  // ── copy feedback ────────────────────────────────────────────────────────
2662
2793
  \u5DF2\u590D\u5236: "Copied",
2663
2794
  \u590D\u5236\u5931\u8D25: "Copy failed"
@@ -2907,6 +3038,7 @@ var ITERATE_CSS = `
2907
3038
  .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
3039
  .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
3040
  .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); }
3041
+ .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
3042
 
2911
3043
  /* v3.0: Experience bank hit highlight */
2912
3044
  .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 +4475,9 @@ var OBS_TABS = [
4343
4475
  // v3.0: Quality Command Center tabs
4344
4476
  { key: "f8", label: { zh: "\u8D28\u91CF\u95E8\u7981", en: "Quality gate" }, hotkey: "8" },
4345
4477
  { 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" }
4478
+ { key: "f10", label: { zh: "\u9632\u5FA1\u4E8B\u4EF6", en: "Defense events" }, hotkey: "0" },
4479
+ // v3.12: pre-run readiness checklist (validation/git/dirty-tree/stale-gate/checkpoint)
4480
+ { key: "f11", label: { zh: "\u5F00\u8DD1\u9884\u68C0", en: "Preflight" }, hotkey: "" }
4347
4481
  ];
4348
4482
  var OBS_HOTKEY_TABS = {
4349
4483
  1: "f1",
@@ -4414,6 +4548,10 @@ function latestDefenseEvents(session) {
4414
4548
  if (!session) return null;
4415
4549
  return scanSessionForDefenseEvents(session);
4416
4550
  }
4551
+ function latestPreflight(session) {
4552
+ if (!session) return null;
4553
+ return scanSessionForPreflight(session);
4554
+ }
4417
4555
  function buildObsFixInstruction(f) {
4418
4556
  return buildFixInstruction(f);
4419
4557
  }
@@ -4426,6 +4564,7 @@ function ObservatoryPanel(props) {
4426
4564
  const qualityGate = latestQualityGate(session);
4427
4565
  const experienceBank = latestExperienceBank(session);
4428
4566
  const defenseEvents = latestDefenseEvents(session);
4567
+ const preflight = latestPreflight(session);
4429
4568
  const [open, setOpen] = React.useState(false);
4430
4569
  const [tab, setTab] = React.useState("live");
4431
4570
  const [expandedThreads, setExpandedThreads] = React.useState(/* @__PURE__ */ new Set());
@@ -5577,6 +5716,212 @@ function ObservatoryPanel(props) {
5577
5716
  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
5717
  );
5579
5718
  };
5719
+ const renderPreflight = () => {
5720
+ const preflightInstruction = buildPreflightInstruction();
5721
+ const report = preflight;
5722
+ const hasReport = report !== null && Array.isArray(report.checks);
5723
+ const ready = report !== null && report.ready === true;
5724
+ const blocking = Number(report && report.blocking != null ? report.blocking : 0);
5725
+ const warnings = Number(report && report.warnings != null ? report.warnings : 0);
5726
+ const statusLabel = !hasReport ? t("\u5F85\u9884\u68C0") : ready ? t("\u5C31\u7EEA") : t("\u672A\u5C31\u7EEA");
5727
+ const statusTone = !hasReport ? "pending" : ready ? "pass" : "fail";
5728
+ const headerBar = React.createElement(
5729
+ "div",
5730
+ { className: "iterate-obs-bar", style: { marginBottom: 8 } },
5731
+ React.createElement("b", {}, t("\u5F00\u8DD1\u9884\u68C0")),
5732
+ React.createElement("span", { className: "iterate-gate", "data-status": statusTone }, statusLabel),
5733
+ React.createElement("button", {
5734
+ className: "iterate-btn",
5735
+ "data-primary": "",
5736
+ "data-copied": copiedKey === "preflight" ? "" : void 0,
5737
+ onClick: () => copyInstruction("preflight", preflightInstruction),
5738
+ title: t("\u590D\u5236 iterate_preflight \u9884\u68C0\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09")
5739
+ }, copiedKey === "preflight" ? t("\u5DF2\u590D\u5236") : t("\u8FD0\u884C\u9884\u68C0"))
5740
+ );
5741
+ if (!hasReport) {
5742
+ return React.createElement(
5743
+ "div",
5744
+ {},
5745
+ headerBar,
5746
+ React.createElement(
5747
+ "div",
5748
+ { className: "iterate-obs-block" },
5749
+ React.createElement(
5750
+ "div",
5751
+ { className: "iterate-obs-block-head" },
5752
+ React.createElement("span", {}, t("\u5F00\u8DD1\u9884\u68C0\u6E05\u5355")),
5753
+ React.createElement("span", { className: "iterate-obs-badge" }, "v3.12")
5754
+ ),
5755
+ React.createElement(
5756
+ "div",
5757
+ { className: "iterate-obs-block-body" },
5758
+ React.createElement(
5759
+ "div",
5760
+ { className: "iterate-obs-msg" },
5761
+ // Gap #4: separate "pulled and empty" from "never pulled".
5762
+ diskEmptyStateText(toolCalledInSession(session, "iterate_preflight"), t("\u9884\u68C0\u7ED3\u679C"), getLang())
5763
+ ),
5764
+ React.createElement(
5765
+ "div",
5766
+ { className: "iterate-obs-msg", style: { marginTop: 6 } },
5767
+ 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")
5768
+ ),
5769
+ React.createElement(
5770
+ "div",
5771
+ { className: "iterate-obs-bar", style: { marginTop: 8 } },
5772
+ React.createElement("button", {
5773
+ className: "iterate-btn",
5774
+ "data-copied": copiedKey === "preflight-instr" ? "" : void 0,
5775
+ onClick: () => copyInstruction("preflight-instr", preflightInstruction),
5776
+ title: t("\u590D\u5236\u9884\u68C0\u6307\u4EE4")
5777
+ }, copiedKey === "preflight-instr" ? t("\u5DF2\u590D\u5236") : t("\u590D\u5236\u9884\u68C0\u6307\u4EE4"))
5778
+ )
5779
+ )
5780
+ )
5781
+ );
5782
+ }
5783
+ if (report && report.ok === false) {
5784
+ return React.createElement(
5785
+ "div",
5786
+ {},
5787
+ headerBar,
5788
+ React.createElement(
5789
+ "div",
5790
+ { className: "iterate-obs-block" },
5791
+ React.createElement(
5792
+ "div",
5793
+ { className: "iterate-obs-block-head" },
5794
+ React.createElement("span", {}, t("\u5F00\u8DD1\u9884\u68C0\u6E05\u5355")),
5795
+ React.createElement("span", { className: "iterate-obs-badge" }, "v3.12")
5796
+ ),
5797
+ React.createElement(
5798
+ "div",
5799
+ { className: "iterate-obs-block-body" },
5800
+ React.createElement(
5801
+ "div",
5802
+ { className: "iterate-obs-msg" },
5803
+ tf("\u9884\u68C0\u5931\u8D25\uFF1A{a0}", { a0: String(report.error || "") })
5804
+ )
5805
+ )
5806
+ )
5807
+ );
5808
+ }
5809
+ const CHECK_META = {
5810
+ validation: {
5811
+ label: t("\u9A8C\u8BC1\u547D\u4EE4"),
5812
+ detail: (d) => {
5813
+ const n = Number(d.commandCount ?? 0);
5814
+ if (n > 0) return tf("\u5DF2\u914D\u7F6E {a0} \u6761\u9A8C\u8BC1\u547D\u4EE4", { a0: n });
5815
+ const suggested = Array.isArray(d.suggestedCommands) ? d.suggestedCommands.map((c) => String(c)).filter((c) => c.length > 0) : [];
5816
+ if (suggested.length > 0) {
5817
+ return tf("\u672A\u914D\u7F6E validation.commands\uFF08\u963B\u65AD\u9879\uFF09\u2014\u2014 \u68C0\u6D4B\u5230 {a0} \u9879\u76EE\uFF0C\u5EFA\u8BAE\uFF1A{a1}", {
5818
+ a0: String(d.ecosystem || ""),
5819
+ a1: suggested.join(" \xB7 ")
5820
+ });
5821
+ }
5822
+ return t("\u672A\u914D\u7F6E validation.commands \u2014\u2014 \u672C\u8F6E\u65E0\u6CD5\u8BC1\u660E\u4FEE\u590D\uFF0C\u5C5E\u963B\u65AD\u9879");
5823
+ }
5824
+ },
5825
+ git: {
5826
+ label: t("Git \u4ED3\u5E93"),
5827
+ 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")
5828
+ },
5829
+ "dirty-tree": {
5830
+ label: t("\u5DE5\u4F5C\u533A"),
5831
+ detail: (d) => {
5832
+ const n = d.changedFiles;
5833
+ if (n === null || n === void 0) return t("\u672A\u63D0\u4EA4\u6539\u52A8\uFF1A\u672A\u77E5");
5834
+ const num = Number(n);
5835
+ 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");
5836
+ }
5837
+ },
5838
+ "stale-gate": {
5839
+ label: t("\u9648\u65E7\u95E8\u7981"),
5840
+ detail: (d) => {
5841
+ const s = d.overallStatus;
5842
+ if (s === "fail") return t("\u78C1\u76D8\u4E0A\u6B8B\u7559 FAIL \u8D28\u91CF\u95E8\u7981 \u2014\u2014 \u5F00\u65B0\u8F6E\u524D\u5148\u6E05\u9664");
5843
+ return s ? tf("\u8D28\u91CF\u95E8\u7981\uFF1A{a0}", { a0: String(s) }) : t("\u65E0\u6301\u4E45\u5316\u8D28\u91CF\u95E8\u7981");
5844
+ }
5845
+ },
5846
+ "interrupted-checkpoint": {
5847
+ label: t("\u4E2D\u65AD\u65AD\u70B9"),
5848
+ detail: (d) => {
5849
+ if (d.interrupted === true) {
5850
+ const rc = Number(d.resumeCount ?? 0);
5851
+ 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 });
5852
+ }
5853
+ return t("\u65E0\u4E2D\u65AD\u65AD\u70B9");
5854
+ }
5855
+ }
5856
+ };
5857
+ const fixFor = (id, detail) => {
5858
+ switch (id) {
5859
+ case "validation": {
5860
+ const suggested = Array.isArray(detail.suggestedCommands) ? detail.suggestedCommands.map((c) => String(c)).filter((c) => c.length > 0) : [];
5861
+ if (suggested.length > 0) {
5862
+ return {
5863
+ key: "preflight-fix-validation",
5864
+ label: t("\u5E94\u7528\u5EFA\u8BAE"),
5865
+ instruction: buildValidationApplyInstruction(suggested)
5866
+ };
5867
+ }
5868
+ return { key: "preflight-fix-validation", label: t("\u914D\u7F6E\u9A8C\u8BC1\u547D\u4EE4"), instruction: buildConfigFieldInstruction("validation.commands") };
5869
+ }
5870
+ case "stale-gate":
5871
+ return { key: "preflight-fix-gate", label: t("\u6E05\u9664\u95E8\u7981"), instruction: buildQualityGateClearInstruction() };
5872
+ case "interrupted-checkpoint":
5873
+ return { key: "preflight-fix-checkpoint", label: t("\u6062\u590D\u65AD\u70B9"), instruction: buildCheckpointResumeInstruction() };
5874
+ default:
5875
+ return null;
5876
+ }
5877
+ };
5878
+ const rows = (report.checks || []).map((c, i) => {
5879
+ const id = String(c.id || "");
5880
+ const status = String(c.status || "warn");
5881
+ const meta = CHECK_META[id];
5882
+ const label = meta ? meta.label : id || "?";
5883
+ const detailText = meta ? meta.detail(c.detail || {}) : "";
5884
+ const fix = status !== "pass" ? fixFor(id, c.detail || {}) : null;
5885
+ return React.createElement(
5886
+ "div",
5887
+ { key: `pf-${i}`, className: "iterate-obs-row" },
5888
+ React.createElement("span", { className: "iterate-gate", "data-status": status }, status.toUpperCase()),
5889
+ React.createElement("b", {}, label),
5890
+ React.createElement("span", { className: "iterate-obs-msg" }, detailText),
5891
+ fix ? React.createElement("button", {
5892
+ className: "iterate-btn",
5893
+ "data-copied": copiedKey === fix.key ? "" : void 0,
5894
+ onClick: () => copyInstruction(fix.key, fix.instruction),
5895
+ title: fix.instruction
5896
+ }, copiedKey === fix.key ? t("\u5DF2\u590D\u5236") : fix.label) : null
5897
+ );
5898
+ });
5899
+ return React.createElement(
5900
+ "div",
5901
+ {},
5902
+ headerBar,
5903
+ React.createElement(
5904
+ "div",
5905
+ { className: "iterate-obs-block" },
5906
+ React.createElement(
5907
+ "div",
5908
+ { className: "iterate-obs-block-head" },
5909
+ React.createElement("span", {}, t("\u5F00\u8DD1\u9884\u68C0\u6E05\u5355")),
5910
+ React.createElement(
5911
+ "span",
5912
+ { className: "iterate-obs-head-meta" },
5913
+ tf("\u963B\u65AD {a0} \xB7 \u8B66\u544A {a1}", { a0: blocking, a1: warnings })
5914
+ )
5915
+ ),
5916
+ React.createElement(
5917
+ "div",
5918
+ { className: "iterate-obs-block-body" },
5919
+ React.createElement("div", { className: "iterate-obs-msg" }, t("\u9884\u68C0\u53EA\u8BFB\uFF0C\u4E0D\u4F1A\u4FEE\u6539\u914D\u7F6E\u6216\u6587\u4EF6\u3002")),
5920
+ ...rows
5921
+ )
5922
+ )
5923
+ );
5924
+ };
5580
5925
  const renderLive = () => {
5581
5926
  const liveEntries = manifest.live || [];
5582
5927
  if (liveEntries.length === 0) {
@@ -5673,6 +6018,8 @@ function ObservatoryPanel(props) {
5673
6018
  return renderExperienceBank();
5674
6019
  case "f10":
5675
6020
  return renderDefenseEvents();
6021
+ case "f11":
6022
+ return renderPreflight();
5676
6023
  default:
5677
6024
  return null;
5678
6025
  }
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,45 @@ 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
+
1957
+ /**
1958
+ * `iterate_config` write instruction that installs the validation commands the
1959
+ * preflight panel detected. Copies the exact command set the user saw, so the
1960
+ * model applies it deterministically instead of re-detecting (and drifting).
1961
+ *
1962
+ * @param {string[]} commands
1963
+ * @returns {string}
1964
+ */
1965
+ export function buildValidationApplyInstruction(commands) {
1966
+ const list = Array.isArray(commands)
1967
+ ? commands.filter((c) => typeof c === 'string' && c.length > 0)
1968
+ : []
1969
+ const payload = {
1970
+ operation: 'write',
1971
+ updates: { validation: { commands: { default: list } } },
1972
+ }
1973
+ return (
1974
+ '请调用 `iterate_config` 把检测到的验证命令写入 `validation.commands.default`(会先备份配置):\n\n' +
1975
+ '```json\n' +
1976
+ `${JSON.stringify(payload, null, 2)}\n` +
1977
+ '```'
1978
+ )
1979
+ }
1980
+
1860
1981
  /**
1861
1982
  * `iterate_rollback` payload for one applied fix id.
1862
1983
  *
@@ -2310,14 +2431,63 @@ export function buildCompletionSummary(report, lang = 'zh') {
2310
2431
  const stats = severityStats(report)
2311
2432
  const converged = conv.converged === true
2312
2433
  const totalFindings = stats.critical + stats.high + stats.medium + stats.low
2434
+ // The persisted `stoppedReason` is the authoritative "why" — a validation
2435
+ // abort or a schema failure must NOT be reported as "reached the round
2436
+ // limit". A normalized report nests it under `convergence.stoppedReason`
2437
+ // (normalizeReport); a raw harness report may carry it at the top level.
2438
+ // Fall back to the convergence flag only when no reason was stored (older
2439
+ // transcripts predate the field).
2440
+ const stopped = typeof conv.stoppedReason === 'string'
2441
+ ? conv.stoppedReason
2442
+ : typeof report.stoppedReason === 'string'
2443
+ ? report.stoppedReason
2444
+ : ''
2445
+ const reason = stopped
2446
+ ? completionReason(stopped, lang, total)
2447
+ : converged
2448
+ ? (lang === 'en' ? 'converged' : '已收敛')
2449
+ : (lang === 'en' ? `reached the round limit ${total}` : `已达最大轮数 ${total}`)
2313
2450
  if (lang === 'en') {
2314
- const reason = converged ? 'converged' : `reached the round limit ${total}`
2315
2451
  return `iterate review complete \u00b7 ${rounds}/${total} rounds \u00b7 ${totalFindings} findings \u00b7 ${reason}`
2316
2452
  }
2317
- const reason = converged ? '已收敛' : `已达最大轮数 ${total}`
2318
2453
  return `iterate 评审完成 · ${rounds}/${total} 轮 · ${totalFindings} 项发现 · ${reason}`
2319
2454
  }
2320
2455
 
2456
+ /**
2457
+ * Compact "why the run ended" phrase for the completion summary.
2458
+ * Mirrors `stoppedReasonLabel` but drops the "已结束 · " prefix so it reads
2459
+ * inside the one-line summary. Unknown reasons degrade to the raw value.
2460
+ *
2461
+ * @param {string} reason
2462
+ * @param {string} lang
2463
+ * @param {number} total
2464
+ * @returns {string}
2465
+ */
2466
+ function completionReason(reason, lang, total) {
2467
+ const zh = {
2468
+ converged: '已收敛',
2469
+ max_rounds_reached: `已达最大轮数 ${total}`,
2470
+ aborted_by_validation: '验证失败后回滚',
2471
+ aborted_by_config: '验证命令未在白名单',
2472
+ inconclusive: '轮次结论不明',
2473
+ schema_invalid: '审查输出 schema 校验失败',
2474
+ no_usable_reviewer_output: '无可用审查输出',
2475
+ }
2476
+ const en = {
2477
+ converged: 'converged',
2478
+ max_rounds_reached: `reached the round limit ${total}`,
2479
+ aborted_by_validation: 'rolled back after a validation failure',
2480
+ aborted_by_config: 'validation command not allow-listed',
2481
+ inconclusive: 'inconclusive rounds',
2482
+ schema_invalid: 'review output failed schema validation',
2483
+ no_usable_reviewer_output: 'no usable review output',
2484
+ }
2485
+ const table = lang === 'en' ? en : zh
2486
+ // Object.hasOwn, NOT `table[reason]`: a hand-edited `__proto__` reason must
2487
+ // not resolve through the prototype chain to a non-string.
2488
+ return Object.hasOwn(table, reason) ? table[reason] : reason
2489
+ }
2490
+
2321
2491
  // ─── Config edit guidance ───────────────────────────────────────────────────
2322
2492
 
2323
2493
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "3.11.0",
3
+ "version": "3.13.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",
@@ -54,6 +54,7 @@
54
54
  "0.2.1-rc.2": "compatible",
55
55
  "0.2.1-rc.3": "compatible",
56
56
  "0.2.1-rc.4": "compatible",
57
+ "0.2.1-rc.5": "compatible",
57
58
  "0.2.2-alpha.2": "compatible"
58
59
  }
59
60
  },
@@ -96,9 +97,9 @@
96
97
  },
97
98
  "peerDependencies": {
98
99
  "@deepseek-ai/cordis": "^4.0.5-alpha.2 || ~4.0.6-alpha.1",
99
- "@deepseek-ai/dsh-session": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.2-alpha.2",
100
- "@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.2-alpha.2",
101
- "@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.2-alpha.2"
100
+ "@deepseek-ai/dsh-session": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2",
101
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2",
102
+ "@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2"
102
103
  },
103
104
  "peerDependenciesMeta": {
104
105
  "@deepseek-ai/cordis": {