@xdevops/issue-auto-finish 1.0.90 → 1.0.92

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.
Files changed (53) hide show
  1. package/dist/{chunk-TDKTI363.js → chunk-2WDVTLVF.js} +1 -1
  2. package/dist/{chunk-Y62E72TA.js → chunk-6T7ZHAV2.js} +2 -2
  3. package/dist/{chunk-IWSMQXBL.js → chunk-WZGEYHCC.js} +1300 -600
  4. package/dist/chunk-WZGEYHCC.js.map +1 -0
  5. package/dist/cli.js +2 -2
  6. package/dist/hooks/HookEventWatcher.d.ts +34 -0
  7. package/dist/hooks/HookEventWatcher.d.ts.map +1 -0
  8. package/dist/hooks/HookInjector.d.ts +85 -0
  9. package/dist/hooks/HookInjector.d.ts.map +1 -0
  10. package/dist/hooks/index.d.ts +4 -0
  11. package/dist/hooks/index.d.ts.map +1 -0
  12. package/dist/index.js +2 -2
  13. package/dist/lib.js +1 -1
  14. package/dist/lifecycle/DefaultLifecycleHook.d.ts +21 -0
  15. package/dist/lifecycle/DefaultLifecycleHook.d.ts.map +1 -0
  16. package/dist/lifecycle/FeedbackTypes.d.ts +52 -0
  17. package/dist/lifecycle/FeedbackTypes.d.ts.map +1 -0
  18. package/dist/lifecycle/PhaseLifecycleHook.d.ts +70 -0
  19. package/dist/lifecycle/PhaseLifecycleHook.d.ts.map +1 -0
  20. package/dist/lifecycle/PhaseMiddleware.d.ts +47 -0
  21. package/dist/lifecycle/PhaseMiddleware.d.ts.map +1 -0
  22. package/dist/lifecycle/PhaseStateMachine.d.ts +111 -0
  23. package/dist/lifecycle/PhaseStateMachine.d.ts.map +1 -0
  24. package/dist/lifecycle/index.d.ts +8 -0
  25. package/dist/lifecycle/index.d.ts.map +1 -1
  26. package/dist/orchestrator/steps/PhaseHelpers.d.ts +24 -0
  27. package/dist/orchestrator/steps/PhaseHelpers.d.ts.map +1 -0
  28. package/dist/orchestrator/steps/PhaseLoopStep.d.ts +10 -0
  29. package/dist/orchestrator/steps/PhaseLoopStep.d.ts.map +1 -1
  30. package/dist/orchestrator/steps/SetupStep.d.ts.map +1 -1
  31. package/dist/orchestrator/strategies/AiPhaseStrategy.d.ts +17 -0
  32. package/dist/orchestrator/strategies/AiPhaseStrategy.d.ts.map +1 -0
  33. package/dist/orchestrator/strategies/GateStrategy.d.ts +15 -0
  34. package/dist/orchestrator/strategies/GateStrategy.d.ts.map +1 -0
  35. package/dist/orchestrator/strategies/PhaseStrategy.d.ts +16 -0
  36. package/dist/orchestrator/strategies/PhaseStrategy.d.ts.map +1 -0
  37. package/dist/orchestrator/strategies/VerifyFixStrategy.d.ts +15 -0
  38. package/dist/orchestrator/strategies/VerifyFixStrategy.d.ts.map +1 -0
  39. package/dist/orchestrator/strategies/index.d.ts +17 -0
  40. package/dist/orchestrator/strategies/index.d.ts.map +1 -0
  41. package/dist/phases/BasePhase.d.ts +2 -0
  42. package/dist/phases/BasePhase.d.ts.map +1 -1
  43. package/dist/{restart-2BCP6AMK.js → restart-5D3ZDD5L.js} +2 -2
  44. package/dist/run.js +2 -2
  45. package/dist/{start-ECUOKGM2.js → start-IQBNXLEI.js} +2 -2
  46. package/package.json +1 -1
  47. package/src/web/frontend/dist/assets/{index-Dby4j-V_.js → index-BR0UoQER.js} +2 -2
  48. package/src/web/frontend/dist/index.html +1 -1
  49. package/dist/chunk-IWSMQXBL.js.map +0 -1
  50. /package/dist/{chunk-TDKTI363.js.map → chunk-2WDVTLVF.js.map} +0 -0
  51. /package/dist/{chunk-Y62E72TA.js.map → chunk-6T7ZHAV2.js.map} +0 -0
  52. /package/dist/{restart-2BCP6AMK.js.map → restart-5D3ZDD5L.js.map} +0 -0
  53. /package/dist/{start-ECUOKGM2.js.map → start-IQBNXLEI.js.map} +0 -0
@@ -233,8 +233,8 @@ var GongfengClient = class {
233
233
  const encoded = encodeURIComponent(this.projectPath);
234
234
  return `${this.apiUrl}/api/v3/projects/${encoded}`;
235
235
  }
236
- async requestRaw(path12, options = {}) {
237
- const url = `${this.projectApiBase}${path12}`;
236
+ async requestRaw(path13, options = {}) {
237
+ const url = `${this.projectApiBase}${path13}`;
238
238
  logger4.debug("API request", { method: options.method || "GET", url });
239
239
  return this.circuitBreaker.execute(
240
240
  () => this.retryPolicy.execute(async () => {
@@ -251,11 +251,11 @@ var GongfengClient = class {
251
251
  throw new GongfengApiError(resp.status, `Gongfeng API error ${resp.status}: ${body}`, body);
252
252
  }
253
253
  return resp;
254
- }, `requestRaw ${options.method || "GET"} ${path12}`)
254
+ }, `requestRaw ${options.method || "GET"} ${path13}`)
255
255
  );
256
256
  }
257
- async request(path12, options = {}) {
258
- const resp = await this.requestRaw(path12, options);
257
+ async request(path13, options = {}) {
258
+ const resp = await this.requestRaw(path13, options);
259
259
  return resp.json();
260
260
  }
261
261
  async createIssue(title, description, labels) {
@@ -434,8 +434,8 @@ var GongfengClient = class {
434
434
  }
435
435
  return mr;
436
436
  }
437
- async requestGlobal(path12, options = {}) {
438
- const url = `${this.apiUrl}${path12}`;
437
+ async requestGlobal(path13, options = {}) {
438
+ const url = `${this.apiUrl}${path13}`;
439
439
  logger4.debug("API request (global)", { method: options.method || "GET", url });
440
440
  const resp = await this.circuitBreaker.execute(
441
441
  () => this.retryPolicy.execute(async () => {
@@ -452,7 +452,7 @@ var GongfengClient = class {
452
452
  throw new GongfengApiError(r.status, `Gongfeng API error ${r.status}: ${body}`, body);
453
453
  }
454
454
  return r;
455
- }, `requestGlobal ${options.method || "GET"} ${path12}`)
455
+ }, `requestGlobal ${options.method || "GET"} ${path13}`)
456
456
  );
457
457
  return resp.json();
458
458
  }
@@ -1646,7 +1646,7 @@ var PlanPersistence = class _PlanPersistence {
1646
1646
  };
1647
1647
 
1648
1648
  // src/phases/BasePhase.ts
1649
- import path4 from "path";
1649
+ import path5 from "path";
1650
1650
 
1651
1651
  // src/rules/RuleResolver.ts
1652
1652
  import { readdir, readFile } from "fs/promises";
@@ -1743,6 +1743,454 @@ ${rule.content}`;
1743
1743
  }
1744
1744
  };
1745
1745
 
1746
+ // src/hooks/HookInjector.ts
1747
+ import fs3 from "fs";
1748
+ import path4 from "path";
1749
+ var logger7 = logger.child("HookInjector");
1750
+ var HOOKS_DIR = ".claude-plan/.hooks";
1751
+ var EVENTS_FILE_NAME = ".hook-events.jsonl";
1752
+ var MANIFEST_FILE_NAME = ".artifact-manifest.jsonl";
1753
+ var CONTEXT_FILE_NAME = ".hook-context.json";
1754
+ var HookInjector = class {
1755
+ inject(ctx) {
1756
+ this.writeHookScripts(ctx);
1757
+ this.writeContextFile(ctx);
1758
+ this.writeSettingsLocal(ctx);
1759
+ this.initEventsFile(ctx);
1760
+ logger7.info("Hooks injected", {
1761
+ workDir: ctx.workDir,
1762
+ issueIid: ctx.issueIid,
1763
+ phase: ctx.phaseName,
1764
+ artifacts: ctx.expectedArtifacts
1765
+ });
1766
+ }
1767
+ /**
1768
+ * 阶段切换时更新 hooks 配置(重写脚本 + settings.local.json)。
1769
+ * 保留 events/manifest 文件(不截断),仅更新脚本和配置。
1770
+ */
1771
+ updateForPhase(ctx) {
1772
+ this.writeHookScripts(ctx);
1773
+ this.writeContextFile(ctx);
1774
+ this.writeSettingsLocal(ctx);
1775
+ logger7.info("Hooks updated for phase", {
1776
+ workDir: ctx.workDir,
1777
+ issueIid: ctx.issueIid,
1778
+ phase: ctx.phaseName
1779
+ });
1780
+ }
1781
+ readManifest(workDir) {
1782
+ const manifestPath = path4.join(workDir, ".claude-plan", MANIFEST_FILE_NAME);
1783
+ return readJsonl(manifestPath);
1784
+ }
1785
+ readEvents(workDir) {
1786
+ const eventsPath = path4.join(workDir, ".claude-plan", EVENTS_FILE_NAME);
1787
+ return readJsonl(eventsPath);
1788
+ }
1789
+ getEventsFilePath(workDir) {
1790
+ return path4.join(workDir, ".claude-plan", EVENTS_FILE_NAME);
1791
+ }
1792
+ getManifestFilePath(workDir) {
1793
+ return path4.join(workDir, ".claude-plan", MANIFEST_FILE_NAME);
1794
+ }
1795
+ cleanup(workDir) {
1796
+ const hooksDir = path4.join(workDir, HOOKS_DIR);
1797
+ try {
1798
+ if (fs3.existsSync(hooksDir)) {
1799
+ fs3.rmSync(hooksDir, { recursive: true });
1800
+ }
1801
+ } catch (err) {
1802
+ logger7.warn("Failed to cleanup hooks", { error: err.message });
1803
+ }
1804
+ }
1805
+ // ---------------------------------------------------------------------------
1806
+ // Private
1807
+ // ---------------------------------------------------------------------------
1808
+ writeHookScripts(ctx) {
1809
+ const hooksDir = path4.join(ctx.workDir, HOOKS_DIR);
1810
+ fs3.mkdirSync(hooksDir, { recursive: true });
1811
+ const eventsFile = path4.join(ctx.workDir, ".claude-plan", EVENTS_FILE_NAME);
1812
+ const manifestFile = path4.join(ctx.workDir, ".claude-plan", MANIFEST_FILE_NAME);
1813
+ const contextFile = path4.join(ctx.workDir, ".claude-plan", CONTEXT_FILE_NAME);
1814
+ const expected = ctx.expectedArtifacts.join(",");
1815
+ const phaseExpected = (ctx.phaseExpectedArtifacts ?? ctx.expectedArtifacts).join(",");
1816
+ const scripts = [
1817
+ { name: "session-start.sh", content: buildSessionStartScript(eventsFile) },
1818
+ { name: "compact-restore.sh", content: buildCompactRestoreScript(eventsFile, contextFile) },
1819
+ { name: "post-tool-use.sh", content: buildPostToolUseScript(eventsFile, manifestFile, expected) },
1820
+ { name: "post-artifact.sh", content: buildPostArtifactScript(manifestFile, expected) },
1821
+ { name: "exit-plan-mode.sh", content: buildExitPlanModeScript(eventsFile) },
1822
+ { name: "permission.sh", content: buildPermissionScript(eventsFile) },
1823
+ { name: "protect-files.sh", content: buildProtectFilesScript(eventsFile, ctx.phaseName, ctx.planDir) },
1824
+ { name: "stop.sh", content: buildStopScript(eventsFile, ctx.planDir, phaseExpected) }
1825
+ ];
1826
+ for (const { name, content } of scripts) {
1827
+ const scriptPath = path4.join(hooksDir, name);
1828
+ fs3.writeFileSync(scriptPath, content, { mode: 493 });
1829
+ }
1830
+ }
1831
+ writeContextFile(ctx) {
1832
+ const contextPath = path4.join(ctx.workDir, ".claude-plan", CONTEXT_FILE_NAME);
1833
+ const context = {
1834
+ issueIid: ctx.issueIid,
1835
+ issueTitle: ctx.issueTitle ?? "",
1836
+ issueDescription: ctx.issueDescription ?? "",
1837
+ phaseName: ctx.phaseName ?? "",
1838
+ expectedArtifacts: ctx.expectedArtifacts,
1839
+ planDir: ctx.planDir
1840
+ };
1841
+ fs3.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
1842
+ }
1843
+ writeSettingsLocal(ctx) {
1844
+ const claudeDir = path4.join(ctx.workDir, ".claude");
1845
+ fs3.mkdirSync(claudeDir, { recursive: true });
1846
+ const settingsPath = path4.join(claudeDir, "settings.local.json");
1847
+ let existing = {};
1848
+ if (fs3.existsSync(settingsPath)) {
1849
+ try {
1850
+ existing = JSON.parse(fs3.readFileSync(settingsPath, "utf-8"));
1851
+ } catch {
1852
+ logger7.warn("Failed to parse existing settings.local.json, overwriting");
1853
+ }
1854
+ }
1855
+ const hooksDir = path4.join(ctx.workDir, HOOKS_DIR);
1856
+ const hooks = buildHooksConfig(hooksDir, ctx);
1857
+ const merged = { ...existing, hooks };
1858
+ fs3.writeFileSync(settingsPath, JSON.stringify(merged, null, 2), "utf-8");
1859
+ }
1860
+ initEventsFile(ctx) {
1861
+ const eventsPath = path4.join(ctx.workDir, ".claude-plan", EVENTS_FILE_NAME);
1862
+ const manifestPath = path4.join(ctx.workDir, ".claude-plan", MANIFEST_FILE_NAME);
1863
+ fs3.writeFileSync(eventsPath, "", "utf-8");
1864
+ fs3.writeFileSync(manifestPath, "", "utf-8");
1865
+ }
1866
+ };
1867
+ function buildHooksConfig(hooksDir, ctx) {
1868
+ const isPlanPhase = ctx.phaseName === "plan";
1869
+ const artifactIfPatterns = buildArtifactIfPatterns(ctx.expectedArtifacts);
1870
+ const config = {
1871
+ SessionStart: [
1872
+ {
1873
+ hooks: [{
1874
+ type: "command",
1875
+ command: path4.join(hooksDir, "session-start.sh"),
1876
+ timeout: 5
1877
+ }]
1878
+ },
1879
+ {
1880
+ matcher: "compact",
1881
+ hooks: [{
1882
+ type: "command",
1883
+ command: path4.join(hooksDir, "compact-restore.sh"),
1884
+ timeout: 5
1885
+ }]
1886
+ }
1887
+ ],
1888
+ PreToolUse: [
1889
+ {
1890
+ matcher: "Edit|Write",
1891
+ hooks: [{
1892
+ type: "command",
1893
+ command: path4.join(hooksDir, "protect-files.sh"),
1894
+ timeout: 5,
1895
+ ...buildProtectIfClause(ctx.phaseName)
1896
+ }]
1897
+ }
1898
+ ],
1899
+ PostToolUse: buildPostToolUseConfig(hooksDir, artifactIfPatterns),
1900
+ PermissionRequest: buildPermissionRequestConfig(hooksDir, isPlanPhase),
1901
+ Stop: [{
1902
+ hooks: [{
1903
+ type: "command",
1904
+ command: path4.join(hooksDir, "stop.sh"),
1905
+ timeout: 15
1906
+ }]
1907
+ }]
1908
+ };
1909
+ return config;
1910
+ }
1911
+ function buildPermissionRequestConfig(hooksDir, isPlanPhase) {
1912
+ const groups = [];
1913
+ if (isPlanPhase) {
1914
+ groups.push({
1915
+ matcher: "ExitPlanMode",
1916
+ hooks: [{
1917
+ type: "command",
1918
+ command: path4.join(hooksDir, "exit-plan-mode.sh"),
1919
+ timeout: 5
1920
+ }]
1921
+ });
1922
+ }
1923
+ groups.push({
1924
+ matcher: "Bash|Edit|Write|Read|Glob|Grep|WebFetch|WebSearch|mcp__.*",
1925
+ hooks: [{
1926
+ type: "command",
1927
+ command: path4.join(hooksDir, "permission.sh"),
1928
+ timeout: 5
1929
+ }]
1930
+ });
1931
+ return groups;
1932
+ }
1933
+ function buildPostToolUseConfig(hooksDir, artifactIfPatterns) {
1934
+ const groups = [];
1935
+ if (artifactIfPatterns) {
1936
+ groups.push({
1937
+ matcher: "Write|Edit",
1938
+ hooks: [{
1939
+ type: "command",
1940
+ command: path4.join(hooksDir, "post-artifact.sh"),
1941
+ timeout: 10,
1942
+ if: artifactIfPatterns
1943
+ }]
1944
+ });
1945
+ }
1946
+ groups.push({
1947
+ matcher: "Write|Edit",
1948
+ hooks: [{
1949
+ type: "command",
1950
+ command: path4.join(hooksDir, "post-tool-use.sh"),
1951
+ timeout: 10
1952
+ }]
1953
+ });
1954
+ return groups;
1955
+ }
1956
+ function buildArtifactIfPatterns(artifacts) {
1957
+ if (artifacts.length === 0) return void 0;
1958
+ return artifacts.flatMap((f) => [`Write(*${f})`, `Edit(*${f})`]).join("|");
1959
+ }
1960
+ function buildProtectIfClause(phaseName) {
1961
+ const alwaysProtected = [".env", ".env.*", "package-lock.json", "pnpm-lock.yaml"];
1962
+ const patterns = [...alwaysProtected];
1963
+ if (phaseName === "build") {
1964
+ patterns.push("01-plan.md");
1965
+ }
1966
+ if (phaseName === "verify") {
1967
+ patterns.push("01-plan.md");
1968
+ }
1969
+ const ifValue = patterns.flatMap((f) => [`Edit(*${f})`, `Write(*${f})`]).join("|");
1970
+ return { if: ifValue };
1971
+ }
1972
+ function buildSessionStartScript(eventsFile) {
1973
+ return `#!/bin/bash
1974
+ set -euo pipefail
1975
+ INPUT=$(cat)
1976
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
1977
+ printf '{"ts":"%s","event":"session_start","session_id":"%s"}\\n' \\
1978
+ "$(date -u +%FT%TZ)" "$SESSION_ID" >> ${quote(eventsFile)}
1979
+ exit 0
1980
+ `;
1981
+ }
1982
+ function buildCompactRestoreScript(eventsFile, contextFile) {
1983
+ return `#!/bin/bash
1984
+ set -euo pipefail
1985
+
1986
+ CONTEXT_FILE=${quote(contextFile)}
1987
+ if [ ! -f "$CONTEXT_FILE" ]; then
1988
+ exit 0
1989
+ fi
1990
+
1991
+ ISSUE_IID=$(jq -r '.issueIid // empty' < "$CONTEXT_FILE")
1992
+ ISSUE_TITLE=$(jq -r '.issueTitle // empty' < "$CONTEXT_FILE")
1993
+ ISSUE_DESC=$(jq -r '.issueDescription // empty' < "$CONTEXT_FILE")
1994
+ PHASE=$(jq -r '.phaseName // empty' < "$CONTEXT_FILE")
1995
+ PLAN_DIR=$(jq -r '.planDir // empty' < "$CONTEXT_FILE")
1996
+ ARTIFACTS=$(jq -r '.expectedArtifacts | join(", ") // empty' < "$CONTEXT_FILE")
1997
+
1998
+ READY=""
1999
+ MISSING=""
2000
+ for f in $(jq -r '.expectedArtifacts[]' < "$CONTEXT_FILE" 2>/dev/null); do
2001
+ FPATH="$PLAN_DIR/$f"
2002
+ if [ -f "$FPATH" ] && [ "$(wc -c < "$FPATH")" -ge 50 ]; then
2003
+ READY="$READY $f"
2004
+ else
2005
+ MISSING="$MISSING $f"
2006
+ fi
2007
+ done
2008
+ READY=$(echo "$READY" | xargs)
2009
+ MISSING=$(echo "$MISSING" | xargs)
2010
+
2011
+ printf '{"ts":"%s","event":"compact_restore"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
2012
+
2013
+ cat <<CONTEXT
2014
+ [\u4E0A\u4E0B\u6587\u6062\u590D \u2014 compaction \u540E\u81EA\u52A8\u6CE8\u5165]
2015
+ Issue #$ISSUE_IID: $ISSUE_TITLE
2016
+ \u5F53\u524D\u9636\u6BB5: $PHASE
2017
+ \u9884\u671F\u4EA7\u7269: $ARTIFACTS
2018
+ \u5DF2\u5C31\u7EEA: \${READY:-\u65E0}
2019
+ \u672A\u5B8C\u6210: \${MISSING:-\u65E0}
2020
+
2021
+ \u9700\u6C42\u63CF\u8FF0:
2022
+ $ISSUE_DESC
2023
+ CONTEXT
2024
+ exit 0
2025
+ `;
2026
+ }
2027
+ function buildPostToolUseScript(eventsFile, manifestFile, expected) {
2028
+ return `#!/bin/bash
2029
+ set -euo pipefail
2030
+ INPUT=$(cat)
2031
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
2032
+ [ -z "$FILE_PATH" ] && exit 0
2033
+
2034
+ EXPECTED=${quote(expected)}
2035
+ BASENAME=$(basename "$FILE_PATH")
2036
+
2037
+ if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
2038
+ BYTES=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
2039
+ printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":%s}\\n' \\
2040
+ "$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
2041
+ fi
2042
+
2043
+ printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":0}\\n' \\
2044
+ "$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" >> ${quote(eventsFile)}
2045
+ exit 0
2046
+ `;
2047
+ }
2048
+ function buildPostArtifactScript(manifestFile, expected) {
2049
+ return `#!/bin/bash
2050
+ set -euo pipefail
2051
+ INPUT=$(cat)
2052
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
2053
+ [ -z "$FILE_PATH" ] && exit 0
2054
+
2055
+ EXPECTED=${quote(expected)}
2056
+ BASENAME=$(basename "$FILE_PATH")
2057
+
2058
+ if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
2059
+ BYTES=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
2060
+ printf '{"ts":"%s","event":"write","file":"%s","path":"%s","bytes":%s}\\n' \\
2061
+ "$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
2062
+ fi
2063
+ exit 0
2064
+ `;
2065
+ }
2066
+ function buildExitPlanModeScript(eventsFile) {
2067
+ return `#!/bin/bash
2068
+ set -euo pipefail
2069
+ INPUT=$(cat)
2070
+
2071
+ printf '{"ts":"%s","event":"exit_plan_mode"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
2072
+
2073
+ echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
2074
+ exit 0
2075
+ `;
2076
+ }
2077
+ function buildPermissionScript(eventsFile) {
2078
+ return `#!/bin/bash
2079
+ set -euo pipefail
2080
+ INPUT=$(cat)
2081
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
2082
+ printf '{"ts":"%s","event":"permission_request","tool":"%s"}\\n' \\
2083
+ "$(date -u +%FT%TZ)" "$TOOL" >> ${quote(eventsFile)}
2084
+ echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
2085
+ exit 0
2086
+ `;
2087
+ }
2088
+ function buildProtectFilesScript(eventsFile, phaseName, planDir) {
2089
+ return `#!/bin/bash
2090
+ set -euo pipefail
2091
+ INPUT=$(cat)
2092
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
2093
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
2094
+ [ -z "$FILE_PATH" ] && exit 0
2095
+
2096
+ BASENAME=$(basename "$FILE_PATH")
2097
+ PHASE=${quote(phaseName ?? "")}
2098
+
2099
+ blocked_reason() {
2100
+ printf '{"ts":"%s","event":"protect_blocked","tool":"%s","file":"%s"}\\n' \\
2101
+ "$(date -u +%FT%TZ)" "$TOOL" "$BASENAME" >> ${quote(eventsFile)}
2102
+ echo "$1" >&2
2103
+ exit 2
2104
+ }
2105
+
2106
+ case "$BASENAME" in
2107
+ .env|.env.*)
2108
+ blocked_reason "\u7981\u6B62\u4FEE\u6539\u73AF\u5883\u914D\u7F6E\u6587\u4EF6 $BASENAME\uFF0C\u8BF7\u901A\u8FC7 .env.example \u6216\u6587\u6863\u8BF4\u660E\u914D\u7F6E\u53D8\u66F4\u3002"
2109
+ ;;
2110
+ package-lock.json|pnpm-lock.yaml)
2111
+ blocked_reason "\u7981\u6B62\u76F4\u63A5\u4FEE\u6539\u9501\u6587\u4EF6 $BASENAME\uFF0C\u8BF7\u901A\u8FC7 npm install / pnpm install \u66F4\u65B0\u4F9D\u8D56\u3002"
2112
+ ;;
2113
+ esac
2114
+
2115
+ if [ "$PHASE" = "build" ] || [ "$PHASE" = "verify" ]; then
2116
+ case "$BASENAME" in
2117
+ 01-plan.md)
2118
+ blocked_reason "\u5728 $PHASE \u9636\u6BB5\u7981\u6B62\u4FEE\u6539\u89C4\u5212\u6587\u6863 01-plan.md\uFF0C\u8BE5\u6587\u4EF6\u5728 plan \u9636\u6BB5\u5DF2\u786E\u5B9A\u3002"
2119
+ ;;
2120
+ esac
2121
+ fi
2122
+
2123
+ exit 0
2124
+ `;
2125
+ }
2126
+ function buildStopScript(eventsFile, planDir, phaseExpected) {
2127
+ return `#!/bin/bash
2128
+ set -euo pipefail
2129
+ INPUT=$(cat)
2130
+ STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
2131
+
2132
+ PLAN_DIR=${quote(planDir)}
2133
+ MIN_BYTES=50
2134
+ PHASE_EXPECTED=${quote(phaseExpected)}
2135
+
2136
+ MISSING=""
2137
+ READY=""
2138
+ for f in $(echo "$PHASE_EXPECTED" | tr ',' ' '); do
2139
+ [ -z "$f" ] && continue
2140
+ FPATH="$PLAN_DIR/$f"
2141
+ if [ -f "$FPATH" ] && [ "$(wc -c < "$FPATH")" -ge "$MIN_BYTES" ]; then
2142
+ BYTES=$(wc -c < "$FPATH")
2143
+ READY="$READY $f(\${BYTES} bytes)"
2144
+ else
2145
+ MISSING="$MISSING $f"
2146
+ fi
2147
+ done
2148
+
2149
+ MISSING=$(echo "$MISSING" | xargs)
2150
+ READY=$(echo "$READY" | xargs)
2151
+
2152
+ if [ -n "$MISSING" ] && [ "$STOP_ACTIVE" != "true" ]; then
2153
+ printf '{"ts":"%s","event":"stop","blocked":true,"missing":"%s"}\\n' \\
2154
+ "$(date -u +%FT%TZ)" "$MISSING" >> ${quote(eventsFile)}
2155
+
2156
+ REASON="\u4EA7\u7269\u672A\u5C31\u7EEA: $MISSING\u3002\u8BF7\u5199\u5165 $PLAN_DIR/ \u4E0B\u7684\u5BF9\u5E94\u6587\u4EF6\u3002\u5DF2\u5C31\u7EEA: \${READY:-\u65E0}"
2157
+
2158
+ printf '{"decision":"block","reason":"%s"}' "$REASON"
2159
+ exit 0
2160
+ fi
2161
+
2162
+ printf '{"ts":"%s","event":"stop","blocked":false,"missing":"%s"}\\n' \\
2163
+ "$(date -u +%FT%TZ)" "\${MISSING:-none}" >> ${quote(eventsFile)}
2164
+ exit 0
2165
+ `;
2166
+ }
2167
+ function quote(s) {
2168
+ return `"${s.replace(/"/g, '\\"')}"`;
2169
+ }
2170
+ function readJsonl(filePath) {
2171
+ if (!fs3.existsSync(filePath)) return [];
2172
+ try {
2173
+ const content = fs3.readFileSync(filePath, "utf-8").trim();
2174
+ if (!content) return [];
2175
+ return content.split("\n").reduce((acc, line) => {
2176
+ const trimmed = line.trim();
2177
+ if (!trimmed) return acc;
2178
+ try {
2179
+ acc.push(JSON.parse(trimmed));
2180
+ } catch {
2181
+ logger7.debug("Skipping malformed JSONL line", { line: trimmed });
2182
+ }
2183
+ return acc;
2184
+ }, []);
2185
+ } catch {
2186
+ return [];
2187
+ }
2188
+ }
2189
+
2190
+ // src/hooks/HookEventWatcher.ts
2191
+ import fs4 from "fs";
2192
+ var logger8 = logger.child("HookEventWatcher");
2193
+
1746
2194
  // src/phases/BasePhase.ts
