@xdevops/issue-auto-finish 1.0.81 → 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.
- package/dist/{chunk-S3ULUOTM.js → chunk-7D2NH37P.js} +1084 -1081
- package/dist/chunk-7D2NH37P.js.map +1 -0
- package/dist/{chunk-JFTXTWGO.js → chunk-7EK2XSRA.js} +2 -2
- package/dist/{chunk-7UBPPE3O.js → chunk-MWK22QHH.js} +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/lib.js +1 -1
- package/dist/orchestrator/PipelineOrchestrator.d.ts.map +1 -1
- package/dist/{restart-B4QEKZRK.js → restart-IEBTRX4Q.js} +2 -2
- package/dist/run.js +2 -2
- package/dist/{start-FX2HQGEP.js → start-DIPQGDKA.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-S3ULUOTM.js.map +0 -1
- /package/dist/{chunk-JFTXTWGO.js.map → chunk-7EK2XSRA.js.map} +0 -0
- /package/dist/{chunk-7UBPPE3O.js.map → chunk-MWK22QHH.js.map} +0 -0
- /package/dist/{restart-B4QEKZRK.js.map → restart-IEBTRX4Q.js.map} +0 -0
- /package/dist/{start-FX2HQGEP.js.map → start-DIPQGDKA.js.map} +0 -0
|
@@ -528,6 +528,372 @@ var IssueState = /* @__PURE__ */ ((IssueState2) => {
|
|
|
528
528
|
return IssueState2;
|
|
529
529
|
})(IssueState || {});
|
|
530
530
|
|
|
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);
|
|
547
|
+
}
|
|
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");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
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);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
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" };
|
|
598
|
+
}
|
|
599
|
+
if (state === "phase_done" /* PhaseDone */ && currentPhase) {
|
|
600
|
+
return { action: currentPhase, status: "ready" };
|
|
601
|
+
}
|
|
602
|
+
if (state === "phase_waiting" /* PhaseWaiting */ && currentPhase) {
|
|
603
|
+
return { action: currentPhase, status: "waiting" };
|
|
604
|
+
}
|
|
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" };
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* 将 action + status 反向映射为 IssueState。
|
|
614
|
+
*/
|
|
615
|
+
toIssueState(action, status) {
|
|
616
|
+
return this.actionToState.get(`${action}:${status}`);
|
|
617
|
+
}
|
|
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";
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* 进行中:running (包含通用 PhaseRunning)
|
|
628
|
+
*/
|
|
629
|
+
isInProgress(state) {
|
|
630
|
+
if (state === "phase_running" /* PhaseRunning */) return true;
|
|
631
|
+
return this.resolve(state).status === "running";
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* 阻塞中:waiting
|
|
635
|
+
*/
|
|
636
|
+
isBlocked(state) {
|
|
637
|
+
if (state === "phase_waiting" /* PhaseWaiting */) return true;
|
|
638
|
+
return this.resolve(state).status === "waiting";
|
|
639
|
+
}
|
|
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
|
+
}
|
|
669
|
+
}
|
|
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;
|
|
706
|
+
}
|
|
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;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* 获取某个 phase 的状态三元组。
|
|
721
|
+
*/
|
|
722
|
+
getPhaseStates(phaseName) {
|
|
723
|
+
return this.phaseStatesMap.get(phaseName);
|
|
724
|
+
}
|
|
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 });
|
|
736
|
+
}
|
|
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;
|
|
743
|
+
}
|
|
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;
|
|
777
|
+
}
|
|
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
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
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;
|
|
831
|
+
}
|
|
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
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return result;
|
|
847
|
+
}
|
|
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);
|
|
855
|
+
}
|
|
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";
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* 查找 gate 类型的阶段。
|
|
866
|
+
*/
|
|
867
|
+
getGatePhase() {
|
|
868
|
+
return this.def.phases.find((p) => p.kind === "gate");
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* 判断指定阶段完成后是否应启动预览服务器。
|
|
872
|
+
*/
|
|
873
|
+
shouldDeployPreview(phaseName) {
|
|
874
|
+
const spec = this.def.phases.find((p) => p.name === phaseName);
|
|
875
|
+
return spec?.deploysPreview ?? false;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* 收集所有阶段的产物文件,扁平化为单一列表。
|
|
879
|
+
*/
|
|
880
|
+
collectArtifacts() {
|
|
881
|
+
return this.def.phases.flatMap((spec) => spec.artifacts ?? []);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* 返回所有阶段的名称和标签(保持定义顺序)。
|
|
885
|
+
*/
|
|
886
|
+
getPhaseDefs() {
|
|
887
|
+
return this.def.phases.map((p) => ({ name: p.name, label: p.label }));
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* 返回所有 kind === 'ai' 的阶段名(可执行阶段)。
|
|
891
|
+
*/
|
|
892
|
+
getExecutablePhaseNames() {
|
|
893
|
+
return this.def.phases.filter((spec) => spec.kind === "ai").map((spec) => spec.name);
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
531
897
|
// src/tracker/IssueTracker.ts
|
|
532
898
|
var logger5 = logger.child("IssueTracker");
|
|
533
899
|
var STATE_MIGRATION_MAP = {
|
|
@@ -738,7 +1104,7 @@ var IssueTracker = class extends BaseTracker {
|
|
|
738
1104
|
resetToPhase(issueIid, phase, def) {
|
|
739
1105
|
const record = this.collection[this.key(issueIid)];
|
|
740
1106
|
if (!record) return false;
|
|
741
|
-
const lm =
|
|
1107
|
+
const lm = new ActionLifecycleManager(def);
|
|
742
1108
|
const targetState = lm.getPhasePreState(phase);
|
|
743
1109
|
if (!targetState) return false;
|
|
744
1110
|
record.state = targetState;
|
|
@@ -1341,1186 +1707,820 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
|
|
|
1341
1707
|
});
|
|
1342
1708
|
return this.runAI(displayId, fullPrompt, onInputRequired, void 0, ctx);
|
|
1343
1709
|
}
|
|
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));
|
|
1361
|
-
}
|
|
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;
|
|
1371
|
-
}
|
|
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;
|
|
1375
|
-
}
|
|
1376
|
-
return false;
|
|
1377
|
-
}
|
|
1378
|
-
persistSessionId(sessionId) {
|
|
1379
|
-
if (sessionId) {
|
|
1380
|
-
this.plan.updatePhaseSessionId(this.phaseName, sessionId);
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
// ── Hook dispatch methods ──
|
|
1384
|
-
async notifyPhaseStart() {
|
|
1385
|
-
await this.hooks.onPhaseStart(this.phaseName);
|
|
1386
|
-
}
|
|
1387
|
-
async notifyPhaseDone() {
|
|
1388
|
-
await this.hooks.onPhaseDone(this.phaseName);
|
|
1389
|
-
}
|
|
1390
|
-
async notifyPhaseFailed(error) {
|
|
1391
|
-
await this.hooks.onPhaseFailed(this.phaseName, error);
|
|
1392
|
-
}
|
|
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
|
-
}
|
|
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
|
-
};
|
|
1439
|
-
}
|
|
1440
|
-
async resolveRules(ctx) {
|
|
1441
|
-
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
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
} else {
|
|
1454
|
-
const rulesDir = path4.join(this.plan.baseDir, ".cursor", "rules");
|
|
1455
|
-
await resolver.loadRules(rulesDir);
|
|
1456
|
-
}
|
|
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);
|
|
1463
|
-
}
|
|
1464
|
-
} catch (err) {
|
|
1465
|
-
this.logger.warn("Failed to resolve MDC rules", { error: err.message });
|
|
1466
|
-
}
|
|
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;
|
|
1478
|
-
}
|
|
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 });
|
|
1496
|
-
}
|
|
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
|
-
}
|
|
1503
|
-
}
|
|
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
|
-
}
|
|
1513
|
-
}
|
|
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 });
|
|
1534
|
-
}
|
|
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
|
-
}
|
|
1556
|
-
} 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
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
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}`);
|
|
1595
|
-
}
|
|
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");
|
|
1599
|
-
}
|
|
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
|
-
};
|
|
1710
|
+
return result;
|
|
1611
1711
|
}
|
|
1612
1712
|
/**
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1713
|
+
* 判断 AI 执行失败是否为永久性错误(不可重试)。
|
|
1714
|
+
* 模型不存在、API key 无效等配置问题重试不会成功。
|
|
1615
1715
|
*/
|
|
1616
|
-
|
|
1617
|
-
const
|
|
1618
|
-
const
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
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));
|
|
1627
1727
|
}
|
|
1628
1728
|
/**
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1729
|
+
* Heuristic: a resume failure is typically an immediate process exit
|
|
1730
|
+
* (exit code != 0, empty output) caused by an invalid/expired session ID.
|
|
1631
1731
|
*/
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
total: parseInt(match[2], 10)
|
|
1638
|
-
};
|
|
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;
|
|
1639
1737
|
}
|
|
1640
|
-
|
|
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" }];
|
|
1651
|
-
}
|
|
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
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
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
|
|
1682
|
-
});
|
|
1683
|
-
return { ...result, verifyReport: parsed };
|
|
1684
|
-
}
|
|
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;
|
|
1685
1741
|
}
|
|
1686
|
-
return
|
|
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);
|
|
1742
|
+
return false;
|
|
1697
1743
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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;
|
|
1744
|
+
persistSessionId(sessionId) {
|
|
1745
|
+
if (sessionId) {
|
|
1746
|
+
this.plan.updatePhaseSessionId(this.phaseName, sessionId);
|
|
1709
1747
|
}
|
|
1710
1748
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
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;
|
|
1720
|
-
}
|
|
1749
|
+
// ── Hook dispatch methods ──
|
|
1750
|
+
async notifyPhaseStart() {
|
|
1751
|
+
await this.hooks.onPhaseStart(this.phaseName);
|
|
1721
1752
|
}
|
|
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" }];
|
|
1753
|
+
async notifyPhaseDone() {
|
|
1754
|
+
await this.hooks.onPhaseDone(this.phaseName);
|
|
1729
1755
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1756
|
+
async notifyPhaseFailed(error) {
|
|
1757
|
+
await this.hooks.onPhaseFailed(this.phaseName, error);
|
|
1732
1758
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
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);
|
|
1748
|
-
}
|
|
1749
|
-
const caps = getRunnerCapabilities(this.config.ai.mode);
|
|
1750
|
-
if (!caps?.nativePlanMode) {
|
|
1751
|
-
basePrompt = `${t("prompt.planModeFallback")}
|
|
1752
|
-
|
|
1753
|
-
${basePrompt}`;
|
|
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 });
|
|
1754
1764
|
}
|
|
1755
|
-
return basePrompt;
|
|
1756
1765
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
if (await repoGit.hasChanges()) {
|
|
1768
|
-
hasAnyChanges = true;
|
|
1769
|
-
break;
|
|
1770
|
-
}
|
|
1766
|
+
/**
|
|
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.
|
|
1770
|
+
*/
|
|
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");
|
|
1771
1776
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
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 });
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
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)
|
|
1777
|
+
this.logger.info("ACP plan-approval requested, delegating to review gate", {
|
|
1778
|
+
issueIid: displayId,
|
|
1779
|
+
phase: this.phaseName
|
|
1794
1780
|
});
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
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
|
+
};
|
|
1819
1805
|
}
|
|
1820
|
-
|
|
1821
|
-
* 读取缓存。返回 null 如果不存在、已过期或校验失败。
|
|
1822
|
-
*/
|
|
1823
|
-
get(projectPath, ttlMs) {
|
|
1824
|
-
const fp = this.filePath(projectPath);
|
|
1806
|
+
async resolveRules(ctx) {
|
|
1825
1807
|
try {
|
|
1826
|
-
|
|
1827
|
-
const
|
|
1828
|
-
const
|
|
1829
|
-
if (
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
|
1834
|
-
if (
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1851
|
-
|
|
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;
|
|
1844
|
+
}
|
|
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 });
|
|
1852
1862
|
}
|
|
1853
|
-
fs5.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
|
|
1854
|
-
logger7.debug("Release detect cache written", { projectPath: result.projectPath, path: fp });
|
|
1855
1863
|
} catch (err) {
|
|
1856
|
-
|
|
1864
|
+
this.logger.warn("Failed to sync result to issue", { error: err.message });
|
|
1865
|
+
await this.notifyComment(
|
|
1866
|
+
issueProgressComment(this.phaseName, "completed")
|
|
1867
|
+
);
|
|
1857
1868
|
}
|
|
1858
1869
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
const fp = this.filePath(projectPath);
|
|
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;
|
|
1864
1874
|
try {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1875
|
+
return fs3.readFileSync(filePath, "utf-8");
|
|
1876
|
+
} catch {
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
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;
|
|
1869
1890
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
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
|
+
}
|
|
1895
|
+
}
|
|
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 });
|
|
1874
1900
|
}
|
|
1875
1901
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
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
|
-
\`\`\`
|
|
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;
|
|
1908
|
+
try {
|
|
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
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
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);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1967
1931
|
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
- \u4E0D\u8981\u8DF3\u8FC7\u4EFB\u4F55\u53D1\u5E03\u524D\u7684\u68C0\u67E5\u6B65\u9AA4`;
|
|
1972
|
-
}
|
|
1932
|
+
// src/phases/VerifyPhase.ts
|
|
1933
|
+
import fs4 from "fs";
|
|
1934
|
+
import path5 from "path";
|
|
1973
1935
|
|
|
1974
|
-
// src/
|
|
1975
|
-
var
|
|
1976
|
-
var
|
|
1977
|
-
var
|
|
1978
|
-
var
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
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 {
|
|
1943
|
+
/**
|
|
1944
|
+
* 解析验证报告内容。
|
|
1945
|
+
*
|
|
1946
|
+
* @param reportContent 验证报告的完整 Markdown 文本
|
|
1947
|
+
*/
|
|
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}`);
|
|
1961
|
+
}
|
|
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");
|
|
1965
|
+
}
|
|
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
|
+
};
|
|
1983
1977
|
}
|
|
1984
|
-
/** 暂存当前 ctx 供 planDir getter 使用 */
|
|
1985
|
-
currentCtx;
|
|
1986
1978
|
/**
|
|
1987
|
-
*
|
|
1979
|
+
* 从 plan 文件中解析 Todolist 完成度。
|
|
1980
|
+
* 统计 `- [x]` (已完成) 和 `- [ ]` (未完成) 的数量。
|
|
1988
1981
|
*/
|
|
1989
|
-
|
|
1990
|
-
|
|
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
|
+
};
|
|
1991
1993
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1994
|
+
/**
|
|
1995
|
+
* 从报告中提取 Todolist 统计数据。
|
|
1996
|
+
* 格式: "Todolist 检查: X/Y 项完成"
|
|
1997
|
+
*/
|
|
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
|
+
};
|
|
1999
2005
|
}
|
|
2006
|
+
return null;
|
|
2000
2007
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
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" }];
|
|
2007
2017
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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
|
+
}
|
|
2011
2051
|
}
|
|
2012
|
-
return
|
|
2052
|
+
return result;
|
|
2013
2053
|
}
|
|
2014
2054
|
buildPrompt(ctx) {
|
|
2015
|
-
const
|
|
2016
|
-
const
|
|
2017
|
-
issueTitle:
|
|
2018
|
-
issueDescription:
|
|
2019
|
-
issueIid: Number(
|
|
2020
|
-
supplementText: dc.supplementText || void 0,
|
|
2055
|
+
const pc = demandToPromptContext(ctx.demand);
|
|
2056
|
+
const promptCtx = {
|
|
2057
|
+
issueTitle: pc.title,
|
|
2058
|
+
issueDescription: pc.description,
|
|
2059
|
+
issueIid: Number(pc.displayId),
|
|
2021
2060
|
workspace: ctx.workspace
|
|
2022
2061
|
};
|
|
2023
|
-
|
|
2024
|
-
const detect = this.readDetectionFile();
|
|
2025
|
-
return releaseExecPrompt(pc, detect);
|
|
2026
|
-
}
|
|
2027
|
-
return releaseDetectPrompt(pc);
|
|
2062
|
+
return ctx.pipelineMode === "plan-mode" ? planModeVerifyPrompt(promptCtx) : verifyPrompt(promptCtx);
|
|
2028
2063
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
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");
|
|
2046
|
-
try {
|
|
2047
|
-
await this.git.add(["."]);
|
|
2048
|
-
await this.git.commit(`chore: release detection (cached) for issue-${ctx.demand.sourceRef.displayId}`);
|
|
2049
|
-
} catch {
|
|
2050
|
-
}
|
|
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
|
-
}
|
|
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;
|
|
2060
2075
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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
|
-
};
|
|
2082
|
-
}
|
|
2083
|
-
return { ...result, gateRequested: false, hasReleaseCapability: false };
|
|
2076
|
+
}
|
|
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;
|
|
2084
2086
|
}
|
|
2085
|
-
return { ...result, gateRequested: false };
|
|
2086
2087
|
}
|
|
2087
|
-
};
|
|
2088
|
-
|
|
2089
|
-
// src/phases/
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
return
|
|
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";
|
|
2097
2098
|
}
|
|
2098
2099
|
buildPrompt(ctx) {
|
|
2099
2100
|
const pc = demandToPromptContext(ctx.demand);
|
|
2101
|
+
const history = this.plan.readReviewHistory();
|
|
2100
2102
|
const promptCtx = {
|
|
2101
2103
|
issueTitle: pc.title,
|
|
2102
2104
|
issueDescription: pc.description,
|
|
2103
2105
|
issueIid: Number(pc.displayId),
|
|
2106
|
+
supplementText: pc.supplementText || void 0,
|
|
2104
2107
|
workspace: ctx.workspace
|
|
2105
2108
|
};
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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
|
|
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")}
|
|
2127
2118
|
|
|
2128
|
-
${
|
|
2119
|
+
${basePrompt}`;
|
|
2120
|
+
}
|
|
2121
|
+
return basePrompt;
|
|
2129
2122
|
}
|
|
2130
2123
|
};
|
|
2131
2124
|
|
|
2132
|
-
// src/phases/
|
|
2133
|
-
var
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
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
|
-
}
|
|
2158
|
-
|
|
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");
|
|
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;
|
|
2204
2136
|
}
|
|
2205
2137
|
}
|
|
2138
|
+
} else {
|
|
2139
|
+
hasAnyChanges = await this.git.hasChanges();
|
|
2206
2140
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
this.
|
|
2211
|
-
}
|
|
2212
|
-
const key = `${action}:${status}`;
|
|
2213
|
-
if (!this.actionToState.has(key)) {
|
|
2214
|
-
this.actionToState.set(key, state);
|
|
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 });
|
|
2215
2145
|
}
|
|
2216
2146
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
return { action: currentPhase, status: "waiting" };
|
|
2232
|
-
}
|
|
2233
|
-
if (state === "phase_approved" /* PhaseApproved */ && currentPhase) {
|
|
2234
|
-
return { action: currentPhase, status: "ready" };
|
|
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
|
+
});
|
|
2235
2161
|
}
|
|
2236
|
-
|
|
2237
|
-
if (mapped) return mapped;
|
|
2238
|
-
return { action: "init", status: "idle" };
|
|
2239
|
-
}
|
|
2240
|
-
/**
|
|
2241
|
-
* 将 action + status 反向映射为 IssueState。
|
|
2242
|
-
*/
|
|
2243
|
-
toIssueState(action, status) {
|
|
2244
|
-
return this.actionToState.get(`${action}:${status}`);
|
|
2245
|
-
}
|
|
2246
|
-
// ─── Classification predicates (替代 3 个 Set 常量 + getDrivableIssues) ───
|
|
2247
|
-
/**
|
|
2248
|
-
* 终态:done | failed | skipped
|
|
2249
|
-
*/
|
|
2250
|
-
isTerminal(state) {
|
|
2251
|
-
const as = this.resolve(state);
|
|
2252
|
-
return as.status === "done" || as.status === "failed" || as.status === "skipped";
|
|
2253
|
-
}
|
|
2254
|
-
/**
|
|
2255
|
-
* 进行中:running (包含通用 PhaseRunning)
|
|
2256
|
-
*/
|
|
2257
|
-
isInProgress(state) {
|
|
2258
|
-
if (state === "phase_running" /* PhaseRunning */) return true;
|
|
2259
|
-
return this.resolve(state).status === "running";
|
|
2260
|
-
}
|
|
2261
|
-
/**
|
|
2262
|
-
* 阻塞中:waiting
|
|
2263
|
-
*/
|
|
2264
|
-
isBlocked(state) {
|
|
2265
|
-
if (state === "phase_waiting" /* PhaseWaiting */) return true;
|
|
2266
|
-
return this.resolve(state).status === "waiting";
|
|
2162
|
+
return base;
|
|
2267
2163
|
}
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
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;
|
|
2296
|
-
}
|
|
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`);
|
|
2297
2185
|
}
|
|
2298
|
-
// ─── Phase navigation (替代 determineStartIndex + getPhasePreState) ───
|
|
2299
2186
|
/**
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2302
|
-
* 从后向前扫描 phases,匹配 currentState 或 failedAtState。
|
|
2303
|
-
* 支持通用状态 PhaseRunning/PhaseDone + currentPhase 的组合。
|
|
2187
|
+
* 读取缓存。返回 null 如果不存在、已过期或校验失败。
|
|
2304
2188
|
*/
|
|
2305
|
-
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
const
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
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;
|
|
2320
|
-
}
|
|
2321
|
-
return idx;
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
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;
|
|
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;
|
|
2328
2198
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
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;
|
|
2331
2203
|
}
|
|
2204
|
+
return data;
|
|
2205
|
+
} catch (err) {
|
|
2206
|
+
logger7.warn("Failed to read release detect cache", { path: fp, error: err.message });
|
|
2207
|
+
return null;
|
|
2332
2208
|
}
|
|
2333
|
-
return 0;
|
|
2334
|
-
}
|
|
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;
|
|
2346
2209
|
}
|
|
2347
2210
|
/**
|
|
2348
|
-
*
|
|
2211
|
+
* 写入缓存。
|
|
2349
2212
|
*/
|
|
2350
|
-
|
|
2351
|
-
|
|
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 });
|
|
2223
|
+
}
|
|
2352
2224
|
}
|
|
2353
|
-
// ─── Display helpers (替代 collectStateLabels + derivePhaseStatuses) ───
|
|
2354
2225
|
/**
|
|
2355
|
-
*
|
|
2356
|
-
*
|
|
2357
|
-
* 对通用状态 PhaseRunning/PhaseDone,需传入 currentPhase 以生成具体标签(如"分析中");
|
|
2358
|
-
* 缺少 currentPhase 时回退到泛化标签(如"阶段执行中")。
|
|
2226
|
+
* 手动失效缓存。
|
|
2359
2227
|
*/
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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;
|
|
2235
|
+
}
|
|
2236
|
+
return false;
|
|
2237
|
+
} catch (err) {
|
|
2238
|
+
logger7.warn("Failed to invalidate release detect cache", { path: fp, error: err.message });
|
|
2239
|
+
return false;
|
|
2368
2240
|
}
|
|
2369
|
-
const labels = this.collectStateLabels();
|
|
2370
|
-
return labels.get(state) ?? state;
|
|
2371
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;
|
|
2372
2352
|
/**
|
|
2373
|
-
*
|
|
2374
|
-
* 为通用状态 PhaseRunning/PhaseDone 生成每个阶段的复合 key 条目。
|
|
2353
|
+
* 检测结果是否已存在(即是否处于执行模式)。
|
|
2375
2354
|
*/
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
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 }));
|
|
2399
|
-
}
|
|
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;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
writeDetectionFile(result) {
|
|
2368
|
+
const dir = this.planDir;
|
|
2369
|
+
if (!fs6.existsSync(dir)) {
|
|
2370
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
2400
2371
|
}
|
|
2401
|
-
|
|
2402
|
-
labels.set("failed" /* Failed */, t("state.failed"));
|
|
2403
|
-
labels.set("resolving_conflict" /* ResolvingConflict */, t("state.resolvingConflict"));
|
|
2404
|
-
return labels;
|
|
2372
|
+
fs6.writeFileSync(path7.join(dir, DETECT_FILENAME), JSON.stringify(result, null, 2), "utf-8");
|
|
2405
2373
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
*/
|
|
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;
|
|
2374
|
+
getResultFiles(_ctx) {
|
|
2375
|
+
if (this.hasDetectionResult()) {
|
|
2376
|
+
return [{ filename: REPORT_FILENAME, label: "\u53D1\u5E03\u62A5\u544A" }];
|
|
2425
2377
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
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);
|
|
2439
2392
|
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
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 {
|
|
2450
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
|
+
};
|
|
2451
2425
|
}
|
|
2452
|
-
return result;
|
|
2453
|
-
}
|
|
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;
|
|
2459
2426
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
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
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
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
|
-
|
|
2509
|
-
|
|
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
|
-
|
|
2515
|
-
|
|
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
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
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();
|
|
@@ -4374,7 +4374,10 @@ var PipelineOrchestrator = class {
|
|
|
4374
4374
|
throw new InvalidPhaseError(phase);
|
|
4375
4375
|
}
|
|
4376
4376
|
logger19.info("Retrying issue from phase", { issueIid, phase });
|
|
4377
|
-
this.tracker.resetToPhase(issueIid, phase, issueDef);
|
|
4377
|
+
const ok = this.tracker.resetToPhase(issueIid, phase, issueDef);
|
|
4378
|
+
if (!ok) {
|
|
4379
|
+
throw new InvalidPhaseError(phase);
|
|
4380
|
+
}
|
|
4378
4381
|
}
|
|
4379
4382
|
getIssueSpecificPipelineDef(record) {
|
|
4380
4383
|
if (record.pipelineMode) {
|
|
@@ -5095,4 +5098,4 @@ export {
|
|
|
5095
5098
|
PipelineOrchestrator,
|
|
5096
5099
|
BrainstormService
|
|
5097
5100
|
};
|
|
5098
|
-
//# sourceMappingURL=chunk-
|
|
5101
|
+
//# sourceMappingURL=chunk-7D2NH37P.js.map
|