@magnusekdahl/parallix 1.2.0 → 1.3.0

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 (127) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +737 -838
  3. package/lib/agents/agents.ts +930 -0
  4. package/lib/agents/claude-telemetry.js +149 -154
  5. package/lib/agents/claude-telemetry.ts +233 -0
  6. package/lib/agents/claude.js +121 -131
  7. package/lib/agents/claude.ts +156 -0
  8. package/lib/agents/codex-telemetry.js +176 -164
  9. package/lib/agents/codex-telemetry.ts +205 -0
  10. package/lib/agents/codex.js +168 -201
  11. package/lib/agents/codex.ts +236 -0
  12. package/lib/agents/limit-hit.js +216 -226
  13. package/lib/agents/limit-hit.ts +262 -0
  14. package/lib/agents/mistral-telemetry.js +7 -10
  15. package/lib/agents/mistral-telemetry.ts +44 -0
  16. package/lib/agents/mistral.js +40 -58
  17. package/lib/agents/mistral.ts +84 -0
  18. package/lib/agents/opencode-export.js +126 -127
  19. package/lib/agents/opencode-export.ts +160 -0
  20. package/lib/agents/opencode-telemetry.js +282 -261
  21. package/lib/agents/opencode-telemetry.ts +350 -0
  22. package/lib/agents/opencode.js +263 -305
  23. package/lib/agents/opencode.ts +370 -0
  24. package/lib/agents/stage-telemetry.js +23 -24
  25. package/lib/agents/stage-telemetry.ts +47 -0
  26. package/lib/commands/active.js +502 -579
  27. package/lib/commands/active.ts +665 -0
  28. package/lib/commands/checkpoint.js +98 -74
  29. package/lib/commands/checkpoint.ts +79 -0
  30. package/lib/commands/config.js +61 -31
  31. package/lib/commands/config.ts +47 -0
  32. package/lib/commands/coverage-gate.js +271 -246
  33. package/lib/commands/coverage-gate.ts +362 -0
  34. package/lib/commands/diff.js +140 -119
  35. package/lib/commands/diff.ts +125 -0
  36. package/lib/commands/draft.js +770 -923
  37. package/lib/commands/draft.ts +1021 -0
  38. package/lib/commands/handoff.js +588 -608
  39. package/lib/commands/handoff.ts +694 -0
  40. package/lib/commands/integrate.js +1332 -1463
  41. package/lib/commands/integrate.ts +1688 -0
  42. package/lib/commands/mission-start.js +279 -237
  43. package/lib/commands/mission-start.ts +263 -0
  44. package/lib/commands/rebase.js +606 -582
  45. package/lib/commands/rebase.ts +635 -0
  46. package/lib/commands/repair-handoff.js +181 -179
  47. package/lib/commands/repair-handoff.ts +228 -0
  48. package/lib/commands/resolve-conflict.js +121 -102
  49. package/lib/commands/resolve-conflict.ts +112 -0
  50. package/lib/commands/review.js +3 -4
  51. package/lib/commands/review.ts +14 -0
  52. package/lib/commands/setup-review.js +4 -5
  53. package/lib/commands/setup-review.ts +14 -0
  54. package/lib/commands/setup.js +3 -3
  55. package/lib/commands/setup.ts +2 -0
  56. package/lib/commands/stats-backfill.js +394 -359
  57. package/lib/commands/stats-backfill.ts +418 -0
  58. package/lib/commands/stats.js +1424 -1665
  59. package/lib/commands/stats.ts +2186 -0
  60. package/lib/commands/status.js +219 -200
  61. package/lib/commands/status.ts +219 -0
  62. package/lib/commands/verify.js +3 -1
  63. package/lib/commands/verify.ts +2 -0
  64. package/lib/core/fmt.js +140 -211
  65. package/lib/core/fmt.ts +189 -0
  66. package/lib/core/git.js +121 -102
  67. package/lib/core/git.ts +146 -0
  68. package/lib/core/gitignore.js +59 -106
  69. package/lib/core/gitignore.ts +103 -0
  70. package/lib/core/mission-utils.js +833 -875
  71. package/lib/core/mission-utils.ts +998 -0
  72. package/lib/core/nels.js +112 -127
  73. package/lib/core/nels.ts +189 -0
  74. package/lib/core/persistent-data-migration.js +247 -205
  75. package/lib/core/persistent-data-migration.ts +255 -0
  76. package/lib/core/product-config.js +400 -476
  77. package/lib/core/product-config.ts +518 -0
  78. package/lib/core/runtime-matrix.js +41 -87
  79. package/lib/core/runtime-matrix.ts +84 -0
  80. package/lib/core/spawn-tee.js +152 -192
  81. package/lib/core/spawn-tee.ts +202 -0
  82. package/lib/core/state-map.js +92 -92
  83. package/lib/core/state-map.ts +115 -0
  84. package/lib/core/storage.js +101 -129
  85. package/lib/core/storage.ts +182 -0
  86. package/lib/core/subagent-limit.js +15 -16
  87. package/lib/core/subagent-limit.ts +25 -0
  88. package/lib/core/verification.js +148 -138
  89. package/lib/core/verification.ts +200 -0
  90. package/lib/index.js +142 -59
  91. package/lib/index.ts +134 -0
  92. package/lib/review/rebase.js +142 -145
  93. package/lib/review/rebase.ts +188 -0
  94. package/lib/review/review-adapter.js +99 -87
  95. package/lib/review/review-adapter.ts +198 -0
  96. package/lib/review/review-artifacts.js +486 -629
  97. package/lib/review/review-artifacts.ts +622 -0
  98. package/lib/review/review-commands.js +1220 -1373
  99. package/lib/review/review-commands.ts +1579 -0
  100. package/lib/review/review-events.js +544 -886
  101. package/lib/review/review-events.ts +954 -0
  102. package/lib/review/review-loop.js +945 -1020
  103. package/lib/review/review-loop.ts +1174 -0
  104. package/lib/review/review-polling.js +86 -115
  105. package/lib/review/review-polling.ts +194 -0
  106. package/lib/review/review-prompts.js +154 -189
  107. package/lib/review/review-prompts.ts +182 -0
  108. package/lib/review/review-state.js +246 -225
  109. package/lib/review/review-state.ts +353 -0
  110. package/lib/review/review.js +63 -84
  111. package/lib/review/review.ts +138 -0
  112. package/lib/tools/backlog.js +694 -680
  113. package/lib/tools/backlog.ts +835 -0
  114. package/lib/tools/forgejo.js +1270 -1405
  115. package/lib/tools/forgejo.ts +1793 -0
  116. package/lib/tools/gatekeeper.js +106 -96
  117. package/lib/tools/gatekeeper.ts +124 -0
  118. package/lib/tools/redgreen.js +200 -181
  119. package/lib/tools/redgreen.ts +221 -0
  120. package/lib/tools/sessions.js +58 -52
  121. package/lib/tools/sessions.ts +81 -0
  122. package/lib/tools/setup-review.js +902 -975
  123. package/lib/tools/setup-review.ts +1152 -0
  124. package/package.json +7 -5
  125. package/prompts/draft.md +1 -0
  126. package/prompts/portfolio.md +1 -1
  127. package/px.js +226 -210
@@ -1,428 +1,423 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
- const child_process = require('child_process');
5
- const { detectRebaseState, git, getCurrentBranch } = require('../core/git');
6
- const { resolveTaskFile, getTaskStatus, setTaskStatus, completeTask, getTaskAssignee } = require('../tools/backlog');
7
- const { toVirtual, toActual } = require('../core/state-map');
8
- const { getPrStatus, getLatestReviewDecision, syncMerged, readToken, resolveTokenFile, resolveForgejoUser, resolveForgejoHome, isForgejoPath, listOpenPrsForSlug } = require('../tools/forgejo');
9
- const fmt = require('../core/fmt');
10
-
11
- const { buildAutonomousReviewMatrix, formatMatrixSummary } = require('../core/runtime-matrix');
12
- const { findMissionDir, resolveWorktree, findMissionArea, missionTitle, parseConflictFilesFromMergeOutput, getConflictFiles, inferSlug, updateGraphifyKnowledgeGraph, getPrimaryWorktree, getPrimaryBranch, conventionalWorktreePath, softResetTrailingBacklogNoise, findMissionDocInBranches, missionBranchName, missionDirForSlug, isMissionArtifact, resolveMissionBaseBranch, resolveBaseWorktree } = require('../core/mission-utils');
13
- const stats = require('./stats');
14
- const verification = require('../core/verification');
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ const node_fs_1 = __importDefault(require("node:fs"));
39
+ const node_os_1 = __importDefault(require("node:os"));
40
+ const node_path_1 = __importDefault(require("node:path"));
41
+ const node_child_process_1 = __importDefault(require("node:child_process"));
42
+ const git_js_1 = require("../core/git.js");
43
+ const backlog_js_1 = require("../tools/backlog.js");
44
+ const state_map_js_1 = require("../core/state-map.js");
45
+ const forgejo_js_1 = require("../tools/forgejo.js");
46
+ const fmt = __importStar(require("../core/fmt.js"));
47
+ const runtime_matrix_js_1 = require("../core/runtime-matrix.js");
48
+ const mission_utils_js_1 = require("../core/mission-utils.js");
49
+ const stats_js_1 = __importDefault(require("./stats.js"));
50
+ const verification = __importStar(require("../core/verification.js"));
15
51
  const { formatVerificationCommand } = verification;
16
- const { isForgejoReviewEnabled } = require('../core/product-config');
17
- const { readReviewState } = require('../review/review-state');
18
-
52
+ const product_config_js_1 = require("../core/product-config.js");
53
+ const review_state_js_1 = require("../review/review-state.js");
19
54
  const VARIANT_B_AUTOMATION_SUMMARY = 'Variant B automation: Backlog task closeout, worktree-path rewrite, squash commit with hook-enforced validation, Forgejo sync-merged, and mission worktree cleanup.';
20
-
21
55
  /** @type{{symptom: string, cause: string, fix: string}[]} */
22
56
  const SYNC_MERGED_DIAGNOSTICS = [
23
- { symptom: 'sync-merged reports generic failure; PR still open', cause: 'allow_manual_merge disabled or drifted off in Forgejo settings', fix: 'Enable "Allow manual merge" in Forgejo PR settings and retry.' },
24
- { symptom: 'POST /pulls/N/merge returns 405 or 500', cause: 'Forgejo API conflict or stale PR state', fix: 'px review <slug> to check if actually merged.' },
25
- { symptom: 'git push rejects mission branch with "stale info" or "fetch first"', cause: 'Local review/<branch> tracking is stale while sync-merged updates the PR branch to the landed squash commit', fix: 'Automated: sync-merged fetches review/<branch>, retries --force-with-lease, then force-pushes only if stale-info persists.' },
26
- { symptom: 'curl: (7) Failed to connect to localhost port 3300', cause: 'Forgejo service not reachable from current runtime (sandbox/network)', fix: 'Ensure Forgejo is running (scripts/start-runner.sh) or use FORGEJO_URL override if external.' },
27
- { symptom: 'PR marked merged but remote branch still exists', cause: 'Remote branch deletion failed (permissions or network)', fix: 'px review <slug> --close (to trigger cleanup; identity resolves from review-state).' }
57
+ { symptom: 'sync-merged reports generic failure; PR still open', cause: 'allow_manual_merge disabled or drifted off in Forgejo settings', fix: 'Enable "Allow manual merge" in Forgejo PR settings and retry.' },
58
+ { symptom: 'POST /pulls/N/merge returns 405 or 500', cause: 'Forgejo API conflict or stale PR state', fix: 'px review <slug> to check if actually merged.' },
59
+ { symptom: 'git push rejects mission branch with "stale info" or "fetch first"', cause: 'Local review/<branch> tracking is stale while sync-merged updates the PR branch to the landed squash commit', fix: 'Automated: sync-merged fetches review/<branch>, retries --force-with-lease, then force-pushes only if stale-info persists.' },
60
+ { symptom: 'curl: (7) Failed to connect to localhost port 3300', cause: 'Forgejo service not reachable from current runtime (sandbox/network)', fix: 'Ensure Forgejo is running (scripts/start-runner.sh) or use FORGEJO_URL override if external.' },
61
+ { symptom: 'PR marked merged but remote branch still exists', cause: 'Remote branch deletion failed (permissions or network)', fix: 'px review <slug> --close (to trigger cleanup; identity resolves from review-state).' }
28
62
  ];
29
-
30
63
  /** @extends{Error} */
31
- class IntegrationAbort extends Error {}
32
-
64
+ class IntegrationAbort extends Error {
65
+ }
33
66
  /** @param {{mission: string, implementer: string, pr_fix_rounds: string, classification: string, date: string}} row */
34
67
  function formatRecordedStatsRow(row) {
35
- return `${row.mission}: implementer=${row.implementer}, pr_fix_rounds=${row.pr_fix_rounds}, classification=${row.classification}, date=${row.date}`;
68
+ return `${row.mission}: implementer=${row.implementer}, pr_fix_rounds=${row.pr_fix_rounds}, classification=${row.classification}, date=${row.date}`;
36
69
  }
37
-
38
70
  /** @param {string} value */
39
71
  function shellQuote(value) {
40
- return `"${String(value).replace(/(["\\$`])/g, '\\$1')}"`;
72
+ return `"${String(value).replace(/(["\\$`])/g, '\\$1')}"`;
41
73
  }
42
-
43
74
  /** @param {string} rootDir @param {{commandRunner?: Function, log?: Function}} opts */
44
- function maybeUpdateGraphifyOnPrimary(rootDir = getPrimaryWorktree(), opts = {}) {
45
- return updateGraphifyKnowledgeGraph({
46
- rootDir,
47
- commandRunner: opts.commandRunner || ((/** @type{string} */ command, /** @type{string[]} */ args, /** @type{object} */ options) => require('../core/git').run(command, args, options)),
48
- log: opts.log,
49
- startMessage: `Updating graphify knowledge graph on ${getPrimaryBranch()}...`,
50
- failureHint: 'Continuing without blocking integration.'
51
- });
75
+ function maybeUpdateGraphifyOnPrimary(rootDir = (0, mission_utils_js_1.getPrimaryWorktree)(), opts = {}) {
76
+ return (0, mission_utils_js_1.updateGraphifyKnowledgeGraph)({
77
+ rootDir,
78
+ commandRunner: opts.commandRunner || ((/** @type{string} */ command, /** @type{string[]} */ args, /** @type{object} */ options) => (0, git_js_1.run)(command, args, options)),
79
+ log: opts.log,
80
+ startMessage: `Updating graphify knowledge graph on ${(0, mission_utils_js_1.getPrimaryBranch)()}...`,
81
+ failureHint: 'Continuing without blocking integration.'
82
+ });
52
83
  }
53
-
54
84
  /** @param {string} rootDir @param {{gitRunner?: Function}} opts */
55
85
  function prepareNoisePatchForSquash(rootDir, opts = {}) {
56
- const runner = opts.gitRunner || git;
57
- const diffResult = runner(['-C', rootDir, 'diff', '--cached', '--binary']);
58
- if (diffResult.status !== 0) {
59
- return {
60
- ok: false,
61
- error: [diffResult.stdout, diffResult.stderr].filter(Boolean).join('\n').trim() || 'Could not capture staged backlog-noise patch.'
62
- };
63
- }
64
-
65
- const patch = diffResult.stdout || '';
66
- if (!patch.trim()) {
67
- return { ok: true, patchPath: null, cleanup: () => {} };
68
- }
69
-
70
- const patchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'parallix-integrate-noise-'));
71
- const patchPath = path.join(patchDir, 'backlog-noise.patch');
72
- fs.writeFileSync(patchPath, patch, 'utf8');
73
-
74
- const resetResult = runner(['-C', rootDir, 'reset', '--hard', 'HEAD']);
75
- if (resetResult.status !== 0) {
86
+ const runner = (opts.gitRunner || git_js_1.git);
87
+ const diffResult = runner(['-C', rootDir, 'diff', '--cached', '--binary']);
88
+ if (diffResult.status !== 0) {
89
+ return {
90
+ ok: false,
91
+ error: [diffResult.stdout, diffResult.stderr].filter(Boolean).join('\n').trim() || 'Could not capture staged backlog-noise patch.'
92
+ };
93
+ }
94
+ const patch = diffResult.stdout || '';
95
+ if (!patch.trim()) {
96
+ return { ok: true, patchPath: null, cleanup: () => { } };
97
+ }
98
+ const patchDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'parallix-integrate-noise-'));
99
+ const patchPath = node_path_1.default.join(patchDir, 'backlog-noise.patch');
100
+ node_fs_1.default.writeFileSync(patchPath, patch, 'utf8');
101
+ const resetResult = runner(['-C', rootDir, 'reset', '--hard', 'HEAD']);
102
+ if (resetResult.status !== 0) {
103
+ return {
104
+ ok: false,
105
+ patchPath,
106
+ error: [resetResult.stdout, resetResult.stderr].filter(Boolean).join('\n').trim() || 'Could not restore a clean checkout after capturing backlog-noise patch.'
107
+ };
108
+ }
76
109
  return {
77
- ok: false,
78
- patchPath,
79
- error: [resetResult.stdout, resetResult.stderr].filter(Boolean).join('\n').trim() || 'Could not restore a clean checkout after capturing backlog-noise patch.'
110
+ ok: true,
111
+ patchPath,
112
+ cleanup: () => node_fs_1.default.rmSync(patchDir, { recursive: true, force: true })
80
113
  };
81
- }
82
-
83
- return {
84
- ok: true,
85
- patchPath,
86
- cleanup: () => fs.rmSync(patchDir, { recursive: true, force: true })
87
- };
88
114
  }
89
-
90
115
  /** @param {string} rootDir @param {string|null} patchPath @param {{gitRunner?: Function}} opts */
91
116
  function restoreNoisePatchAfterSquash(rootDir, patchPath, opts = {}) {
92
- if (!patchPath) {return { ok: true };}
93
- const runner = opts.gitRunner || git;
94
- const applyResult = runner(['-C', rootDir, 'apply', '--index', patchPath]);
95
- if (applyResult.status !== 0) {
96
- return {
97
- ok: false,
98
- error: [applyResult.stdout, applyResult.stderr].filter(Boolean).join('\n').trim() || 'Could not re-apply backlog-noise patch after squash merge.'
99
- };
100
- }
101
- return { ok: true };
117
+ if (!patchPath) {
118
+ return { ok: true };
119
+ }
120
+ const runner = (opts.gitRunner || git_js_1.git);
121
+ const applyResult = runner(['-C', rootDir, 'apply', '--index', patchPath]);
122
+ if (applyResult.status !== 0) {
123
+ return {
124
+ ok: false,
125
+ error: [applyResult.stdout, applyResult.stderr].filter(Boolean).join('\n').trim() || 'Could not re-apply backlog-noise patch after squash merge.'
126
+ };
127
+ }
128
+ return { ok: true };
102
129
  }
103
-
104
130
  /** @param {string|null} taskAssignee */
105
131
  function resolveForgejoUserForIntegration(taskAssignee) {
106
- // If taskAssignee is set, use it directly as the Forgejo user identity.
107
- // This ensures that even non-agent assignees (e.g., human users) have their
108
- // identity properly propagated through all Forgejo operations.
109
- if (taskAssignee) {
132
+ // If taskAssignee is set, use it directly as the Forgejo user identity.
133
+ // This ensures that even non-agent assignees (e.g., human users) have their
134
+ // identity properly propagated through all Forgejo operations.
135
+ if (taskAssignee) {
136
+ return {
137
+ forgejoUser: taskAssignee,
138
+ warning: null
139
+ };
140
+ }
141
+ // Fall back to environment variable or default only when no assignee is available
110
142
  return {
111
- forgejoUser: taskAssignee,
112
- warning: null
143
+ forgejoUser: (0, forgejo_js_1.resolveForgejoUser)(""),
144
+ warning: null
113
145
  };
114
- }
115
-
116
- // Fall back to environment variable or default only when no assignee is available
117
- return {
118
- forgejoUser: resolveForgejoUser(""),
119
- warning: null
120
- };
121
146
  }
122
-
123
147
  /** @param {string} rootDir @param {{gitRunner?: Function}} opts */
