@xdevops/issue-auto-finish 1.0.81 → 1.0.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -528,6 +528,372 @@ var IssueState = /* @__PURE__ */ ((IssueState2) => {
528
528
  return IssueState2;
529
529
  })(IssueState || {});
530
530
 
531
+ // src/lifecycle/ActionLifecycleManager.ts
532
+ var ActionLifecycleManager = class {
533
+ /** IssueState → ActionState */
534
+ stateToAction;
535
+ /** "action:status" → IssueState */
536
+ actionToState;
537
+ /** phase name → { startState, doneState, approvedState? } */
538
+ phaseStatesMap;
539
+ /** Ordered phase indices by IssueState for determineResumePhaseIndex */
540
+ def;
541
+ constructor(def) {
542
+ this.def = def;
543
+ this.stateToAction = /* @__PURE__ */ new Map();
544
+ this.actionToState = /* @__PURE__ */ new Map();
545
+ this.phaseStatesMap = /* @__PURE__ */ new Map();
546
+ this.buildMappings(def);
547
+ }
548
+ buildMappings(def) {
549
+ this.addMapping("pending" /* Pending */, "init", "idle");
550
+ this.addMapping("skipped" /* Skipped */, "init", "skipped");
551
+ this.addMapping("branch_created" /* BranchCreated */, "init", "ready");
552
+ this.addMapping("failed" /* Failed */, "init", "failed");
553
+ this.addMapping("deployed" /* Deployed */, "init", "done");
554
+ this.addMapping("resolving_conflict" /* ResolvingConflict */, "conflict", "running");
555
+ for (const spec of def.phases) {
556
+ this.phaseStatesMap.set(spec.name, {
557
+ startState: spec.startState,
558
+ doneState: spec.doneState,
559
+ approvedState: spec.approvedState
560
+ });
561
+ if (spec.kind === "ai") {
562
+ if (spec.startState !== "phase_running" /* PhaseRunning */) {
563
+ this.addMapping(spec.startState, spec.name, "running");
564
+ }
565
+ if (spec.doneState === "completed" /* Completed */) {
566
+ this.addMapping(spec.doneState, spec.name, "done");
567
+ } else if (spec.doneState !== "phase_done" /* PhaseDone */) {
568
+ this.addMapping(spec.doneState, spec.name, "ready");
569
+ }
570
+ } else if (spec.kind === "gate") {
571
+ if (spec.startState !== "phase_waiting" /* PhaseWaiting */) {
572
+ this.addMapping(spec.startState, spec.name, "waiting");
573
+ }
574
+ if (spec.approvedState && spec.approvedState !== "phase_approved" /* PhaseApproved */) {
575
+ this.addMapping(spec.approvedState, spec.name, "ready");
576
+ }
577
+ }
578
+ }
579
+ }
580
+ addMapping(state, action, status) {
581
+ if (!this.stateToAction.has(state)) {
582
+ this.stateToAction.set(state, { action, status });
583
+ }
584
+ const key = `${action}:${status}`;
585
+ if (!this.actionToState.has(key)) {
586
+ this.actionToState.set(key, state);
587
+ }
588
+ }
589
+ // ─── Query API ───
590
+ /**
591
+ * 将 IssueState 解析为语义化的 ActionState。
592
+ *
593
+ * 对于通用状态 PhaseRunning/PhaseDone,需额外传入 currentPhase 来区分具体阶段。
594
+ */
595
+ resolve(state, currentPhase) {
596
+ if (state === "phase_running" /* PhaseRunning */ && currentPhase) {
597
+ return { action: currentPhase, status: "running" };
598
+ }
599
+ if (state === "phase_done" /* PhaseDone */ && currentPhase) {
600
+ return { action: currentPhase, status: "ready" };
601
+ }
602
+ if (state === "phase_waiting" /* PhaseWaiting */ && currentPhase) {
603
+ return { action: currentPhase, status: "waiting" };
604
+ }
605
+ if (state === "phase_approved" /* PhaseApproved */ && currentPhase) {
606
+ return { action: currentPhase, status: "ready" };
607
+ }
608
+ const mapped = this.stateToAction.get(state);
609
+ if (mapped) return mapped;
610
+ return { action: "init", status: "idle" };
611
+ }
612
+ /**
613
+ * 将 action + status 反向映射为 IssueState。
614
+ */
615
+ toIssueState(action, status) {
616
+ return this.actionToState.get(`${action}:${status}`);
617
+ }
618
+ // ─── Classification predicates (替代 3 个 Set 常量 + getDrivableIssues) ───
619
+ /**
620
+ * 终态:done | failed | skipped
621
+ */
622
+ isTerminal(state) {
623
+ const as = this.resolve(state);
624
+ return as.status === "done" || as.status === "failed" || as.status === "skipped";
625
+ }
626
+ /**
627
+ * 进行中:running (包含通用 PhaseRunning)
628
+ */
629
+ isInProgress(state) {
630
+ if (state === "phase_running" /* PhaseRunning */) return true;
631
+ return this.resolve(state).status === "running";
632
+ }
633
+ /**
634
+ * 阻塞中:waiting
635
+ */
636
+ isBlocked(state) {
637
+ if (state === "phase_waiting" /* PhaseWaiting */) return true;
638
+ return this.resolve(state).status === "waiting";
639
+ }
640
+ /**
641
+ * 可驱动判断(集中化 getDrivableIssues 的过滤逻辑)。
642
+ *
643
+ * - idle → 可驱动 (Pending)
644
+ * - ready → 可驱动 (BranchCreated, PhaseDone, PhaseApproved)
645
+ * - failed && attempts < maxRetries → 可驱动
646
+ * - waiting → 不可驱动 (PhaseWaiting)
647
+ * - running → 不可驱动(stalled 由外部叠加)
648
+ * - done/skipped → 不可驱动
649
+ */
650
+ isDrivable(state, attempts, maxRetries, lastErrorRetryable) {
651
+ if (state === "phase_done" /* PhaseDone */) return true;
652
+ if (state === "phase_approved" /* PhaseApproved */) return true;
653
+ if (state === "phase_running" /* PhaseRunning */) return false;
654
+ if (state === "phase_waiting" /* PhaseWaiting */) return false;
655
+ const as = this.resolve(state);
656
+ switch (as.status) {
657
+ case "idle":
658
+ case "ready":
659
+ return true;
660
+ case "failed":
661
+ if (lastErrorRetryable === false) return false;
662
+ return attempts < maxRetries;
663
+ case "waiting":
664
+ case "running":
665
+ case "done":
666
+ case "skipped":
667
+ return false;
668
+ }
669
+ }
670
+ // ─── Phase navigation (替代 determineStartIndex + getPhasePreState) ───
671
+ /**
672
+ * 确定从哪个阶段索引恢复执行(替代 PipelineOrchestrator.determineStartIndex)。
673
+ *
674
+ * 从后向前扫描 phases,匹配 currentState 或 failedAtState。
675
+ * 支持通用状态 PhaseRunning/PhaseDone + currentPhase 的组合。
676
+ */
677
+ determineResumePhaseIndex(currentState, failedAtState, currentPhase) {
678
+ const target = failedAtState || currentState;
679
+ const phases = this.def.phases;
680
+ if ((target === "phase_running" /* PhaseRunning */ || target === "phase_done" /* PhaseDone */) && currentPhase) {
681
+ const idx = phases.findIndex((p) => p.name === currentPhase);
682
+ if (idx >= 0) {
683
+ return target === "phase_done" /* PhaseDone */ ? idx + 1 : idx;
684
+ }
685
+ }
686
+ if ((target === "phase_waiting" /* PhaseWaiting */ || target === "phase_approved" /* PhaseApproved */) && currentPhase) {
687
+ const idx = phases.findIndex((p) => p.name === currentPhase);
688
+ if (idx >= 0) {
689
+ if (target === "phase_approved" /* PhaseApproved */) {
690
+ const spec = phases[idx];
691
+ return spec.kind === "ai" && spec.approvedState ? idx : idx + 1;
692
+ }
693
+ return idx;
694
+ }
695
+ }
696
+ for (let i = phases.length - 1; i >= 0; i--) {
697
+ const spec = phases[i];
698
+ if (spec.kind === "gate" && spec.approvedState === target) {
699
+ return i + 1;
700
+ }
701
+ if (spec.startState === target || spec.doneState === target) {
702
+ return spec.doneState === target ? i + 1 : i;
703
+ }
704
+ }
705
+ return 0;
706
+ }
707
+ /**
708
+ * 获取某个 phase 的前驱状态(即重置到该 phase 需要设置的状态)。
709
+ * 第一个 phase 的前驱是 BranchCreated;后续 phase 的前驱是上一个 phase 的 approvedState 或 doneState。
710
+ */
711
+ getPhasePreState(phaseName) {
712
+ const phases = this.def.phases;
713
+ const idx = phases.findIndex((p) => p.name === phaseName);
714
+ if (idx < 0) return void 0;
715
+ if (idx === 0) return "branch_created" /* BranchCreated */;
716
+ const prev = phases[idx - 1];
717
+ return prev.approvedState ?? prev.doneState;
718
+ }
719
+ /**
720
+ * 获取某个 phase 的状态三元组。
721
+ */
722
+ getPhaseStates(phaseName) {
723
+ return this.phaseStatesMap.get(phaseName);
724
+ }
725
+ // ─── Display helpers (替代 collectStateLabels + derivePhaseStatuses) ───
726
+ /**
727
+ * 解析单条状态的展示标签。
728
+ *
729
+ * 对通用状态 PhaseRunning/PhaseDone,需传入 currentPhase 以生成具体标签(如"分析中");
730
+ * 缺少 currentPhase 时回退到泛化标签(如"阶段执行中")。
731
+ */
732
+ resolveLabel(state, currentPhase) {
733
+ if ((state === "phase_running" /* PhaseRunning */ || state === "phase_done" /* PhaseDone */) && currentPhase) {
734
+ const phaseLabel = t(`pipeline.phase.${currentPhase}`);
735
+ return state === "phase_running" /* PhaseRunning */ ? t("state.phaseDoing", { label: phaseLabel }) : t("state.phaseDone", { label: phaseLabel });
736
+ }
737
+ if ((state === "phase_waiting" /* PhaseWaiting */ || state === "phase_approved" /* PhaseApproved */) && currentPhase) {
738
+ const phaseLabel = t(`pipeline.phase.${currentPhase}`);
739
+ return state === "phase_waiting" /* PhaseWaiting */ ? t("state.phaseWaiting", { label: phaseLabel }) : t("state.phaseApproved", { label: phaseLabel });
740
+ }
741
+ const labels = this.collectStateLabels();
742
+ return labels.get(state) ?? state;
743
+ }
744
+ /**
745
+ * 收集所有状态及其展示标签。
746
+ * 为通用状态 PhaseRunning/PhaseDone 生成每个阶段的复合 key 条目。
747
+ */
748
+ collectStateLabels() {
749
+ const labels = /* @__PURE__ */ new Map();
750
+ labels.set("pending" /* Pending */, t("state.pending"));
751
+ labels.set("skipped" /* Skipped */, t("state.skipped"));
752
+ labels.set("branch_created" /* BranchCreated */, t("state.branchCreated"));
753
+ for (const phase of this.def.phases) {
754
+ const phaseLabel = t(`pipeline.phase.${phase.name}`);
755
+ if (phase.startState === "phase_running" /* PhaseRunning */) {
756
+ labels.set(`phase_running:${phase.name}`, t("state.phaseDoing", { label: phaseLabel }));
757
+ } else if (phase.startState === "phase_waiting" /* PhaseWaiting */) {
758
+ labels.set(`phase_waiting:${phase.name}`, t("state.phaseWaiting", { label: phaseLabel }));
759
+ } else {
760
+ labels.set(phase.startState, t("state.phaseDoing", { label: phaseLabel }));
761
+ }
762
+ if (phase.doneState === "phase_done" /* PhaseDone */) {
763
+ labels.set(`phase_done:${phase.name}`, t("state.phaseDone", { label: phaseLabel }));
764
+ } else if (phase.doneState === "phase_approved" /* PhaseApproved */) {
765
+ labels.set(`phase_approved:${phase.name}`, t("state.phaseApproved", { label: phaseLabel }));
766
+ } else if (phase.doneState !== "completed" /* Completed */) {
767
+ labels.set(phase.doneState, t("state.phaseDone", { label: phaseLabel }));
768
+ }
769
+ if (phase.approvedState && phase.approvedState !== "phase_approved" /* PhaseApproved */ && phase.approvedState !== phase.doneState) {
770
+ labels.set(phase.approvedState, t("state.phaseApproved", { label: phaseLabel }));
771
+ }
772
+ }
773
+ labels.set("completed" /* Completed */, t("state.completed"));
774
+ labels.set("failed" /* Failed */, t("state.failed"));
775
+ labels.set("resolving_conflict" /* ResolvingConflict */, t("state.resolvingConflict"));
776
+ return labels;
777
+ }
778
+ /**
779
+ * 根据当前 state,推导每个 phase 的进度状态。
780
+ * 支持通用状态 PhaseRunning/PhaseDone + currentPhase。
781
+ */
782
+ derivePhaseStatuses(currentState, currentPhase) {
783
+ const result = {};
784
+ let passedCurrent = false;
785
+ if (currentState === "failed" /* Failed */ && currentPhase) {
786
+ for (const phase of this.def.phases) {
787
+ if (passedCurrent) {
788
+ result[phase.name] = "pending";
789
+ } else if (phase.name === currentPhase) {
790
+ result[phase.name] = "failed";
791
+ passedCurrent = true;
792
+ } else {
793
+ result[phase.name] = "completed";
794
+ }
795
+ }
796
+ return result;
797
+ }
798
+ if ((currentState === "phase_running" /* PhaseRunning */ || currentState === "phase_done" /* PhaseDone */) && currentPhase) {
799
+ for (const phase of this.def.phases) {
800
+ if (passedCurrent) {
801
+ result[phase.name] = "pending";
802
+ } else if (phase.name === currentPhase) {
803
+ result[phase.name] = currentState === "phase_running" /* PhaseRunning */ ? "in_progress" : "completed";
804
+ passedCurrent = currentState === "phase_running" /* PhaseRunning */;
805
+ if (currentState === "phase_done" /* PhaseDone */) passedCurrent = true;
806
+ } else {
807
+ result[phase.name] = "completed";
808
+ }
809
+ }
810
+ return result;
811
+ }
812
+ if ((currentState === "phase_waiting" /* PhaseWaiting */ || currentState === "phase_approved" /* PhaseApproved */) && currentPhase) {
813
+ for (const phase of this.def.phases) {
814
+ if (passedCurrent) {
815
+ result[phase.name] = "pending";
816
+ } else if (phase.name === currentPhase) {
817
+ const isGatedAi = phase.kind === "ai" && phase.approvedState;
818
+ result[phase.name] = currentState === "phase_waiting" /* PhaseWaiting */ || currentState === "phase_approved" /* PhaseApproved */ && isGatedAi ? "in_progress" : "completed";
819
+ passedCurrent = true;
820
+ } else {
821
+ result[phase.name] = "completed";
822
+ }
823
+ }
824
+ return result;
825
+ }
826
+ if (currentState === "completed" /* Completed */ || currentState === "resolving_conflict" /* ResolvingConflict */) {
827
+ for (const phase of this.def.phases) {
828
+ result[phase.name] = "completed";
829
+ }
830
+ return result;
831
+ }
832
+ for (const phase of this.def.phases) {
833
+ if (passedCurrent) {
834
+ result[phase.name] = "pending";
835
+ continue;
836
+ }
837
+ if (phase.startState === currentState) {
838
+ result[phase.name] = "in_progress";
839
+ passedCurrent = true;
840
+ } else if (phase.doneState === currentState || phase.approvedState === currentState) {
841
+ result[phase.name] = "completed";
842
+ } else {
843
+ result[phase.name] = "pending";
844
+ }
845
+ }
846
+ return result;
847
+ }
848
+ // ─── Pipeline Protocol queries (P1) ───
849
+ /**
850
+ * 返回可被用户单独重试的阶段名列表。
851
+ * 优先使用 spec.retryable,未声明时默认 kind === 'ai'。
852
+ */
853
+ getRetryablePhases() {
854
+ return this.def.phases.filter((spec) => spec.retryable ?? spec.kind === "ai").map((spec) => spec.name);
855
+ }
856
+ /**
857
+ * 判断指定阶段是否可重试。
858
+ */
859
+ isRetryable(phaseName) {
860
+ const spec = this.def.phases.find((p) => p.name === phaseName);
861
+ if (!spec) return false;
862
+ return spec.retryable ?? spec.kind === "ai";
863
+ }
864
+ /**
865
+ * 查找 gate 类型的阶段。
866
+ */
867
+ getGatePhase() {
868
+ return this.def.phases.find((p) => p.kind === "gate");
869
+ }
870
+ /**
871
+ * 判断指定阶段完成后是否应启动预览服务器。
872
+ */
873
+ shouldDeployPreview(phaseName) {
874
+ const spec = this.def.phases.find((p) => p.name === phaseName);
875
+ return spec?.deploysPreview ?? false;
876
+ }
877
+ /**
878
+ * 收集所有阶段的产物文件,扁平化为单一列表。
879
+ */
880
+ collectArtifacts() {
881
+ return this.def.phases.flatMap((spec) => spec.artifacts ?? []);
882
+ }
883
+ /**
884
+ * 返回所有阶段的名称和标签(保持定义顺序)。
885
+ */
886
+ getPhaseDefs() {
887
+ return this.def.phases.map((p) => ({ name: p.name, label: p.label }));
888
+ }
889
+ /**
890
+ * 返回所有 kind === 'ai' 的阶段名(可执行阶段)。
891
+ */
892
+ getExecutablePhaseNames() {
893
+ return this.def.phases.filter((spec) => spec.kind === "ai").map((spec) => spec.name);
894
+ }
895
+ };
896
+
531
897
  // src/tracker/IssueTracker.ts
