@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.
- package/index.js +190 -185
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1332 -1463
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +7 -5
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +226 -210
|
@@ -1,428 +1,423 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
17
|
-
const
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 =
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}));
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
533
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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 (!
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
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
|
-
|
|
689
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
739
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
if (
|
|
776
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
if (
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
}
|
|
1170
|
-
if (
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
})
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
|
|
1288
|
-
|
|
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
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
slug
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
};
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
)
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
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
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
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
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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(' #
|
|
1579
|
-
fmt.log.info(
|
|
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:
|
|
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
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
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
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
module.exports
|
|
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);
|