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