532
898
  var logger5 = logger.child("IssueTracker");
533
899
  var STATE_MIGRATION_MAP = {
@@ -738,7 +1104,7 @@ var IssueTracker = class extends BaseTracker {
738
1104
  resetToPhase(issueIid, phase, def) {
739
1105
  const record = this.collection[this.key(issueIid)];
740
1106
  if (!record) return false;
741
- const lm = this.lifecycleManagers.get(def.mode) ?? this.lifecycleFor(record);
1107
+ const lm = new ActionLifecycleManager(def);
742
1108
  const targetState = lm.getPhasePreState(phase);
743
1109
  if (!targetState) return false;
744
1110
  record.state = targetState;
@@ -1341,1186 +1707,820 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1341
1707
  });
1342
1708
  return this.runAI(displayId, fullPrompt, onInputRequired, void 0, ctx);
1343
1709
  }
1344
- return result;
1345
- }
1346
- /**
1347
- * 判断 AI 执行失败是否为永久性错误(不可重试)。
1348
- * 模型不存在、API key 无效等配置问题重试不会成功。
1349
- */
1350
- classifyRetryable(result) {
1351
- const msg = (result.errorMessage ?? result.output ?? "").toLowerCase();
1352
- const permanentPatterns = [
1353
- /model\b.*\b(?:not found|not supported|unavailable|service info not found)/,
1354
- /invalid.?api.?key/,
1355
- /authentication.*(?:failed|denied|error)/,
1356
- /permission.?denied/,
1357
- /billing/,
1358
- /quota.*exceeded/
1359
- ];
1360
- return !permanentPatterns.some((p) => p.test(msg));
1361
- }
1362
- /**
1363
- * Heuristic: a resume failure is typically an immediate process exit
1364
- * (exit code != 0, empty output) caused by an invalid/expired session ID.
1365
- */
1366
- isResumeFailure(result) {
1367
- if (result.success) return false;
1368
- const msg = (result.errorMessage ?? "").toLowerCase();
1369
- if (msg.includes("session") || msg.includes("resume") || msg.includes("session_id")) {
1370
- return true;
1371
- }
1372
- if (result.output.length === 0 && result.exitCode !== null && result.exitCode !== 0) {
1373
- const isConfigError = msg.includes("model") || msg.includes("api key") || msg.includes("authentication");
1374
- return !isConfigError;
1375
- }
1376
- return false;
1377
- }
1378
- persistSessionId(sessionId) {
1379
- if (sessionId) {
1380
- this.plan.updatePhaseSessionId(this.phaseName, sessionId);
1381
- }
1382
- }
1383
- // ── Hook dispatch methods ──
1384
- async notifyPhaseStart() {
1385
- await this.hooks.onPhaseStart(this.phaseName);
1386
- }
1387
- async notifyPhaseDone() {
1388
- await this.hooks.onPhaseDone(this.phaseName);
1389
- }
1390
- async notifyPhaseFailed(error) {
1391
- await this.hooks.onPhaseFailed(this.phaseName, error);
1392
- }
1393
- async notifyComment(message) {
1394
- try {
1395
- await this.hooks.onComment(message);
1396
- } catch (err) {
1397
- this.logger.warn("Hook onComment failed", { error: err.message });
1398
- }
1399
- }
1400
- /**
1401
- * Build an onInputRequired handler for ACP permission delegation.
1402
- * Only returns a handler when codebuddyAcpAutoApprove is false,
1403
- * enabling review gate delegation for permission requests.
1404
- */
1405
- buildInputRequiredHandler(displayId) {
1406
- if (this.config.ai.codebuddyAcpAutoApprove !== false) return void 0;
1407
- return (request) => {
1408
- if (request.type !== "plan-approval") {
1409
- return Promise.resolve("allow");
1410
- }
1411
- this.logger.info("ACP plan-approval requested, delegating to review gate", {
1412
- issueIid: displayId,
1413
- phase: this.phaseName
1414
- });
1415
- eventBus.emitTyped("review:requested", { issueIid: displayId });
1416
- return new Promise((resolve) => {
1417
- const onApproved = (payload) => {
1418
- const data = payload.data;
1419
- if (data.issueIid !== displayId) return;
1420
- cleanup();
1421
- this.logger.info("ACP plan-approval approved via review gate", { issueIid: displayId });
1422
- resolve("allow");
1423
- };
1424
- const onRejected = (payload) => {
1425
- const data = payload.data;
1426
- if (data.issueIid !== displayId) return;
1427
- cleanup();
1428
- this.logger.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
1429
- resolve("reject");
1430
- };
1431
- const cleanup = () => {
1432
- eventBus.removeListener("review:approved", onApproved);
1433
- eventBus.removeListener("review:rejected", onRejected);
1434
- };
1435
- eventBus.on("review:approved", onApproved);
1436
- eventBus.on("review:rejected", onRejected);
1437
- });
1438
- };
1439
- }
1440
- async resolveRules(ctx) {
1441
- try {
1442
- const knowledge = getProjectKnowledge();
1443
- const resolver = new RuleResolver(knowledge?.ruleTriggers);
1444
- const context = `${ctx.demand.title} ${ctx.demand.description} ${ctx.demand.supplement ? JSON.stringify(ctx.demand.supplement) : ""}`;
1445
- if (ctx.workspace && ctx.workspace.repos.length > 1) {
1446
- for (const repo of ctx.workspace.repos) {
1447
- const rulesDir = path4.join(repo.gitRootDir, ".cursor", "rules");
1448
- try {
1449
- await resolver.loadRules(rulesDir);
1450
- } catch {
1451
- }
1452
- }
1453
- } else {
1454
- const rulesDir = path4.join(this.plan.baseDir, ".cursor", "rules");
1455
- await resolver.loadRules(rulesDir);
1456
- }
1457
- const matched = resolver.matchRules(context);
1458
- if (matched.length > 0) {
1459
- this.logger.info(`Matched ${matched.length} MDC rules`, {
1460
- rules: matched.map((r) => r.filename)
1461
- });
1462
- return resolver.formatForPrompt(matched);
1463
- }
1464
- } catch (err) {
1465
- this.logger.warn("Failed to resolve MDC rules", { error: err.message });
1466
- }
1467
- return null;
1468
- }
1469
- async syncResultToIssue(ctx, displayId) {
1470
- try {
1471
- const enabled = this.hooks.isNoteSyncEnabled();
1472
- const resultFiles = this.getResultFiles(ctx);
1473
- if (!enabled || resultFiles.length === 0) {
1474
- await this.notifyComment(
1475
- issueProgressComment(this.phaseName, "completed")
1476
- );
1477
- return;
1478
- }
1479
- const baseUrl = this.config.issueNoteSync.webBaseUrl.replace(/\/$/, "");
1480
- const phaseLabel = t(`phase.${this.phaseName}`) || this.phaseName;
1481
- const dashboardUrl = `${baseUrl}/?issue=${displayId}`;
1482
- for (const file of resultFiles) {
1483
- const content = this.readResultFile(displayId, file.filename);
1484
- if (!content) continue;
1485
- const summary = truncateToSummary(content);
1486
- const docUrl = `${baseUrl}/doc/${displayId}/${file.filename}`;
1487
- const comment = buildNoteSyncComment(
1488
- this.phaseName,
1489
- file.label || phaseLabel,
1490
- docUrl,
1491
- dashboardUrl,
1492
- summary
1493
- );
1494
- await this.notifyComment(comment);
1495
- this.logger.info("Result synced to issue", { issueIid: displayId, file: file.filename });
1496
- }
1497
- } catch (err) {
1498
- this.logger.warn("Failed to sync result to issue", { error: err.message });
1499
- await this.notifyComment(
1500
- issueProgressComment(this.phaseName, "completed")
1501
- );
1502
- }
1503
- }
1504
- readResultFile(issueIid, filename) {
1505
- const planDir = path4.join(this.plan.baseDir, ".claude-plan", `issue-${issueIid}`);
1506
- const filePath = path4.join(planDir, filename);
1507
- if (!fs3.existsSync(filePath)) return null;
1508
- try {
1509
- return fs3.readFileSync(filePath, "utf-8");
1510
- } catch {
1511
- return null;
1512
- }
1513
- }
1514
- async validatePhaseOutput(ctx, displayId) {
1515
- const resultFiles = this.getResultFiles(ctx);
1516
- if (resultFiles.length === 0) return;
1517
- const planDir = path4.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
1518
- const missing = [];
1519
- for (const file of resultFiles) {
1520
- const filePath = path4.join(planDir, file.filename);
1521
- if (!fs3.existsSync(filePath)) {
1522
- missing.push(file.filename);
1523
- continue;
1524
- }
1525
- const stat = fs3.statSync(filePath);
1526
- if (stat.size < _BasePhase.MIN_ARTIFACT_BYTES) {
1527
- missing.push(`${file.filename} (${stat.size} bytes, \u5185\u5BB9\u4E0D\u8DB3)`);
1528
- }
1529
- }
1530
- if (missing.length > 0) {
1531
- const msg = `AI \u8FDB\u7A0B\u6210\u529F\u9000\u51FA\u4F46\u672A\u751F\u6210\u9884\u671F\u4EA7\u7269: ${missing.join(", ")}`;
1532
- this.logger.error(msg, { phase: this.phaseName, displayId });
1533
- throw new AIExecutionError(this.phaseName, msg, { output: "", exitCode: 0 });
1534
- }
1535
- }
1536
- async commitPlanFiles(ctx, displayId) {
1537
- const commitMsg = `chore(auto): ${this.phaseName} phase completed for issue #${displayId}`;
1538
- if (ctx.workspace && ctx.workspace.repos.length > 1) {
1539
- for (const repo of ctx.workspace.repos) {
1540
- const repoGit = this.wtGitMap?.get(repo.name) ?? new GitOperations(repo.gitRootDir);
1541
- const branch = repo.branchPrefix ? `${repo.branchPrefix}-${displayId}` : ctx.branchName;
1542
- try {
1543
- if (await repoGit.hasChanges()) {
1544
- await repoGit.add(["."]);
1545
- await repoGit.commit(commitMsg);
1546
- await repoGit.push(branch);
1547
- this.logger.info("Committed changes for repo", { repo: repo.name, branch });
1548
- }
1549
- } catch (err) {
1550
- this.logger.warn("Failed to commit/push for repo", {
1551
- repo: repo.name,
1552
- error: err.message
1553
- });
1554
- }
1555
- }
1556
- } else {
1557
- if (await this.git.hasChanges()) {
1558
- await this.git.add(["."]);
1559
- await this.git.commit(commitMsg);
1560
- await this.git.push(ctx.branchName);
1561
- }
1562
- }
1563
- }
1564
- };
1565
-
1566
- // src/phases/VerifyPhase.ts
1567
- import fs4 from "fs";
1568
- import path5 from "path";
1569
-
1570
- // src/verify/VerifyReportParser.ts
1571
- var LINT_FAIL_RE = /\*{0,2}Lint\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1572
- var BUILD_FAIL_RE = /\*{0,2}Build\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1573
- var TEST_FAIL_RE = /\*{0,2}Test\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1574
- var SUMMARY_FAIL_RE = /\*{0,2}(?:总结|Summary)\*{0,2}\s*[::].*(?:验证失败|verification\s+failed|failed|失败)/i;
1575
- var TODOLIST_STATS_RE = /\*{0,2}(?:Todolist|Todo)\s*(?:检查|check)?\*{0,2}\s*[::]\s*(\d+)\s*[//]\s*(\d+)/i;
1576
- var VerifyReportParser = class {
1577
- /**
1578
- * 解析验证报告内容。
1579
- *
1580
- * @param reportContent 验证报告的完整 Markdown 文本
1581
- */
1582
- parse(reportContent) {
1583
- const lintPassed = !LINT_FAIL_RE.test(reportContent);
1584
- const buildPassed = !BUILD_FAIL_RE.test(reportContent);
1585
- const testPassed = !TEST_FAIL_RE.test(reportContent);
1586
- const todolistStats = this.parseTodolistStats(reportContent);
1587
- const todolistComplete = todolistStats ? todolistStats.total === 0 || todolistStats.completed === todolistStats.total : true;
1588
- const failureReasons = [];
1589
- if (!lintPassed) failureReasons.push("Lint \u68C0\u67E5\u5931\u8D25");
1590
- if (!buildPassed) failureReasons.push("Build \u7F16\u8BD1\u5931\u8D25");
1591
- if (!testPassed) failureReasons.push("\u6D4B\u8BD5\u672A\u901A\u8FC7");
1592
- if (!todolistComplete) {
1593
- const statsText = todolistStats ? `(${todolistStats.completed}/${todolistStats.total})` : "";
1594
- failureReasons.push(`Todolist \u672A\u5168\u90E8\u5B8C\u6210${statsText}`);
1595
- }
1596
- const summaryFailed = SUMMARY_FAIL_RE.test(reportContent);
1597
- if (failureReasons.length === 0 && summaryFailed) {
1598
- failureReasons.push("\u9A8C\u8BC1\u62A5\u544A\u603B\u7ED3\u5224\u5B9A\u4E3A\u5931\u8D25");
1599
- }
1600
- const passed = failureReasons.length === 0;
1601
- return {
1602
- passed,
1603
- lintPassed,
1604
- buildPassed,
1605
- testPassed,
1606
- todolistComplete,
1607
- todolistStats: todolistStats ?? void 0,
1608
- failureReasons,
1609
- rawReport: reportContent
1610
- };
1710
+ return result;
1611
1711
  }
1612
1712
  /**
1613
- * plan 文件中解析 Todolist 完成度。
1614
- * 统计 `- [x]` (已完成) 和 `- [ ]` (未完成) 的数量。
1713
+ * 判断 AI 执行失败是否为永久性错误(不可重试)。
1714
+ * 模型不存在、API key 无效等配置问题重试不会成功。
1615
1715
  */
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
- };
1716
+ classifyRetryable(result) {
1717
+ const msg = (result.errorMessage ?? result.output ?? "").toLowerCase();
1718
+ const permanentPatterns = [
1719
+ /model\b.*\b(?:not found|not supported|unavailable|service info not found)/,
1720
+ /invalid.?api.?key/,
1721
+ /authentication.*(?:failed|denied|error)/,
1722
+ /permission.?denied/,
1723
+ /billing/,
1724
+ /quota.*exceeded/
1725
+ ];
1726
+ return !permanentPatterns.some((p) => p.test(msg));
1627
1727
  }
1628
1728
  /**
1629
- * 从报告中提取 Todolist 统计数据。
1630
- * 格式: "Todolist 检查: X/Y 项完成"
1729
+ * Heuristic: a resume failure is typically an immediate process exit
1730
+ * (exit code != 0, empty output) caused by an invalid/expired session ID.
1631
1731
  */
1632
- 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
- };
1732
+ isResumeFailure(result) {
1733
+ if (result.success) return false;
1734
+ const msg = (result.errorMessage ?? "").toLowerCase();
1735
+ if (msg.includes("session") || msg.includes("resume") || msg.includes("session_id")) {
1736
+ return true;
1639
1737
  }
1640
- return null;
1641
- }
1642
- };
1643
-
1644
- // src/phases/VerifyPhase.ts
1645
- var VerifyPhase = class extends BasePhase {
1646
- phaseName = "verify";
1647
- reportParser = new VerifyReportParser();
1648
- getResultFiles(ctx) {
1649
- const filename = ctx?.pipelineMode === "plan-mode" ? "02-verify-report.md" : "04-verify-report.md";
1650
- return [{ filename, label: "\u9A8C\u8BC1\u62A5\u544A" }];
1651
- }
1652
- async execute(ctx) {
1653
- const result = await super.execute(ctx);
1654
- if (result.success) {
1655
- const report = this.readVerifyReport(ctx);
1656
- if (report) {
1657
- const parsed = this.reportParser.parse(report);
1658
- if (this.config.verifyFixLoop.todolistCheckEnabled && !parsed.todolistStats) {
1659
- const planContent = this.readPlanFile(ctx);
1660
- if (planContent) {
1661
- const todoStats = this.reportParser.parseTodolistFromPlan(planContent);
1662
- if (todoStats.total > 0) {
1663
- parsed.todolistStats = todoStats;
1664
- parsed.todolistComplete = todoStats.completed === todoStats.total;
1665
- if (!parsed.todolistComplete) {
1666
- parsed.failureReasons.push(
1667
- `Todolist \u672A\u5168\u90E8\u5B8C\u6210(${todoStats.completed}/${todoStats.total})`
1668
- );
1669
- parsed.passed = false;
1670
- }
1671
- }
1672
- }
1673
- }
1674
- this.logger.info("Verify report parsed", {
1675
- passed: parsed.passed,
1676
- lintPassed: parsed.lintPassed,
1677
- buildPassed: parsed.buildPassed,
1678
- testPassed: parsed.testPassed,
1679
- todolistComplete: parsed.todolistComplete,
1680
- todolistStats: parsed.todolistStats,
1681
- failureCount: parsed.failureReasons.length
1682
- });
1683
- return { ...result, verifyReport: parsed };
1684
- }
1738
+ if (result.output.length === 0 && result.exitCode !== null && result.exitCode !== 0) {
1739
+ const isConfigError = msg.includes("model") || msg.includes("api key") || msg.includes("authentication");
1740
+ return !isConfigError;
1685
1741
  }
1686
- return result;
1687
- }
1688
- buildPrompt(ctx) {
1689
- const pc = demandToPromptContext(ctx.demand);
1690
- const promptCtx = {
1691
- issueTitle: pc.title,
1692
- issueDescription: pc.description,
1693
- issueIid: Number(pc.displayId),
1694
- workspace: ctx.workspace
1695
- };
1696
- return ctx.pipelineMode === "plan-mode" ? planModeVerifyPrompt(promptCtx) : verifyPrompt(promptCtx);
1742
+ return false;
1697
1743
  }
1698
- readVerifyReport(ctx) {
1699
- const files = this.getResultFiles(ctx);
1700
- if (files.length === 0) return null;
1701
- const displayId = Number(ctx.demand.sourceRef.displayId);
1702
- const planDir = path5.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
1703
- const filePath = path5.join(planDir, files[0].filename);
1704
- if (!fs4.existsSync(filePath)) return null;
1705
- try {
1706
- return fs4.readFileSync(filePath, "utf-8");
1707
- } catch {
1708
- return null;
1744
+ persistSessionId(sessionId) {
1745
+ if (sessionId) {
1746
+ this.plan.updatePhaseSessionId(this.phaseName, sessionId);
1709
1747
  }
1710
1748
  }
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;
1720
- }
1749
+ // ── Hook dispatch methods ──
1750
+ async notifyPhaseStart() {
1751
+ await this.hooks.onPhaseStart(this.phaseName);
1721
1752
  }
1722
- };
1723
-
1724
- // src/phases/PlanPhase.ts
1725
- var PlanPhase = class extends BasePhase {
1726
- phaseName = "plan";
1727
- getResultFiles() {
1728
- return [{ filename: "01-plan.md", label: "\u5B9E\u65BD\u8BA1\u5212" }];
1753
+ async notifyPhaseDone() {
1754
+ await this.hooks.onPhaseDone(this.phaseName);
1729
1755
  }
1730
- getRunMode() {
1731
- return "plan";
1756
+ async notifyPhaseFailed(error) {
1757
+ await this.hooks.onPhaseFailed(this.phaseName, error);
1732
1758
  }
1733
- buildPrompt(ctx) {
1734
- const pc = demandToPromptContext(ctx.demand);
1735
- const history = this.plan.readReviewHistory();
1736
- const promptCtx = {
1737
- issueTitle: pc.title,
1738
- issueDescription: pc.description,
1739
- issueIid: Number(pc.displayId),
1740
- supplementText: pc.supplementText || void 0,
1741
- workspace: ctx.workspace
1742
- };
1743
- let basePrompt;
1744
- if (history.length > 0) {
1745
- basePrompt = rePlanPrompt(promptCtx, history);
1746
- } else {
1747
- basePrompt = planPrompt(promptCtx);
1748
- }
1749
- const caps = getRunnerCapabilities(this.config.ai.mode);
1750
- if (!caps?.nativePlanMode) {
1751
- basePrompt = `${t("prompt.planModeFallback")}
1752
-
1753
- ${basePrompt}`;
1759
+ async notifyComment(message) {
1760
+ try {
1761
+ await this.hooks.onComment(message);
1762
+ } catch (err) {
1763
+ this.logger.warn("Hook onComment failed", { error: err.message });
1754
1764
  }
1755
- return basePrompt;
1756
1765
  }
1757
- };
1758
-
1759
- // src/phases/BuildPhase.ts
1760
- var BuildPhase = class extends BasePhase {
1761
- phaseName = "build";
1762
- async validatePhaseOutput(ctx, _displayId) {
1763
- let hasAnyChanges = false;
1764
- if (ctx.workspace && ctx.workspace.repos.length > 1) {
1765
- for (const repo of ctx.workspace.repos) {
1766
- const repoGit = new GitOperations(repo.gitRootDir);
1767
- if (await repoGit.hasChanges()) {
1768
- hasAnyChanges = true;
1769
- break;
1770
- }
1766
+ /**
1767
+ * Build an onInputRequired handler for ACP permission delegation.
1768
+ * Only returns a handler when codebuddyAcpAutoApprove is false,
1769
+ * enabling review gate delegation for permission requests.
1770
+ */
1771
+ buildInputRequiredHandler(displayId) {
1772
+ if (this.config.ai.codebuddyAcpAutoApprove !== false) return void 0;
1773
+ return (request) => {
1774
+ if (request.type !== "plan-approval") {
1775
+ return Promise.resolve("allow");
1771
1776
  }
1772
- } 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 });
1779
- }
1780
- }
1781
- buildPrompt(ctx) {
1782
- const pc = demandToPromptContext(ctx.demand);
1783
- const base = buildPrompt({
1784
- issueTitle: pc.title,
1785
- issueDescription: pc.description,
1786
- issueIid: Number(pc.displayId),
1787
- workspace: ctx.workspace
1788
- });
1789
- if (ctx.fixContext) {
1790
- return base + t("prompt.buildFixSuffix", {
1791
- iteration: ctx.fixContext.iteration,
1792
- failures: ctx.fixContext.verifyFailures.map((f, i) => `${i + 1}. ${f}`).join("\n"),
1793
- rawReport: ctx.fixContext.rawReport.slice(0, 2e3)
1777
+ this.logger.info("ACP plan-approval requested, delegating to review gate", {
1778
+ issueIid: displayId,
1779
+ phase: this.phaseName
1794
1780
  });
1795
- }
1796
- return base;
1797
- }
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");
1816
- }
1817
- filePath(projectPath) {
1818
- return path6.join(this.cacheDir, `${hashProjectPath(projectPath)}.json`);
1781
+ eventBus.emitTyped("review:requested", { issueIid: displayId });
1782
+ return new Promise((resolve) => {
1783
+ const onApproved = (payload) => {
1784
+ const data = payload.data;
1785
+ if (data.issueIid !== displayId) return;
1786
+ cleanup();
1787
+ this.logger.info("ACP plan-approval approved via review gate", { issueIid: displayId });
1788
+ resolve("allow");
1789
+ };
1790
+ const onRejected = (payload) => {
1791
+ const data = payload.data;
1792
+ if (data.issueIid !== displayId) return;
1793
+ cleanup();
1794
+ this.logger.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
1795
+ resolve("reject");
1796
+ };
1797
+ const cleanup = () => {
1798
+ eventBus.removeListener("review:approved", onApproved);
1799
+ eventBus.removeListener("review:rejected", onRejected);
1800
+ };
1801
+ eventBus.on("review:approved", onApproved);
1802
+ eventBus.on("review:rejected", onRejected);
1803
+ });
1804
+ };
1819
1805
  }
1820
- /**
1821
- * 读取缓存。返回 null 如果不存在、已过期或校验失败。
1822
- */
1823
- get(projectPath, ttlMs) {
1824
- const fp = this.filePath(projectPath);
1806
+ async resolveRules(ctx) {
1825
1807
  try {
1826
- if (!fs5.existsSync(fp)) return null;
1827
- const raw = fs5.readFileSync(fp, "utf-8");
1828
- const data = JSON.parse(raw);
1829
- if (data.projectPath !== projectPath) {
1830
- logger7.warn("Cache projectPath mismatch, ignoring", { expected: projectPath, got: data.projectPath });
1831
- return null;
1808
+ const knowledge = getProjectKnowledge();
1809
+ const resolver = new RuleResolver(knowledge?.ruleTriggers);
1810
+ const context = `${ctx.demand.title} ${ctx.demand.description} ${ctx.demand.supplement ? JSON.stringify(ctx.demand.supplement) : ""}`;
1811
+ if (ctx.workspace && ctx.workspace.repos.length > 1) {
1812
+ for (const repo of ctx.workspace.repos) {
1813
+ const rulesDir = path4.join(repo.gitRootDir, ".cursor", "rules");
1814
+ try {
1815
+ await resolver.loadRules(rulesDir);
1816
+ } catch {
1817
+ }
1818
+ }
1819
+ } else {
1820
+ const rulesDir = path4.join(this.plan.baseDir, ".cursor", "rules");
1821
+ await resolver.loadRules(rulesDir);
1832
1822
  }
1833
- const age = Date.now() - new Date(data.detectedAt).getTime();
1834
- if (age > ttlMs) {
1835
- logger7.debug("Cache expired", { projectPath, ageMs: age, ttlMs });
1836
- return null;
1823
+ const matched = resolver.matchRules(context);
1824
+ if (matched.length > 0) {
1825
+ this.logger.info(`Matched ${matched.length} MDC rules`, {
1826
+ rules: matched.map((r) => r.filename)
1827
+ });
1828
+ return resolver.formatForPrompt(matched);
1837
1829
  }
1838
- return data;
1839
1830
  } catch (err) {
1840
- logger7.warn("Failed to read release detect cache", { path: fp, error: err.message });
1841
- return null;
1831
+ this.logger.warn("Failed to resolve MDC rules", { error: err.message });
1842
1832
  }
1833
+ return null;
1843
1834
  }
1844
- /**
1845
- * 写入缓存。
1846
- */
1847
- set(result) {
1848
- const fp = this.filePath(result.projectPath);
1835
+ async syncResultToIssue(ctx, displayId) {
1849
1836
  try {
1850
- if (!fs5.existsSync(this.cacheDir)) {
1851
- fs5.mkdirSync(this.cacheDir, { recursive: true });
1837
+ const enabled = this.hooks.isNoteSyncEnabled();
1838
+ const resultFiles = this.getResultFiles(ctx);
1839
+ if (!enabled || resultFiles.length === 0) {
1840
+ await this.notifyComment(
1841
+ issueProgressComment(this.phaseName, "completed")
1842
+ );
1843
+ return;
1844
+ }
1845
+ const baseUrl = this.config.issueNoteSync.webBaseUrl.replace(/\/$/, "");
1846
+ const phaseLabel = t(`phase.${this.phaseName}`) || this.phaseName;
1847
+ const dashboardUrl = `${baseUrl}/?issue=${displayId}`;
1848
+ for (const file of resultFiles) {
1849
+ const content = this.readResultFile(displayId, file.filename);
1850
+ if (!content) continue;
1851
+ const summary = truncateToSummary(content);
1852
+ const docUrl = `${baseUrl}/doc/${displayId}/${file.filename}`;
1853
+ const comment = buildNoteSyncComment(
1854
+ this.phaseName,
1855
+ file.label || phaseLabel,
1856
+ docUrl,
1857
+ dashboardUrl,
1858
+ summary
1859
+ );
1860
+ await this.notifyComment(comment);
1861
+ this.logger.info("Result synced to issue", { issueIid: displayId, file: file.filename });
1852
1862
  }
1853
- fs5.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
1854
- logger7.debug("Release detect cache written", { projectPath: result.projectPath, path: fp });
1855
1863
  } catch (err) {
1856
- logger7.warn("Failed to write release detect cache", { path: fp, error: err.message });
1864
+ this.logger.warn("Failed to sync result to issue", { error: err.message });
1865
+ await this.notifyComment(
1866
+ issueProgressComment(this.phaseName, "completed")
1867
+ );
1857
1868
  }
1858
1869
  }
1859
- /**
1860
- * 手动失效缓存。
1861
- */
1862
- invalidate(projectPath) {
1863
- const fp = this.filePath(projectPath);
1870
+ readResultFile(issueIid, filename) {
1871
+ const planDir = path4.join(this.plan.baseDir, ".claude-plan", `issue-${issueIid}`);
1872
+ const filePath = path4.join(planDir, filename);
1873
+ if (!fs3.existsSync(filePath)) return null;
1864
1874
  try {
1865
- if (fs5.existsSync(fp)) {
1866
- fs5.unlinkSync(fp);
1867
- logger7.info("Release detect cache invalidated", { projectPath });
1868
- return true;
1875
+ return fs3.readFileSync(filePath, "utf-8");
1876
+ } catch {
1877
+ return null;
1878
+ }
1879
+ }
1880
+ async validatePhaseOutput(ctx, displayId) {
1881
+ const resultFiles = this.getResultFiles(ctx);
1882
+ if (resultFiles.length === 0) return;
1883
+ const planDir = path4.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
1884
+ const missing = [];
1885
+ for (const file of resultFiles) {
1886
+ const filePath = path4.join(planDir, file.filename);
1887
+ if (!fs3.existsSync(filePath)) {
1888
+ missing.push(file.filename);
1889
+ continue;
1869
1890
  }
1870
- return false;
1871
- } catch (err) {
1872
- logger7.warn("Failed to invalidate release detect cache", { path: fp, error: err.message });
1873
- return false;
1891
+ const stat = fs3.statSync(filePath);
1892
+ if (stat.size < _BasePhase.MIN_ARTIFACT_BYTES) {
1893
+ missing.push(`${file.filename} (${stat.size} bytes, \u5185\u5BB9\u4E0D\u8DB3)`);
1894
+ }
1895
+ }
1896
+ if (missing.length > 0) {
1897
+ const msg = `AI \u8FDB\u7A0B\u6210\u529F\u9000\u51FA\u4F46\u672A\u751F\u6210\u9884\u671F\u4EA7\u7269: ${missing.join(", ")}`;
1898
+ this.logger.error(msg, { phase: this.phaseName, displayId });
1899
+ throw new AIExecutionError(this.phaseName, msg, { output: "", exitCode: 0 });
1874
1900
  }
1875
1901
  }
1876
- };
1877
-
1878
- // src/prompts/release-templates.ts
1879
- function releaseDetectPrompt(ctx) {
1880
- const pd = `.claude-plan/issue-${ctx.issueIid}`;
1881
- return `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684 DevOps \u5206\u6790\u5E08\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u63A2\u7D22\u5F53\u524D\u9879\u76EE\uFF0C\u67E5\u627E\u6240\u6709\u4E0E**\u53D1\u5E03/\u90E8\u7F72**\u76F8\u5173\u7684\u673A\u5236\u3002
1882
-
1883
- ## \u63A2\u7D22\u8303\u56F4
1884
-
1885
- \u8BF7\u6309\u4F18\u5148\u7EA7\u4F9D\u6B21\u68C0\u67E5\u4EE5\u4E0B\u4F4D\u7F6E\uFF1A
1886
-
1887
- 1. **Skill \u6587\u4EF6**
1888
- - \`.cursor/skills/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy/publish \u76F8\u5173\u7684 skill
1889
- - \u9879\u76EE\u6839\u76EE\u5F55\u7684 \`SKILL.md\` \u6216 \`release.md\`
1890
-
1891
- 2. **Rule \u6587\u4EF6**
1892
- - \`.cursor/rules/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy \u76F8\u5173\u7684 \`.mdc\` \u89C4\u5219
1893
-
1894
- 3. **CI/CD \u914D\u7F6E**
1895
- - \`.gitlab-ci.yml\` / \`Jenkinsfile\` / \`.github/workflows/\`
1896
- - \u67E5\u627E release/deploy/publish \u76F8\u5173\u7684 stage/job/workflow
1897
-
1898
- 4. **Package Manager**
1899
- - \`package.json\` \u4E2D\u7684 \`scripts\` \u5B57\u6BB5\uFF1Apublish\u3001release\u3001deploy \u7B49
1900
- - \`Makefile\` \u4E2D\u7684 release/deploy target
1901
-
1902
- 5. **\u811A\u672C\u4E0E\u6587\u6863**
1903
- - \`scripts/release.*\`\u3001\`scripts/deploy.*\`\u3001\`scripts/publish.*\`
1904
- - \`RELEASE.md\`\u3001\`CHANGELOG.md\`\u3001\`CONTRIBUTING.md\` \u4E2D\u7684\u53D1\u5E03\u8BF4\u660E
1905
- - \`Dockerfile\`\u3001\`docker-compose*.yml\`
1906
-
1907
- ## \u8F93\u51FA\u8981\u6C42
1908
-
1909
- \u5C06\u68C0\u6D4B\u7ED3\u679C\u4EE5 **\u4E25\u683C JSON** \u683C\u5F0F\u5199\u5165 \`${pd}/03-release-detect.json\`\uFF1A
1910
-
1911
- \`\`\`json
1912
- {
1913
- "hasReleaseCapability": true,
1914
- "releaseMethod": "npm-publish",
1915
- "artifacts": ["package.json", ".gitlab-ci.yml"],
1916
- "instructions": "\u8FD0\u884C npm publish \u53D1\u5E03\u5230 npm registry..."
1917
- }
1918
- \`\`\`
1919
-
1920
- \u5B57\u6BB5\u8BF4\u660E\uFF1A
1921
- - \`hasReleaseCapability\`: \u662F\u5426\u627E\u5230\u53D1\u5E03\u673A\u5236\uFF08boolean\uFF09
1922
- - \`releaseMethod\`: \u53D1\u5E03\u65B9\u5F0F\uFF0C\u53EF\u9009\u503C\uFF1A"npm-publish" | "ci-pipeline" | "makefile" | "custom-script" | "skill" | "docker" | \u5176\u4ED6\u63CF\u8FF0
1923
- - \`artifacts\`: \u53D1\u73B0\u7684\u53D1\u5E03\u76F8\u5173\u6587\u4EF6\u8DEF\u5F84\u6570\u7EC4
1924
- - \`instructions\`: \u4ECE\u6587\u4EF6\u4E2D\u63D0\u53D6\u7684\u53D1\u5E03\u6B65\u9AA4\u8BF4\u660E\u3002\u5982\u679C\u662F skill/rule \u6587\u4EF6\uFF0C\u8BF7\u5B8C\u6574\u590D\u5236\u5176\u5185\u5BB9
1925
-
1926
- **\u91CD\u8981**\uFF1A
1927
- - \u5982\u679C\u6CA1\u6709\u627E\u5230\u4EFB\u4F55\u53D1\u5E03\u673A\u5236\uFF0C\u5C06 \`hasReleaseCapability\` \u8BBE\u4E3A \`false\`\uFF0C\u5176\u4ED6\u5B57\u6BB5\u53EF\u7701\u7565
1928
- - \u5982\u679C\u627E\u5230\u4E86 skill \u6216 rule \u6587\u4EF6\uFF0C\u8BF7\u5728 \`instructions\` \u4E2D\u5B8C\u6574\u4FDD\u7559\u539F\u6587\u5185\u5BB9\uFF0C\u540E\u7EED\u53D1\u5E03\u6267\u884C\u5C06\u4F9D\u8D56\u5B83
1929
- - \u53EA\u8F93\u51FA JSON \u6587\u4EF6\uFF0C\u4E0D\u8981\u6267\u884C\u4EFB\u4F55\u53D1\u5E03\u64CD\u4F5C`;
1930
- }
1931
- function releaseExecPrompt(ctx, detectResult) {
1932
- const pd = `.claude-plan/issue-${ctx.issueIid}`;
1933
- const artifactList = detectResult.artifacts.length > 0 ? detectResult.artifacts.map((a) => `- \`${a}\``).join("\n") : "\uFF08\u65E0\uFF09";
1934
- const instructionSection = detectResult.instructions ? `
1935
- ## \u53D1\u5E03\u6307\u5F15\uFF08\u6765\u81EA\u9879\u76EE skill/rule/\u6587\u6863\uFF09
1936
-
1937
- ${detectResult.instructions}` : "";
1938
- return `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u53D1\u5E03\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6839\u636E\u4EE5\u4E0B\u68C0\u6D4B\u7ED3\u679C\uFF0C\u6267\u884C\u9879\u76EE\u7684\u53D1\u5E03\u6D41\u7A0B\u3002
1939
-
1940
- ## \u68C0\u6D4B\u7ED3\u679C
1941
-
1942
- - **\u53D1\u5E03\u65B9\u5F0F**: ${detectResult.releaseMethod ?? "\u672A\u77E5"}
1943
- - **\u76F8\u5173\u6587\u4EF6**:
1944
- ${artifactList}
1945
- ${instructionSection}
1946
-
1947
- ## \u4EFB\u52A1
1948
-
1949
- 1. \u9605\u8BFB\u4E0A\u8FF0\u53D1\u5E03\u6307\u5F15\u548C\u76F8\u5173\u6587\u4EF6
1950
- 2. \u6309\u7167\u9879\u76EE\u5B9A\u4E49\u7684\u53D1\u5E03\u6D41\u7A0B\u6267\u884C\u53D1\u5E03\u64CD\u4F5C
1951
- 3. \u5C06\u53D1\u5E03\u7ED3\u679C\u5199\u5165 \`${pd}/04-release-report.md\`
1952
-
1953
- ## \u53D1\u5E03\u62A5\u544A\u683C\u5F0F
1954
-
1955
- \`\`\`markdown
1956
- # \u53D1\u5E03\u62A5\u544A
1957
-
1958
- ## \u53D1\u5E03\u65B9\u5F0F
1959
- <\u4F7F\u7528\u7684\u53D1\u5E03\u65B9\u5F0F>
1960
-
1961
- ## \u6267\u884C\u6B65\u9AA4
1962
- <\u9010\u6B65\u8BB0\u5F55\u6267\u884C\u7684\u64CD\u4F5C>
1963
-
1964
- ## \u7ED3\u679C
1965
- <\u6210\u529F/\u5931\u8D25\u53CA\u8BE6\u60C5>
1966
- \`\`\`
1902
+ async commitPlanFiles(ctx, displayId) {
1903
+ const commitMsg = `chore(auto): ${this.phaseName} phase completed for issue #${displayId}`;
1904
+ if (ctx.workspace && ctx.workspace.repos.length > 1) {
1905
+ for (const repo of ctx.workspace.repos) {
1906
+ const repoGit = this.wtGitMap?.get(repo.name) ?? new GitOperations(repo.gitRootDir);
1907
+ const branch = repo.branchPrefix ? `${repo.branchPrefix}-${displayId}` : ctx.branchName;
1908
+ try {
1909
+ if (await repoGit.hasChanges()) {
1910
+ await repoGit.add(["."]);
1911
+ await repoGit.commit(commitMsg);
1912
+ await repoGit.push(branch);
1913
+ this.logger.info("Committed changes for repo", { repo: repo.name, branch });
1914
+ }
1915
+ } catch (err) {
1916
+ this.logger.warn("Failed to commit/push for repo", {
1917
+ repo: repo.name,
1918
+ error: err.message
1919
+ });
1920
+ }
1921
+ }
1922
+ } else {
1923
+ if (await this.git.hasChanges()) {
1924
+ await this.git.add(["."]);
1925
+ await this.git.commit(commitMsg);
1926
+ await this.git.push(ctx.branchName);
1927
+ }
1928
+ }
1929
+ }
1930
+ };
1967
1931
 
1968
- **\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
- }
1932
+ // src/phases/VerifyPhase.ts
1933
+ import fs4 from "fs";
1934
+ import path5 from "path";
1973
1935
 
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}`);
1936
+ // src/verify/VerifyReportParser.ts
1937
+ var LINT_FAIL_RE = /\*{0,2}Lint\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1938
+ var BUILD_FAIL_RE = /\*{0,2}Build\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1939
+ var TEST_FAIL_RE = /\*{0,2}Test\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
1940
+ var SUMMARY_FAIL_RE = /\*{0,2}(?:总结|Summary)\*{0,2}\s*[::].*(?:验证失败|verification\s+failed|failed|失败)/i;
1941
+ var TODOLIST_STATS_RE = /\*{0,2}(?:Todolist|Todo)\s*(?:检查|check)?\*{0,2}\s*[::]\s*(\d+)\s*[//]\s*(\d+)/i;
1942
+ var VerifyReportParser = class {
1943
+ /**
1944
+ * 解析验证报告内容。
1945
+ *
1946
+ * @param reportContent 验证报告的完整 Markdown 文本
1947
+ */
1948
+ parse(reportContent) {
1949
+ const lintPassed = !LINT_FAIL_RE.test(reportContent);
1950
+ const buildPassed = !BUILD_FAIL_RE.test(reportContent);
1951
+ const testPassed = !TEST_FAIL_RE.test(reportContent);
1952
+ const todolistStats = this.parseTodolistStats(reportContent);
1953
+ const todolistComplete = todolistStats ? todolistStats.total === 0 || todolistStats.completed === todolistStats.total : true;
1954
+ const failureReasons = [];
1955
+ if (!lintPassed) failureReasons.push("Lint \u68C0\u67E5\u5931\u8D25");
1956
+ if (!buildPassed) failureReasons.push("Build \u7F16\u8BD1\u5931\u8D25");
1957
+ if (!testPassed) failureReasons.push("\u6D4B\u8BD5\u672A\u901A\u8FC7");
1958
+ if (!todolistComplete) {
1959
+ const statsText = todolistStats ? `(${todolistStats.completed}/${todolistStats.total})` : "";
1960
+ failureReasons.push(`Todolist \u672A\u5168\u90E8\u5B8C\u6210${statsText}`);
1961
+ }
1962
+ const summaryFailed = SUMMARY_FAIL_RE.test(reportContent);
1963
+ if (failureReasons.length === 0 && summaryFailed) {
1964
+ failureReasons.push("\u9A8C\u8BC1\u62A5\u544A\u603B\u7ED3\u5224\u5B9A\u4E3A\u5931\u8D25");
1965
+ }
1966
+ const passed = failureReasons.length === 0;
1967
+ return {
1968
+ passed,
1969
+ lintPassed,
1970
+ buildPassed,
1971
+ testPassed,
1972
+ todolistComplete,
1973
+ todolistStats: todolistStats ?? void 0,
1974
+ failureReasons,
1975
+ rawReport: reportContent
1976
+ };
1983
1977
  }
1984
- /** 暂存当前 ctx 供 planDir getter 使用 */
1985
- currentCtx;
1986
1978
  /**
1987
- * 检测结果是否已存在(即是否处于执行模式)。
1979
+ * 从 plan 文件中解析 Todolist 完成度。
1980
+ * 统计 `- [x]` (已完成) 和 `- [ ]` (未完成) 的数量。
1988
1981
  */
1989
- hasDetectionResult() {
1990
- return fs6.existsSync(path7.join(this.planDir, DETECT_FILENAME));
1982
+ parseTodolistFromPlan(planContent) {
1983
+ const completedRe = /^[ \t]*-\s+\[x\]/gim;
1984
+ const uncompletedRe = /^[ \t]*-\s+\[\s\]/gm;
1985
+ const completedMatches = planContent.match(completedRe);
1986
+ const uncompletedMatches = planContent.match(uncompletedRe);
1987
+ const completed = completedMatches?.length ?? 0;
1988
+ const uncompleted = uncompletedMatches?.length ?? 0;
1989
+ return {
1990
+ completed,
1991
+ total: completed + uncompleted
1992
+ };
1991
1993
  }
1992
- readDetectionFile() {
1993
- const fp = path7.join(this.planDir, DETECT_FILENAME);
1994
- if (!fs6.existsSync(fp)) return null;
1995
- try {
1996
- return JSON.parse(fs6.readFileSync(fp, "utf-8"));
1997
- } catch {
1998
- return null;
1994
+ /**
1995
+ * 从报告中提取 Todolist 统计数据。
1996
+ * 格式: "Todolist 检查: X/Y 项完成"
1997
+ */
1998
+ parseTodolistStats(reportContent) {
1999
+ const match = reportContent.match(TODOLIST_STATS_RE);
2000
+ if (match) {
2001
+ return {
2002
+ completed: parseInt(match[1], 10),
2003
+ total: parseInt(match[2], 10)
2004
+ };
1999
2005
  }
2006
+ return null;
2000
2007
  }
2001
- writeDetectionFile(result) {
2002
- const dir = this.planDir;
2003
- if (!fs6.existsSync(dir)) {
2004
- fs6.mkdirSync(dir, { recursive: true });
2005
- }
2006
- fs6.writeFileSync(path7.join(dir, DETECT_FILENAME), JSON.stringify(result, null, 2), "utf-8");
2008
+ };
2009
+
2010
+ // src/phases/VerifyPhase.ts
2011
+ var VerifyPhase = class extends BasePhase {
2012
+ phaseName = "verify";
2013
+ reportParser = new VerifyReportParser();
2014
+ getResultFiles(ctx) {
2015
+ const filename = ctx?.pipelineMode === "plan-mode" ? "02-verify-report.md" : "04-verify-report.md";
2016
+ return [{ filename, label: "\u9A8C\u8BC1\u62A5\u544A" }];
2007
2017
  }
2008
- getResultFiles(_ctx) {
2009
- if (this.hasDetectionResult()) {
2010
- return [{ filename: REPORT_FILENAME, label: "\u53D1\u5E03\u62A5\u544A" }];
2018
+ async execute(ctx) {
2019
+ const result = await super.execute(ctx);
2020
+ if (result.success) {
2021
+ const report = this.readVerifyReport(ctx);
2022
+ if (report) {
2023
+ const parsed = this.reportParser.parse(report);
2024
+ if (this.config.verifyFixLoop.todolistCheckEnabled && !parsed.todolistStats) {
2025
+ const planContent = this.readPlanFile(ctx);
2026
+ if (planContent) {
2027
+ const todoStats = this.reportParser.parseTodolistFromPlan(planContent);
2028
+ if (todoStats.total > 0) {
2029
+ parsed.todolistStats = todoStats;
2030
+ parsed.todolistComplete = todoStats.completed === todoStats.total;
2031
+ if (!parsed.todolistComplete) {
2032
+ parsed.failureReasons.push(
2033
+ `Todolist \u672A\u5168\u90E8\u5B8C\u6210(${todoStats.completed}/${todoStats.total})`
2034
+ );
2035
+ parsed.passed = false;
2036
+ }
2037
+ }
2038
+ }
2039
+ }
2040
+ this.logger.info("Verify report parsed", {
2041
+ passed: parsed.passed,
2042
+ lintPassed: parsed.lintPassed,
2043
+ buildPassed: parsed.buildPassed,
2044
+ testPassed: parsed.testPassed,
2045
+ todolistComplete: parsed.todolistComplete,
2046
+ todolistStats: parsed.todolistStats,
2047
+ failureCount: parsed.failureReasons.length
2048
+ });
2049
+ return { ...result, verifyReport: parsed };
2050
+ }
2011
2051
  }
2012
- return [{ filename: DETECT_FILENAME, label: "\u53D1\u5E03\u68C0\u6D4B\u62A5\u544A" }];
2052
+ return result;
2013
2053
  }
2014
2054
  buildPrompt(ctx) {
2015
- const dc = demandToPromptContext(ctx.demand);
2016
- const pc = {
2017
- issueTitle: dc.title,
2018
- issueDescription: dc.description,
2019
- issueIid: Number(dc.displayId),
2020
- supplementText: dc.supplementText || void 0,
2055
+ const pc = demandToPromptContext(ctx.demand);
2056
+ const promptCtx = {
2057
+ issueTitle: pc.title,
2058
+ issueDescription: pc.description,
2059
+ issueIid: Number(pc.displayId),
2021
2060
  workspace: ctx.workspace
2022
2061
  };
2023
- if (this.hasDetectionResult()) {
2024
- const detect = this.readDetectionFile();
2025
- return releaseExecPrompt(pc, detect);
2026
- }
2027
- return releaseDetectPrompt(pc);
2062
+ return ctx.pipelineMode === "plan-mode" ? planModeVerifyPrompt(promptCtx) : verifyPrompt(promptCtx);
2028
2063
  }
2029
- async execute(ctx) {
2030
- this.currentCtx = ctx;
2031
- const isDetect = !this.hasDetectionResult();
2032
- if (isDetect) {
2033
- const cache = new ReleaseDetectCache(resolveDataDir());
2034
- const projectPath = this.config.gongfeng.projectPath;
2035
- const ttlMs = this.config.release.detectCacheTtlMs;
2036
- const cached = cache.get(projectPath, ttlMs);
2037
- if (cached) {
2038
- logger8.info("Using cached release detection result", {
2039
- projectPath,
2040
- hasCapability: cached.hasReleaseCapability,
2041
- method: cached.releaseMethod
2042
- });
2043
- await this.hooks.onPhaseStart(this.phaseName);
2044
- this.writeDetectionFile(cached);
2045
- this.plan.updatePhaseProgress(this.phaseName, "completed");
2046
- try {
2047
- await this.git.add(["."]);
2048
- await this.git.commit(`chore: release detection (cached) for issue-${ctx.demand.sourceRef.displayId}`);
2049
- } catch {
2050
- }
2051
- await this.hooks.onPhaseDone(this.phaseName);
2052
- return {
2053
- success: true,
2054
- output: `Release detection cached: ${cached.hasReleaseCapability ? cached.releaseMethod ?? "found" : "no capability"}`,
2055
- exitCode: 0,
2056
- gateRequested: cached.hasReleaseCapability,
2057
- hasReleaseCapability: cached.hasReleaseCapability
2058
- };
2059
- }
2064
+ readVerifyReport(ctx) {
2065
+ const files = this.getResultFiles(ctx);
2066
+ if (files.length === 0) return null;
2067
+ const displayId = Number(ctx.demand.sourceRef.displayId);
2068
+ const planDir = path5.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
2069
+ const filePath = path5.join(planDir, files[0].filename);
2070
+ if (!fs4.existsSync(filePath)) return null;
2071
+ try {
2072
+ return fs4.readFileSync(filePath, "utf-8");
2073
+ } catch {
2074
+ return null;
2060
2075
  }
2061
- const result = await super.execute(ctx);
2062
- if (isDetect && result.success) {
2063
- const detected = this.readDetectionFile();
2064
- if (detected) {
2065
- const cache = new ReleaseDetectCache(resolveDataDir());
2066
- const projectPath = this.config.gongfeng.projectPath;
2067
- cache.set({
2068
- ...detected,
2069
- projectPath,
2070
- detectedAt: (/* @__PURE__ */ new Date()).toISOString()
2071
- });
2072
- logger8.info("Release detection completed", {
2073
- hasCapability: detected.hasReleaseCapability,
2074
- method: detected.releaseMethod,
2075
- artifacts: detected.artifacts
2076
- });
2077
- return {
2078
- ...result,
2079
- gateRequested: detected.hasReleaseCapability,
2080
- hasReleaseCapability: detected.hasReleaseCapability
2081
- };
2082
- }
2083
- return { ...result, gateRequested: false, hasReleaseCapability: false };
2076
+ }
2077
+ readPlanFile(ctx) {
2078
+ const displayId = Number(ctx.demand.sourceRef.displayId);
2079
+ const planDir = path5.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
2080
+ const filePath = path5.join(planDir, "01-plan.md");
2081
+ if (!fs4.existsSync(filePath)) return null;
2082
+ try {
2083
+ return fs4.readFileSync(filePath, "utf-8");
2084
+ } catch {
2085
+ return null;
2084
2086
  }
2085
- return { ...result, gateRequested: false };
2086
2087
  }
2087
- };
2088
-
2089
- // src/phases/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" }];
2088
+ };
2089
+
2090
+ // src/phases/PlanPhase.ts
2091
+ var PlanPhase = class extends BasePhase {
2092
+ phaseName = "plan";
2093
+ getResultFiles() {
2094
+ return [{ filename: "01-plan.md", label: "\u5B9E\u65BD\u8BA1\u5212" }];
2095
+ }
2096
+ getRunMode() {
2097
+ return "plan";
2097
2098
  }
2098
2099
  buildPrompt(ctx) {
2099
2100
  const pc = demandToPromptContext(ctx.demand);
2101
+ const history = this.plan.readReviewHistory();
2100
2102
  const promptCtx = {
2101
2103
  issueTitle: pc.title,
2102
2104
  issueDescription: pc.description,
2103
2105
  issueIid: Number(pc.displayId),
2106
+ supplementText: pc.supplementText || void 0,
2104
2107
  workspace: ctx.workspace
2105
2108
  };
2106
- 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
2109
+ let basePrompt;
2110
+ if (history.length > 0) {
2111
+ basePrompt = rePlanPrompt(promptCtx, history);
2112
+ } else {
2113
+ basePrompt = planPrompt(promptCtx);
2114
+ }
2115
+ const caps = getRunnerCapabilities(this.config.ai.mode);
2116
+ if (!caps?.nativePlanMode) {
2117
+ basePrompt = `${t("prompt.planModeFallback")}
2127
2118
 
2128
- ${e2eSuffix}`;
2119
+ ${basePrompt}`;
2120
+ }
2121
+ return basePrompt;
2129
2122
  }
2130
2123
  };
2131
2124
 
2132
- // src/phases/PhaseFactory.ts
2133
- var PHASE_REGISTRY = /* @__PURE__ */ new Map();
2134
- function registerPhase(name, ctor) {
2135
- PHASE_REGISTRY.set(name, ctor);
2136
- }
2137
- function registerBuiltinPhases() {
2138
- PHASE_REGISTRY.set("plan", PlanPhase);
2139
- PHASE_REGISTRY.set("build", BuildPhase);
2140
- PHASE_REGISTRY.set("verify", VerifyPhase);
2141
- PHASE_REGISTRY.set("uat", UatPhase);
2142
- PHASE_REGISTRY.set("release", ReleasePhase);
2143
- }
2144
- registerBuiltinPhases();
2145
- function createPhase(name, ...args) {
2146
- const Ctor = PHASE_REGISTRY.get(name);
2147
- if (!Ctor) {
2148
- throw new PhaseNotRegisteredError(name, [...PHASE_REGISTRY.keys()]);
2149
- }
2150
- return new Ctor(...args);
2151
- }
2152
- function validatePhaseRegistry(phaseNames) {
2153
- const missing = phaseNames.filter((name) => !PHASE_REGISTRY.has(name));
2154
- if (missing.length > 0) {
2155
- throw new UnregisteredPhasesError(missing, [...PHASE_REGISTRY.keys()]);
2156
- }
2157
- }
2158
-
2159
- // src/lifecycle/ActionLifecycleManager.ts
2160
- var ActionLifecycleManager = class {
2161
- /** IssueState → ActionState */
2162
- stateToAction;
2163
- /** "action:status" → IssueState */
2164
- actionToState;
2165
- /** phase name → { startState, doneState, approvedState? } */
2166
- phaseStatesMap;
2167
- /** Ordered phase indices by IssueState for determineResumePhaseIndex */
2168
- def;
2169
- constructor(def) {
2170
- this.def = def;
2171
- this.stateToAction = /* @__PURE__ */ new Map();
2172
- this.actionToState = /* @__PURE__ */ new Map();
2173
- this.phaseStatesMap = /* @__PURE__ */ new Map();
2174
- this.buildMappings(def);
2175
- }
2176
- buildMappings(def) {
2177
- this.addMapping("pending" /* Pending */, "init", "idle");
2178
- this.addMapping("skipped" /* Skipped */, "init", "skipped");
2179
- this.addMapping("branch_created" /* BranchCreated */, "init", "ready");
2180
- this.addMapping("failed" /* Failed */, "init", "failed");
2181
- this.addMapping("deployed" /* Deployed */, "init", "done");
2182
- this.addMapping("resolving_conflict" /* ResolvingConflict */, "conflict", "running");
2183
- for (const spec of def.phases) {
2184
- this.phaseStatesMap.set(spec.name, {
2185
- startState: spec.startState,
2186
- doneState: spec.doneState,
2187
- approvedState: spec.approvedState
2188
- });
2189
- if (spec.kind === "ai") {
2190
- if (spec.startState !== "phase_running" /* PhaseRunning */) {
2191
- this.addMapping(spec.startState, spec.name, "running");
2192
- }
2193
- if (spec.doneState === "completed" /* Completed */) {
2194
- this.addMapping(spec.doneState, spec.name, "done");
2195
- } else if (spec.doneState !== "phase_done" /* PhaseDone */) {
2196
- this.addMapping(spec.doneState, spec.name, "ready");
2197
- }
2198
- } else if (spec.kind === "gate") {
2199
- if (spec.startState !== "phase_waiting" /* PhaseWaiting */) {
2200
- this.addMapping(spec.startState, spec.name, "waiting");
2201
- }
2202
- if (spec.approvedState && spec.approvedState !== "phase_approved" /* PhaseApproved */) {
2203
- this.addMapping(spec.approvedState, spec.name, "ready");
2125
+ // src/phases/BuildPhase.ts
2126
+ var BuildPhase = class extends BasePhase {
2127
+ phaseName = "build";
2128
+ async validatePhaseOutput(ctx, _displayId) {
2129
+ let hasAnyChanges = false;
2130
+ if (ctx.workspace && ctx.workspace.repos.length > 1) {
2131
+ for (const repo of ctx.workspace.repos) {
2132
+ const repoGit = new GitOperations(repo.gitRootDir);
2133
+ if (await repoGit.hasChanges()) {
2134
+ hasAnyChanges = true;
2135
+ break;
2204
2136
  }
2205
2137
  }
2138
+ } else {
2139
+ hasAnyChanges = await this.git.hasChanges();
2206
2140
  }
2207
- }
2208
- 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);
2141
+ if (!hasAnyChanges) {
2142
+ const msg = "AI \u8FDB\u7A0B\u6210\u529F\u9000\u51FA\u4F46\u672A\u4EA7\u751F\u4EFB\u4F55\u4EE3\u7801\u53D8\u66F4";
2143
+ this.logger.error(msg, { phase: this.phaseName });
2144
+ throw new AIExecutionError(this.phaseName, msg, { output: "", exitCode: 0 });
2215
2145
  }
2216
2146
  }
2217
- // ─── Query API ───
2218
- /**
2219
- * IssueState 解析为语义化的 ActionState。
2220
- *
2221
- * 对于通用状态 PhaseRunning/PhaseDone,需额外传入 currentPhase 来区分具体阶段。
2222
- */
2223
- resolve(state, currentPhase) {
2224
- if (state === "phase_running" /* PhaseRunning */ && currentPhase) {
2225
- return { action: currentPhase, status: "running" };
2226
- }
2227
- if (state === "phase_done" /* PhaseDone */ && currentPhase) {
2228
- return { action: currentPhase, status: "ready" };
2229
- }
2230
- if (state === "phase_waiting" /* PhaseWaiting */ && currentPhase) {
2231
- return { action: currentPhase, status: "waiting" };
2232
- }
2233
- if (state === "phase_approved" /* PhaseApproved */ && currentPhase) {
2234
- return { action: currentPhase, status: "ready" };
2147
+ buildPrompt(ctx) {
2148
+ const pc = demandToPromptContext(ctx.demand);
2149
+ const base = buildPrompt({
2150
+ issueTitle: pc.title,
2151
+ issueDescription: pc.description,
2152
+ issueIid: Number(pc.displayId),
2153
+ workspace: ctx.workspace
2154
+ });
2155
+ if (ctx.fixContext) {
2156
+ return base + t("prompt.buildFixSuffix", {
2157
+ iteration: ctx.fixContext.iteration,
2158
+ failures: ctx.fixContext.verifyFailures.map((f, i) => `${i + 1}. ${f}`).join("\n"),
2159
+ rawReport: ctx.fixContext.rawReport.slice(0, 2e3)
2160
+ });
2235
2161
  }
2236
- const mapped = this.stateToAction.get(state);
2237
- if (mapped) return mapped;
2238
- return { action: "init", status: "idle" };
2239
- }
2240
- /**
2241
- * 将 action + status 反向映射为 IssueState。
2242
- */
2243
- toIssueState(action, status) {
2244
- return this.actionToState.get(`${action}:${status}`);
2245
- }
2246
- // ─── Classification predicates (替代 3 个 Set 常量 + getDrivableIssues) ───
2247
- /**
2248
- * 终态:done | failed | skipped
2249
- */
2250
- isTerminal(state) {
2251
- const as = this.resolve(state);
2252
- return as.status === "done" || as.status === "failed" || as.status === "skipped";
2253
- }
2254
- /**
2255
- * 进行中:running (包含通用 PhaseRunning)
2256
- */
2257
- isInProgress(state) {
2258
- if (state === "phase_running" /* PhaseRunning */) return true;
2259
- return this.resolve(state).status === "running";
2260
- }
2261
- /**
2262
- * 阻塞中:waiting
2263
- */
2264
- isBlocked(state) {
2265
- if (state === "phase_waiting" /* PhaseWaiting */) return true;
2266
- return this.resolve(state).status === "waiting";
2162
+ return base;
2267
2163
  }
2268
- /**
2269
- * 可驱动判断(集中化 getDrivableIssues 的过滤逻辑)。
2270
- *
2271
- * - idle → 可驱动 (Pending)
2272
- * - ready → 可驱动 (BranchCreated, PhaseDone, PhaseApproved)
2273
- * - failed && attempts < maxRetries → 可驱动
2274
- * - waiting → 不可驱动 (PhaseWaiting)
2275
- * - running → 不可驱动(stalled 由外部叠加)
2276
- * - done/skipped → 不可驱动
2277
- */
2278
- isDrivable(state, attempts, maxRetries, lastErrorRetryable) {
2279
- if (state === "phase_done" /* PhaseDone */) return true;
2280
- if (state === "phase_approved" /* PhaseApproved */) return true;
2281
- if (state === "phase_running" /* PhaseRunning */) return false;
2282
- if (state === "phase_waiting" /* PhaseWaiting */) return false;
2283
- const as = this.resolve(state);
2284
- switch (as.status) {
2285
- case "idle":
2286
- case "ready":
2287
- return true;
2288
- case "failed":
2289
- if (lastErrorRetryable === false) return false;
2290
- return attempts < maxRetries;
2291
- case "waiting":
2292
- case "running":
2293
- case "done":
2294
- case "skipped":
2295
- return false;
2296
- }
2164
+ };
2165
+
2166
+ // src/phases/ReleasePhase.ts
2167
+ import fs6 from "fs";
2168
+ import path7 from "path";
2169
+
2170
+ // src/release/ReleaseDetectCache.ts
2171
+ import fs5 from "fs";
2172
+ import path6 from "path";
2173
+ import { createHash } from "crypto";
2174
+ var logger7 = logger.child("ReleaseDetectCache");
2175
+ function hashProjectPath(projectPath) {
2176
+ return createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
2177
+ }
2178
+ var ReleaseDetectCache = class {
2179
+ cacheDir;
2180
+ constructor(dataDir) {
2181
+ this.cacheDir = path6.join(dataDir, "release-detect");
2182
+ }
2183
+ filePath(projectPath) {
2184
+ return path6.join(this.cacheDir, `${hashProjectPath(projectPath)}.json`);
2297
2185
  }
2298
- // ─── Phase navigation (替代 determineStartIndex + getPhasePreState) ───
2299
2186
  /**
2300
- * 确定从哪个阶段索引恢复执行(替代 PipelineOrchestrator.determineStartIndex)。
2301
- *
2302
- * 从后向前扫描 phases,匹配 currentState 或 failedAtState。
2303
- * 支持通用状态 PhaseRunning/PhaseDone + currentPhase 的组合。
2187
+ * 读取缓存。返回 null 如果不存在、已过期或校验失败。
2304
2188
  */
2305
- determineResumePhaseIndex(currentState, failedAtState, currentPhase) {
2306
- const target = failedAtState || currentState;
2307
- const phases = this.def.phases;
2308
- if ((target === "phase_running" /* PhaseRunning */ || target === "phase_done" /* PhaseDone */) && currentPhase) {
2309
- const idx = phases.findIndex((p) => p.name === currentPhase);
2310
- if (idx >= 0) {
2311
- return target === "phase_done" /* PhaseDone */ ? idx + 1 : idx;
2312
- }
2313
- }
2314
- if ((target === "phase_waiting" /* PhaseWaiting */ || target === "phase_approved" /* PhaseApproved */) && currentPhase) {
2315
- const idx = phases.findIndex((p) => p.name === currentPhase);
2316
- if (idx >= 0) {
2317
- if (target === "phase_approved" /* PhaseApproved */) {
2318
- const spec = phases[idx];
2319
- return spec.kind === "ai" && spec.approvedState ? idx : idx + 1;
2320
- }
2321
- return idx;
2322
- }
2323
- }
2324
- for (let i = phases.length - 1; i >= 0; i--) {
2325
- const spec = phases[i];
2326
- if (spec.kind === "gate" && spec.approvedState === target) {
2327
- return i + 1;
2189
+ get(projectPath, ttlMs) {
2190
+ const fp = this.filePath(projectPath);
2191
+ try {
2192
+ if (!fs5.existsSync(fp)) return null;
2193
+ const raw = fs5.readFileSync(fp, "utf-8");
2194
+ const data = JSON.parse(raw);
2195
+ if (data.projectPath !== projectPath) {
2196
+ logger7.warn("Cache projectPath mismatch, ignoring", { expected: projectPath, got: data.projectPath });
2197
+ return null;
2328
2198
  }
2329
- if (spec.startState === target || spec.doneState === target) {
2330
- return spec.doneState === target ? i + 1 : i;
2199
+ const age = Date.now() - new Date(data.detectedAt).getTime();
2200
+ if (age > ttlMs) {
2201
+ logger7.debug("Cache expired", { projectPath, ageMs: age, ttlMs });
2202
+ return null;
2331
2203
  }
2204
+ return data;
2205
+ } catch (err) {
2206
+ logger7.warn("Failed to read release detect cache", { path: fp, error: err.message });
2207
+ return null;
2332
2208
  }
2333
- return 0;
2334
- }
2335
- /**
2336
- * 获取某个 phase 的前驱状态(即重置到该 phase 需要设置的状态)。
2337
- * 第一个 phase 的前驱是 BranchCreated;后续 phase 的前驱是上一个 phase 的 approvedState 或 doneState。
2338
- */
2339
- getPhasePreState(phaseName) {
2340
- const phases = this.def.phases;
2341
- const idx = phases.findIndex((p) => p.name === phaseName);
2342
- if (idx < 0) return void 0;
2343
- if (idx === 0) return "branch_created" /* BranchCreated */;
2344
- const prev = phases[idx - 1];
2345
- return prev.approvedState ?? prev.doneState;
2346
2209
  }
2347
2210
  /**
2348
- * 获取某个 phase 的状态三元组。
2211
+ * 写入缓存。
2349
2212
  */
2350
- getPhaseStates(phaseName) {
2351
- return this.phaseStatesMap.get(phaseName);
2213
+ set(result) {
2214
+ const fp = this.filePath(result.projectPath);
2215
+ try {
2216
+ if (!fs5.existsSync(this.cacheDir)) {
2217
+ fs5.mkdirSync(this.cacheDir, { recursive: true });
2218
+ }
2219
+ fs5.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
2220
+ logger7.debug("Release detect cache written", { projectPath: result.projectPath, path: fp });
2221
+ } catch (err) {
2222
+ logger7.warn("Failed to write release detect cache", { path: fp, error: err.message });
2223
+ }
2352
2224
  }
2353
- // ─── Display helpers (替代 collectStateLabels + derivePhaseStatuses) ───
2354
2225
  /**
2355
- * 解析单条状态的展示标签。
2356
- *
2357
- * 对通用状态 PhaseRunning/PhaseDone,需传入 currentPhase 以生成具体标签(如"分析中");
2358
- * 缺少 currentPhase 时回退到泛化标签(如"阶段执行中")。
2226
+ * 手动失效缓存。
2359
2227
  */
2360
- resolveLabel(state, currentPhase) {
2361
- if ((state === "phase_running" /* PhaseRunning */ || state === "phase_done" /* PhaseDone */) && currentPhase) {
2362
- const phaseLabel = t(`pipeline.phase.${currentPhase}`);
2363
- return state === "phase_running" /* PhaseRunning */ ? t("state.phaseDoing", { label: phaseLabel }) : t("state.phaseDone", { label: phaseLabel });
2364
- }
2365
- if ((state === "phase_waiting" /* PhaseWaiting */ || state === "phase_approved" /* PhaseApproved */) && currentPhase) {
2366
- const phaseLabel = t(`pipeline.phase.${currentPhase}`);
2367
- return state === "phase_waiting" /* PhaseWaiting */ ? t("state.phaseWaiting", { label: phaseLabel }) : t("state.phaseApproved", { label: phaseLabel });
2228
+ invalidate(projectPath) {
2229
+ const fp = this.filePath(projectPath);
2230
+ try {
2231
+ if (fs5.existsSync(fp)) {
2232
+ fs5.unlinkSync(fp);
2233
+ logger7.info("Release detect cache invalidated", { projectPath });
2234
+ return true;
2235
+ }
2236
+ return false;
2237
+ } catch (err) {
2238
+ logger7.warn("Failed to invalidate release detect cache", { path: fp, error: err.message });
2239
+ return false;
2368
2240
  }
2369
- const labels = this.collectStateLabels();
2370
- return labels.get(state) ?? state;
2371
2241
  }
2242
+ };
2243
+
2244
+ // src/prompts/release-templates.ts
2245
+ function releaseDetectPrompt(ctx) {
2246
+ const pd = `.claude-plan/issue-${ctx.issueIid}`;
2247
+ return `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684 DevOps \u5206\u6790\u5E08\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u63A2\u7D22\u5F53\u524D\u9879\u76EE\uFF0C\u67E5\u627E\u6240\u6709\u4E0E**\u53D1\u5E03/\u90E8\u7F72**\u76F8\u5173\u7684\u673A\u5236\u3002
2248
+
2249
+ ## \u63A2\u7D22\u8303\u56F4
2250
+
2251
+ \u8BF7\u6309\u4F18\u5148\u7EA7\u4F9D\u6B21\u68C0\u67E5\u4EE5\u4E0B\u4F4D\u7F6E\uFF1A
2252
+
2253
+ 1. **Skill \u6587\u4EF6**
2254
+ - \`.cursor/skills/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy/publish \u76F8\u5173\u7684 skill
2255
+ - \u9879\u76EE\u6839\u76EE\u5F55\u7684 \`SKILL.md\` \u6216 \`release.md\`
2256
+
2257
+ 2. **Rule \u6587\u4EF6**
2258
+ - \`.cursor/rules/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy \u76F8\u5173\u7684 \`.mdc\` \u89C4\u5219
2259
+
2260
+ 3. **CI/CD \u914D\u7F6E**
2261
+ - \`.gitlab-ci.yml\` / \`Jenkinsfile\` / \`.github/workflows/\`
2262
+ - \u67E5\u627E release/deploy/publish \u76F8\u5173\u7684 stage/job/workflow
2263
+
2264
+ 4. **Package Manager**
2265
+ - \`package.json\` \u4E2D\u7684 \`scripts\` \u5B57\u6BB5\uFF1Apublish\u3001release\u3001deploy \u7B49
2266
+ - \`Makefile\` \u4E2D\u7684 release/deploy target
2267
+
2268
+ 5. **\u811A\u672C\u4E0E\u6587\u6863**
2269
+ - \`scripts/release.*\`\u3001\`scripts/deploy.*\`\u3001\`scripts/publish.*\`
2270
+ - \`RELEASE.md\`\u3001\`CHANGELOG.md\`\u3001\`CONTRIBUTING.md\` \u4E2D\u7684\u53D1\u5E03\u8BF4\u660E
2271
+ - \`Dockerfile\`\u3001\`docker-compose*.yml\`
2272
+
2273
+ ## \u8F93\u51FA\u8981\u6C42
2274
+
2275
+ \u5C06\u68C0\u6D4B\u7ED3\u679C\u4EE5 **\u4E25\u683C JSON** \u683C\u5F0F\u5199\u5165 \`${pd}/03-release-detect.json\`\uFF1A
2276
+
2277
+ \`\`\`json
2278
+ {
2279
+ "hasReleaseCapability": true,
2280
+ "releaseMethod": "npm-publish",
2281
+ "artifacts": ["package.json", ".gitlab-ci.yml"],
2282
+ "instructions": "\u8FD0\u884C npm publish \u53D1\u5E03\u5230 npm registry..."
2283
+ }
2284
+ \`\`\`
2285
+
2286
+ \u5B57\u6BB5\u8BF4\u660E\uFF1A
2287
+ - \`hasReleaseCapability\`: \u662F\u5426\u627E\u5230\u53D1\u5E03\u673A\u5236\uFF08boolean\uFF09
2288
+ - \`releaseMethod\`: \u53D1\u5E03\u65B9\u5F0F\uFF0C\u53EF\u9009\u503C\uFF1A"npm-publish" | "ci-pipeline" | "makefile" | "custom-script" | "skill" | "docker" | \u5176\u4ED6\u63CF\u8FF0
2289
+ - \`artifacts\`: \u53D1\u73B0\u7684\u53D1\u5E03\u76F8\u5173\u6587\u4EF6\u8DEF\u5F84\u6570\u7EC4
2290
+ - \`instructions\`: \u4ECE\u6587\u4EF6\u4E2D\u63D0\u53D6\u7684\u53D1\u5E03\u6B65\u9AA4\u8BF4\u660E\u3002\u5982\u679C\u662F skill/rule \u6587\u4EF6\uFF0C\u8BF7\u5B8C\u6574\u590D\u5236\u5176\u5185\u5BB9
2291
+
2292
+ **\u91CD\u8981**\uFF1A
2293
+ - \u5982\u679C\u6CA1\u6709\u627E\u5230\u4EFB\u4F55\u53D1\u5E03\u673A\u5236\uFF0C\u5C06 \`hasReleaseCapability\` \u8BBE\u4E3A \`false\`\uFF0C\u5176\u4ED6\u5B57\u6BB5\u53EF\u7701\u7565
2294
+ - \u5982\u679C\u627E\u5230\u4E86 skill \u6216 rule \u6587\u4EF6\uFF0C\u8BF7\u5728 \`instructions\` \u4E2D\u5B8C\u6574\u4FDD\u7559\u539F\u6587\u5185\u5BB9\uFF0C\u540E\u7EED\u53D1\u5E03\u6267\u884C\u5C06\u4F9D\u8D56\u5B83
2295
+ - \u53EA\u8F93\u51FA JSON \u6587\u4EF6\uFF0C\u4E0D\u8981\u6267\u884C\u4EFB\u4F55\u53D1\u5E03\u64CD\u4F5C`;
2296
+ }
2297
+ function releaseExecPrompt(ctx, detectResult) {
2298
+ const pd = `.claude-plan/issue-${ctx.issueIid}`;
2299
+ const artifactList = detectResult.artifacts.length > 0 ? detectResult.artifacts.map((a) => `- \`${a}\``).join("\n") : "\uFF08\u65E0\uFF09";
2300
+ const instructionSection = detectResult.instructions ? `
2301
+ ## \u53D1\u5E03\u6307\u5F15\uFF08\u6765\u81EA\u9879\u76EE skill/rule/\u6587\u6863\uFF09
2302
+
2303
+ ${detectResult.instructions}` : "";
2304
+ return `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u53D1\u5E03\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6839\u636E\u4EE5\u4E0B\u68C0\u6D4B\u7ED3\u679C\uFF0C\u6267\u884C\u9879\u76EE\u7684\u53D1\u5E03\u6D41\u7A0B\u3002
2305
+
2306
+ ## \u68C0\u6D4B\u7ED3\u679C
2307
+
2308
+ - **\u53D1\u5E03\u65B9\u5F0F**: ${detectResult.releaseMethod ?? "\u672A\u77E5"}
2309
+ - **\u76F8\u5173\u6587\u4EF6**:
2310
+ ${artifactList}
2311
+ ${instructionSection}
2312
+
2313
+ ## \u4EFB\u52A1
2314
+
2315
+ 1. \u9605\u8BFB\u4E0A\u8FF0\u53D1\u5E03\u6307\u5F15\u548C\u76F8\u5173\u6587\u4EF6
2316
+ 2. \u6309\u7167\u9879\u76EE\u5B9A\u4E49\u7684\u53D1\u5E03\u6D41\u7A0B\u6267\u884C\u53D1\u5E03\u64CD\u4F5C
2317
+ 3. \u5C06\u53D1\u5E03\u7ED3\u679C\u5199\u5165 \`${pd}/04-release-report.md\`
2318
+
2319
+ ## \u53D1\u5E03\u62A5\u544A\u683C\u5F0F
2320
+
2321
+ \`\`\`markdown
2322
+ # \u53D1\u5E03\u62A5\u544A
2323
+
2324
+ ## \u53D1\u5E03\u65B9\u5F0F
2325
+ <\u4F7F\u7528\u7684\u53D1\u5E03\u65B9\u5F0F>
2326
+
2327
+ ## \u6267\u884C\u6B65\u9AA4
2328
+ <\u9010\u6B65\u8BB0\u5F55\u6267\u884C\u7684\u64CD\u4F5C>
2329
+
2330
+ ## \u7ED3\u679C
2331
+ <\u6210\u529F/\u5931\u8D25\u53CA\u8BE6\u60C5>
2332
+ \`\`\`
2333
+
2334
+ **\u91CD\u8981**\uFF1A
2335
+ - \u4E25\u683C\u6309\u7167\u9879\u76EE\u81EA\u8EAB\u7684\u53D1\u5E03\u6D41\u7A0B\u64CD\u4F5C
2336
+ - \u5982\u6709\u4EFB\u4F55\u9519\u8BEF\u6216\u5F02\u5E38\uFF0C\u8BB0\u5F55\u5728\u62A5\u544A\u4E2D
2337
+ - \u4E0D\u8981\u8DF3\u8FC7\u4EFB\u4F55\u53D1\u5E03\u524D\u7684\u68C0\u67E5\u6B65\u9AA4`;
2338
+ }
2339
+
2340
+ // src/phases/ReleasePhase.ts
2341
+ var logger8 = logger.child("ReleasePhase");
2342
+ var DETECT_FILENAME = "05-release-detect.json";
2343
+ var REPORT_FILENAME = "06-release-report.md";
2344
+ var ReleasePhase = class extends BasePhase {
2345
+ phaseName = "release";
2346
+ get planDir() {
2347
+ const displayId = Number(this.currentCtx?.demand.sourceRef.displayId ?? 0);
2348
+ return path7.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
2349
+ }
2350
+ /** 暂存当前 ctx 供 planDir getter 使用 */
2351
+ currentCtx;
2372
2352
  /**
2373
- * 收集所有状态及其展示标签。
2374
- * 为通用状态 PhaseRunning/PhaseDone 生成每个阶段的复合 key 条目。
2353
+ * 检测结果是否已存在(即是否处于执行模式)。
2375
2354
  */
2376
- collectStateLabels() {
2377
- const labels = /* @__PURE__ */ new Map();
2378
- labels.set("pending" /* Pending */, t("state.pending"));
2379
- labels.set("skipped" /* Skipped */, t("state.skipped"));
2380
- labels.set("branch_created" /* BranchCreated */, t("state.branchCreated"));
2381
- for (const phase of this.def.phases) {
2382
- const phaseLabel = t(`pipeline.phase.${phase.name}`);
2383
- if (phase.startState === "phase_running" /* PhaseRunning */) {
2384
- labels.set(`phase_running:${phase.name}`, t("state.phaseDoing", { label: phaseLabel }));
2385
- } else if (phase.startState === "phase_waiting" /* PhaseWaiting */) {
2386
- labels.set(`phase_waiting:${phase.name}`, t("state.phaseWaiting", { label: phaseLabel }));
2387
- } else {
2388
- labels.set(phase.startState, t("state.phaseDoing", { label: phaseLabel }));
2389
- }
2390
- if (phase.doneState === "phase_done" /* PhaseDone */) {
2391
- labels.set(`phase_done:${phase.name}`, t("state.phaseDone", { label: phaseLabel }));
2392
- } else if (phase.doneState === "phase_approved" /* PhaseApproved */) {
2393
- labels.set(`phase_approved:${phase.name}`, t("state.phaseApproved", { label: phaseLabel }));
2394
- } else if (phase.doneState !== "completed" /* Completed */) {
2395
- labels.set(phase.doneState, t("state.phaseDone", { label: phaseLabel }));
2396
- }
2397
- if (phase.approvedState && phase.approvedState !== "phase_approved" /* PhaseApproved */ && phase.approvedState !== phase.doneState) {
2398
- labels.set(phase.approvedState, t("state.phaseApproved", { label: phaseLabel }));
2399
- }
2355
+ hasDetectionResult() {
2356
+ return fs6.existsSync(path7.join(this.planDir, DETECT_FILENAME));
2357
+ }
2358
+ readDetectionFile() {
2359
+ const fp = path7.join(this.planDir, DETECT_FILENAME);
2360
+ if (!fs6.existsSync(fp)) return null;
2361
+ try {
2362
+ return JSON.parse(fs6.readFileSync(fp, "utf-8"));
2363
+ } catch {
2364
+ return null;
2365
+ }
2366
+ }
2367
+ writeDetectionFile(result) {
2368
+ const dir = this.planDir;
2369
+ if (!fs6.existsSync(dir)) {
2370
+ fs6.mkdirSync(dir, { recursive: true });
2400
2371
  }
2401
- 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;
2372
+ fs6.writeFileSync(path7.join(dir, DETECT_FILENAME), JSON.stringify(result, null, 2), "utf-8");
2405
2373
  }
2406
- /**
2407
- * 根据当前 state,推导每个 phase 的进度状态。
2408
- * 支持通用状态 PhaseRunning/PhaseDone + currentPhase。
2409
- */
2410
- derivePhaseStatuses(currentState, currentPhase) {
2411
- const result = {};
2412
- let passedCurrent = false;
2413
- if (currentState === "failed" /* Failed */ && currentPhase) {
2414
- for (const phase of this.def.phases) {
2415
- if (passedCurrent) {
2416
- result[phase.name] = "pending";
2417
- } else if (phase.name === currentPhase) {
2418
- result[phase.name] = "failed";
2419
- passedCurrent = true;
2420
- } else {
2421
- result[phase.name] = "completed";
2422
- }
2423
- }
2424
- return result;
2374
+ getResultFiles(_ctx) {
2375
+ if (this.hasDetectionResult()) {
2376
+ return [{ filename: REPORT_FILENAME, label: "\u53D1\u5E03\u62A5\u544A" }];
2425
2377
  }
2426
- if ((currentState === "phase_running" /* PhaseRunning */ || currentState === "phase_done" /* PhaseDone */) && currentPhase) {
2427
- for (const phase of this.def.phases) {
2428
- if (passedCurrent) {
2429
- result[phase.name] = "pending";
2430
- } else if (phase.name === currentPhase) {
2431
- result[phase.name] = currentState === "phase_running" /* PhaseRunning */ ? "in_progress" : "completed";
2432
- passedCurrent = currentState === "phase_running" /* PhaseRunning */;
2433
- if (currentState === "phase_done" /* PhaseDone */) passedCurrent = true;
2434
- } else {
2435
- result[phase.name] = "completed";
2436
- }
2437
- }
2438
- return result;
2378
+ return [{ filename: DETECT_FILENAME, label: "\u53D1\u5E03\u68C0\u6D4B\u62A5\u544A" }];
2379
+ }
2380
+ buildPrompt(ctx) {
2381
+ const dc = demandToPromptContext(ctx.demand);
2382
+ const pc = {
2383
+ issueTitle: dc.title,
2384
+ issueDescription: dc.description,
2385
+ issueIid: Number(dc.displayId),
2386
+ supplementText: dc.supplementText || void 0,
2387
+ workspace: ctx.workspace
2388
+ };
2389
+ if (this.hasDetectionResult()) {
2390
+ const detect = this.readDetectionFile();
2391
+ return releaseExecPrompt(pc, detect);
2439
2392
  }
2440
- if ((currentState === "phase_waiting" /* PhaseWaiting */ || currentState === "phase_approved" /* PhaseApproved */) && currentPhase) {
2441
- for (const phase of this.def.phases) {
2442
- if (passedCurrent) {
2443
- result[phase.name] = "pending";
2444
- } else if (phase.name === currentPhase) {
2445
- const isGatedAi = phase.kind === "ai" && phase.approvedState;
2446
- result[phase.name] = currentState === "phase_waiting" /* PhaseWaiting */ || currentState === "phase_approved" /* PhaseApproved */ && isGatedAi ? "in_progress" : "completed";
2447
- passedCurrent = true;
2448
- } else {
2449
- result[phase.name] = "completed";
2393
+ return releaseDetectPrompt(pc);
2394
+ }
2395
+ async execute(ctx) {
2396
+ this.currentCtx = ctx;
2397
+ const isDetect = !this.hasDetectionResult();
2398
+ if (isDetect) {
2399
+ const cache = new ReleaseDetectCache(resolveDataDir());
2400
+ const projectPath = this.config.gongfeng.projectPath;
2401
+ const ttlMs = this.config.release.detectCacheTtlMs;
2402
+ const cached = cache.get(projectPath, ttlMs);
2403
+ if (cached) {
2404
+ logger8.info("Using cached release detection result", {
2405
+ projectPath,
2406
+ hasCapability: cached.hasReleaseCapability,
2407
+ method: cached.releaseMethod
2408
+ });
2409
+ await this.hooks.onPhaseStart(this.phaseName);
2410
+ this.writeDetectionFile(cached);
2411
+ this.plan.updatePhaseProgress(this.phaseName, "completed");
2412
+ try {
2413
+ await this.git.add(["."]);
2414
+ await this.git.commit(`chore: release detection (cached) for issue-${ctx.demand.sourceRef.displayId}`);
2415
+ } catch {
2450
2416
  }
2417
+ await this.hooks.onPhaseDone(this.phaseName);
2418
+ return {
2419
+ success: true,
2420
+ output: `Release detection cached: ${cached.hasReleaseCapability ? cached.releaseMethod ?? "found" : "no capability"}`,
2421
+ exitCode: 0,
2422
+ gateRequested: cached.hasReleaseCapability,
2423
+ hasReleaseCapability: cached.hasReleaseCapability
2424
+ };
2451
2425
  }
2452
- return result;
2453
- }
2454
- if (currentState === "completed" /* Completed */ || currentState === "resolving_conflict" /* ResolvingConflict */) {
2455
- for (const phase of this.def.phases) {
2456
- result[phase.name] = "completed";
2457
- }
2458
- return result;
2459
2426
  }
2460
- for (const phase of this.def.phases) {
2461
- if (passedCurrent) {
2462
- result[phase.name] = "pending";
2463
- continue;
2464
- }
2465
- if (phase.startState === currentState) {
2466
- result[phase.name] = "in_progress";
2467
- passedCurrent = true;
2468
- } else if (phase.doneState === currentState || phase.approvedState === currentState) {
2469
- result[phase.name] = "completed";
2470
- } else {
2471
- result[phase.name] = "pending";
2427
+ const result = await super.execute(ctx);
2428
+ if (isDetect && result.success) {
2429
+ const detected = this.readDetectionFile();
2430
+ if (detected) {
2431
+ const cache = new ReleaseDetectCache(resolveDataDir());
2432
+ const projectPath = this.config.gongfeng.projectPath;
2433
+ cache.set({
2434
+ ...detected,
2435
+ projectPath,
2436
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString()
2437
+ });
2438
+ logger8.info("Release detection completed", {
2439
+ hasCapability: detected.hasReleaseCapability,
2440
+ method: detected.releaseMethod,
2441
+ artifacts: detected.artifacts
2442
+ });
2443
+ return {
2444
+ ...result,
2445
+ gateRequested: detected.hasReleaseCapability,
2446
+ hasReleaseCapability: detected.hasReleaseCapability
2447
+ };
2472
2448
  }
2449
+ return { ...result, gateRequested: false, hasReleaseCapability: false };
2473
2450
  }
2474
- return result;
2475
- }
2476
- // ─── Pipeline Protocol queries (P1) ───
2477
- /**
2478
- * 返回可被用户单独重试的阶段名列表。
2479
- * 优先使用 spec.retryable,未声明时默认 kind === 'ai'。
2480
- */
2481
- getRetryablePhases() {
2482
- return this.def.phases.filter((spec) => spec.retryable ?? spec.kind === "ai").map((spec) => spec.name);
2483
- }
2484
- /**
2485
- * 判断指定阶段是否可重试。
2486
- */
2487
- isRetryable(phaseName) {
2488
- const spec = this.def.phases.find((p) => p.name === phaseName);
2489
- if (!spec) return false;
2490
- return spec.retryable ?? spec.kind === "ai";
2491
- }
2492
- /**
2493
- * 查找 gate 类型的阶段。
2494
- */
2495
- getGatePhase() {
2496
- return this.def.phases.find((p) => p.kind === "gate");
2451
+ return { ...result, gateRequested: false };
2497
2452
  }
2498
- /**
2499
- * 判断指定阶段完成后是否应启动预览服务器。
2500
- */
2501
- shouldDeployPreview(phaseName) {
2502
- const spec = this.def.phases.find((p) => p.name === phaseName);
2503
- return spec?.deploysPreview ?? false;
2453
+ };
2454
+
2455
+ // src/phases/UatPhase.ts
2456
+ function getDefaultHost() {
2457
+ return getLocalIP();
2458
+ }
2459
+ var UatPhase = class extends BasePhase {
2460
+ phaseName = "uat";
2461
+ getResultFiles(_ctx) {
2462
+ return [{ filename: "03-uat-report.md", label: "UAT \u62A5\u544A" }];
2504
2463
  }
2505
- /**
2506
- * 收集所有阶段的产物文件,扁平化为单一列表。
2507
- */
2508
- collectArtifacts() {
2509
- return this.def.phases.flatMap((spec) => spec.artifacts ?? []);
2464
+ buildPrompt(ctx) {
2465
+ const pc = demandToPromptContext(ctx.demand);
2466
+ const promptCtx = {
2467
+ issueTitle: pc.title,
2468
+ issueDescription: pc.description,
2469
+ issueIid: Number(pc.displayId),
2470
+ workspace: ctx.workspace
2471
+ };
2472
+ const hasUatTool = !!this.config.e2e.uatVendorDir;
2473
+ const systemE2e = this.hooks.isE2eEnabled();
2474
+ const e2ePorts = systemE2e && ctx.ports ? {
2475
+ backendPort: ctx.ports.backendPort,
2476
+ frontendPort: ctx.ports.frontendPort,
2477
+ host: this.config.preview.host || getDefaultHost()
2478
+ } : void 0;
2479
+ const uatTool = hasUatTool ? { vendorDir: this.config.e2e.uatVendorDir, configFile: this.config.e2e.uatConfigFile } : void 0;
2480
+ const e2eSuffix = e2eVerifyPromptSuffix(promptCtx, e2ePorts, uatTool);
2481
+ return `# UAT/E2E \u9A8C\u8BC1\u9636\u6BB5
2482
+
2483
+ ## \u4EFB\u52A1\u80CC\u666F
2484
+ - Issue: #${promptCtx.issueIid} ${promptCtx.issueTitle}
2485
+ - \u672C\u9636\u6BB5\u4E13\u6CE8\u4E8E E2E/UAT \u7AEF\u5BF9\u7AEF\u9A8C\u8BC1\uFF0Clint/build/test \u68C0\u67E5\u5DF2\u5728\u4E0A\u4E00\u9A8C\u8BC1\u9636\u6BB5\u5B8C\u6210\u3002
2486
+
2487
+ ## \u4EA7\u7269\u8981\u6C42
2488
+ \u8BF7\u5C06 E2E \u9A8C\u8BC1\u7ED3\u679C\u5199\u5165 \`.claude-plan/issue-${promptCtx.issueIid}/03-uat-report.md\`\uFF0C\u5305\u542B\uFF1A
2489
+ - \u6D4B\u8BD5\u573A\u666F\u6E05\u5355
2490
+ - \u6267\u884C\u7ED3\u679C\uFF08\u901A\u8FC7/\u5931\u8D25\uFF09
2491
+ - \u622A\u56FE\u8DEF\u5F84\uFF08\u5982\u6709\uFF09
2492
+ - \u5931\u8D25\u539F\u56E0\u5206\u6790\uFF08\u5982\u6709\u5931\u8D25\uFF09
2493
+
2494
+ ${e2eSuffix}`;
2510
2495
  }
2511
- /**
2512
- * 返回所有阶段的名称和标签(保持定义顺序)。
2513
- */
2514
- getPhaseDefs() {
2515
- return this.def.phases.map((p) => ({ name: p.name, label: p.label }));
2496
+ };
2497
+
2498
+ // src/phases/PhaseFactory.ts
2499
+ var PHASE_REGISTRY = /* @__PURE__ */ new Map();
2500
+ function registerPhase(name, ctor) {
2501
+ PHASE_REGISTRY.set(name, ctor);
2502
+ }
2503
+ function registerBuiltinPhases() {
2504
+ PHASE_REGISTRY.set("plan", PlanPhase);
2505
+ PHASE_REGISTRY.set("build", BuildPhase);
2506
+ PHASE_REGISTRY.set("verify", VerifyPhase);
2507
+ PHASE_REGISTRY.set("uat", UatPhase);
2508
+ PHASE_REGISTRY.set("release", ReleasePhase);
2509
+ }
2510
+ registerBuiltinPhases();
2511
+ function createPhase(name, ...args) {
2512
+ const Ctor = PHASE_REGISTRY.get(name);
2513
+ if (!Ctor) {
2514
+ throw new PhaseNotRegisteredError(name, [...PHASE_REGISTRY.keys()]);
2516
2515
  }
2517
- /**
2518
- * 返回所有 kind === 'ai' 的阶段名(可执行阶段)。
2519
- */
2520
- getExecutablePhaseNames() {
2521
- return this.def.phases.filter((spec) => spec.kind === "ai").map((spec) => spec.name);
2516
+ return new Ctor(...args);
2517
+ }
2518
+ function validatePhaseRegistry(phaseNames) {
2519
+ const missing = phaseNames.filter((name) => !PHASE_REGISTRY.has(name));
2520
+ if (missing.length > 0) {
2521
+ throw new UnregisteredPhasesError(missing, [...PHASE_REGISTRY.keys()]);
2522
2522
  }
2523
- };
2523
+ }
2524
2524
 
2525
2525
  // src/pipeline/PipelineDefinition.ts
2526
2526
  var pipelineRegistry = /* @__PURE__ */ new Map();
@@ -4374,7 +4374,10 @@ var PipelineOrchestrator = class {
4374
4374
  throw new InvalidPhaseError(phase);
4375
4375
  }
4376
4376
  logger19.info("Retrying issue from phase", { issueIid, phase });
4377
- this.tracker.resetToPhase(issueIid, phase, issueDef);
4377
+ const ok = this.tracker.resetToPhase(issueIid, phase, issueDef);
4378
+ if (!ok) {
4379
+ throw new InvalidPhaseError(phase);
4380
+ }
4378
4381
  }
4379
4382
  getIssueSpecificPipelineDef(record) {
4380
4383
  if (record.pipelineMode) {
@@ -5095,4 +5098,4 @@ export {
5095
5098
  PipelineOrchestrator,
5096
5099
  BrainstormService
5097
5100
  };
5098
- //# sourceMappingURL=chunk-S3ULUOTM.js.map
5101
+ //# sourceMappingURL=chunk-7D2NH37P.js.map