1747
2195
  var BasePhase = class _BasePhase {
1748
2196
  static MIN_ARTIFACT_BYTES = 50;
@@ -1887,7 +2335,7 @@ ${t("basePhase.rulesSection", { rules })}`;
1887
2335
  const resultFiles = this.getResultFiles(ctx);
1888
2336
  const snapshotFilenames = resultFiles.map((f) => f.filename);
1889
2337
  const artifactCheck = snapshotFilenames.length > 0 ? () => snapshotFilenames.every((fn) => this.plan.isArtifactReady(fn, _BasePhase.MIN_ARTIFACT_BYTES)) : void 0;
1890
- const artifactPaths = snapshotFilenames.length > 0 ? snapshotFilenames.map((fn) => path4.join(this.plan.planDir, fn)) : void 0;
2338
+ const artifactPaths = snapshotFilenames.length > 0 ? snapshotFilenames.map((fn) => path5.join(this.plan.planDir, fn)) : void 0;
1891
2339
  let capturedSessionId;
1892
2340
  const result = await this.aiRunner.run({
1893
2341
  prompt,
@@ -1984,14 +2432,14 @@ ${t("basePhase.rulesSection", { rules })}`;
1984
2432
  const context = `${ctx.demand.title} ${ctx.demand.description} ${ctx.demand.supplement ? JSON.stringify(ctx.demand.supplement) : ""}`;
1985
2433
  if (ctx.workspace && ctx.workspace.repos.length > 1) {
1986
2434
  for (const repo of ctx.workspace.repos) {
1987
- const rulesDir = path4.join(repo.gitRootDir, ".cursor", "rules");
2435
+ const rulesDir = path5.join(repo.gitRootDir, ".cursor", "rules");
1988
2436
  try {
1989
2437
  await resolver.loadRules(rulesDir);
1990
2438
  } catch {
1991
2439
  }
1992
2440
  }
1993
2441
  } else {
1994
- const rulesDir = path4.join(this.plan.baseDir, ".cursor", "rules");
2442
+ const rulesDir = path5.join(this.plan.baseDir, ".cursor", "rules");
1995
2443
  await resolver.loadRules(rulesDir);
1996
2444
  }
1997
2445
  const matched = resolver.matchRules(context);
@@ -2024,6 +2472,24 @@ ${t("basePhase.rulesSection", { rules })}`;
2024
2472
  this.logger.error(msg, { phase: this.phaseName, displayId: _displayId });
2025
2473
  throw new AIExecutionError(this.phaseName, msg, { output: "", exitCode: 0 });
2026
2474
  }
2475
+ this.logHookManifest();
2476
+ }
2477
+ /** 读取 hook manifest 并记录产物写入历史(P1 增强层,不影响主流程) */
2478
+ logHookManifest() {
2479
+ try {
2480
+ const injector = new HookInjector();
2481
+ const entries = injector.readManifest(this.plan.baseDir);
2482
+ if (entries.length === 0) return;
2483
+ const artifactWrites = entries.filter((e) => "file" in e && e.event === "write");
2484
+ if (artifactWrites.length > 0) {
2485
+ this.logger.info("Hook manifest: artifact writes recorded", {
2486
+ phase: this.phaseName,
2487
+ writes: artifactWrites.length,
2488
+ files: artifactWrites.map((e) => "file" in e ? e.file : "unknown")
2489
+ });
2490
+ }
2491
+ } catch {
2492
+ }
2027
2493
  }
2028
2494
  };
2029
2495
 
@@ -2247,20 +2713,20 @@ var BuildPhase = class extends BasePhase {
2247
2713
  };
2248
2714
 
2249
2715
  // src/release/ReleaseDetectCache.ts
2250
- import fs3 from "fs";
2251
- import path5 from "path";
2716
+ import fs5 from "fs";
2717
+ import path6 from "path";
2252
2718
  import { createHash } from "crypto";
2253
- var logger7 = logger.child("ReleaseDetectCache");
2719
+ var logger9 = logger.child("ReleaseDetectCache");
2254
2720
  function hashProjectPath(projectPath) {
2255
2721
  return createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
2256
2722
  }
2257
2723
  var ReleaseDetectCache = class {
2258
2724
  cacheDir;
2259
2725
  constructor(dataDir) {
2260
- this.cacheDir = path5.join(dataDir, "release-detect");
2726
+ this.cacheDir = path6.join(dataDir, "release-detect");
2261
2727
  }
2262
2728
  filePath(projectPath) {
2263
- return path5.join(this.cacheDir, `${hashProjectPath(projectPath)}.json`);
2729
+ return path6.join(this.cacheDir, `${hashProjectPath(projectPath)}.json`);
2264
2730
  }
2265
2731
  /**
2266
2732
  * 读取缓存。返回 null 如果不存在、已过期或校验失败。
@@ -2268,21 +2734,21 @@ var ReleaseDetectCache = class {
2268
2734
  get(projectPath, ttlMs) {
2269
2735
  const fp = this.filePath(projectPath);
2270
2736
  try {
2271
- if (!fs3.existsSync(fp)) return null;
2272
- const raw = fs3.readFileSync(fp, "utf-8");
2737
+ if (!fs5.existsSync(fp)) return null;
2738
+ const raw = fs5.readFileSync(fp, "utf-8");
2273
2739
  const data = JSON.parse(raw);
2274
2740
  if (data.projectPath !== projectPath) {
2275
- logger7.warn("Cache projectPath mismatch, ignoring", { expected: projectPath, got: data.projectPath });
2741
+ logger9.warn("Cache projectPath mismatch, ignoring", { expected: projectPath, got: data.projectPath });
2276
2742
  return null;
2277
2743
  }
2278
2744
  const age = Date.now() - new Date(data.detectedAt).getTime();
2279
2745
  if (age > ttlMs) {
2280
- logger7.debug("Cache expired", { projectPath, ageMs: age, ttlMs });
2746
+ logger9.debug("Cache expired", { projectPath, ageMs: age, ttlMs });
2281
2747
  return null;
2282
2748
  }
2283
2749
  return data;
2284
2750
  } catch (err) {
2285
- logger7.warn("Failed to read release detect cache", { path: fp, error: err.message });
2751
+ logger9.warn("Failed to read release detect cache", { path: fp, error: err.message });
2286
2752
  return null;
2287
2753
  }
2288
2754
  }
@@ -2292,13 +2758,13 @@ var ReleaseDetectCache = class {
2292
2758
  set(result) {
2293
2759
  const fp = this.filePath(result.projectPath);
2294
2760
  try {
2295
- if (!fs3.existsSync(this.cacheDir)) {
2296
- fs3.mkdirSync(this.cacheDir, { recursive: true });
2761
+ if (!fs5.existsSync(this.cacheDir)) {
2762
+ fs5.mkdirSync(this.cacheDir, { recursive: true });
2297
2763
  }
2298
- fs3.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
2299
- logger7.debug("Release detect cache written", { projectPath: result.projectPath, path: fp });
2764
+ fs5.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
2765
+ logger9.debug("Release detect cache written", { projectPath: result.projectPath, path: fp });
2300
2766
  } catch (err) {
2301
- logger7.warn("Failed to write release detect cache", { path: fp, error: err.message });
2767
+ logger9.warn("Failed to write release detect cache", { path: fp, error: err.message });
2302
2768
  }
2303
2769
  }
2304
2770
  /**
@@ -2307,14 +2773,14 @@ var ReleaseDetectCache = class {
2307
2773
  invalidate(projectPath) {
2308
2774
  const fp = this.filePath(projectPath);
2309
2775
  try {
2310
- if (fs3.existsSync(fp)) {
2311
- fs3.unlinkSync(fp);
2312
- logger7.info("Release detect cache invalidated", { projectPath });
2776
+ if (fs5.existsSync(fp)) {
2777
+ fs5.unlinkSync(fp);
2778
+ logger9.info("Release detect cache invalidated", { projectPath });
2313
2779
  return true;
2314
2780
  }
2315
2781
  return false;
2316
2782
  } catch (err) {
2317
- logger7.warn("Failed to invalidate release detect cache", { path: fp, error: err.message });
2783
+ logger9.warn("Failed to invalidate release detect cache", { path: fp, error: err.message });
2318
2784
  return false;
2319
2785
  }
2320
2786
  }
@@ -2765,9 +3231,9 @@ function createLifecycleManager(def) {
2765
3231
 
2766
3232
  // src/workspace/WorkspaceConfig.ts
2767
3233
  import { z } from "zod";
2768
- import fs4 from "fs";
2769
- import path6 from "path";
2770
- var logger8 = logger.child("WorkspaceConfig");
3234
+ import fs6 from "fs";
3235
+ import path7 from "path";
3236
+ var logger10 = logger.child("WorkspaceConfig");
2771
3237
  var repoConfigSchema = z.object({
2772
3238
  name: z.string().min(1, "Repo name is required"),
2773
3239
  projectPath: z.string().min(1, "Gongfeng project path is required"),
@@ -2783,29 +3249,29 @@ var workspaceConfigSchema = z.object({
2783
3249
  });
2784
3250
  function loadWorkspaceConfig(configPath) {
2785
3251
  if (!configPath) return null;
2786
- if (!fs4.existsSync(configPath)) {
2787
- logger8.warn("Workspace config file not found, falling back to single-repo mode", {
3252
+ if (!fs6.existsSync(configPath)) {
3253
+ logger10.warn("Workspace config file not found, falling back to single-repo mode", {
2788
3254
  path: configPath
2789
3255
  });
2790
3256
  return null;
2791
3257
  }
2792
3258
  try {
2793
- const raw = fs4.readFileSync(configPath, "utf-8");
3259
+ const raw = fs6.readFileSync(configPath, "utf-8");
2794
3260
  const json = JSON.parse(raw);
2795
3261
  const result = workspaceConfigSchema.safeParse(json);
2796
3262
  if (!result.success) {
2797
3263
  const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
2798
- logger8.error(`Workspace config validation failed:
3264
+ logger10.error(`Workspace config validation failed:
2799
3265
  ${issues}`);
2800
3266
  return null;
2801
3267
  }
