@xdevops/issue-auto-finish 1.0.80 → 1.0.82

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.
@@ -528,1999 +528,1999 @@ var IssueState = /* @__PURE__ */ ((IssueState2) => {
528
528
  return IssueState2;
529
529
  })(IssueState || {});
530
530
 
531
- // src/tracker/IssueTracker.ts
532
- var logger5 = logger.child("IssueTracker");
533
- var STATE_MIGRATION_MAP = {
534
- analyzing: { state: "phase_running", currentPhase: "analysis" },
535
- analysis_done: { state: "phase_done", currentPhase: "analysis" },
536
- designing: { state: "phase_running", currentPhase: "design" },
537
- design_done: { state: "phase_done", currentPhase: "design" },
538
- implementing: { state: "phase_running", currentPhase: "implement" },
539
- implement_done: { state: "phase_done", currentPhase: "implement" },
540
- planning: { state: "phase_running", currentPhase: "plan" },
541
- plan_done: { state: "phase_done", currentPhase: "plan" },
542
- building: { state: "phase_running", currentPhase: "build" },
543
- build_done: { state: "phase_done", currentPhase: "build" },
544
- verifying: { state: "phase_running", currentPhase: "verify" },
545
- // gate 硬编码状态 通用 gate 状态
546
- waiting_for_review: { state: "phase_waiting", currentPhase: "review" },
547
- review_approved: { state: "phase_approved", currentPhase: "review" }
548
- };
549
- var IssueTracker = class extends BaseTracker {
550
- lifecycleManagers;
551
- tenantId;
552
- constructor(dataDir, lifecycleManagers, tenantId) {
553
- const tid = tenantId ?? "default";
554
- const filename = tid === "default" ? "tracker.json" : `tracker-${tid}.json`;
555
- super(dataDir, filename, "issues", "tracker");
556
- this.lifecycleManagers = lifecycleManagers;
557
- this.tenantId = tid;
558
- this.migrateRecords();
531
+ // src/lifecycle/ActionLifecycleManager.ts
532
+ var ActionLifecycleManager = class {
533
+ /** IssueState ActionState */
534
+ stateToAction;
535
+ /** "action:status" IssueState */
536
+ actionToState;
537
+ /** phase name → { startState, doneState, approvedState? } */
538
+ phaseStatesMap;
539
+ /** Ordered phase indices by IssueState for determineResumePhaseIndex */
540
+ def;
541
+ constructor(def) {
542
+ this.def = def;
543
+ this.stateToAction = /* @__PURE__ */ new Map();
544
+ this.actionToState = /* @__PURE__ */ new Map();
545
+ this.phaseStatesMap = /* @__PURE__ */ new Map();
546
+ this.buildMappings(def);
559
547
  }
560
- /**
561
- * 迁移旧格式记录到新格式:
562
- * 1. 旧阶段专属状态 PhaseRunning/PhaseDone + currentPhase
563
- * 2. failedAtState 同步迁移
564
- * 3. 为缺少 demandSpec 的旧记录回填
565
- *
566
- * 幂等:已迁移的记录不会重复处理。
567
- */
568
- migrateRecords() {
569
- let migrated = 0;
570
- for (const record of this.getAllRecords()) {
571
- const raw = record;
572
- const stateStr = raw.state;
573
- const migration = STATE_MIGRATION_MAP[stateStr];
574
- if (migration) {
575
- raw.state = migration.state;
576
- raw.currentPhase = migration.currentPhase;
577
- migrated++;
578
- }
579
- const failedAtStr = raw.failedAtState;
580
- if (failedAtStr) {
581
- const failedMigration = STATE_MIGRATION_MAP[failedAtStr];
582
- if (failedMigration) {
583
- raw.failedAtState = failedMigration.state;
584
- if (!raw.currentPhase) {
585
- raw.currentPhase = failedMigration.currentPhase;
586
- }
548
+ buildMappings(def) {
549
+ this.addMapping("pending" /* Pending */, "init", "idle");
550
+ this.addMapping("skipped" /* Skipped */, "init", "skipped");
551
+ this.addMapping("branch_created" /* BranchCreated */, "init", "ready");
552
+ this.addMapping("failed" /* Failed */, "init", "failed");
553
+ this.addMapping("deployed" /* Deployed */, "init", "done");
554
+ this.addMapping("resolving_conflict" /* ResolvingConflict */, "conflict", "running");
555
+ for (const spec of def.phases) {
556
+ this.phaseStatesMap.set(spec.name, {
557
+ startState: spec.startState,
558
+ doneState: spec.doneState,
559
+ approvedState: spec.approvedState
560
+ });
561
+ if (spec.kind === "ai") {
562
+ if (spec.startState !== "phase_running" /* PhaseRunning */) {
563
+ this.addMapping(spec.startState, spec.name, "running");
564
+ }
565
+ if (spec.doneState === "completed" /* Completed */) {
566
+ this.addMapping(spec.doneState, spec.name, "done");
567
+ } else if (spec.doneState !== "phase_done" /* PhaseDone */) {
568
+ this.addMapping(spec.doneState, spec.name, "ready");
569
+ }
570
+ } else if (spec.kind === "gate") {
571
+ if (spec.startState !== "phase_waiting" /* PhaseWaiting */) {
572
+ this.addMapping(spec.startState, spec.name, "waiting");
573
+ }
574
+ if (spec.approvedState && spec.approvedState !== "phase_approved" /* PhaseApproved */) {
575
+ this.addMapping(spec.approvedState, spec.name, "ready");
587
576
  }
588
577
  }
589
- if (!raw.demandSpec && raw.issueIid) {
590
- raw.demandSpec = {
591
- demandId: `gf-${raw.issueIid}`,
592
- sourceRef: {
593
- source: "gongfeng-issue",
594
- externalId: String(raw.issueId ?? raw.issueIid),
595
- displayId: String(raw.issueIid)
596
- },
597
- title: raw.issueTitle || "",
598
- description: "",
599
- createdAt: raw.createdAt || (/* @__PURE__ */ new Date()).toISOString()
600
- };
601
- }
602
- if (raw.issueId !== void 0 || raw.issueIid !== void 0 || raw.issueTitle !== void 0) {
603
- delete raw.issueId;
604
- delete raw.issueIid;
605
- delete raw.issueTitle;
606
- migrated++;
607
- }
608
- }
609
- if (migrated > 0) {
610
- this.save();
611
- logger5.info("Migrated tracker records", { migrated });
612
578
  }
613
579
  }
614
- lifecycleFor(record) {
615
- if (record.pipelineMode) {
616
- const lm = this.lifecycleManagers.get(record.pipelineMode);
617
- if (lm) return lm;
580
+ addMapping(state, action, status) {
581
+ if (!this.stateToAction.has(state)) {
582
+ this.stateToAction.set(state, { action, status });
583
+ }
584
+ const key = `${action}:${status}`;
585
+ if (!this.actionToState.has(key)) {
586
+ this.actionToState.set(key, state);
618
587
  }
619
- return this.lifecycleManagers.get("plan-mode") ?? this.lifecycleManagers.values().next().value;
620
- }
621
- key(issueIid) {
622
- return String(issueIid);
623
- }
624
- get(issueIid) {
625
- return this.getByKey(this.key(issueIid));
626
- }
627
- create(record) {
628
- const now = (/* @__PURE__ */ new Date()).toISOString();
629
- const full = {
630
- ...record,
631
- attempts: 0,
632
- createdAt: now,
633
- updatedAt: now
634
- };
635
- this.setRecord(this.key(getIid(full)), full);
636
- this.save();
637
- logger5.info("Issue tracked", { issueIid: getIid(full), state: record.state });
638
- eventBus.emitTyped("issue:created", full);
639
- return full;
640
588
  }
641
- updateState(issueIid, state, extra) {
642
- const record = this.collection[this.key(issueIid)];
643
- if (!record) {
644
- throw new IssueNotFoundError(issueIid);
589
+ // ─── Query API ───
590
+ /**
591
+ * IssueState 解析为语义化的 ActionState。
592
+ *
593
+ * 对于通用状态 PhaseRunning/PhaseDone,需额外传入 currentPhase 来区分具体阶段。
594
+ */
595
+ resolve(state, currentPhase) {
596
+ if (state === "phase_running" /* PhaseRunning */ && currentPhase) {
597
+ return { action: currentPhase, status: "running" };
645
598
  }
646
- record.state = state;
647
- record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
648
- if (state === "completed" /* Completed */) {
649
- record.lastError = void 0;
650
- record.failedAtState = void 0;
599
+ if (state === "phase_done" /* PhaseDone */ && currentPhase) {
600
+ return { action: currentPhase, status: "ready" };
651
601
  }
652
- if (extra) {
653
- Object.assign(record, extra);
602
+ if (state === "phase_waiting" /* PhaseWaiting */ && currentPhase) {
603
+ return { action: currentPhase, status: "waiting" };
654
604
  }
655
- this.save();
656
- logger5.info("Issue state updated", { issueIid, state });
657
- eventBus.emitTyped("issue:stateChanged", { issueIid, state, record });
658
- }
659
- markFailed(issueIid, error, failedAtState, isRetryable) {
660
- const record = this.collection[this.key(issueIid)];
661
- if (!record) return;
662
- record.state = "failed" /* Failed */;
663
- record.lastError = error;
664
- record.failedAtState = failedAtState;
665
- record.lastErrorRetryable = isRetryable;
666
- record.attempts += 1;
667
- record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
668
- this.save();
669
- logger5.warn("Issue marked as failed", { issueIid, error, failedAtState, attempts: record.attempts, isRetryable });
670
- eventBus.emitTyped("issue:failed", { issueIid, error, failedAtState, record });
671
- }
672
- isProcessing(issueIid) {
673
- const record = this.get(issueIid);
674
- if (!record) return false;
675
- return !this.lifecycleFor(record).isTerminal(record.state);
676
- }
677
- isCompleted(issueIid) {
678
- const record = this.get(issueIid);
679
- return record?.state === "completed" /* Completed */;
605
+ if (state === "phase_approved" /* PhaseApproved */ && currentPhase) {
606
+ return { action: currentPhase, status: "ready" };
607
+ }
608
+ const mapped = this.stateToAction.get(state);
609
+ if (mapped) return mapped;
610
+ return { action: "init", status: "idle" };
680
611
  }
681
- canRetry(issueIid, maxRetries) {
682
- const record = this.get(issueIid);
683
- if (!record || record.state !== "failed" /* Failed */) return false;
684
- return record.attempts < maxRetries;
612
+ /**
613
+ * action + status 反向映射为 IssueState。
614
+ */
615
+ toIssueState(action, status) {
616
+ return this.actionToState.get(`${action}:${status}`);
685
617
  }
686
- getRetryState(issueIid) {
687
- const record = this.get(issueIid);
688
- return record?.failedAtState;
618
+ // ─── Classification predicates (替代 3 个 Set 常量 + getDrivableIssues) ───
619
+ /**
620
+ * 终态:done | failed | skipped
621
+ */
622
+ isTerminal(state) {
623
+ const as = this.resolve(state);
624
+ return as.status === "done" || as.status === "failed" || as.status === "skipped";
689
625
  }
690
- isStalled(issueIid, thresholdMs = 5 * 60 * 1e3) {
691
- const record = this.get(issueIid);
692
- if (!record) return false;
693
- if (this.lifecycleFor(record).isBlocked(record.state)) return false;
694
- if (!this.isProcessing(issueIid)) return false;
695
- const elapsed = Date.now() - new Date(record.updatedAt).getTime();
696
- return elapsed > thresholdMs;
626
+ /**
627
+ * 进行中:running (包含通用 PhaseRunning)
628
+ */
629
+ isInProgress(state) {
630
+ if (state === "phase_running" /* PhaseRunning */) return true;
631
+ return this.resolve(state).status === "running";
697
632
  }
698
- getDrivableIssues(maxRetries, stalledThresholdMs) {
699
- return this.getAllRecords().filter((record) => {
700
- const lm = this.lifecycleFor(record);
701
- if (lm.isDrivable(record.state, record.attempts, maxRetries, record.lastErrorRetryable)) return true;
702
- if (this.isStalled(getIid(record), stalledThresholdMs)) return true;
703
- return false;
704
- });
633
+ /**
634
+ * 阻塞中:waiting
635
+ */
636
+ isBlocked(state) {
637
+ if (state === "phase_waiting" /* PhaseWaiting */) return true;
638
+ return this.resolve(state).status === "waiting";
705
639
  }
706
- getAllActive() {
707
- return this.getAllRecords().filter(
708
- (r) => !this.lifecycleFor(r).isTerminal(r.state)
709
- );
640
+ /**
641
+ * 可驱动判断(集中化 getDrivableIssues 的过滤逻辑)。
642
+ *
643
+ * - idle → 可驱动 (Pending)
644
+ * - ready → 可驱动 (BranchCreated, PhaseDone, PhaseApproved)
645
+ * - failed && attempts < maxRetries → 可驱动
646
+ * - waiting → 不可驱动 (PhaseWaiting)
647
+ * - running → 不可驱动(stalled 由外部叠加)
648
+ * - done/skipped → 不可驱动
649
+ */
650
+ isDrivable(state, attempts, maxRetries, lastErrorRetryable) {
651
+ if (state === "phase_done" /* PhaseDone */) return true;
652
+ if (state === "phase_approved" /* PhaseApproved */) return true;
653
+ if (state === "phase_running" /* PhaseRunning */) return false;
654
+ if (state === "phase_waiting" /* PhaseWaiting */) return false;
655
+ const as = this.resolve(state);
656
+ switch (as.status) {
657
+ case "idle":
658
+ case "ready":
659
+ return true;
660
+ case "failed":
661
+ if (lastErrorRetryable === false) return false;
662
+ return attempts < maxRetries;
663
+ case "waiting":
664
+ case "running":
665
+ case "done":
666
+ case "skipped":
667
+ return false;
668
+ }
710
669
  }
711
- getAll() {
712
- return this.getAllRecords();
670
+ // ─── Phase navigation (替代 determineStartIndex + getPhasePreState) ───
671
+ /**
672
+ * 确定从哪个阶段索引恢复执行(替代 PipelineOrchestrator.determineStartIndex)。
673
+ *
674
+ * 从后向前扫描 phases,匹配 currentState 或 failedAtState。
675
+ * 支持通用状态 PhaseRunning/PhaseDone + currentPhase 的组合。
676
+ */
677
+ determineResumePhaseIndex(currentState, failedAtState, currentPhase) {
678
+ const target = failedAtState || currentState;
679
+ const phases = this.def.phases;
680
+ if ((target === "phase_running" /* PhaseRunning */ || target === "phase_done" /* PhaseDone */) && currentPhase) {
681
+ const idx = phases.findIndex((p) => p.name === currentPhase);
682
+ if (idx >= 0) {
683
+ return target === "phase_done" /* PhaseDone */ ? idx + 1 : idx;
684
+ }
685
+ }
686
+ if ((target === "phase_waiting" /* PhaseWaiting */ || target === "phase_approved" /* PhaseApproved */) && currentPhase) {
687
+ const idx = phases.findIndex((p) => p.name === currentPhase);
688
+ if (idx >= 0) {
689
+ if (target === "phase_approved" /* PhaseApproved */) {
690
+ const spec = phases[idx];
691
+ return spec.kind === "ai" && spec.approvedState ? idx : idx + 1;
692
+ }
693
+ return idx;
694
+ }
695
+ }
696
+ for (let i = phases.length - 1; i >= 0; i--) {
697
+ const spec = phases[i];
698
+ if (spec.kind === "gate" && spec.approvedState === target) {
699
+ return i + 1;
700
+ }
701
+ if (spec.startState === target || spec.doneState === target) {
702
+ return spec.doneState === target ? i + 1 : i;
703
+ }
704
+ }
705
+ return 0;
713
706
  }
714
- startSkipped(issueIid) {
715
- const record = this.collection[this.key(issueIid)];
716
- if (!record || record.state !== "skipped" /* Skipped */) return false;
717
- record.state = "pending" /* Pending */;
718
- record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
719
- this.save();
720
- logger5.info("Skipped issue started", { issueIid });
721
- eventBus.emitTyped("issue:stateChanged", { issueIid, state: "pending" /* Pending */, record });
722
- return true;
707
+ /**
708
+ * 获取某个 phase 的前驱状态(即重置到该 phase 需要设置的状态)。
709
+ * 第一个 phase 的前驱是 BranchCreated;后续 phase 的前驱是上一个 phase approvedState 或 doneState。
710
+ */
711
+ getPhasePreState(phaseName) {
712
+ const phases = this.def.phases;
713
+ const idx = phases.findIndex((p) => p.name === phaseName);
714
+ if (idx < 0) return void 0;
715
+ if (idx === 0) return "branch_created" /* BranchCreated */;
716
+ const prev = phases[idx - 1];
717
+ return prev.approvedState ?? prev.doneState;
723
718
  }
724
- resetFull(issueIid) {
725
- const record = this.collection[this.key(issueIid)];
726
- if (!record) return false;
727
- record.state = "pending" /* Pending */;
728
- record.currentPhase = void 0;
729
- record.attempts = 0;
730
- record.failedAtState = void 0;
731
- record.lastError = void 0;
732
- record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
733
- this.save();
734
- logger5.info("Issue fully reset", { issueIid });
735
- eventBus.emitTyped("issue:restarted", { issueIid, record });
736
- return true;
719
+ /**
720
+ * 获取某个 phase 的状态三元组。
721
+ */
722
+ getPhaseStates(phaseName) {
723
+ return this.phaseStatesMap.get(phaseName);
737
724
  }
738
- resetToPhase(issueIid, phase, def) {
739
- const record = this.collection[this.key(issueIid)];
740
- if (!record) return false;
741
- const lm = this.lifecycleManagers.get(def.mode) ?? this.lifecycleFor(record);
742
- const targetState = lm.getPhasePreState(phase);
743
- if (!targetState) return false;
744
- record.state = targetState;
745
- if (targetState === "phase_running" /* PhaseRunning */ || targetState === "phase_done" /* PhaseDone */ || targetState === "phase_waiting" /* PhaseWaiting */ || targetState === "phase_approved" /* PhaseApproved */) {
746
- const phases = def.phases;
747
- const idx = phases.findIndex((p) => p.name === phase);
748
- if (idx > 0) {
749
- record.currentPhase = phases[idx - 1].name;
750
- }
725
+ // ─── Display helpers (替代 collectStateLabels + derivePhaseStatuses) ───
726
+ /**
727
+ * 解析单条状态的展示标签。
728
+ *
729
+ * 对通用状态 PhaseRunning/PhaseDone,需传入 currentPhase 以生成具体标签(如"分析中");
730
+ * 缺少 currentPhase 时回退到泛化标签(如"阶段执行中")。
731
+ */
732
+ resolveLabel(state, currentPhase) {
733
+ if ((state === "phase_running" /* PhaseRunning */ || state === "phase_done" /* PhaseDone */) && currentPhase) {
734
+ const phaseLabel = t(`pipeline.phase.${currentPhase}`);
735
+ return state === "phase_running" /* PhaseRunning */ ? t("state.phaseDoing", { label: phaseLabel }) : t("state.phaseDone", { label: phaseLabel });
751
736
  }
752
- record.failedAtState = void 0;
753
- record.lastError = void 0;
754
- record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
755
- this.save();
756
- logger5.info("Issue reset to phase", { issueIid, phase, state: targetState });
757
- eventBus.emitTyped("issue:retryFromPhase", { issueIid, phase, record });
758
- return true;
759
- }
760
- resetForRetry(issueIid) {
761
- const record = this.collection[this.key(issueIid)];
762
- if (!record || record.state !== "failed" /* Failed */) return false;
763
- const restoreState = record.failedAtState ?? "pending" /* Pending */;
764
- record.state = restoreState;
765
- record.lastError = void 0;
766
- record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
767
- this.save();
768
- logger5.info("Issue reset for retry", { issueIid, restoreState });
769
- eventBus.emitTyped("issue:resetForRetry", { issueIid, restoreState, record });
770
- return true;
737
+ if ((state === "phase_waiting" /* PhaseWaiting */ || state === "phase_approved" /* PhaseApproved */) && currentPhase) {
738
+ const phaseLabel = t(`pipeline.phase.${currentPhase}`);
739
+ return state === "phase_waiting" /* PhaseWaiting */ ? t("state.phaseWaiting", { label: phaseLabel }) : t("state.phaseApproved", { label: phaseLabel });
740
+ }
741
+ const labels = this.collectStateLabels();
742
+ return labels.get(state) ?? state;
771
743
  }
772
- delete(issueIid) {
773
- const key = this.key(issueIid);
774
- const record = this.collection[key];
775
- if (!record) return false;
776
- delete this.collection[key];
777
- this.save();
778
- logger5.info("Issue deleted from tracker", { issueIid });
779
- eventBus.emitTyped("issue:deleted", { issueIid, record });
780
- return true;
744
+ /**
745
+ * 收集所有状态及其展示标签。
746
+ * 为通用状态 PhaseRunning/PhaseDone 生成每个阶段的复合 key 条目。
747
+ */
748
+ collectStateLabels() {
749
+ const labels = /* @__PURE__ */ new Map();
750
+ labels.set("pending" /* Pending */, t("state.pending"));
751
+ labels.set("skipped" /* Skipped */, t("state.skipped"));
752
+ labels.set("branch_created" /* BranchCreated */, t("state.branchCreated"));
753
+ for (const phase of this.def.phases) {
754
+ const phaseLabel = t(`pipeline.phase.${phase.name}`);
755
+ if (phase.startState === "phase_running" /* PhaseRunning */) {
756
+ labels.set(`phase_running:${phase.name}`, t("state.phaseDoing", { label: phaseLabel }));
757
+ } else if (phase.startState === "phase_waiting" /* PhaseWaiting */) {
758
+ labels.set(`phase_waiting:${phase.name}`, t("state.phaseWaiting", { label: phaseLabel }));
759
+ } else {
760
+ labels.set(phase.startState, t("state.phaseDoing", { label: phaseLabel }));
761
+ }
762
+ if (phase.doneState === "phase_done" /* PhaseDone */) {
763
+ labels.set(`phase_done:${phase.name}`, t("state.phaseDone", { label: phaseLabel }));
764
+ } else if (phase.doneState === "phase_approved" /* PhaseApproved */) {
765
+ labels.set(`phase_approved:${phase.name}`, t("state.phaseApproved", { label: phaseLabel }));
766
+ } else if (phase.doneState !== "completed" /* Completed */) {
767
+ labels.set(phase.doneState, t("state.phaseDone", { label: phaseLabel }));
768
+ }
769
+ if (phase.approvedState && phase.approvedState !== "phase_approved" /* PhaseApproved */ && phase.approvedState !== phase.doneState) {
770
+ labels.set(phase.approvedState, t("state.phaseApproved", { label: phaseLabel }));
771
+ }
772
+ }
773
+ labels.set("completed" /* Completed */, t("state.completed"));
774
+ labels.set("failed" /* Failed */, t("state.failed"));
775
+ labels.set("resolving_conflict" /* ResolvingConflict */, t("state.resolvingConflict"));
776
+ return labels;
781
777
  }
782
- recoverInterruptedIssues() {
783
- let count = 0;
784
- for (const record of this.getAllRecords()) {
785
- const lm = this.lifecycleFor(record);
786
- if (lm.isInProgress(record.state)) {
787
- const iid = getIid(record);
788
- logger5.warn("Recovering interrupted issue", {
789
- issueIid: iid,
790
- state: record.state
791
- });
792
- record.failedAtState = record.state;
793
- record.state = "failed" /* Failed */;
794
- record.lastError = "Interrupted by service restart";
795
- record.attempts += 1;
796
- record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
797
- count++;
798
- eventBus.emitTyped("issue:failed", {
799
- issueIid: iid,
800
- error: "Interrupted by service restart",
801
- failedAtState: record.failedAtState,
802
- record
803
- });
778
+ /**
779
+ * 根据当前 state,推导每个 phase 的进度状态。
780
+ * 支持通用状态 PhaseRunning/PhaseDone + currentPhase。
781
+ */
782
+ derivePhaseStatuses(currentState, currentPhase) {
783
+ const result = {};
784
+ let passedCurrent = false;
785
+ if (currentState === "failed" /* Failed */ && currentPhase) {
786
+ for (const phase of this.def.phases) {
787
+ if (passedCurrent) {
788
+ result[phase.name] = "pending";
789
+ } else if (phase.name === currentPhase) {
790
+ result[phase.name] = "failed";
791
+ passedCurrent = true;
792
+ } else {
793
+ result[phase.name] = "completed";
794
+ }
795
+ }
796
+ return result;
797
+ }
798
+ if ((currentState === "phase_running" /* PhaseRunning */ || currentState === "phase_done" /* PhaseDone */) && currentPhase) {
799
+ for (const phase of this.def.phases) {
800
+ if (passedCurrent) {
801
+ result[phase.name] = "pending";
802
+ } else if (phase.name === currentPhase) {
803
+ result[phase.name] = currentState === "phase_running" /* PhaseRunning */ ? "in_progress" : "completed";
804
+ passedCurrent = currentState === "phase_running" /* PhaseRunning */;
805
+ if (currentState === "phase_done" /* PhaseDone */) passedCurrent = true;
806
+ } else {
807
+ result[phase.name] = "completed";
808
+ }
809
+ }
810
+ return result;
811
+ }
812
+ if ((currentState === "phase_waiting" /* PhaseWaiting */ || currentState === "phase_approved" /* PhaseApproved */) && currentPhase) {
813
+ for (const phase of this.def.phases) {
814
+ if (passedCurrent) {
815
+ result[phase.name] = "pending";
816
+ } else if (phase.name === currentPhase) {
817
+ const isGatedAi = phase.kind === "ai" && phase.approvedState;
818
+ result[phase.name] = currentState === "phase_waiting" /* PhaseWaiting */ || currentState === "phase_approved" /* PhaseApproved */ && isGatedAi ? "in_progress" : "completed";
819
+ passedCurrent = true;
820
+ } else {
821
+ result[phase.name] = "completed";
822
+ }
804
823
  }
824
+ return result;
805
825
  }
806
- if (count > 0) {
807
- this.save();
808
- logger5.info("Recovered interrupted issues", { count });
826
+ if (currentState === "completed" /* Completed */ || currentState === "resolving_conflict" /* ResolvingConflict */) {
827
+ for (const phase of this.def.phases) {
828
+ result[phase.name] = "completed";
829
+ }
830
+ return result;
809
831
  }
810
- return count;
811
- }
812
- /** 将所有 IssueRecord 投影为 ExecutableTask[] */
813
- toExecutableTasks() {
814
- return this.getAllRecords().map((record) => {
815
- const lm = this.lifecycleFor(record);
816
- return issueToExecutableTask(record, lm);
817
- });
818
- }
819
- };
820
-
821
- // src/persistence/PlanPersistence.ts
822
- import fs2 from "fs";
823
- import path2 from "path";
824
- var logger6 = logger.child("PlanPersistence");
825
- var PLAN_DIR = ".claude-plan";
826
- var PlanPersistence = class _PlanPersistence {
827
- workDir;
828
- issueIid;
829
- constructor(workDir, issueIid) {
830
- this.workDir = workDir;
831
- this.issueIid = issueIid;
832
- }
833
- get baseDir() {
834
- return this.workDir;
835
- }
836
- get planDir() {
837
- return path2.join(this.workDir, PLAN_DIR, `issue-${this.issueIid}`);
838
- }
839
- ensureDir() {
840
- if (!fs2.existsSync(this.planDir)) {
841
- fs2.mkdirSync(this.planDir, { recursive: true });
832
+ for (const phase of this.def.phases) {
833
+ if (passedCurrent) {
834
+ result[phase.name] = "pending";
835
+ continue;
836
+ }
837
+ if (phase.startState === currentState) {
838
+ result[phase.name] = "in_progress";
839
+ passedCurrent = true;
840
+ } else if (phase.doneState === currentState || phase.approvedState === currentState) {
841
+ result[phase.name] = "completed";
842
+ } else {
843
+ result[phase.name] = "pending";
844
+ }
842
845
  }
846
+ return result;
843
847
  }
844
- writeIssueMeta(meta) {
845
- this.ensureDir();
846
- const filePath = path2.join(this.planDir, "issue-meta.json");
847
- fs2.writeFileSync(filePath, JSON.stringify(meta, null, 2), "utf-8");
848
- logger6.info("Issue meta written");
848
+ // ─── Pipeline Protocol queries (P1) ───
849
+ /**
850
+ * 返回可被用户单独重试的阶段名列表。
851
+ * 优先使用 spec.retryable,未声明时默认 kind === 'ai'。
852
+ */
853
+ getRetryablePhases() {
854
+ return this.def.phases.filter((spec) => spec.retryable ?? spec.kind === "ai").map((spec) => spec.name);
849
855
  }
850
- writeProgress(data) {
851
- this.ensureDir();
852
- const filePath = path2.join(this.planDir, "progress.json");
853
- fs2.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
854
- logger6.debug("Progress written", { currentPhase: data.currentPhase });
856
+ /**
857
+ * 判断指定阶段是否可重试。
858
+ */
859
+ isRetryable(phaseName) {
860
+ const spec = this.def.phases.find((p) => p.name === phaseName);
861
+ if (!spec) return false;
862
+ return spec.retryable ?? spec.kind === "ai";
855
863
  }
856
- readProgress() {
857
- const filePath = path2.join(this.planDir, "progress.json");
858
- if (!fs2.existsSync(filePath)) return null;
859
- try {
860
- return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
861
- } catch {
862
- return null;
863
- }
864
+ /**
865
+ * 查找 gate 类型的阶段。
866
+ */
867
+ getGatePhase() {
868
+ return this.def.phases.find((p) => p.kind === "gate");
864
869
  }
865
- writeAnalysis(content) {
866
- this.ensureDir();
867
- fs2.writeFileSync(path2.join(this.planDir, "01-analysis.md"), content, "utf-8");
868
- logger6.info("Analysis document written");
870
+ /**
871
+ * 判断指定阶段完成后是否应启动预览服务器。
872
+ */
873
+ shouldDeployPreview(phaseName) {
874
+ const spec = this.def.phases.find((p) => p.name === phaseName);
875
+ return spec?.deploysPreview ?? false;
869
876
  }
870
- writeDesign(content) {
871
- this.ensureDir();
872
- fs2.writeFileSync(path2.join(this.planDir, "02-design.md"), content, "utf-8");
873
- logger6.info("Design document written");
877
+ /**
878
+ * 收集所有阶段的产物文件,扁平化为单一列表。
879
+ */
880
+ collectArtifacts() {
881
+ return this.def.phases.flatMap((spec) => spec.artifacts ?? []);
874
882
  }
875
- writeTodolist(content) {
876
- this.ensureDir();
877
- fs2.writeFileSync(path2.join(this.planDir, "03-todolist.md"), content, "utf-8");
878
- logger6.info("Todolist written");
883
+ /**
884
+ * 返回所有阶段的名称和标签(保持定义顺序)。
885
+ */
886
+ getPhaseDefs() {
887
+ return this.def.phases.map((p) => ({ name: p.name, label: p.label }));
879
888
  }
880
- writeVerifyReport(content, filename = "04-verify-report.md") {
881
- this.ensureDir();
882
- fs2.writeFileSync(path2.join(this.planDir, filename), content, "utf-8");
883
- logger6.info("Verify report written", { filename });
889
+ /**
890
+ * 返回所有 kind === 'ai' 的阶段名(可执行阶段)。
891
+ */
892
+ getExecutablePhaseNames() {
893
+ return this.def.phases.filter((spec) => spec.kind === "ai").map((spec) => spec.name);
884
894
  }
885
- getAllPlanFiles() {
886
- if (!fs2.existsSync(this.planDir)) return [];
887
- return fs2.readdirSync(this.planDir).map((f) => path2.join(PLAN_DIR, `issue-${this.issueIid}`, f));
895
+ };
896
+
897
+ // src/tracker/IssueTracker.ts
898
+ var logger5 = logger.child("IssueTracker");
899
+ var STATE_MIGRATION_MAP = {
900
+ analyzing: { state: "phase_running", currentPhase: "analysis" },
901
+ analysis_done: { state: "phase_done", currentPhase: "analysis" },
902
+ designing: { state: "phase_running", currentPhase: "design" },
903
+ design_done: { state: "phase_done", currentPhase: "design" },
904
+ implementing: { state: "phase_running", currentPhase: "implement" },
905
+ implement_done: { state: "phase_done", currentPhase: "implement" },
906
+ planning: { state: "phase_running", currentPhase: "plan" },
907
+ plan_done: { state: "phase_done", currentPhase: "plan" },
908
+ building: { state: "phase_running", currentPhase: "build" },
909
+ build_done: { state: "phase_done", currentPhase: "build" },
910
+ verifying: { state: "phase_running", currentPhase: "verify" },
911
+ // 旧 gate 硬编码状态 → 通用 gate 状态
912
+ waiting_for_review: { state: "phase_waiting", currentPhase: "review" },
913
+ review_approved: { state: "phase_approved", currentPhase: "review" }
914
+ };
915
+ var IssueTracker = class extends BaseTracker {
916
+ lifecycleManagers;
917
+ tenantId;
918
+ constructor(dataDir, lifecycleManagers, tenantId) {
919
+ const tid = tenantId ?? "default";
920
+ const filename = tid === "default" ? "tracker.json" : `tracker-${tid}.json`;
921
+ super(dataDir, filename, "issues", "tracker");
922
+ this.lifecycleManagers = lifecycleManagers;
923
+ this.tenantId = tid;
924
+ this.migrateRecords();
888
925
  }
889
- createInitialProgress(displayId, title, branchName, def) {
890
- const pending = { status: "pending" };
891
- if (def) {
892
- const phases = {};
893
- for (const spec of def.phases) {
894
- phases[spec.name] = { ...pending };
926
+ /**
927
+ * 迁移旧格式记录到新格式:
928
+ * 1. 旧阶段专属状态 → PhaseRunning/PhaseDone + currentPhase
929
+ * 2. failedAtState 同步迁移
930
+ * 3. 为缺少 demandSpec 的旧记录回填
931
+ *
932
+ * 幂等:已迁移的记录不会重复处理。
933
+ */
934
+ migrateRecords() {
935
+ let migrated = 0;
936
+ for (const record of this.getAllRecords()) {
937
+ const raw = record;
938
+ const stateStr = raw.state;
939
+ const migration = STATE_MIGRATION_MAP[stateStr];
940
+ if (migration) {
941
+ raw.state = migration.state;
942
+ raw.currentPhase = migration.currentPhase;
943
+ migrated++;
895
944
  }
896
- return {
897
- displayId,
898
- title,
899
- branchName,
900
- pipelineMode: def.mode,
901
- currentPhase: def.phases[0].name,
902
- phases
903
- };
904
- }
905
- return {
906
- displayId,
907
- title,
908
- branchName,
909
- currentPhase: "analysis",
910
- phases: {
911
- analysis: { ...pending },
912
- design: { ...pending },
913
- implement: { ...pending },
914
- verify: { ...pending }
945
+ const failedAtStr = raw.failedAtState;
946
+ if (failedAtStr) {
947
+ const failedMigration = STATE_MIGRATION_MAP[failedAtStr];
948
+ if (failedMigration) {
949
+ raw.failedAtState = failedMigration.state;
950
+ if (!raw.currentPhase) {
951
+ raw.currentPhase = failedMigration.currentPhase;
952
+ }
953
+ }
954
+ }
955
+ if (!raw.demandSpec && raw.issueIid) {
956
+ raw.demandSpec = {
957
+ demandId: `gf-${raw.issueIid}`,
958
+ sourceRef: {
959
+ source: "gongfeng-issue",
960
+ externalId: String(raw.issueId ?? raw.issueIid),
961
+ displayId: String(raw.issueIid)
962
+ },
963
+ title: raw.issueTitle || "",
964
+ description: "",
965
+ createdAt: raw.createdAt || (/* @__PURE__ */ new Date()).toISOString()
966
+ };
967
+ }
968
+ if (raw.issueId !== void 0 || raw.issueIid !== void 0 || raw.issueTitle !== void 0) {
969
+ delete raw.issueId;
970
+ delete raw.issueIid;
971
+ delete raw.issueTitle;
972
+ migrated++;
915
973
  }
916
- };
917
- }
918
- writePlan(content) {
919
- this.ensureDir();
920
- fs2.writeFileSync(path2.join(this.planDir, "01-plan.md"), content, "utf-8");
921
- logger6.info("Plan document written");
922
- }
923
- writeReviewFeedback(content) {
924
- this.ensureDir();
925
- const history = this.readReviewHistory();
926
- const round = {
927
- round: history.length + 1,
928
- feedback: content,
929
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
930
- };
931
- history.push(round);
932
- fs2.writeFileSync(
933
- path2.join(this.planDir, "review-history.json"),
934
- JSON.stringify(history, null, 2),
935
- "utf-8"
936
- );
937
- fs2.writeFileSync(
938
- path2.join(this.planDir, "review-feedback.md"),
939
- _PlanPersistence.renderReviewHistoryMarkdown(history),
940
- "utf-8"
941
- );
942
- logger6.info("Review feedback appended", { round: round.round });
943
- }
944
- readReviewFeedback() {
945
- const filePath = path2.join(this.planDir, "review-feedback.md");
946
- if (!fs2.existsSync(filePath)) return null;
947
- try {
948
- return fs2.readFileSync(filePath, "utf-8");
949
- } catch {
950
- return null;
951
974
  }
952
- }
953
- readReviewHistory() {
954
- const filePath = path2.join(this.planDir, "review-history.json");
955
- if (!fs2.existsSync(filePath)) return [];
956
- try {
957
- const data = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
958
- return Array.isArray(data) ? data : [];
959
- } catch {
960
- return [];
975
+ if (migrated > 0) {
976
+ this.save();
977
+ logger5.info("Migrated tracker records", { migrated });
961
978
  }
962
979
  }
963
- static renderReviewHistoryMarkdown(history) {
964
- if (history.length === 0) return "";
965
- const lines = ["# \u5BA1\u6838\u53CD\u9988\u5386\u53F2", ""];
966
- for (const r of history) {
967
- lines.push(`## \u7B2C ${r.round} \u8F6E\u5BA1\u6838\u53CD\u9988`);
968
- lines.push(`> \u65F6\u95F4: ${r.timestamp}`);
969
- lines.push("");
970
- lines.push(r.feedback);
971
- lines.push("");
980
+ lifecycleFor(record) {
981
+ if (record.pipelineMode) {
982
+ const lm = this.lifecycleManagers.get(record.pipelineMode);
983
+ if (lm) return lm;
972
984
  }
973
- return lines.join("\n");
985
+ return this.lifecycleManagers.get("plan-mode") ?? this.lifecycleManagers.values().next().value;
974
986
  }
975
- updatePhaseProgress(phaseName, status, error, options) {
976
- const progress = this.readProgress();
977
- if (!progress) return;
987
+ key(issueIid) {
988
+ return String(issueIid);
989
+ }
990
+ get(issueIid) {
991
+ return this.getByKey(this.key(issueIid));
992
+ }
993
+ create(record) {
978
994
  const now = (/* @__PURE__ */ new Date()).toISOString();
979
- if (!progress.phases[phaseName]) {
980
- progress.phases[phaseName] = { status: "pending" };
995
+ const full = {
996
+ ...record,
997
+ attempts: 0,
998
+ createdAt: now,
999
+ updatedAt: now
1000
+ };
1001
+ this.setRecord(this.key(getIid(full)), full);
1002
+ this.save();
1003
+ logger5.info("Issue tracked", { issueIid: getIid(full), state: record.state });
1004
+ eventBus.emitTyped("issue:created", full);
1005
+ return full;
1006
+ }
1007
+ updateState(issueIid, state, extra) {
1008
+ const record = this.collection[this.key(issueIid)];
1009
+ if (!record) {
1010
+ throw new IssueNotFoundError(issueIid);
981
1011
  }
982
- const phase = progress.phases[phaseName];
983
- phase.status = status;
984
- if (status === "in_progress") {
985
- phase.startedAt = now;
986
- progress.currentPhase = phaseName;
987
- if (!options?.preserveSessionId) {
988
- delete phase.sessionId;
989
- }
990
- } else if (status === "completed") {
991
- phase.completedAt = now;
992
- } else if (status === "failed") {
993
- phase.error = error;
1012
+ record.state = state;
1013
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1014
+ if (state === "completed" /* Completed */) {
1015
+ record.lastError = void 0;
1016
+ record.failedAtState = void 0;
994
1017
  }
995
- this.writeProgress(progress);
1018
+ if (extra) {
1019
+ Object.assign(record, extra);
1020
+ }
1021
+ this.save();
1022
+ logger5.info("Issue state updated", { issueIid, state });
1023
+ eventBus.emitTyped("issue:stateChanged", { issueIid, state, record });
996
1024
  }
997
- updatePhaseSessionId(phaseName, sessionId) {
998
- const progress = this.readProgress();
999
- if (!progress?.phases[phaseName]) return;
1000
- progress.phases[phaseName].sessionId = sessionId;
1001
- this.writeProgress(progress);
1025
+ markFailed(issueIid, error, failedAtState, isRetryable) {
1026
+ const record = this.collection[this.key(issueIid)];
1027
+ if (!record) return;
1028
+ record.state = "failed" /* Failed */;
1029
+ record.lastError = error;
1030
+ record.failedAtState = failedAtState;
1031
+ record.lastErrorRetryable = isRetryable;
1032
+ record.attempts += 1;
1033
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1034
+ this.save();
1035
+ logger5.warn("Issue marked as failed", { issueIid, error, failedAtState, attempts: record.attempts, isRetryable });
1036
+ eventBus.emitTyped("issue:failed", { issueIid, error, failedAtState, record });
1002
1037
  }
1003
- getPhaseSessionId(phaseName) {
1004
- const progress = this.readProgress();
1005
- return progress?.phases[phaseName]?.sessionId;
1038
+ isProcessing(issueIid) {
1039
+ const record = this.get(issueIid);
1040
+ if (!record) return false;
1041
+ return !this.lifecycleFor(record).isTerminal(record.state);
1006
1042
  }
1007
- };
1008
-
1009
- // src/phases/BasePhase.ts
1010
- import fs3 from "fs";
1011
- import path4 from "path";
1012
-
1013
- // src/rules/RuleResolver.ts
1014
- import { readdir, readFile } from "fs/promises";
1015
- import path3 from "path";
1016
- function parseFrontmatter(raw) {
1017
- const fmRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
1018
- const match = raw.match(fmRegex);
1019
- if (!match) {
1020
- return { description: "", alwaysApply: false, content: raw.trim() };
1043
+ isCompleted(issueIid) {
1044
+ const record = this.get(issueIid);
1045
+ return record?.state === "completed" /* Completed */;
1021
1046
  }
1022
- const yamlBlock = match[1];
1023
- const content = raw.slice(match[0].length).trim();
1024
- let description = "";
1025
- const descMatch = yamlBlock.match(/description:\s*(.+)/);
1026
- if (descMatch) {
1027
- description = descMatch[1].trim();
1047
+ canRetry(issueIid, maxRetries) {
1048
+ const record = this.get(issueIid);
1049
+ if (!record || record.state !== "failed" /* Failed */) return false;
1050
+ return record.attempts < maxRetries;
1028
1051
  }
1029
- let alwaysApply = false;
1030
- const applyMatch = yamlBlock.match(/alwaysApply:\s*(true|false)/i);
1031
- if (applyMatch) {
1032
- alwaysApply = applyMatch[1].toLowerCase() === "true";
1052
+ getRetryState(issueIid) {
1053
+ const record = this.get(issueIid);
1054
+ return record?.failedAtState;
1033
1055
  }
1034
- return { description, alwaysApply, content };
1035
- }
1036
- var RuleResolver = class {
1037
- rules = [];
1038
- triggers;
1039
- constructor(ruleTriggers) {
1040
- this.triggers = ruleTriggers;
1056
+ isStalled(issueIid, thresholdMs = 5 * 60 * 1e3) {
1057
+ const record = this.get(issueIid);
1058
+ if (!record) return false;
1059
+ if (this.lifecycleFor(record).isBlocked(record.state)) return false;
1060
+ if (!this.isProcessing(issueIid)) return false;
1061
+ const elapsed = Date.now() - new Date(record.updatedAt).getTime();
1062
+ return elapsed > thresholdMs;
1041
1063
  }
1042
- async loadRules(rulesDir) {
1043
- this.rules = [];
1044
- let files;
1045
- try {
1046
- files = await readdir(rulesDir);
1047
- } catch {
1048
- return;
1049
- }
1050
- const mdcFiles = files.filter((f) => f.endsWith(".mdc"));
1051
- const loadPromises = mdcFiles.map(async (filename) => {
1052
- try {
1053
- const raw = await readFile(path3.join(rulesDir, filename), "utf-8");
1054
- const { description, alwaysApply, content } = parseFrontmatter(raw);
1055
- if (content) {
1056
- this.rules.push({ filename, description, alwaysApply, content });
1057
- }
1058
- } catch {
1059
- }
1064
+ getDrivableIssues(maxRetries, stalledThresholdMs) {
1065
+ return this.getAllRecords().filter((record) => {
1066
+ const lm = this.lifecycleFor(record);
1067
+ if (lm.isDrivable(record.state, record.attempts, maxRetries, record.lastErrorRetryable)) return true;
1068
+ if (this.isStalled(getIid(record), stalledThresholdMs)) return true;
1069
+ return false;
1060
1070
  });
1061
- await Promise.all(loadPromises);
1062
1071
  }
1063
- getRules() {
1064
- return this.rules;
1072
+ getAllActive() {
1073
+ return this.getAllRecords().filter(
1074
+ (r) => !this.lifecycleFor(r).isTerminal(r.state)
1075
+ );
1076
+ }
1077
+ getAll() {
1078
+ return this.getAllRecords();
1079
+ }
1080
+ startSkipped(issueIid) {
1081
+ const record = this.collection[this.key(issueIid)];
1082
+ if (!record || record.state !== "skipped" /* Skipped */) return false;
1083
+ record.state = "pending" /* Pending */;
1084
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1085
+ this.save();
1086
+ logger5.info("Skipped issue started", { issueIid });
1087
+ eventBus.emitTyped("issue:stateChanged", { issueIid, state: "pending" /* Pending */, record });
1088
+ return true;
1089
+ }
1090
+ resetFull(issueIid) {
1091
+ const record = this.collection[this.key(issueIid)];
1092
+ if (!record) return false;
1093
+ record.state = "pending" /* Pending */;
1094
+ record.currentPhase = void 0;
1095
+ record.attempts = 0;
1096
+ record.failedAtState = void 0;
1097
+ record.lastError = void 0;
1098
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1099
+ this.save();
1100
+ logger5.info("Issue fully reset", { issueIid });
1101
+ eventBus.emitTyped("issue:restarted", { issueIid, record });
1102
+ return true;
1103
+ }
1104
+ resetToPhase(issueIid, phase, def) {
1105
+ const record = this.collection[this.key(issueIid)];
1106
+ if (!record) return false;
1107
+ const lm = new ActionLifecycleManager(def);
1108
+ const targetState = lm.getPhasePreState(phase);
1109
+ if (!targetState) return false;
1110
+ record.state = targetState;
1111
+ if (targetState === "phase_running" /* PhaseRunning */ || targetState === "phase_done" /* PhaseDone */ || targetState === "phase_waiting" /* PhaseWaiting */ || targetState === "phase_approved" /* PhaseApproved */) {
1112
+ const phases = def.phases;
1113
+ const idx = phases.findIndex((p) => p.name === phase);
1114
+ if (idx > 0) {
1115
+ record.currentPhase = phases[idx - 1].name;
1116
+ }
1117
+ }
1118
+ record.failedAtState = void 0;
1119
+ record.lastError = void 0;
1120
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1121
+ this.save();
1122
+ logger5.info("Issue reset to phase", { issueIid, phase, state: targetState });
1123
+ eventBus.emitTyped("issue:retryFromPhase", { issueIid, phase, record });
1124
+ return true;
1125
+ }
1126
+ resetForRetry(issueIid) {
1127
+ const record = this.collection[this.key(issueIid)];
1128
+ if (!record || record.state !== "failed" /* Failed */) return false;
1129
+ const restoreState = record.failedAtState ?? "pending" /* Pending */;
1130
+ record.state = restoreState;
1131
+ record.lastError = void 0;
1132
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1133
+ this.save();
1134
+ logger5.info("Issue reset for retry", { issueIid, restoreState });
1135
+ eventBus.emitTyped("issue:resetForRetry", { issueIid, restoreState, record });
1136
+ return true;
1065
1137
  }
1066
- matchRules(text) {
1067
- const lowerText = text.toLowerCase();
1068
- const matched = /* @__PURE__ */ new Map();
1069
- for (const rule of this.rules) {
1070
- if (rule.alwaysApply) {
1071
- matched.set(rule.filename, rule);
1138
+ delete(issueIid) {
1139
+ const key = this.key(issueIid);
1140
+ const record = this.collection[key];
1141
+ if (!record) return false;
1142
+ delete this.collection[key];
1143
+ this.save();
1144
+ logger5.info("Issue deleted from tracker", { issueIid });
1145
+ eventBus.emitTyped("issue:deleted", { issueIid, record });
1146
+ return true;
1147
+ }
1148
+ recoverInterruptedIssues() {
1149
+ let count = 0;
1150
+ for (const record of this.getAllRecords()) {
1151
+ const lm = this.lifecycleFor(record);
1152
+ if (lm.isInProgress(record.state)) {
1153
+ const iid = getIid(record);
1154
+ logger5.warn("Recovering interrupted issue", {
1155
+ issueIid: iid,
1156
+ state: record.state
1157
+ });
1158
+ record.failedAtState = record.state;
1159
+ record.state = "failed" /* Failed */;
1160
+ record.lastError = "Interrupted by service restart";
1161
+ record.attempts += 1;
1162
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1163
+ count++;
1164
+ eventBus.emitTyped("issue:failed", {
1165
+ issueIid: iid,
1166
+ error: "Interrupted by service restart",
1167
+ failedAtState: record.failedAtState,
1168
+ record
1169
+ });
1072
1170
  }
1073
1171
  }
1074
- if (this.triggers && this.triggers.length > 0) {
1075
- for (const trigger of this.triggers) {
1076
- if (matched.has(trigger.filename)) continue;
1077
- const isTriggered = trigger.keywords.some((kw) => lowerText.includes(kw.toLowerCase()));
1078
- if (!isTriggered) continue;
1079
- const rule = this.rules.find((r) => r.filename === trigger.filename);
1080
- if (rule) {
1081
- matched.set(rule.filename, rule);
1082
- }
1083
- }
1084
- } else {
1085
- for (const rule of this.rules) {
1086
- if (matched.has(rule.filename)) continue;
1087
- if (!rule.description) continue;
1088
- const descWords = rule.description.split(/[\s,,;;、/|]+/).filter((w) => w.length >= 2);
1089
- const isMatched = descWords.some((word) => lowerText.includes(word.toLowerCase()));
1090
- if (isMatched) {
1091
- matched.set(rule.filename, rule);
1092
- }
1093
- }
1172
+ if (count > 0) {
1173
+ this.save();
1174
+ logger5.info("Recovered interrupted issues", { count });
1094
1175
  }
1095
- return Array.from(matched.values());
1176
+ return count;
1096
1177
  }
1097
- formatForPrompt(rules) {
1098
- if (rules.length === 0) return "";
1099
- return rules.map((rule) => {
1100
- const header = rule.description ? `### ${rule.description} (${rule.filename})` : `### ${rule.filename}`;
1101
- return `${header}
1102
-
1103
- ${rule.content}`;
1104
- }).join("\n\n---\n\n");
1178
+ /** 将所有 IssueRecord 投影为 ExecutableTask[] */
1179
+ toExecutableTasks() {
1180
+ return this.getAllRecords().map((record) => {
1181
+ const lm = this.lifecycleFor(record);
1182
+ return issueToExecutableTask(record, lm);
1183
+ });
1105
1184
  }
1106
1185
  };
1107
1186
 
1108
- // src/notesync/NoteSyncSettings.ts
1109
- var noteSyncOverride;
1110
- function getNoteSyncEnabled(cfg) {
1111
- return noteSyncOverride ?? cfg.issueNoteSync.enabled;
1112
- }
1113
- function setNoteSyncOverride(value) {
1114
- noteSyncOverride = value;
1115
- }
1116
- function isNoteSyncEnabledForIssue(issueIid, tracker, cfg) {
1117
- const record = tracker.get(issueIid);
1118
- if (record?.issueNoteSyncEnabled !== void 0) return record.issueNoteSyncEnabled;
1119
- return getNoteSyncEnabled(cfg);
1120
- }
1121
- var SUMMARY_MAX_LENGTH = 500;
1122
- function truncateToSummary(content) {
1123
- if (content.length <= SUMMARY_MAX_LENGTH) return content;
1124
- const cut = content.slice(0, SUMMARY_MAX_LENGTH);
1125
- const lastNewline = cut.lastIndexOf("\n\n");
1126
- const boundary = lastNewline > SUMMARY_MAX_LENGTH * 0.3 ? lastNewline : cut.lastIndexOf("\n");
1127
- const summary = boundary > SUMMARY_MAX_LENGTH * 0.3 ? cut.slice(0, boundary) : cut;
1128
- return summary + "\n\n...";
1129
- }
1130
- function buildNoteSyncComment(phaseName, phaseLabel, docUrl, dashboardUrl, summary) {
1131
- const emoji = {
1132
- analysis: "\u{1F50D}",
1133
- design: "\u{1F4D0}",
1134
- implement: "\u{1F4BB}",
1135
- verify: "\u2705",
1136
- plan: "\u{1F4CB}",
1137
- review: "\u{1F440}",
1138
- build: "\u{1F528}"
1139
- };
1140
- const icon = emoji[phaseName] || "\u{1F4CB}";
1141
- return [
1142
- t("notesync.phaseCompleted", { icon, label: phaseLabel }),
1143
- "",
1144
- summary,
1145
- "",
1146
- "---",
1147
- t("notesync.viewDoc", { label: phaseLabel, url: docUrl }),
1148
- t("notesync.viewDashboard", { url: dashboardUrl })
1149
- ].join("\n");
1150
- }
1151
-
1152
- // src/phases/BasePhase.ts
1153
- var BasePhase = class _BasePhase {
1154
- static MIN_ARTIFACT_BYTES = 50;
1155
- aiRunner;
1156
- git;
1157
- plan;
1158
- config;
1159
- logger;
1160
- lifecycle;
1161
- hooks;
1162
- /** 多仓模式下所有 repo 的 GitOperations(repo name → GitOperations) */
1163
- wtGitMap;
1164
- get startState() {
1165
- const states = this.lifecycle.getPhaseStates(this.phaseName);
1166
- if (!states) throw new Error(`Phase "${this.phaseName}" not found in lifecycle manager`);
1167
- return states.startState;
1187
+ // src/persistence/PlanPersistence.ts
1188
+ import fs2 from "fs";
1189
+ import path2 from "path";
1190
+ var logger6 = logger.child("PlanPersistence");
1191
+ var PLAN_DIR = ".claude-plan";
1192
+ var PlanPersistence = class _PlanPersistence {
1193
+ workDir;
1194
+ issueIid;
1195
+ constructor(workDir, issueIid) {
1196
+ this.workDir = workDir;
1197
+ this.issueIid = issueIid;
1168
1198
  }
1169
- get doneState() {
1170
- const states = this.lifecycle.getPhaseStates(this.phaseName);
1171
- if (!states) throw new Error(`Phase "${this.phaseName}" not found in lifecycle manager`);
1172
- return states.doneState;
1199
+ get baseDir() {
1200
+ return this.workDir;
1173
1201
  }
1174
- constructor(aiRunner, git, plan, config, lifecycle, hooks) {
1175
- this.aiRunner = aiRunner;
1176
- this.git = git;
1177
- this.plan = plan;
1178
- this.config = config;
1179
- this.lifecycle = lifecycle;
1180
- this.hooks = hooks;
1181
- this.logger = logger.child(this.constructor.name);
1202
+ get planDir() {
1203
+ return path2.join(this.workDir, PLAN_DIR, `issue-${this.issueIid}`);
1182
1204
  }
1183
- /** 注入多仓库 GitOperations map(由编排器在创建 phase 后调用) */
1184
- setWtGitMap(map) {
1185
- this.wtGitMap = map;
1205
+ ensureDir() {
1206
+ if (!fs2.existsSync(this.planDir)) {
1207
+ fs2.mkdirSync(this.planDir, { recursive: true });
1208
+ }
1186
1209
  }
1187
- getRunMode() {
1188
- return void 0;
1210
+ writeIssueMeta(meta) {
1211
+ this.ensureDir();
1212
+ const filePath = path2.join(this.planDir, "issue-meta.json");
1213
+ fs2.writeFileSync(filePath, JSON.stringify(meta, null, 2), "utf-8");
1214
+ logger6.info("Issue meta written");
1189
1215
  }
1190
- getResultFiles(_ctx) {
1191
- return [];
1216
+ writeProgress(data) {
1217
+ this.ensureDir();
1218
+ const filePath = path2.join(this.planDir, "progress.json");
1219
+ fs2.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
1220
+ logger6.debug("Progress written", { currentPhase: data.currentPhase });
1192
1221
  }
1193
- async execute(ctx) {
1194
- const displayId = Number(ctx.demand.sourceRef.displayId);
1195
- this.logger.info(`Phase ${this.phaseName} starting`, { issueIid: displayId });
1196
- const resumeInfo = this.resolveResumeInfo();
1197
- await this.notifyPhaseStart();
1198
- this.plan.updatePhaseProgress(this.phaseName, "in_progress", void 0, {
1199
- preserveSessionId: resumeInfo.resumable
1200
- });
1201
- await this.notifyComment(
1202
- issueProgressComment(this.phaseName, "in_progress")
1203
- );
1204
- const phaseLabel = t(`phase.${this.phaseName}`) || this.phaseName;
1205
- const systemMessage = resumeInfo.resumable ? t("basePhase.aiResuming", { label: phaseLabel }) : t("basePhase.aiStarting", { label: phaseLabel });
1206
- eventBus.emitTyped("agent:output", {
1207
- issueIid: displayId,
1208
- phase: this.phaseName,
1209
- event: { type: "system", content: systemMessage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1210
- });
1211
- let basePrompt = this.buildPrompt(ctx);
1212
- const wsSection = buildWorkspaceSection(ctx.workspace);
1213
- if (wsSection) {
1214
- basePrompt = `${basePrompt}
1215
-
1216
- ${wsSection}`;
1222
+ readProgress() {
1223
+ const filePath = path2.join(this.planDir, "progress.json");
1224
+ if (!fs2.existsSync(filePath)) return null;
1225
+ try {
1226
+ return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
1227
+ } catch {
1228
+ return null;
1217
1229
  }
1218
- const matchedRulesText = await this.resolveRules(ctx);
1219
- const fullPrompt = matchedRulesText ? `${basePrompt}
1220
-
1221
- ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1222
- const resumePrompt = t("basePhase.resumePrompt");
1223
- const onInputRequired = this.buildInputRequiredHandler(displayId);
1224
- let result;
1225
- if (resumeInfo.resumable) {
1226
- this.logger.info("Attempting session resume", {
1227
- issueIid: displayId,
1228
- phase: this.phaseName,
1229
- sessionId: resumeInfo.sessionId
1230
- });
1231
- result = await this.runWithResumeFallback(
1230
+ }
1231
+ writeAnalysis(content) {
1232
+ this.ensureDir();
1233
+ fs2.writeFileSync(path2.join(this.planDir, "01-analysis.md"), content, "utf-8");
1234
+ logger6.info("Analysis document written");
1235
+ }
1236
+ writeDesign(content) {
1237
+ this.ensureDir();
1238
+ fs2.writeFileSync(path2.join(this.planDir, "02-design.md"), content, "utf-8");
1239
+ logger6.info("Design document written");
1240
+ }
1241
+ writeTodolist(content) {
1242
+ this.ensureDir();
1243
+ fs2.writeFileSync(path2.join(this.planDir, "03-todolist.md"), content, "utf-8");
1244
+ logger6.info("Todolist written");
1245
+ }
1246
+ writeVerifyReport(content, filename = "04-verify-report.md") {
1247
+ this.ensureDir();
1248
+ fs2.writeFileSync(path2.join(this.planDir, filename), content, "utf-8");
1249
+ logger6.info("Verify report written", { filename });
1250
+ }
1251
+ getAllPlanFiles() {
1252
+ if (!fs2.existsSync(this.planDir)) return [];
1253
+ return fs2.readdirSync(this.planDir).map((f) => path2.join(PLAN_DIR, `issue-${this.issueIid}`, f));
1254
+ }
1255
+ createInitialProgress(displayId, title, branchName, def) {
1256
+ const pending = { status: "pending" };
1257
+ if (def) {
1258
+ const phases = {};
1259
+ for (const spec of def.phases) {
1260
+ phases[spec.name] = { ...pending };
1261
+ }
1262
+ return {
1232
1263
  displayId,
1233
- resumeInfo.sessionId,
1234
- resumePrompt,
1235
- fullPrompt,
1236
- onInputRequired,
1237
- ctx
1238
- );
1239
- } else {
1240
- result = await this.runAI(displayId, fullPrompt, onInputRequired, void 0, ctx);
1241
- }
1242
- if (!result.success) {
1243
- this.persistSessionId(result.sessionId);
1244
- const failReason = (result.errorMessage || result.output).slice(0, 500);
1245
- const shortReason = (result.errorMessage || result.output).slice(0, 200);
1246
- this.plan.updatePhaseProgress(this.phaseName, "failed", failReason);
1247
- await this.notifyPhaseFailed(failReason);
1248
- await this.notifyComment(
1249
- issueProgressComment(this.phaseName, "failed", t("basePhase.error", { message: shortReason }))
1250
- );
1251
- throw new AIExecutionError(this.phaseName, `Phase ${this.phaseName} failed: ${shortReason}`, {
1252
- output: result.output,
1253
- exitCode: result.exitCode,
1254
- isRetryable: this.classifyRetryable(result)
1255
- });
1264
+ title,
1265
+ branchName,
1266
+ pipelineMode: def.mode,
1267
+ currentPhase: def.phases[0].name,
1268
+ phases
1269
+ };
1256
1270
  }
1257
- this.persistSessionId(result.sessionId);
1258
- await this.validatePhaseOutput(ctx, displayId);
1259
- await this.notifyPhaseDone();
1260
- this.plan.updatePhaseProgress(this.phaseName, "completed");
1261
- await this.commitPlanFiles(ctx, displayId);
1262
- await this.syncResultToIssue(ctx, displayId);
1263
- this.logger.info(`Phase ${this.phaseName} completed`, { issueIid: displayId });
1264
- return result;
1271
+ return {
1272
+ displayId,
1273
+ title,
1274
+ branchName,
1275
+ currentPhase: "analysis",
1276
+ phases: {
1277
+ analysis: { ...pending },
1278
+ design: { ...pending },
1279
+ implement: { ...pending },
1280
+ verify: { ...pending }
1281
+ }
1282
+ };
1265
1283
  }
1266
- // ── Session resume helpers ──
1267
- resolveResumeInfo() {
1268
- if (this.isAcpMode()) {
1269
- return { resumable: false };
1270
- }
1271
- const previousSessionId = this.plan.getPhaseSessionId(this.phaseName);
1272
- if (!previousSessionId) {
1273
- return { resumable: false };
1274
- }
1275
- const progress = this.plan.readProgress();
1276
- const phaseStatus = progress?.phases[this.phaseName]?.status;
1277
- if (phaseStatus !== "failed" && phaseStatus !== "in_progress") {
1278
- return { resumable: false };
1279
- }
1280
- return { resumable: true, sessionId: previousSessionId };
1284
+ writePlan(content) {
1285
+ this.ensureDir();
1286
+ fs2.writeFileSync(path2.join(this.planDir, "01-plan.md"), content, "utf-8");
1287
+ logger6.info("Plan document written");
1281
1288
  }
1282
- isAcpMode() {
1283
- return this.config.ai.mode === "codebuddy-acp";
1289
+ writeReviewFeedback(content) {
1290
+ this.ensureDir();
1291
+ const history = this.readReviewHistory();
1292
+ const round = {
1293
+ round: history.length + 1,
1294
+ feedback: content,
1295
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1296
+ };
1297
+ history.push(round);
1298
+ fs2.writeFileSync(
1299
+ path2.join(this.planDir, "review-history.json"),
1300
+ JSON.stringify(history, null, 2),
1301
+ "utf-8"
1302
+ );
1303
+ fs2.writeFileSync(
1304
+ path2.join(this.planDir, "review-feedback.md"),
1305
+ _PlanPersistence.renderReviewHistoryMarkdown(history),
1306
+ "utf-8"
1307
+ );
1308
+ logger6.info("Review feedback appended", { round: round.round });
1284
1309
  }
1285
- resolveAIWorkDir(ctx) {
1286
- if (ctx?.workspace && ctx.workspace.repos.length > 1) {
1287
- return ctx.workspace.workspaceRoot;
1310
+ readReviewFeedback() {
1311
+ const filePath = path2.join(this.planDir, "review-feedback.md");
1312
+ if (!fs2.existsSync(filePath)) return null;
1313
+ try {
1314
+ return fs2.readFileSync(filePath, "utf-8");
1315
+ } catch {
1316
+ return null;
1288
1317
  }
1289
- return this.plan.baseDir;
1290
1318
  }
1291
- async runAI(displayId, prompt, onInputRequired, options, ctx) {
1292
- let capturedSessionId;
1293
- const result = await this.aiRunner.run({
1294
- prompt,
1295
- workDir: this.resolveAIWorkDir(ctx),
1296
- timeoutMs: this.config.ai.phaseTimeoutMs,
1297
- idleTimeoutMs: this.config.ai.idleTimeoutMs,
1298
- mode: this.getRunMode(),
1299
- sessionId: options?.sessionId,
1300
- continueSession: options?.continueSession,
1301
- onStreamEvent: (event) => {
1302
- if (!capturedSessionId && event.type !== "raw") {
1303
- const content = event.content;
1304
- if (content?.session_id && typeof content.session_id === "string") {
1305
- capturedSessionId = content.session_id;
1306
- this.persistSessionId(capturedSessionId);
1307
- }
1308
- }
1309
- eventBus.emitTyped("agent:output", {
1310
- issueIid: displayId,
1311
- phase: this.phaseName,
1312
- event
1313
- });
1314
- },
1315
- onInputRequired
1316
- });
1317
- if (result.sessionId) {
1318
- this.persistSessionId(result.sessionId);
1319
+ readReviewHistory() {
1320
+ const filePath = path2.join(this.planDir, "review-history.json");
1321
+ if (!fs2.existsSync(filePath)) return [];
1322
+ try {
1323
+ const data = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
1324
+ return Array.isArray(data) ? data : [];
1325
+ } catch {
1326
+ return [];
1319
1327
  }
1320
- return result;
1321
1328
  }
1322
- async runWithResumeFallback(displayId, sessionId, resumePrompt, fullPrompt, onInputRequired, ctx) {
1323
- const result = await this.runAI(displayId, resumePrompt, onInputRequired, {
1324
- sessionId,
1325
- continueSession: true
1326
- }, ctx);
1327
- if (!result.success && this.isResumeFailure(result)) {
1328
- this.logger.warn(t("basePhase.resumeFallback"), {
1329
- issueIid: displayId,
1330
- phase: this.phaseName,
1331
- exitCode: result.exitCode
1332
- });
1333
- eventBus.emitTyped("agent:output", {
1334
- issueIid: displayId,
1335
- phase: this.phaseName,
1336
- event: {
1337
- type: "system",
1338
- content: t("basePhase.resumeFallback"),
1339
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1340
- }
1341
- });
1342
- return this.runAI(displayId, fullPrompt, onInputRequired, void 0, ctx);
1329
+ static renderReviewHistoryMarkdown(history) {
1330
+ if (history.length === 0) return "";
1331
+ const lines = ["# \u5BA1\u6838\u53CD\u9988\u5386\u53F2", ""];
1332
+ for (const r of history) {
1333
+ lines.push(`## \u7B2C ${r.round} \u8F6E\u5BA1\u6838\u53CD\u9988`);
1334
+ lines.push(`> \u65F6\u95F4: ${r.timestamp}`);
1335
+ lines.push("");
1336
+ lines.push(r.feedback);
1337
+ lines.push("");
1343
1338
  }
1344
- return result;
1345
- }
1346
- /**
1347
- * 判断 AI 执行失败是否为永久性错误(不可重试)。
1348
- * 模型不存在、API key 无效等配置问题重试不会成功。
1349
- */
1350
- classifyRetryable(result) {
1351
- const msg = (result.errorMessage ?? result.output ?? "").toLowerCase();
1352
- const permanentPatterns = [
1353
- /model\b.*\b(?:not found|not supported|unavailable|service info not found)/,
1354
- /invalid.?api.?key/,
1355
- /authentication.*(?:failed|denied|error)/,
1356
- /permission.?denied/,
1357
- /billing/,
1358
- /quota.*exceeded/
1359
- ];
1360
- return !permanentPatterns.some((p) => p.test(msg));
1339
+ return lines.join("\n");
1361
1340
  }
1362
- /**
1363
- * Heuristic: a resume failure is typically an immediate process exit
1364
- * (exit code != 0, empty output) caused by an invalid/expired session ID.
1365
- */
1366
- isResumeFailure(result) {
1367
- if (result.success) return false;
1368
- const msg = (result.errorMessage ?? "").toLowerCase();
1369
- if (msg.includes("session") || msg.includes("resume") || msg.includes("session_id")) {
1370
- return true;
1341
+ updatePhaseProgress(phaseName, status, error, options) {
1342
+ const progress = this.readProgress();
1343
+ if (!progress) return;
1344
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1345
+ if (!progress.phases[phaseName]) {
1346
+ progress.phases[phaseName] = { status: "pending" };
1371
1347
  }
1372
- if (result.output.length === 0 && result.exitCode !== null && result.exitCode !== 0) {
1373
- const isConfigError = msg.includes("model") || msg.includes("api key") || msg.includes("authentication");
1374
- return !isConfigError;
1348
+ const phase = progress.phases[phaseName];
1349
+ phase.status = status;
1350
+ if (status === "in_progress") {
1351
+ phase.startedAt = now;
1352
+ progress.currentPhase = phaseName;
1353
+ if (!options?.preserveSessionId) {
1354
+ delete phase.sessionId;
1355
+ }
1356
+ } else if (status === "completed") {
1357
+ phase.completedAt = now;
1358
+ } else if (status === "failed") {
1359
+ phase.error = error;
1375
1360
  }
1376
- return false;
1361
+ this.writeProgress(progress);
1377
1362
  }
1378
- persistSessionId(sessionId) {
1379
- if (sessionId) {
1380
- this.plan.updatePhaseSessionId(this.phaseName, sessionId);
1381
- }
1363
+ updatePhaseSessionId(phaseName, sessionId) {
1364
+ const progress = this.readProgress();
1365
+ if (!progress?.phases[phaseName]) return;
1366
+ progress.phases[phaseName].sessionId = sessionId;
1367
+ this.writeProgress(progress);
1382
1368
  }
1383
- // ── Hook dispatch methods ──
1384
- async notifyPhaseStart() {
1385
- await this.hooks.onPhaseStart(this.phaseName);
1369
+ getPhaseSessionId(phaseName) {
1370
+ const progress = this.readProgress();
1371
+ return progress?.phases[phaseName]?.sessionId;
1386
1372
  }
1387
- async notifyPhaseDone() {
1388
- await this.hooks.onPhaseDone(this.phaseName);
1373
+ };
1374
+
1375
+ // src/phases/BasePhase.ts
1376
+ import fs3 from "fs";
1377
+ import path4 from "path";
1378
+
1379
+ // src/rules/RuleResolver.ts
1380
+ import { readdir, readFile } from "fs/promises";
1381
+ import path3 from "path";
1382
+ function parseFrontmatter(raw) {
1383
+ const fmRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
1384
+ const match = raw.match(fmRegex);
1385
+ if (!match) {
1386
+ return { description: "", alwaysApply: false, content: raw.trim() };
1389
1387
  }
1390
- async notifyPhaseFailed(error) {
1391
- await this.hooks.onPhaseFailed(this.phaseName, error);
1388
+ const yamlBlock = match[1];
1389
+ const content = raw.slice(match[0].length).trim();
1390
+ let description = "";
1391
+ const descMatch = yamlBlock.match(/description:\s*(.+)/);
1392
+ if (descMatch) {
1393
+ description = descMatch[1].trim();
1392
1394
  }
1393
- async notifyComment(message) {
1394
- try {
1395
- await this.hooks.onComment(message);
1396
- } catch (err) {
1397
- this.logger.warn("Hook onComment failed", { error: err.message });
1398
- }
1395
+ let alwaysApply = false;
1396
+ const applyMatch = yamlBlock.match(/alwaysApply:\s*(true|false)/i);
1397
+ if (applyMatch) {
1398
+ alwaysApply = applyMatch[1].toLowerCase() === "true";
1399
1399
  }
1400
- /**
1401
- * Build an onInputRequired handler for ACP permission delegation.
1402
- * Only returns a handler when codebuddyAcpAutoApprove is false,
1403
- * enabling review gate delegation for permission requests.
1404
- */
1405
- buildInputRequiredHandler(displayId) {
1406
- if (this.config.ai.codebuddyAcpAutoApprove !== false) return void 0;
1407
- return (request) => {
1408
- if (request.type !== "plan-approval") {
1409
- return Promise.resolve("allow");
1410
- }
1411
- this.logger.info("ACP plan-approval requested, delegating to review gate", {
1412
- issueIid: displayId,
1413
- phase: this.phaseName
1414
- });
1415
- eventBus.emitTyped("review:requested", { issueIid: displayId });
1416
- return new Promise((resolve) => {
1417
- const onApproved = (payload) => {
1418
- const data = payload.data;
1419
- if (data.issueIid !== displayId) return;
1420
- cleanup();
1421
- this.logger.info("ACP plan-approval approved via review gate", { issueIid: displayId });
1422
- resolve("allow");
1423
- };
1424
- const onRejected = (payload) => {
1425
- const data = payload.data;
1426
- if (data.issueIid !== displayId) return;
1427
- cleanup();
1428
- this.logger.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
1429
- resolve("reject");
1430
- };
1431
- const cleanup = () => {
1432
- eventBus.removeListener("review:approved", onApproved);
1433
- eventBus.removeListener("review:rejected", onRejected);
1434
- };
1435
- eventBus.on("review:approved", onApproved);
1436
- eventBus.on("review:rejected", onRejected);
1437
- });
1438
- };
1400
+ return { description, alwaysApply, content };
1401
+ }
1402
+ var RuleResolver = class {
1403
+ rules = [];
1404
+ triggers;
1405
+ constructor(ruleTriggers) {
1406
+ this.triggers = ruleTriggers;
1439
1407
  }
1440
- async resolveRules(ctx) {
1408
+ async loadRules(rulesDir) {
1409
+ this.rules = [];
1410
+ let files;
1441
1411
  try {
1442
- const knowledge = getProjectKnowledge();
1443
- const resolver = new RuleResolver(knowledge?.ruleTriggers);
1444
- const context = `${ctx.demand.title} ${ctx.demand.description} ${ctx.demand.supplement ? JSON.stringify(ctx.demand.supplement) : ""}`;
1445
- if (ctx.workspace && ctx.workspace.repos.length > 1) {
1446
- for (const repo of ctx.workspace.repos) {
1447
- const rulesDir = path4.join(repo.gitRootDir, ".cursor", "rules");
1448
- try {
1449
- await resolver.loadRules(rulesDir);
1450
- } catch {
1451
- }
1412
+ files = await readdir(rulesDir);
1413
+ } catch {
1414
+ return;
1415
+ }
1416
+ const mdcFiles = files.filter((f) => f.endsWith(".mdc"));
1417
+ const loadPromises = mdcFiles.map(async (filename) => {
1418
+ try {
1419
+ const raw = await readFile(path3.join(rulesDir, filename), "utf-8");
1420
+ const { description, alwaysApply, content } = parseFrontmatter(raw);
1421
+ if (content) {
1422
+ this.rules.push({ filename, description, alwaysApply, content });
1452
1423
  }
1453
- } else {
1454
- const rulesDir = path4.join(this.plan.baseDir, ".cursor", "rules");
1455
- await resolver.loadRules(rulesDir);
1424
+ } catch {
1456
1425
  }
1457
- const matched = resolver.matchRules(context);
1458
- if (matched.length > 0) {
1459
- this.logger.info(`Matched ${matched.length} MDC rules`, {
1460
- rules: matched.map((r) => r.filename)
1461
- });
1462
- return resolver.formatForPrompt(matched);
1426
+ });
1427
+ await Promise.all(loadPromises);
1428
+ }
1429
+ getRules() {
1430
+ return this.rules;
1431
+ }
1432
+ matchRules(text) {
1433
+ const lowerText = text.toLowerCase();
1434
+ const matched = /* @__PURE__ */ new Map();
1435
+ for (const rule of this.rules) {
1436
+ if (rule.alwaysApply) {
1437
+ matched.set(rule.filename, rule);
1463
1438
  }
1464
- } catch (err) {
1465
- this.logger.warn("Failed to resolve MDC rules", { error: err.message });
1466
1439
  }
1467
- return null;
1468
- }
1469
- async syncResultToIssue(ctx, displayId) {
1470
- try {
1471
- const enabled = this.hooks.isNoteSyncEnabled();
1472
- const resultFiles = this.getResultFiles(ctx);
1473
- if (!enabled || resultFiles.length === 0) {
1474
- await this.notifyComment(
1475
- issueProgressComment(this.phaseName, "completed")
1476
- );
1477
- return;
1440
+ if (this.triggers && this.triggers.length > 0) {
1441
+ for (const trigger of this.triggers) {
1442
+ if (matched.has(trigger.filename)) continue;
1443
+ const isTriggered = trigger.keywords.some((kw) => lowerText.includes(kw.toLowerCase()));
1444
+ if (!isTriggered) continue;
1445
+ const rule = this.rules.find((r) => r.filename === trigger.filename);
1446
+ if (rule) {
1447
+ matched.set(rule.filename, rule);
1448
+ }
1478
1449
  }
1479
- const baseUrl = this.config.issueNoteSync.webBaseUrl.replace(/\/$/, "");
1480
- const phaseLabel = t(`phase.${this.phaseName}`) || this.phaseName;
1481
- const dashboardUrl = `${baseUrl}/?issue=${displayId}`;
1482
- for (const file of resultFiles) {
1483
- const content = this.readResultFile(displayId, file.filename);
1484
- if (!content) continue;
1485
- const summary = truncateToSummary(content);
1486
- const docUrl = `${baseUrl}/doc/${displayId}/${file.filename}`;
1487
- const comment = buildNoteSyncComment(
1488
- this.phaseName,
1489
- file.label || phaseLabel,
1490
- docUrl,
1491
- dashboardUrl,
1492
- summary
1493
- );
1494
- await this.notifyComment(comment);
1495
- this.logger.info("Result synced to issue", { issueIid: displayId, file: file.filename });
1450
+ } else {
1451
+ for (const rule of this.rules) {
1452
+ if (matched.has(rule.filename)) continue;
1453
+ if (!rule.description) continue;
1454
+ const descWords = rule.description.split(/[\s,,;;、/|]+/).filter((w) => w.length >= 2);
1455
+ const isMatched = descWords.some((word) => lowerText.includes(word.toLowerCase()));
1456
+ if (isMatched) {
1457
+ matched.set(rule.filename, rule);
1458
+ }
1496
1459
  }
1497
- } catch (err) {
1498
- this.logger.warn("Failed to sync result to issue", { error: err.message });
1499
- await this.notifyComment(
1500
- issueProgressComment(this.phaseName, "completed")
1501
- );
1502
1460
  }
1461
+ return Array.from(matched.values());
1462
+ }
1463
+ formatForPrompt(rules) {
1464
+ if (rules.length === 0) return "";
1465
+ return rules.map((rule) => {
1466
+ const header = rule.description ? `### ${rule.description} (${rule.filename})` : `### ${rule.filename}`;
1467
+ return `${header}
1468
+
1469
+ ${rule.content}`;
1470
+ }).join("\n\n---\n\n");
1471
+ }
1472
+ };
1473
+
1474
+ // src/notesync/NoteSyncSettings.ts
1475
+ var noteSyncOverride;
1476
+ function getNoteSyncEnabled(cfg) {
1477
+ return noteSyncOverride ?? cfg.issueNoteSync.enabled;
1478
+ }
1479
+ function setNoteSyncOverride(value) {
1480
+ noteSyncOverride = value;
1481
+ }
1482
+ function isNoteSyncEnabledForIssue(issueIid, tracker, cfg) {
1483
+ const record = tracker.get(issueIid);
1484
+ if (record?.issueNoteSyncEnabled !== void 0) return record.issueNoteSyncEnabled;
1485
+ return getNoteSyncEnabled(cfg);
1486
+ }
1487
+ var SUMMARY_MAX_LENGTH = 500;
1488
+ function truncateToSummary(content) {
1489
+ if (content.length <= SUMMARY_MAX_LENGTH) return content;
1490
+ const cut = content.slice(0, SUMMARY_MAX_LENGTH);
1491
+ const lastNewline = cut.lastIndexOf("\n\n");
1492
+ const boundary = lastNewline > SUMMARY_MAX_LENGTH * 0.3 ? lastNewline : cut.lastIndexOf("\n");
1493
+ const summary = boundary > SUMMARY_MAX_LENGTH * 0.3 ? cut.slice(0, boundary) : cut;
1494
+ return summary + "\n\n...";
1495
+ }
1496
+ function buildNoteSyncComment(phaseName, phaseLabel, docUrl, dashboardUrl, summary) {
1497
+ const emoji = {
1498
+ analysis: "\u{1F50D}",
1499
+ design: "\u{1F4D0}",
1500
+ implement: "\u{1F4BB}",
1501
+ verify: "\u2705",
1502
+ plan: "\u{1F4CB}",
1503
+ review: "\u{1F440}",
1504
+ build: "\u{1F528}"
1505
+ };
1506
+ const icon = emoji[phaseName] || "\u{1F4CB}";
1507
+ return [
1508
+ t("notesync.phaseCompleted", { icon, label: phaseLabel }),
1509
+ "",
1510
+ summary,
1511
+ "",
1512
+ "---",
1513
+ t("notesync.viewDoc", { label: phaseLabel, url: docUrl }),
1514
+ t("notesync.viewDashboard", { url: dashboardUrl })
1515
+ ].join("\n");
1516
+ }
1517
+
1518
+ // src/phases/BasePhase.ts
1519
+ var BasePhase = class _BasePhase {
1520
+ static MIN_ARTIFACT_BYTES = 50;
1521
+ aiRunner;
1522
+ git;
1523
+ plan;
1524
+ config;
1525
+ logger;
1526
+ lifecycle;
1527
+ hooks;
1528
+ /** 多仓模式下所有 repo 的 GitOperations(repo name → GitOperations) */
1529
+ wtGitMap;
1530
+ get startState() {
1531
+ const states = this.lifecycle.getPhaseStates(this.phaseName);
1532
+ if (!states) throw new Error(`Phase "${this.phaseName}" not found in lifecycle manager`);
1533
+ return states.startState;
1534
+ }
1535
+ get doneState() {
1536
+ const states = this.lifecycle.getPhaseStates(this.phaseName);
1537
+ if (!states) throw new Error(`Phase "${this.phaseName}" not found in lifecycle manager`);
1538
+ return states.doneState;
1539
+ }
1540
+ constructor(aiRunner, git, plan, config, lifecycle, hooks) {
1541
+ this.aiRunner = aiRunner;
1542
+ this.git = git;
1543
+ this.plan = plan;
1544
+ this.config = config;
1545
+ this.lifecycle = lifecycle;
1546
+ this.hooks = hooks;
1547
+ this.logger = logger.child(this.constructor.name);
1548
+ }
1549
+ /** 注入多仓库 GitOperations map(由编排器在创建 phase 后调用) */
1550
+ setWtGitMap(map) {
1551
+ this.wtGitMap = map;
1552
+ }
1553
+ getRunMode() {
1554
+ return void 0;
1503
1555
  }
1504
- readResultFile(issueIid, filename) {
1505
- const planDir = path4.join(this.plan.baseDir, ".claude-plan", `issue-${issueIid}`);
1506
- const filePath = path4.join(planDir, filename);
1507
- if (!fs3.existsSync(filePath)) return null;
1508
- try {
1509
- return fs3.readFileSync(filePath, "utf-8");
1510
- } catch {
1511
- return null;
1512
- }
1556
+ getResultFiles(_ctx) {
1557
+ return [];
1513
1558
  }
1514
- async validatePhaseOutput(ctx, displayId) {
1515
- const resultFiles = this.getResultFiles(ctx);
1516
- if (resultFiles.length === 0) return;
1517
- const planDir = path4.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
1518
- const missing = [];
1519
- for (const file of resultFiles) {
1520
- const filePath = path4.join(planDir, file.filename);
1521
- if (!fs3.existsSync(filePath)) {
1522
- missing.push(file.filename);
1523
- continue;
1524
- }
1525
- const stat = fs3.statSync(filePath);
1526
- if (stat.size < _BasePhase.MIN_ARTIFACT_BYTES) {
1527
- missing.push(`${file.filename} (${stat.size} bytes, \u5185\u5BB9\u4E0D\u8DB3)`);
1528
- }
1529
- }
1530
- if (missing.length > 0) {
1531
- const msg = `AI \u8FDB\u7A0B\u6210\u529F\u9000\u51FA\u4F46\u672A\u751F\u6210\u9884\u671F\u4EA7\u7269: ${missing.join(", ")}`;
1532
- this.logger.error(msg, { phase: this.phaseName, displayId });
1533
- throw new AIExecutionError(this.phaseName, msg, { output: "", exitCode: 0 });
1559
+ async execute(ctx) {
1560
+ const displayId = Number(ctx.demand.sourceRef.displayId);
1561
+ this.logger.info(`Phase ${this.phaseName} starting`, { issueIid: displayId });
1562
+ const resumeInfo = this.resolveResumeInfo();
1563
+ await this.notifyPhaseStart();
1564
+ this.plan.updatePhaseProgress(this.phaseName, "in_progress", void 0, {
1565
+ preserveSessionId: resumeInfo.resumable
1566
+ });
1567
+ await this.notifyComment(
1568
+ issueProgressComment(this.phaseName, "in_progress")
1569
+ );
1570
+ const phaseLabel = t(`phase.${this.phaseName}`) || this.phaseName;
1571
+ const systemMessage = resumeInfo.resumable ? t("basePhase.aiResuming", { label: phaseLabel }) : t("basePhase.aiStarting", { label: phaseLabel });
1572
+ eventBus.emitTyped("agent:output", {
1573
+ issueIid: displayId,
1574
+ phase: this.phaseName,
1575
+ event: { type: "system", content: systemMessage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1576
+ });
1577
+ let basePrompt = this.buildPrompt(ctx);
1578
+ const wsSection = buildWorkspaceSection(ctx.workspace);
1579
+ if (wsSection) {
1580
+ basePrompt = `${basePrompt}
1581
+
1582
+ ${wsSection}`;
1534
1583
  }
1535
- }
1536
- async commitPlanFiles(ctx, displayId) {
1537
- const commitMsg = `chore(auto): ${this.phaseName} phase completed for issue #${displayId}`;
1538
- if (ctx.workspace && ctx.workspace.repos.length > 1) {
1539
- for (const repo of ctx.workspace.repos) {
1540
- const repoGit = this.wtGitMap?.get(repo.name) ?? new GitOperations(repo.gitRootDir);
1541
- const branch = repo.branchPrefix ? `${repo.branchPrefix}-${displayId}` : ctx.branchName;
1542
- try {
1543
- if (await repoGit.hasChanges()) {
1544
- await repoGit.add(["."]);
1545
- await repoGit.commit(commitMsg);
1546
- await repoGit.push(branch);
1547
- this.logger.info("Committed changes for repo", { repo: repo.name, branch });
1548
- }
1549
- } catch (err) {
1550
- this.logger.warn("Failed to commit/push for repo", {
1551
- repo: repo.name,
1552
- error: err.message
1553
- });
1554
- }
1555
- }
1584
+ const matchedRulesText = await this.resolveRules(ctx);
1585
+ const fullPrompt = matchedRulesText ? `${basePrompt}
1586
+
1587
+ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1588
+ const resumePrompt = t("basePhase.resumePrompt");
1589
+ const onInputRequired = this.buildInputRequiredHandler(displayId);
1590
+ let result;
1591
+ if (resumeInfo.resumable) {
1592
+ this.logger.info("Attempting session resume", {
1593
+ issueIid: displayId,
1594
+ phase: this.phaseName,
1595
+ sessionId: resumeInfo.sessionId
1596
+ });
1597
+ result = await this.runWithResumeFallback(
1598
+ displayId,
1599
+ resumeInfo.sessionId,
1600
+ resumePrompt,
1601
+ fullPrompt,
1602
+ onInputRequired,
1603
+ ctx
1604
+ );
1556
1605
  } else {
1557
- if (await this.git.hasChanges()) {
1558
- await this.git.add(["."]);
1559
- await this.git.commit(commitMsg);
1560
- await this.git.push(ctx.branchName);
1561
- }
1606
+ result = await this.runAI(displayId, fullPrompt, onInputRequired, void 0, ctx);
1607
+ }
1608
+ if (!result.success) {
1609
+ this.persistSessionId(result.sessionId);
1610
+ const failReason = (result.errorMessage || result.output).slice(0, 500);
1611
+ const shortReason = (result.errorMessage || result.output).slice(0, 200);
1612
+ this.plan.updatePhaseProgress(this.phaseName, "failed", failReason);
1613
+ await this.notifyPhaseFailed(failReason);
1614
+ await this.notifyComment(
1615
+ issueProgressComment(this.phaseName, "failed", t("basePhase.error", { message: shortReason }))
1616
+ );
1617
+ throw new AIExecutionError(this.phaseName, `Phase ${this.phaseName} failed: ${shortReason}`, {
1618
+ output: result.output,
1619
+ exitCode: result.exitCode,
1620
+ isRetryable: this.classifyRetryable(result)
1621
+ });
1562
1622
  }
1623
+ this.persistSessionId(result.sessionId);
1624
+ await this.validatePhaseOutput(ctx, displayId);
1625
+ await this.notifyPhaseDone();
1626
+ this.plan.updatePhaseProgress(this.phaseName, "completed");
1627
+ await this.commitPlanFiles(ctx, displayId);
1628
+ await this.syncResultToIssue(ctx, displayId);
1629
+ this.logger.info(`Phase ${this.phaseName} completed`, { issueIid: displayId });
1630
+ return result;
1563
1631
  }
1564
- };
1565
-
1566
- // src/phases/VerifyPhase.ts
1567
- import fs4 from "fs";
1568
- import path5 from "path";
1569
-
1570
- // src/verify/VerifyReportParser.ts
1571
- var LINT_FAIL_RE = /\*{0,2}Lint\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1572
- var BUILD_FAIL_RE = /\*{0,2}Build\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1573
- var TEST_FAIL_RE = /\*{0,2}Test\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1574
- var SUMMARY_FAIL_RE = /\*{0,2}(?:总结|Summary)\*{0,2}\s*[::].*(?:验证失败|verification\s+failed|failed|失败)/i;
1575
- var TODOLIST_STATS_RE = /\*{0,2}(?:Todolist|Todo)\s*(?:检查|check)?\*{0,2}\s*[::]\s*(\d+)\s*[//]\s*(\d+)/i;
1576
- var VerifyReportParser = class {
1577
- /**
1578
- * 解析验证报告内容。
1579
- *
1580
- * @param reportContent 验证报告的完整 Markdown 文本
1581
- */
1582
- parse(reportContent) {
1583
- const lintPassed = !LINT_FAIL_RE.test(reportContent);
1584
- const buildPassed = !BUILD_FAIL_RE.test(reportContent);
1585
- const testPassed = !TEST_FAIL_RE.test(reportContent);
1586
- const todolistStats = this.parseTodolistStats(reportContent);
1587
- const todolistComplete = todolistStats ? todolistStats.total === 0 || todolistStats.completed === todolistStats.total : true;
1588
- const failureReasons = [];
1589
- if (!lintPassed) failureReasons.push("Lint \u68C0\u67E5\u5931\u8D25");
1590
- if (!buildPassed) failureReasons.push("Build \u7F16\u8BD1\u5931\u8D25");
1591
- if (!testPassed) failureReasons.push("\u6D4B\u8BD5\u672A\u901A\u8FC7");
1592
- if (!todolistComplete) {
1593
- const statsText = todolistStats ? `(${todolistStats.completed}/${todolistStats.total})` : "";
1594
- failureReasons.push(`Todolist \u672A\u5168\u90E8\u5B8C\u6210${statsText}`);
1632
+ // ── Session resume helpers ──
1633
+ resolveResumeInfo() {
1634
+ if (this.isAcpMode()) {
1635
+ return { resumable: false };
1595
1636
  }
1596
- const summaryFailed = SUMMARY_FAIL_RE.test(reportContent);
1597
- if (failureReasons.length === 0 && summaryFailed) {
1598
- failureReasons.push("\u9A8C\u8BC1\u62A5\u544A\u603B\u7ED3\u5224\u5B9A\u4E3A\u5931\u8D25");
1637
+ const previousSessionId = this.plan.getPhaseSessionId(this.phaseName);
1638
+ if (!previousSessionId) {
1639
+ return { resumable: false };
1599
1640
  }
1600
- const passed = failureReasons.length === 0;
1601
- return {
1602
- passed,
1603
- lintPassed,
1604
- buildPassed,
1605
- testPassed,
1606
- todolistComplete,
1607
- todolistStats: todolistStats ?? void 0,
1608
- failureReasons,
1609
- rawReport: reportContent
1610
- };
1641
+ const progress = this.plan.readProgress();
1642
+ const phaseStatus = progress?.phases[this.phaseName]?.status;
1643
+ if (phaseStatus !== "failed" && phaseStatus !== "in_progress") {
1644
+ return { resumable: false };
1645
+ }
1646
+ return { resumable: true, sessionId: previousSessionId };
1611
1647
  }
1612
- /**
1613
- * plan 文件中解析 Todolist 完成度。
1614
- * 统计 `- [x]` (已完成) 和 `- [ ]` (未完成) 的数量。
1615
- */
1616
- parseTodolistFromPlan(planContent) {
1617
- const completedRe = /^[ \t]*-\s+\[x\]/gim;
1618
- const uncompletedRe = /^[ \t]*-\s+\[\s\]/gm;
1619
- const completedMatches = planContent.match(completedRe);
1620
- const uncompletedMatches = planContent.match(uncompletedRe);
1621
- const completed = completedMatches?.length ?? 0;
1622
- const uncompleted = uncompletedMatches?.length ?? 0;
1623
- return {
1624
- completed,
1625
- total: completed + uncompleted
1626
- };
1648
+ isAcpMode() {
1649
+ return this.config.ai.mode === "codebuddy-acp";
1627
1650
  }
1628
- /**
1629
- * 从报告中提取 Todolist 统计数据。
1630
- * 格式: "Todolist 检查: X/Y 项完成"
1631
- */
1632
- parseTodolistStats(reportContent) {
1633
- const match = reportContent.match(TODOLIST_STATS_RE);
1634
- if (match) {
1635
- return {
1636
- completed: parseInt(match[1], 10),
1637
- total: parseInt(match[2], 10)
1638
- };
1651
+ resolveAIWorkDir(ctx) {
1652
+ if (ctx?.workspace && ctx.workspace.repos.length > 1) {
1653
+ return ctx.workspace.workspaceRoot;
1639
1654
  }
1640
- return null;
1641
- }
1642
- };
1643
-
1644
- // src/phases/VerifyPhase.ts
1645
- var VerifyPhase = class extends BasePhase {
1646
- phaseName = "verify";
1647
- reportParser = new VerifyReportParser();
1648
- getResultFiles(ctx) {
1649
- const filename = ctx?.pipelineMode === "plan-mode" ? "02-verify-report.md" : "04-verify-report.md";
1650
- return [{ filename, label: "\u9A8C\u8BC1\u62A5\u544A" }];
1655
+ return this.plan.baseDir;
1651
1656
  }
1652
- async execute(ctx) {
1653
- const result = await super.execute(ctx);
1654
- if (result.success) {
1655
- const report = this.readVerifyReport(ctx);
1656
- if (report) {
1657
- const parsed = this.reportParser.parse(report);
1658
- if (this.config.verifyFixLoop.todolistCheckEnabled && !parsed.todolistStats) {
1659
- const planContent = this.readPlanFile(ctx);
1660
- if (planContent) {
1661
- const todoStats = this.reportParser.parseTodolistFromPlan(planContent);
1662
- if (todoStats.total > 0) {
1663
- parsed.todolistStats = todoStats;
1664
- parsed.todolistComplete = todoStats.completed === todoStats.total;
1665
- if (!parsed.todolistComplete) {
1666
- parsed.failureReasons.push(
1667
- `Todolist \u672A\u5168\u90E8\u5B8C\u6210(${todoStats.completed}/${todoStats.total})`
1668
- );
1669
- parsed.passed = false;
1670
- }
1671
- }
1657
+ async runAI(displayId, prompt, onInputRequired, options, ctx) {
1658
+ let capturedSessionId;
1659
+ const result = await this.aiRunner.run({
1660
+ prompt,
1661
+ workDir: this.resolveAIWorkDir(ctx),
1662
+ timeoutMs: this.config.ai.phaseTimeoutMs,
1663
+ idleTimeoutMs: this.config.ai.idleTimeoutMs,
1664
+ mode: this.getRunMode(),
1665
+ sessionId: options?.sessionId,
1666
+ continueSession: options?.continueSession,
1667
+ onStreamEvent: (event) => {
1668
+ if (!capturedSessionId && event.type !== "raw") {
1669
+ const content = event.content;
1670
+ if (content?.session_id && typeof content.session_id === "string") {
1671
+ capturedSessionId = content.session_id;
1672
+ this.persistSessionId(capturedSessionId);
1672
1673
  }
1673
1674
  }
1674
- this.logger.info("Verify report parsed", {
1675
- passed: parsed.passed,
1676
- lintPassed: parsed.lintPassed,
1677
- buildPassed: parsed.buildPassed,
1678
- testPassed: parsed.testPassed,
1679
- todolistComplete: parsed.todolistComplete,
1680
- todolistStats: parsed.todolistStats,
1681
- failureCount: parsed.failureReasons.length
1675
+ eventBus.emitTyped("agent:output", {
1676
+ issueIid: displayId,
1677
+ phase: this.phaseName,
1678
+ event
1682
1679
  });
1683
- return { ...result, verifyReport: parsed };
1684
- }
1680
+ },
1681
+ onInputRequired
1682
+ });
1683
+ if (result.sessionId) {
1684
+ this.persistSessionId(result.sessionId);
1685
1685
  }
1686
1686
  return result;
1687
1687
  }
1688
- buildPrompt(ctx) {
1689
- const pc = demandToPromptContext(ctx.demand);
1690
- const promptCtx = {
1691
- issueTitle: pc.title,
1692
- issueDescription: pc.description,
1693
- issueIid: Number(pc.displayId),
1694
- workspace: ctx.workspace
1695
- };
1696
- return ctx.pipelineMode === "plan-mode" ? planModeVerifyPrompt(promptCtx) : verifyPrompt(promptCtx);
1697
- }
1698
- readVerifyReport(ctx) {
1699
- const files = this.getResultFiles(ctx);
1700
- if (files.length === 0) return null;
1701
- const displayId = Number(ctx.demand.sourceRef.displayId);
1702
- const planDir = path5.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
1703
- const filePath = path5.join(planDir, files[0].filename);
1704
- if (!fs4.existsSync(filePath)) return null;
1705
- try {
1706
- return fs4.readFileSync(filePath, "utf-8");
1707
- } catch {
1708
- return null;
1709
- }
1710
- }
1711
- readPlanFile(ctx) {
1712
- const displayId = Number(ctx.demand.sourceRef.displayId);
1713
- const planDir = path5.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
1714
- const filePath = path5.join(planDir, "01-plan.md");
1715
- if (!fs4.existsSync(filePath)) return null;
1716
- try {
1717
- return fs4.readFileSync(filePath, "utf-8");
1718
- } catch {
1719
- return null;
1688
+ async runWithResumeFallback(displayId, sessionId, resumePrompt, fullPrompt, onInputRequired, ctx) {
1689
+ const result = await this.runAI(displayId, resumePrompt, onInputRequired, {
1690
+ sessionId,
1691
+ continueSession: true
1692
+ }, ctx);
1693
+ if (!result.success && this.isResumeFailure(result)) {
1694
+ this.logger.warn(t("basePhase.resumeFallback"), {
1695
+ issueIid: displayId,
1696
+ phase: this.phaseName,
1697
+ exitCode: result.exitCode
1698
+ });
1699
+ eventBus.emitTyped("agent:output", {
1700
+ issueIid: displayId,
1701
+ phase: this.phaseName,
1702
+ event: {
1703
+ type: "system",
1704
+ content: t("basePhase.resumeFallback"),
1705
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1706
+ }
1707
+ });
1708
+ return this.runAI(displayId, fullPrompt, onInputRequired, void 0, ctx);
1720
1709
  }
1710
+ return result;
1721
1711
  }
1722
- };
1723
-
1724
- // src/phases/PlanPhase.ts
1725
- var PlanPhase = class extends BasePhase {
1726
- phaseName = "plan";
1727
- getResultFiles() {
1728
- return [{ filename: "01-plan.md", label: "\u5B9E\u65BD\u8BA1\u5212" }];
1729
- }
1730
- getRunMode() {
1731
- return "plan";
1712
+ /**
1713
+ * 判断 AI 执行失败是否为永久性错误(不可重试)。
1714
+ * 模型不存在、API key 无效等配置问题重试不会成功。
1715
+ */
1716
+ classifyRetryable(result) {
1717
+ const msg = (result.errorMessage ?? result.output ?? "").toLowerCase();
1718
+ const permanentPatterns = [
1719
+ /model\b.*\b(?:not found|not supported|unavailable|service info not found)/,
1720
+ /invalid.?api.?key/,
1721
+ /authentication.*(?:failed|denied|error)/,
1722
+ /permission.?denied/,
1723
+ /billing/,
1724
+ /quota.*exceeded/
1725
+ ];
1726
+ return !permanentPatterns.some((p) => p.test(msg));
1732
1727
  }
1733
- buildPrompt(ctx) {
1734
- const pc = demandToPromptContext(ctx.demand);
1735
- const history = this.plan.readReviewHistory();
1736
- const promptCtx = {
1737
- issueTitle: pc.title,
1738
- issueDescription: pc.description,
1739
- issueIid: Number(pc.displayId),
1740
- supplementText: pc.supplementText || void 0,
1741
- workspace: ctx.workspace
1742
- };
1743
- let basePrompt;
1744
- if (history.length > 0) {
1745
- basePrompt = rePlanPrompt(promptCtx, history);
1746
- } else {
1747
- basePrompt = planPrompt(promptCtx);
1728
+ /**
1729
+ * Heuristic: a resume failure is typically an immediate process exit
1730
+ * (exit code != 0, empty output) caused by an invalid/expired session ID.
1731
+ */
1732
+ isResumeFailure(result) {
1733
+ if (result.success) return false;
1734
+ const msg = (result.errorMessage ?? "").toLowerCase();
1735
+ if (msg.includes("session") || msg.includes("resume") || msg.includes("session_id")) {
1736
+ return true;
1748
1737
  }
1749
- const caps = getRunnerCapabilities(this.config.ai.mode);
1750
- if (!caps?.nativePlanMode) {
1751
- basePrompt = `${t("prompt.planModeFallback")}
1752
-
1753
- ${basePrompt}`;
1738
+ if (result.output.length === 0 && result.exitCode !== null && result.exitCode !== 0) {
1739
+ const isConfigError = msg.includes("model") || msg.includes("api key") || msg.includes("authentication");
1740
+ return !isConfigError;
1754
1741
  }
1755
- return basePrompt;
1742
+ return false;
1756
1743
  }
1757
- };
1758
-
1759
- // src/phases/BuildPhase.ts
1760
- var BuildPhase = class extends BasePhase {
1761
- phaseName = "build";
1762
- async validatePhaseOutput(ctx, _displayId) {
1763
- let hasAnyChanges = false;
1764
- if (ctx.workspace && ctx.workspace.repos.length > 1) {
1765
- for (const repo of ctx.workspace.repos) {
1766
- const repoGit = new GitOperations(repo.gitRootDir);
1767
- if (await repoGit.hasChanges()) {
1768
- hasAnyChanges = true;
1769
- break;
1770
- }
1771
- }
1772
- } else {
1773
- hasAnyChanges = await this.git.hasChanges();
1774
- }
1775
- if (!hasAnyChanges) {
1776
- const msg = "AI \u8FDB\u7A0B\u6210\u529F\u9000\u51FA\u4F46\u672A\u4EA7\u751F\u4EFB\u4F55\u4EE3\u7801\u53D8\u66F4";
1777
- this.logger.error(msg, { phase: this.phaseName });
1778
- throw new AIExecutionError(this.phaseName, msg, { output: "", exitCode: 0 });
1744
+ persistSessionId(sessionId) {
1745
+ if (sessionId) {
1746
+ this.plan.updatePhaseSessionId(this.phaseName, sessionId);
1779
1747
  }
1780
1748
  }
1781
- buildPrompt(ctx) {
1782
- const pc = demandToPromptContext(ctx.demand);
1783
- const base = buildPrompt({
1784
- issueTitle: pc.title,
1785
- issueDescription: pc.description,
1786
- issueIid: Number(pc.displayId),
1787
- workspace: ctx.workspace
1788
- });
1789
- if (ctx.fixContext) {
1790
- return base + t("prompt.buildFixSuffix", {
1791
- iteration: ctx.fixContext.iteration,
1792
- failures: ctx.fixContext.verifyFailures.map((f, i) => `${i + 1}. ${f}`).join("\n"),
1793
- rawReport: ctx.fixContext.rawReport.slice(0, 2e3)
1794
- });
1795
- }
1796
- return base;
1749
+ // ── Hook dispatch methods ──
1750
+ async notifyPhaseStart() {
1751
+ await this.hooks.onPhaseStart(this.phaseName);
1797
1752
  }
1798
- };
1799
-
1800
- // src/phases/ReleasePhase.ts
1801
- import fs6 from "fs";
1802
- import path7 from "path";
1803
-
1804
- // src/release/ReleaseDetectCache.ts
1805
- import fs5 from "fs";
1806
- import path6 from "path";
1807
- import { createHash } from "crypto";
1808
- var logger7 = logger.child("ReleaseDetectCache");
1809
- function hashProjectPath(projectPath) {
1810
- return createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
1811
- }
1812
- var ReleaseDetectCache = class {
1813
- cacheDir;
1814
- constructor(dataDir) {
1815
- this.cacheDir = path6.join(dataDir, "release-detect");
1753
+ async notifyPhaseDone() {
1754
+ await this.hooks.onPhaseDone(this.phaseName);
1816
1755
  }
1817
- filePath(projectPath) {
1818
- return path6.join(this.cacheDir, `${hashProjectPath(projectPath)}.json`);
1756
+ async notifyPhaseFailed(error) {
1757
+ await this.hooks.onPhaseFailed(this.phaseName, error);
1758
+ }
1759
+ async notifyComment(message) {
1760
+ try {
1761
+ await this.hooks.onComment(message);
1762
+ } catch (err) {
1763
+ this.logger.warn("Hook onComment failed", { error: err.message });
1764
+ }
1819
1765
  }
1820
1766
  /**
1821
- * 读取缓存。返回 null 如果不存在、已过期或校验失败。
1767
+ * Build an onInputRequired handler for ACP permission delegation.
1768
+ * Only returns a handler when codebuddyAcpAutoApprove is false,
1769
+ * enabling review gate delegation for permission requests.
1822
1770
  */
1823
- get(projectPath, ttlMs) {
1824
- const fp = this.filePath(projectPath);
1771
+ buildInputRequiredHandler(displayId) {
1772
+ if (this.config.ai.codebuddyAcpAutoApprove !== false) return void 0;
1773
+ return (request) => {
1774
+ if (request.type !== "plan-approval") {
1775
+ return Promise.resolve("allow");
1776
+ }
1777
+ this.logger.info("ACP plan-approval requested, delegating to review gate", {
1778
+ issueIid: displayId,
1779
+ phase: this.phaseName
1780
+ });
1781
+ eventBus.emitTyped("review:requested", { issueIid: displayId });
1782
+ return new Promise((resolve) => {
1783
+ const onApproved = (payload) => {
1784
+ const data = payload.data;
1785
+ if (data.issueIid !== displayId) return;
1786
+ cleanup();
1787
+ this.logger.info("ACP plan-approval approved via review gate", { issueIid: displayId });
1788
+ resolve("allow");
1789
+ };
1790
+ const onRejected = (payload) => {
1791
+ const data = payload.data;
1792
+ if (data.issueIid !== displayId) return;
1793
+ cleanup();
1794
+ this.logger.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
1795
+ resolve("reject");
1796
+ };
1797
+ const cleanup = () => {
1798
+ eventBus.removeListener("review:approved", onApproved);
1799
+ eventBus.removeListener("review:rejected", onRejected);
1800
+ };
1801
+ eventBus.on("review:approved", onApproved);
1802
+ eventBus.on("review:rejected", onRejected);
1803
+ });
1804
+ };
1805
+ }
1806
+ async resolveRules(ctx) {
1825
1807
  try {
1826
- if (!fs5.existsSync(fp)) return null;
1827
- const raw = fs5.readFileSync(fp, "utf-8");
1828
- const data = JSON.parse(raw);
1829
- if (data.projectPath !== projectPath) {
1830
- logger7.warn("Cache projectPath mismatch, ignoring", { expected: projectPath, got: data.projectPath });
1831
- return null;
1808
+ const knowledge = getProjectKnowledge();
1809
+ const resolver = new RuleResolver(knowledge?.ruleTriggers);
1810
+ const context = `${ctx.demand.title} ${ctx.demand.description} ${ctx.demand.supplement ? JSON.stringify(ctx.demand.supplement) : ""}`;
1811
+ if (ctx.workspace && ctx.workspace.repos.length > 1) {
1812
+ for (const repo of ctx.workspace.repos) {
1813
+ const rulesDir = path4.join(repo.gitRootDir, ".cursor", "rules");
1814
+ try {
1815
+ await resolver.loadRules(rulesDir);
1816
+ } catch {
1817
+ }
1818
+ }
1819
+ } else {
1820
+ const rulesDir = path4.join(this.plan.baseDir, ".cursor", "rules");
1821
+ await resolver.loadRules(rulesDir);
1832
1822
  }
1833
- const age = Date.now() - new Date(data.detectedAt).getTime();
1834
- if (age > ttlMs) {
1835
- logger7.debug("Cache expired", { projectPath, ageMs: age, ttlMs });
1836
- return null;
1823
+ const matched = resolver.matchRules(context);
1824
+ if (matched.length > 0) {
1825
+ this.logger.info(`Matched ${matched.length} MDC rules`, {
1826
+ rules: matched.map((r) => r.filename)
1827
+ });
1828
+ return resolver.formatForPrompt(matched);
1837
1829
  }
1838
- return data;
1839
1830
  } catch (err) {
1840
- logger7.warn("Failed to read release detect cache", { path: fp, error: err.message });
1841
- return null;
1831
+ this.logger.warn("Failed to resolve MDC rules", { error: err.message });
1842
1832
  }
1833
+ return null;
1843
1834
  }
1844
- /**
1845
- * 写入缓存。
1846
- */
1847
- set(result) {
1848
- const fp = this.filePath(result.projectPath);
1835
+ async syncResultToIssue(ctx, displayId) {
1849
1836
  try {
1850
- if (!fs5.existsSync(this.cacheDir)) {
1851
- fs5.mkdirSync(this.cacheDir, { recursive: true });
1837
+ const enabled = this.hooks.isNoteSyncEnabled();
1838
+ const resultFiles = this.getResultFiles(ctx);
1839
+ if (!enabled || resultFiles.length === 0) {
1840
+ await this.notifyComment(
1841
+ issueProgressComment(this.phaseName, "completed")
1842
+ );
1843
+ return;
1852
1844
  }
1853
- fs5.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
1854
- logger7.debug("Release detect cache written", { projectPath: result.projectPath, path: fp });
1855
- } catch (err) {
1856
- logger7.warn("Failed to write release detect cache", { path: fp, error: err.message });
1857
- }
1858
- }
1859
- /**
1860
- * 手动失效缓存。
1861
- */
1862
- invalidate(projectPath) {
1863
- const fp = this.filePath(projectPath);
1864
- try {
1865
- if (fs5.existsSync(fp)) {
1866
- fs5.unlinkSync(fp);
1867
- logger7.info("Release detect cache invalidated", { projectPath });
1868
- return true;
1845
+ const baseUrl = this.config.issueNoteSync.webBaseUrl.replace(/\/$/, "");
1846
+ const phaseLabel = t(`phase.${this.phaseName}`) || this.phaseName;
1847
+ const dashboardUrl = `${baseUrl}/?issue=${displayId}`;
1848
+ for (const file of resultFiles) {
1849
+ const content = this.readResultFile(displayId, file.filename);
1850
+ if (!content) continue;
1851
+ const summary = truncateToSummary(content);
1852
+ const docUrl = `${baseUrl}/doc/${displayId}/${file.filename}`;
1853
+ const comment = buildNoteSyncComment(
1854
+ this.phaseName,
1855
+ file.label || phaseLabel,
1856
+ docUrl,
1857
+ dashboardUrl,
1858
+ summary
1859
+ );
1860
+ await this.notifyComment(comment);
1861
+ this.logger.info("Result synced to issue", { issueIid: displayId, file: file.filename });
1869
1862
  }
1870
- return false;
1871
1863
  } catch (err) {
1872
- logger7.warn("Failed to invalidate release detect cache", { path: fp, error: err.message });
1873
- return false;
1864
+ this.logger.warn("Failed to sync result to issue", { error: err.message });
1865
+ await this.notifyComment(
1866
+ issueProgressComment(this.phaseName, "completed")
1867
+ );
1874
1868
  }
1875
1869
  }
1876
- };
1877
-
1878
- // src/prompts/release-templates.ts
1879
- function releaseDetectPrompt(ctx) {
1880
- const pd = `.claude-plan/issue-${ctx.issueIid}`;
1881
- return `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684 DevOps \u5206\u6790\u5E08\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u63A2\u7D22\u5F53\u524D\u9879\u76EE\uFF0C\u67E5\u627E\u6240\u6709\u4E0E**\u53D1\u5E03/\u90E8\u7F72**\u76F8\u5173\u7684\u673A\u5236\u3002
1882
-
1883
- ## \u63A2\u7D22\u8303\u56F4
1884
-
1885
- \u8BF7\u6309\u4F18\u5148\u7EA7\u4F9D\u6B21\u68C0\u67E5\u4EE5\u4E0B\u4F4D\u7F6E\uFF1A
1886
-
1887
- 1. **Skill \u6587\u4EF6**
1888
- - \`.cursor/skills/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy/publish \u76F8\u5173\u7684 skill
1889
- - \u9879\u76EE\u6839\u76EE\u5F55\u7684 \`SKILL.md\` \u6216 \`release.md\`
1890
-
1891
- 2. **Rule \u6587\u4EF6**
1892
- - \`.cursor/rules/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy \u76F8\u5173\u7684 \`.mdc\` \u89C4\u5219
1893
-
1894
- 3. **CI/CD \u914D\u7F6E**
1895
- - \`.gitlab-ci.yml\` / \`Jenkinsfile\` / \`.github/workflows/\`
1896
- - \u67E5\u627E release/deploy/publish \u76F8\u5173\u7684 stage/job/workflow
1897
-
1898
- 4. **Package Manager**
1899
- - \`package.json\` \u4E2D\u7684 \`scripts\` \u5B57\u6BB5\uFF1Apublish\u3001release\u3001deploy \u7B49
1900
- - \`Makefile\` \u4E2D\u7684 release/deploy target
1901
-
1902
- 5. **\u811A\u672C\u4E0E\u6587\u6863**
1903
- - \`scripts/release.*\`\u3001\`scripts/deploy.*\`\u3001\`scripts/publish.*\`
1904
- - \`RELEASE.md\`\u3001\`CHANGELOG.md\`\u3001\`CONTRIBUTING.md\` \u4E2D\u7684\u53D1\u5E03\u8BF4\u660E
1905
- - \`Dockerfile\`\u3001\`docker-compose*.yml\`
1906
-
1907
- ## \u8F93\u51FA\u8981\u6C42
1908
-
1909
- \u5C06\u68C0\u6D4B\u7ED3\u679C\u4EE5 **\u4E25\u683C JSON** \u683C\u5F0F\u5199\u5165 \`${pd}/03-release-detect.json\`\uFF1A
1910
-
1911
- \`\`\`json
1912
- {
1913
- "hasReleaseCapability": true,
1914
- "releaseMethod": "npm-publish",
1915
- "artifacts": ["package.json", ".gitlab-ci.yml"],
1916
- "instructions": "\u8FD0\u884C npm publish \u53D1\u5E03\u5230 npm registry..."
1917
- }
1918
- \`\`\`
1919
-
1920
- \u5B57\u6BB5\u8BF4\u660E\uFF1A
1921
- - \`hasReleaseCapability\`: \u662F\u5426\u627E\u5230\u53D1\u5E03\u673A\u5236\uFF08boolean\uFF09
1922
- - \`releaseMethod\`: \u53D1\u5E03\u65B9\u5F0F\uFF0C\u53EF\u9009\u503C\uFF1A"npm-publish" | "ci-pipeline" | "makefile" | "custom-script" | "skill" | "docker" | \u5176\u4ED6\u63CF\u8FF0
1923
- - \`artifacts\`: \u53D1\u73B0\u7684\u53D1\u5E03\u76F8\u5173\u6587\u4EF6\u8DEF\u5F84\u6570\u7EC4
1924
- - \`instructions\`: \u4ECE\u6587\u4EF6\u4E2D\u63D0\u53D6\u7684\u53D1\u5E03\u6B65\u9AA4\u8BF4\u660E\u3002\u5982\u679C\u662F skill/rule \u6587\u4EF6\uFF0C\u8BF7\u5B8C\u6574\u590D\u5236\u5176\u5185\u5BB9
1925
-
1926
- **\u91CD\u8981**\uFF1A
1927
- - \u5982\u679C\u6CA1\u6709\u627E\u5230\u4EFB\u4F55\u53D1\u5E03\u673A\u5236\uFF0C\u5C06 \`hasReleaseCapability\` \u8BBE\u4E3A \`false\`\uFF0C\u5176\u4ED6\u5B57\u6BB5\u53EF\u7701\u7565
1928
- - \u5982\u679C\u627E\u5230\u4E86 skill \u6216 rule \u6587\u4EF6\uFF0C\u8BF7\u5728 \`instructions\` \u4E2D\u5B8C\u6574\u4FDD\u7559\u539F\u6587\u5185\u5BB9\uFF0C\u540E\u7EED\u53D1\u5E03\u6267\u884C\u5C06\u4F9D\u8D56\u5B83
1929
- - \u53EA\u8F93\u51FA JSON \u6587\u4EF6\uFF0C\u4E0D\u8981\u6267\u884C\u4EFB\u4F55\u53D1\u5E03\u64CD\u4F5C`;
1930
- }
1931
- function releaseExecPrompt(ctx, detectResult) {
1932
- const pd = `.claude-plan/issue-${ctx.issueIid}`;
1933
- const artifactList = detectResult.artifacts.length > 0 ? detectResult.artifacts.map((a) => `- \`${a}\``).join("\n") : "\uFF08\u65E0\uFF09";
1934
- const instructionSection = detectResult.instructions ? `
1935
- ## \u53D1\u5E03\u6307\u5F15\uFF08\u6765\u81EA\u9879\u76EE skill/rule/\u6587\u6863\uFF09
1936
-
1937
- ${detectResult.instructions}` : "";
1938
- return `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u53D1\u5E03\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6839\u636E\u4EE5\u4E0B\u68C0\u6D4B\u7ED3\u679C\uFF0C\u6267\u884C\u9879\u76EE\u7684\u53D1\u5E03\u6D41\u7A0B\u3002
1939
-
1940
- ## \u68C0\u6D4B\u7ED3\u679C
1941
-
1942
- - **\u53D1\u5E03\u65B9\u5F0F**: ${detectResult.releaseMethod ?? "\u672A\u77E5"}
1943
- - **\u76F8\u5173\u6587\u4EF6**:
1944
- ${artifactList}
1945
- ${instructionSection}
1946
-
1947
- ## \u4EFB\u52A1
1948
-
1949
- 1. \u9605\u8BFB\u4E0A\u8FF0\u53D1\u5E03\u6307\u5F15\u548C\u76F8\u5173\u6587\u4EF6
1950
- 2. \u6309\u7167\u9879\u76EE\u5B9A\u4E49\u7684\u53D1\u5E03\u6D41\u7A0B\u6267\u884C\u53D1\u5E03\u64CD\u4F5C
1951
- 3. \u5C06\u53D1\u5E03\u7ED3\u679C\u5199\u5165 \`${pd}/04-release-report.md\`
1952
-
1953
- ## \u53D1\u5E03\u62A5\u544A\u683C\u5F0F
1954
-
1955
- \`\`\`markdown
1956
- # \u53D1\u5E03\u62A5\u544A
1957
-
1958
- ## \u53D1\u5E03\u65B9\u5F0F
1959
- <\u4F7F\u7528\u7684\u53D1\u5E03\u65B9\u5F0F>
1960
-
1961
- ## \u6267\u884C\u6B65\u9AA4
1962
- <\u9010\u6B65\u8BB0\u5F55\u6267\u884C\u7684\u64CD\u4F5C>
1963
-
1964
- ## \u7ED3\u679C
1965
- <\u6210\u529F/\u5931\u8D25\u53CA\u8BE6\u60C5>
1966
- \`\`\`
1967
-
1968
- **\u91CD\u8981**\uFF1A
1969
- - \u4E25\u683C\u6309\u7167\u9879\u76EE\u81EA\u8EAB\u7684\u53D1\u5E03\u6D41\u7A0B\u64CD\u4F5C
1970
- - \u5982\u6709\u4EFB\u4F55\u9519\u8BEF\u6216\u5F02\u5E38\uFF0C\u8BB0\u5F55\u5728\u62A5\u544A\u4E2D
1971
- - \u4E0D\u8981\u8DF3\u8FC7\u4EFB\u4F55\u53D1\u5E03\u524D\u7684\u68C0\u67E5\u6B65\u9AA4`;
1972
- }
1973
-
1974
- // src/phases/ReleasePhase.ts
1975
- var logger8 = logger.child("ReleasePhase");
1976
- var DETECT_FILENAME = "05-release-detect.json";
1977
- var REPORT_FILENAME = "06-release-report.md";
1978
- var ReleasePhase = class extends BasePhase {
1979
- phaseName = "release";
1980
- get planDir() {
1981
- const displayId = Number(this.currentCtx?.demand.sourceRef.displayId ?? 0);
1982
- return path7.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
1983
- }
1984
- /** 暂存当前 ctx 供 planDir getter 使用 */
1985
- currentCtx;
1986
- /**
1987
- * 检测结果是否已存在(即是否处于执行模式)。
1988
- */
1989
- hasDetectionResult() {
1990
- return fs6.existsSync(path7.join(this.planDir, DETECT_FILENAME));
1991
- }
1992
- readDetectionFile() {
1993
- const fp = path7.join(this.planDir, DETECT_FILENAME);
1994
- if (!fs6.existsSync(fp)) return null;
1870
+ readResultFile(issueIid, filename) {
1871
+ const planDir = path4.join(this.plan.baseDir, ".claude-plan", `issue-${issueIid}`);
1872
+ const filePath = path4.join(planDir, filename);
1873
+ if (!fs3.existsSync(filePath)) return null;
1995
1874
  try {
1996
- return JSON.parse(fs6.readFileSync(fp, "utf-8"));
1875
+ return fs3.readFileSync(filePath, "utf-8");
1997
1876
  } catch {
1998
1877
  return null;
1999
1878
  }
2000
1879
  }
2001
- writeDetectionFile(result) {
2002
- const dir = this.planDir;
2003
- if (!fs6.existsSync(dir)) {
2004
- fs6.mkdirSync(dir, { recursive: true });
2005
- }
2006
- fs6.writeFileSync(path7.join(dir, DETECT_FILENAME), JSON.stringify(result, null, 2), "utf-8");
2007
- }
2008
- getResultFiles(_ctx) {
2009
- if (this.hasDetectionResult()) {
2010
- return [{ filename: REPORT_FILENAME, label: "\u53D1\u5E03\u62A5\u544A" }];
1880
+ async validatePhaseOutput(ctx, displayId) {
1881
+ const resultFiles = this.getResultFiles(ctx);
1882
+ if (resultFiles.length === 0) return;
1883
+ const planDir = path4.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
1884
+ const missing = [];
1885
+ for (const file of resultFiles) {
1886
+ const filePath = path4.join(planDir, file.filename);
1887
+ if (!fs3.existsSync(filePath)) {
1888
+ missing.push(file.filename);
1889
+ continue;
1890
+ }
1891
+ const stat = fs3.statSync(filePath);
1892
+ if (stat.size < _BasePhase.MIN_ARTIFACT_BYTES) {
1893
+ missing.push(`${file.filename} (${stat.size} bytes, \u5185\u5BB9\u4E0D\u8DB3)`);
1894
+ }
2011
1895
  }
2012
- return [{ filename: DETECT_FILENAME, label: "\u53D1\u5E03\u68C0\u6D4B\u62A5\u544A" }];
2013
- }
2014
- buildPrompt(ctx) {
2015
- const dc = demandToPromptContext(ctx.demand);
2016
- const pc = {
2017
- issueTitle: dc.title,
2018
- issueDescription: dc.description,
2019
- issueIid: Number(dc.displayId),
2020
- supplementText: dc.supplementText || void 0,
2021
- workspace: ctx.workspace
2022
- };
2023
- if (this.hasDetectionResult()) {
2024
- const detect = this.readDetectionFile();
2025
- return releaseExecPrompt(pc, detect);
1896
+ if (missing.length > 0) {
1897
+ const msg = `AI \u8FDB\u7A0B\u6210\u529F\u9000\u51FA\u4F46\u672A\u751F\u6210\u9884\u671F\u4EA7\u7269: ${missing.join(", ")}`;
1898
+ this.logger.error(msg, { phase: this.phaseName, displayId });
1899
+ throw new AIExecutionError(this.phaseName, msg, { output: "", exitCode: 0 });
2026
1900
  }
2027
- return releaseDetectPrompt(pc);
2028
1901
  }
2029
- async execute(ctx) {
2030
- this.currentCtx = ctx;
2031
- const isDetect = !this.hasDetectionResult();
2032
- if (isDetect) {
2033
- const cache = new ReleaseDetectCache(resolveDataDir());
2034
- const projectPath = this.config.gongfeng.projectPath;
2035
- const ttlMs = this.config.release.detectCacheTtlMs;
2036
- const cached = cache.get(projectPath, ttlMs);
2037
- if (cached) {
2038
- logger8.info("Using cached release detection result", {
2039
- projectPath,
2040
- hasCapability: cached.hasReleaseCapability,
2041
- method: cached.releaseMethod
2042
- });
2043
- await this.hooks.onPhaseStart(this.phaseName);
2044
- this.writeDetectionFile(cached);
2045
- this.plan.updatePhaseProgress(this.phaseName, "completed");
1902
+ async commitPlanFiles(ctx, displayId) {
1903
+ const commitMsg = `chore(auto): ${this.phaseName} phase completed for issue #${displayId}`;
1904
+ if (ctx.workspace && ctx.workspace.repos.length > 1) {
1905
+ for (const repo of ctx.workspace.repos) {
1906
+ const repoGit = this.wtGitMap?.get(repo.name) ?? new GitOperations(repo.gitRootDir);
1907
+ const branch = repo.branchPrefix ? `${repo.branchPrefix}-${displayId}` : ctx.branchName;
2046
1908
  try {
2047
- await this.git.add(["."]);
2048
- await this.git.commit(`chore: release detection (cached) for issue-${ctx.demand.sourceRef.displayId}`);
2049
- } catch {
1909
+ if (await repoGit.hasChanges()) {
1910
+ await repoGit.add(["."]);
1911
+ await repoGit.commit(commitMsg);
1912
+ await repoGit.push(branch);
1913
+ this.logger.info("Committed changes for repo", { repo: repo.name, branch });
1914
+ }
1915
+ } catch (err) {
1916
+ this.logger.warn("Failed to commit/push for repo", {
1917
+ repo: repo.name,
1918
+ error: err.message
1919
+ });
2050
1920
  }
2051
- await this.hooks.onPhaseDone(this.phaseName);
2052
- return {
2053
- success: true,
2054
- output: `Release detection cached: ${cached.hasReleaseCapability ? cached.releaseMethod ?? "found" : "no capability"}`,
2055
- exitCode: 0,
2056
- gateRequested: cached.hasReleaseCapability,
2057
- hasReleaseCapability: cached.hasReleaseCapability
2058
- };
2059
1921
  }
2060
- }
2061
- const result = await super.execute(ctx);
2062
- if (isDetect && result.success) {
2063
- const detected = this.readDetectionFile();
2064
- if (detected) {
2065
- const cache = new ReleaseDetectCache(resolveDataDir());
2066
- const projectPath = this.config.gongfeng.projectPath;
2067
- cache.set({
2068
- ...detected,
2069
- projectPath,
2070
- detectedAt: (/* @__PURE__ */ new Date()).toISOString()
2071
- });
2072
- logger8.info("Release detection completed", {
2073
- hasCapability: detected.hasReleaseCapability,
2074
- method: detected.releaseMethod,
2075
- artifacts: detected.artifacts
2076
- });
2077
- return {
2078
- ...result,
2079
- gateRequested: detected.hasReleaseCapability,
2080
- hasReleaseCapability: detected.hasReleaseCapability
2081
- };
1922
+ } else {
1923
+ if (await this.git.hasChanges()) {
1924
+ await this.git.add(["."]);
1925
+ await this.git.commit(commitMsg);
1926
+ await this.git.push(ctx.branchName);
2082
1927
  }
2083
- return { ...result, gateRequested: false, hasReleaseCapability: false };
2084
1928
  }
2085
- return { ...result, gateRequested: false };
2086
- }
2087
- };
2088
-
2089
- // src/phases/UatPhase.ts
2090
- function getDefaultHost() {
2091
- return getLocalIP();
2092
- }
2093
- var UatPhase = class extends BasePhase {
2094
- phaseName = "uat";
2095
- getResultFiles(_ctx) {
2096
- return [{ filename: "03-uat-report.md", label: "UAT \u62A5\u544A" }];
2097
- }
2098
- buildPrompt(ctx) {
2099
- const pc = demandToPromptContext(ctx.demand);
2100
- const promptCtx = {
2101
- issueTitle: pc.title,
2102
- issueDescription: pc.description,
2103
- issueIid: Number(pc.displayId),
2104
- workspace: ctx.workspace
2105
- };
2106
- const hasUatTool = !!this.config.e2e.uatVendorDir;
2107
- const systemE2e = this.hooks.isE2eEnabled();
2108
- const e2ePorts = systemE2e && ctx.ports ? {
2109
- backendPort: ctx.ports.backendPort,
2110
- frontendPort: ctx.ports.frontendPort,
2111
- host: this.config.preview.host || getDefaultHost()
2112
- } : void 0;
2113
- const uatTool = hasUatTool ? { vendorDir: this.config.e2e.uatVendorDir, configFile: this.config.e2e.uatConfigFile } : void 0;
2114
- const e2eSuffix = e2eVerifyPromptSuffix(promptCtx, e2ePorts, uatTool);
2115
- return `# UAT/E2E \u9A8C\u8BC1\u9636\u6BB5
2116
-
2117
- ## \u4EFB\u52A1\u80CC\u666F
2118
- - Issue: #${promptCtx.issueIid} ${promptCtx.issueTitle}
2119
- - \u672C\u9636\u6BB5\u4E13\u6CE8\u4E8E E2E/UAT \u7AEF\u5BF9\u7AEF\u9A8C\u8BC1\uFF0Clint/build/test \u68C0\u67E5\u5DF2\u5728\u4E0A\u4E00\u9A8C\u8BC1\u9636\u6BB5\u5B8C\u6210\u3002
2120
-
2121
- ## \u4EA7\u7269\u8981\u6C42
2122
- \u8BF7\u5C06 E2E \u9A8C\u8BC1\u7ED3\u679C\u5199\u5165 \`.claude-plan/issue-${promptCtx.issueIid}/03-uat-report.md\`\uFF0C\u5305\u542B\uFF1A
2123
- - \u6D4B\u8BD5\u573A\u666F\u6E05\u5355
2124
- - \u6267\u884C\u7ED3\u679C\uFF08\u901A\u8FC7/\u5931\u8D25\uFF09
2125
- - \u622A\u56FE\u8DEF\u5F84\uFF08\u5982\u6709\uFF09
2126
- - \u5931\u8D25\u539F\u56E0\u5206\u6790\uFF08\u5982\u6709\u5931\u8D25\uFF09
2127
-
2128
- ${e2eSuffix}`;
2129
1929
  }
2130
1930
  };
2131
1931
 
2132
- // src/phases/PhaseFactory.ts
2133
- var PHASE_REGISTRY = /* @__PURE__ */ new Map();
2134
- function registerPhase(name, ctor) {
2135
- PHASE_REGISTRY.set(name, ctor);
2136
- }
2137
- function registerBuiltinPhases() {
2138
- PHASE_REGISTRY.set("plan", PlanPhase);
2139
- PHASE_REGISTRY.set("build", BuildPhase);
2140
- PHASE_REGISTRY.set("verify", VerifyPhase);
2141
- PHASE_REGISTRY.set("uat", UatPhase);
2142
- PHASE_REGISTRY.set("release", ReleasePhase);
2143
- }
2144
- registerBuiltinPhases();
2145
- function createPhase(name, ...args) {
2146
- const Ctor = PHASE_REGISTRY.get(name);
2147
- if (!Ctor) {
2148
- throw new PhaseNotRegisteredError(name, [...PHASE_REGISTRY.keys()]);
2149
- }
2150
- return new Ctor(...args);
2151
- }
2152
- function validatePhaseRegistry(phaseNames) {
2153
- const missing = phaseNames.filter((name) => !PHASE_REGISTRY.has(name));
2154
- if (missing.length > 0) {
2155
- throw new UnregisteredPhasesError(missing, [...PHASE_REGISTRY.keys()]);
2156
- }
2157
- }
1932
+ // src/phases/VerifyPhase.ts
1933
+ import fs4 from "fs";
1934
+ import path5 from "path";
2158
1935
 
2159
- // src/lifecycle/ActionLifecycleManager.ts
2160
- var ActionLifecycleManager = class {
2161
- /** IssueState ActionState */
2162
- stateToAction;
2163
- /** "action:status" IssueState */
2164
- actionToState;
2165
- /** phase name { startState, doneState, approvedState? } */
2166
- phaseStatesMap;
2167
- /** Ordered phase indices by IssueState for determineResumePhaseIndex */
2168
- def;
2169
- constructor(def) {
2170
- this.def = def;
2171
- this.stateToAction = /* @__PURE__ */ new Map();
2172
- this.actionToState = /* @__PURE__ */ new Map();
2173
- this.phaseStatesMap = /* @__PURE__ */ new Map();
2174
- this.buildMappings(def);
2175
- }
2176
- buildMappings(def) {
2177
- this.addMapping("pending" /* Pending */, "init", "idle");
2178
- this.addMapping("skipped" /* Skipped */, "init", "skipped");
2179
- this.addMapping("branch_created" /* BranchCreated */, "init", "ready");
2180
- this.addMapping("failed" /* Failed */, "init", "failed");
2181
- this.addMapping("deployed" /* Deployed */, "init", "done");
2182
- this.addMapping("resolving_conflict" /* ResolvingConflict */, "conflict", "running");
2183
- for (const spec of def.phases) {
2184
- this.phaseStatesMap.set(spec.name, {
2185
- startState: spec.startState,
2186
- doneState: spec.doneState,
2187
- approvedState: spec.approvedState
2188
- });
2189
- if (spec.kind === "ai") {
2190
- if (spec.startState !== "phase_running" /* PhaseRunning */) {
2191
- this.addMapping(spec.startState, spec.name, "running");
2192
- }
2193
- if (spec.doneState === "completed" /* Completed */) {
2194
- this.addMapping(spec.doneState, spec.name, "done");
2195
- } else if (spec.doneState !== "phase_done" /* PhaseDone */) {
2196
- this.addMapping(spec.doneState, spec.name, "ready");
2197
- }
2198
- } else if (spec.kind === "gate") {
2199
- if (spec.startState !== "phase_waiting" /* PhaseWaiting */) {
2200
- this.addMapping(spec.startState, spec.name, "waiting");
2201
- }
2202
- if (spec.approvedState && spec.approvedState !== "phase_approved" /* PhaseApproved */) {
2203
- this.addMapping(spec.approvedState, spec.name, "ready");
2204
- }
2205
- }
2206
- }
2207
- }
2208
- addMapping(state, action, status) {
2209
- if (!this.stateToAction.has(state)) {
2210
- this.stateToAction.set(state, { action, status });
2211
- }
2212
- const key = `${action}:${status}`;
2213
- if (!this.actionToState.has(key)) {
2214
- this.actionToState.set(key, state);
2215
- }
2216
- }
2217
- // ─── Query API ───
1936
+ // src/verify/VerifyReportParser.ts
1937
+ var LINT_FAIL_RE = /\*{0,2}Lint\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1938
+ var BUILD_FAIL_RE = /\*{0,2}Build\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1939
+ var TEST_FAIL_RE = /\*{0,2}Test\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1940
+ var SUMMARY_FAIL_RE = /\*{0,2}(?:总结|Summary)\*{0,2}\s*[::].*(?:验证失败|verification\s+failed|failed|失败)/i;
1941
+ var TODOLIST_STATS_RE = /\*{0,2}(?:Todolist|Todo)\s*(?:检查|check)?\*{0,2}\s*[::]\s*(\d+)\s*[//]\s*(\d+)/i;
1942
+ var VerifyReportParser = class {
2218
1943
  /**
2219
- * 将 IssueState 解析为语义化的 ActionState。
1944
+ * 解析验证报告内容。
2220
1945
  *
2221
- * 对于通用状态 PhaseRunning/PhaseDone,需额外传入 currentPhase 来区分具体阶段。
1946
+ * @param reportContent 验证报告的完整 Markdown 文本
2222
1947
  */
2223
- resolve(state, currentPhase) {
2224
- if (state === "phase_running" /* PhaseRunning */ && currentPhase) {
2225
- return { action: currentPhase, status: "running" };
2226
- }
2227
- if (state === "phase_done" /* PhaseDone */ && currentPhase) {
2228
- return { action: currentPhase, status: "ready" };
2229
- }
2230
- if (state === "phase_waiting" /* PhaseWaiting */ && currentPhase) {
2231
- return { action: currentPhase, status: "waiting" };
1948
+ parse(reportContent) {
1949
+ const lintPassed = !LINT_FAIL_RE.test(reportContent);
1950
+ const buildPassed = !BUILD_FAIL_RE.test(reportContent);
1951
+ const testPassed = !TEST_FAIL_RE.test(reportContent);
1952
+ const todolistStats = this.parseTodolistStats(reportContent);
1953
+ const todolistComplete = todolistStats ? todolistStats.total === 0 || todolistStats.completed === todolistStats.total : true;
1954
+ const failureReasons = [];
1955
+ if (!lintPassed) failureReasons.push("Lint \u68C0\u67E5\u5931\u8D25");
1956
+ if (!buildPassed) failureReasons.push("Build \u7F16\u8BD1\u5931\u8D25");
1957
+ if (!testPassed) failureReasons.push("\u6D4B\u8BD5\u672A\u901A\u8FC7");
1958
+ if (!todolistComplete) {
1959
+ const statsText = todolistStats ? `(${todolistStats.completed}/${todolistStats.total})` : "";
1960
+ failureReasons.push(`Todolist \u672A\u5168\u90E8\u5B8C\u6210${statsText}`);
2232
1961
  }
2233
- if (state === "phase_approved" /* PhaseApproved */ && currentPhase) {
2234
- return { action: currentPhase, status: "ready" };
1962
+ const summaryFailed = SUMMARY_FAIL_RE.test(reportContent);
1963
+ if (failureReasons.length === 0 && summaryFailed) {
1964
+ failureReasons.push("\u9A8C\u8BC1\u62A5\u544A\u603B\u7ED3\u5224\u5B9A\u4E3A\u5931\u8D25");
2235
1965
  }
2236
- const mapped = this.stateToAction.get(state);
2237
- if (mapped) return mapped;
2238
- return { action: "init", status: "idle" };
1966
+ const passed = failureReasons.length === 0;
1967
+ return {
1968
+ passed,
1969
+ lintPassed,
1970
+ buildPassed,
1971
+ testPassed,
1972
+ todolistComplete,
1973
+ todolistStats: todolistStats ?? void 0,
1974
+ failureReasons,
1975
+ rawReport: reportContent
1976
+ };
2239
1977
  }
2240
1978
  /**
2241
- * action + status 反向映射为 IssueState。
1979
+ * plan 文件中解析 Todolist 完成度。
1980
+ * 统计 `- [x]` (已完成) 和 `- [ ]` (未完成) 的数量。
2242
1981
  */
2243
- toIssueState(action, status) {
2244
- return this.actionToState.get(`${action}:${status}`);
1982
+ parseTodolistFromPlan(planContent) {
1983
+ const completedRe = /^[ \t]*-\s+\[x\]/gim;
1984
+ const uncompletedRe = /^[ \t]*-\s+\[\s\]/gm;
1985
+ const completedMatches = planContent.match(completedRe);
1986
+ const uncompletedMatches = planContent.match(uncompletedRe);
1987
+ const completed = completedMatches?.length ?? 0;
1988
+ const uncompleted = uncompletedMatches?.length ?? 0;
1989
+ return {
1990
+ completed,
1991
+ total: completed + uncompleted
1992
+ };
2245
1993
  }
2246
- // ─── Classification predicates (替代 3 个 Set 常量 + getDrivableIssues) ───
2247
1994
  /**
2248
- * 终态:done | failed | skipped
1995
+ * 从报告中提取 Todolist 统计数据。
1996
+ * 格式: "Todolist 检查: X/Y 项完成"
2249
1997
  */
2250
- isTerminal(state) {
2251
- const as = this.resolve(state);
2252
- return as.status === "done" || as.status === "failed" || as.status === "skipped";
1998
+ parseTodolistStats(reportContent) {
1999
+ const match = reportContent.match(TODOLIST_STATS_RE);
2000
+ if (match) {
2001
+ return {
2002
+ completed: parseInt(match[1], 10),
2003
+ total: parseInt(match[2], 10)
2004
+ };
2005
+ }
2006
+ return null;
2253
2007
  }
2254
- /**
2255
- * 进行中:running (包含通用 PhaseRunning)
2256
- */
2257
- isInProgress(state) {
2258
- if (state === "phase_running" /* PhaseRunning */) return true;
2259
- return this.resolve(state).status === "running";
2008
+ };
2009
+
2010
+ // src/phases/VerifyPhase.ts
2011
+ var VerifyPhase = class extends BasePhase {
2012
+ phaseName = "verify";
2013
+ reportParser = new VerifyReportParser();
2014
+ getResultFiles(ctx) {
2015
+ const filename = ctx?.pipelineMode === "plan-mode" ? "02-verify-report.md" : "04-verify-report.md";
2016
+ return [{ filename, label: "\u9A8C\u8BC1\u62A5\u544A" }];
2260
2017
  }
2261
- /**
2262
- * 阻塞中:waiting
2263
- */
2264
- isBlocked(state) {
2265
- if (state === "phase_waiting" /* PhaseWaiting */) return true;
2266
- return this.resolve(state).status === "waiting";
2018
+ async execute(ctx) {
2019
+ const result = await super.execute(ctx);
2020
+ if (result.success) {
2021
+ const report = this.readVerifyReport(ctx);
2022
+ if (report) {
2023
+ const parsed = this.reportParser.parse(report);
2024
+ if (this.config.verifyFixLoop.todolistCheckEnabled && !parsed.todolistStats) {
2025
+ const planContent = this.readPlanFile(ctx);
2026
+ if (planContent) {
2027
+ const todoStats = this.reportParser.parseTodolistFromPlan(planContent);
2028
+ if (todoStats.total > 0) {
2029
+ parsed.todolistStats = todoStats;
2030
+ parsed.todolistComplete = todoStats.completed === todoStats.total;
2031
+ if (!parsed.todolistComplete) {
2032
+ parsed.failureReasons.push(
2033
+ `Todolist \u672A\u5168\u90E8\u5B8C\u6210(${todoStats.completed}/${todoStats.total})`
2034
+ );
2035
+ parsed.passed = false;
2036
+ }
2037
+ }
2038
+ }
2039
+ }
2040
+ this.logger.info("Verify report parsed", {
2041
+ passed: parsed.passed,
2042
+ lintPassed: parsed.lintPassed,
2043
+ buildPassed: parsed.buildPassed,
2044
+ testPassed: parsed.testPassed,
2045
+ todolistComplete: parsed.todolistComplete,
2046
+ todolistStats: parsed.todolistStats,
2047
+ failureCount: parsed.failureReasons.length
2048
+ });
2049
+ return { ...result, verifyReport: parsed };
2050
+ }
2051
+ }
2052
+ return result;
2267
2053
  }
2268
- /**
2269
- * 可驱动判断(集中化 getDrivableIssues 的过滤逻辑)。
2270
- *
2271
- * - idle → 可驱动 (Pending)
2272
- * - ready → 可驱动 (BranchCreated, PhaseDone, PhaseApproved)
2273
- * - failed && attempts < maxRetries → 可驱动
2274
- * - waiting → 不可驱动 (PhaseWaiting)
2275
- * - running → 不可驱动(stalled 由外部叠加)
2276
- * - done/skipped 不可驱动
2277
- */
2278
- isDrivable(state, attempts, maxRetries, lastErrorRetryable) {
2279
- if (state === "phase_done" /* PhaseDone */) return true;
2280
- if (state === "phase_approved" /* PhaseApproved */) return true;
2281
- if (state === "phase_running" /* PhaseRunning */) return false;
2282
- if (state === "phase_waiting" /* PhaseWaiting */) return false;
2283
- const as = this.resolve(state);
2284
- switch (as.status) {
2285
- case "idle":
2286
- case "ready":
2287
- return true;
2288
- case "failed":
2289
- if (lastErrorRetryable === false) return false;
2290
- return attempts < maxRetries;
2291
- case "waiting":
2292
- case "running":
2293
- case "done":
2294
- case "skipped":
2295
- return false;
2054
+ buildPrompt(ctx) {
2055
+ const pc = demandToPromptContext(ctx.demand);
2056
+ const promptCtx = {
2057
+ issueTitle: pc.title,
2058
+ issueDescription: pc.description,
2059
+ issueIid: Number(pc.displayId),
2060
+ workspace: ctx.workspace
2061
+ };
2062
+ return ctx.pipelineMode === "plan-mode" ? planModeVerifyPrompt(promptCtx) : verifyPrompt(promptCtx);
2063
+ }
2064
+ readVerifyReport(ctx) {
2065
+ const files = this.getResultFiles(ctx);
2066
+ if (files.length === 0) return null;
2067
+ const displayId = Number(ctx.demand.sourceRef.displayId);
2068
+ const planDir = path5.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
2069
+ const filePath = path5.join(planDir, files[0].filename);
2070
+ if (!fs4.existsSync(filePath)) return null;
2071
+ try {
2072
+ return fs4.readFileSync(filePath, "utf-8");
2073
+ } catch {
2074
+ return null;
2296
2075
  }
2297
2076
  }
2298
- // ─── Phase navigation (替代 determineStartIndex + getPhasePreState) ───
2299
- /**
2300
- * 确定从哪个阶段索引恢复执行(替代 PipelineOrchestrator.determineStartIndex)。
2301
- *
2302
- * 从后向前扫描 phases,匹配 currentState 或 failedAtState。
2303
- * 支持通用状态 PhaseRunning/PhaseDone + currentPhase 的组合。
2304
- */
2305
- determineResumePhaseIndex(currentState, failedAtState, currentPhase) {
2306
- const target = failedAtState || currentState;
2307
- const phases = this.def.phases;
2308
- if ((target === "phase_running" /* PhaseRunning */ || target === "phase_done" /* PhaseDone */) && currentPhase) {
2309
- const idx = phases.findIndex((p) => p.name === currentPhase);
2310
- if (idx >= 0) {
2311
- return target === "phase_done" /* PhaseDone */ ? idx + 1 : idx;
2312
- }
2077
+ readPlanFile(ctx) {
2078
+ const displayId = Number(ctx.demand.sourceRef.displayId);
2079
+ const planDir = path5.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
2080
+ const filePath = path5.join(planDir, "01-plan.md");
2081
+ if (!fs4.existsSync(filePath)) return null;
2082
+ try {
2083
+ return fs4.readFileSync(filePath, "utf-8");
2084
+ } catch {
2085
+ return null;
2313
2086
  }
2314
- if ((target === "phase_waiting" /* PhaseWaiting */ || target === "phase_approved" /* PhaseApproved */) && currentPhase) {
2315
- const idx = phases.findIndex((p) => p.name === currentPhase);
2316
- if (idx >= 0) {
2317
- if (target === "phase_approved" /* PhaseApproved */) {
2318
- const spec = phases[idx];
2319
- return spec.kind === "ai" && spec.approvedState ? idx : idx + 1;
2087
+ }
2088
+ };
2089
+
2090
+ // src/phases/PlanPhase.ts
2091
+ var PlanPhase = class extends BasePhase {
2092
+ phaseName = "plan";
2093
+ getResultFiles() {
2094
+ return [{ filename: "01-plan.md", label: "\u5B9E\u65BD\u8BA1\u5212" }];
2095
+ }
2096
+ getRunMode() {
2097
+ return "plan";
2098
+ }
2099
+ buildPrompt(ctx) {
2100
+ const pc = demandToPromptContext(ctx.demand);
2101
+ const history = this.plan.readReviewHistory();
2102
+ const promptCtx = {
2103
+ issueTitle: pc.title,
2104
+ issueDescription: pc.description,
2105
+ issueIid: Number(pc.displayId),
2106
+ supplementText: pc.supplementText || void 0,
2107
+ workspace: ctx.workspace
2108
+ };
2109
+ let basePrompt;
2110
+ if (history.length > 0) {
2111
+ basePrompt = rePlanPrompt(promptCtx, history);
2112
+ } else {
2113
+ basePrompt = planPrompt(promptCtx);
2114
+ }
2115
+ const caps = getRunnerCapabilities(this.config.ai.mode);
2116
+ if (!caps?.nativePlanMode) {
2117
+ basePrompt = `${t("prompt.planModeFallback")}
2118
+
2119
+ ${basePrompt}`;
2120
+ }
2121
+ return basePrompt;
2122
+ }
2123
+ };
2124
+
2125
+ // src/phases/BuildPhase.ts
2126
+ var BuildPhase = class extends BasePhase {
2127
+ phaseName = "build";
2128
+ async validatePhaseOutput(ctx, _displayId) {
2129
+ let hasAnyChanges = false;
2130
+ if (ctx.workspace && ctx.workspace.repos.length > 1) {
2131
+ for (const repo of ctx.workspace.repos) {
2132
+ const repoGit = new GitOperations(repo.gitRootDir);
2133
+ if (await repoGit.hasChanges()) {
2134
+ hasAnyChanges = true;
2135
+ break;
2320
2136
  }
2321
- return idx;
2322
2137
  }
2138
+ } else {
2139
+ hasAnyChanges = await this.git.hasChanges();
2323
2140
  }
2324
- for (let i = phases.length - 1; i >= 0; i--) {
2325
- const spec = phases[i];
2326
- if (spec.kind === "gate" && spec.approvedState === target) {
2327
- return i + 1;
2328
- }
2329
- if (spec.startState === target || spec.doneState === target) {
2330
- return spec.doneState === target ? i + 1 : i;
2331
- }
2141
+ if (!hasAnyChanges) {
2142
+ const msg = "AI \u8FDB\u7A0B\u6210\u529F\u9000\u51FA\u4F46\u672A\u4EA7\u751F\u4EFB\u4F55\u4EE3\u7801\u53D8\u66F4";
2143
+ this.logger.error(msg, { phase: this.phaseName });
2144
+ throw new AIExecutionError(this.phaseName, msg, { output: "", exitCode: 0 });
2332
2145
  }
2333
- return 0;
2334
2146
  }
2335
- /**
2336
- * 获取某个 phase 的前驱状态(即重置到该 phase 需要设置的状态)。
2337
- * 第一个 phase 的前驱是 BranchCreated;后续 phase 的前驱是上一个 phase 的 approvedState 或 doneState。
2338
- */
2339
- getPhasePreState(phaseName) {
2340
- const phases = this.def.phases;
2341
- const idx = phases.findIndex((p) => p.name === phaseName);
2342
- if (idx < 0) return void 0;
2343
- if (idx === 0) return "branch_created" /* BranchCreated */;
2344
- const prev = phases[idx - 1];
2345
- return prev.approvedState ?? prev.doneState;
2147
+ buildPrompt(ctx) {
2148
+ const pc = demandToPromptContext(ctx.demand);
2149
+ const base = buildPrompt({
2150
+ issueTitle: pc.title,
2151
+ issueDescription: pc.description,
2152
+ issueIid: Number(pc.displayId),
2153
+ workspace: ctx.workspace
2154
+ });
2155
+ if (ctx.fixContext) {
2156
+ return base + t("prompt.buildFixSuffix", {
2157
+ iteration: ctx.fixContext.iteration,
2158
+ failures: ctx.fixContext.verifyFailures.map((f, i) => `${i + 1}. ${f}`).join("\n"),
2159
+ rawReport: ctx.fixContext.rawReport.slice(0, 2e3)
2160
+ });
2161
+ }
2162
+ return base;
2163
+ }
2164
+ };
2165
+
2166
+ // src/phases/ReleasePhase.ts
2167
+ import fs6 from "fs";
2168
+ import path7 from "path";
2169
+
2170
+ // src/release/ReleaseDetectCache.ts
2171
+ import fs5 from "fs";
2172
+ import path6 from "path";
2173
+ import { createHash } from "crypto";
2174
+ var logger7 = logger.child("ReleaseDetectCache");
2175
+ function hashProjectPath(projectPath) {
2176
+ return createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
2177
+ }
2178
+ var ReleaseDetectCache = class {
2179
+ cacheDir;
2180
+ constructor(dataDir) {
2181
+ this.cacheDir = path6.join(dataDir, "release-detect");
2182
+ }
2183
+ filePath(projectPath) {
2184
+ return path6.join(this.cacheDir, `${hashProjectPath(projectPath)}.json`);
2346
2185
  }
2347
2186
  /**
2348
- * 获取某个 phase 的状态三元组。
2187
+ * 读取缓存。返回 null 如果不存在、已过期或校验失败。
2349
2188
  */
2350
- getPhaseStates(phaseName) {
2351
- return this.phaseStatesMap.get(phaseName);
2189
+ get(projectPath, ttlMs) {
2190
+ const fp = this.filePath(projectPath);
2191
+ try {
2192
+ if (!fs5.existsSync(fp)) return null;
2193
+ const raw = fs5.readFileSync(fp, "utf-8");
2194
+ const data = JSON.parse(raw);
2195
+ if (data.projectPath !== projectPath) {
2196
+ logger7.warn("Cache projectPath mismatch, ignoring", { expected: projectPath, got: data.projectPath });
2197
+ return null;
2198
+ }
2199
+ const age = Date.now() - new Date(data.detectedAt).getTime();
2200
+ if (age > ttlMs) {
2201
+ logger7.debug("Cache expired", { projectPath, ageMs: age, ttlMs });
2202
+ return null;
2203
+ }
2204
+ return data;
2205
+ } catch (err) {
2206
+ logger7.warn("Failed to read release detect cache", { path: fp, error: err.message });
2207
+ return null;
2208
+ }
2352
2209
  }
2353
- // ─── Display helpers (替代 collectStateLabels + derivePhaseStatuses) ───
2354
2210
  /**
2355
- * 解析单条状态的展示标签。
2356
- *
2357
- * 对通用状态 PhaseRunning/PhaseDone,需传入 currentPhase 以生成具体标签(如"分析中");
2358
- * 缺少 currentPhase 时回退到泛化标签(如"阶段执行中")。
2211
+ * 写入缓存。
2359
2212
  */
2360
- resolveLabel(state, currentPhase) {
2361
- if ((state === "phase_running" /* PhaseRunning */ || state === "phase_done" /* PhaseDone */) && currentPhase) {
2362
- const phaseLabel = t(`pipeline.phase.${currentPhase}`);
2363
- return state === "phase_running" /* PhaseRunning */ ? t("state.phaseDoing", { label: phaseLabel }) : t("state.phaseDone", { label: phaseLabel });
2364
- }
2365
- if ((state === "phase_waiting" /* PhaseWaiting */ || state === "phase_approved" /* PhaseApproved */) && currentPhase) {
2366
- const phaseLabel = t(`pipeline.phase.${currentPhase}`);
2367
- return state === "phase_waiting" /* PhaseWaiting */ ? t("state.phaseWaiting", { label: phaseLabel }) : t("state.phaseApproved", { label: phaseLabel });
2213
+ set(result) {
2214
+ const fp = this.filePath(result.projectPath);
2215
+ try {
2216
+ if (!fs5.existsSync(this.cacheDir)) {
2217
+ fs5.mkdirSync(this.cacheDir, { recursive: true });
2218
+ }
2219
+ fs5.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
2220
+ logger7.debug("Release detect cache written", { projectPath: result.projectPath, path: fp });
2221
+ } catch (err) {
2222
+ logger7.warn("Failed to write release detect cache", { path: fp, error: err.message });
2368
2223
  }
2369
- const labels = this.collectStateLabels();
2370
- return labels.get(state) ?? state;
2371
2224
  }
2372
2225
  /**
2373
- * 收集所有状态及其展示标签。
2374
- * 为通用状态 PhaseRunning/PhaseDone 生成每个阶段的复合 key 条目。
2226
+ * 手动失效缓存。
2375
2227
  */
2376
- collectStateLabels() {
2377
- const labels = /* @__PURE__ */ new Map();
2378
- labels.set("pending" /* Pending */, t("state.pending"));
2379
- labels.set("skipped" /* Skipped */, t("state.skipped"));
2380
- labels.set("branch_created" /* BranchCreated */, t("state.branchCreated"));
2381
- for (const phase of this.def.phases) {
2382
- const phaseLabel = t(`pipeline.phase.${phase.name}`);
2383
- if (phase.startState === "phase_running" /* PhaseRunning */) {
2384
- labels.set(`phase_running:${phase.name}`, t("state.phaseDoing", { label: phaseLabel }));
2385
- } else if (phase.startState === "phase_waiting" /* PhaseWaiting */) {
2386
- labels.set(`phase_waiting:${phase.name}`, t("state.phaseWaiting", { label: phaseLabel }));
2387
- } else {
2388
- labels.set(phase.startState, t("state.phaseDoing", { label: phaseLabel }));
2389
- }
2390
- if (phase.doneState === "phase_done" /* PhaseDone */) {
2391
- labels.set(`phase_done:${phase.name}`, t("state.phaseDone", { label: phaseLabel }));
2392
- } else if (phase.doneState === "phase_approved" /* PhaseApproved */) {
2393
- labels.set(`phase_approved:${phase.name}`, t("state.phaseApproved", { label: phaseLabel }));
2394
- } else if (phase.doneState !== "completed" /* Completed */) {
2395
- labels.set(phase.doneState, t("state.phaseDone", { label: phaseLabel }));
2396
- }
2397
- if (phase.approvedState && phase.approvedState !== "phase_approved" /* PhaseApproved */ && phase.approvedState !== phase.doneState) {
2398
- labels.set(phase.approvedState, t("state.phaseApproved", { label: phaseLabel }));
2228
+ invalidate(projectPath) {
2229
+ const fp = this.filePath(projectPath);
2230
+ try {
2231
+ if (fs5.existsSync(fp)) {
2232
+ fs5.unlinkSync(fp);
2233
+ logger7.info("Release detect cache invalidated", { projectPath });
2234
+ return true;
2399
2235
  }
2236
+ return false;
2237
+ } catch (err) {
2238
+ logger7.warn("Failed to invalidate release detect cache", { path: fp, error: err.message });
2239
+ return false;
2400
2240
  }
2401
- labels.set("completed" /* Completed */, t("state.completed"));
2402
- labels.set("failed" /* Failed */, t("state.failed"));
2403
- labels.set("resolving_conflict" /* ResolvingConflict */, t("state.resolvingConflict"));
2404
- return labels;
2405
2241
  }
2242
+ };
2243
+
2244
+ // src/prompts/release-templates.ts
2245
+ function releaseDetectPrompt(ctx) {
2246
+ const pd = `.claude-plan/issue-${ctx.issueIid}`;
2247
+ return `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684 DevOps \u5206\u6790\u5E08\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u63A2\u7D22\u5F53\u524D\u9879\u76EE\uFF0C\u67E5\u627E\u6240\u6709\u4E0E**\u53D1\u5E03/\u90E8\u7F72**\u76F8\u5173\u7684\u673A\u5236\u3002
2248
+
2249
+ ## \u63A2\u7D22\u8303\u56F4
2250
+
2251
+ \u8BF7\u6309\u4F18\u5148\u7EA7\u4F9D\u6B21\u68C0\u67E5\u4EE5\u4E0B\u4F4D\u7F6E\uFF1A
2252
+
2253
+ 1. **Skill \u6587\u4EF6**
2254
+ - \`.cursor/skills/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy/publish \u76F8\u5173\u7684 skill
2255
+ - \u9879\u76EE\u6839\u76EE\u5F55\u7684 \`SKILL.md\` \u6216 \`release.md\`
2256
+
2257
+ 2. **Rule \u6587\u4EF6**
2258
+ - \`.cursor/rules/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy \u76F8\u5173\u7684 \`.mdc\` \u89C4\u5219
2259
+
2260
+ 3. **CI/CD \u914D\u7F6E**
2261
+ - \`.gitlab-ci.yml\` / \`Jenkinsfile\` / \`.github/workflows/\`
2262
+ - \u67E5\u627E release/deploy/publish \u76F8\u5173\u7684 stage/job/workflow
2263
+
2264
+ 4. **Package Manager**
2265
+ - \`package.json\` \u4E2D\u7684 \`scripts\` \u5B57\u6BB5\uFF1Apublish\u3001release\u3001deploy \u7B49
2266
+ - \`Makefile\` \u4E2D\u7684 release/deploy target
2267
+
2268
+ 5. **\u811A\u672C\u4E0E\u6587\u6863**
2269
+ - \`scripts/release.*\`\u3001\`scripts/deploy.*\`\u3001\`scripts/publish.*\`
2270
+ - \`RELEASE.md\`\u3001\`CHANGELOG.md\`\u3001\`CONTRIBUTING.md\` \u4E2D\u7684\u53D1\u5E03\u8BF4\u660E
2271
+ - \`Dockerfile\`\u3001\`docker-compose*.yml\`
2272
+
2273
+ ## \u8F93\u51FA\u8981\u6C42
2274
+
2275
+ \u5C06\u68C0\u6D4B\u7ED3\u679C\u4EE5 **\u4E25\u683C JSON** \u683C\u5F0F\u5199\u5165 \`${pd}/03-release-detect.json\`\uFF1A
2276
+
2277
+ \`\`\`json
2278
+ {
2279
+ "hasReleaseCapability": true,
2280
+ "releaseMethod": "npm-publish",
2281
+ "artifacts": ["package.json", ".gitlab-ci.yml"],
2282
+ "instructions": "\u8FD0\u884C npm publish \u53D1\u5E03\u5230 npm registry..."
2283
+ }
2284
+ \`\`\`
2285
+
2286
+ \u5B57\u6BB5\u8BF4\u660E\uFF1A
2287
+ - \`hasReleaseCapability\`: \u662F\u5426\u627E\u5230\u53D1\u5E03\u673A\u5236\uFF08boolean\uFF09
2288
+ - \`releaseMethod\`: \u53D1\u5E03\u65B9\u5F0F\uFF0C\u53EF\u9009\u503C\uFF1A"npm-publish" | "ci-pipeline" | "makefile" | "custom-script" | "skill" | "docker" | \u5176\u4ED6\u63CF\u8FF0
2289
+ - \`artifacts\`: \u53D1\u73B0\u7684\u53D1\u5E03\u76F8\u5173\u6587\u4EF6\u8DEF\u5F84\u6570\u7EC4
2290
+ - \`instructions\`: \u4ECE\u6587\u4EF6\u4E2D\u63D0\u53D6\u7684\u53D1\u5E03\u6B65\u9AA4\u8BF4\u660E\u3002\u5982\u679C\u662F skill/rule \u6587\u4EF6\uFF0C\u8BF7\u5B8C\u6574\u590D\u5236\u5176\u5185\u5BB9
2291
+
2292
+ **\u91CD\u8981**\uFF1A
2293
+ - \u5982\u679C\u6CA1\u6709\u627E\u5230\u4EFB\u4F55\u53D1\u5E03\u673A\u5236\uFF0C\u5C06 \`hasReleaseCapability\` \u8BBE\u4E3A \`false\`\uFF0C\u5176\u4ED6\u5B57\u6BB5\u53EF\u7701\u7565
2294
+ - \u5982\u679C\u627E\u5230\u4E86 skill \u6216 rule \u6587\u4EF6\uFF0C\u8BF7\u5728 \`instructions\` \u4E2D\u5B8C\u6574\u4FDD\u7559\u539F\u6587\u5185\u5BB9\uFF0C\u540E\u7EED\u53D1\u5E03\u6267\u884C\u5C06\u4F9D\u8D56\u5B83
2295
+ - \u53EA\u8F93\u51FA JSON \u6587\u4EF6\uFF0C\u4E0D\u8981\u6267\u884C\u4EFB\u4F55\u53D1\u5E03\u64CD\u4F5C`;
2296
+ }
2297
+ function releaseExecPrompt(ctx, detectResult) {
2298
+ const pd = `.claude-plan/issue-${ctx.issueIid}`;
2299
+ const artifactList = detectResult.artifacts.length > 0 ? detectResult.artifacts.map((a) => `- \`${a}\``).join("\n") : "\uFF08\u65E0\uFF09";
2300
+ const instructionSection = detectResult.instructions ? `
2301
+ ## \u53D1\u5E03\u6307\u5F15\uFF08\u6765\u81EA\u9879\u76EE skill/rule/\u6587\u6863\uFF09
2302
+
2303
+ ${detectResult.instructions}` : "";
2304
+ return `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u53D1\u5E03\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6839\u636E\u4EE5\u4E0B\u68C0\u6D4B\u7ED3\u679C\uFF0C\u6267\u884C\u9879\u76EE\u7684\u53D1\u5E03\u6D41\u7A0B\u3002
2305
+
2306
+ ## \u68C0\u6D4B\u7ED3\u679C
2307
+
2308
+ - **\u53D1\u5E03\u65B9\u5F0F**: ${detectResult.releaseMethod ?? "\u672A\u77E5"}
2309
+ - **\u76F8\u5173\u6587\u4EF6**:
2310
+ ${artifactList}
2311
+ ${instructionSection}
2312
+
2313
+ ## \u4EFB\u52A1
2314
+
2315
+ 1. \u9605\u8BFB\u4E0A\u8FF0\u53D1\u5E03\u6307\u5F15\u548C\u76F8\u5173\u6587\u4EF6
2316
+ 2. \u6309\u7167\u9879\u76EE\u5B9A\u4E49\u7684\u53D1\u5E03\u6D41\u7A0B\u6267\u884C\u53D1\u5E03\u64CD\u4F5C
2317
+ 3. \u5C06\u53D1\u5E03\u7ED3\u679C\u5199\u5165 \`${pd}/04-release-report.md\`
2318
+
2319
+ ## \u53D1\u5E03\u62A5\u544A\u683C\u5F0F
2320
+
2321
+ \`\`\`markdown
2322
+ # \u53D1\u5E03\u62A5\u544A
2323
+
2324
+ ## \u53D1\u5E03\u65B9\u5F0F
2325
+ <\u4F7F\u7528\u7684\u53D1\u5E03\u65B9\u5F0F>
2326
+
2327
+ ## \u6267\u884C\u6B65\u9AA4
2328
+ <\u9010\u6B65\u8BB0\u5F55\u6267\u884C\u7684\u64CD\u4F5C>
2329
+
2330
+ ## \u7ED3\u679C
2331
+ <\u6210\u529F/\u5931\u8D25\u53CA\u8BE6\u60C5>
2332
+ \`\`\`
2333
+
2334
+ **\u91CD\u8981**\uFF1A
2335
+ - \u4E25\u683C\u6309\u7167\u9879\u76EE\u81EA\u8EAB\u7684\u53D1\u5E03\u6D41\u7A0B\u64CD\u4F5C
2336
+ - \u5982\u6709\u4EFB\u4F55\u9519\u8BEF\u6216\u5F02\u5E38\uFF0C\u8BB0\u5F55\u5728\u62A5\u544A\u4E2D
2337
+ - \u4E0D\u8981\u8DF3\u8FC7\u4EFB\u4F55\u53D1\u5E03\u524D\u7684\u68C0\u67E5\u6B65\u9AA4`;
2338
+ }
2339
+
2340
+ // src/phases/ReleasePhase.ts
2341
+ var logger8 = logger.child("ReleasePhase");
2342
+ var DETECT_FILENAME = "05-release-detect.json";
2343
+ var REPORT_FILENAME = "06-release-report.md";
2344
+ var ReleasePhase = class extends BasePhase {
2345
+ phaseName = "release";
2346
+ get planDir() {
2347
+ const displayId = Number(this.currentCtx?.demand.sourceRef.displayId ?? 0);
2348
+ return path7.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
2349
+ }
2350
+ /** 暂存当前 ctx 供 planDir getter 使用 */
2351
+ currentCtx;
2406
2352
  /**
2407
- * 根据当前 state,推导每个 phase 的进度状态。
2408
- * 支持通用状态 PhaseRunning/PhaseDone + currentPhase。
2353
+ * 检测结果是否已存在(即是否处于执行模式)。
2409
2354
  */
2410
- derivePhaseStatuses(currentState, currentPhase) {
2411
- const result = {};
2412
- let passedCurrent = false;
2413
- if (currentState === "failed" /* Failed */ && currentPhase) {
2414
- for (const phase of this.def.phases) {
2415
- if (passedCurrent) {
2416
- result[phase.name] = "pending";
2417
- } else if (phase.name === currentPhase) {
2418
- result[phase.name] = "failed";
2419
- passedCurrent = true;
2420
- } else {
2421
- result[phase.name] = "completed";
2422
- }
2423
- }
2424
- return result;
2355
+ hasDetectionResult() {
2356
+ return fs6.existsSync(path7.join(this.planDir, DETECT_FILENAME));
2357
+ }
2358
+ readDetectionFile() {
2359
+ const fp = path7.join(this.planDir, DETECT_FILENAME);
2360
+ if (!fs6.existsSync(fp)) return null;
2361
+ try {
2362
+ return JSON.parse(fs6.readFileSync(fp, "utf-8"));
2363
+ } catch {
2364
+ return null;
2425
2365
  }
2426
- if ((currentState === "phase_running" /* PhaseRunning */ || currentState === "phase_done" /* PhaseDone */) && currentPhase) {
2427
- for (const phase of this.def.phases) {
2428
- if (passedCurrent) {
2429
- result[phase.name] = "pending";
2430
- } else if (phase.name === currentPhase) {
2431
- result[phase.name] = currentState === "phase_running" /* PhaseRunning */ ? "in_progress" : "completed";
2432
- passedCurrent = currentState === "phase_running" /* PhaseRunning */;
2433
- if (currentState === "phase_done" /* PhaseDone */) passedCurrent = true;
2434
- } else {
2435
- result[phase.name] = "completed";
2436
- }
2437
- }
2438
- return result;
2366
+ }
2367
+ writeDetectionFile(result) {
2368
+ const dir = this.planDir;
2369
+ if (!fs6.existsSync(dir)) {
2370
+ fs6.mkdirSync(dir, { recursive: true });
2439
2371
  }
2440
- if ((currentState === "phase_waiting" /* PhaseWaiting */ || currentState === "phase_approved" /* PhaseApproved */) && currentPhase) {
2441
- for (const phase of this.def.phases) {
2442
- if (passedCurrent) {
2443
- result[phase.name] = "pending";
2444
- } else if (phase.name === currentPhase) {
2445
- const isGatedAi = phase.kind === "ai" && phase.approvedState;
2446
- result[phase.name] = currentState === "phase_waiting" /* PhaseWaiting */ || currentState === "phase_approved" /* PhaseApproved */ && isGatedAi ? "in_progress" : "completed";
2447
- passedCurrent = true;
2448
- } else {
2449
- result[phase.name] = "completed";
2450
- }
2451
- }
2452
- return result;
2372
+ fs6.writeFileSync(path7.join(dir, DETECT_FILENAME), JSON.stringify(result, null, 2), "utf-8");
2373
+ }
2374
+ getResultFiles(_ctx) {
2375
+ if (this.hasDetectionResult()) {
2376
+ return [{ filename: REPORT_FILENAME, label: "\u53D1\u5E03\u62A5\u544A" }];
2453
2377
  }
2454
- if (currentState === "completed" /* Completed */ || currentState === "resolving_conflict" /* ResolvingConflict */) {
2455
- for (const phase of this.def.phases) {
2456
- result[phase.name] = "completed";
2457
- }
2458
- return result;
2378
+ return [{ filename: DETECT_FILENAME, label: "\u53D1\u5E03\u68C0\u6D4B\u62A5\u544A" }];
2379
+ }
2380
+ buildPrompt(ctx) {
2381
+ const dc = demandToPromptContext(ctx.demand);
2382
+ const pc = {
2383
+ issueTitle: dc.title,
2384
+ issueDescription: dc.description,
2385
+ issueIid: Number(dc.displayId),
2386
+ supplementText: dc.supplementText || void 0,
2387
+ workspace: ctx.workspace
2388
+ };
2389
+ if (this.hasDetectionResult()) {
2390
+ const detect = this.readDetectionFile();
2391
+ return releaseExecPrompt(pc, detect);
2459
2392
  }
2460
- for (const phase of this.def.phases) {
2461
- if (passedCurrent) {
2462
- result[phase.name] = "pending";
2463
- continue;
2393
+ return releaseDetectPrompt(pc);
2394
+ }
2395
+ async execute(ctx) {
2396
+ this.currentCtx = ctx;
2397
+ const isDetect = !this.hasDetectionResult();
2398
+ if (isDetect) {
2399
+ const cache = new ReleaseDetectCache(resolveDataDir());
2400
+ const projectPath = this.config.gongfeng.projectPath;
2401
+ const ttlMs = this.config.release.detectCacheTtlMs;
2402
+ const cached = cache.get(projectPath, ttlMs);
2403
+ if (cached) {
2404
+ logger8.info("Using cached release detection result", {
2405
+ projectPath,
2406
+ hasCapability: cached.hasReleaseCapability,
2407
+ method: cached.releaseMethod
2408
+ });
2409
+ await this.hooks.onPhaseStart(this.phaseName);
2410
+ this.writeDetectionFile(cached);
2411
+ this.plan.updatePhaseProgress(this.phaseName, "completed");
2412
+ try {
2413
+ await this.git.add(["."]);
2414
+ await this.git.commit(`chore: release detection (cached) for issue-${ctx.demand.sourceRef.displayId}`);
2415
+ } catch {
2416
+ }
2417
+ await this.hooks.onPhaseDone(this.phaseName);
2418
+ return {
2419
+ success: true,
2420
+ output: `Release detection cached: ${cached.hasReleaseCapability ? cached.releaseMethod ?? "found" : "no capability"}`,
2421
+ exitCode: 0,
2422
+ gateRequested: cached.hasReleaseCapability,
2423
+ hasReleaseCapability: cached.hasReleaseCapability
2424
+ };
2464
2425
  }
2465
- if (phase.startState === currentState) {
2466
- result[phase.name] = "in_progress";
2467
- passedCurrent = true;
2468
- } else if (phase.doneState === currentState || phase.approvedState === currentState) {
2469
- result[phase.name] = "completed";
2470
- } else {
2471
- result[phase.name] = "pending";
2426
+ }
2427
+ const result = await super.execute(ctx);
2428
+ if (isDetect && result.success) {
2429
+ const detected = this.readDetectionFile();
2430
+ if (detected) {
2431
+ const cache = new ReleaseDetectCache(resolveDataDir());
2432
+ const projectPath = this.config.gongfeng.projectPath;
2433
+ cache.set({
2434
+ ...detected,
2435
+ projectPath,
2436
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString()
2437
+ });
2438
+ logger8.info("Release detection completed", {
2439
+ hasCapability: detected.hasReleaseCapability,
2440
+ method: detected.releaseMethod,
2441
+ artifacts: detected.artifacts
2442
+ });
2443
+ return {
2444
+ ...result,
2445
+ gateRequested: detected.hasReleaseCapability,
2446
+ hasReleaseCapability: detected.hasReleaseCapability
2447
+ };
2472
2448
  }
2449
+ return { ...result, gateRequested: false, hasReleaseCapability: false };
2473
2450
  }
2474
- return result;
2475
- }
2476
- // ─── Pipeline Protocol queries (P1) ───
2477
- /**
2478
- * 返回可被用户单独重试的阶段名列表。
2479
- * 优先使用 spec.retryable,未声明时默认 kind === 'ai'。
2480
- */
2481
- getRetryablePhases() {
2482
- return this.def.phases.filter((spec) => spec.retryable ?? spec.kind === "ai").map((spec) => spec.name);
2483
- }
2484
- /**
2485
- * 判断指定阶段是否可重试。
2486
- */
2487
- isRetryable(phaseName) {
2488
- const spec = this.def.phases.find((p) => p.name === phaseName);
2489
- if (!spec) return false;
2490
- return spec.retryable ?? spec.kind === "ai";
2491
- }
2492
- /**
2493
- * 查找 gate 类型的阶段。
2494
- */
2495
- getGatePhase() {
2496
- return this.def.phases.find((p) => p.kind === "gate");
2451
+ return { ...result, gateRequested: false };
2497
2452
  }
2498
- /**
2499
- * 判断指定阶段完成后是否应启动预览服务器。
2500
- */
2501
- shouldDeployPreview(phaseName) {
2502
- const spec = this.def.phases.find((p) => p.name === phaseName);
2503
- return spec?.deploysPreview ?? false;
2453
+ };
2454
+
2455
+ // src/phases/UatPhase.ts
2456
+ function getDefaultHost() {
2457
+ return getLocalIP();
2458
+ }
2459
+ var UatPhase = class extends BasePhase {
2460
+ phaseName = "uat";
2461
+ getResultFiles(_ctx) {
2462
+ return [{ filename: "03-uat-report.md", label: "UAT \u62A5\u544A" }];
2504
2463
  }
2505
- /**
2506
- * 收集所有阶段的产物文件,扁平化为单一列表。
2507
- */
2508
- collectArtifacts() {
2509
- return this.def.phases.flatMap((spec) => spec.artifacts ?? []);
2464
+ buildPrompt(ctx) {
2465
+ const pc = demandToPromptContext(ctx.demand);
2466
+ const promptCtx = {
2467
+ issueTitle: pc.title,
2468
+ issueDescription: pc.description,
2469
+ issueIid: Number(pc.displayId),
2470
+ workspace: ctx.workspace
2471
+ };
2472
+ const hasUatTool = !!this.config.e2e.uatVendorDir;
2473
+ const systemE2e = this.hooks.isE2eEnabled();
2474
+ const e2ePorts = systemE2e && ctx.ports ? {
2475
+ backendPort: ctx.ports.backendPort,
2476
+ frontendPort: ctx.ports.frontendPort,
2477
+ host: this.config.preview.host || getDefaultHost()
2478
+ } : void 0;
2479
+ const uatTool = hasUatTool ? { vendorDir: this.config.e2e.uatVendorDir, configFile: this.config.e2e.uatConfigFile } : void 0;
2480
+ const e2eSuffix = e2eVerifyPromptSuffix(promptCtx, e2ePorts, uatTool);
2481
+ return `# UAT/E2E \u9A8C\u8BC1\u9636\u6BB5
2482
+
2483
+ ## \u4EFB\u52A1\u80CC\u666F
2484
+ - Issue: #${promptCtx.issueIid} ${promptCtx.issueTitle}
2485
+ - \u672C\u9636\u6BB5\u4E13\u6CE8\u4E8E E2E/UAT \u7AEF\u5BF9\u7AEF\u9A8C\u8BC1\uFF0Clint/build/test \u68C0\u67E5\u5DF2\u5728\u4E0A\u4E00\u9A8C\u8BC1\u9636\u6BB5\u5B8C\u6210\u3002
2486
+
2487
+ ## \u4EA7\u7269\u8981\u6C42
2488
+ \u8BF7\u5C06 E2E \u9A8C\u8BC1\u7ED3\u679C\u5199\u5165 \`.claude-plan/issue-${promptCtx.issueIid}/03-uat-report.md\`\uFF0C\u5305\u542B\uFF1A
2489
+ - \u6D4B\u8BD5\u573A\u666F\u6E05\u5355
2490
+ - \u6267\u884C\u7ED3\u679C\uFF08\u901A\u8FC7/\u5931\u8D25\uFF09
2491
+ - \u622A\u56FE\u8DEF\u5F84\uFF08\u5982\u6709\uFF09
2492
+ - \u5931\u8D25\u539F\u56E0\u5206\u6790\uFF08\u5982\u6709\u5931\u8D25\uFF09
2493
+
2494
+ ${e2eSuffix}`;
2510
2495
  }
2511
- /**
2512
- * 返回所有阶段的名称和标签(保持定义顺序)。
2513
- */
2514
- getPhaseDefs() {
2515
- return this.def.phases.map((p) => ({ name: p.name, label: p.label }));
2496
+ };
2497
+
2498
+ // src/phases/PhaseFactory.ts
2499
+ var PHASE_REGISTRY = /* @__PURE__ */ new Map();
2500
+ function registerPhase(name, ctor) {
2501
+ PHASE_REGISTRY.set(name, ctor);
2502
+ }
2503
+ function registerBuiltinPhases() {
2504
+ PHASE_REGISTRY.set("plan", PlanPhase);
2505
+ PHASE_REGISTRY.set("build", BuildPhase);
2506
+ PHASE_REGISTRY.set("verify", VerifyPhase);
2507
+ PHASE_REGISTRY.set("uat", UatPhase);
2508
+ PHASE_REGISTRY.set("release", ReleasePhase);
2509
+ }
2510
+ registerBuiltinPhases();
2511
+ function createPhase(name, ...args) {
2512
+ const Ctor = PHASE_REGISTRY.get(name);
2513
+ if (!Ctor) {
2514
+ throw new PhaseNotRegisteredError(name, [...PHASE_REGISTRY.keys()]);
2516
2515
  }
2517
- /**
2518
- * 返回所有 kind === 'ai' 的阶段名(可执行阶段)。
2519
- */
2520
- getExecutablePhaseNames() {
2521
- return this.def.phases.filter((spec) => spec.kind === "ai").map((spec) => spec.name);
2516
+ return new Ctor(...args);
2517
+ }
2518
+ function validatePhaseRegistry(phaseNames) {
2519
+ const missing = phaseNames.filter((name) => !PHASE_REGISTRY.has(name));
2520
+ if (missing.length > 0) {
2521
+ throw new UnregisteredPhasesError(missing, [...PHASE_REGISTRY.keys()]);
2522
2522
  }
2523
- };
2523
+ }
2524
2524
 
2525
2525
  // src/pipeline/PipelineDefinition.ts
2526
2526
  var pipelineRegistry = /* @__PURE__ */ new Map();
@@ -3171,38 +3171,8 @@ var PortAllocator = class {
3171
3171
  import { spawn } from "child_process";
3172
3172
  import fs10 from "fs";
3173
3173
  import path11 from "path";
3174
- import net2 from "net";
3175
3174
  var logger12 = logger.child("DevServerManager");
3176
- var DEFAULT_OPTIONS2 = {
3177
- healthCheckTimeoutMs: 12e4,
3178
- healthCheckIntervalMs: 3e3
3179
- };
3180
- function waitForPort(port, timeoutMs, intervalMs) {
3181
- return new Promise((resolve, reject) => {
3182
- const deadline = Date.now() + timeoutMs;
3183
- const check = () => {
3184
- if (Date.now() > deadline) {
3185
- reject(new Error(`Port ${port} not ready after ${timeoutMs}ms`));
3186
- return;
3187
- }
3188
- const socket = net2.createConnection({ host: "127.0.0.1", port });
3189
- socket.setTimeout(5e3);
3190
- socket.on("connect", () => {
3191
- socket.destroy();
3192
- resolve();
3193
- });
3194
- socket.on("error", () => {
3195
- socket.destroy();
3196
- setTimeout(check, intervalMs);
3197
- });
3198
- socket.on("timeout", () => {
3199
- socket.destroy();
3200
- setTimeout(check, intervalMs);
3201
- });
3202
- };
3203
- check();
3204
- });
3205
- }
3175
+ var DEFAULT_OPTIONS2 = {};
3206
3176
  var DevServerManager = class {
3207
3177
  servers = /* @__PURE__ */ new Map();
3208
3178
  options;
@@ -3287,29 +3257,9 @@ var DevServerManager = class {
3287
3257
  frontendLog
3288
3258
  };
3289
3259
  this.servers.set(wtCtx.issueIid, serverSet);
3290
- logger12.info("Waiting for servers to become healthy", { issueIid: wtCtx.issueIid });
3291
- try {
3292
- await Promise.all([
3293
- waitForPort(
3294
- ports.backendPort,
3295
- this.options.healthCheckTimeoutMs,
3296
- this.options.healthCheckIntervalMs
3297
- ),
3298
- waitForPort(
3299
- ports.frontendPort,
3300
- this.options.healthCheckTimeoutMs,
3301
- this.options.healthCheckIntervalMs
3302
- )
3303
- ]);
3304
- logger12.info("Dev servers healthy", { issueIid: wtCtx.issueIid, ...ports });
3305
- } catch (err) {
3306
- logger12.error("Dev servers failed health check, cleaning up", {
3307
- issueIid: wtCtx.issueIid,
3308
- error: err.message
3309
- });
3310
- this.stopServers(wtCtx.issueIid);
3311
- throw err;
3312
- }
3260
+ logger12.info("Dev servers spawned, waiting for startup", { issueIid: wtCtx.issueIid, ...ports });
3261
+ await new Promise((r) => setTimeout(r, 1e4));
3262
+ logger12.info("Dev servers startup grace period done", { issueIid: wtCtx.issueIid });
3313
3263
  }
3314
3264
  stopServers(issueIid) {
3315
3265
  const set = this.servers.get(issueIid);
@@ -4424,7 +4374,10 @@ var PipelineOrchestrator = class {
4424
4374
  throw new InvalidPhaseError(phase);
4425
4375
  }
4426
4376
  logger19.info("Retrying issue from phase", { issueIid, phase });
4427
- this.tracker.resetToPhase(issueIid, phase, issueDef);
4377
+ const ok = this.tracker.resetToPhase(issueIid, phase, issueDef);
4378
+ if (!ok) {
4379
+ throw new InvalidPhaseError(phase);
4380
+ }
4428
4381
  }
4429
4382
  getIssueSpecificPipelineDef(record) {
4430
4383
  if (record.pipelineMode) {
@@ -5145,4 +5098,4 @@ export {
5145
5098
  PipelineOrchestrator,
5146
5099
  BrainstormService
5147
5100
  };
5148
- //# sourceMappingURL=chunk-CUZCZUMA.js.map
5101
+ //# sourceMappingURL=chunk-7D2NH37P.js.map