124
- function getUnresolvedIndexConflicts(rootDir = getPrimaryWorktree(), opts = {}) {
125
- const runner = opts.gitRunner || git;
126
- const result = runner(['-C', rootDir, 'ls-files', '-u']);
127
- if (result.status !== 0) {
148
+ function getUnresolvedIndexConflicts(rootDir = (0, mission_utils_js_1.getPrimaryWorktree)(), opts = {}) {
149
+ const runner = (opts.gitRunner || git_js_1.git);
150
+ const result = runner(['-C', rootDir, 'ls-files', '-u']);
151
+ if (result.status !== 0) {
152
+ return {
153
+ ok: false,
154
+ files: [],
155
+ error: [result.stdout, result.stderr].filter(Boolean).join('\n').trim()
156
+ };
157
+ }
158
+ const files = Array.from(new Set(result.stdout
159
+ .split('\n')
160
+ .map((line) => line.trim())
161
+ .filter(Boolean)
162
+ .map((line) => line.split('\t')[1])
163
+ .filter(Boolean)));
128
164
  return {
129
- ok: false,
130
- files: [],
131
- error: [result.stdout, result.stderr].filter(Boolean).join('\n').trim()
165
+ ok: true,
166
+ files
132
167
  };
133
- }
134
-
135
- const files = Array.from(new Set(
136
- result.stdout
137
- .split('\n')
138
- .map((/** @type{string} */ line) => line.trim())
139
- .filter(Boolean)
140
- .map((/** @type{string} */ line) => line.split('\t')[1])
141
- .filter(Boolean)
142
- ));
143
-
144
- return {
145
- ok: true,
146
- files
147
- };
148
168
  }
149
-
150
169
  function parseStashPopCollisionFiles(output = '') {
151
- return output
152
- .split('\n')
153
- .map(line => line.trim())
154
- .filter(line => /already exists, no checkout$/i.test(line))
155
- .map(line => line.replace(/\s+already exists, no checkout$/i, ''));
170
+ return output
171
+ .split('\n')
172
+ .map(line => line.trim())
173
+ .filter(line => /already exists, no checkout$/i.test(line))
174
+ .map(line => line.replace(/\s+already exists, no checkout$/i, ''));
156
175
  }
157
-
158
176
  /**
159
177
  * @param {string} slug
160
178
  * @param {{stdout: string, stderr: string, status: number}} restoreResult
161
179
  * @param {{rootDir?: string, gitRunner?: Function, getUnresolvedIndexConflictsFn?: Function}} opts
162
180
  */
163
181
  function reportStashPopFailure(slug, restoreResult, opts = {}) {
164
- const output = [restoreResult.stdout, restoreResult.stderr].filter(Boolean).join('\n').trim();
165
- const runner = opts.gitRunner || git;
166
- const rootDir = opts.rootDir || getPrimaryWorktree();
167
- const headResult = runner(['-C', rootDir, 'log', '-1', '--oneline']);
168
- const headLine = headResult.status === 0 ? headResult.stdout.trim() : '(unavailable)';
169
- const integrationLanded = headLine.includes(`${missionBranchName(slug, rootDir)}:`);
170
- const indexConflicts = (opts.getUnresolvedIndexConflictsFn || getUnresolvedIndexConflicts)(rootDir);
171
- const collisionFiles = parseStashPopCollisionFiles(output);
172
-
173
- fmt.log.fail('Could not restore the temporarily stashed local integration checkout changes.');
174
- if (integrationLanded) {
175
- fmt.log.fail(`Integration commit landed: ${headLine}`);
176
- } else {
177
- fmt.log.fail(`Integration landing not confirmed by HEAD: ${headLine}`);
178
- }
179
-
180
- if (indexConflicts.ok && indexConflicts.files.length > 0) {
181
- fmt.log.fail('Stash restore failure type: merge-conflict');
182
- indexConflicts.files.forEach((/** @type{string} */ file) => fmt.log.fail(` - ${file}`));
183
- fmt.log.fail('Recovery steps:');
184
- fmt.log.fail(` 1. cd ${rootDir}`);
185
- indexConflicts.files.forEach((/** @type{string} */ file) => {
186
- fmt.log.fail(` 2. Resolve ${file}, then run git add "${file}" or git rm "${file}"`);
187
- });
188
- fmt.log.fail(' 3. git status --short');
189
- fmt.log.fail(' 4. git stash drop');
190
- } else {
191
- fmt.log.fail('Stash restore failure type: file-collision');
192
- if (collisionFiles.length > 0) {
193
- collisionFiles.forEach((/** @type{string} */ file) => fmt.log.fail(` - ${file}`));
194
- }
195
- fmt.log.fail('Recovery steps:');
196
- fmt.log.fail(` 1. git -C ${rootDir} stash show --name-only stash@{0}`);
197
- if (collisionFiles.length > 0) {
198
- collisionFiles.forEach((/** @type{string} */ file) => {
199
- fmt.log.fail(` 2. mv ${path.join(rootDir, file)} ${path.join(rootDir, `${file}.pre-stash-pop`)}`);
200
- });
201
- fmt.log.fail(` 3. git -C ${rootDir} stash pop`);
202
- } else {
203
- fmt.log.fail(` 2. Inspect the latest stash-pop output and move or remove the colliding files in ${rootDir}`);
204
- fmt.log.fail(` 3. git -C ${rootDir} stash pop`);
205
- }
206
- }
207
-
208
- if (output) {
209
- fmt.log.fail('stash pop output:');
210
- fmt.log.fail(output);
211
- }
182
+ const output = [restoreResult.stdout, restoreResult.stderr].filter(Boolean).join('\n').trim();
183
+ const runner = (opts.gitRunner || git_js_1.git);
184
+ const rootDir = opts.rootDir || (0, mission_utils_js_1.getPrimaryWorktree)();
185
+ const headResult = runner(['-C', rootDir, 'log', '-1', '--oneline']);
186
+ const headLine = headResult.status === 0 ? headResult.stdout.trim() : '(unavailable)';
187
+ const integrationLanded = headLine.includes(`${(0, mission_utils_js_1.missionBranchName)(slug, rootDir)}:`);
188
+ const indexConflicts = (opts.getUnresolvedIndexConflictsFn || getUnresolvedIndexConflicts)(rootDir);
189
+ const collisionFiles = parseStashPopCollisionFiles(output);
190
+ fmt.log.fail('Could not restore the temporarily stashed local integration checkout changes.');
191
+ if (integrationLanded) {
192
+ fmt.log.fail(`Integration commit landed: ${headLine}`);
193
+ }
194
+ else {
195
+ fmt.log.fail(`Integration landing not confirmed by HEAD: ${headLine}`);
196
+ }
197
+ if (indexConflicts.ok && indexConflicts.files.length > 0) {
198
+ fmt.log.fail('Stash restore failure type: merge-conflict');
199
+ indexConflicts.files.forEach((file) => fmt.log.fail(` - ${file}`));
200
+ fmt.log.fail('Recovery steps:');
201
+ fmt.log.fail(` 1. cd ${rootDir}`);
202
+ indexConflicts.files.forEach((file) => {
203
+ fmt.log.fail(` 2. Resolve ${file}, then run git add "${file}" or git rm "${file}"`);
204
+ });
205
+ fmt.log.fail(' 3. git status --short');
206
+ fmt.log.fail(' 4. git stash drop');
207
+ }
208
+ else {
209
+ fmt.log.fail('Stash restore failure type: file-collision');
210
+ if (collisionFiles.length > 0) {
211
+ collisionFiles.forEach((file) => fmt.log.fail(` - ${file}`));
212
+ }
213
+ fmt.log.fail('Recovery steps:');
214
+ fmt.log.fail(` 1. git -C ${rootDir} stash show --name-only stash@{0}`);
215
+ if (collisionFiles.length > 0) {
216
+ collisionFiles.forEach((file) => {
217
+ fmt.log.fail(` 2. mv ${node_path_1.default.join(rootDir, file)} ${node_path_1.default.join(rootDir, `${file}.pre-stash-pop`)}`);
218
+ });
219
+ fmt.log.fail(` 3. git -C ${rootDir} stash pop`);
220
+ }
221
+ else {
222
+ fmt.log.fail(` 2. Inspect the latest stash-pop output and move or remove the colliding files in ${rootDir}`);
223
+ fmt.log.fail(` 3. git -C ${rootDir} stash pop`);
224
+ }
225
+ }
226
+ if (output) {
227
+ fmt.log.fail('stash pop output:');
228
+ fmt.log.fail(output);
229
+ }
212
230
  }
213
-
214
231
  function printDiagnosticTable() {
215
- fmt.log.info('\n--- Node sync-merged Diagnostic Table ---');
216
- fmt.log.info('| Symptom | Root Cause | Fix Command |');
217
- fmt.log.info('| :--- | :--- | :--- |');
218
- SYNC_MERGED_DIAGNOSTICS.forEach(d => {
219
- fmt.log.info(`| ${d.symptom} | ${d.cause} | ${d.fix} |`);
220
- });
221
- fmt.log.info('------------------------------------\n');
232
+ fmt.log.info('\n--- Node sync-merged Diagnostic Table ---');
233
+ fmt.log.info('| Symptom | Root Cause | Fix Command |');
234
+ fmt.log.info('| :--- | :--- | :--- |');
235
+ SYNC_MERGED_DIAGNOSTICS.forEach(d => {
236
+ fmt.log.info(`| ${d.symptom} | ${d.cause} | ${d.fix} |`);
237
+ });
238
+ fmt.log.info('------------------------------------\n');
222
239
  }
223
-
224
240
  /** @param {{statusCode?: number, error: string, raw?: string}} syncResult */
225
241
  function reportSyncMergedFailure(syncResult) {
226
- const detail = syncResult.statusCode ? ` (${syncResult.error}: ${syncResult.statusCode})` : ` (${syncResult.error})`;
227
- fmt.log.fail(`Forgejo sync-merged failed${detail}.`);
228
- if (syncResult.raw) {
229
- fmt.log.info('sync-merged raw output:');
230
- fmt.log.plainError(syncResult.raw);
231
- }
232
- printDiagnosticTable();
242
+ const detail = syncResult.statusCode ? ` (${syncResult.error}: ${syncResult.statusCode})` : ` (${syncResult.error})`;
243
+ fmt.log.fail(`Forgejo sync-merged failed${detail}.`);
244
+ if (syncResult.raw) {
245
+ fmt.log.info('sync-merged raw output:');
246
+ fmt.log.plainError(syncResult.raw);
247
+ }
248
+ printDiagnosticTable();
233
249
  }
234
-
235
250
  // Integration gates support
236
- const INTEGRATION_CONFIG_PATH = path.join(getPrimaryWorktree(), 'config', 'integration-pipelines.json');
237
-
251
+ const INTEGRATION_CONFIG_PATH = node_path_1.default.join((0, mission_utils_js_1.getPrimaryWorktree)(), 'config', 'integration-pipelines.json');
238
252
  /**
239
253
  * Detect which top-level areas have been modified in the mission branch vs primary branch
240
254
  * Returns an array of area names (e.g., ['server', 'auth-server', 'web-client'])
241
255
  */
242
256
  /** @param {string} slug @param {{gitRunner?: Function, rootDir?: string, baseBranch?: string|null}} opts */
243
257
  function detectChangedAreas(slug, opts = {}) {
244
- const primaryBranch = opts.baseBranch || getPrimaryBranch();
245
- const branch = `mission/${slug}`;
246
- const runner = opts.gitRunner || git;
247
-
248
- // Get changed files between primary branch and mission branch
249
- const diffResult = runner(['-C', opts.rootDir || getPrimaryWorktree(), 'diff', '--name-only', primaryBranch, branch, '--']);
250
-
251
- if (diffResult.status !== 0 && !diffResult.stdout.trim()) {
252
- // No changes or error - fall back to checking against HEAD in the worktree
253
- const worktree = resolveWorktree(slug) || conventionalWorktreePath(slug, opts.rootDir);
254
- if (fs.existsSync(worktree)) {
255
- const worktreeDiff = runner(['-C', worktree, 'diff', '--name-only', primaryBranch, 'HEAD', '--']);
256
- if (worktreeDiff.status === 0 && worktreeDiff.stdout.trim()) {
257
- return parseFilesToAreas(worktreeDiff.stdout);
258
- }
259
- }
260
- return [];
261
- }
262
-
263
- return parseFilesToAreas(diffResult.stdout);
258
+ const primaryBranch = opts.baseBranch || (0, mission_utils_js_1.getPrimaryBranch)();
259
+ const branch = `mission/${slug}`;
260
+ const runner = (opts.gitRunner || git_js_1.git);
261
+ // Get changed files between primary branch and mission branch
262
+ const diffResult = runner(['-C', opts.rootDir || (0, mission_utils_js_1.getPrimaryWorktree)(), 'diff', '--name-only', primaryBranch, branch, '--']);
263
+ if (diffResult.status !== 0 && !diffResult.stdout.trim()) {
264
+ // No changes or error - fall back to checking against HEAD in the worktree
265
+ const worktree = (0, mission_utils_js_1.resolveWorktree)(slug) || (0, mission_utils_js_1.conventionalWorktreePath)(slug, opts.rootDir);
266
+ if (node_fs_1.default.existsSync(worktree)) {
267
+ const worktreeDiff = runner(['-C', worktree, 'diff', '--name-only', primaryBranch, 'HEAD', '--']);
268
+ if (worktreeDiff.status === 0 && worktreeDiff.stdout.trim()) {
269
+ return parseFilesToAreas(worktreeDiff.stdout);
270
+ }
271
+ }
272
+ return [];
273
+ }
274
+ return parseFilesToAreas(diffResult.stdout);
264
275
  }
265
-
266
276
  /**
267
277
  * Parse a list of files (one per line) and extract the top-level area directories
268
278
  */
269
279
  /** @param {string} filesOutput */
270
280
  function parseFilesToAreas(filesOutput) {
271
- const areas = new Set();
272
- const knownAreas = ['lib', 'server', 'auth-server', 'web-client', 'docs', 'workflow', 'android', 'kubernetes'];
273
-
274
- filesOutput.split('\n').forEach((/** @type{string} */ file) => {
275
- file = file.trim();
276
- if (!file) {return;}
277
- const topDir = file.split('/')[0];
278
- if (knownAreas.includes(topDir)) {
279
- areas.add(topDir);
280
- }
281
- });
282
-
283
- return Array.from(areas);
281
+ const areas = new Set();
282
+ const knownAreas = ['lib', 'server', 'auth-server', 'web-client', 'docs', 'workflow', 'android', 'kubernetes'];
283
+ filesOutput.split('\n').forEach((file) => {
284
+ file = file.trim();
285
+ if (!file) {
286
+ return;
287
+ }
288
+ const topDir = file.split('/')[0];
289
+ if (knownAreas.includes(topDir)) {
290
+ areas.add(topDir);
291
+ }
292
+ });
293
+ return Array.from(areas);
284
294
  }
285
-
286
295
  /**
287
296
  * Load integration pipelines config from repo-side file
288
297
  */
289
298
  /** @param {{configPath?: string}} opts */
290
299
  function loadIntegrationConfig(opts = {}) {
291
- const configPath = opts.configPath || INTEGRATION_CONFIG_PATH;
292
- if (!fs.existsSync(configPath) || !fs.statSync(configPath).size) {
293
- return { ok: false, error: 'no config present' };
294
- }
295
-
296
- try {
297
- const content = fs.readFileSync(configPath, 'utf8');
298
- const config = JSON.parse(content);
299
- return { ok: true, config };
300
- } catch (/** @type{any} */ error) {
301
- return { ok: false, error: `invalid JSON: ${error.message}` };
302
- }
300
+ const configPath = opts.configPath || INTEGRATION_CONFIG_PATH;
301
+ if (!node_fs_1.default.existsSync(configPath) || !node_fs_1.default.statSync(configPath).size) {
302
+ return { ok: false, error: 'no config present' };
303
+ }
304
+ try {
305
+ const content = node_fs_1.default.readFileSync(configPath, 'utf8');
306
+ const config = JSON.parse(content);
307
+ return { ok: true, config };
308
+ }
309
+ catch ( /** @type{any} */error) {
310
+ return { ok: false, error: `invalid JSON: ${error.message}` };
311
+ }
303
312
  }
304
-
305
313
  /**
306
314
  * Get the integration gate plan for changed areas
307
315
  * Returns { gates: [{key, command, run_last}], changedAreas: [...] }
308
316
  */
309
317
  /** @param {string} slug @param {{runIntegrationGates?: boolean, gitRunner?: Function, dryRun?: boolean, configPath?: string}} opts */
310
318
  function getIntegrationGatePlan(slug, opts = {}) {
311
- // Check config
312
- const configResult = loadIntegrationConfig(opts);
313
- if (!configResult.ok) {
314
- if (opts.runIntegrationGates) {
315
- fmt.log.info(`integration-gates: ${configResult.error}, skipping`);
316
- }
317
- return { gates: [], changedAreas: [], configError: configResult.error };
318
- }
319
-
320
- const config = configResult.config;
321
- if (!config.gates || Object.keys(config.gates).length === 0) {
322
- if (opts.runIntegrationGates) {
323
- fmt.log.info('integration-gates: no gates defined in config, skipping');
324
- }
325
- return { gates: [], changedAreas: [], configError: 'no gates defined' };
326
- }
327
-
328
- // Detect changed areas (always detect for dry-run; for real run, detect only if runIntegrationGates)
329
- const changedAreas = (opts.runIntegrationGates || opts.dryRun) ? detectChangedAreas(slug, opts) : [];
330
-
331
- if (opts.runIntegrationGates && !opts.dryRun && changedAreas.length === 0) {
332
- fmt.log.info('integration-gates: no area changes detected, skipping');
333
- return { gates: [], changedAreas: [], configError: null };
334
- }
335
-
336
- // Build list of gates to run, preserving order with run_last handling
337
- const gateEntries = Object.entries(config.gates);
338
- const gates = gateEntries
339
- .map(([key, value]) => ({
340
- key,
341
- command: value.command,
342
- order: value.order || 0,
343
- run_last: value.run_last || false
319
+ // Check config
320
+ const configResult = loadIntegrationConfig(opts);
321
+ if (!configResult.ok) {
322
+ if (opts.runIntegrationGates) {
323
+ fmt.log.info(`integration-gates: ${configResult.error}, skipping`);
324
+ }
325
+ return { gates: [], changedAreas: [], configError: configResult.error };
326
+ }
327
+ const config = configResult.config;
328
+ if (!config.gates || Object.keys(config.gates).length === 0) {
329
+ if (opts.runIntegrationGates) {
330
+ fmt.log.info('integration-gates: no gates defined in config, skipping');
331
+ }
332
+ return { gates: [], changedAreas: [], configError: 'no gates defined' };
333
+ }
334
+ // Detect changed areas (always detect for dry-run; for real run, detect only if runIntegrationGates)
335
+ const changedAreas = (opts.runIntegrationGates || opts.dryRun) ? detectChangedAreas(slug, opts) : [];
336
+ if (opts.runIntegrationGates && !opts.dryRun && changedAreas.length === 0) {
337
+ fmt.log.info('integration-gates: no area changes detected, skipping');
338
+ return { gates: [], changedAreas: [], configError: null };
339
+ }
340
+ // Build list of gates to run, preserving order with run_last handling
341
+ const gateEntries = Object.entries(config.gates);
342
+ const gates = gateEntries
343
+ .map(([key, value]) => ({
344
+ key,
345
+ command: value.command,
346
+ order: value.order || 0,
347
+ run_last: value.run_last || false
344
348
  }));
345
-
346
- // Separate into run_last and non-run_last, sort each by order
347
- const nonRunLast = gates.filter(g => !g.run_last).sort((a, b) => a.order - b.order);
348
- const runLastGates = gates.filter(g => g.run_last).sort((a, b) => a.order - b.order);
349
-
350
- // Combine: non-run_last first (by order), then run_last (by order)
351
- const orderedGates = [...nonRunLast, ...runLastGates];
352
-
353
- // Filter to only relevant gates (based on changed areas)
354
- const relevantGates = [];
355
- for (const gate of orderedGates) {
356
- // Always include web-e2e if web-client changed
357
- if (gate.key === 'web-e2e') {
358
- if (changedAreas.includes('web-client') || changedAreas.length === 0) {
359
- relevantGates.push(gate);
360
- }
361
- } else if (changedAreas.includes(gate.key) || changedAreas.length === 0) {
362
- relevantGates.push(gate);
363
- }
364
- }
365
-
366
- return { gates: relevantGates, changedAreas, configError: null };
349
+ // Separate into run_last and non-run_last, sort each by order
350
+ const nonRunLast = gates.filter(g => !g.run_last).sort((a, b) => a.order - b.order);
351
+ const runLastGates = gates.filter(g => g.run_last).sort((a, b) => a.order - b.order);
352
+ // Combine: non-run_last first (by order), then run_last (by order)
353
+ const orderedGates = [...nonRunLast, ...runLastGates];
354
+ // Filter to only relevant gates (based on changed areas)
355
+ const relevantGates = [];
356
+ for (const gate of orderedGates) {
357
+ // Always include web-e2e if web-client changed
358
+ if (gate.key === 'web-e2e') {
359
+ if (changedAreas.includes('web-client') || changedAreas.length === 0) {
360
+ relevantGates.push(gate);
361
+ }
362
+ }
363
+ else if (changedAreas.includes(gate.key) || changedAreas.length === 0) {
364
+ relevantGates.push(gate);
365
+ }
366
+ }
367
+ return { gates: relevantGates, changedAreas, configError: null };
367
368
  }
368
-
369
369
  /**
370
370
  * Print the integration gate plan (for --dry-run)
371
371
  */
372
372
  /** @param{{key: string, command: string, order: number, run_last: boolean}[]} gates */
373
373
  function printIntegrationGatePlan(gates) {
374
- fmt.log.info('Integration gate plan:');
375
- for (const gate of gates) {
376
- fmt.log.info(` ${gate.key}: ${gate.command}`);
377
- }
374
+ fmt.log.info('Integration gate plan:');
375
+ for (const gate of gates) {
376
+ fmt.log.info(` ${gate.key}: ${gate.command}`);
377
+ }
378
378
  }
379
-
380
379
  /**
381
380
  * @param {string} slug
382
381
  * @param {{dryRun?: boolean, processEnv?: NodeJS.ProcessEnv, gitRunner?: Function, baseBranch?: string|null, baseWorktree?: string|null}} opts
383
382
  */
384
383
  function buildIntegrationGateEnv(slug, opts = {}) {
385
- /** @type {{[key: string]: string | undefined}} */
386
- const env = /** @type {{[key: string]: string | undefined}} */ ({
387
- ...opts.processEnv,
388
- INTEGRATE_DRY_RUN: opts.dryRun ? 'true' : 'false',
389
- INTEGRATE_CHANGED_AREAS: detectChangedAreas(slug, {
390
- gitRunner: opts.gitRunner,
391
- baseBranch: opts.baseBranch,
392
- ...(opts.baseWorktree ? { rootDir: opts.baseWorktree } : {})
393
- }).join(' ')
394
- });
395
-
396
- // Integration must always use the repo-side config, but the changed-area set
397
- // should be the one resolved for this mission branch, not rediscovered later.
398
- delete env.INTEGRATION_CONFIG_PATH;
399
- return env;
384
+ /** @type {{[key: string]: string | undefined}} */
385
+ const env = /** @type {{[key: string]: string | undefined}} */ ({
386
+ ...opts.processEnv,
387
+ INTEGRATE_DRY_RUN: opts.dryRun ? 'true' : 'false',
388
+ INTEGRATE_CHANGED_AREAS: detectChangedAreas(slug, {
389
+ gitRunner: opts.gitRunner,
390
+ baseBranch: opts.baseBranch,
391
+ ...(opts.baseWorktree ? { rootDir: opts.baseWorktree } : {})
392
+ }).join(' ')
393
+ });
394
+ // Integration must always use the repo-side config, but the changed-area set
395
+ // should be the one resolved for this mission branch, not rediscovered later.
396
+ delete env.INTEGRATION_CONFIG_PATH;
397
+ return env;
400
398
  }
401
-
402
399
  /**
403
400
  * @param {string} slug
404
401
  * @param {{baseWorktree?: string, resolveWorktreeFn?: Function, conventionalWorktreePathFn?: Function}} opts
405
402
  */
406
403
  function resolveIntegrationVerificationWorktree(slug, opts = {}) {
407
- const resolveFn = opts.resolveWorktreeFn || resolveWorktree;
408
- const convFn = opts.conventionalWorktreePathFn || conventionalWorktreePath;
409
- return resolveFn(slug, { cwd: opts.baseWorktree })
410
- || convFn(slug, opts.baseWorktree);
404
+ const resolveFn = (opts.resolveWorktreeFn || mission_utils_js_1.resolveWorktree);
405
+ const convFn = (opts.conventionalWorktreePathFn || mission_utils_js_1.conventionalWorktreePath);
406
+ return resolveFn(slug, { cwd: opts.baseWorktree })
407
+ || convFn(slug, opts.baseWorktree);
411
408
  }
412
-
413
409
  /**
414
410
  * @param {string} slug
415
411
  * @param {{baseWorktree?: string, resolveWorktreeFn?: Function, conventionalWorktreePathFn?: Function, formatVerificationCommandFn?: Function}} opts
416
412
  */
417
413
  function buildIntegrationVerificationInvocation(slug, opts = {}) {
418
- const cwd = resolveIntegrationVerificationWorktree(slug, opts);
419
- const fmtFn = opts.formatVerificationCommandFn || formatVerificationCommand;
420
- return {
421
- command: fmtFn('integrate', cwd),
422
- cwd
423
- };
414
+ const cwd = resolveIntegrationVerificationWorktree(slug, opts);
415
+ const fmtFn = (opts.formatVerificationCommandFn || formatVerificationCommand);
416
+ return {
417
+ command: fmtFn('integrate', cwd),
418
+ cwd
419
+ };
424
420
  }
425
-
426
421
  /**
427
422
  * Execute integration gates, aborting on first failure
428
423
  * Returns { ok: boolean, failedGate: string|null, error: string|null }
@@ -432,1069 +427,946 @@ function buildIntegrationVerificationInvocation(slug, opts = {}) {
432
427
  * @param {{commandRunner?: Function, rootDir?: string}} opts
433
428
  */
434
429
  async function executeIntegrationGates(gates, opts = {}) {
435
- const runner = opts.commandRunner || ((/** @type{string} */ cmd) => require('child_process').spawnSync(cmd, {
436
- shell: true,
437
- cwd: opts.rootDir || getPrimaryWorktree(),
438
- stdio: 'inherit'
439
- }));
440
-
441
- for (const gate of gates) {
442
- fmt.log.info(`Running integration gate: ${gate.key}...`);
443
- fmt.log.info(` Command: ${gate.command}`);
444
-
445
- // Execute the command using the injected runner
446
- const result = runner(gate.command, [], { cwd: opts.rootDir, stdio: 'inherit' });
447
-
448
- if (result.status !== 0) {
449
- fmt.log.fail(`Integration gate failed: ${gate.key}`);
450
- fmt.log.fail(` Command: ${gate.command}`);
451
- fmt.log.fail(` Exit code: ${result.status}`);
452
- return { ok: false, failedGate: gate.key, error: `Command exited with code ${result.status}` };
453
- }
454
-
455
- fmt.log.pass(`Integration gate passed: ${gate.key}`);
456
- }
457
-
458
- return { ok: true, failedGate: null, error: null };
430
+ const runner = (opts.commandRunner || ((/** @type{string} */ cmd) => node_child_process_1.default.spawnSync(cmd, {
431
+ shell: true,
432
+ cwd: opts.rootDir || (0, mission_utils_js_1.getPrimaryWorktree)(),
433
+ stdio: 'inherit'
434
+ })));
435
+ for (const gate of gates) {
436
+ fmt.log.info(`Running integration gate: ${gate.key}...`);
437
+ fmt.log.info(` Command: ${gate.command}`);
438
+ // Execute the command using the injected runner
439
+ const result = runner(gate.command, [], { cwd: opts.rootDir, stdio: 'inherit' });
440
+ if (result.status !== 0) {
441
+ fmt.log.fail(`Integration gate failed: ${gate.key}`);
442
+ fmt.log.fail(` Command: ${gate.command}`);
443
+ fmt.log.fail(` Exit code: ${result.status}`);
444
+ return { ok: false, failedGate: gate.key, error: `Command exited with code ${result.status}` };
445
+ }
446
+ fmt.log.pass(`Integration gate passed: ${gate.key}`);
447
+ }
448
+ return { ok: true, failedGate: null, error: null };
459
449
  }
460
-
461
450
  /** @param {string[]} args */
462
451
  async function integrate(args) {
463
- let exitCode = 0;
464
- /** @type{{created?: boolean, message?: string, rootDir?: string}|null} */
465
- let temporaryStash = null;
466
- let nextActionMessage = null;
467
- const flags = args.filter((/** @type{string} */ arg) => arg.startsWith('--'));
468
- const params = args.filter((/** @type{string} */ arg) => !arg.startsWith('--'));
469
- const explicitSlug = params[0];
470
- const slug = inferSlug(explicitSlug);
471
- const dryRun = flags.includes('--dry-run');
472
- const noIntegrationGates = flags.includes('--no-integration-gates');
473
-
474
- /** @type {{slug: string, branch: string, currentBranch: string, missionDir?: string, area: string, task: {ok: boolean, taskFile?: string, reason?: string, matches?: string[]}, taskStatus?: string, taskAssignee?: string|null, forgejoUser?: string|null, forgejoToken?: string|null, taskAssigneeWarning?: string|null, pr: {exists?: boolean, state?: string, number?: number, merged?: boolean, raw?: string}, siblingPrs: any[], approval: {ok?: boolean, error?: string, reviewState?: string, defaultUserApproved?: boolean, source?: string}, baseBranch?: string, baseWorktree?: string, mainBranch: string, mainDirtyEntries: string[], mainDirty: boolean}} */
475
- let context;
476
-
477
- if (process.env.FORGEJO_USER === 'gemini' || process.env.WORKFLOW_AGENT === 'gemini') {
478
- fmt.log.fail('Gemini is not authorized to run integrate. Post a handoff comment on the PR and stop.');
479
- process.exit(1);
480
- }
481
-
482
- if (!slug) {
483
- fmt.log.fail('Usage: px integrate [<slug>] [--dry-run] [--no-integration-gates]');
484
- process.exit(1);
485
- }
486
-
452
+ let exitCode = 0;
453
+ /** @type{{created?: boolean, message?: string, rootDir?: string}|null} */
454
+ let temporaryStash = null;
455
+ let nextActionMessage = null;
456
+ const flags = args.filter((arg) => arg.startsWith('--'));
457
+ const params = args.filter((arg) => !arg.startsWith('--'));
458
+ const explicitSlug = params[0];
459
+ const slug = (0, mission_utils_js_1.inferSlug)(explicitSlug);
460
+ const dryRun = flags.includes('--dry-run');
461
+ const noIntegrationGates = flags.includes('--no-integration-gates');
462
+ /** @type {{slug: string, branch: string, currentBranch: string, missionDir?: string, area: string, task: {ok: boolean, taskFile?: string, reason?: string, matches?: string[]}, taskStatus?: string, taskAssignee?: string|null, forgejoUser?: string|null, forgejoToken?: string|null, taskAssigneeWarning?: string|null, pr: {exists?: boolean, state?: string, number?: number, merged?: boolean, raw?: string}, siblingPrs: any[], approval: {ok?: boolean, error?: string, reviewState?: string, defaultUserApproved?: boolean, source?: string}, baseBranch?: string, baseWorktree?: string, mainBranch: string, mainDirtyEntries: string[], mainDirty: boolean}} */
463
+ let context;
464
+ if (process.env.FORGEJO_USER === 'gemini' || process.env.WORKFLOW_AGENT === 'gemini') {
465
+ fmt.log.fail('Gemini is not authorized to run integrate. Post a handoff comment on the PR and stop.');
466
+ process.exit(1);
467
+ }
468
+ if (!slug) {
469
+ fmt.log.fail('Usage: px integrate [<slug>] [--dry-run] [--no-integration-gates]');
470
+ process.exit(1);
471
+ }
487
472
  if (flags.includes('--no-gate')) {
488
- fmt.log.warn('integrate ignores --no-gate. The landed squash commit relies on the local git hooks for validation.');
473
+ fmt.log.warn('integrate ignores --no-gate. The landed squash commit relies on the local git hooks for validation.');
489
474
  }
490
-
491
475
  try {
492
- const executionDir = process.cwd();
493
- // @ts-expect-error buildIntegrationContext return type mismatch
494
- context = buildIntegrationContext(slug);
495
- // The integration target is the mission's recorded base worktree/branch.
496
- // For legacy missions these resolve to the primary worktree/branch, keeping
497
- // the merge/commit/sync path byte-identical to today.
498
- const baseWorktree = context.baseWorktree;
499
- const baseBranch = context.baseBranch;
500
- const { failures } = printIntegrationPreflight(context);
501
-
502
- if (failures.length > 0) {
503
- fmt.log.fail('\nIntegration preflight failed. Resolve the blockers above before running integrate.');
504
- throw new IntegrationAbort();
505
- }
506
-
507
- if (!noIntegrationGates) {
508
- const verification = buildIntegrationVerificationInvocation(slug, { baseWorktree });
509
- const env = buildIntegrationGateEnv(slug, { dryRun, baseBranch, baseWorktree });
510
-
511
- if (dryRun) {
512
- fmt.log.info('Running integration gates (dry-run)...');
513
- } else {
514
- fmt.log.info('Running integration gates...');
476
+ const executionDir = process.cwd();
477
+ context = buildIntegrationContext(slug);
478
+ // The integration target is the mission's recorded base worktree/branch.
479
+ // For legacy missions these resolve to the primary worktree/branch, keeping
480
+ // the merge/commit/sync path byte-identical to today.
481
+ const baseWorktree = context.baseWorktree;
482
+ const baseBranch = context.baseBranch;
483
+ const { failures } = printIntegrationPreflight(context);
484
+ if (failures.length > 0) {
485
+ fmt.log.fail('\nIntegration preflight failed. Resolve the blockers above before running integrate.');
486
+ throw new IntegrationAbort();
515
487
  }
516
-
517
- const result = child_process.spawnSync('bash', ['-lc', verification.command], {
518
- cwd: verification.cwd,
519
- env,
520
- stdio: 'inherit'
521
- });
522
-
523
- if (result.status !== 0) {
524
- fmt.log.fail(`\nIntegration gates failed with exit code ${result.status}`);
525
- fmt.log.fail('Aborting before merge.');
526
- throw new IntegrationAbort();
488
+ if (!noIntegrationGates) {
489
+ const verification = buildIntegrationVerificationInvocation(slug, { baseWorktree });
490
+ const env = buildIntegrationGateEnv(slug, { dryRun, baseBranch, baseWorktree });
491
+ if (dryRun) {
492
+ fmt.log.info('Running integration gates (dry-run)...');
493
+ }
494
+ else {
495
+ fmt.log.info('Running integration gates...');
496
+ }
497
+ const result = node_child_process_1.default.spawnSync('bash', ['-lc', verification.command], {
498
+ cwd: verification.cwd,
499
+ env,
500
+ stdio: 'inherit'
501
+ });
502
+ if (result.status !== 0) {
503
+ fmt.log.fail(`\nIntegration gates failed with exit code ${result.status}`);
504
+ fmt.log.fail('Aborting before merge.');
505
+ throw new IntegrationAbort();
506
+ }
507
+ if (!dryRun) {
508
+ fmt.log.pass('All integration gates passed.');
509
+ }
527
510
  }
528
-
529
- if (!dryRun) {
530
- fmt.log.pass('All integration gates passed.');
511
+ else {
512
+ fmt.log.info('Integration gates skipped via --no-integration-gates flag');
531
513
  }
532
- } else {
533
- fmt.log.info('Integration gates skipped via --no-integration-gates flag');
534
- }
535
-
536
- if (dryRun) {
537
- promoteTaskForIntegrationIfNeeded(context, { dryRun: true });
538
- fmt.log.pass('\nDry run complete. Integration preflight passed.');
539
- return;
540
- }
541
-
542
- promoteTaskForIntegrationIfNeeded(context);
543
-
544
- temporaryStash = stashMainCheckoutIfNeeded({
545
- slug,
546
- dirtyEntries: context.mainDirtyEntries,
547
- rootDir: /** @type{string} */(baseWorktree)
548
- });
549
-
550
- if (dryRun) {
551
- promoteTaskForIntegrationIfNeeded(context, { dryRun: true });
552
- fmt.log.pass('\nDry run complete. Integration preflight passed.');
553
- return;
554
- }
555
-
556
- promoteTaskForIntegrationIfNeeded(context);
557
-
558
- temporaryStash = stashMainCheckoutIfNeeded({
559
- slug,
560
- dirtyEntries: context.mainDirtyEntries,
561
- rootDir: baseWorktree
562
- });
563
-
564
- // End-context check: if we are in the worktree that is about to be deleted,
565
- // move the Node process to the base worktree to avoid being left in a ghost directory.
566
- const missionWorktree = conventionalWorktreePath(slug);
567
- if (process.cwd() === missionWorktree || process.cwd().startsWith(missionWorktree + '/')) {
568
- fmt.log.info(`Moving process directory to ${baseWorktree} before mission worktree deletion.`);
569
- // @ts-expect-error baseWorktree may be undefined
570
- process.chdir(baseWorktree);
571
- }
572
-
573
- // Variant Selection
574
- const branch = missionBranchName(slug, baseWorktree);
575
- const mainTitle = missionTitle(slug) || slug;
576
- const summary = mainTitle.replace(/\s+/g, ' ').trim();
577
- // @ts-expect-error context.task.taskFile may be undefined
578
- const mainTaskFile = context.task.taskFile.replace(executionDir, baseWorktree);
579
- const useVariantA = context.pr.merged;
580
- fmt.log.info(`Selecting integration variant: ${useVariantA ? 'Variant A (fast-path)' : 'Variant B (full squash-merge)'}`);
581
-
582
- if (useVariantA) {
583
- fmt.log.info(`Variant A: already merged on Forgejo; syncing local ${baseBranch} in base worktree ${baseWorktree} for closeout.`);
584
- const closeoutResult = finalizeVariantACloseout({
585
- slug,
586
- summary,
587
- mainTaskFile,
588
- rootDir: baseWorktree,
589
- baseBranch
590
- });
591
- if (!closeoutResult.ok) {
592
- fmt.log.fail(`Variant A closeout failed (${closeoutResult.error}).`);
593
- if (closeoutResult.detail) {
594
- fmt.log.fail(closeoutResult.detail);
514
+ if (dryRun) {
515
+ promoteTaskForIntegrationIfNeeded(context, { dryRun: true });
516
+ fmt.log.pass('\nDry run complete. Integration preflight passed.');
517
+ return;
595
518
  }
596
- if (closeoutResult.error === 'commit-failed') {
597
- fmt.log.info(`The closeout commit runs the repo git hooks. Fix the reported hook failure in ${baseWorktree} and retry integrate.`);
598
- fmt.log.info(`For this mission, the relevant verification command is ${formatVerificationCommand(context.area, baseWorktree)}`);
519
+ promoteTaskForIntegrationIfNeeded(context);
520
+ temporaryStash = stashMainCheckoutIfNeeded({
521
+ slug,
522
+ dirtyEntries: context.mainDirtyEntries,
523
+ rootDir: /** @type{string} */ (baseWorktree)
524
+ });
525
+ if (dryRun) {
526
+ promoteTaskForIntegrationIfNeeded(context, { dryRun: true });
527
+ fmt.log.pass('\nDry run complete. Integration preflight passed.');
528
+ return;
599
529
  }
600
- throw new IntegrationAbort();
601
- }
602
- nextActionMessage = `Next: cd ${baseWorktree}`;
603
- recordPostIntegrationStatsOrAbort(slug, { rootDir: baseWorktree });
604
- if (!cleanupMissionWorktree(slug)) {
605
- fmt.log.fail('Mission worktree cleanup failed for Variant A.');
606
- throw new IntegrationAbort();
607
- }
608
- maybeUpdateGraphifyOnPrimary(baseWorktree);
609
- fmt.log.pass('Variant A integration completed.');
610
- } else {
611
- fmt.log.info(`\nStep 1: Using base worktree ${baseWorktree} on ${baseBranch} as the squash-merge target...`);
612
-
613
- fmt.log.info(`Step 2: Checking merge conflicts against local ${baseBranch} in the base worktree...`);
614
- // @ts-expect-error baseWorktree may be undefined
615
- const dryMerge = git(['-C', baseWorktree, 'merge', '--no-commit', '--no-ff', branch]);
616
- // @ts-expect-error baseWorktree may be undefined
617
- const abortResult = git(['-C', baseWorktree, 'merge', '--abort']);
618
- // @ts-expect-error dryMerge/abortResult type mismatch
619
- if (abortResult.status !== 0 && !isNoMergeToAbortResult(abortResult)) {
620
- fmt.log.fail('Dry-run merge could not be aborted cleanly. Inspect the local integration checkout before retrying integrate.');
621
- throw new IntegrationAbort();
622
- }
623
- if (dryMerge.status !== 0) {
624
- // Before failing, check if a squash commit for this mission already landed on the primary branch
625
- // from a previous partial integration run (e.g. sync-merged failed after the commit was created).
626
- // @ts-expect-error baseWorktree may be undefined
627
- const existingSquash = findExistingSquashCommit(baseWorktree, slug);
628
- if (existingSquash) {
629
- fmt.log.warn(`Squash commit already exists on local ${baseBranch} from a previous partial integration (${existingSquash.slice(0, 12)}). Resuming from sync-merged step.`);
630
- const mergedCommit = existingSquash;
631
- if (isForgejoReviewEnabled(baseWorktree)) {
632
- fmt.log.info('Step 6 (resume): Syncing merged state to Forgejo...');
633
- const syncResult = syncMerged(branch, mergedCommit, {
634
- rootDir: baseWorktree,
635
- forgejoUser: context.forgejoUser,
636
- token: context.forgejoToken,
637
- baseBranch: context.baseBranch
530
+ promoteTaskForIntegrationIfNeeded(context);
531
+ temporaryStash = stashMainCheckoutIfNeeded({
532
+ slug,
533
+ dirtyEntries: context.mainDirtyEntries,
534
+ rootDir: baseWorktree
535
+ });
536
+ // End-context check: if we are in the worktree that is about to be deleted,
537
+ // move the Node process to the base worktree to avoid being left in a ghost directory.
538
+ const missionWorktree = (0, mission_utils_js_1.conventionalWorktreePath)(slug);
539
+ if (process.cwd() === missionWorktree || process.cwd().startsWith(missionWorktree + '/')) {
540
+ fmt.log.info(`Moving process directory to ${baseWorktree} before mission worktree deletion.`);
541
+ process.chdir(baseWorktree);
542
+ }
543
+ // Variant Selection
544
+ const branch = (0, mission_utils_js_1.missionBranchName)(slug, baseWorktree);
545
+ const mainTitle = (0, mission_utils_js_1.missionTitle)(slug) || slug;
546
+ const summary = mainTitle.replace(/\s+/g, ' ').trim();
547
+ const mainTaskFile = context.task.taskFile.replace(executionDir, baseWorktree);
548
+ const useVariantA = context.pr.merged;
549
+ fmt.log.info(`Selecting integration variant: ${useVariantA ? 'Variant A (fast-path)' : 'Variant B (full squash-merge)'}`);
550
+ if (useVariantA) {
551
+ fmt.log.info(`Variant A: already merged on Forgejo; syncing local ${baseBranch} in base worktree ${baseWorktree} for closeout.`);
552
+ const closeoutResult = finalizeVariantACloseout({
553
+ slug,
554
+ summary,
555
+ mainTaskFile,
556
+ rootDir: baseWorktree,
557
+ baseBranch
638
558
  });
639
- if (!syncResult.ok) {
640
- // @ts-expect-error reportSyncMergedFailure expects {statusCode, error}
641
- reportSyncMergedFailure(syncResult);
642
- throw new IntegrationAbort();
559
+ if (!closeoutResult.ok) {
560
+ fmt.log.fail(`Variant A closeout failed (${closeoutResult.error}).`);
561
+ if (closeoutResult.detail) {
562
+ fmt.log.fail(closeoutResult.detail);
563
+ }
564
+ if (closeoutResult.error === 'commit-failed') {
565
+ fmt.log.info(`The closeout commit runs the repo git hooks. Fix the reported hook failure in ${baseWorktree} and retry integrate.`);
566
+ fmt.log.info(`For this mission, the relevant verification command is ${formatVerificationCommand(context.area, baseWorktree)}`);
567
+ }
568
+ throw new IntegrationAbort();
643
569
  }
644
- } else {
645
- fmt.log.info('Step 6 (resume): Skipping Forgejo sync (review provider is not forgejo).');
646
- }
647
- nextActionMessage = `Next: cd ${baseWorktree}`;
648
- recordPostIntegrationStatsOrAbort(slug, { rootDir: baseWorktree });
649
- fmt.log.info('Step 7 (resume): Cleaning up the local mission worktree...');
650
- if (!cleanupMissionWorktree(slug)) {
651
- fmt.log.fail('Mission worktree cleanup failed.');
652
- throw new IntegrationAbort();
653
- }
654
- maybeUpdateGraphifyOnPrimary(baseWorktree);
655
- fmt.log.pass('Integration completed successfully (resumed from partial state).');
656
- } else {
657
- fmt.log.fail('Merge conflicts detected. Rebase the mission branch before integrating.');
658
- const conflictOutput = [/** @type {any} */ (dryMerge).stdout, /** @type {any} */ (dryMerge).stderr].filter(Boolean).join('\n');
659
- const conflictFiles = parseConflictFilesFromMergeOutput(conflictOutput);
660
- if (conflictFiles.length > 0) {
661
- fmt.log.info(`Conflicting files (${conflictFiles.length}):`);
662
- conflictFiles.forEach(f => fmt.log.info(` - ${f}`));
663
- }
664
- fmt.log.info('Conflict helper path:');
665
- for (const line of formatMatrixSummary(buildAutonomousReviewMatrix())) {
666
- fmt.log.info(line);
667
- }
668
- for (const line of buildConflictResolutionPrompt(slug, context.area, { baseBranch: context.baseBranch || '' })) {
669
- fmt.log.info(line);
670
- }
671
- throw new IntegrationAbort();
570
+ if (node_fs_1.default.existsSync(baseWorktree)) {
571
+ nextActionMessage = `Next: cd ${baseWorktree}`;
572
+ }
573
+ recordPostIntegrationStatsOrAbort(slug, { rootDir: baseWorktree });
574
+ if (!cleanupMissionWorktree(slug)) {
575
+ fmt.log.fail('Mission worktree cleanup failed for Variant A.');
576
+ throw new IntegrationAbort();
577
+ }
578
+ maybeUpdateGraphifyOnPrimary(baseWorktree);
579
+ fmt.log.pass('Variant A integration completed.');
672
580
  }
673
- } else {
674
- fmt.log.info('Step 3: Squash-merging the mission branch...');
675
- let noisePatchState = null;
676
- // @ts-expect-error baseWorktree may be undefined
677
- if (softResetTrailingBacklogNoise(baseWorktree, git)) {
678
- // @ts-expect-error baseWorktree may be undefined
679
- noisePatchState = prepareNoisePatchForSquash(baseWorktree, { gitRunner: git });
680
- if (!noisePatchState.ok) {
681
- fmt.log.fail('Could not preserve trailing backlog noise before squash merge.');
682
- if (noisePatchState.error) {
683
- fmt.log.fail(noisePatchState.error);
581
+ else {
582
+ fmt.log.info(`\nStep 1: Using base worktree ${baseWorktree} on ${baseBranch} as the squash-merge target...`);
583
+ fmt.log.info(`Step 2: Checking merge conflicts against local ${baseBranch} in the base worktree...`);
584
+ const dryMerge = (0, git_js_1.git)(['-C', baseWorktree, 'merge', '--no-commit', '--no-ff', branch]);
585
+ const abortResult = (0, git_js_1.git)(['-C', baseWorktree, 'merge', '--abort']);
586
+ if (abortResult.status !== 0 && !isNoMergeToAbortResult(abortResult)) {
587
+ fmt.log.fail('Dry-run merge could not be aborted cleanly. Inspect the local integration checkout before retrying integrate.');
588
+ throw new IntegrationAbort();
684
589
  }
685
- throw new IntegrationAbort();
686
- }
590
+ if (dryMerge.status !== 0) {
591
+ // Before failing, check if a squash commit for this mission already landed on the primary branch
592
+ // from a previous partial integration run (e.g. sync-merged failed after the commit was created).
593
+ const existingSquash = findExistingSquashCommit(baseWorktree, slug);
594
+ if (existingSquash) {
595
+ fmt.log.warn(`Squash commit already exists on local ${baseBranch} from a previous partial integration (${existingSquash.slice(0, 12)}). Resuming from sync-merged step.`);
596
+ const mergedCommit = existingSquash;
597
+ if ((0, product_config_js_1.isForgejoReviewEnabled)(baseWorktree)) {
598
+ fmt.log.info('Step 6 (resume): Syncing merged state to Forgejo...');
599
+ const syncResult = (0, forgejo_js_1.syncMerged)(branch, mergedCommit, {
600
+ rootDir: baseWorktree,
601
+ forgejoUser: context.forgejoUser,
602
+ token: context.forgejoToken,
603
+ baseBranch: context.baseBranch
604
+ });
605
+ if (!syncResult.ok) {
606
+ reportSyncMergedFailure(syncResult);
607
+ throw new IntegrationAbort();
608
+ }
609
+ }
610
+ else {
611
+ fmt.log.info('Step 6 (resume): Skipping Forgejo sync (review provider is not forgejo).');
612
+ }
613
+ if (node_fs_1.default.existsSync(baseWorktree)) {
614
+ nextActionMessage = `Next: cd ${baseWorktree}`;
615
+ }
616
+ recordPostIntegrationStatsOrAbort(slug, { rootDir: baseWorktree });
617
+ fmt.log.info('Step 7 (resume): Cleaning up the local mission worktree...');
618
+ if (!cleanupMissionWorktree(slug)) {
619
+ fmt.log.fail('Mission worktree cleanup failed.');
620
+ throw new IntegrationAbort();
621
+ }
622
+ maybeUpdateGraphifyOnPrimary(baseWorktree);
623
+ fmt.log.pass('Integration completed successfully (resumed from partial state).');
624
+ }
625
+ else {
626
+ fmt.log.fail('Merge conflicts detected. Rebase the mission branch before integrating.');
627
+ const conflictOutput = [/** @type {any} */ (dryMerge).stdout, /** @type {any} */ (dryMerge).stderr].filter(Boolean).join('\n');
628
+ const conflictFiles = (0, mission_utils_js_1.parseConflictFilesFromMergeOutput)(conflictOutput);
629
+ if (conflictFiles.length > 0) {
630
+ fmt.log.info(`Conflicting files (${conflictFiles.length}):`);
631
+ conflictFiles.forEach(f => fmt.log.info(` - ${f}`));
632
+ }
633
+ fmt.log.info('Conflict helper path:');
634
+ for (const line of (0, runtime_matrix_js_1.formatMatrixSummary)((0, runtime_matrix_js_1.buildAutonomousReviewMatrix)())) {
635
+ fmt.log.info(line);
636
+ }
637
+ for (const line of buildConflictResolutionPrompt(slug, context.area, { baseBranch: context.baseBranch || '' })) {
638
+ fmt.log.info(line);
639
+ }
640
+ throw new IntegrationAbort();
641
+ }
642
+ }
643
+ else {
644
+ fmt.log.info('Step 3: Squash-merging the mission branch...');
645
+ let noisePatchState = null;
646
+ if ((0, mission_utils_js_1.softResetTrailingBacklogNoise)(baseWorktree, git_js_1.git)) {
647
+ noisePatchState = prepareNoisePatchForSquash(baseWorktree, { gitRunner: git_js_1.git });
648
+ if (!noisePatchState.ok) {
649
+ fmt.log.fail('Could not preserve trailing backlog noise before squash merge.');
650
+ if (noisePatchState.error) {
651
+ fmt.log.fail(noisePatchState.error);
652
+ }
653
+ throw new IntegrationAbort();
654
+ }
655
+ }
656
+ const squashResult = (0, git_js_1.git)(['-C', baseWorktree, 'merge', '--squash', branch]);
657
+ if (squashResult.status !== 0) {
658
+ noisePatchState?.cleanup?.();
659
+ fmt.log.fail('Squash merge failed.');
660
+ throw new IntegrationAbort();
661
+ }
662
+ if (noisePatchState?.patchPath) {
663
+ const restoreNoiseResult = restoreNoisePatchAfterSquash(/** @type {string} */ (baseWorktree), noisePatchState.patchPath, { gitRunner: git_js_1.git });
664
+ noisePatchState.cleanup();
665
+ if (!restoreNoiseResult.ok) {
666
+ fmt.log.fail('Could not restore trailing backlog noise after squash merge.');
667
+ if (restoreNoiseResult.error) {
668
+ fmt.log.fail(restoreNoiseResult.error);
669
+ }
670
+ throw new IntegrationAbort();
671
+ }
672
+ }
673
+ fmt.log.info('Step 4: Final closeout checks in the local integration checkout...');
674
+ if (node_fs_1.default.existsSync(mainTaskFile)) {
675
+ (0, backlog_js_1.completeTask)(slug, baseWorktree);
676
+ // Re-resolve because it moved
677
+ const updatedResolution = (0, backlog_js_1.resolveTaskFile)(slug, baseWorktree);
678
+ if (updatedResolution.ok) {
679
+ rewriteWorktreePaths(updatedResolution.taskFile, slug, { rootDir: baseWorktree });
680
+ }
681
+ }
682
+ (0, git_js_1.git)(['-C', baseWorktree, 'add', '-A']);
683
+ fmt.log.info('Step 5: Creating the landed squash commit in the local integration checkout...');
684
+ const commitResult = (0, git_js_1.git)([
685
+ '-C',
686
+ /** @type {string} */ (baseWorktree),
687
+ 'commit',
688
+ '-m',
689
+ `${branch}: ${summary}`
690
+ ]);
691
+ if (commitResult.status !== 0) {
692
+ const output = [commitResult.stdout, commitResult.stderr].filter(Boolean).join('\n').trim();
693
+ fmt.log.fail('Could not create the squash commit in the local integration checkout.');
694
+ if (output) {
695
+ fmt.log.fail(output);
696
+ }
697
+ fmt.log.info(`The squash commit runs the repo git hooks. Fix the reported hook failure in ${baseWorktree} and retry integrate.`);
698
+ fmt.log.info(`For this mission, the relevant verification command is ${formatVerificationCommand(context.area, baseWorktree)}`);
699
+ throw new IntegrationAbort();
700
+ }
701
+ const proofResult = verification.captureVerifiedTreeProof(context.area, baseWorktree, {
702
+ gitRunner: git_js_1.git,
703
+ runFn: /** @type {Function} */ (node_child_process_1.default.spawnSync)
704
+ });
705
+ if (!(proofResult).ok) {
706
+ fmt.log.fail(`Could not verify the exact tree being published: ${ /** @type {any} */(proofResult).error}`);
707
+ throw new IntegrationAbort();
708
+ }
709
+ const proof = /** @type {any} */ (proofResult).proof;
710
+ const mergedCommit = (0, git_js_1.git)(['-C', baseWorktree, 'rev-parse', 'HEAD']).stdout.trim();
711
+ const proofCheck = verification.assertVerifiedTreeProof(proof, baseWorktree, { gitRunner: git_js_1.git });
712
+ if (!proofCheck.ok) {
713
+ fmt.log.fail(`Verification proof is stale for the publish tree: ${ /** @type {any} */(proofCheck).error}`);
714
+ throw new IntegrationAbort();
715
+ }
716
+ if ((0, product_config_js_1.isForgejoReviewEnabled)(baseWorktree)) {
717
+ fmt.log.info('Step 6: Syncing merged state to Forgejo...');
718
+ const syncResult = (0, forgejo_js_1.syncMerged)(branch, mergedCommit, {
719
+ rootDir: baseWorktree,
720
+ forgejoUser: context.forgejoUser,
721
+ token: context.forgejoToken,
722
+ baseBranch: context.baseBranch
723
+ });
724
+ if (!syncResult.ok) {
725
+ reportSyncMergedFailure(syncResult);
726
+ throw new IntegrationAbort();
727
+ }
728
+ }
729
+ else {
730
+ fmt.log.info('Step 6: Skipping Forgejo sync (review provider is not forgejo).');
731
+ }
732
+ if (node_fs_1.default.existsSync(baseWorktree)) {
733
+ nextActionMessage = `Next: cd ${baseWorktree}`;
734
+ }
735
+ recordPostIntegrationStatsOrAbort(slug, { rootDir: baseWorktree });
736
+ fmt.log.info('Step 7: Cleaning up the local mission worktree...');
737
+ if (!cleanupMissionWorktree(slug)) {
738
+ fmt.log.fail('Mission worktree cleanup failed.');
739
+ throw new IntegrationAbort();
740
+ }
741
+ maybeUpdateGraphifyOnPrimary(baseWorktree);
742
+ fmt.log.pass('Integration completed successfully.');
743
+ }
744
+ }
745
+ }
746
+ catch (error) {
747
+ if (error instanceof IntegrationAbort) {
748
+ exitCode = 1;
687
749
  }
688
- // @ts-expect-error baseWorktree may be undefined
689
- const squashResult = git(['-C', baseWorktree, 'merge', '--squash', branch]);
690
- if (squashResult.status !== 0) {
691
- noisePatchState?.cleanup?.();
692
- fmt.log.fail('Squash merge failed.');
693
- throw new IntegrationAbort();
750
+ else {
751
+ throw /** @type {any} */ (error);
694
752
  }
695
- if (noisePatchState?.patchPath) {
696
- const restoreNoiseResult = restoreNoisePatchAfterSquash(/** @type {string} */ (baseWorktree), noisePatchState.patchPath, { gitRunner: git });
697
- /** @type {Function} */ (noisePatchState.cleanup)();
698
- if (!restoreNoiseResult.ok) {
699
- fmt.log.fail('Could not restore trailing backlog noise after squash merge.');
700
- if (restoreNoiseResult.error) {
701
- fmt.log.fail(restoreNoiseResult.error);
753
+ }
754
+ finally {
755
+ if (temporaryStash?.created) {
756
+ const restoreResult = restoreMainCheckoutStash(temporaryStash);
757
+ if (restoreResult.status !== 0) {
758
+ reportStashPopFailure(slug, restoreResult, { rootDir: /** @type {any} */ (temporaryStash).rootDir });
759
+ exitCode = 1;
702
760
  }
703
- throw new IntegrationAbort();
704
- }
705
761
  }
706
-
707
- fmt.log.info('Step 4: Final closeout checks in the local integration checkout...');
708
- if (fs.existsSync(mainTaskFile)) {
709
- completeTask(slug, baseWorktree);
710
- // Re-resolve because it moved
711
- const updatedResolution = resolveTaskFile(slug, baseWorktree);
712
- if (updatedResolution.ok) {
713
- // @ts-expect-error baseWorktree may be undefined
714
- rewriteWorktreePaths(updatedResolution.taskFile, slug, { rootDir: baseWorktree });
715
- }
762
+ if (nextActionMessage) {
763
+ fmt.log.info(`\n${nextActionMessage}`);
716
764
  }
717
-
718
- // @ts-expect-error baseWorktree may be undefined
719
- git(['-C', baseWorktree, 'add', '-A']);
720
- fmt.log.info('Step 5: Creating the landed squash commit in the local integration checkout...');
721
- const commitResult = git([
722
- '-C',
723
- /** @type {string} */ (baseWorktree),
724
- 'commit',
725
- '-m',
726
- `${branch}: ${summary}`
727
- ]);
728
- if (commitResult.status !== 0) {
729
- const output = [commitResult.stdout, commitResult.stderr].filter(Boolean).join('\n').trim();
730
- fmt.log.fail('Could not create the squash commit in the local integration checkout.');
731
- if (output) {
732
- fmt.log.fail(output);
733
- }
734
- fmt.log.info(`The squash commit runs the repo git hooks. Fix the reported hook failure in ${baseWorktree} and retry integrate.`);
735
- fmt.log.info(`For this mission, the relevant verification command is ${formatVerificationCommand(context.area, baseWorktree)}`);
736
- throw new IntegrationAbort();
765
+ process.exit(exitCode);
766
+ }
767
+ }
768
+ /** @param {string} slug @param{{baseBranch?: string|null, baseWorktree?: string|null, isForgejoReviewEnabledFn?: Function}} opts */
769
+ function buildIntegrationContext(slug, { baseBranch = null, baseWorktree = null, isForgejoReviewEnabledFn = product_config_js_1.isForgejoReviewEnabled } = {}) {
770
+ const branch = `mission/${slug}`;
771
+ const currentBranch = (0, git_js_1.getCurrentBranch)();
772
+ const missionDir = (0, mission_utils_js_1.findMissionDir)(slug);
773
+ const area = missionDir ? (0, mission_utils_js_1.findMissionArea)(missionDir) : 'docs';
774
+ // The mission integrates back into its recorded base branch/worktree. When no
775
+ // base was recorded (every legacy mission) these resolve to the primary
776
+ // branch/worktree, so the rest of integration is byte-identical to today.
777
+ let resolvedBaseBranch = baseBranch;
778
+ if (!resolvedBaseBranch) {
779
+ try {
780
+ resolvedBaseBranch = (0, mission_utils_js_1.resolveMissionBaseBranch)(slug, process.cwd());
737
781
  }
738
- const proofResult = verification.captureVerifiedTreeProof(context.area, baseWorktree, {
739
- gitRunner: git,
740
- runFn: /** @type {Function} */ (child_process.spawnSync)
741
- });
742
- if (!/** @type {any} */ (proofResult).ok) {
743
- fmt.log.fail(`Could not verify the exact tree being published: ${/** @type {any} */ (proofResult).error}`);
744
- throw new IntegrationAbort();
782
+ catch (_) {
783
+ resolvedBaseBranch = (0, mission_utils_js_1.getPrimaryBranch)();
745
784
  }
746
- const proof = /** @type {any} */ (proofResult).proof;
747
- // @ts-expect-error baseWorktree may be undefined
748
- const mergedCommit = git(['-C', baseWorktree, 'rev-parse', 'HEAD']).stdout.trim();
749
- const proofCheck = verification.assertVerifiedTreeProof(proof, baseWorktree, { gitRunner: git });
750
- if (!proofCheck.ok) {
751
- fmt.log.fail(`Verification proof is stale for the publish tree: ${/** @type {any} */ (proofCheck).error}`);
752
- throw new IntegrationAbort();
785
+ }
786
+ let resolvedBaseWorktree = baseWorktree;
787
+ if (!resolvedBaseWorktree) {
788
+ try {
789
+ resolvedBaseWorktree = (0, mission_utils_js_1.resolveBaseWorktree)(slug, { rootDir: process.cwd() });
753
790
  }
754
-
755
- if (isForgejoReviewEnabled(baseWorktree)) {
756
- fmt.log.info('Step 6: Syncing merged state to Forgejo...');
757
- const syncResult = syncMerged(branch, mergedCommit, {
758
- rootDir: baseWorktree,
759
- forgejoUser: context.forgejoUser,
760
- token: context.forgejoToken,
761
- baseBranch: context.baseBranch
762
- });
763
- if (!syncResult.ok) {
764
- // @ts-expect-error reportSyncMergedFailure expects {statusCode, error}
765
- reportSyncMergedFailure(syncResult);
766
- throw new IntegrationAbort();
767
- }
768
- } else {
769
- fmt.log.info('Step 6: Skipping Forgejo sync (review provider is not forgejo).');
791
+ catch (_) {
792
+ resolvedBaseWorktree = (0, mission_utils_js_1.getPrimaryWorktree)();
770
793
  }
771
-
772
- nextActionMessage = `Next: cd ${baseWorktree}`;
773
- recordPostIntegrationStatsOrAbort(slug, { rootDir: baseWorktree });
774
- fmt.log.info('Step 7: Cleaning up the local mission worktree...');
775
- if (!cleanupMissionWorktree(slug)) {
776
- fmt.log.fail('Mission worktree cleanup failed.');
777
- throw new IntegrationAbort();
794
+ }
795
+ let task = (0, backlog_js_1.resolveTaskFile)(slug);
796
+ if (!task.ok) {
797
+ const worktree = (0, mission_utils_js_1.resolveWorktree)(slug);
798
+ if (worktree) {
799
+ task = (0, backlog_js_1.resolveTaskFile)(slug, worktree);
778
800
  }
779
-
780
- maybeUpdateGraphifyOnPrimary(baseWorktree);
781
- fmt.log.pass('Integration completed successfully.');
782
- }
783
- }
784
- } catch (error) {
785
- if (error instanceof IntegrationAbort) {
786
- exitCode = 1;
787
- } else {
788
- throw /** @type {any} */ (error);
789
- }
790
- } finally {
791
- if (temporaryStash?.created) {
792
- const restoreResult = restoreMainCheckoutStash(/** @type {any} */ (temporaryStash));
793
- if (restoreResult.status !== 0) {
794
- reportStashPopFailure(slug, restoreResult, { rootDir: /** @type {any} */ (temporaryStash).rootDir });
795
- exitCode = 1;
796
- }
797
- }
798
-
799
- if (nextActionMessage) {
800
- fmt.log.info(`\n${nextActionMessage}`);
801
- }
802
- process.exit(exitCode);
803
- }
804
- }
805
-
806
- /** @param {string} slug @param{{baseBranch?: string|null, baseWorktree?: string|null, isForgejoReviewEnabledFn?: Function}} opts */
807
- function buildIntegrationContext(slug, {
808
- baseBranch = null,
809
- baseWorktree = null,
810
- isForgejoReviewEnabledFn = isForgejoReviewEnabled
811
- } = {}) {
812
- const branch = `mission/${slug}`;
813
- const currentBranch = getCurrentBranch();
814
- const missionDir = findMissionDir(slug);
815
- const area = missionDir ? findMissionArea(missionDir) : 'docs';
816
-
817
- // The mission integrates back into its recorded base branch/worktree. When no
818
- // base was recorded (every legacy mission) these resolve to the primary
819
- // branch/worktree, so the rest of integration is byte-identical to today.
820
- let resolvedBaseBranch = baseBranch;
821
- if (!resolvedBaseBranch) {
822
- try { resolvedBaseBranch = resolveMissionBaseBranch(slug, process.cwd()); } catch (_) { resolvedBaseBranch = getPrimaryBranch(); }
823
- }
824
- let resolvedBaseWorktree = baseWorktree;
825
- if (!resolvedBaseWorktree) {
826
- try { resolvedBaseWorktree = resolveBaseWorktree(slug, { rootDir: process.cwd() }); } catch (_) { resolvedBaseWorktree = getPrimaryWorktree(); }
827
- }
828
- let task = resolveTaskFile(slug);
829
- if (!task.ok) {
830
- const worktree = resolveWorktree(slug);
831
- if (worktree) {task = resolveTaskFile(slug, worktree);}
832
- }
833
- let taskStatus = task.ok ? getTaskStatus(/** @type {string} */ (task.taskFile)) : null;
834
- if (task.ok && taskStatus === 'backlog') {
835
- const worktree = resolveWorktree(slug);
836
- if (worktree) {
837
- const wtTask = resolveTaskFile(slug, worktree);
838
- if (wtTask.ok) {
839
- const wtStatus = getTaskStatus(/** @type {string} */ (wtTask.taskFile));
840
- if (wtStatus && wtStatus !== 'backlog') {
841
- task = wtTask;
842
- taskStatus = wtStatus;
801
+ }
802
+ let taskStatus = task.ok ? (0, backlog_js_1.getTaskStatus)(task.taskFile) : null;
803
+ if (task.ok && taskStatus === 'backlog') {
804
+ const worktree = (0, mission_utils_js_1.resolveWorktree)(slug);
805
+ if (worktree) {
806
+ const wtTask = (0, backlog_js_1.resolveTaskFile)(slug, worktree);
807
+ if (wtTask.ok) {
808
+ const wtStatus = (0, backlog_js_1.getTaskStatus)(wtTask.taskFile);
809
+ if (wtStatus && wtStatus !== 'backlog') {
810
+ task = wtTask;
811
+ taskStatus = wtStatus;
812
+ }
813
+ }
843
814
  }
844
- }
845
- }
846
- }
847
- const taskAssignee = task.ok ? getTaskAssignee(/** @type {string} */ (task.taskFile)) : null;
848
- const forgejoEnabled = isForgejoReviewEnabledFn(/** @type {string} */ (resolvedBaseWorktree));
849
-
850
- let forgejoIdentity = { forgejoUser: null, warning: null };
851
- let forgejoToken = null;
852
- let pr = { exists: false };
853
- /** @type{any[]} */ let siblingPrs = [];
854
- let approval = { ok: false, error: 'forgejo-off', reviewState: null };
855
-
856
- if (forgejoEnabled) {
857
- // @ts-expect-error forgejoIdentity.forgejoUser type mismatch
858
- forgejoIdentity = resolveForgejoUserForIntegration(taskAssignee);
859
- forgejoToken = readToken(/** @type {any} */ (forgejoIdentity.forgejoUser));
860
- pr = /** @type {any} */ (getPrStatus(branch, process.cwd(), {
861
- forgejoUser: /** @type {any} */ (forgejoIdentity.forgejoUser),
862
- // @ts-expect-error forgejoToken may be null
863
- token: forgejoToken
864
- }));
865
-
866
- if (pr.exists && slug) {
867
- const baseSlugMatch = slug.match(/^(task-\d+)/i);
868
- const baseSlug = baseSlugMatch ? baseSlugMatch[1].toLowerCase() : slug;
869
- if (forgejoToken) {
870
- const allOpen = listOpenPrsForSlug(baseSlug, forgejoToken);
871
- siblingPrs = allOpen.filter(p => p.head !== branch);
872
- }
873
- }
874
-
875
- approval = pr.exists ? /** @type {any} */ (getLatestReviewDecision(branch, {
876
- forgejoUser: /** @type {any} */ (forgejoIdentity.forgejoUser),
877
- token: /** @type {any} */ (forgejoToken)
878
- })) : /** @type {any} */ ({ ok: false, error: 'pr-missing', reviewState: undefined });
879
- }
880
-
881
- // Local review-state fallback: when forgejo token/API is unavailable but the
882
- // mission's review-state.json shows approved, populate approval from local state
883
- // so that integrate can proceed without a live Forgejo connection.
884
- // Only applies when Forgejo was enabled but approval could not be obtained.
885
- if (forgejoEnabled && !approval.ok) {
886
- const localStateFallback = readReviewState(slug, /** @type {string} */ (resolvedBaseWorktree));
887
- if (localStateFallback && localStateFallback.phase === 'approved' && localStateFallback.disposition === 'APPROVED') {
888
- approval = /** @type {any} */ ({ ok: true, reviewState: 'APPROVED', source: 'local-review-state' });
889
- }
890
- }
891
-
892
- const mainBranchResult = git(['-C', /** @type {string} */ (resolvedBaseWorktree), 'branch', '--show-current']);
893
- const mainBranch = mainBranchResult.stdout.trim();
894
- const mainStatus = git(['-C', /** @type {string} */ (resolvedBaseWorktree), 'status', '--short']);
895
-
896
- return {
897
- slug,
898
- branch,
899
- currentBranch,
900
- missionDir,
901
- area,
902
- task,
903
- taskStatus,
904
- taskAssignee,
905
- forgejoUser: forgejoIdentity.forgejoUser,
906
- forgejoToken,
907
- taskAssigneeWarning: forgejoIdentity.warning,
908
- pr,
909
- siblingPrs,
910
- approval,
911
- baseBranch: resolvedBaseBranch,
912
- baseWorktree: resolvedBaseWorktree,
913
- mainBranch,
914
- mainDirtyEntries: mainStatus.stdout.trim().length > 0
915
- ? mainStatus.stdout.trim().split('\n').filter(Boolean)
916
- : [],
917
- mainDirty: mainStatus.stdout.trim().length > 0
918
- };
815
+ }
816
+ const taskAssignee = task.ok ? (0, backlog_js_1.getTaskAssignee)(task.taskFile) : null;
817
+ const forgejoEnabled = isForgejoReviewEnabledFn(/** @type {string} */ (resolvedBaseWorktree));
818
+ let forgejoIdentity = { forgejoUser: null, warning: null };
819
+ let forgejoToken = null;
820
+ let pr = { exists: false };
821
+ /** @type{any[]} */ let siblingPrs = [];
822
+ let approval = { ok: false, error: 'forgejo-off', reviewState: null };
823
+ if (forgejoEnabled) {
824
+ forgejoIdentity = resolveForgejoUserForIntegration(taskAssignee);
825
+ forgejoToken = (0, forgejo_js_1.readToken)(/** @type {any} */ (forgejoIdentity.forgejoUser || 'default'));
826
+ pr = /** @type {any} */ ((0, forgejo_js_1.getPrStatus)(branch, process.cwd(), {
827
+ forgejoUser: /** @type {any} */ (forgejoIdentity.forgejoUser),
828
+ token: forgejoToken
829
+ }));
830
+ if (pr.exists && slug) {
831
+ const baseSlugMatch = slug.match(/^(task-\d+)/i);
832
+ const baseSlug = baseSlugMatch ? baseSlugMatch[1].toLowerCase() : slug;
833
+ if (forgejoToken) {
834
+ const allOpen = (0, forgejo_js_1.listOpenPrsForSlug)(baseSlug, forgejoToken);
835
+ siblingPrs = allOpen.filter(p => p.head !== branch);
836
+ }
837
+ }
838
+ approval = pr.exists ? /** @type {any} */ ((0, forgejo_js_1.getLatestReviewDecision)(branch, {
839
+ forgejoUser: /** @type {any} */ (forgejoIdentity.forgejoUser),
840
+ token: /** @type {any} */ (forgejoToken)
841
+ })) : /** @type {any} */ ({ ok: false, error: 'pr-missing', reviewState: undefined });
842
+ }
843
+ // Local review-state fallback: when forgejo token/API is unavailable but the
844
+ // mission's review-state.json shows approved, populate approval from local state
845
+ // so that integrate can proceed without a live Forgejo connection.
846
+ // Only applies when Forgejo was enabled but approval could not be obtained.
847
+ if (forgejoEnabled && !approval.ok) {
848
+ const localStateFallback = (0, review_state_js_1.readReviewState)(slug, /** @type {string} */ (resolvedBaseWorktree));
849
+ if (localStateFallback && localStateFallback.phase === 'approved' && localStateFallback.disposition === 'APPROVED') {
850
+ approval = /** @type {any} */ ({ ok: true, reviewState: 'APPROVED', source: 'local-review-state' });
851
+ }
852
+ }
853
+ const mainBranchResult = (0, git_js_1.git)(['-C', /** @type {string} */ (resolvedBaseWorktree), 'branch', '--show-current']);
854
+ const mainBranch = mainBranchResult.stdout.trim();
855
+ const mainStatus = (0, git_js_1.git)(['-C', /** @type {string} */ (resolvedBaseWorktree), 'status', '--short']);
856
+ return {
857
+ slug,
858
+ branch,
859
+ currentBranch,
860
+ missionDir,
861
+ area,
862
+ task,
863
+ taskStatus,
864
+ taskAssignee,
865
+ forgejoUser: forgejoIdentity.forgejoUser,
866
+ forgejoToken,
867
+ taskAssigneeWarning: forgejoIdentity.warning,
868
+ pr,
869
+ siblingPrs,
870
+ approval,
871
+ baseBranch: resolvedBaseBranch,
872
+ baseWorktree: resolvedBaseWorktree,
873
+ mainBranch,
874
+ mainDirtyEntries: mainStatus.stdout.trim().length > 0
875
+ ? mainStatus.stdout.trim().split('\n').filter(Boolean)
876
+ : [],
877
+ mainDirty: mainStatus.stdout.trim().length > 0
878
+ };
919
879
  }
920
-
921
880
  /** @param {{slug: string, branch: string, currentBranch: string, missionDir?: string, area: string, task: {ok: boolean, taskFile?: string, reason?: string, matches?: string[]}, taskStatus?: string, taskAssignee?: string|null, forgejoUser?: string|null, forgejoToken?: string|null, taskAssigneeWarning?: string|null, pr: {exists?: boolean, state?: string, number?: number, merged?: boolean, raw?: string}, siblingPrs: any[], approval: {ok?: boolean, error?: string, reviewState?: string, defaultUserApproved?: boolean, source?: string}, baseBranch?: string, baseWorktree?: string, mainBranch: string, mainDirtyEntries: string[], mainDirty: boolean}} context */
922
881
  function evaluateTaskStatusForIntegration(context) {
923
- const stateMapOptions = { rootDir: /** @type {string} */ (context.baseWorktree) };
924
- // @ts-expect-error context.taskStatus may be undefined
925
- if (toVirtual(context.taskStatus, /** @type {any} */ (stateMapOptions)) === 'approved') {
926
- return {
927
- ok: true,
928
- level: 'pass',
929
- message: `Backlog status: approved`
930
- };
931
- }
932
-
933
- const reviewApproved = context.approval?.ok && context.approval.reviewState === 'APPROVED';
934
- const defaultUserApproved = context.approval?.ok && context.approval.defaultUserApproved === true;
935
- const localApproved = context.approval?.source === 'local-review-state';
936
- const reviewCanProceed = context.taskStatus === 'review' && (context.pr?.merged || reviewApproved || localApproved);
937
- const defaultUserOverride = context.taskStatus === 'review' && context.pr?.merged === false && defaultUserApproved && context.approval.reviewState !== 'APPROVED';
938
-
939
- if (reviewCanProceed) {
940
- let reason;
941
- if (context.pr?.merged) {
942
- reason = 'Forgejo PR already merged';
943
- } else if (localApproved) {
944
- reason = 'local review-state: approved';
945
- } else {
946
- reason = `latest formal review state is ${context.approval.reviewState}`;
882
+ const stateMapOptions = { rootDir: /** @type {string} */ (context.baseWorktree) };
883
+ if ((0, state_map_js_1.toVirtual)(context.taskStatus, /** @type {any} */ (stateMapOptions)) === 'approved') {
884
+ return {
885
+ ok: true,
886
+ level: 'pass',
887
+ message: `Backlog status: approved`
888
+ };
889
+ }
890
+ const reviewApproved = context.approval?.ok && context.approval.reviewState === 'APPROVED';
891
+ const defaultUserApproved = context.approval?.ok && context.approval.defaultUserApproved === true;
892
+ const localApproved = context.approval?.source === 'local-review-state';
893
+ const reviewCanProceed = context.taskStatus === 'review' && (context.pr?.merged || reviewApproved || localApproved);
894
+ const defaultUserOverride = context.taskStatus === 'review' && context.pr?.merged === false && defaultUserApproved && context.approval.reviewState !== 'APPROVED';
895
+ if (reviewCanProceed) {
896
+ let reason;
897
+ if (context.pr?.merged) {
898
+ reason = 'Forgejo PR already merged';
899
+ }
900
+ else if (localApproved) {
901
+ reason = 'local review-state: approved';
902
+ }
903
+ else {
904
+ reason = `latest formal review state is ${context.approval.reviewState}`;
905
+ }
906
+ return {
907
+ ok: true,
908
+ level: 'warn',
909
+ message: `Backlog status: review accepted for integration because ${reason}`
910
+ };
911
+ }
912
+ if (defaultUserOverride) {
913
+ return {
914
+ ok: true,
915
+ level: 'warn',
916
+ message: `Backlog status: default user approved for integration despite ${context.approval.reviewState}`
917
+ };
947
918
  }
948
919
  return {
949
- ok: true,
950
- level: 'warn',
951
- message: `Backlog status: review accepted for integration because ${reason}`
952
- };
953
- }
954
-
955
- if (defaultUserOverride) {
956
- return {
957
- ok: true,
958
- level: 'warn',
959
- message: `Backlog status: default user approved for integration despite ${context.approval.reviewState}`
920
+ ok: false,
921
+ level: 'fail',
922
+ message: `Backlog status: expected approved, or review with an approved/merged Forgejo PR; found ${(0, state_map_js_1.toVirtual)(context.taskStatus, stateMapOptions)}`
960
923
  };
961
- }
962
-
963
- return {
964
- ok: false,
965
- level: 'fail',
966
- // @ts-expect-error stateMapOptions type mismatch
967
- message: `Backlog status: expected approved, or review with an approved/merged Forgejo PR; found ${toVirtual(context.taskStatus, stateMapOptions)}`
968
- };
969
924
  }
970
-
971
925
  /** @param {{slug: string, branch: string, currentBranch: string, missionDir?: string, area: string, task: {ok: boolean, taskFile?: string, reason?: string, matches?: string[]}, taskStatus?: string, taskAssignee?: string|null, forgejoUser?: string|null, forgejoToken?: string|null, taskAssigneeWarning?: string|null, pr: {exists?: boolean, state?: string, number?: number, merged?: boolean, raw?: string}, siblingPrs: any[], approval: {ok?: boolean, error?: string, reviewState?: string, defaultUserApproved?: boolean, source?: string}, baseBranch?: string, baseWorktree?: string, mainBranch: string, mainDirtyEntries: string[], mainDirty: boolean}} context */
972
926
  function promoteTaskForIntegrationIfNeeded(context, { dryRun = false } = {}) {
973
- const taskStatusCheck = evaluateTaskStatusForIntegration(context);
974
- const needsPromotion = context.task?.ok && context.taskStatus === 'review' && taskStatusCheck.ok;
975
-
976
- if (!needsPromotion) {
977
- return { changed: false, dryRun: false };
978
- }
979
-
980
- if (dryRun) {
981
- fmt.log.info('Dry run: integration would promote Backlog status from review to approved because review is already fulfilled.');
982
- return { changed: false, dryRun: true };
983
- }
984
-
985
- const stateMapOptions = { rootDir: /** @type {string} */ (context.baseWorktree) };
986
- if (!setTaskStatus(/** @type {string} */ (context.task.taskFile), toActual('approved', stateMapOptions))) {
987
- fmt.log.fail('Could not promote the Backlog task to approved before integration.');
988
- throw new IntegrationAbort();
989
- }
990
-
991
- context.taskStatus = toActual('approved', stateMapOptions);
992
- fmt.log.info('Promoted Backlog status from review to approved because review is already fulfilled.');
993
- return { changed: true, dryRun: false };
927
+ const taskStatusCheck = evaluateTaskStatusForIntegration(context);
928
+ const needsPromotion = context.task?.ok && context.taskStatus === 'review' && taskStatusCheck.ok;
929
+ if (!needsPromotion) {
930
+ return { changed: false, dryRun: false };
931
+ }
932
+ if (dryRun) {
933
+ fmt.log.info('Dry run: integration would promote Backlog status from review to approved because review is already fulfilled.');
934
+ return { changed: false, dryRun: true };
935
+ }
936
+ const stateMapOptions = { rootDir: /** @type {string} */ (context.baseWorktree) };
937
+ const approvedStatus = (0, state_map_js_1.toActual)('approved', stateMapOptions) || 'approved';
938
+ if (!(0, backlog_js_1.setTaskStatus)(/** @type {string} */ (context.task.taskFile || ''), approvedStatus)) {
939
+ fmt.log.fail('Could not promote the Backlog task to approved before integration.');
940
+ throw new IntegrationAbort();
941
+ }
942
+ context.taskStatus = (0, state_map_js_1.toActual)('approved', stateMapOptions);
943
+ fmt.log.info('Promoted Backlog status from review to approved because review is already fulfilled.');
944
+ return { changed: true, dryRun: false };
994
945
  }
995
-
996
946
  /**
997
947
  * @param{{slug: string, branch: string, missionDir?: string, area: string, task: {ok: boolean, taskFile?: string, reason?: string, matches?: string[]}, taskStatus?: string, taskAssignee?: string|null, forgejoUser?: string|null, forgejoToken?: string|null, taskAssigneeWarning?: string|null, pr: {exists?: boolean, state?: string, number?: number, merged?: boolean, raw?: string}, siblingPrs: any[], approval: {ok?: boolean, error?: string, reviewState?: string, defaultUserApproved?: boolean, source?: string}, baseBranch?: string, baseWorktree?: string, mainBranch: string, mainDirtyEntries: string[], mainDirty: boolean}} context
998
948
  */
999
- function printIntegrationPreflight(
1000
- context,
1001
- {
1002
- readTokenFn = readToken,
1003
- resolveTokenFileFn = resolveTokenFile,
1004
- detectRebaseStateFn = detectRebaseState,
1005
- getUnresolvedIndexConflictsFn = getUnresolvedIndexConflicts,
1006
- findMissionDocInBranchesFn = findMissionDocInBranches,
1007
- isForgejoReviewEnabledFn = isForgejoReviewEnabled,
1008
- log = fmt.log.plain
1009
- } = {}
1010
- ) {
1011
- const failures = [];
1012
- const warnings = [];
1013
-
1014
- // The integration "checkout" is the mission's base worktree on its base branch.
1015
- // For legacy missions these fall back to the primary worktree/branch, so the
1016
- // preflight output and checks are byte-identical to today.
1017
- const baseWorktree = context.baseWorktree || getPrimaryWorktree();
1018
- const baseBranch = context.baseBranch || getPrimaryBranch();
1019
-
1020
- log(fmt.status('INFO', `Integration preflight for ${context.slug}`));
1021
-
1022
- const branchPrefix = missionBranchName(context.slug, baseWorktree);
1023
- // @ts-expect-error context.currentBranch doesn't exist on type
1024
- if ((/** @type {any} */ context).currentBranch === context.branch || (/** @type {any} */ context).currentBranch.startsWith(`${branchPrefix}-`)) {
1025
- // @ts-expect-error context.currentBranch doesn't exist on type
1026
- log(fmt.status('PASS', `Mission branch: ${(/** @type {any} */ context).currentBranch}`));
1027
- } else {
1028
- failures.push('branch');
1029
- // @ts-expect-error context.currentBranch doesn't exist on type
1030
- log(fmt.status('FAIL', `Mission branch: current branch is ${(/** @type {any} */ context).currentBranch}, expected ${context.branch} (or a branch with a suffix)`));
1031
- }
1032
-
1033
- if (context.missionDir) {
1034
- log(fmt.status('PASS', `Mission doc: ${path.join(context.missionDir, 'MISSION.md')}`));
1035
- } else {
1036
- failures.push('mission-doc');
1037
- // Use the base slug (e.g. task-1054) for the canonical path even when the
1038
- // working slug carries a suffix (e.g. task-1054-modern).
1039
- const baseSlugMatch = context.slug.match(/^(task-\d+)/i);
1040
- const canonicalSlug = baseSlugMatch ? baseSlugMatch[1].toLowerCase() : context.slug;
1041
- const canonicalPath = path.relative(
1042
- baseWorktree,
1043
- path.join(missionDirForSlug(baseWorktree, canonicalSlug), 'MISSION.md')
1044
- ).split(path.sep).join('/');
1045
- log(fmt.status('FAIL', `Mission doc: ${canonicalPath} not found`));
1046
-
1047
- const candidates = findMissionDocInBranchesFn(context.slug, baseWorktree);
1048
- if (candidates && candidates.length > 0) {
1049
- log(fmt.status('INFO', `Found mission doc candidates on other branches. To recover, run:`));
1050
- candidates.forEach(c => {
1051
- log(fmt.status('INFO', ` git show ${c.branch}:${c.path} > ${canonicalPath}`));
1052
- });
1053
- }
1054
- }
1055
-
1056
- if (context.task.ok) {
1057
- log(fmt.status('PASS', `Backlog task: ${path.basename(/** @type {string} */ (context.task.taskFile))} (${context.taskStatus})`));
1058
-
1059
- try {
1060
- const { classification, error: classificationError } = stats.resolveMissionClassification(context.slug);
1061
- if (!classification) {
1062
- failures.push('classification');
1063
- log(fmt.status('FAIL', `Backlog classification: ${classificationError || 'missing'}`));
1064
- } else {
1065
- log(fmt.status('PASS', `Backlog classification: ${classification}`));
1066
- }
1067
- } catch (/** @type{any} */ error) {
1068
- failures.push('classification');
1069
- log(fmt.status('FAIL', `Backlog classification: ${error.message}`));
1070
- }
1071
-
1072
- // @ts-expect-error context type mismatch (missing currentBranch)
1073
- const taskStatusCheck = evaluateTaskStatusForIntegration(context);
1074
- if (!taskStatusCheck.ok) {
1075
- failures.push('task-status');
1076
- log(fmt.status('FAIL', `${taskStatusCheck.message}`));
1077
- } else if (taskStatusCheck.level === 'warn') {
1078
- warnings.push('task-status-review-approved');
1079
- log(fmt.status('WARN', `${taskStatusCheck.message}`));
1080
- } else {
1081
- log(fmt.status('PASS', `${taskStatusCheck.message}`));
1082
- }
1083
- } else if (context.task.reason === 'ambiguous') {
1084
- failures.push('task-ambiguity');
1085
- log(fmt.status('FAIL', `Backlog task: ambiguous slug ${context.slug}`));
1086
- // @ts-expect-error context.task.matches may be undefined
1087
- context.task.matches.forEach((/** @type{string} */ match) => log(` - ${match}`));
1088
- } else {
1089
- log(fmt.status('WARN', `Backlog task: no task file found for ${context.slug}; continuing with synthetic/unknown task metadata.`));
1090
- log(fmt.status('PASS', 'Backlog classification: unknown'));
1091
- }
1092
-
1093
- if (isForgejoReviewEnabledFn(baseWorktree)) {
1094
- const localApproved = context.approval?.source === 'local-review-state';
1095
- const localApprovalFallback = localApproved;
1096
-
1097
- if (context.pr.exists && context.pr.state === 'open' && !context.pr.merged) {
1098
- log(fmt.status('PASS', `Forgejo PR: PR #${context.pr.number} open`));
1099
- if (localApprovalFallback) {
1100
- log(fmt.status('INFO', `Forgejo approval: token unavailable, approval sourced from local review-state.json (phase=approved)`));
1101
- } else if (!context.approval.ok) {
1102
- failures.push('pr-approval');
1103
- log(fmt.status('FAIL', `Forgejo approval: could not verify an approved review (${context.approval.error})`));
1104
- } else if (context.approval.reviewState !== 'APPROVED' && context.approval.defaultUserApproved !== true) {
1105
- failures.push('pr-approval');
1106
- log(fmt.status('FAIL', `Forgejo approval: latest formal review state is ${context.approval.reviewState || 'missing'}, expected APPROVED`));
1107
- } else if (context.approval.reviewState !== 'APPROVED' && context.approval.defaultUserApproved === true) {
1108
- warnings.push('pr-approval-default-user-override');
1109
- log(fmt.status('WARN', `Forgejo approval: default user approved despite latest review state being ${context.approval.reviewState}`));
1110
- } else {
1111
- log(fmt.status('PASS', `Forgejo approval: latest formal review state is ${context.approval.reviewState}`));
1112
- }
1113
- } else if (context.pr.exists && context.pr.merged) {
1114
- warnings.push('pr-merged');
1115
- log(fmt.status('WARN', `Forgejo PR: PR #${context.pr.number} is already marked merged`));
1116
- } else if (context.pr.exists) {
1117
- failures.push('pr-state');
1118
- log(fmt.status('FAIL', `Forgejo PR: unexpected state '${context.pr.state}'`));
1119
- } else {
1120
- failures.push('pr-missing');
1121
- log(fmt.status('FAIL', `Forgejo PR: ${context.pr.raw || 'no PR found'}`));
1122
- }
1123
-
1124
- if (context.siblingPrs && context.siblingPrs.length > 0) {
1125
- warnings.push('sibling-prs');
1126
- const baseSlugMatch = context.slug.match(/^(task-\d+)/i);
1127
- const baseSlug = baseSlugMatch ? baseSlugMatch[1].toLowerCase() : context.slug;
1128
- log(fmt.status('WARN', `Multiple open PRs detected for ${baseSlug}. Close stale PRs before integrating:`));
1129
- context.siblingPrs.forEach((/** @type{{number: number, head: string, html_url: string}} */ p) => {
1130
- log(fmt.status('INFO', ` - PR #${p.number} (${p.head}): ${p.html_url}`));
1131
- });
1132
- }
1133
-
1134
- const tokenPath = resolveTokenFileFn(/** @type {string} */ (context.forgejoUser));
1135
- const token = readTokenFn(/** @type {string} */ (context.forgejoUser));
1136
- if (token) {
1137
- log(fmt.status('PASS', `Forgejo token: resolved for ${context.forgejoUser} (${tokenPath || 'env:FORGEJO_TOKEN'})`));
1138
- } else if (localApprovalFallback) {
1139
- log(fmt.status('INFO', `Forgejo token: no token file found for ${context.forgejoUser} (approval sourced from local review-state.json)`));
1140
- } else {
1141
- failures.push('forgejo-token');
1142
- log(fmt.status('FAIL', `Forgejo token: no token file found for ${context.forgejoUser}`));
1143
- }
1144
- } else {
1145
- log(fmt.status('INFO', 'Forgejo PR/approval checks skipped (review provider is not forgejo).'));
1146
- }
1147
-
1148
- if (context.taskAssigneeWarning) {
1149
- warnings.push('task-assignee');
1150
- log(fmt.status('WARN', `${context.taskAssigneeWarning}`));
1151
- }
1152
-
1153
- const expectedPrimaryBranch = baseBranch;
1154
- if (context.mainBranch === expectedPrimaryBranch) {
1155
- log(fmt.status('PASS', `Integration checkout branch: ${baseWorktree} is on ${expectedPrimaryBranch}`));
1156
- } else {
1157
- failures.push('main-branch');
1158
- const branchLabel = context.mainBranch || '(detached HEAD)';
1159
- log(fmt.status('FAIL', `Integration checkout branch: expected ${expectedPrimaryBranch}, found ${branchLabel}`));
1160
- log(fmt.status('INFO', `Retry with: git -C ${baseWorktree} checkout ${expectedPrimaryBranch}`));
1161
- }
1162
-
1163
- const rebaseState = detectRebaseStateFn(baseWorktree);
1164
- if (rebaseState.inProgress) {
1165
- failures.push('rebase-in-progress');
1166
- log(fmt.status('FAIL', `Integration checkout rebase: rebase in progress in ${baseWorktree}`));
1167
- if (rebaseState.rebaseHead) {
1168
- log(fmt.status('INFO', `Current rebase head: ${rebaseState.rebaseHead}`));
1169
- }
1170
- if (rebaseState.unmergedFiles.length > 0) {
1171
- rebaseState.unmergedFiles.forEach(file => log(fmt.status('INFO', ` - ${file}`)));
1172
- }
1173
- log(fmt.status('INFO', 'Finish or abort the existing rebase before retrying:'));
1174
- log(fmt.status('INFO', ` git -C ${baseWorktree} rebase --continue`));
1175
- log(fmt.status('INFO', ` git -C ${baseWorktree} rebase --abort`));
1176
- log(fmt.status('INFO', ` git -C ${baseWorktree} rebase --skip`));
1177
- log(fmt.status('INFO', `Retry with: px integrate ${context.slug} --dry-run`));
1178
- }
1179
-
1180
- const indexConflicts = getUnresolvedIndexConflictsFn(baseWorktree);
1181
- if (!indexConflicts.ok) {
1182
- failures.push('main-index-conflict-check');
1183
- log(fmt.status('FAIL', `Integration checkout conflict scan: could not inspect ${baseWorktree} (${indexConflicts.error || 'unknown error'})`));
1184
- } else if (indexConflicts.files.length > 0) {
1185
- failures.push('main-index-conflicts');
1186
- log(fmt.status('FAIL', `Integration checkout conflicts: unresolved merge entries detected in ${baseWorktree}`));
1187
- indexConflicts.files.forEach(file => log(fmt.status('INFO', ` - ${file}`)));
1188
- log(fmt.status('INFO', 'Resolve each conflicted path, then drop the stale stash entry before retrying:'));
1189
- indexConflicts.files.forEach(file => {
1190
- log(fmt.status('INFO', ` git -C ${baseWorktree} rm "${file}"`));
1191
- log(fmt.status('INFO', ` git -C ${baseWorktree} add "${file}"`));
1192
- });
1193
- log(fmt.status('INFO', ` git -C ${baseWorktree} stash drop`));
1194
- log(fmt.status('INFO', `Retry with: px integrate ${context.slug} --dry-run`));
1195
- } else {
1196
- log(fmt.status('PASS', 'Integration checkout conflicts: no unresolved merge entries in the git index'));
1197
- }
1198
-
1199
- if (context.mainDirty) {
1200
- warnings.push('main-dirty');
1201
- log(fmt.status('WARN', `Integration checkout dirty: ${baseWorktree} has uncommitted changes that will be stashed temporarily`));
1202
- context.mainDirtyEntries.forEach((/** @type{string} */ entry) => log(fmt.status('INFO', ` - ${entry}`)));
1203
- } else {
1204
- log(fmt.status('PASS', 'Integration checkout dirty state: clean'));
1205
- }
1206
-
1207
- const gitDir = path.join(process.cwd(), '.git');
1208
- const isMainRepo = fs.existsSync(gitDir) && !fs.lstatSync(gitDir).isSymbolicLink();
1209
- const isCorrectPath = process.cwd() === baseWorktree;
1210
- if (isMainRepo && isCorrectPath) {
1211
- log(fmt.status('PASS', 'Backlog context: resolves to main repository'));
1212
- } else {
1213
- warnings.push('backlog-context');
1214
- log(fmt.status('WARN', `Backlog context: does not resolve to ${baseWorktree}. (Ignore if running from worktree to test dry-run; post-squash closeout still requires the local integration checkout).`));
1215
- }
1216
-
1217
- log(fmt.status('INFO', 'Forgejo configuration: allow_manual_merge assumed enabled'));
1218
-
1219
- if (warnings.length > 0) {
1220
- log(fmt.status('WARN', `Integration warnings: ${warnings.join(', ')}`));
1221
- }
1222
-
1223
- log(fmt.status('INFO', `${VARIANT_B_AUTOMATION_SUMMARY}`));
1224
-
1225
- return { failures, warnings };
949
+ function printIntegrationPreflight(context, { readTokenFn = forgejo_js_1.readToken, resolveTokenFileFn = forgejo_js_1.resolveTokenFile, detectRebaseStateFn = git_js_1.detectRebaseState, getUnresolvedIndexConflictsFn = getUnresolvedIndexConflicts, findMissionDocInBranchesFn = mission_utils_js_1.findMissionDocInBranches, isForgejoReviewEnabledFn = product_config_js_1.isForgejoReviewEnabled, log = fmt.log.plain } = {}) {
950
+ const failures = [];
951
+ const warnings = [];
952
+ // The integration "checkout" is the mission's base worktree on its base branch.
953
+ // For legacy missions these fall back to the primary worktree/branch, so the
954
+ // preflight output and checks are byte-identical to today.
955
+ const baseWorktree = context.baseWorktree || (0, mission_utils_js_1.getPrimaryWorktree)();
956
+ const baseBranch = context.baseBranch || (0, mission_utils_js_1.getPrimaryBranch)();
957
+ log(fmt.status('INFO', `Integration preflight for ${context.slug}`));
958
+ const branchPrefix = (0, mission_utils_js_1.missionBranchName)(context.slug, baseWorktree);
959
+ if (( /** @type {any} */context).currentBranch === context.branch || ( /** @type {any} */context).currentBranch.startsWith(`${branchPrefix}-`)) {
960
+ log(fmt.status('PASS', `Mission branch: ${( /** @type {any} */context).currentBranch}`));
961
+ }
962
+ else {
963
+ failures.push('branch');
964
+ log(fmt.status('FAIL', `Mission branch: current branch is ${( /** @type {any} */context).currentBranch}, expected ${context.branch} (or a branch with a suffix)`));
965
+ }
966
+ if (context.missionDir) {
967
+ log(fmt.status('PASS', `Mission doc: ${node_path_1.default.join(context.missionDir, 'MISSION.md')}`));
968
+ }
969
+ else {
970
+ failures.push('mission-doc');
971
+ // Use the base slug (e.g. task-1054) for the canonical path even when the
972
+ // working slug carries a suffix (e.g. task-1054-modern).
973
+ const baseSlugMatch = context.slug.match(/^(task-\d+)/i);
974
+ const canonicalSlug = baseSlugMatch ? baseSlugMatch[1].toLowerCase() : context.slug;
975
+ const canonicalPath = node_path_1.default.relative(baseWorktree, node_path_1.default.join((0, mission_utils_js_1.missionDirForSlug)(baseWorktree, canonicalSlug), 'MISSION.md')).split(node_path_1.default.sep).join('/');
976
+ log(fmt.status('FAIL', `Mission doc: ${canonicalPath} not found`));
977
+ const candidates = findMissionDocInBranchesFn(context.slug, baseWorktree);
978
+ if (candidates && candidates.length > 0) {
979
+ log(fmt.status('INFO', `Found mission doc candidates on other branches. To recover, run:`));
980
+ candidates.forEach(c => {
981
+ log(fmt.status('INFO', ` git show ${c.branch}:${c.path} > ${canonicalPath}`));
982
+ });
983
+ }
984
+ }
985
+ if (context.task.ok) {
986
+ log(fmt.status('PASS', `Backlog task: ${node_path_1.default.basename(/** @type {string} */ (context.task.taskFile))} (${context.taskStatus})`));
987
+ try {
988
+ const { classification, error: classificationError } = stats_js_1.default.resolveMissionClassification(context.slug);
989
+ if (!classification) {
990
+ failures.push('classification');
991
+ log(fmt.status('FAIL', `Backlog classification: ${classificationError || 'missing'}`));
992
+ }
993
+ else {
994
+ log(fmt.status('PASS', `Backlog classification: ${classification}`));
995
+ }
996
+ }
997
+ catch ( /** @type{any} */error) {
998
+ failures.push('classification');
999
+ log(fmt.status('FAIL', `Backlog classification: ${error.message || String(error)}`));
1000
+ }
1001
+ const taskStatusCheck = evaluateTaskStatusForIntegration(context);
1002
+ if (!taskStatusCheck.ok) {
1003
+ failures.push('task-status');
1004
+ log(fmt.status('FAIL', `${taskStatusCheck.message}`));
1005
+ }
1006
+ else if (taskStatusCheck.level === 'warn') {
1007
+ warnings.push('task-status-review-approved');
1008
+ log(fmt.status('WARN', `${taskStatusCheck.message}`));
1009
+ }
1010
+ else {
1011
+ log(fmt.status('PASS', `${taskStatusCheck.message}`));
1012
+ }
1013
+ }
1014
+ else if (context.task.reason === 'ambiguous') {
1015
+ failures.push('task-ambiguity');
1016
+ log(fmt.status('FAIL', `Backlog task: ambiguous slug ${context.slug}`));
1017
+ // @ts-expect-error context.task.matches may be undefined
1018
+ context.task.matches.forEach((/** @type{string} */ match) => log(` - ${match}`));
1019
+ }
1020
+ else {
1021
+ log(fmt.status('WARN', `Backlog task: no task file found for ${context.slug}; continuing with synthetic/unknown task metadata.`));
1022
+ log(fmt.status('PASS', 'Backlog classification: unknown'));
1023
+ }
1024
+ if (isForgejoReviewEnabledFn(baseWorktree)) {
1025
+ const localApproved = context.approval?.source === 'local-review-state';
1026
+ const localApprovalFallback = localApproved;
1027
+ if (context.pr.exists && context.pr.state === 'open' && !context.pr.merged) {
1028
+ log(fmt.status('PASS', `Forgejo PR: PR #${context.pr.number} open`));
1029
+ if (localApprovalFallback) {
1030
+ log(fmt.status('INFO', `Forgejo approval: token unavailable, approval sourced from local review-state.json (phase=approved)`));
1031
+ }
1032
+ else if (!context.approval.ok) {
1033
+ failures.push('pr-approval');
1034
+ log(fmt.status('FAIL', `Forgejo approval: could not verify an approved review (${context.approval.error})`));
1035
+ }
1036
+ else if (context.approval.reviewState !== 'APPROVED' && context.approval.defaultUserApproved !== true) {
1037
+ failures.push('pr-approval');
1038
+ log(fmt.status('FAIL', `Forgejo approval: latest formal review state is ${context.approval.reviewState || 'missing'}, expected APPROVED`));
1039
+ }
1040
+ else if (context.approval.reviewState !== 'APPROVED' && context.approval.defaultUserApproved === true) {
1041
+ warnings.push('pr-approval-default-user-override');
1042
+ log(fmt.status('WARN', `Forgejo approval: default user approved despite latest review state being ${context.approval.reviewState}`));
1043
+ }
1044
+ else {
1045
+ log(fmt.status('PASS', `Forgejo approval: latest formal review state is ${context.approval.reviewState}`));
1046
+ }
1047
+ }
1048
+ else if (context.pr.exists && context.pr.merged) {
1049
+ warnings.push('pr-merged');
1050
+ log(fmt.status('WARN', `Forgejo PR: PR #${context.pr.number} is already marked merged`));
1051
+ }
1052
+ else if (context.pr.exists) {
1053
+ failures.push('pr-state');
1054
+ log(fmt.status('FAIL', `Forgejo PR: unexpected state '${context.pr.state}'`));
1055
+ }
1056
+ else {
1057
+ failures.push('pr-missing');
1058
+ log(fmt.status('FAIL', `Forgejo PR: ${context.pr.raw || 'no PR found'}`));
1059
+ }
1060
+ if (context.siblingPrs && context.siblingPrs.length > 0) {
1061
+ warnings.push('sibling-prs');
1062
+ const baseSlugMatch = context.slug.match(/^(task-\d+)/i);
1063
+ const baseSlug = baseSlugMatch ? baseSlugMatch[1].toLowerCase() : context.slug;
1064
+ log(fmt.status('WARN', `Multiple open PRs detected for ${baseSlug}. Close stale PRs before integrating:`));
1065
+ context.siblingPrs.forEach((p) => {
1066
+ log(fmt.status('INFO', ` - PR #${p.number} (${p.head}): ${p.html_url}`));
1067
+ });
1068
+ }
1069
+ const tokenPath = resolveTokenFileFn(/** @type {string} */ (context.forgejoUser));
1070
+ const token = readTokenFn(/** @type {string} */ (context.forgejoUser));
1071
+ if (token) {
1072
+ log(fmt.status('PASS', `Forgejo token: resolved for ${context.forgejoUser} (${tokenPath || 'env:FORGEJO_TOKEN'})`));
1073
+ }
1074
+ else if (localApprovalFallback) {
1075
+ log(fmt.status('INFO', `Forgejo token: no token file found for ${context.forgejoUser} (approval sourced from local review-state.json)`));
1076
+ }
1077
+ else {
1078
+ failures.push('forgejo-token');
1079
+ log(fmt.status('FAIL', `Forgejo token: no token file found for ${context.forgejoUser}`));
1080
+ }
1081
+ }
1082
+ else {
1083
+ log(fmt.status('INFO', 'Forgejo PR/approval checks skipped (review provider is not forgejo).'));
1084
+ }
1085
+ if (context.taskAssigneeWarning) {
1086
+ warnings.push('task-assignee');
1087
+ log(fmt.status('WARN', `${context.taskAssigneeWarning}`));
1088
+ }
1089
+ const expectedPrimaryBranch = baseBranch;
1090
+ if (context.mainBranch === expectedPrimaryBranch) {
1091
+ log(fmt.status('PASS', `Integration checkout branch: ${baseWorktree} is on ${expectedPrimaryBranch}`));
1092
+ }
1093
+ else {
1094
+ failures.push('main-branch');
1095
+ const branchLabel = context.mainBranch || '(detached HEAD)';
1096
+ log(fmt.status('FAIL', `Integration checkout branch: expected ${expectedPrimaryBranch}, found ${branchLabel}`));
1097
+ log(fmt.status('INFO', `Retry with: git -C ${baseWorktree} checkout ${expectedPrimaryBranch}`));
1098
+ }
1099
+ const rebaseState = detectRebaseStateFn(baseWorktree);
1100
+ if (rebaseState.inProgress) {
1101
+ failures.push('rebase-in-progress');
1102
+ log(fmt.status('FAIL', `Integration checkout rebase: rebase in progress in ${baseWorktree}`));
1103
+ if (rebaseState.rebaseHead) {
1104
+ log(fmt.status('INFO', `Current rebase head: ${rebaseState.rebaseHead}`));
1105
+ }
1106
+ if (rebaseState.unmergedFiles.length > 0) {
1107
+ rebaseState.unmergedFiles.forEach(file => log(fmt.status('INFO', ` - ${file}`)));
1108
+ }
1109
+ log(fmt.status('INFO', 'Finish or abort the existing rebase before retrying:'));
1110
+ log(fmt.status('INFO', ` git -C ${baseWorktree} rebase --continue`));
1111
+ log(fmt.status('INFO', ` git -C ${baseWorktree} rebase --abort`));
1112
+ log(fmt.status('INFO', ` git -C ${baseWorktree} rebase --skip`));
1113
+ log(fmt.status('INFO', `Retry with: px integrate ${context.slug} --dry-run`));
1114
+ }
1115
+ const indexConflicts = getUnresolvedIndexConflictsFn(baseWorktree);
1116
+ if (!indexConflicts.ok) {
1117
+ failures.push('main-index-conflict-check');
1118
+ log(fmt.status('FAIL', `Integration checkout conflict scan: could not inspect ${baseWorktree} (${indexConflicts.error || 'unknown error'})`));
1119
+ }
1120
+ else if (indexConflicts.files.length > 0) {
1121
+ failures.push('main-index-conflicts');
1122
+ log(fmt.status('FAIL', `Integration checkout conflicts: unresolved merge entries detected in ${baseWorktree}`));
1123
+ indexConflicts.files.forEach(file => log(fmt.status('INFO', ` - ${file}`)));
1124
+ log(fmt.status('INFO', 'Resolve each conflicted path, then drop the stale stash entry before retrying:'));
1125
+ indexConflicts.files.forEach(file => {
1126
+ log(fmt.status('INFO', ` git -C ${baseWorktree} rm "${file}"`));
1127
+ log(fmt.status('INFO', ` git -C ${baseWorktree} add "${file}"`));
1128
+ });
1129
+ log(fmt.status('INFO', ` git -C ${baseWorktree} stash drop`));
1130
+ log(fmt.status('INFO', `Retry with: px integrate ${context.slug} --dry-run`));
1131
+ }
1132
+ else {
1133
+ log(fmt.status('PASS', 'Integration checkout conflicts: no unresolved merge entries in the git index'));
1134
+ }
1135
+ if (context.mainDirty) {
1136
+ warnings.push('main-dirty');
1137
+ log(fmt.status('WARN', `Integration checkout dirty: ${baseWorktree} has uncommitted changes that will be stashed temporarily`));
1138
+ context.mainDirtyEntries.forEach((entry) => log(fmt.status('INFO', ` - ${entry}`)));
1139
+ }
1140
+ else {
1141
+ log(fmt.status('PASS', 'Integration checkout dirty state: clean'));
1142
+ }
1143
+ const gitDir = node_path_1.default.join(process.cwd(), '.git');
1144
+ const isMainRepo = node_fs_1.default.existsSync(gitDir) && !node_fs_1.default.lstatSync(gitDir).isSymbolicLink();
1145
+ const isCorrectPath = process.cwd() === baseWorktree;
1146
+ if (isMainRepo && isCorrectPath) {
1147
+ log(fmt.status('PASS', 'Backlog context: resolves to main repository'));
1148
+ }
1149
+ else {
1150
+ warnings.push('backlog-context');
1151
+ log(fmt.status('WARN', `Backlog context: does not resolve to ${baseWorktree}. (Ignore if running from worktree to test dry-run; post-squash closeout still requires the local integration checkout).`));
1152
+ }
1153
+ log(fmt.status('INFO', 'Forgejo configuration: allow_manual_merge assumed enabled'));
1154
+ if (warnings.length > 0) {
1155
+ log(fmt.status('WARN', `Integration warnings: ${warnings.join(', ')}`));
1156
+ }
1157
+ log(fmt.status('INFO', `${VARIANT_B_AUTOMATION_SUMMARY}`));
1158
+ return { failures, warnings };
1226
1159
  }
1227
-
1228
1160
  /** @param {string} taskFilePath @param {string} slug @param{{rootDir?: string}} options */
1229
1161
  function rewriteWorktreePaths(taskFilePath, slug, options = {}) {
1230
- const rootDir = options.rootDir || getPrimaryWorktree();
1231
- const before = fs.readFileSync(taskFilePath, 'utf8');
1232
- const updated = before.replace(
1233
- new RegExp(`${conventionalWorktreePath(slug, rootDir)}`, 'g'),
1234
- rootDir
1235
- );
1236
-
1237
- if (before !== updated) {
1238
- fs.writeFileSync(taskFilePath, updated, 'utf8');
1239
- }
1162
+ const rootDir = options.rootDir || (0, mission_utils_js_1.getPrimaryWorktree)();
1163
+ const before = node_fs_1.default.readFileSync(taskFilePath, 'utf8');
1164
+ const updated = before.replace(new RegExp(`${(0, mission_utils_js_1.conventionalWorktreePath)(slug, rootDir)}`, 'g'), rootDir);
1165
+ if (before !== updated) {
1166
+ node_fs_1.default.writeFileSync(taskFilePath, updated, 'utf8');
1167
+ }
1240
1168
  }
1241
-
1242
1169
  /** @param{{slug: string, dirtyEntries?: string[], rootDir?: string, gitRunner?: Function}} params */
1243
- function stashMainCheckoutIfNeeded({
1244
- slug,
1245
- dirtyEntries = [],
1246
- rootDir = getPrimaryWorktree(),
1247
- gitRunner = git
1248
- }) {
1249
- if (dirtyEntries.length === 0) {
1250
- return { created: false };
1251
- }
1252
-
1253
- const message = `integrate:${slug}: temporary integration checkout stash`;
1254
- fmt.log.info(`Stashing unrelated local integration checkout changes before integration: ${message}`);
1255
- const result = gitRunner([
1256
- '-C',
1257
- rootDir,
1258
- 'stash',
1259
- 'push',
1260
- '--include-untracked',
1261
- '-m',
1262
- message
1263
- ]);
1264
-
1265
- if (result.status !== 0) {
1266
- fmt.log.fail('Could not stash the unrelated local integration checkout changes.');
1267
- throw new IntegrationAbort();
1268
- }
1269
-
1270
- return {
1271
- created: true,
1272
- message,
1273
- rootDir
1274
- };
1170
+ function stashMainCheckoutIfNeeded({ slug, dirtyEntries = [], rootDir = (0, mission_utils_js_1.getPrimaryWorktree)(), gitRunner = git_js_1.git }) {
1171
+ if (dirtyEntries.length === 0) {
1172
+ return { created: false };
1173
+ }
1174
+ const message = `integrate:${slug}: temporary integration checkout stash`;
1175
+ fmt.log.info(`Stashing unrelated local integration checkout changes before integration: ${message}`);
1176
+ const result = gitRunner([
1177
+ '-C',
1178
+ rootDir,
1179
+ 'stash',
1180
+ 'push',
1181
+ '--include-untracked',
1182
+ '-m',
1183
+ message
1184
+ ]);
1185
+ if (result.status !== 0) {
1186
+ fmt.log.fail('Could not stash the unrelated local integration checkout changes.');
1187
+ throw new IntegrationAbort();
1188
+ }
1189
+ return {
1190
+ created: true,
1191
+ message,
1192
+ rootDir
1193
+ };
1275
1194
  }
1276
-
1277
1195
  /** @param{{message: string, rootDir?: string, gitRunner?: Function}} params */
1278
- function restoreMainCheckoutStash({ message, rootDir = getPrimaryWorktree(), gitRunner = git }) {
1279
- fmt.log.info(`Restoring temporarily stashed local integration checkout changes: ${message}`);
1280
- // Pass cwd explicitly so spawnSync does not inherit the process cwd, which may have been
1281
- // deleted by worktree cleanup earlier in the same integrate run.
1282
- return gitRunner(['-C', rootDir, 'stash', 'pop'], { cwd: rootDir });
1196
+ function restoreMainCheckoutStash({ message, rootDir = (0, mission_utils_js_1.getPrimaryWorktree)(), gitRunner = git_js_1.git }) {
1197
+ fmt.log.info(`Restoring temporarily stashed local integration checkout changes: ${message}`);
1198
+ // Pass cwd explicitly so spawnSync does not inherit the process cwd, which may have been
1199
+ // deleted by worktree cleanup earlier in the same integrate run.
1200
+ return gitRunner(['-C', rootDir, 'stash', 'pop'], { cwd: rootDir });
1283
1201
  }
1284
-
1285
1202
  /** @param{{stdout: string, stderr: string, status: number}} result */
1286
1203
  function isNoMergeToAbortResult(result) {
1287
- const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
1288
- return /MERGE_HEAD missing|There is no merge to abort/i.test(output);
1204
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
1205
+ return /MERGE_HEAD missing|There is no merge to abort/i.test(output);
1289
1206
  }
1290
-
1291
1207
  /**
1292
1208
  * @param {string} slug
1293
1209
  * @param{{rootDir?: string, gitRunner?: Function, recordIntegrationStatsFn?: Function}} options
1294
1210
  */
1295
- function recordPostIntegrationStats(
1296
- slug,
1297
- {
1298
- rootDir = getPrimaryWorktree(),
1299
- gitRunner = git,
1300
- recordIntegrationStatsFn = stats.recordIntegrationStats,
1301
- } = {}
1302
- ) {
1303
- const dateResult = gitRunner(['-C', rootDir, 'log', '-1', '--format=%cs']);
1304
- if (dateResult.status !== 0) {
1305
- throw new Error(`Could not determine integration date for ${slug}.`);
1306
- }
1307
-
1308
- const statsCsvPath = stats.resolveStatsFilePath(rootDir);
1309
-
1310
- const outcome = recordIntegrationStatsFn({
1311
- slug,
1312
- rootDir,
1313
- filePath: statsCsvPath,
1314
- date: dateResult.stdout.trim(),
1315
- });
1316
-
1317
- fmt.log.info(`Workflow stats recorded: ${formatRecordedStatsRow(outcome.row)}`);
1318
- fmt.log.info('Workflow stats updated:');
1319
- fmt.log.plain(outcome.report);
1320
-
1321
- const missionRows = outcome.data?.rows || [];
1322
- const missionReport = stats.renderMissionPhaseReport(missionRows, slug);
1323
- const firstLine = missionReport.split('\n')[0];
1324
- fmt.log.info(firstLine);
1325
- fmt.log.plain(missionReport.split('\n').slice(1).join('\n'));
1326
-
1327
- return outcome;
1211
+ function recordPostIntegrationStats(slug, { rootDir = (0, mission_utils_js_1.getPrimaryWorktree)(), gitRunner = git_js_1.git, recordIntegrationStatsFn = stats_js_1.default.recordIntegrationStats, } = {}) {
1212
+ const dateResult = gitRunner(['-C', rootDir, 'log', '-1', '--format=%cs']);
1213
+ if (dateResult.status !== 0) {
1214
+ throw new Error(`Could not determine integration date for ${slug}.`);
1215
+ }
1216
+ const statsCsvPath = stats_js_1.default.resolveStatsFilePath(rootDir);
1217
+ const outcome = recordIntegrationStatsFn({
1218
+ slug,
1219
+ rootDir,
1220
+ filePath: statsCsvPath,
1221
+ date: dateResult.stdout.trim(),
1222
+ });
1223
+ fmt.log.info(`Workflow stats recorded: ${formatRecordedStatsRow(outcome.row)}`);
1224
+ fmt.log.info('Workflow stats updated:');
1225
+ fmt.log.plain(outcome.report);
1226
+ const missionRows = outcome.data?.rows || [];
1227
+ const missionReport = stats_js_1.default.renderMissionPhaseReport(missionRows, slug);
1228
+ const firstLine = missionReport.split('\n')[0];
1229
+ fmt.log.info(firstLine);
1230
+ fmt.log.plain(missionReport.split('\n').slice(1).join('\n'));
1231
+ return outcome;
1328
1232
  }
1329
-
1330
1233
  /** @param {string} slug @param{{rootDir?: string}} options */
1331
1234
  function recordPostIntegrationStatsOrAbort(slug, options = {}) {
1332
- try {
1333
- return recordPostIntegrationStats(slug, options);
1334
- } catch (/** @type{any} */ error) {
1335
- const detail = error && error.message ? error.message : String(error);
1336
- fmt.log.fail(`Post-integration workflow stats failed for ${slug}: ${detail}`);
1337
- throw new IntegrationAbort();
1338
- }
1235
+ try {
1236
+ return recordPostIntegrationStats(slug, options);
1237
+ }
1238
+ catch (error) {
1239
+ const detail = error && error.message ? error.message : String(error);
1240
+ fmt.log.fail(`Post-integration workflow stats failed for ${slug}: ${detail}`);
1241
+ throw new IntegrationAbort();
1242
+ }
1339
1243
  }
1340
-
1341
1244
  /** @param{{slug: string, summary: string, mainTaskFile?: string, rootDir?: string, gitRunner?: Function, baseBranch?: string|null, verificationArea?: string|null, captureVerifiedTreeProofFn?: Function, assertVerifiedTreeProofFn?: Function}} params */
1342
- function finalizeVariantACloseout({
1343
- slug,
1344
- summary,
1345
- mainTaskFile,
1346
- rootDir = getPrimaryWorktree(),
1347
- gitRunner = git,
1348
- baseBranch = null,
1349
- verificationArea = null,
1350
- captureVerifiedTreeProofFn = verification.captureVerifiedTreeProof,
1351
- assertVerifiedTreeProofFn = verification.assertVerifiedTreeProof
1352
- }) {
1353
- softResetTrailingBacklogNoise(rootDir, gitRunner);
1354
-
1355
- if (mainTaskFile && fs.existsSync(mainTaskFile)) {
1356
- completeTask(slug, rootDir);
1357
- // Re-resolve because it moved
1358
- const updatedResolution = resolveTaskFile(slug, rootDir);
1359
- if (updatedResolution.ok) {
1360
- rewriteWorktreePaths(/** @type {string} */ (updatedResolution.taskFile), slug, { rootDir });
1361
- }
1362
- }
1363
-
1364
- gitRunner(['-C', rootDir, 'add', '-A']);
1365
-
1366
- const stagedDiff = gitRunner(['-C', rootDir, 'diff', '--cached', '--quiet']);
1367
- if (stagedDiff.status === 0) {
1368
- return { ok: true, changed: false };
1369
- }
1370
-
1371
- const commitResult = gitRunner([
1372
- '-C',
1373
- rootDir,
1374
- 'commit',
1375
- '-m',
1376
- `${missionBranchName(slug, rootDir)}: ${summary} integration closeout`
1377
- ]);
1378
- if (commitResult.status !== 0) {
1379
- return {
1380
- ok: false,
1381
- error: 'commit-failed',
1382
- detail: [commitResult.stdout, commitResult.stderr].filter(Boolean).join('\n').trim()
1383
- };
1384
- }
1385
-
1386
- const resolvedVerificationArea = verificationArea || verification.resolveVerificationAdapter(rootDir).defaultArea;
1387
- const proofResult = captureVerifiedTreeProofFn(resolvedVerificationArea, rootDir, {
1388
- gitRunner,
1389
- runFn: child_process.spawnSync
1390
- });
1391
- if (!proofResult.ok) {
1392
- return {
1393
- ok: false,
1394
- error: 'verification-failed',
1395
- detail: proofResult.error || 'failed to verify published tree'
1396
- };
1397
- }
1398
-
1399
- const proofCheck = assertVerifiedTreeProofFn(proofResult.proof, rootDir, { gitRunner });
1400
- if (!proofCheck.ok) {
1401
- return {
1402
- ok: false,
1403
- error: 'verification-proof-mismatch',
1404
- detail: proofCheck.error || 'verification proof does not match the tree being published'
1405
- };
1406
- }
1407
-
1408
- const pushTarget = baseBranch || getPrimaryBranch(gitRunner);
1409
- const pushResult = gitRunner(['-C', rootDir, 'push', 'review', pushTarget]);
1410
- if (pushResult.status !== 0) {
1411
- return {
1412
- ok: false,
1413
- error: 'push-primary-failed',
1414
- detail: [pushResult.stdout, pushResult.stderr].filter(Boolean).join('\n').trim()
1415
- };
1416
- }
1417
-
1418
- return { ok: true, changed: true };
1245
+ function finalizeVariantACloseout({ slug, summary, mainTaskFile, rootDir = (0, mission_utils_js_1.getPrimaryWorktree)(), gitRunner = git_js_1.git, baseBranch = null, verificationArea = null, captureVerifiedTreeProofFn = verification.captureVerifiedTreeProof, assertVerifiedTreeProofFn = verification.assertVerifiedTreeProof }) {
1246
+ (0, mission_utils_js_1.softResetTrailingBacklogNoise)(rootDir, gitRunner);
1247
+ if (mainTaskFile && node_fs_1.default.existsSync(mainTaskFile)) {
1248
+ (0, backlog_js_1.completeTask)(slug, rootDir);
1249
+ // Re-resolve because it moved
1250
+ const updatedResolution = (0, backlog_js_1.resolveTaskFile)(slug, rootDir);
1251
+ if (updatedResolution.ok) {
1252
+ rewriteWorktreePaths(updatedResolution.taskFile || '', slug, { rootDir });
1253
+ }
1254
+ }
1255
+ gitRunner(['-C', rootDir, 'add', '-A']);
1256
+ const stagedDiff = gitRunner(['-C', rootDir, 'diff', '--cached', '--quiet']);
1257
+ if (stagedDiff.status === 0) {
1258
+ return { ok: true, changed: false };
1259
+ }
1260
+ const commitResult = gitRunner([
1261
+ '-C',
1262
+ rootDir,
1263
+ 'commit',
1264
+ '-m',
1265
+ `${(0, mission_utils_js_1.missionBranchName)(slug, rootDir)}: ${summary} integration closeout`
1266
+ ]);
1267
+ if (commitResult.status !== 0) {
1268
+ return {
1269
+ ok: false,
1270
+ error: 'commit-failed',
1271
+ detail: [commitResult.stdout, commitResult.stderr].filter(Boolean).join('\n').trim()
1272
+ };
1273
+ }
1274
+ const resolvedVerificationArea = verificationArea || verification.resolveVerificationAdapter(rootDir).defaultArea;
1275
+ const proofResult = captureVerifiedTreeProofFn(resolvedVerificationArea, rootDir, {
1276
+ gitRunner,
1277
+ runFn: node_child_process_1.default.spawnSync
1278
+ });
1279
+ if (!proofResult.ok) {
1280
+ return {
1281
+ ok: false,
1282
+ error: 'verification-failed',
1283
+ detail: proofResult.error || 'failed to verify published tree'
1284
+ };
1285
+ }
1286
+ const proofCheck = assertVerifiedTreeProofFn(proofResult.proof, rootDir, { gitRunner });
1287
+ if (!proofCheck.ok) {
1288
+ return {
1289
+ ok: false,
1290
+ error: 'verification-proof-mismatch',
1291
+ detail: proofCheck.error || 'verification proof does not match the tree being published'
1292
+ };
1293
+ }
1294
+ const pushTarget = baseBranch || (0, mission_utils_js_1.getPrimaryBranch)(gitRunner);
1295
+ const pushResult = gitRunner(['-C', rootDir, 'push', 'review', pushTarget]);
1296
+ if (pushResult.status !== 0) {
1297
+ return {
1298
+ ok: false,
1299
+ error: 'push-primary-failed',
1300
+ detail: [pushResult.stdout, pushResult.stderr].filter(Boolean).join('\n').trim()
1301
+ };
1302
+ }
1303
+ return { ok: true, changed: true };
1419
1304
  }
1420
-
1421
1305
  /**
1422
1306
  * @param {string} slug
1423
1307
  * @param{{rootDir?: string, gitRunner?: Function, removeDir?: Function, existsSync?: Function}} options
1424
1308
  */
1425
- function cleanupMissionWorktree(
1426
- slug,
1427
- {
1428
- rootDir = getPrimaryWorktree(),
1429
- gitRunner = git,
1430
- removeDir = (/** @type{string} */ target) => {
1431
- if (isForgejoPath(target, { forgejoHome: resolveForgejoHome() })) {
1309
+ function cleanupMissionWorktree(slug, { rootDir = (0, mission_utils_js_1.getPrimaryWorktree)(), gitRunner = git_js_1.git, removeDir = (target) => {
1310
+ if ((0, forgejo_js_1.isForgejoPath)(target, { forgejoHome: (0, forgejo_js_1.resolveForgejoHome)() })) {
1432
1311
  throw new Error(`CRITICAL SAFETY VIOLATION: cleanupMissionWorktree attempted to delete Forgejo home: ${target}`);
1433
- }
1434
- return fs.rmSync(target, { recursive: true, force: true });
1435
- },
1436
- existsSync = fs.existsSync
1437
- } = {}
1438
- ) {
1439
- const branch = missionBranchName(slug, rootDir);
1440
- const worktreePath = `${conventionalWorktreePath(slug, rootDir)}`;
1441
- const currentBranch = gitRunner(['-C', rootDir, 'branch', '--show-current']).stdout.trim();
1442
- if (currentBranch === branch) {
1443
- return false;
1444
- }
1445
-
1446
- const branchExists = gitRunner(['-C', rootDir, 'show-ref', '--verify', '--quiet', `refs/heads/${branch}`]);
1447
- if (branchExists.status !== 0) {
1448
- return false;
1449
- }
1450
-
1451
- const worktreeList = gitRunner(['-C', rootDir, 'worktree', 'list', '--porcelain']).stdout;
1452
- // @ts-expect-error line implicitly any
1453
- const isRegistered = worktreeList.split('\n').some(line => line.trim() === `worktree ${worktreePath}`);
1454
-
1455
- if (existsSync(worktreePath) && isRegistered) {
1456
- let removeResult = gitRunner(['-C', rootDir, 'worktree', 'remove', worktreePath]);
1457
- if (removeResult.status !== 0) {
1458
- removeResult = gitRunner(['-C', rootDir, 'worktree', 'remove', '--force', worktreePath]);
1459
- if (removeResult.status !== 0) {
1312
+ }
1313
+ return node_fs_1.default.rmSync(target, { recursive: true, force: true });
1314
+ }, existsSync = node_fs_1.default.existsSync } = {}) {
1315
+ const branch = (0, mission_utils_js_1.missionBranchName)(slug, rootDir);
1316
+ const worktreePath = `${(0, mission_utils_js_1.conventionalWorktreePath)(slug, rootDir)}`;
1317
+ const currentBranch = gitRunner(['-C', rootDir, 'branch', '--show-current']).stdout.trim();
1318
+ if (currentBranch === branch) {
1319
+ return false;
1320
+ }
1321
+ const branchExists = gitRunner(['-C', rootDir, 'show-ref', '--verify', '--quiet', `refs/heads/${branch}`]);
1322
+ if (branchExists.status !== 0) {
1460
1323
  return false;
1461
- }
1462
- }
1463
- }
1464
-
1465
- if (existsSync(worktreePath)) {
1466
- removeDir(worktreePath);
1467
- }
1468
-
1469
- // Prune stale prunable worktrees — e.g. leftover registrations at near-match
1470
- // paths like ${getPrimaryWorktree()}-<n> for the same mission branch —
1471
- // before attempting branch deletion. Otherwise `git branch -D` refuses because
1472
- // git still thinks the branch is checked out somewhere.
1473
- gitRunner(['-C', rootDir, 'worktree', 'prune']);
1474
-
1475
- const deleteBranchResult = gitRunner(['-C', rootDir, 'branch', '-D', branch]);
1476
- if (deleteBranchResult.status !== 0) {
1477
- return false;
1478
- }
1479
-
1480
- return !existsSync(worktreePath);
1324
+ }
1325
+ const worktreeList = gitRunner(['-C', rootDir, 'worktree', 'list', '--porcelain']).stdout;
1326
+ const isRegistered = worktreeList.split('\n').some((line) => line.trim() === `worktree ${worktreePath}`);
1327
+ if (existsSync(worktreePath) && isRegistered) {
1328
+ let removeResult = gitRunner(['-C', rootDir, 'worktree', 'remove', worktreePath]);
1329
+ if (removeResult.status !== 0) {
1330
+ removeResult = gitRunner(['-C', rootDir, 'worktree', 'remove', '--force', worktreePath]);
1331
+ if (removeResult.status !== 0) {
1332
+ return false;
1333
+ }
1334
+ }
1335
+ }
1336
+ if (existsSync(worktreePath)) {
1337
+ removeDir(worktreePath);
1338
+ }
1339
+ // Prune stale prunable worktrees — e.g. leftover registrations at near-match
1340
+ // paths like ${getPrimaryWorktree()}-<n> for the same mission branch —
1341
+ // before attempting branch deletion. Otherwise `git branch -D` refuses because
1342
+ // git still thinks the branch is checked out somewhere.
1343
+ gitRunner(['-C', rootDir, 'worktree', 'prune']);
1344
+ const deleteBranchResult = gitRunner(['-C', rootDir, 'branch', '-D', branch]);
1345
+ if (deleteBranchResult.status !== 0) {
1346
+ return false;
1347
+ }
1348
+ return !existsSync(worktreePath);
1481
1349
  }
1482
-
1483
1350
  /** @param {string} rootDir @param {string} slug */
1484
1351
  function findExistingSquashCommit(rootDir, slug) {
1485
- const result = git(['-C', rootDir, 'log', '--format=%H %s', '-50']);
1486
- if (result.status !== 0) {return null;}
1487
- const prefix = `${missionBranchName(slug, rootDir)}:`;
1488
- for (const line of result.stdout.trim().split('\n')) {
1489
- const spaceIdx = line.indexOf(' ');
1490
- if (spaceIdx === -1) {continue;}
1491
- const hash = line.slice(0, spaceIdx);
1492
- const subject = line.slice(spaceIdx + 1);
1493
- if (subject.startsWith(prefix)) {return hash;}
1494
- }
1495
- return null;
1352
+ const result = (0, git_js_1.git)(['-C', rootDir, 'log', '--format=%H %s', '-50']);
1353
+ if (result.status !== 0) {
1354
+ return null;
1355
+ }
1356
+ const prefix = `${(0, mission_utils_js_1.missionBranchName)(slug, rootDir)}:`;
1357
+ for (const line of result.stdout.trim().split('\n')) {
1358
+ const spaceIdx = line.indexOf(' ');
1359
+ if (spaceIdx === -1) {
1360
+ continue;
1361
+ }
1362
+ const hash = line.slice(0, spaceIdx);
1363
+ const subject = line.slice(spaceIdx + 1);
1364
+ if (subject.startsWith(prefix)) {
1365
+ return hash;
1366
+ }
1367
+ }
1368
+ return null;
1496
1369
  }
1497
-
1498
1370
  /**
1499
1371
  * Detect merge conflicts in the mission worktree and emit a bounded resolution plan.
1500
1372
  *
@@ -1514,154 +1386,151 @@ function findExistingSquashCommit(rootDir, slug) {
1514
1386
  * @param{{getConflictFilesFn: Function, resolveWorktreeFn?: Function, worktreePathOverride?: string|null, rootDir?: string, baseBranch?: string|null}} options
1515
1387
  */
1516
1388
  // @ts-expect-error resolveConflictsForMission options missing getConflictFilesFn
1517
- function resolveConflictsForMission(slug, area, { getConflictFilesFn, resolveWorktreeFn = resolveWorktree, worktreePathOverride = null, rootDir = getPrimaryWorktree(), baseBranch = null } = {}) {
1518
- // Resolve the actual attached worktree dynamically; fall back to the conventional
1519
- // path only when git worktree list has no entry for this mission branch.
1520
- const conventionalPath = `${conventionalWorktreePath(slug, rootDir)}`;
1521
- const resolvedPath = worktreePathOverride || resolveWorktreeFn(slug);
1522
- const worktreePath = resolvedPath || conventionalPath;
1523
- if (!resolvedPath) {
1524
- fmt.log.warn(`No registered worktree found for ${missionBranchName(slug, rootDir)}; falling back to conventional path.`);
1525
- }
1526
-
1527
- // Conflicts must be detected against the branch the mission integrates back
1528
- // into. For a feature-branch mission that is the recorded base branch, not the
1529
- // primary branch; for legacy missions resolveMissionBaseBranch returns primary.
1530
- let targetBranch = baseBranch;
1531
- if (!targetBranch) {
1532
- try { targetBranch = resolveMissionBaseBranch(slug, rootDir); } catch (_) { targetBranch = getPrimaryBranch(); }
1533
- }
1534
-
1535
- // @ts-expect-error getConflictFilesFn may be undefined
1536
- const detectFn = getConflictFilesFn || (() => getConflictFiles(worktreePath, targetBranch));
1537
-
1538
- if (!fs.existsSync(worktreePath)) {
1539
- fmt.log.fail(`Mission worktree not found: ${worktreePath}`);
1540
- fmt.log.info(`Expected path: ${worktreePath}`);
1541
- return { ok: false, error: 'worktree-missing', conflictFiles: [], sharedFiles: [], missionSpecificFiles: [], worktreePath };
1542
- }
1543
-
1544
- fmt.log.info(`Detecting conflicts in ${worktreePath} against ${targetBranch}...`);
1545
- let conflictFiles;
1546
- try {
1547
- conflictFiles = detectFn(worktreePath, targetBranch);
1548
- } catch (/** @type{any} */ err) {
1549
- fmt.log.fail('Merge check failed with a non-conflict error:');
1550
- fmt.log.fail(err.message);
1551
- return { ok: false, error: 'merge-failed', conflictFiles: [], sharedFiles: [], missionSpecificFiles: [], worktreePath };
1552
- }
1553
-
1554
- if (conflictFiles.length === 0) {
1555
- fmt.log.pass('No conflicts detected in mission worktree. Integration should proceed cleanly.');
1556
- fmt.log.info(`Retry: px integrate ${slug} --dry-run`);
1557
- return { ok: true, conflictFiles: [], sharedFiles: [], missionSpecificFiles: [], worktreePath };
1558
- }
1559
-
1560
- fmt.log.info(`Conflicting files (${conflictFiles.length}):`);
1561
- conflictFiles.forEach((/** @type{string} */ f) => fmt.log.info(` - ${f}`));
1562
-
1563
- // Derive the mission-doc directory prefix from the actual located mission dir
1564
- // so the classification works for non-standard doc paths (e.g. renamed year dir).
1565
- const taskPattern = new RegExp(`backlog/(?:tasks|completed)/[^/]*${slug}`);
1566
- const missionSpecificFiles = conflictFiles.filter((/** @type{string} */ f) =>
1567
- isMissionArtifact(f, slug, worktreePath) || taskPattern.test(f)
1568
- );
1569
- const sharedFiles = conflictFiles.filter((/** @type{string} */ f) => !missionSpecificFiles.includes(f));
1570
- const quotedWorktreePath = shellQuote(worktreePath);
1571
-
1572
- if (sharedFiles.length > 0) {
1573
- fmt.log.warn(`Conflicts in ${sharedFiles.length} shared file(s) require manual resolution:`);
1574
- sharedFiles.forEach((/** @type{string} */ f) => fmt.log.plain(` - ${f}`));
1575
- fmt.log.info('Manual resolution path:');
1389
+ function resolveConflictsForMission(slug, area, { getConflictFilesFn, resolveWorktreeFn = mission_utils_js_1.resolveWorktree, worktreePathOverride = null, rootDir = (0, mission_utils_js_1.getPrimaryWorktree)(), baseBranch = null } = {}) {
1390
+ // Resolve the actual attached worktree dynamically; fall back to the conventional
1391
+ // path only when git worktree list has no entry for this mission branch.
1392
+ const conventionalPath = `${(0, mission_utils_js_1.conventionalWorktreePath)(slug, rootDir)}`;
1393
+ const resolvedPath = worktreePathOverride || resolveWorktreeFn(slug);
1394
+ const worktreePath = resolvedPath || conventionalPath;
1395
+ if (!resolvedPath) {
1396
+ fmt.log.warn(`No registered worktree found for ${(0, mission_utils_js_1.missionBranchName)(slug, rootDir)}; falling back to conventional path.`);
1397
+ }
1398
+ // Conflicts must be detected against the branch the mission integrates back
1399
+ // into. For a feature-branch mission that is the recorded base branch, not the
1400
+ // primary branch; for legacy missions resolveMissionBaseBranch returns primary.
1401
+ /** @type {string} */
1402
+ let targetBranch = /** @type {string} */ (baseBranch || (0, mission_utils_js_1.getPrimaryBranch)());
1403
+ if (!targetBranch) {
1404
+ try {
1405
+ targetBranch = (0, mission_utils_js_1.resolveMissionBaseBranch)(slug, rootDir);
1406
+ }
1407
+ catch (_) {
1408
+ targetBranch = (0, mission_utils_js_1.getPrimaryBranch)();
1409
+ }
1410
+ }
1411
+ const detectFn = getConflictFilesFn || (() => (0, mission_utils_js_1.getConflictFiles)(worktreePath, targetBranch));
1412
+ if (!node_fs_1.default.existsSync(worktreePath)) {
1413
+ fmt.log.fail(`Mission worktree not found: ${worktreePath}`);
1414
+ fmt.log.info(`Expected path: ${worktreePath}`);
1415
+ return { ok: false, error: 'worktree-missing', conflictFiles: [], sharedFiles: [], missionSpecificFiles: [], worktreePath };
1416
+ }
1417
+ fmt.log.info(`Detecting conflicts in ${worktreePath} against ${targetBranch}...`);
1418
+ let conflictFiles;
1419
+ try {
1420
+ conflictFiles = detectFn(worktreePath, targetBranch);
1421
+ }
1422
+ catch (err) {
1423
+ fmt.log.fail('Merge check failed with a non-conflict error:');
1424
+ fmt.log.fail(err.message);
1425
+ return { ok: false, error: 'merge-failed', conflictFiles: [], sharedFiles: [], missionSpecificFiles: [], worktreePath };
1426
+ }
1427
+ if (conflictFiles.length === 0) {
1428
+ fmt.log.pass('No conflicts detected in mission worktree. Integration should proceed cleanly.');
1429
+ fmt.log.info(`Retry: px integrate ${slug} --dry-run`);
1430
+ return { ok: true, conflictFiles: [], sharedFiles: [], missionSpecificFiles: [], worktreePath };
1431
+ }
1432
+ fmt.log.info(`Conflicting files (${conflictFiles.length}):`);
1433
+ conflictFiles.forEach((f) => fmt.log.info(` - ${f}`));
1434
+ // Derive the mission-doc directory prefix from the actual located mission dir
1435
+ // so the classification works for non-standard doc paths (e.g. renamed year dir).
1436
+ const taskPattern = new RegExp(`backlog/(?:tasks|completed)/[^/]*${slug}`);
1437
+ const missionSpecificFiles = conflictFiles.filter((f) => (0, mission_utils_js_1.isMissionArtifact)(f, slug, worktreePath) || taskPattern.test(f));
1438
+ const sharedFiles = conflictFiles.filter((f) => !missionSpecificFiles.includes(f));
1439
+ const quotedWorktreePath = shellQuote(worktreePath);
1440
+ if (sharedFiles.length > 0) {
1441
+ fmt.log.warn(`Conflicts in ${sharedFiles.length} shared file(s) require manual resolution:`);
1442
+ sharedFiles.forEach((f) => fmt.log.plain(` - ${f}`));
1443
+ fmt.log.info('Manual resolution path:');
1444
+ fmt.log.info(` cd ${quotedWorktreePath}`);
1445
+ fmt.log.info(` git rebase ${targetBranch}`);
1446
+ fmt.log.info(' # resolve each shared file conflict manually');
1447
+ fmt.log.info(' git add <resolved-files>');
1448
+ fmt.log.info(' git rebase --continue');
1449
+ fmt.log.info(` ${formatVerificationCommand(area, worktreePath)}`);
1450
+ fmt.log.info(` px integrate ${slug} --dry-run`);
1451
+ return { ok: false, error: 'shared-file-conflicts', conflictFiles, sharedFiles, missionSpecificFiles, worktreePath };
1452
+ }
1453
+ fmt.log.info(`All ${missionSpecificFiles.length} conflict(s) are in mission-specific files.`);
1454
+ fmt.log.info('Skip-all-conflicts path (use --theirs to keep the mission version):');
1576
1455
  fmt.log.info(` cd ${quotedWorktreePath}`);
1577
1456
  fmt.log.info(` git rebase ${targetBranch}`);
1578
- fmt.log.info(' # resolve each shared file conflict manually');
1579
- fmt.log.info(' git add <resolved-files>');
1457
+ fmt.log.info(' # after the rebase pauses on conflicts:');
1458
+ missionSpecificFiles.forEach((f) => fmt.log.info(` git checkout --theirs "${f}" && git add "${f}"`));
1580
1459
  fmt.log.info(' git rebase --continue');
1581
1460
  fmt.log.info(` ${formatVerificationCommand(area, worktreePath)}`);
1582
1461
  fmt.log.info(` px integrate ${slug} --dry-run`);
1583
- return { ok: false, error: 'shared-file-conflicts', conflictFiles, sharedFiles, missionSpecificFiles, worktreePath };
1584
- }
1585
-
1586
- fmt.log.info(`All ${missionSpecificFiles.length} conflict(s) are in mission-specific files.`);
1587
- fmt.log.info('Skip-all-conflicts path (use --theirs to keep the mission version):');
1588
- fmt.log.info(` cd ${quotedWorktreePath}`);
1589
- fmt.log.info(` git rebase ${targetBranch}`);
1590
- fmt.log.info(' # after the rebase pauses on conflicts:');
1591
- missionSpecificFiles.forEach((/** @type{string} */ f) => fmt.log.info(` git checkout --theirs "${f}" && git add "${f}"`));
1592
- fmt.log.info(' git rebase --continue');
1593
- fmt.log.info(` ${formatVerificationCommand(area, worktreePath)}`);
1594
- fmt.log.info(` px integrate ${slug} --dry-run`);
1595
-
1596
- return { ok: true, conflictFiles, sharedFiles: [], missionSpecificFiles, worktreePath };
1462
+ return { ok: true, conflictFiles, sharedFiles: [], missionSpecificFiles, worktreePath };
1597
1463
  }
1598
-
1599
1464
  /** @param{string} slug @param{string} area @param{{rootDir?: string, worktreePath?: string, baseBranch?: string|null}} options */
1600
1465
  function buildConflictResolutionPrompt(slug = '<slug>', area = '<area>', options = {}) {
1601
- const rootDir = options.rootDir || getPrimaryWorktree();
1602
- const worktreePath = options.worktreePath || resolveWorktree(slug) || conventionalWorktreePath(slug, rootDir);
1603
- const quotedWorktreePath = shellQuote(worktreePath);
1604
- // Rebase guidance must target the branch the mission integrates back into: the
1605
- // recorded base for feature-branch missions, the primary branch otherwise.
1606
- let targetBranch = options.baseBranch;
1607
- if (!targetBranch) {
1608
- try { targetBranch = resolveMissionBaseBranch(slug, rootDir); } catch (_) { targetBranch = getPrimaryBranch(); }
1609
- }
1610
- return [
1611
- 'Conflict resolution options:',
1612
- '',
1613
- 'Option A — Agent-assisted (recommended):',
1614
- ' Copy/paste from the mission worktree:',
1615
- ` cd ${quotedWorktreePath} && px resolve-conflict ${slug}`,
1616
- ' (Detects conflict files, categorises mission-specific vs shared, and prints exact resolution commands.)',
1617
- '',
1618
- 'Option B — Manual rebase path:',
1619
- ` 1. Stay in the mission worktree (${worktreePath}); do not resolve non-trivial conflicts inside ${rootDir}.`,
1620
- ` cd ${quotedWorktreePath}`,
1621
- ' git status --short',
1622
- ` 2. Rebase the mission branch onto the local ${targetBranch} branch:`,
1623
- ` git fetch review ${targetBranch}`,
1624
- ` git rebase ${targetBranch}`,
1625
- ` 3. Re-run the mission gate: ${formatVerificationCommand(area, worktreePath)}`,
1626
- ` 4. Retry the dry-run integration preflight: px integrate ${slug} --dry-run`
1627
- ];
1466
+ const rootDir = options.rootDir || (0, mission_utils_js_1.getPrimaryWorktree)();
1467
+ const worktreePath = options.worktreePath || (0, mission_utils_js_1.resolveWorktree)(slug) || (0, mission_utils_js_1.conventionalWorktreePath)(slug, rootDir);
1468
+ const quotedWorktreePath = shellQuote(worktreePath);
1469
+ // Rebase guidance must target the branch the mission integrates back into: the
1470
+ // recorded base for feature-branch missions, the primary branch otherwise.
1471
+ let targetBranch = options.baseBranch;
1472
+ if (!targetBranch) {
1473
+ try {
1474
+ targetBranch = (0, mission_utils_js_1.resolveMissionBaseBranch)(slug, rootDir);
1475
+ }
1476
+ catch (_) {
1477
+ targetBranch = (0, mission_utils_js_1.getPrimaryBranch)();
1478
+ }
1479
+ }
1480
+ return [
1481
+ 'Conflict resolution options:',
1482
+ '',
1483
+ 'Option A — Agent-assisted (recommended):',
1484
+ ' Copy/paste from the mission worktree:',
1485
+ ` cd ${quotedWorktreePath} && px resolve-conflict ${slug}`,
1486
+ ' (Detects conflict files, categorises mission-specific vs shared, and prints exact resolution commands.)',
1487
+ '',
1488
+ 'Option B — Manual rebase path:',
1489
+ ` 1. Stay in the mission worktree (${worktreePath}); do not resolve non-trivial conflicts inside ${rootDir}.`,
1490
+ ` cd ${quotedWorktreePath}`,
1491
+ ' git status --short',
1492
+ ` 2. Rebase the mission branch onto the local ${targetBranch} branch:`,
1493
+ ` git fetch review ${targetBranch}`,
1494
+ ` git rebase ${targetBranch}`,
1495
+ ` 3. Re-run the mission gate: ${formatVerificationCommand(area, worktreePath)}`,
1496
+ ` 4. Retry the dry-run integration preflight: px integrate ${slug} --dry-run`
1497
+ ];
1628
1498
  }
1629
-
1630
- module.exports = integrate;
1631
- module.exports.resolveConflictsForMission = resolveConflictsForMission;
1632
- module.exports.cleanupMissionWorktree = cleanupMissionWorktree;
1633
- module.exports.rewriteWorktreePaths = rewriteWorktreePaths;
1634
- module.exports.finalizeVariantACloseout = finalizeVariantACloseout;
1635
- module.exports.isNoMergeToAbortResult = isNoMergeToAbortResult;
1636
- module.exports.buildConflictResolutionPrompt = buildConflictResolutionPrompt;
1637
- module.exports.VARIANT_B_AUTOMATION_SUMMARY = VARIANT_B_AUTOMATION_SUMMARY;
1638
- module.exports.stashMainCheckoutIfNeeded = stashMainCheckoutIfNeeded;
1639
- module.exports.restoreMainCheckoutStash = restoreMainCheckoutStash;
1640
- module.exports.getPrimaryWorktree = getPrimaryWorktree;
1641
- module.exports.evaluateTaskStatusForIntegration = evaluateTaskStatusForIntegration;
1642
- module.exports.promoteTaskForIntegrationIfNeeded = promoteTaskForIntegrationIfNeeded;
1643
- module.exports.findExistingSquashCommit = findExistingSquashCommit;
1644
- module.exports.printIntegrationPreflight = printIntegrationPreflight;
1645
- module.exports.resolveForgejoUserForIntegration = resolveForgejoUserForIntegration;
1646
- module.exports.getUnresolvedIndexConflicts = getUnresolvedIndexConflicts;
1647
- module.exports.parseStashPopCollisionFiles = parseStashPopCollisionFiles;
1648
- module.exports.reportStashPopFailure = reportStashPopFailure;
1649
- module.exports.maybeUpdateGraphifyOnPrimary = maybeUpdateGraphifyOnPrimary;
1650
- module.exports.SYNC_MERGED_DIAGNOSTICS = SYNC_MERGED_DIAGNOSTICS;
1651
- module.exports.printDiagnosticTable = printDiagnosticTable;
1652
- module.exports.reportSyncMergedFailure = reportSyncMergedFailure;
1653
- module.exports.recordPostIntegrationStats = recordPostIntegrationStats;
1654
- module.exports.recordPostIntegrationStatsOrAbort = recordPostIntegrationStatsOrAbort;
1655
- module.exports.formatRecordedStatsRow = formatRecordedStatsRow;
1656
-
1657
- // Integration gates exports
1658
- module.exports.detectChangedAreas = detectChangedAreas;
1659
- module.exports.parseFilesToAreas = parseFilesToAreas;
1660
- module.exports.loadIntegrationConfig = loadIntegrationConfig;
1661
- module.exports.getIntegrationGatePlan = getIntegrationGatePlan;
1662
- module.exports.printIntegrationGatePlan = printIntegrationGatePlan;
1663
- module.exports.buildIntegrationGateEnv = buildIntegrationGateEnv;
1664
- module.exports.resolveIntegrationVerificationWorktree = resolveIntegrationVerificationWorktree;
1665
- module.exports.buildIntegrationVerificationInvocation = buildIntegrationVerificationInvocation;
1666
- module.exports.executeIntegrationGates = executeIntegrationGates;
1667
- module.exports.buildIntegrationContext = buildIntegrationContext;
1499
+ // Attach all named exports as properties of the default export (mirrors original CJS shape for CommonJS require compatibility)
1500
+ integrate.resolveConflictsForMission = resolveConflictsForMission;
1501
+ integrate.cleanupMissionWorktree = cleanupMissionWorktree;
1502
+ integrate.rewriteWorktreePaths = rewriteWorktreePaths;
1503
+ integrate.finalizeVariantACloseout = finalizeVariantACloseout;
1504
+ integrate.isNoMergeToAbortResult = isNoMergeToAbortResult;
1505
+ integrate.buildConflictResolutionPrompt = buildConflictResolutionPrompt;
1506
+ integrate.VARIANT_B_AUTOMATION_SUMMARY = VARIANT_B_AUTOMATION_SUMMARY;
1507
+ integrate.stashMainCheckoutIfNeeded = stashMainCheckoutIfNeeded;
1508
+ integrate.restoreMainCheckoutStash = restoreMainCheckoutStash;
1509
+ integrate.evaluateTaskStatusForIntegration = evaluateTaskStatusForIntegration;
1510
+ integrate.promoteTaskForIntegrationIfNeeded = promoteTaskForIntegrationIfNeeded;
1511
+ integrate.findExistingSquashCommit = findExistingSquashCommit;
1512
+ integrate.printIntegrationPreflight = printIntegrationPreflight;
1513
+ integrate.resolveForgejoUserForIntegration = resolveForgejoUserForIntegration;
1514
+ integrate.getUnresolvedIndexConflicts = getUnresolvedIndexConflicts;
1515
+ integrate.parseStashPopCollisionFiles = parseStashPopCollisionFiles;
1516
+ integrate.reportStashPopFailure = reportStashPopFailure;
1517
+ integrate.maybeUpdateGraphifyOnPrimary = maybeUpdateGraphifyOnPrimary;
1518
+ integrate.SYNC_MERGED_DIAGNOSTICS = SYNC_MERGED_DIAGNOSTICS;
1519
+ integrate.printDiagnosticTable = printDiagnosticTable;
1520
+ integrate.reportSyncMergedFailure = reportSyncMergedFailure;
1521
+ integrate.recordPostIntegrationStats = recordPostIntegrationStats;
1522
+ integrate.recordPostIntegrationStatsOrAbort = recordPostIntegrationStatsOrAbort;
1523
+ integrate.formatRecordedStatsRow = formatRecordedStatsRow;
1524
+ integrate.detectChangedAreas = detectChangedAreas;
1525
+ integrate.parseFilesToAreas = parseFilesToAreas;
1526
+ integrate.loadIntegrationConfig = loadIntegrationConfig;
1527
+ integrate.getIntegrationGatePlan = getIntegrationGatePlan;
1528
+ integrate.printIntegrationGatePlan = printIntegrationGatePlan;
1529
+ integrate.buildIntegrationGateEnv = buildIntegrationGateEnv;
1530
+ integrate.resolveIntegrationVerificationWorktree = resolveIntegrationVerificationWorktree;
1531
+ integrate.buildIntegrationVerificationInvocation = buildIntegrationVerificationInvocation;
1532
+ integrate.executeIntegrationGates = executeIntegrationGates;
1533
+ integrate.buildIntegrationContext = buildIntegrationContext;
1534
+ // Re-export getPrimaryWorktree from mission-utils
1535
+ integrate.getPrimaryWorktree = mission_utils_js_1.getPrimaryWorktree;
1536
+ module.exports = (integrate);