2802
- logger8.info("Workspace config loaded", {
3268
+ logger10.info("Workspace config loaded", {
2803
3269
  primary: result.data.primary.name,
2804
3270
  associates: result.data.associates.map((a) => a.name)
2805
3271
  });
2806
3272
  return result.data;
2807
3273
  } catch (err) {
2808
- logger8.error("Failed to parse workspace config", {
3274
+ logger10.error("Failed to parse workspace config", {
2809
3275
  path: configPath,
2810
3276
  error: err.message
2811
3277
  });
@@ -2831,14 +3297,14 @@ function isMultiRepo(ws) {
2831
3297
  }
2832
3298
  function persistWorkspaceConfig(ws, filePath) {
2833
3299
  try {
2834
- const dir = path6.dirname(filePath);
2835
- if (!fs4.existsSync(dir)) {
2836
- fs4.mkdirSync(dir, { recursive: true });
3300
+ const dir = path7.dirname(filePath);
3301
+ if (!fs6.existsSync(dir)) {
3302
+ fs6.mkdirSync(dir, { recursive: true });
2837
3303
  }
2838
- fs4.writeFileSync(filePath, JSON.stringify(ws, null, 2) + "\n", "utf-8");
2839
- logger8.info("Workspace config auto-generated from .env", { path: filePath });
3304
+ fs6.writeFileSync(filePath, JSON.stringify(ws, null, 2) + "\n", "utf-8");
3305
+ logger10.info("Workspace config auto-generated from .env", { path: filePath });
2840
3306
  } catch (err) {
2841
- logger8.warn("Failed to persist workspace config", {
3307
+ logger10.warn("Failed to persist workspace config", {
2842
3308
  path: filePath,
2843
3309
  error: err.message
2844
3310
  });
@@ -2846,12 +3312,12 @@ function persistWorkspaceConfig(ws, filePath) {
2846
3312
  }
2847
3313
 
2848
3314
  // src/workspace/WorkspaceManager.ts
2849
- import path7 from "path";
2850
- import fs5 from "fs/promises";
3315
+ import path8 from "path";
3316
+ import fs7 from "fs/promises";
2851
3317
  import { execFile } from "child_process";
2852
3318
  import { promisify } from "util";
2853
3319
  var execFileAsync = promisify(execFile);
2854
- var logger9 = logger.child("WorkspaceManager");
3320
+ var logger11 = logger.child("WorkspaceManager");
2855
3321
  var WorkspaceManager = class {
2856
3322
  wsConfig;
2857
3323
  worktreeBaseDir;
@@ -2880,7 +3346,7 @@ var WorkspaceManager = class {
2880
3346
  */
2881
3347
  async prepareWorkspace(issueIid, branchName, globalBaseBranch, globalBranchPrefix) {
2882
3348
  const wsRoot = this.getWorkspaceRoot(issueIid);
2883
- await fs5.mkdir(wsRoot, { recursive: true });
3349
+ await fs7.mkdir(wsRoot, { recursive: true });
2884
3350
  const primaryCtx = await this.preparePrimaryRepo(
2885
3351
  issueIid,
2886
3352
  branchName,
@@ -2899,7 +3365,7 @@ var WorkspaceManager = class {
2899
3365
  );
2900
3366
  associateCtxs.push(ctx);
2901
3367
  }
2902
- logger9.info("Workspace prepared", {
3368
+ logger11.info("Workspace prepared", {
2903
3369
  issueIid,
2904
3370
  wsRoot,
2905
3371
  repos: [primaryCtx.name, ...associateCtxs.map((a) => a.name)]
@@ -2924,7 +3390,7 @@ var WorkspaceManager = class {
2924
3390
  await git.commit(message);
2925
3391
  await git.push(wsCtx.branchName);
2926
3392
  committed.push(repo.name);
2927
- logger9.info("Committed and pushed changes", {
3393
+ logger11.info("Committed and pushed changes", {
2928
3394
  repo: repo.name,
2929
3395
  branch: wsCtx.branchName
2930
3396
  });
@@ -2938,19 +3404,19 @@ var WorkspaceManager = class {
2938
3404
  async cleanupWorkspace(wsCtx) {
2939
3405
  try {
2940
3406
  await this.mainGit.worktreeRemove(wsCtx.primary.gitRootDir, true);
2941
- logger9.info("Primary worktree removed", { dir: wsCtx.primary.gitRootDir });
3407
+ logger11.info("Primary worktree removed", { dir: wsCtx.primary.gitRootDir });
2942
3408
  } catch (err) {
2943
- logger9.warn("Failed to remove primary worktree", {
3409
+ logger11.warn("Failed to remove primary worktree", {
2944
3410
  dir: wsCtx.primary.gitRootDir,
2945
3411
  error: err.message
2946
3412
  });
2947
3413
  }
2948
3414
  for (const assoc of wsCtx.associates) {
2949
3415
  try {
2950
- await fs5.rm(assoc.gitRootDir, { recursive: true, force: true });
2951
- logger9.info("Associate repo dir removed", { name: assoc.name, dir: assoc.gitRootDir });
3416
+ await fs7.rm(assoc.gitRootDir, { recursive: true, force: true });
3417
+ logger11.info("Associate repo dir removed", { name: assoc.name, dir: assoc.gitRootDir });
2952
3418
  } catch (err) {
2953
- logger9.warn("Failed to remove associate repo dir", {
3419
+ logger11.warn("Failed to remove associate repo dir", {
2954
3420
  name: assoc.name,
2955
3421
  dir: assoc.gitRootDir,
2956
3422
  error: err.message
@@ -2958,9 +3424,9 @@ var WorkspaceManager = class {
2958
3424
  }
2959
3425
  }
2960
3426
  try {
2961
- const entries = await fs5.readdir(wsCtx.workspaceRoot);
3427
+ const entries = await fs7.readdir(wsCtx.workspaceRoot);
2962
3428
  if (entries.length === 0) {
2963
- await fs5.rmdir(wsCtx.workspaceRoot);
3429
+ await fs7.rmdir(wsCtx.workspaceRoot);
2964
3430
  }
2965
3431
  } catch {
2966
3432
  }
@@ -2972,13 +3438,13 @@ var WorkspaceManager = class {
2972
3438
  const wsRoot = this.getWorkspaceRoot(issueIid);
2973
3439
  const primary = this.wsConfig.primary;
2974
3440
  const defaultPrefix = globalBranchPrefix ?? primary.branchPrefix ?? "feat/issue";
2975
- const primaryDir = path7.join(wsRoot, primary.name);
3441
+ const primaryDir = path8.join(wsRoot, primary.name);
2976
3442
  const repos = [{
2977
3443
  name: primary.name,
2978
3444
  projectPath: primary.projectPath,
2979
3445
  role: primary.role ?? "",
2980
3446
  gitRootDir: primaryDir,
2981
- workDir: path7.join(primaryDir, primary.projectSubDir ?? ""),
3447
+ workDir: path8.join(primaryDir, primary.projectSubDir ?? ""),
2982
3448
  baseBranch: primary.baseBranch ?? globalBaseBranch,
2983
3449
  branchPrefix: primary.branchPrefix ?? defaultPrefix,
2984
3450
  isPrimary: true
@@ -2988,8 +3454,8 @@ var WorkspaceManager = class {
2988
3454
  name: assoc.name,
2989
3455
  projectPath: assoc.projectPath,
2990
3456
  role: assoc.role ?? "",
2991
- gitRootDir: path7.join(wsRoot, assoc.name),
2992
- workDir: path7.join(wsRoot, assoc.name, assoc.projectSubDir ?? ""),
3457
+ gitRootDir: path8.join(wsRoot, assoc.name),
3458
+ workDir: path8.join(wsRoot, assoc.name, assoc.projectSubDir ?? ""),
2993
3459
  baseBranch: assoc.baseBranch ?? globalBaseBranch,
2994
3460
  branchPrefix: assoc.branchPrefix ?? defaultPrefix,
2995
3461
  isPrimary: false
@@ -2998,12 +3464,12 @@ var WorkspaceManager = class {
2998
3464
  return repos;
2999
3465
  }
3000
3466
  getWorkspaceRoot(issueIid) {
3001
- return path7.join(this.worktreeBaseDir, `issue-${issueIid}`);
3467
+ return path8.join(this.worktreeBaseDir, `issue-${issueIid}`);
3002
3468
  }
3003
3469
  // ── Internal helpers ──
3004
3470
  async preparePrimaryRepo(issueIid, branchName, wsRoot, globalBaseBranch) {
3005
3471
  const primary = this.wsConfig.primary;
3006
- const repoDir = path7.join(wsRoot, primary.name);
3472
+ const repoDir = path8.join(wsRoot, primary.name);
3007
3473
  const baseBranch = primary.baseBranch ?? globalBaseBranch;
3008
3474
  await this.ensurePrimaryWorktree(repoDir, branchName, baseBranch);
3009
3475
  return {
@@ -3011,18 +3477,18 @@ var WorkspaceManager = class {
3011
3477
  projectPath: primary.projectPath,
3012
3478
  role: primary.role ?? "",
3013
3479
  gitRootDir: repoDir,
3014
- workDir: path7.join(repoDir, primary.projectSubDir ?? ""),
3480
+ workDir: path8.join(repoDir, primary.projectSubDir ?? ""),
3015
3481
  baseBranch,
3016
3482
  branchPrefix: primary.branchPrefix ?? "feat/issue",
3017
3483
  isPrimary: true
3018
3484
  };
3019
3485
  }
3020
3486
  async ensurePrimaryWorktree(repoDir, branchName, baseBranch) {
3021
- const wsRoot = path7.dirname(repoDir);
3487
+ const wsRoot = path8.dirname(repoDir);
3022
3488
  if (wsRoot !== repoDir) {
3023
3489
  try {
3024
- await fs5.access(path7.join(wsRoot, ".git"));
3025
- logger9.info("Migrating legacy worktree to primary subdir", { from: wsRoot, to: repoDir });
3490
+ await fs7.access(path8.join(wsRoot, ".git"));
3491
+ logger11.info("Migrating legacy worktree to primary subdir", { from: wsRoot, to: repoDir });
3026
3492
  await this.mainGit.worktreeRemove(wsRoot, true);
3027
3493
  await this.mainGit.worktreePrune();
3028
3494
  await this.cleanStaleDir(wsRoot);
@@ -3032,11 +3498,11 @@ var WorkspaceManager = class {
3032
3498
  const worktrees = await this.mainGit.worktreeList();
3033
3499
  if (worktrees.includes(repoDir)) {
3034
3500
  try {
3035
- await fs5.access(path7.join(repoDir, ".git"));
3036
- logger9.info("Reusing existing primary worktree", { dir: repoDir });
3501
+ await fs7.access(path8.join(repoDir, ".git"));
3502
+ logger11.info("Reusing existing primary worktree", { dir: repoDir });
3037
3503
  return;
3038
3504
  } catch {
3039
- logger9.warn("Primary worktree registered but .git missing, recreating", { dir: repoDir });
3505
+ logger11.warn("Primary worktree registered but .git missing, recreating", { dir: repoDir });
3040
3506
  await this.mainGit.worktreeRemove(repoDir, true);
3041
3507
  await this.mainGit.worktreePrune();
3042
3508
  }
@@ -3055,19 +3521,19 @@ var WorkspaceManager = class {
3055
3521
  await this.mainGit.worktreeAdd(repoDir, branchName, `origin/${baseBranch}`);
3056
3522
  }
3057
3523
  async prepareAssociateRepo(assoc, _issueIid, branchName, wsRoot, globalBaseBranch, globalBranchPrefix) {
3058
- const repoDir = path7.join(wsRoot, assoc.name);
3524
+ const repoDir = path8.join(wsRoot, assoc.name);
3059
3525
  const baseBranch = assoc.baseBranch ?? globalBaseBranch;
3060
3526
  const cloneUrl = `${this.gongfengApiUrl}/${assoc.projectPath}.git`;
3061
- const gitDirExists = await this.dirExists(path7.join(repoDir, ".git"));
3527
+ const gitDirExists = await this.dirExists(path8.join(repoDir, ".git"));
3062
3528
  if (!gitDirExists) {
3063
3529
  await this.cleanStaleDir(repoDir);
3064
- logger9.info("Cloning associate repo", { name: assoc.name, url: cloneUrl });
3530
+ logger11.info("Cloning associate repo", { name: assoc.name, url: cloneUrl });
3065
3531
  await execFileAsync("git", ["clone", "--depth", "50", cloneUrl, repoDir], {
3066
3532
  timeout: 3e5,
3067
3533
  maxBuffer: 10 * 1024 * 1024
3068
3534
  });
3069
3535
  } else {
3070
- logger9.info("Reusing existing associate clone", { name: assoc.name, dir: repoDir });
3536
+ logger11.info("Reusing existing associate clone", { name: assoc.name, dir: repoDir });
3071
3537
  }
3072
3538
  const assocGit = new GitOperations(repoDir);
3073
3539
  await assocGit.fetch();
@@ -3090,7 +3556,7 @@ var WorkspaceManager = class {
3090
3556
  projectPath: assoc.projectPath,
3091
3557
  role: assoc.role ?? "",
3092
3558
  gitRootDir: repoDir,
3093
- workDir: path7.join(repoDir, assoc.projectSubDir ?? ""),
3559
+ workDir: path8.join(repoDir, assoc.projectSubDir ?? ""),
3094
3560
  baseBranch,
3095
3561
  branchPrefix: assoc.branchPrefix ?? globalBranchPrefix,
3096
3562
  isPrimary: false
@@ -3098,13 +3564,13 @@ var WorkspaceManager = class {
3098
3564
  }
3099
3565
  async cleanStaleDir(dir) {
3100
3566
  if (await this.dirExists(dir)) {
3101
- logger9.warn("Removing stale directory", { dir });
3102
- await fs5.rm(dir, { recursive: true, force: true });
3567
+ logger11.warn("Removing stale directory", { dir });
3568
+ await fs7.rm(dir, { recursive: true, force: true });
3103
3569
  }
3104
3570
  }
3105
3571
  async dirExists(dir) {
3106
3572
  try {
3107
- await fs5.access(dir);
3573
+ await fs7.access(dir);
3108
3574
  return true;
3109
3575
  } catch {
3110
3576
  return false;
@@ -3113,8 +3579,8 @@ var WorkspaceManager = class {
3113
3579
  };
3114
3580
 
3115
3581
  // src/orchestrator/PipelineOrchestrator.ts
3116
- import path11 from "path";
3117
- import fs9 from "fs/promises";
3582
+ import path12 from "path";
3583
+ import fs11 from "fs/promises";
3118
3584
  import fsSync from "fs";
3119
3585
  import { execFile as execFile2 } from "child_process";
3120
3586
  import { promisify as promisify2 } from "util";
@@ -3147,8 +3613,8 @@ function mapSupplement(s) {
3147
3613
  }
3148
3614
 
3149
3615
  // src/utils/MergeRequestHelper.ts
3150
- import fs6 from "fs";
3151
- import path8 from "path";
3616
+ import fs8 from "fs";
3617
+ import path9 from "path";
3152
3618
  var TAPD_PATTERNS = [
3153
3619
  /--story=(\d+)/i,
3154
3620
  /--bug=(\d+)/i,
@@ -3192,9 +3658,9 @@ function generateMRDescription(options) {
3192
3658
  ];
3193
3659
  const planSections = [];
3194
3660
  for (const { filename, label } of summaryFiles) {
3195
- const filePath = path8.join(planDir, ".claude-plan", `issue-${issueIid}`, filename);
3196
- if (fs6.existsSync(filePath)) {
3197
- const content = fs6.readFileSync(filePath, "utf-8");
3661
+ const filePath = path9.join(planDir, ".claude-plan", `issue-${issueIid}`, filename);
3662
+ if (fs8.existsSync(filePath)) {
3663
+ const content = fs8.readFileSync(filePath, "utf-8");
3198
3664
  const summary = extractSummary(content);
3199
3665
  if (summary) {
3200
3666
  planSections.push(`### ${label}
@@ -3218,7 +3684,7 @@ function extractSummary(content, maxLines = 20) {
3218
3684
 
3219
3685
  // src/deploy/PortAllocator.ts
3220
3686
  import net from "net";
3221
- var logger10 = logger.child("PortAllocator");
3687
+ var logger12 = logger.child("PortAllocator");
3222
3688
  var DEFAULT_OPTIONS = {
3223
3689
  backendPortBase: 4e3,
3224
3690
  frontendPortBase: 9e3,
@@ -3243,7 +3709,7 @@ var PortAllocator = class {
3243
3709
  async allocate(issueIid) {
3244
3710
  const existing = this.allocated.get(issueIid);
3245
3711
  if (existing) {
3246
- logger10.info("Returning already allocated ports", { issueIid, ports: existing });
3712
+ logger12.info("Returning already allocated ports", { issueIid, ports: existing });
3247
3713
  return existing;
3248
3714
  }
3249
3715
  const usedBackend = new Set([...this.allocated.values()].map((p) => p.backendPort));
@@ -3261,10 +3727,10 @@ var PortAllocator = class {
3261
3727
  if (beOk && feOk) {
3262
3728
  const pair = { backendPort, frontendPort };
3263
3729
  this.allocated.set(issueIid, pair);
3264
- logger10.info("Ports allocated", { issueIid, ...pair });
3730
+ logger12.info("Ports allocated", { issueIid, ...pair });
3265
3731
  return pair;
3266
3732
  }
3267
- logger10.debug("Port pair unavailable, trying next", {
3733
+ logger12.debug("Port pair unavailable, trying next", {
3268
3734
  backendPort,
3269
3735
  frontendPort,
3270
3736
  beOk,
@@ -3279,7 +3745,7 @@ var PortAllocator = class {
3279
3745
  const pair = this.allocated.get(issueIid);
3280
3746
  if (pair) {
3281
3747
  this.allocated.delete(issueIid);
3282
- logger10.info("Ports released", { issueIid, ...pair });
3748
+ logger12.info("Ports released", { issueIid, ...pair });
3283
3749
  }
3284
3750
  }
3285
3751
  getPortsForIssue(issueIid) {
@@ -3290,15 +3756,15 @@ var PortAllocator = class {
3290
3756
  }
3291
3757
  restore(issueIid, ports) {
3292
3758
  this.allocated.set(issueIid, ports);
3293
- logger10.info("Ports restored from persistence", { issueIid, ...ports });
3759
+ logger12.info("Ports restored from persistence", { issueIid, ...ports });
3294
3760
  }
3295
3761
  };
3296
3762
 
3297
3763
  // src/deploy/DevServerManager.ts
3298
3764
  import { spawn } from "child_process";
3299
- import fs7 from "fs";
3300
- import path9 from "path";
3301
- var logger11 = logger.child("DevServerManager");
3765
+ import fs9 from "fs";
3766
+ import path10 from "path";
3767
+ var logger13 = logger.child("DevServerManager");
3302
3768
  var DEFAULT_OPTIONS2 = {};
3303
3769
  var DevServerManager = class {
3304
3770
  servers = /* @__PURE__ */ new Map();
@@ -3306,25 +3772,25 @@ var DevServerManager = class {
3306
3772
  logDir;
3307
3773
  constructor(options) {
3308
3774
  this.options = { ...DEFAULT_OPTIONS2, ...options };
3309
- this.logDir = path9.join(resolveDataDir(), "preview-logs");
3310
- if (!fs7.existsSync(this.logDir)) {
3311
- fs7.mkdirSync(this.logDir, { recursive: true });
3775
+ this.logDir = path10.join(resolveDataDir(), "preview-logs");
3776
+ if (!fs9.existsSync(this.logDir)) {
3777
+ fs9.mkdirSync(this.logDir, { recursive: true });
3312
3778
  }
3313
3779
  }
3314
3780
  getLogPath(issueIid, type) {
3315
- const filePath = path9.join(this.logDir, `${issueIid}-${type}.log`);
3316
- return fs7.existsSync(filePath) ? filePath : null;
3781
+ const filePath = path10.join(this.logDir, `${issueIid}-${type}.log`);
3782
+ return fs9.existsSync(filePath) ? filePath : null;
3317
3783
  }
3318
3784
  async startServers(wtCtx, ports) {
3319
3785
  if (this.servers.has(wtCtx.issueIid)) {
3320
- logger11.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
3786
+ logger13.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
3321
3787
  return;
3322
3788
  }
3323
- logger11.info("Starting dev servers", { issueIid: wtCtx.issueIid, ...ports });
3324
- const backendLogPath = path9.join(this.logDir, `${wtCtx.issueIid}-backend.log`);
3325
- const frontendLogPath = path9.join(this.logDir, `${wtCtx.issueIid}-frontend.log`);
3326
- const backendLog = fs7.createWriteStream(backendLogPath, { flags: "a" });
3327
- const frontendLog = fs7.createWriteStream(frontendLogPath, { flags: "a" });
3789
+ logger13.info("Starting dev servers", { issueIid: wtCtx.issueIid, ...ports });
3790
+ const backendLogPath = path10.join(this.logDir, `${wtCtx.issueIid}-backend.log`);
3791
+ const frontendLogPath = path10.join(this.logDir, `${wtCtx.issueIid}-frontend.log`);
3792
+ const backendLog = fs9.createWriteStream(backendLogPath, { flags: "a" });
3793
+ const frontendLog = fs9.createWriteStream(frontendLogPath, { flags: "a" });
3328
3794
  const tsLine = (stream, data) => `[${(/* @__PURE__ */ new Date()).toISOString()}] [${stream}] ${data.toString().trimEnd()}
3329
3795
  `;
3330
3796
  const backendEnv = {
@@ -3348,9 +3814,9 @@ var DevServerManager = class {
3348
3814
  backendLog.write(tsLine("stderr", data));
3349
3815
  });
3350
3816
  backend.on("exit", (code) => {
3351
- logger11.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
3817
+ logger13.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
3352
3818
  });
3353
- const frontendDir = path9.join(wtCtx.workDir, "frontend");
3819
+ const frontendDir = path10.join(wtCtx.workDir, "frontend");
3354
3820
  const frontendEnv = {
3355
3821
  ...process.env,
3356
3822
  BACKEND_PORT: String(ports.backendPort),
@@ -3372,7 +3838,7 @@ var DevServerManager = class {
3372
3838
  frontendLog.write(tsLine("stderr", data));
3373
3839
  });
3374
3840
  frontend.on("exit", (code) => {
3375
- logger11.info("Frontend process exited", { issueIid: wtCtx.issueIid, code });
3841
+ logger13.info("Frontend process exited", { issueIid: wtCtx.issueIid, code });
3376
3842
  });
3377
3843
  const serverSet = {
3378
3844
  backend,
@@ -3384,14 +3850,14 @@ var DevServerManager = class {
3384
3850
  frontendLog
3385
3851
  };
3386
3852
  this.servers.set(wtCtx.issueIid, serverSet);
3387
- logger11.info("Dev servers spawned, waiting for startup", { issueIid: wtCtx.issueIid, ...ports });
3853
+ logger13.info("Dev servers spawned, waiting for startup", { issueIid: wtCtx.issueIid, ...ports });
3388
3854
  await new Promise((r) => setTimeout(r, 1e4));
3389
- logger11.info("Dev servers startup grace period done", { issueIid: wtCtx.issueIid });
3855
+ logger13.info("Dev servers startup grace period done", { issueIid: wtCtx.issueIid });
3390
3856
  }
3391
3857
  stopServers(issueIid) {
3392
3858
  const set = this.servers.get(issueIid);
3393
3859
  if (!set) return;
3394
- logger11.info("Stopping dev servers", { issueIid, ports: set.ports });
3860
+ logger13.info("Stopping dev servers", { issueIid, ports: set.ports });
3395
3861
  killProcess(set.backend, `backend #${issueIid}`);
3396
3862
  killProcess(set.frontend, `frontend #${issueIid}`);
3397
3863
  set.backendLog.end();
@@ -3428,7 +3894,7 @@ function killProcess(proc, label) {
3428
3894
  }
3429
3895
  setTimeout(() => {
3430
3896
  if (!proc.killed && proc.exitCode === null) {
3431
- logger11.warn(`Force killing ${label}`);
3897
+ logger13.warn(`Force killing ${label}`);
3432
3898
  try {
3433
3899
  process.kill(-pid, "SIGKILL");
3434
3900
  } catch {
@@ -3437,7 +3903,7 @@ function killProcess(proc, label) {
3437
3903
  }
3438
3904
  }, 5e3);
3439
3905
  } catch (err) {
3440
- logger11.warn(`Failed to kill ${label}`, { error: err.message });
3906
+ logger13.warn(`Failed to kill ${label}`, { error: err.message });
3441
3907
  }
3442
3908
  }
3443
3909
 
@@ -3456,13 +3922,13 @@ function isE2eEnabledForIssue(issueIid, tracker, cfg) {
3456
3922
  }
3457
3923
 
3458
3924
  // src/e2e/ScreenshotCollector.ts
3459
- import fs8 from "fs";
3460
- import path10 from "path";
3461
- var logger12 = logger.child("ScreenshotCollector");
3925
+ import fs10 from "fs";
3926
+ import path11 from "path";
3927
+ var logger14 = logger.child("ScreenshotCollector");
3462
3928
  var MAX_SCREENSHOTS = 20;
3463
3929
  function walkDir(dir, files = []) {
3464
- for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
3465
- const full = path10.join(dir, entry.name);
3930
+ for (const entry of fs10.readdirSync(dir, { withFileTypes: true })) {
3931
+ const full = path11.join(dir, entry.name);
3466
3932
  if (entry.isDirectory()) {
3467
3933
  walkDir(full, files);
3468
3934
  } else if (entry.isFile() && entry.name.endsWith(".png")) {
@@ -3472,34 +3938,34 @@ function walkDir(dir, files = []) {
3472
3938
  return files;
3473
3939
  }
3474
3940
  function collectScreenshots(workDir) {
3475
- const testResultsDir = path10.join(workDir, "frontend", "test-results");
3476
- if (!fs8.existsSync(testResultsDir)) {
3477
- logger12.debug("test-results directory not found", { dir: testResultsDir });
3941
+ const testResultsDir = path11.join(workDir, "frontend", "test-results");
3942
+ if (!fs10.existsSync(testResultsDir)) {
3943
+ logger14.debug("test-results directory not found", { dir: testResultsDir });
3478
3944
  return [];
3479
3945
  }
3480
3946
  const pngFiles = walkDir(testResultsDir);
3481
3947
  if (pngFiles.length === 0) {
3482
- logger12.debug("No screenshots found");
3948
+ logger14.debug("No screenshots found");
3483
3949
  return [];
3484
3950
  }
3485
3951
  const screenshots = pngFiles.map((filePath) => {
3486
- const relative = path10.relative(testResultsDir, filePath);
3487
- const testName = relative.split(path10.sep)[0] || path10.basename(filePath, ".png");
3952
+ const relative = path11.relative(testResultsDir, filePath);
3953
+ const testName = relative.split(path11.sep)[0] || path11.basename(filePath, ".png");
3488
3954
  return { filePath, testName };
3489
3955
  });
3490
3956
  if (screenshots.length > MAX_SCREENSHOTS) {
3491
- logger12.warn("Too many screenshots, truncating", {
3957
+ logger14.warn("Too many screenshots, truncating", {
3492
3958
  total: screenshots.length,
3493
3959
  max: MAX_SCREENSHOTS
3494
3960
  });
3495
3961
  return screenshots.slice(0, MAX_SCREENSHOTS);
3496
3962
  }
3497
- logger12.info("Screenshots collected", { count: screenshots.length });
3963
+ logger14.info("Screenshots collected", { count: screenshots.length });
3498
3964
  return screenshots;
3499
3965
  }
3500
3966
 
3501
3967
  // src/e2e/ScreenshotPublisher.ts
3502
- var logger13 = logger.child("ScreenshotPublisher");
3968
+ var logger15 = logger.child("ScreenshotPublisher");
3503
3969
  function buildComment(uploaded, truncated) {
3504
3970
  const lines = [t("screenshot.title"), ""];
3505
3971
  for (const item of uploaded) {
@@ -3518,12 +3984,12 @@ var ScreenshotPublisher = class {
3518
3984
  const { workDir, issueIid, issueId, mrIid } = options;
3519
3985
  const screenshots = collectScreenshots(workDir);
3520
3986
  if (screenshots.length === 0) {
3521
- logger13.info("No E2E screenshots to publish", { issueIid });
3987
+ logger15.info("No E2E screenshots to publish", { issueIid });
3522
3988
  return;
3523
3989
  }
3524
3990
  const uploaded = await this.uploadAll(screenshots);
3525
3991
  if (uploaded.length === 0) {
3526
- logger13.warn("All screenshot uploads failed", { issueIid });
3992
+ logger15.warn("All screenshot uploads failed", { issueIid });
3527
3993
  return;
3528
3994
  }
3529
3995
  const truncated = screenshots.length >= 20;
@@ -3532,7 +3998,7 @@ var ScreenshotPublisher = class {
3532
3998
  if (mrIid) {
3533
3999
  await this.postToMergeRequest(mrIid, comment);
3534
4000
  }
3535
- logger13.info("E2E screenshots published", {
4001
+ logger15.info("E2E screenshots published", {
3536
4002
  issueIid,
3537
4003
  mrIid,
3538
4004
  count: uploaded.length
@@ -3548,7 +4014,7 @@ var ScreenshotPublisher = class {
3548
4014
  markdown: result.markdown
3549
4015
  });
3550
4016
  } catch (err) {
3551
- logger13.warn("Failed to upload screenshot", {
4017
+ logger15.warn("Failed to upload screenshot", {
3552
4018
  filePath: screenshot.filePath,
3553
4019
  error: err.message
3554
4020
  });
@@ -3560,7 +4026,7 @@ var ScreenshotPublisher = class {
3560
4026
  try {
3561
4027
  await this.gongfeng.createIssueNote(issueId, comment);
3562
4028
  } catch (err) {
3563
- logger13.warn("Failed to post screenshots to issue", {
4029
+ logger15.warn("Failed to post screenshots to issue", {
3564
4030
  issueId,
3565
4031
  error: err.message
3566
4032
  });
@@ -3570,7 +4036,7 @@ var ScreenshotPublisher = class {
3570
4036
  try {
3571
4037
  await this.gongfeng.createMergeRequestNote(mrIid, comment);
3572
4038
  } catch (err) {
3573
- logger13.warn("Failed to post screenshots to merge request", {
4039
+ logger15.warn("Failed to post screenshots to merge request", {
3574
4040
  mrIid,
3575
4041
  error: err.message
3576
4042
  });
@@ -3753,7 +4219,7 @@ metrics.registerCounter("iaf_braindump_batches_total", "Total braindump batches"
3753
4219
  metrics.registerCounter("iaf_braindump_tasks_total", "Total braindump tasks");
3754
4220
 
3755
4221
  // src/orchestrator/steps/SetupStep.ts
3756
- var logger14 = logger.child("SetupStep");
4222
+ var logger16 = logger.child("SetupStep");
3757
4223
  async function executeSetup(ctx, deps) {
3758
4224
  const { issue, wtCtx, record, pipelineDef, branchName } = ctx;
3759
4225
  try {
@@ -3762,7 +4228,7 @@ async function executeSetup(ctx, deps) {
3762
4228
  "auto-finish:processing"
3763
4229
  ]);
3764
4230
  } catch (err) {
3765
- logger14.warn("Failed to update issue labels", { error: err.message });
4231
+ logger16.warn("Failed to update issue labels", { error: err.message });
3766
4232
  }
3767
4233
  await deps.mainGitMutex.runExclusive(async () => {
3768
4234
  deps.emitProgress(issue.iid, "fetch", t("orchestrator.fetchProgress"));
@@ -3804,6 +4270,28 @@ async function executeSetup(ctx, deps) {
3804
4270
  if (!record.phaseProgress) {
3805
4271
  deps.tracker.initPhaseProgress(issue.iid, pipelineDef);
3806
4272
  }
4273
+ const allArtifacts = pipelineDef.phases.flatMap((p) => p.artifacts ?? []).map((a) => a.filename);
4274
+ if (allArtifacts.length > 0) {
4275
+ try {
4276
+ const firstPhase = pipelineDef.phases.find((p) => p.kind === "ai");
4277
+ const firstPhaseArtifacts = (firstPhase?.artifacts ?? []).map((a) => a.filename);
4278
+ const hookInjector = new HookInjector();
4279
+ hookInjector.inject({
4280
+ workDir: primaryWorkDir,
4281
+ planDir: wtPlan.planDir,
4282
+ expectedArtifacts: allArtifacts,
4283
+ phaseExpectedArtifacts: firstPhaseArtifacts,
4284
+ issueIid: issue.iid,
4285
+ phaseName: firstPhase?.name,
4286
+ issueTitle: issue.title,
4287
+ issueDescription: issue.description
4288
+ });
4289
+ } catch (err) {
4290
+ logger16.warn("Failed to inject Claude Code hooks (non-blocking)", {
4291
+ error: err.message
4292
+ });
4293
+ }
4294
+ }
3807
4295
  const wtGitMap = /* @__PURE__ */ new Map();
3808
4296
  if (wtCtx.workspace) {
3809
4297
  wtGitMap.set(wtCtx.workspace.primary.name, wtGit);
@@ -3816,6 +4304,44 @@ async function executeSetup(ctx, deps) {
3816
4304
  return { wtGit, wtPlan, wtGitMap };
3817
4305
  }
3818
4306
 
4307
+ // src/lifecycle/FeedbackTypes.ts
4308
+ function inputRequestToFeedback(request) {
4309
+ switch (request.type) {
4310
+ case "interactive-dialog":
4311
+ return {
4312
+ kind: "interactive-dialog",
4313
+ question: request.content,
4314
+ options: request.options ?? []
4315
+ };
4316
+ case "plan-approval":
4317
+ return { kind: "approval-required", scope: "plan" };
4318
+ case "generic":
4319
+ return { kind: "generic", content: request.content };
4320
+ }
4321
+ }
4322
+ function feedbackResponseToString(response) {
4323
+ switch (response.action) {
4324
+ case "approve":
4325
+ return "allow";
4326
+ case "reject":
4327
+ return "reject";
4328
+ case "select":
4329
+ return response.value;
4330
+ case "dismiss":
4331
+ return "";
4332
+ case "pause":
4333
+ return "";
4334
+ }
4335
+ }
4336
+ function stringToFeedbackResponse(str, originalFeedback) {
4337
+ if (str === "allow") return { action: "approve" };
4338
+ if (str === "reject") return { action: "reject" };
4339
+ if (str === "") {
4340
+ return originalFeedback.kind === "interactive-dialog" ? { action: "dismiss" } : { action: "approve" };
4341
+ }
4342
+ return { action: "select", value: str };
4343
+ }
4344
+
3819
4345
  // src/notesync/NoteSyncSettings.ts
3820
4346
  var noteSyncOverride;
3821
4347
  function getNoteSyncEnabled(cfg) {
@@ -3872,8 +4398,14 @@ function clearPendingDialog(issueIid) {
3872
4398
  store.delete(issueIid);
3873
4399
  }
3874
4400
 
3875
- // src/orchestrator/steps/PhaseLoopStep.ts
3876
- var logger15 = logger.child("PhaseLoopStep");
4401
+ // src/orchestrator/steps/PhaseHelpers.ts
4402
+ var logger17 = logger.child("PhaseHelpers");
4403
+ async function safeComment(deps, issueId, message) {
4404
+ try {
4405
+ await deps.gongfeng.createIssueNote(issueId, message);
4406
+ } catch {
4407
+ }
4408
+ }
3877
4409
  function resolveVerifyRunner(deps) {
3878
4410
  return deps.aiRunner;
3879
4411
  }
@@ -3889,7 +4421,7 @@ async function commitPlanFiles(ctx, wtGit, wtGitMap, phaseName, displayId) {
3889
4421
  for (const repo of ctx.workspace.repos) {
3890
4422
  const repoGit = wtGitMap?.get(repo.name);
3891
4423
  if (!repoGit) {
3892
- logger15.warn("Missing GitOperations for repo, skipping commit", { repo: repo.name });
4424
+ logger17.warn("Missing GitOperations for repo, skipping commit", { repo: repo.name });
3893
4425
  continue;
3894
4426
  }
3895
4427
  const branch = repo.branchPrefix ? `${repo.branchPrefix}-${displayId}` : ctx.branchName;
@@ -3898,10 +4430,10 @@ async function commitPlanFiles(ctx, wtGit, wtGitMap, phaseName, displayId) {
3898
4430
  await repoGit.add(["."]);
3899
4431
  await repoGit.commit(commitMsg);
3900
4432
  await repoGit.push(branch);
3901
- logger15.info("Committed changes for repo", { repo: repo.name, branch });
4433
+ logger17.info("Committed changes for repo", { repo: repo.name, branch });
3902
4434
  }
3903
4435
  } catch (err) {
3904
- logger15.warn("Failed to commit/push for repo", {
4436
+ logger17.warn("Failed to commit/push for repo", {
3905
4437
  repo: repo.name,
3906
4438
  error: err.message
3907
4439
  });
@@ -3939,27 +4471,15 @@ async function syncResultToIssue(phase, ctx, displayId, phaseName, deps, issueId
3939
4471
  summary
3940
4472
  );
3941
4473
  await safeComment(deps, issueId, comment);
3942
- logger15.info("Result synced to issue", { issueIid: displayId, file: file.filename });
4474
+ logger17.info("Result synced to issue", { issueIid: displayId, file: file.filename });
3943
4475
  }
3944
4476
  } catch (err) {
3945
- logger15.warn("Failed to sync result to issue", { error: err.message });
4477
+ logger17.warn("Failed to sync result to issue", { error: err.message });
3946
4478
  await safeComment(deps, issueId, issueProgressComment(phaseName, "completed"));
3947
4479
  }
3948
4480
  }
3949
- function buildInputHandler(displayId, phaseName, deps) {
3950
- const useAcpGate = deps.config.ai.codebuddyAcpAutoApprove === false;
3951
- return (request) => {
3952
- if (request.type === "interactive-dialog") {
3953
- return handleInteractiveDialog(displayId, phaseName, deps, request);
3954
- }
3955
- if (request.type === "plan-approval" && useAcpGate) {
3956
- return handlePlanApproval(displayId, phaseName, deps);
3957
- }
3958
- return Promise.resolve("allow");
3959
- };
3960
- }
3961
4481
  function handlePlanApproval(displayId, phaseName, deps) {
3962
- logger15.info("ACP plan-approval requested, delegating to review gate", {
4482
+ logger17.info("ACP plan-approval requested, delegating to review gate", {
3963
4483
  issueIid: displayId,
3964
4484
  phase: phaseName
3965
4485
  });
@@ -3969,14 +4489,14 @@ function handlePlanApproval(displayId, phaseName, deps) {
3969
4489
  const data = payload.data;
3970
4490
  if (data.issueIid !== displayId) return;
3971
4491
  cleanup();
3972
- logger15.info("ACP plan-approval approved via review gate", { issueIid: displayId });
4492
+ logger17.info("ACP plan-approval approved via review gate", { issueIid: displayId });
3973
4493
  resolve("allow");
3974
4494
  };
3975
4495
  const onRejected = (payload) => {
3976
4496
  const data = payload.data;
3977
4497
  if (data.issueIid !== displayId) return;
3978
4498
  cleanup();
3979
- logger15.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
4499
+ logger17.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
3980
4500
  resolve("reject");
3981
4501
  };
3982
4502
  const cleanup = () => {
@@ -3988,7 +4508,7 @@ function handlePlanApproval(displayId, phaseName, deps) {
3988
4508
  });
3989
4509
  }
3990
4510
  function handleInteractiveDialog(displayId, phaseName, deps, request) {
3991
- logger15.info("Interactive dialog forwarded to frontend", {
4511
+ logger17.info("Interactive dialog forwarded to frontend", {
3992
4512
  issueIid: displayId,
3993
4513
  phase: phaseName,
3994
4514
  question: request.content.slice(0, 80),
@@ -4018,7 +4538,7 @@ function handleInteractiveDialog(displayId, phaseName, deps, request) {
4018
4538
  if (data.issueIid !== displayId) return;
4019
4539
  cleanup();
4020
4540
  clearPendingDialog(displayId);
4021
- logger15.info("Interactive dialog response received from frontend", {
4541
+ logger17.info("Interactive dialog response received from frontend", {
4022
4542
  issueIid: displayId,
4023
4543
  response: data.response
4024
4544
  });
@@ -4028,46 +4548,99 @@ function handleInteractiveDialog(displayId, phaseName, deps, request) {
4028
4548
  const data = payload.data;
4029
4549
  if (data.issueIid !== displayId) return;
4030
4550
  cleanup();
4031
- logger15.info("Interactive dialog dismissed by user (false positive)", { issueIid: displayId });
4551
+ logger17.info("Interactive dialog dismissed by user (false positive)", { issueIid: displayId });
4032
4552
  resolve("");
4033
4553
  };
4034
4554
  deps.eventBus.on("agent:inputResponse", onResponse);
4035
4555
  deps.eventBus.on("agent:dialogDismissed", onDismiss);
4036
4556
  });
4037
4557
  }
4038
- async function safeComment(deps, issueId, message) {
4558
+ function updateHooksForPhase(spec, pipelineDef, ctx, wtPlan) {
4559
+ const phaseArtifacts = (spec.artifacts ?? []).map((a) => a.filename);
4560
+ if (phaseArtifacts.length === 0 && spec.kind !== "ai") return;
4039
4561
  try {
4040
- await deps.gongfeng.createIssueNote(issueId, message);
4041
- } catch {
4562
+ const allArtifacts = pipelineDef.phases.flatMap((p) => p.artifacts ?? []).map((a) => a.filename);
4563
+ const hookInjector = new HookInjector();
4564
+ hookInjector.updateForPhase({
4565
+ workDir: wtPlan.baseDir,
4566
+ planDir: wtPlan.planDir,
4567
+ expectedArtifacts: allArtifacts.length > 0 ? allArtifacts : phaseArtifacts,
4568
+ phaseExpectedArtifacts: phaseArtifacts,
4569
+ issueIid: ctx.issue.iid,
4570
+ phaseName: spec.name,
4571
+ issueTitle: ctx.issue.title,
4572
+ issueDescription: ctx.issue.description
4573
+ });
4574
+ } catch (err) {
4575
+ logger17.warn("Failed to update hooks for phase (non-blocking)", {
4576
+ phase: spec.name,
4577
+ error: err.message
4578
+ });
4042
4579
  }
4043
4580
  }
4044
- async function runPhaseWithLifecycle(phase, phaseCtx, spec, ctx, deps, wtGit, wtPlan, wtGitMap) {
4045
- const { issue } = ctx;
4046
- const displayId = issue.iid;
4047
- deps.tracker.updateState(displayId, spec.startState, { currentPhase: spec.name });
4048
- deps.tracker.updatePhaseProgress(displayId, spec.name, {
4049
- status: "in_progress",
4050
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
4051
- });
4052
- wtPlan.updatePhaseProgress(spec.name, "in_progress");
4053
- await safeComment(deps, issue.id, issueProgressComment(spec.name, "in_progress"));
4054
- const phaseLabel = t(`phase.${spec.name}`) || spec.name;
4055
- deps.eventBus.emitTyped("agent:output", {
4056
- issueIid: displayId,
4057
- phase: spec.name,
4058
- event: { type: "system", content: t("basePhase.aiStarting", { label: phaseLabel }), timestamp: (/* @__PURE__ */ new Date()).toISOString() }
4059
- });
4060
- const callbacks = {
4061
- onStreamEvent: (event) => deps.eventBus.emitTyped("agent:output", {
4581
+
4582
+ // src/lifecycle/DefaultLifecycleHook.ts
4583
+ var logger18 = logger.child("DefaultLifecycleHook");
4584
+ var DefaultLifecycleHook = class {
4585
+ async beforePhase(ctx) {
4586
+ const { spec, issueCtx, deps, wtPlan } = ctx;
4587
+ const { issue } = issueCtx;
4588
+ const displayId = issue.iid;
4589
+ deps.tracker.updateState(displayId, spec.startState, { currentPhase: spec.name });
4590
+ deps.tracker.updatePhaseProgress(displayId, spec.name, {
4591
+ status: "in_progress",
4592
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
4593
+ });
4594
+ wtPlan.updatePhaseProgress(spec.name, "in_progress");
4595
+ await safeComment(deps, issue.id, issueProgressComment(spec.name, "in_progress"));
4596
+ const phaseLabel = t(`phase.${spec.name}`) || spec.name;
4597
+ deps.eventBus.emitTyped("agent:output", {
4062
4598
  issueIid: displayId,
4063
4599
  phase: spec.name,
4600
+ event: {
4601
+ type: "system",
4602
+ content: t("basePhase.aiStarting", { label: phaseLabel }),
4603
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4604
+ }
4605
+ });
4606
+ }
4607
+ onStream(ctx, event) {
4608
+ const { spec, issueCtx, deps } = ctx;
4609
+ deps.eventBus.emitTyped("agent:output", {
4610
+ issueIid: issueCtx.issue.iid,
4611
+ phase: spec.name,
4064
4612
  event
4065
- }),
4066
- onInputRequired: buildInputHandler(displayId, spec.name, deps)
4067
- };
4068
- const outcome = await phase.run(phaseCtx, callbacks);
4069
- if (outcome.sessionId) wtPlan.updatePhaseSessionId(spec.name, outcome.sessionId);
4070
- if (outcome.status === "completed") {
4613
+ });
4614
+ }
4615
+ beforeExecute;
4616
+ afterExecute;
4617
+ async onFeedback(ctx, feedback) {
4618
+ const { deps, displayId, spec } = ctx;
4619
+ if (feedback.kind === "interactive-dialog") {
4620
+ const result = await handleInteractiveDialog(displayId, spec.name, deps, {
4621
+ type: "interactive-dialog",
4622
+ content: feedback.question,
4623
+ options: feedback.options
4624
+ });
4625
+ return stringToFeedbackResponse(result, feedback);
4626
+ }
4627
+ if (feedback.kind === "approval-required" && feedback.scope === "plan") {
4628
+ const useAcpGate = deps.config.ai.codebuddyAcpAutoApprove === false;
4629
+ if (useAcpGate) {
4630
+ const result = await handlePlanApproval(displayId, spec.name, deps);
4631
+ return result === "allow" ? { action: "approve" } : { action: "reject" };
4632
+ }
4633
+ return { action: "approve" };
4634
+ }
4635
+ return { action: "approve" };
4636
+ }
4637
+ async afterPhase(ctx, outcome) {
4638
+ const { spec, issueCtx, deps, wtGit, wtPlan, wtGitMap, phase } = ctx;
4639
+ const { issue, phaseCtx } = issueCtx;
4640
+ const displayId = issue.iid;
4641
+ if (outcome.sessionId) {
4642
+ wtPlan.updatePhaseSessionId(spec.name, outcome.sessionId);
4643
+ }
4071
4644
  deps.tracker.updateState(displayId, spec.doneState, { currentPhase: spec.name });
4072
4645
  deps.tracker.updatePhaseProgress(displayId, spec.name, {
4073
4646
  status: "completed",
@@ -4075,28 +4648,400 @@ async function runPhaseWithLifecycle(phase, phaseCtx, spec, ctx, deps, wtGit, wt
4075
4648
  });
4076
4649
  wtPlan.updatePhaseProgress(spec.name, "completed");
4077
4650
  await commitPlanFiles(phaseCtx, wtGit, wtGitMap, spec.name, displayId);
4078
- await syncResultToIssue(phase, phaseCtx, displayId, spec.name, deps, issue.id, wtPlan);
4079
- return outcome;
4651
+ if (phase) {
4652
+ await syncResultToIssue(phase, phaseCtx, displayId, spec.name, deps, issue.id, wtPlan);
4653
+ }
4654
+ }
4655
+ async onError(ctx, error) {
4656
+ const { spec, issueCtx, deps, wtPlan } = ctx;
4657
+ const { issue } = issueCtx;
4658
+ const displayId = issue.iid;
4659
+ wtPlan.updatePhaseProgress(spec.name, "failed", error.message);
4660
+ deps.tracker.updatePhaseProgress(displayId, spec.name, { status: "failed" });
4661
+ if (error.wasActiveAtTimeout) {
4662
+ deps.tracker.markFailedSoft(displayId, error.message, "phase_running" /* PhaseRunning */);
4663
+ } else {
4664
+ deps.tracker.markFailed(displayId, error.message, "phase_running" /* PhaseRunning */);
4665
+ }
4666
+ const shortErr = error.message.slice(0, 200);
4667
+ await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
4668
+ return { action: "fail", softFail: error.wasActiveAtTimeout };
4080
4669
  }
4081
- if (outcome.status === "running") {
4082
- return outcome;
4670
+ };
4671
+ function createCallbacksFromHook(hook, ctx) {
4672
+ return {
4673
+ onStreamEvent: hook.onStream ? (event) => hook.onStream(ctx, event) : void 0,
4674
+ onInputRequired: hook.onFeedback ? async (request) => {
4675
+ const feedback = inputRequestToFeedback(request);
4676
+ const response = await hook.onFeedback(ctx, feedback);
4677
+ return feedbackResponseToString(response);
4678
+ } : void 0
4679
+ };
4680
+ }
4681
+
4682
+ // src/orchestrator/strategies/GateStrategy.ts
4683
+ var logger19 = logger.child("GateStrategy");
4684
+ var GateStrategy = class {
4685
+ name = "gate";
4686
+ shouldSkip() {
4687
+ return false;
4083
4688
  }
4084
- const errMsg = outcome.error?.message ?? "Unknown error";
4085
- const shortErr = errMsg.slice(0, 200);
4086
- const wasActive = outcome.error?.wasActiveAtTimeout ?? false;
4087
- wtPlan.updatePhaseProgress(spec.name, "failed", errMsg);
4088
- deps.tracker.updatePhaseProgress(displayId, spec.name, { status: "failed" });
4089
- if (wasActive) {
4090
- deps.tracker.markFailedSoft(displayId, errMsg, "phase_running" /* PhaseRunning */);
4091
- } else {
4092
- deps.tracker.markFailed(displayId, errMsg, "phase_running" /* PhaseRunning */);
4689
+ async execute(ctx, _hooks) {
4690
+ const { spec, issueCtx, deps, wtPlan } = ctx;
4691
+ const { issue } = issueCtx;
4692
+ if (deps.shouldAutoApprove(issue.labels)) {
4693
+ logger19.info("Auto-approving review gate (matched autoApproveLabels)", {
4694
+ iid: issue.iid,
4695
+ labels: issue.labels,
4696
+ autoApproveLabels: deps.config.review.autoApproveLabels
4697
+ });
4698
+ if (spec.approvedState) {
4699
+ deps.tracker.updateState(issue.iid, spec.approvedState, { currentPhase: spec.name });
4700
+ }
4701
+ wtPlan.updatePhaseProgress(spec.name, "completed");
4702
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
4703
+ status: "completed",
4704
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
4705
+ });
4706
+ try {
4707
+ await deps.gongfeng.createIssueNote(issue.id, t("orchestrator.autoApproveComment"));
4708
+ } catch {
4709
+ }
4710
+ return { paused: false };
4711
+ }
4712
+ deps.tracker.updateState(issue.iid, spec.startState, { currentPhase: spec.name });
4713
+ wtPlan.updatePhaseProgress(spec.name, "in_progress");
4714
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
4715
+ status: "in_progress",
4716
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
4717
+ });
4718
+ deps.eventBus.emitTyped("review:requested", { issueIid: issue.iid });
4719
+ logger19.info("Review gate reached, pausing", { iid: issue.iid });
4720
+ return { paused: true };
4721
+ }
4722
+ };
4723
+
4724
+ // src/orchestrator/strategies/AiPhaseStrategy.ts
4725
+ var logger20 = logger.child("AiPhaseStrategy");
4726
+ var AiPhaseStrategy = class {
4727
+ name = "ai";
4728
+ shouldSkip(ctx) {
4729
+ const { spec, issueCtx, deps } = ctx;
4730
+ if (spec.name === "uat" && !isE2eEnabledForIssue(issueCtx.issue.iid, deps.tracker, deps.config)) {
4731
+ logger20.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issueCtx.issue.iid });
4732
+ return true;
4733
+ }
4734
+ return false;
4093
4735
  }
4094
- await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
4736
+ async execute(ctx, hooks) {
4737
+ const { spec, issueCtx, deps, wtGit, wtPlan, wtGitMap } = ctx;
4738
+ const { issue, phaseCtx } = issueCtx;
4739
+ if (this.shouldSkip(ctx)) {
4740
+ deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
4741
+ wtPlan.updatePhaseProgress(spec.name, "completed");
4742
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
4743
+ status: "completed",
4744
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
4745
+ });
4746
+ return { paused: false };
4747
+ }
4748
+ updateHooksForPhase(spec, issueCtx.pipelineDef, issueCtx, wtPlan);
4749
+ const runner = this.resolveRunner(ctx);
4750
+ if (spec.name === "uat") {
4751
+ const runnerName = runner === deps.e2eAiRunner ? "e2eAiRunner (CodeBuddy)" : "mainRunner";
4752
+ logger20.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
4753
+ }
4754
+ const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
4755
+ if (wtGitMap) phase.setWtGitMap(wtGitMap);
4756
+ ctx.phase = phase;
4757
+ const outcome = await runPhaseWithLifecycle(
4758
+ phase,
4759
+ phaseCtx,
4760
+ spec,
4761
+ issueCtx,
4762
+ deps,
4763
+ wtGit,
4764
+ wtPlan,
4765
+ wtGitMap,
4766
+ hooks
4767
+ );
4768
+ if (outcome.status === "running") {
4769
+ return this.handleAsyncOutcome(ctx, outcome);
4770
+ }
4771
+ if (spec.approvedState && outcome.data?.hasReleaseCapability) {
4772
+ return this.handleGateRequest(ctx);
4773
+ }
4774
+ return { paused: false };
4775
+ }
4776
+ resolveRunner(ctx) {
4777
+ const { spec, deps, displayId } = ctx;
4778
+ if (spec.name === "verify") return resolveVerifyRunner(deps);
4779
+ if (spec.name === "uat") return resolveUatRunner(deps, displayId);
4780
+ return deps.aiRunner;
4781
+ }
4782
+ async handleAsyncOutcome(ctx, outcome) {
4783
+ const { spec, issueCtx, deps, wtGit, wtPlan, wtGitMap } = ctx;
4784
+ const { issue, phaseCtx } = issueCtx;
4785
+ if (outcome.awaitCompletion) {
4786
+ logger20.info("Async phase running, awaiting completion", { iid: issue.iid, phase: spec.name });
4787
+ const finalOutcome = await outcome.awaitCompletion;
4788
+ if (finalOutcome.sessionId) {
4789
+ wtPlan.updatePhaseSessionId(spec.name, finalOutcome.sessionId);
4790
+ }
4791
+ if (finalOutcome.status === "completed") {
4792
+ deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
4793
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
4794
+ status: "completed",
4795
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
4796
+ });
4797
+ wtPlan.updatePhaseProgress(spec.name, "completed");
4798
+ await commitPlanFiles(phaseCtx, wtGit, wtGitMap, spec.name, issue.iid);
4799
+ const runner = this.resolveRunner(ctx);
4800
+ const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
4801
+ await syncResultToIssue(phase, phaseCtx, issue.iid, spec.name, deps, issue.id, wtPlan);
4802
+ logger20.info("Async phase completed successfully", { iid: issue.iid, phase: spec.name });
4803
+ return { paused: false };
4804
+ }
4805
+ const errMsg = finalOutcome.error?.message ?? "Unknown error";
4806
+ const shortErr = errMsg.slice(0, 200);
4807
+ const wasActive = finalOutcome.error?.wasActiveAtTimeout ?? false;
4808
+ wtPlan.updatePhaseProgress(spec.name, "failed", errMsg);
4809
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "failed" });
4810
+ if (wasActive) {
4811
+ deps.tracker.markFailedSoft(issue.iid, errMsg, "phase_running" /* PhaseRunning */);
4812
+ } else {
4813
+ deps.tracker.markFailed(issue.iid, errMsg, "phase_running" /* PhaseRunning */);
4814
+ }
4815
+ await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
4816
+ throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
4817
+ output: finalOutcome.error?.rawOutput ?? finalOutcome.output,
4818
+ exitCode: finalOutcome.exitCode ?? 1,
4819
+ isRetryable: finalOutcome.error?.isRetryable,
4820
+ wasActiveAtTimeout: wasActive
4821
+ });
4822
+ }
4823
+ deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
4824
+ wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
4825
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
4826
+ const gateEvent = spec.name === "uat" ? "uat:gateRequested" : "release:gateRequested";
4827
+ deps.eventBus.emitTyped(gateEvent, { issueIid: issue.iid });
4828
+ logger20.info("Async phase running (no awaitCompletion), pausing pipeline", {
4829
+ iid: issue.iid,
4830
+ phase: spec.name
4831
+ });
4832
+ return { paused: true };
4833
+ }
4834
+ handleGateRequest(ctx) {
4835
+ const { spec, issueCtx, deps, wtPlan } = ctx;
4836
+ const { issue } = issueCtx;
4837
+ deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
4838
+ wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
4839
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
4840
+ deps.eventBus.emitTyped("release:gateRequested", { issueIid: issue.iid });
4841
+ logger20.info("Phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
4842
+ return { paused: true };
4843
+ }
4844
+ };
4845
+
4846
+ // src/orchestrator/strategies/VerifyFixStrategy.ts
4847
+ var logger21 = logger.child("VerifyFixStrategy");
4848
+ var VerifyFixStrategy = class {
4849
+ name = "verify-fix";
4850
+ shouldSkip() {
4851
+ return false;
4852
+ }
4853
+ async execute(ctx, hooks) {
4854
+ const { issueCtx, deps, wtGit, wtPlan, wtGitMap } = ctx;
4855
+ const { issue, phaseCtx, pipelineDef } = issueCtx;
4856
+ const maxIterations = deps.config.verifyFixLoop.maxIterations;
4857
+ const verifySpec = ctx.spec;
4858
+ const verifyPhaseIdx = pipelineDef.phases.findIndex((p) => p.name === verifySpec.name);
4859
+ const buildPhaseIdx = this.findPreviousAiPhaseIndex(pipelineDef.phases, verifyPhaseIdx);
4860
+ deps.eventBus.emitTyped("verify:loopStarted", {
4861
+ issueIid: issue.iid,
4862
+ maxIterations
4863
+ });
4864
+ logger21.info("Verify-fix loop started", {
4865
+ iid: issue.iid,
4866
+ maxIterations,
4867
+ buildPhaseIdx
4868
+ });
4869
+ for (let iteration = 1; iteration <= maxIterations; iteration++) {
4870
+ if (isShuttingDown()) throw new ServiceShutdownError();
4871
+ logger21.info("Verify-fix loop iteration", { iteration, maxIterations, iid: issue.iid });
4872
+ updateHooksForPhase(verifySpec, pipelineDef, issueCtx, wtPlan);
4873
+ const verifyRunner = resolveVerifyRunner(deps);
4874
+ const verifyPhase = createPhase("verify", verifyRunner, wtGit, wtPlan, deps.config);
4875
+ if (wtGitMap) verifyPhase.setWtGitMap(wtGitMap);
4876
+ let verifyOutcome;
4877
+ try {
4878
+ verifyOutcome = await runPhaseWithLifecycle(
4879
+ verifyPhase,
4880
+ phaseCtx,
4881
+ verifySpec,
4882
+ issueCtx,
4883
+ deps,
4884
+ wtGit,
4885
+ wtPlan,
4886
+ wtGitMap,
4887
+ hooks
4888
+ );
4889
+ } catch (err) {
4890
+ logger21.warn("Verify phase execution failed", {
4891
+ iteration,
4892
+ iid: issue.iid,
4893
+ error: err.message
4894
+ });
4895
+ deps.eventBus.emitTyped("verify:iterationComplete", {
4896
+ issueIid: issue.iid,
4897
+ iteration,
4898
+ passed: false,
4899
+ failures: ["AI runner execution failed"]
4900
+ });
4901
+ if (iteration === maxIterations) throw err;
4902
+ if (buildPhaseIdx >= 0) {
4903
+ await this.executeBuildFix(issueCtx, deps, wtGit, wtPlan, buildPhaseIdx, {
4904
+ iteration,
4905
+ verifyFailures: ["AI runner execution failed: " + err.message],
4906
+ rawReport: ""
4907
+ }, wtGitMap, hooks);
4908
+ }
4909
+ continue;
4910
+ }
4911
+ const report = verifyOutcome.data?.verifyReport;
4912
+ const passed = report ? report.passed : true;
4913
+ deps.eventBus.emitTyped("verify:iterationComplete", {
4914
+ issueIid: issue.iid,
4915
+ iteration,
4916
+ passed,
4917
+ failures: report?.failureReasons
4918
+ });
4919
+ if (passed) {
4920
+ logger21.info("Verify-fix loop passed", { iteration, iid: issue.iid });
4921
+ return { paused: false };
4922
+ }
4923
+ logger21.info("Verify failed, issues found", {
4924
+ iteration,
4925
+ iid: issue.iid,
4926
+ failures: report?.failureReasons,
4927
+ todolistStats: report?.todolistStats
4928
+ });
4929
+ if (iteration === maxIterations) {
4930
+ deps.eventBus.emitTyped("verify:loopExhausted", {
4931
+ issueIid: issue.iid,
4932
+ totalIterations: iteration,
4933
+ failures: report?.failureReasons ?? []
4934
+ });
4935
+ const failMsg = `Verify-fix loop exhausted after ${maxIterations} iterations. Remaining issues: ${report?.failureReasons?.join("; ") ?? "unknown"}`;
4936
+ logger21.warn(failMsg, { iid: issue.iid });
4937
+ throw new AIExecutionError("verify", failMsg, {
4938
+ output: report?.rawReport ?? "",
4939
+ exitCode: 0
4940
+ });
4941
+ }
4942
+ if (buildPhaseIdx >= 0) {
4943
+ await this.executeBuildFix(issueCtx, deps, wtGit, wtPlan, buildPhaseIdx, {
4944
+ iteration,
4945
+ verifyFailures: report?.failureReasons ?? [],
4946
+ rawReport: report?.rawReport ?? ""
4947
+ }, wtGitMap, hooks);
4948
+ }
4949
+ }
4950
+ return { paused: false };
4951
+ }
4952
+ async executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixContext, wtGitMap, hooks) {
4953
+ const { issue, phaseCtx, pipelineDef } = ctx;
4954
+ const buildSpec = pipelineDef.phases[buildPhaseIdx];
4955
+ logger21.info("Looping back to build for fix", {
4956
+ iteration: fixContext.iteration,
4957
+ iid: issue.iid,
4958
+ failures: fixContext.verifyFailures
4959
+ });
4960
+ phaseCtx.fixContext = fixContext;
4961
+ try {
4962
+ updateHooksForPhase(buildSpec, pipelineDef, ctx, wtPlan);
4963
+ const buildPhase = createPhase("build", deps.aiRunner, wtGit, wtPlan, deps.config);
4964
+ if (wtGitMap) buildPhase.setWtGitMap(wtGitMap);
4965
+ await runPhaseWithLifecycle(
4966
+ buildPhase,
4967
+ phaseCtx,
4968
+ buildSpec,
4969
+ ctx,
4970
+ deps,
4971
+ wtGit,
4972
+ wtPlan,
4973
+ wtGitMap,
4974
+ hooks
4975
+ );
4976
+ } finally {
4977
+ delete phaseCtx.fixContext;
4978
+ }
4979
+ }
4980
+ findPreviousAiPhaseIndex(phases, currentIdx) {
4981
+ for (let j = currentIdx - 1; j >= 0; j--) {
4982
+ if (phases[j].kind === "ai") return j;
4983
+ }
4984
+ return -1;
4985
+ }
4986
+ };
4987
+
4988
+ // src/orchestrator/strategies/index.ts
4989
+ var gateStrategy = new GateStrategy();
4990
+ var aiStrategy = new AiPhaseStrategy();
4991
+ var verifyFixStrategy = new VerifyFixStrategy();
4992
+ function resolveStrategy(spec, config) {
4993
+ if (spec.kind === "gate") {
4994
+ return gateStrategy;
4995
+ }
4996
+ if (spec.name === "verify" && config.verifyFixLoop.enabled) {
4997
+ return verifyFixStrategy;
4998
+ }
4999
+ return aiStrategy;
5000
+ }
5001
+
5002
+ // src/orchestrator/steps/PhaseLoopStep.ts
5003
+ var logger22 = logger.child("PhaseLoopStep");
5004
+ async function runPhaseWithLifecycle(phase, phaseCtx, spec, ctx, deps, wtGit, wtPlan, wtGitMap, hook) {
5005
+ const lifecycleHook = hook ?? new DefaultLifecycleHook();
5006
+ const execCtx = {
5007
+ spec,
5008
+ issueCtx: ctx,
5009
+ deps,
5010
+ wtGit,
5011
+ wtPlan,
5012
+ wtGitMap,
5013
+ phase,
5014
+ displayId: ctx.issue.iid
5015
+ };
5016
+ await lifecycleHook.beforePhase(execCtx);
5017
+ if (lifecycleHook.beforeExecute) {
5018
+ await lifecycleHook.beforeExecute(execCtx);
5019
+ }
5020
+ const callbacks = createCallbacksFromHook(lifecycleHook, execCtx);
5021
+ const outcome = await phase.run(phaseCtx, callbacks);
5022
+ if (outcome.sessionId) wtPlan.updatePhaseSessionId(spec.name, outcome.sessionId);
5023
+ const finalOutcome = lifecycleHook.afterExecute ? await lifecycleHook.afterExecute(execCtx, outcome) : outcome;
5024
+ if (finalOutcome.status === "completed") {
5025
+ await lifecycleHook.afterPhase(execCtx, finalOutcome);
5026
+ return finalOutcome;
5027
+ }
5028
+ if (finalOutcome.status === "running") {
5029
+ return finalOutcome;
5030
+ }
5031
+ const errMsg = finalOutcome.error?.message ?? "Unknown error";
5032
+ const phaseError = {
5033
+ message: errMsg,
5034
+ isRetryable: finalOutcome.error?.isRetryable ?? true,
5035
+ rawOutput: finalOutcome.error?.rawOutput ?? finalOutcome.output,
5036
+ wasActiveAtTimeout: finalOutcome.error?.wasActiveAtTimeout ?? false
5037
+ };
5038
+ await lifecycleHook.onError(execCtx, phaseError);
5039
+ const shortErr = errMsg.slice(0, 200);
4095
5040
  throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
4096
- output: outcome.error?.rawOutput ?? outcome.output,
4097
- exitCode: outcome.exitCode ?? 1,
4098
- isRetryable: outcome.error?.isRetryable,
4099
- wasActiveAtTimeout: wasActive
5041
+ output: finalOutcome.error?.rawOutput ?? finalOutcome.output,
5042
+ exitCode: finalOutcome.exitCode ?? 1,
5043
+ isRetryable: finalOutcome.error?.isRetryable,
5044
+ wasActiveAtTimeout: finalOutcome.error?.wasActiveAtTimeout ?? false
4100
5045
  });
4101
5046
  }
4102
5047
  async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
@@ -4120,15 +5065,15 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
4120
5065
  if (skippedDeployPhase && !phaseCtx.ports) {
4121
5066
  const existingPorts = deps.getPortsForIssue(issue.iid);
4122
5067
  if (existingPorts && deps.isPreviewRunning(issue.iid)) {
4123
- logger15.info("Restored preview ports from allocator", { iid: issue.iid, ...existingPorts });
5068
+ logger22.info("Restored preview ports from allocator", { iid: issue.iid, ...existingPorts });
4124
5069
  phaseCtx.ports = existingPorts;
4125
5070
  ctx.wtCtx.ports = existingPorts;
4126
5071
  serversStarted = true;
4127
5072
  } else {
4128
5073
  if (existingPorts) {
4129
- logger15.info("Ports allocated but servers not running, restarting", { iid: issue.iid });
5074
+ logger22.info("Ports allocated but servers not running, restarting", { iid: issue.iid });
4130
5075
  } else {
4131
- logger15.info("Restarting preview servers for resumed pipeline", { iid: issue.iid });
5076
+ logger22.info("Restarting preview servers for resumed pipeline", { iid: issue.iid });
4132
5077
  }
4133
5078
  const ports = await deps.startPreviewServers(ctx.wtCtx, issue);
4134
5079
  if (ports) {
@@ -4140,93 +5085,25 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
4140
5085
  }
4141
5086
  }
4142
5087
  if (startIdx > 0) {
4143
- const currentProgress = wtPlan.readProgress();
4144
- if (currentProgress) {
4145
- let patched = false;
4146
- for (let i = 0; i < startIdx; i++) {
4147
- const prevSpec = pipelineDef.phases[i];
4148
- const pp = currentProgress.phases[prevSpec.name];
4149
- if (pp && pp.status !== "completed") {
4150
- logger15.warn("Fixing stale phase progress", {
4151
- iid: issue.iid,
4152
- phase: prevSpec.name,
4153
- was: pp.status,
4154
- now: "completed"
4155
- });
4156
- pp.status = "completed";
4157
- if (!pp.completedAt) {
4158
- pp.completedAt = (/* @__PURE__ */ new Date()).toISOString();
4159
- }
4160
- patched = true;
4161
- }
4162
- }
4163
- if (patched) {
4164
- wtPlan.writeProgress(currentProgress);
4165
- }
4166
- }
4167
- if (record.phaseProgress) {
4168
- for (let i = 0; i < startIdx; i++) {
4169
- const prevSpec = pipelineDef.phases[i];
4170
- const tp = record.phaseProgress[prevSpec.name];
4171
- if (tp && tp.status !== "completed") {
4172
- deps.tracker.updatePhaseProgress(issue.iid, prevSpec.name, {
4173
- status: "completed",
4174
- completedAt: tp.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
4175
- });
4176
- }
4177
- }
4178
- }
5088
+ healStaleProgress(ctx, deps, wtPlan, startIdx);
4179
5089
  }
5090
+ const hooks = new DefaultLifecycleHook();
4180
5091
  for (let i = startIdx; i < pipelineDef.phases.length; i++) {
4181
- if (isShuttingDown()) {
4182
- throw new ServiceShutdownError();
4183
- }
5092
+ if (isShuttingDown()) throw new ServiceShutdownError();
4184
5093
  const spec = pipelineDef.phases[i];
4185
5094
  const pendingAction = deps.consumePendingAction?.(issue.iid);
4186
- if (pendingAction) {
4187
- throw new PhaseAbortedError(spec.name, pendingAction);
4188
- }
4189
- if (spec.kind === "gate") {
4190
- if (deps.shouldAutoApprove(issue.labels)) {
4191
- logger15.info("Auto-approving review gate (matched autoApproveLabels)", {
4192
- iid: issue.iid,
4193
- labels: issue.labels,
4194
- autoApproveLabels: deps.config.review.autoApproveLabels
4195
- });
4196
- if (spec.approvedState) {
4197
- deps.tracker.updateState(issue.iid, spec.approvedState, { currentPhase: spec.name });
4198
- }
4199
- wtPlan.updatePhaseProgress(spec.name, "completed");
4200
- deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
4201
- status: "completed",
4202
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
4203
- });
4204
- try {
4205
- await deps.gongfeng.createIssueNote(
4206
- issue.id,
4207
- t("orchestrator.autoApproveComment")
4208
- );
4209
- } catch {
4210
- }
4211
- continue;
4212
- }
4213
- deps.tracker.updateState(issue.iid, spec.startState, { currentPhase: spec.name });
4214
- wtPlan.updatePhaseProgress(spec.name, "in_progress");
4215
- deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
4216
- status: "in_progress",
4217
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
4218
- });
4219
- deps.eventBus.emitTyped("review:requested", { issueIid: issue.iid });
4220
- logger15.info("Review gate reached, pausing", { iid: issue.iid });
4221
- return { serversStarted, paused: true };
4222
- }
4223
- if (spec.name === "verify" && deps.config.verifyFixLoop.enabled) {
4224
- const buildIdx = findPreviousAiPhaseIndex(pipelineDef.phases, i);
4225
- await executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, i, buildIdx, wtGitMap);
4226
- continue;
4227
- }
4228
- if (spec.name === "uat" && !isE2eEnabledForIssue(issue.iid, deps.tracker, deps.config)) {
4229
- logger15.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issue.iid });
5095
+ if (pendingAction) throw new PhaseAbortedError(spec.name, pendingAction);
5096
+ const strategy = resolveStrategy(spec, deps.config);
5097
+ const execCtx = {
5098
+ spec,
5099
+ issueCtx: ctx,
5100
+ deps,
5101
+ wtGit,
5102
+ wtPlan,
5103
+ wtGitMap,
5104
+ displayId: issue.iid
5105
+ };
5106
+ if (strategy.shouldSkip(execCtx)) {
4230
5107
  deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
4231
5108
  wtPlan.updatePhaseProgress(spec.name, "completed");
4232
5109
  deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
@@ -4235,48 +5112,12 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
4235
5112
  });
4236
5113
  continue;
4237
5114
  }
4238
- const runner = spec.name === "verify" ? resolveVerifyRunner(deps) : spec.name === "uat" ? resolveUatRunner(deps, issue.iid) : deps.aiRunner;
4239
- if (spec.name === "uat") {
4240
- const runnerName = runner === deps.e2eAiRunner ? "e2eAiRunner (CodeBuddy)" : "mainRunner";
4241
- logger15.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
4242
- }
4243
- const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
4244
- if (wtGitMap) {
4245
- phase.setWtGitMap(wtGitMap);
4246
- }
4247
- const outcome = await runPhaseWithLifecycle(
4248
- phase,
4249
- phaseCtx,
4250
- spec,
4251
- ctx,
4252
- deps,
4253
- wtGit,
4254
- wtPlan,
4255
- wtGitMap
4256
- );
4257
- if (outcome.status === "running") {
4258
- if (outcome.awaitCompletion) {
4259
- logger15.info("Async phase running, awaiting completion", { iid: issue.iid, phase: spec.name });
4260
- const finalOutcome = await awaitAsyncPhase(outcome, spec, ctx, deps, wtGit, wtPlan, wtGitMap);
4261
- if (finalOutcome.status === "completed") {
4262
- continue;
4263
- }
4264
- }
4265
- deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
4266
- wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
4267
- deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
4268
- const gateEvent = spec.name === "uat" ? "uat:gateRequested" : "release:gateRequested";
4269
- deps.eventBus.emitTyped(gateEvent, { issueIid: issue.iid });
4270
- logger15.info("Async phase running (no awaitCompletion), pausing pipeline", { iid: issue.iid, phase: spec.name });
4271
- return { serversStarted, paused: true };
5115
+ const result = await strategy.execute(execCtx, hooks);
5116
+ if (result.paused) {
5117
+ return { serversStarted: serversStarted || !!result.serversStarted, paused: true };
4272
5118
  }
4273
- if (spec.approvedState && outcome.data?.hasReleaseCapability) {
4274
- deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
4275
- wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
4276
- deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
4277
- deps.eventBus.emitTyped("release:gateRequested", { issueIid: issue.iid });
4278
- logger15.info("Phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
4279
- return { serversStarted, paused: true };
5119
+ if (result.serversStarted) {
5120
+ serversStarted = true;
4280
5121
  }
4281
5122
  if (needsDeployment && !serversStarted && lifecycleManager.shouldDeployPreview(spec.name)) {
4282
5123
  const ports = await deps.startPreviewServers(ctx.wtCtx, issue);
@@ -4289,189 +5130,48 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
4289
5130
  }
4290
5131
  return { serversStarted, paused: false };
4291
5132
  }
4292
- async function awaitAsyncPhase(outcome, spec, ctx, deps, wtGit, wtPlan, wtGitMap) {
4293
- const { issue } = ctx;
4294
- const displayId = issue.iid;
4295
- const phaseCtx = ctx.phaseCtx;
4296
- const finalOutcome = await outcome.awaitCompletion;
4297
- if (finalOutcome.sessionId) {
4298
- wtPlan.updatePhaseSessionId(spec.name, finalOutcome.sessionId);
4299
- }
4300
- if (finalOutcome.status === "completed") {
4301
- deps.tracker.updateState(displayId, spec.doneState, { currentPhase: spec.name });
4302
- deps.tracker.updatePhaseProgress(displayId, spec.name, {
4303
- status: "completed",
4304
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
4305
- });
4306
- wtPlan.updatePhaseProgress(spec.name, "completed");
4307
- await commitPlanFiles(phaseCtx, wtGit, wtGitMap, spec.name, displayId);
4308
- const runner = spec.name === "uat" ? resolveUatRunner(deps, displayId) : deps.aiRunner;
4309
- const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
4310
- await syncResultToIssue(phase, phaseCtx, displayId, spec.name, deps, issue.id, wtPlan);
4311
- logger15.info("Async phase completed successfully", { iid: displayId, phase: spec.name });
4312
- return finalOutcome;
4313
- }
4314
- const errMsg = finalOutcome.error?.message ?? "Unknown error";
4315
- const shortErr = errMsg.slice(0, 200);
4316
- const wasActive = finalOutcome.error?.wasActiveAtTimeout ?? false;
4317
- wtPlan.updatePhaseProgress(spec.name, "failed", errMsg);
4318
- deps.tracker.updatePhaseProgress(displayId, spec.name, { status: "failed" });
4319
- if (wasActive) {
4320
- deps.tracker.markFailedSoft(displayId, errMsg, "phase_running" /* PhaseRunning */);
4321
- } else {
4322
- deps.tracker.markFailed(displayId, errMsg, "phase_running" /* PhaseRunning */);
4323
- }
4324
- await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
4325
- throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
4326
- output: finalOutcome.error?.rawOutput ?? finalOutcome.output,
4327
- exitCode: finalOutcome.exitCode ?? 1,
4328
- isRetryable: finalOutcome.error?.isRetryable,
4329
- wasActiveAtTimeout: wasActive
4330
- });
4331
- }
4332
- function findPreviousAiPhaseIndex(phases, currentIdx) {
4333
- for (let j = currentIdx - 1; j >= 0; j--) {
4334
- if (phases[j].kind === "ai") return j;
4335
- }
4336
- return -1;
4337
- }
4338
- async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, buildPhaseIdx, wtGitMap) {
4339
- const { issue, lifecycleManager, phaseCtx } = ctx;
4340
- const maxIterations = deps.config.verifyFixLoop.maxIterations;
4341
- const verifySpec = ctx.pipelineDef.phases[verifyPhaseIdx];
4342
- deps.eventBus.emitTyped("verify:loopStarted", {
4343
- issueIid: issue.iid,
4344
- maxIterations
4345
- });
4346
- logger15.info("Verify-fix loop started", {
4347
- iid: issue.iid,
4348
- maxIterations,
4349
- buildPhaseIdx
4350
- });
4351
- for (let iteration = 1; iteration <= maxIterations; iteration++) {
4352
- if (isShuttingDown()) {
4353
- throw new ServiceShutdownError();
4354
- }
4355
- logger15.info("Verify-fix loop iteration", {
4356
- iteration,
4357
- maxIterations,
4358
- iid: issue.iid
4359
- });
4360
- const verifyRunner = resolveVerifyRunner(deps);
4361
- const verifyPhase = createPhase("verify", verifyRunner, wtGit, wtPlan, deps.config);
4362
- if (wtGitMap) {
4363
- verifyPhase.setWtGitMap(wtGitMap);
4364
- }
4365
- let verifyOutcome;
4366
- try {
4367
- verifyOutcome = await runPhaseWithLifecycle(
4368
- verifyPhase,
4369
- phaseCtx,
4370
- verifySpec,
4371
- ctx,
4372
- deps,
4373
- wtGit,
4374
- wtPlan,
4375
- wtGitMap
4376
- );
4377
- } catch (err) {
4378
- logger15.warn("Verify phase execution failed", {
4379
- iteration,
4380
- iid: issue.iid,
4381
- error: err.message
4382
- });
4383
- deps.eventBus.emitTyped("verify:iterationComplete", {
4384
- issueIid: issue.iid,
4385
- iteration,
4386
- passed: false,
4387
- failures: ["AI runner execution failed"]
4388
- });
4389
- if (iteration === maxIterations) {
4390
- throw err;
4391
- }
4392
- if (buildPhaseIdx >= 0) {
4393
- await executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, {
4394
- iteration,
4395
- verifyFailures: ["AI runner execution failed: " + err.message],
4396
- rawReport: ""
4397
- }, wtGitMap);
5133
+ function healStaleProgress(ctx, deps, wtPlan, startIdx) {
5134
+ const { issue, pipelineDef, record } = ctx;
5135
+ const currentProgress = wtPlan.readProgress();
5136
+ if (currentProgress) {
5137
+ let patched = false;
5138
+ for (let i = 0; i < startIdx; i++) {
5139
+ const prevSpec = pipelineDef.phases[i];
5140
+ const pp = currentProgress.phases[prevSpec.name];
5141
+ if (pp && pp.status !== "completed") {
5142
+ logger22.warn("Fixing stale phase progress", {
5143
+ iid: issue.iid,
5144
+ phase: prevSpec.name,
5145
+ was: pp.status,
5146
+ now: "completed"
5147
+ });
5148
+ pp.status = "completed";
5149
+ if (!pp.completedAt) {
5150
+ pp.completedAt = (/* @__PURE__ */ new Date()).toISOString();
5151
+ }
5152
+ patched = true;
4398
5153
  }
4399
- continue;
4400
- }
4401
- const report = verifyOutcome.data?.verifyReport;
4402
- const passed = report ? report.passed : true;
4403
- deps.eventBus.emitTyped("verify:iterationComplete", {
4404
- issueIid: issue.iid,
4405
- iteration,
4406
- passed,
4407
- failures: report?.failureReasons
4408
- });
4409
- if (passed) {
4410
- logger15.info("Verify-fix loop passed", {
4411
- iteration,
4412
- iid: issue.iid
4413
- });
4414
- return;
4415
- }
4416
- logger15.info("Verify failed, issues found", {
4417
- iteration,
4418
- iid: issue.iid,
4419
- failures: report?.failureReasons,
4420
- todolistStats: report?.todolistStats
4421
- });
4422
- if (iteration === maxIterations) {
4423
- deps.eventBus.emitTyped("verify:loopExhausted", {
4424
- issueIid: issue.iid,
4425
- totalIterations: iteration,
4426
- failures: report?.failureReasons ?? []
4427
- });
4428
- const failMsg = `Verify-fix loop exhausted after ${maxIterations} iterations. Remaining issues: ${report?.failureReasons?.join("; ") ?? "unknown"}`;
4429
- logger15.warn(failMsg, { iid: issue.iid });
4430
- throw new AIExecutionError("verify", failMsg, {
4431
- output: report?.rawReport ?? "",
4432
- exitCode: 0
4433
- });
4434
5154
  }
4435
- if (buildPhaseIdx >= 0) {
4436
- await executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, {
4437
- iteration,
4438
- verifyFailures: report?.failureReasons ?? [],
4439
- rawReport: report?.rawReport ?? ""
4440
- }, wtGitMap);
5155
+ if (patched) {
5156
+ wtPlan.writeProgress(currentProgress);
4441
5157
  }
4442
5158
  }
4443
- }
4444
- async function executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixContext, wtGitMap) {
4445
- const { issue, phaseCtx } = ctx;
4446
- const buildSpec = ctx.pipelineDef.phases[buildPhaseIdx];
4447
- logger15.info("Looping back to build for fix", {
4448
- iteration: fixContext.iteration,
4449
- iid: issue.iid,
4450
- failures: fixContext.verifyFailures
4451
- });
4452
- phaseCtx.fixContext = fixContext;
4453
- try {
4454
- const buildPhase = createPhase("build", deps.aiRunner, wtGit, wtPlan, deps.config);
4455
- if (wtGitMap) {
4456
- buildPhase.setWtGitMap(wtGitMap);
5159
+ if (record.phaseProgress) {
5160
+ for (let i = 0; i < startIdx; i++) {
5161
+ const prevSpec = pipelineDef.phases[i];
5162
+ const tp = record.phaseProgress[prevSpec.name];
5163
+ if (tp && tp.status !== "completed") {
5164
+ deps.tracker.updatePhaseProgress(issue.iid, prevSpec.name, {
5165
+ status: "completed",
5166
+ completedAt: tp.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
5167
+ });
5168
+ }
4457
5169
  }
4458
- await runPhaseWithLifecycle(
4459
- buildPhase,
4460
- phaseCtx,
4461
- buildSpec,
4462
- ctx,
4463
- deps,
4464
- wtGit,
4465
- wtPlan,
4466
- wtGitMap
4467
- );
4468
- } finally {
4469
- delete phaseCtx.fixContext;
4470
5170
  }
4471
5171
  }
4472
5172
 
4473
5173
  // src/orchestrator/steps/CompletionStep.ts
4474
- var logger16 = logger.child("CompletionStep");
5174
+ var logger23 = logger.child("CompletionStep");
4475
5175
  async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
4476
5176
  const { issue, branchName, wtCtx } = ctx;
4477
5177
  deps.emitProgress(issue.iid, "create_mr", t("orchestrator.createMrProgress"));
@@ -4503,7 +5203,7 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
4503
5203
  mrIid: void 0
4504
5204
  });
4505
5205
  } catch (err) {
4506
- logger16.warn("Failed to publish E2E screenshots", {
5206
+ logger23.warn("Failed to publish E2E screenshots", {
4507
5207
  iid: issue.iid,
4508
5208
  error: err.message
4509
5209
  });
@@ -4523,19 +5223,19 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
4523
5223
  await deps.claimer.releaseClaim(issue.id, issue.iid, "completed");
4524
5224
  }
4525
5225
  if (phaseResult.serversStarted && deps.config.preview.keepAfterComplete) {
4526
- logger16.info("Preview servers kept running after completion", { iid: issue.iid });
5226
+ logger23.info("Preview servers kept running after completion", { iid: issue.iid });
4527
5227
  } else {
4528
5228
  deps.stopPreviewServers(issue.iid);
4529
5229
  await deps.mainGitMutex.runExclusive(async () => {
4530
5230
  if (wtCtx.workspace) {
4531
5231
  await deps.workspaceManager.cleanupWorkspace(wtCtx.workspace);
4532
- logger16.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
5232
+ logger23.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
4533
5233
  } else {
4534
5234
  try {
4535
5235
  await deps.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
4536
- logger16.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
5236
+ logger23.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4537
5237
  } catch (err) {
4538
- logger16.warn("Failed to cleanup worktree", {
5238
+ logger23.warn("Failed to cleanup worktree", {
4539
5239
  dir: wtCtx.gitRootDir,
4540
5240
  error: err.message
4541
5241
  });
@@ -4543,16 +5243,16 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
4543
5243
  }
4544
5244
  });
4545
5245
  }
4546
- logger16.info("Issue processing completed", { iid: issue.iid });
5246
+ logger23.info("Issue processing completed", { iid: issue.iid });
4547
5247
  }
4548
5248
 
4549
5249
  // src/orchestrator/steps/FailureHandler.ts
4550
- var logger17 = logger.child("FailureHandler");
5250
+ var logger24 = logger.child("FailureHandler");
4551
5251
  async function handleFailure(err, issue, wtCtx, deps) {
4552
5252
  const errorMsg = err.message;
4553
5253
  const isRetryable = err instanceof AIExecutionError ? err.isRetryable : true;
4554
5254
  const wasActiveAtTimeout = err instanceof AIExecutionError && err.wasActiveAtTimeout;
4555
- logger17.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable, wasActiveAtTimeout });
5255
+ logger24.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable, wasActiveAtTimeout });
4556
5256
  metrics.incCounter("iaf_issues_failed_total");
4557
5257
  const currentRecord = deps.tracker.get(issue.iid);
4558
5258
  const failedAtState = currentRecord?.state || "pending" /* Pending */;
@@ -4565,11 +5265,11 @@ async function handleFailure(err, issue, wtCtx, deps) {
4565
5265
  }
4566
5266
  }
4567
5267
  if (wasReset) {
4568
- logger17.info("Issue was reset during processing, skipping failure marking", { iid: issue.iid });
5268
+ logger24.info("Issue was reset during processing, skipping failure marking", { iid: issue.iid });
4569
5269
  throw err;
4570
5270
  }
4571
5271
  if (failedAtState === "paused" /* Paused */) {
4572
- logger17.info("Issue was paused during processing, skipping failure handling", { iid: issue.iid });
5272
+ logger24.info("Issue was paused during processing, skipping failure handling", { iid: issue.iid });
4573
5273
  throw err;
4574
5274
  }
4575
5275
  try {
@@ -4591,7 +5291,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4591
5291
  try {
4592
5292
  await deps.claimer.releaseClaim(issue.id, issue.iid, "failed");
4593
5293
  } catch (releaseErr) {
4594
- logger17.warn("Failed to release lock on failure", {
5294
+ logger24.warn("Failed to release lock on failure", {
4595
5295
  iid: issue.iid,
4596
5296
  error: releaseErr.message
4597
5297
  });
@@ -4599,7 +5299,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4599
5299
  }
4600
5300
  deps.stopPreviewServers(issue.iid);
4601
5301
  const preservedDirs = wtCtx.workspace ? [wtCtx.workspace.primary.gitRootDir, ...wtCtx.workspace.associates.map((a) => a.gitRootDir)] : [wtCtx.gitRootDir];
4602
- logger17.info("Worktree(s) preserved for debugging", {
5302
+ logger24.info("Worktree(s) preserved for debugging", {
4603
5303
  primary: wtCtx.gitRootDir,
4604
5304
  all: preservedDirs
4605
5305
  });
@@ -4608,7 +5308,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4608
5308
 
4609
5309
  // src/orchestrator/PipelineOrchestrator.ts
4610
5310
  var execFileAsync2 = promisify2(execFile2);
4611
- var logger18 = logger.child("PipelineOrchestrator");
5311
+ var logger25 = logger.child("PipelineOrchestrator");
4612
5312
  var PipelineOrchestrator = class {
4613
5313
  config;
4614
5314
  gongfeng;
@@ -4638,7 +5338,7 @@ var PipelineOrchestrator = class {
4638
5338
  setAIRunner(runner) {
4639
5339
  this.aiRunner = runner;
4640
5340
  this.conflictResolver = new ConflictResolver(runner);
4641
- logger18.info("AIRunner replaced via hot-reload");
5341
+ logger25.info("AIRunner replaced via hot-reload");
4642
5342
  }
4643
5343
  constructor(config, gongfeng, git, aiRunner, tracker, supplementStore, mainGitMutex, eventBusInstance, wsConfig, tenantId, e2eAiRunner) {
4644
5344
  this.config = config;
@@ -4656,14 +5356,14 @@ var PipelineOrchestrator = class {
4656
5356
  this.pipelineDef = mode === "plan-mode" ? buildPlanModePipeline({ releaseEnabled: config.release.enabled, e2eEnabled: config.e2e.enabled }) : getPipelineDef(mode);
4657
5357
  registerPipeline(this.pipelineDef);
4658
5358
  this.lifecycleManager = createLifecycleManager(this.pipelineDef);
4659
- logger18.info("Pipeline mode resolved", { tenantId: this.tenantId, mode: this.pipelineDef.mode, aiMode: config.ai.mode });
5359
+ logger25.info("Pipeline mode resolved", { tenantId: this.tenantId, mode: this.pipelineDef.mode, aiMode: config.ai.mode });
4660
5360
  this.portAllocator = new PortAllocator({
4661
5361
  backendPortBase: config.e2e.backendPortBase,
4662
5362
  frontendPortBase: config.e2e.frontendPortBase
4663
5363
  });
4664
5364
  this.devServerManager = new DevServerManager();
4665
5365
  this.screenshotPublisher = new ScreenshotPublisher(gongfeng);
4666
- this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir : path11.join(config.project.worktreeBaseDir, this.tenantId);
5366
+ this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir : path12.join(config.project.worktreeBaseDir, this.tenantId);
4667
5367
  const effectiveWsConfig = wsConfig ?? buildSingleRepoWorkspace(config.project, config.gongfeng.projectPath);
4668
5368
  this.workspaceManager = new WorkspaceManager({
4669
5369
  wsConfig: effectiveWsConfig,
@@ -4672,7 +5372,7 @@ var PipelineOrchestrator = class {
4672
5372
  mainGitMutex: this.mainGitMutex,
4673
5373
  gongfengApiUrl: config.gongfeng.apiUrl
4674
5374
  });
4675
- logger18.info("WorkspaceManager initialized", {
5375
+ logger25.info("WorkspaceManager initialized", {
4676
5376
  tenantId: this.tenantId,
4677
5377
  primary: effectiveWsConfig.primary.name,
4678
5378
  associates: effectiveWsConfig.associates.map((a) => a.name)
@@ -4693,7 +5393,7 @@ var PipelineOrchestrator = class {
4693
5393
  this.claimer = claimer;
4694
5394
  }
4695
5395
  async cleanupStaleState() {
4696
- logger18.info("Cleaning up stale worktree state...");
5396
+ logger25.info("Cleaning up stale worktree state...");
4697
5397
  let cleaned = 0;
4698
5398
  const repoGitRoot = this.config.project.gitRootDir;
4699
5399
  try {
@@ -4702,11 +5402,11 @@ var PipelineOrchestrator = class {
4702
5402
  if (wtDir === repoGitRoot) continue;
4703
5403
  if (!wtDir.includes("/issue-")) continue;
4704
5404
  try {
4705
- const gitFile = path11.join(wtDir, ".git");
5405
+ const gitFile = path12.join(wtDir, ".git");
4706
5406
  try {
4707
- await fs9.access(gitFile);
5407
+ await fs11.access(gitFile);
4708
5408
  } catch {
4709
- logger18.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
5409
+ logger25.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
4710
5410
  await this.mainGit.worktreeRemove(wtDir, true).catch(() => {
4711
5411
  });
4712
5412
  await this.mainGit.worktreePrune();
@@ -4715,32 +5415,32 @@ var PipelineOrchestrator = class {
4715
5415
  }
4716
5416
  const wtGit = new GitOperations(wtDir);
4717
5417
  if (await wtGit.isRebaseInProgress()) {
4718
- logger18.warn("Aborting residual rebase in worktree", { dir: wtDir });
5418
+ logger25.warn("Aborting residual rebase in worktree", { dir: wtDir });
4719
5419
  await wtGit.rebaseAbort();
4720
5420
  cleaned++;
4721
5421
  }
4722
- const indexLock = path11.join(wtDir, ".git", "index.lock");
5422
+ const indexLock = path12.join(wtDir, ".git", "index.lock");
4723
5423
  try {
4724
- await fs9.unlink(indexLock);
4725
- logger18.warn("Removed stale index.lock", { path: indexLock });
5424
+ await fs11.unlink(indexLock);
5425
+ logger25.warn("Removed stale index.lock", { path: indexLock });
4726
5426
  cleaned++;
4727
5427
  } catch {
4728
5428
  }
4729
5429
  } catch (err) {
4730
- logger18.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
5430
+ logger25.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
4731
5431
  }
4732
5432
  }
4733
5433
  } catch (err) {
4734
- logger18.warn("Failed to list worktrees for cleanup", { error: err.message });
5434
+ logger25.warn("Failed to list worktrees for cleanup", { error: err.message });
4735
5435
  }
4736
- const mainIndexLock = path11.join(repoGitRoot, ".git", "index.lock");
5436
+ const mainIndexLock = path12.join(repoGitRoot, ".git", "index.lock");
4737
5437
  try {
4738
- await fs9.unlink(mainIndexLock);
4739
- logger18.warn("Removed stale main repo index.lock", { path: mainIndexLock });
5438
+ await fs11.unlink(mainIndexLock);
5439
+ logger25.warn("Removed stale main repo index.lock", { path: mainIndexLock });
4740
5440
  cleaned++;
4741
5441
  } catch {
4742
5442
  }
4743
- logger18.info("Stale state cleanup complete", { cleaned });
5443
+ logger25.info("Stale state cleanup complete", { cleaned });
4744
5444
  }
4745
5445
  /**
4746
5446
  * 重启后清理幽灵端口分配。
@@ -4753,7 +5453,7 @@ var PipelineOrchestrator = class {
4753
5453
  for (const record of this.tracker.getAll()) {
4754
5454
  if (record.ports) {
4755
5455
  const iid = getIid(record);
4756
- logger18.info("Clearing stale port allocation after restart", { iid, ports: record.ports });
5456
+ logger25.info("Clearing stale port allocation after restart", { iid, ports: record.ports });
4757
5457
  this.tracker.updateState(iid, record.state, {
4758
5458
  ports: void 0,
4759
5459
  previewStartedAt: void 0
@@ -4798,20 +5498,20 @@ var PipelineOrchestrator = class {
4798
5498
  }
4799
5499
  try {
4800
5500
  await this.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
4801
- logger18.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
5501
+ logger25.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4802
5502
  } catch (err) {
4803
- logger18.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
5503
+ logger25.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
4804
5504
  }
4805
5505
  }
4806
5506
  async installDependencies(workDir) {
4807
- logger18.info("Installing dependencies in worktree", { workDir });
5507
+ logger25.info("Installing dependencies in worktree", { workDir });
4808
5508
  const knowledge = getProjectKnowledge() ?? KNOWLEDGE_DEFAULTS;
4809
5509
  const pkgMgr = knowledge.toolchain.packageManager.toLowerCase();
4810
5510
  const isNodeProject = ["npm", "pnpm", "yarn", "bun"].some((m) => pkgMgr.includes(m));
4811
5511
  if (isNodeProject) {
4812
5512
  const ready = await this.ensureNodeModules(workDir);
4813
5513
  if (ready) {
4814
- logger18.info("node_modules ready \u2014 skipping install");
5514
+ logger25.info("node_modules ready \u2014 skipping install");
4815
5515
  return;
4816
5516
  }
4817
5517
  }
@@ -4824,10 +5524,10 @@ var PipelineOrchestrator = class {
4824
5524
  maxBuffer: 10 * 1024 * 1024,
4825
5525
  timeout: 3e5
4826
5526
  });
4827
- logger18.info("Dependencies installed");
5527
+ logger25.info("Dependencies installed");
4828
5528
  } catch (err) {
4829
5529
  if (fallbackCmd) {
4830
- logger18.warn(`${installCmd} failed, retrying with fallback command`, {
5530
+ logger25.warn(`${installCmd} failed, retrying with fallback command`, {
4831
5531
  error: err.message
4832
5532
  });
4833
5533
  const [fallbackBin, ...fallbackArgs] = fallbackCmd.split(/\s+/);
@@ -4837,45 +5537,45 @@ var PipelineOrchestrator = class {
4837
5537
  maxBuffer: 10 * 1024 * 1024,
4838
5538
  timeout: 3e5
4839
5539
  });
4840
- logger18.info("Dependencies installed (fallback)");
5540
+ logger25.info("Dependencies installed (fallback)");
4841
5541
  } catch (retryErr) {
4842
- logger18.warn("Fallback install also failed", {
5542
+ logger25.warn("Fallback install also failed", {
4843
5543
  error: retryErr.message
4844
5544
  });
4845
5545
  }
4846
5546
  } else {
4847
- logger18.warn("Install failed, no fallback configured", {
5547
+ logger25.warn("Install failed, no fallback configured", {
4848
5548
  error: err.message
4849
5549
  });
4850
5550
  }
4851
5551
  }
4852
5552
  }
4853
5553
  async ensureNodeModules(workDir) {
4854
- const targetBin = path11.join(workDir, "node_modules", ".bin");
5554
+ const targetBin = path12.join(workDir, "node_modules", ".bin");
4855
5555
  try {
4856
- await fs9.access(targetBin);
4857
- logger18.info("node_modules already complete (has .bin/)");
5556
+ await fs11.access(targetBin);
5557
+ logger25.info("node_modules already complete (has .bin/)");
4858
5558
  return true;
4859
5559
  } catch {
4860
5560
  }
4861
- const sourceNM = path11.join(this.config.project.workDir, "node_modules");
4862
- const targetNM = path11.join(workDir, "node_modules");
5561
+ const sourceNM = path12.join(this.config.project.workDir, "node_modules");
5562
+ const targetNM = path12.join(workDir, "node_modules");
4863
5563
  try {
4864
- await fs9.access(sourceNM);
5564
+ await fs11.access(sourceNM);
4865
5565
  } catch {
4866
- logger18.warn("Main repo node_modules not found, skipping seed", { sourceNM });
5566
+ logger25.warn("Main repo node_modules not found, skipping seed", { sourceNM });
4867
5567
  return false;
4868
5568
  }
4869
- logger18.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
5569
+ logger25.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
4870
5570
  try {
4871
5571
  await execFileAsync2("rm", ["-rf", targetNM], { timeout: 6e4 });
4872
5572
  await execFileAsync2("cp", ["-a", "--reflink=auto", sourceNM, targetNM], {
4873
5573
  timeout: 12e4
4874
5574
  });
4875
- logger18.info("node_modules seeded from main repo");
5575
+ logger25.info("node_modules seeded from main repo");
4876
5576
  return true;
4877
5577
  } catch (err) {
4878
- logger18.warn("Failed to seed node_modules from main repo", {
5578
+ logger25.warn("Failed to seed node_modules from main repo", {
4879
5579
  error: err.message
4880
5580
  });
4881
5581
  return false;
@@ -4885,16 +5585,16 @@ var PipelineOrchestrator = class {
4885
5585
  const record = this.tracker.get(issueIid);
4886
5586
  if (!record) throw new IssueNotFoundError(issueIid);
4887
5587
  const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4888
- logger18.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
5588
+ logger25.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
4889
5589
  this.pendingActions.set(issueIid, "restart");
4890
5590
  this.aiRunner.killByWorkDir(wtCtx.workDir);
4891
5591
  this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
4892
5592
  this.stopPreviewServers(issueIid);
4893
5593
  try {
4894
5594
  const deleted = await this.gongfeng.cleanupAgentNotes(getExternalId(record));
4895
- logger18.info("Agent notes cleaned up", { issueIid, deleted });
5595
+ logger25.info("Agent notes cleaned up", { issueIid, deleted });
4896
5596
  } catch (err) {
4897
- logger18.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
5597
+ logger25.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
4898
5598
  }
4899
5599
  await this.mainGitMutex.runExclusive(async () => {
4900
5600
  await this.cleanupWorktree(wtCtx);
@@ -4911,19 +5611,19 @@ var PipelineOrchestrator = class {
4911
5611
  await this.cleanupE2eOutputs(issueIid);
4912
5612
  this.tracker.resetFull(issueIid);
4913
5613
  this.pendingActions.delete(issueIid);
4914
- logger18.info("Issue restarted", { issueIid });
5614
+ logger25.info("Issue restarted", { issueIid });
4915
5615
  }
4916
5616
  async cancelIssue(issueIid) {
4917
5617
  const record = this.tracker.get(issueIid);
4918
5618
  if (!record) throw new IssueNotFoundError(issueIid);
4919
5619
  const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4920
- logger18.info("Cancelling issue \u2014 cleaning all resources", { issueIid, branchName: record.branchName });
5620
+ logger25.info("Cancelling issue \u2014 cleaning all resources", { issueIid, branchName: record.branchName });
4921
5621
  this.aiRunner.killByWorkDir(wtCtx.workDir);
4922
5622
  this.stopPreviewServers(issueIid);
4923
5623
  try {
4924
5624
  await this.gongfeng.removeLabelsWithPrefix(getExternalId(record), "auto-finish");
4925
5625
  } catch (err) {
4926
- logger18.warn("Failed to remove labels on cancel", { issueIid, error: err.message });
5626
+ logger25.warn("Failed to remove labels on cancel", { issueIid, error: err.message });
4927
5627
  }
4928
5628
  await this.mainGitMutex.runExclusive(async () => {
4929
5629
  await this.cleanupWorktree(wtCtx);
@@ -4940,7 +5640,7 @@ var PipelineOrchestrator = class {
4940
5640
  this.tracker.clearProcessingLock(issueIid);
4941
5641
  this.tracker.updateState(issueIid, "skipped" /* Skipped */);
4942
5642
  await this.cleanupE2eOutputs(issueIid);
4943
- logger18.info("Issue cancelled", { issueIid });
5643
+ logger25.info("Issue cancelled", { issueIid });
4944
5644
  }
4945
5645
  /**
4946
5646
  * Remove the E2E output directory for an issue: {uatVendorDir}/outputs/issue-{iid}
@@ -4948,13 +5648,13 @@ var PipelineOrchestrator = class {
4948
5648
  async cleanupE2eOutputs(issueIid) {
4949
5649
  const vendorDir = this.config.e2e.uatVendorDir;
4950
5650
  if (!vendorDir) return;
4951
- const abs = path11.isAbsolute(vendorDir) ? vendorDir : path11.resolve(this.config.project.workDir, vendorDir);
4952
- const outputDir = path11.join(abs, "outputs", `issue-${issueIid}`);
5651
+ const abs = path12.isAbsolute(vendorDir) ? vendorDir : path12.resolve(this.config.project.workDir, vendorDir);
5652
+ const outputDir = path12.join(abs, "outputs", `issue-${issueIid}`);
4953
5653
  try {
4954
- await fs9.rm(outputDir, { recursive: true, force: true });
4955
- logger18.info("E2E outputs cleaned up", { issueIid, dir: outputDir });
5654
+ await fs11.rm(outputDir, { recursive: true, force: true });
5655
+ logger25.info("E2E outputs cleaned up", { issueIid, dir: outputDir });
4956
5656
  } catch (err) {
4957
- logger18.warn("Failed to cleanup E2E outputs", { issueIid, dir: outputDir, error: err.message });
5657
+ logger25.warn("Failed to cleanup E2E outputs", { issueIid, dir: outputDir, error: err.message });
4958
5658
  }
4959
5659
  }
4960
5660
  /**
@@ -4966,10 +5666,10 @@ var PipelineOrchestrator = class {
4966
5666
  if (!this.workspaceManager) return;
4967
5667
  const wsRoot = this.workspaceManager.getWorkspaceRoot(issueIid);
4968
5668
  try {
4969
- await fs9.rm(wsRoot, { recursive: true, force: true });
4970
- logger18.info("Workspace root cleaned up", { issueIid, dir: wsRoot });
5669
+ await fs11.rm(wsRoot, { recursive: true, force: true });
5670
+ logger25.info("Workspace root cleaned up", { issueIid, dir: wsRoot });
4971
5671
  } catch (err) {
4972
- logger18.warn("Failed to cleanup workspace root", { issueIid, dir: wsRoot, error: err.message });
5672
+ logger25.warn("Failed to cleanup workspace root", { issueIid, dir: wsRoot, error: err.message });
4973
5673
  }
4974
5674
  }
4975
5675
  retryFromPhase(issueIid, phase) {
@@ -4985,7 +5685,7 @@ var PipelineOrchestrator = class {
4985
5685
  this.aiRunner.killByWorkDir(wtCtx.workDir);
4986
5686
  }
4987
5687
  this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
4988
- logger18.info("Retrying issue from phase", { issueIid, phase });
5688
+ logger25.info("Retrying issue from phase", { issueIid, phase });
4989
5689
  const ok = this.tracker.resetToPhase(issueIid, phase, issueDef);
4990
5690
  if (!ok) {
4991
5691
  throw new InvalidPhaseError(phase);
@@ -5012,7 +5712,7 @@ var PipelineOrchestrator = class {
5012
5712
  } else {
5013
5713
  this.tracker.pauseIssue(issueIid, record.currentPhase ?? "");
5014
5714
  }
5015
- logger18.info("Issue abort requested", { issueIid, state: record.state });
5715
+ logger25.info("Issue abort requested", { issueIid, state: record.state });
5016
5716
  }
5017
5717
  continueIssue(issueIid) {
5018
5718
  const record = this.tracker.get(issueIid);
@@ -5022,7 +5722,7 @@ var PipelineOrchestrator = class {
5022
5722
  }
5023
5723
  const issueDef = this.getIssueSpecificPipelineDef(record);
5024
5724
  this.tracker.resumeFromPause(issueIid, issueDef, false);
5025
- logger18.info("Issue continued from pause", { issueIid });
5725
+ logger25.info("Issue continued from pause", { issueIid });
5026
5726
  }
5027
5727
  redoPhase(issueIid) {
5028
5728
  const record = this.tracker.get(issueIid);
@@ -5066,7 +5766,7 @@ var PipelineOrchestrator = class {
5066
5766
  }
5067
5767
  this.eventBus.emitTyped("issue:redone", { issueIid });
5068
5768
  }
5069
- logger18.info("Issue redo requested", { issueIid, state: record.state });
5769
+ logger25.info("Issue redo requested", { issueIid, state: record.state });
5070
5770
  }
5071
5771
  /**
5072
5772
  * 处理中止/重做的共享逻辑:
@@ -5139,7 +5839,7 @@ var PipelineOrchestrator = class {
5139
5839
  async _processIssueImpl(issue) {
5140
5840
  const branchName = `${this.config.project.branchPrefix}-${issue.iid}`;
5141
5841
  const wtCtx = this.computeWorktreeContext(issue.iid, branchName);
5142
- logger18.info("Processing issue", {
5842
+ logger25.info("Processing issue", {
5143
5843
  iid: issue.iid,
5144
5844
  title: issue.title,
5145
5845
  branchName,
@@ -5246,7 +5946,7 @@ var PipelineOrchestrator = class {
5246
5946
  title,
5247
5947
  description
5248
5948
  });
5249
- logger18.info("Merge request created successfully", {
5949
+ logger25.info("Merge request created successfully", {
5250
5950
  iid: issue.iid,
5251
5951
  mrIid: mr.iid,
5252
5952
  mrUrl: mr.web_url
@@ -5254,7 +5954,7 @@ var PipelineOrchestrator = class {
5254
5954
  return { url: mr.web_url, iid: mr.iid };
5255
5955
  } catch (err) {
5256
5956
  const errorMsg = err.message;
5257
- logger18.warn("Failed to create merge request, trying to find existing one", {
5957
+ logger25.warn("Failed to create merge request, trying to find existing one", {
5258
5958
  iid: issue.iid,
5259
5959
  error: errorMsg
5260
5960
  });
@@ -5271,7 +5971,7 @@ var PipelineOrchestrator = class {
5271
5971
  this.config.project.baseBranch
5272
5972
  );
5273
5973
  if (existing) {
5274
- logger18.info("Found existing merge request", {
5974
+ logger25.info("Found existing merge request", {
5275
5975
  iid: issueIid,
5276
5976
  mrIid: existing.iid,
5277
5977
  mrUrl: existing.web_url
@@ -5279,7 +5979,7 @@ var PipelineOrchestrator = class {
5279
5979
  return { url: existing.web_url, iid: existing.iid };
5280
5980
  }
5281
5981
  } catch (findErr) {
5282
- logger18.warn("Failed to find existing merge request", {
5982
+ logger25.warn("Failed to find existing merge request", {
5283
5983
  iid: issueIid,
5284
5984
  error: findErr.message
5285
5985
  });
@@ -5324,7 +6024,7 @@ var PipelineOrchestrator = class {
5324
6024
  });
5325
6025
  return ports;
5326
6026
  } catch (err) {
5327
- logger18.error("Failed to start preview servers", {
6027
+ logger25.error("Failed to start preview servers", {
5328
6028
  iid: issue.iid,
5329
6029
  error: err.message
5330
6030
  });
@@ -5359,7 +6059,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5359
6059
  await this.mainGitMutex.runExclusive(async () => {
5360
6060
  await this.cleanupWorktree(wtCtx);
5361
6061
  });
5362
- logger18.info("Preview stopped and worktree cleaned", { iid: issueIid });
6062
+ logger25.info("Preview stopped and worktree cleaned", { iid: issueIid });
5363
6063
  }
5364
6064
  async markDeployed(issueIid) {
5365
6065
  const record = this.tracker.get(issueIid);
@@ -5376,7 +6076,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5376
6076
  try {
5377
6077
  await this.gongfeng.closeIssue(externalId);
5378
6078
  } catch (err) {
5379
- logger18.warn("Failed to close issue on Gongfeng", { iid: issueIid, error: err.message });
6079
+ logger25.warn("Failed to close issue on Gongfeng", { iid: issueIid, error: err.message });
5380
6080
  }
5381
6081
  try {
5382
6082
  const issue = await this.gongfeng.getIssueDetail(externalId);
@@ -5384,10 +6084,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5384
6084
  labels.push("auto-finish:deployed");
5385
6085
  await this.gongfeng.updateIssueLabels(externalId, labels);
5386
6086
  } catch (err) {
5387
- logger18.warn("Failed to update labels", { iid: issueIid, error: err.message });
6087
+ logger25.warn("Failed to update labels", { iid: issueIid, error: err.message });
5388
6088
  }
5389
6089
  this.tracker.updateState(issueIid, "deployed" /* Deployed */);
5390
- logger18.info("Issue marked as deployed", { iid: issueIid });
6090
+ logger25.info("Issue marked as deployed", { iid: issueIid });
5391
6091
  }
5392
6092
  async restartPreview(issueIid) {
5393
6093
  const record = this.tracker.get(issueIid);
@@ -5414,7 +6114,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5414
6114
  throw err;
5415
6115
  }
5416
6116
  const url = this.buildPreviewUrl(issueIid);
5417
- logger18.info("Preview restarted", { iid: issueIid, url });
6117
+ logger25.info("Preview restarted", { iid: issueIid, url });
5418
6118
  return url;
5419
6119
  }
5420
6120
  getPreviewHost() {
@@ -5447,7 +6147,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5447
6147
  if (!record) throw new IssueNotFoundError(issueIid);
5448
6148
  const baseBranch = this.config.project.baseBranch;
5449
6149
  const branchName = record.branchName;
5450
- logger18.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
6150
+ logger25.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
5451
6151
  this.tracker.updateState(issueIid, "resolving_conflict" /* ResolvingConflict */);
5452
6152
  this.eventBus.emitTyped("conflict:started", { issueIid });
5453
6153
  try {
@@ -5480,7 +6180,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5480
6180
  });
5481
6181
  }
5482
6182
  });
5483
- logger18.info("Running verification after conflict resolution", { issueIid });
6183
+ logger25.info("Running verification after conflict resolution", { issueIid });
5484
6184
  const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
5485
6185
  wtPlan.ensureDir();
5486
6186
  const verifyPhase = createPhase("verify", this.aiRunner, wtGit, wtPlan, this.config);
@@ -5518,10 +6218,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5518
6218
  } catch {
5519
6219
  }
5520
6220
  await this.commentOnMr(record.mrUrl, t("conflict.mrResolvedComment"));
5521
- logger18.info("Conflict resolution completed", { issueIid });
6221
+ logger25.info("Conflict resolution completed", { issueIid });
5522
6222
  } catch (err) {
5523
6223
  const errorMsg = err.message;
5524
- logger18.error("Conflict resolution failed", { issueIid, error: errorMsg });
6224
+ logger25.error("Conflict resolution failed", { issueIid, error: errorMsg });
5525
6225
  try {
5526
6226
  const wtGit = new GitOperations(wtCtx.gitRootDir);
5527
6227
  if (await wtGit.isRebaseInProgress()) {
@@ -5551,7 +6251,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5551
6251
  try {
5552
6252
  await this.gongfeng.createMergeRequestNote(mrIid, body);
5553
6253
  } catch (err) {
5554
- logger18.warn("Failed to comment on MR", { mrIid, error: err.message });
6254
+ logger25.warn("Failed to comment on MR", { mrIid, error: err.message });
5555
6255
  }
5556
6256
  }
5557
6257
  };
@@ -5627,7 +6327,7 @@ ${questions}
5627
6327
  }
5628
6328
 
5629
6329
  // src/services/BrainstormService.ts
5630
- var logger19 = logger.child("Brainstorm");
6330
+ var logger26 = logger.child("Brainstorm");
5631
6331
  function agentConfigToAIConfig(agentCfg, timeoutMs) {
5632
6332
  return {
5633
6333
  mode: agentCfg.mode,
@@ -5663,7 +6363,7 @@ var BrainstormService = class {
5663
6363
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
5664
6364
  };
5665
6365
  this.sessions.set(session.id, session);
5666
- logger19.info("Created brainstorm session", { sessionId: session.id });
6366
+ logger26.info("Created brainstorm session", { sessionId: session.id });
5667
6367
  return session;
5668
6368
  }
5669
6369
  getSession(id) {
@@ -5672,7 +6372,7 @@ var BrainstormService = class {
5672
6372
  async generate(sessionId, onEvent) {
5673
6373
  const session = this.requireSession(sessionId);
5674
6374
  session.status = "generating";
5675
- logger19.info("Generating SDD", { sessionId });
6375
+ logger26.info("Generating SDD", { sessionId });
5676
6376
  const prompt = buildGeneratePrompt(session.transcript);
5677
6377
  const result = await this.generatorRunner.run({
5678
6378
  prompt,
@@ -5698,7 +6398,7 @@ var BrainstormService = class {
5698
6398
  const session = this.requireSession(sessionId);
5699
6399
  const roundNum = session.rounds.length + 1;
5700
6400
  session.status = "reviewing";
5701
- logger19.info("Reviewing SDD", { sessionId, round: roundNum });
6401
+ logger26.info("Reviewing SDD", { sessionId, round: roundNum });
5702
6402
  onEvent?.({ type: "round:start", data: { round: roundNum, phase: "review" }, round: roundNum });
5703
6403
  const prompt = buildReviewPrompt(session.currentSdd, roundNum);
5704
6404
  const result = await this.reviewerRunner.run({
@@ -5731,7 +6431,7 @@ var BrainstormService = class {
5731
6431
  throw new Error("No review round to refine from");
5732
6432
  }
5733
6433
  session.status = "refining";
5734
- logger19.info("Refining SDD", { sessionId, round: currentRound.round });
6434
+ logger26.info("Refining SDD", { sessionId, round: currentRound.round });
5735
6435
  const prompt = buildRefinePrompt(currentRound.questions);
5736
6436
  const result = await this.generatorRunner.run({
5737
6437
  prompt,
@@ -5818,4 +6518,4 @@ export {
5818
6518
  PipelineOrchestrator,
5819
6519
  BrainstormService
5820
6520
  };
5821
- //# sourceMappingURL=chunk-IWSMQXBL.js.map
6521
+ //# sourceMappingURL=chunk-WZGEYHCC.js.map