substrate-ai 0.20.147 → 0.20.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter-registry-CauYNA1U.js +4 -0
- package/dist/cli/index.js +42 -26
- package/dist/{decisions-qFnqErZ5.js → decisions-De43bvYq.js} +1 -1
- package/dist/{dist-DT378h1M.js → dist-C2CAZDep.js} +912 -27
- package/dist/{errors-BGE3kaH8.js → errors-Db8EIM9w.js} +2 -2
- package/dist/{experimenter-COLnRT8i.js → experimenter-Dm5jUPO6.js} +1 -1
- package/dist/{health-nQ_Txd5T.js → health-ByQadq3J.js} +3 -3
- package/dist/{health-BJoVPIyR.js → health-WGcvcIYK.js} +3 -3
- package/dist/{index-DaBYDiWn.d.ts → index-DxeIRBBf.d.ts} +10 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +3 -3
- package/dist/{interactive-prompt-xJ3K-IUQ.js → interactive-prompt-CMBlj6UF.js} +2 -2
- package/dist/{manifest-read-DlWwZpfO.js → manifest-read-knD2uWkz.js} +2 -2
- package/dist/modules/interactive-prompt/index.js +3 -3
- package/dist/{routing-Dz4GQta5.js → routing-B3sgH5MZ.js} +1 -1
- package/dist/{run-BBVZdkEo.js → run-BAUlPom3.js} +210 -838
- package/dist/{run-D3mhgkIL.js → run-CtIc7_rK.js} +5 -5
- package/dist/src/modules/recovery-engine/index.d.ts +1 -1
- package/dist/{upgrade-V0xYGkQs.js → upgrade-B3iamwjO.js} +2 -2
- package/dist/{upgrade-BVHnUYUd.js → upgrade-DOmKCyOY.js} +2 -2
- package/dist/{version-manager-impl-BWqvme_f.js → version-manager-impl-DizCvYy8.js} +1 -1
- package/package.json +1 -1
- package/dist/adapter-registry-SL7-DtOW.js +0 -4
- /package/dist/{adapter-registry-DIcrxjH8.js → adapter-registry-DXLMTmfD.js} +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { BMAD_BASELINE_TOKENS_FULL, FileKvStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, validateStoryKey } from "./health-
|
|
1
|
+
import { BMAD_BASELINE_TOKENS_FULL, FileKvStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, validateStoryKey } from "./health-ByQadq3J.js";
|
|
2
2
|
import { createLogger } from "./logger-KeHncl-f.js";
|
|
3
3
|
import { TypedEventBusImpl, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-CElYrONe.js";
|
|
4
|
-
import { ADVISORY_NOTES, CLAUDE_AUTH_FAILURE_HINT, CODEX_SANDBOX_BLOCK_HINT, Categorizer, ConsumerAnalyzer, DEFAULT_GLOBAL_SETTINGS, DispatcherImpl, DoltClient, ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, EfficiencyScorer, IngestionServer, LogTurnAnalyzer, OPERATIONAL_FINDING, Recommender, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, STORY_METRICS, STORY_OUTCOME, SubstrateConfigSchema, TEST_EXPANSION_FINDING, TEST_PLAN, TelemetryNormalizer, TelemetryPipeline, TurnAnalyzer, addTokenUsage, aggregateTokenUsageForRun, aggregateTokenUsageForStory, callLLM, classifyVersionGap, createConfigSystem, createDatabaseAdapter$1, createDecision, createPipelineRun, createRequirement,
|
|
5
|
-
import { FindingsInjector, RunManifest, RuntimeProbeListSchema, applyConfigToGraph, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, parseRuntimeProbes, renderFindings, resolveGraphPath, resolveMainRepoRoot, runAcTraceabilityCheck, runStaleVerificationRecovery } from "./manifest-read-
|
|
4
|
+
import { ADVISORY_NOTES, BRANCH_PREFIX, CLAUDE_AUTH_FAILURE_HINT, CODEX_SANDBOX_BLOCK_HINT, Categorizer, ConsumerAnalyzer, DEFAULT_GLOBAL_SETTINGS, DispatcherImpl, DoltClient, ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, EfficiencyScorer, IngestionServer, LogTurnAnalyzer, OPERATIONAL_FINDING, Recommender, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, STORY_METRICS, STORY_OUTCOME, SubstrateConfigSchema, TEST_EXPANSION_FINDING, TEST_PLAN, TelemetryNormalizer, TelemetryPipeline, TurnAnalyzer, addTokenUsage, aggregateTokenUsageForRun, aggregateTokenUsageForStory, callLLM, classifyVersionGap, createConfigSystem, createDatabaseAdapter$1, createDecision, createGitWorktreeManager, createPipelineRun, createRequirement, detectClaudeAuthFailure, detectCodexSandboxBlock, detectInterfaceChanges, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunMetrics, getRunningPipelineRuns, getStoryMetricsForRun, getTokenUsageSummary, initSchema, listRequirements, loadModelRoutingConfig, registerArtifact, swallowDebug, updatePipelineRun, updatePipelineRunConfig, upsertDecision, writeRunMetrics, writeStoryMetrics } from "./dist-C2CAZDep.js";
|
|
5
|
+
import { FindingsInjector, RunManifest, RuntimeProbeListSchema, applyConfigToGraph, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, parseRuntimeProbes, renderFindings, resolveGraphPath, resolveMainRepoRoot, runAcTraceabilityCheck, runStaleVerificationRecovery } from "./manifest-read-knD2uWkz.js";
|
|
6
6
|
import { WorkGraphRepository, detectCycles } from "./work-graph-repository-DZyJv5pV.js";
|
|
7
7
|
import { deriveExitCode, routeDecision } from "./decision-router-BAPpON_C.js";
|
|
8
|
-
import { runInteractivePrompt } from "./interactive-prompt-
|
|
8
|
+
import { runInteractivePrompt } from "./interactive-prompt-CMBlj6UF.js";
|
|
9
9
|
import { runRecoveryEngine } from "./recovery-engine-BKGBeBnW.js";
|
|
10
10
|
import { basename, dirname, extname, join } from "path";
|
|
11
11
|
import { access, readFile, readdir, stat } from "fs/promises";
|
|
@@ -15,14 +15,12 @@ import yaml from "js-yaml";
|
|
|
15
15
|
import * as actualFS from "node:fs";
|
|
16
16
|
import { accessSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
|
|
17
17
|
import { exec, execFile, execFileSync, execSync, spawn } from "node:child_process";
|
|
18
|
-
import * as path$5 from "node:path";
|
|
19
|
-
import * as path$4 from "node:path";
|
|
20
18
|
import * as path$2 from "node:path";
|
|
21
19
|
import path, { basename as basename$1, dirname as dirname$1, extname as extname$1, isAbsolute, join as join$1, posix, relative, resolve as resolve$1, win32 } from "node:path";
|
|
22
20
|
import { tmpdir } from "node:os";
|
|
23
21
|
import { createHash, randomUUID } from "node:crypto";
|
|
24
22
|
import { z } from "zod";
|
|
25
|
-
import { access as access$1,
|
|
23
|
+
import { access as access$1, lstat, mkdir as mkdir$1, readFile as readFile$1, readdir as readdir$1, readlink, realpath, rename, stat as stat$1, unlink as unlink$1, writeFile as writeFile$1 } from "node:fs/promises";
|
|
26
24
|
import { existsSync as existsSync$1, lstatSync, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdir as readdir$2, readdirSync as readdirSync$1, readlinkSync, realpathSync as realpathSync$1, unlinkSync as unlinkSync$1, writeFileSync as writeFileSync$1 } from "fs";
|
|
27
25
|
import { promisify } from "node:util";
|
|
28
26
|
import { fileURLToPath } from "node:url";
|
|
@@ -34,780 +32,6 @@ import { channel, tracingChannel } from "node:diagnostics_channel";
|
|
|
34
32
|
import Stream from "node:stream";
|
|
35
33
|
import { StringDecoder } from "node:string_decoder";
|
|
36
34
|
|
|
37
|
-
//#region packages/core/dist/git/git-utils.js
|
|
38
|
-
const MIN_GIT_MAJOR = 2;
|
|
39
|
-
const MIN_GIT_MINOR = 20;
|
|
40
|
-
/**
|
|
41
|
-
* Spawn a git subprocess with the given args.
|
|
42
|
-
*
|
|
43
|
-
* @param args - Arguments to pass to git (e.g., ['worktree', 'add', ...])
|
|
44
|
-
* @param options - Optional spawn options (cwd, env)
|
|
45
|
-
* @returns - Object with stdout, stderr, and exit code
|
|
46
|
-
*/
|
|
47
|
-
function spawnGit(args, options) {
|
|
48
|
-
return new Promise((resolve$6) => {
|
|
49
|
-
const proc$1 = spawn("git", args, {
|
|
50
|
-
cwd: options?.cwd,
|
|
51
|
-
env: options?.env ?? process.env,
|
|
52
|
-
stdio: [
|
|
53
|
-
"ignore",
|
|
54
|
-
"pipe",
|
|
55
|
-
"pipe"
|
|
56
|
-
]
|
|
57
|
-
});
|
|
58
|
-
let stdout = "";
|
|
59
|
-
let stderr = "";
|
|
60
|
-
proc$1.stdout?.on("data", (chunk) => {
|
|
61
|
-
stdout += chunk.toString();
|
|
62
|
-
});
|
|
63
|
-
proc$1.stderr?.on("data", (chunk) => {
|
|
64
|
-
stderr += chunk.toString();
|
|
65
|
-
});
|
|
66
|
-
proc$1.on("close", (code) => {
|
|
67
|
-
resolve$6({
|
|
68
|
-
stdout: stdout.trim(),
|
|
69
|
-
stderr: stderr.trim(),
|
|
70
|
-
code: code ?? 1
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
proc$1.on("error", (err) => {
|
|
74
|
-
resolve$6({
|
|
75
|
-
stdout: "",
|
|
76
|
-
stderr: err.message,
|
|
77
|
-
code: 1
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Get the installed git version string.
|
|
84
|
-
*
|
|
85
|
-
* @returns Version string like "2.42.0"
|
|
86
|
-
* @throws Error if git is not installed or version cannot be parsed
|
|
87
|
-
*/
|
|
88
|
-
async function getGitVersion() {
|
|
89
|
-
const result = await spawnGit(["--version"]);
|
|
90
|
-
if (result.code !== 0) throw new Error(`git --version failed: ${result.stderr}`);
|
|
91
|
-
const match$2 = /git version\s+(\d+\.\d+(?:\.\d+)?)/.exec(result.stdout);
|
|
92
|
-
if (match$2 === null || match$2[1] === void 0) throw new Error(`Unable to parse git version from output: "${result.stdout}"`);
|
|
93
|
-
return match$2[1];
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Parse a git version string into major/minor/patch components.
|
|
97
|
-
*
|
|
98
|
-
* @param versionString - Version string like "2.42.0" or "2.20"
|
|
99
|
-
* @returns - Parsed version components
|
|
100
|
-
*/
|
|
101
|
-
function parseGitVersion(versionString) {
|
|
102
|
-
const parts = versionString.split(".").map(Number);
|
|
103
|
-
return {
|
|
104
|
-
major: parts[0] ?? 0,
|
|
105
|
-
minor: parts[1] ?? 0,
|
|
106
|
-
patch: parts[2] ?? 0
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Check if the given git version string is >= 2.20.0.
|
|
111
|
-
*
|
|
112
|
-
* @param version - Version string like "2.42.0"
|
|
113
|
-
* @returns - true if version >= 2.20.0
|
|
114
|
-
*/
|
|
115
|
-
function isGitVersionSupported(version) {
|
|
116
|
-
const { major, minor } = parseGitVersion(version);
|
|
117
|
-
if (major > MIN_GIT_MAJOR) return true;
|
|
118
|
-
if (major === MIN_GIT_MAJOR && minor >= MIN_GIT_MINOR) return true;
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Verify that git is installed and version >= 2.20.
|
|
123
|
-
*
|
|
124
|
-
* @throws Error with clear message if git is not installed or too old
|
|
125
|
-
*/
|
|
126
|
-
async function verifyGitVersion() {
|
|
127
|
-
let version;
|
|
128
|
-
try {
|
|
129
|
-
version = await getGitVersion();
|
|
130
|
-
} catch (err) {
|
|
131
|
-
throw new Error(`git is not installed or could not be executed. Please install git 2.20 or newer. Details: ${String(err)}`);
|
|
132
|
-
}
|
|
133
|
-
if (!isGitVersionSupported(version)) {
|
|
134
|
-
const { major, minor, patch } = parseGitVersion(version);
|
|
135
|
-
throw new Error(`Git version ${major}.${minor}.${patch} is too old. Substrate requires git 2.20 or newer. Please upgrade git: https://git-scm.com/downloads`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Decide whether a registered worktree from a prior dispatch can be reclaimed
|
|
140
|
-
* for a fresh re-run. Safe only when there is nothing to lose: no uncommitted
|
|
141
|
-
* changes AND no commits on the branch beyond the base branch. A negative
|
|
142
|
-
* `commitsAhead` means the ahead-count could not be determined → not safe.
|
|
143
|
-
*
|
|
144
|
-
* Pure + exported for testing (the git I/O that produces the inputs lives in
|
|
145
|
-
* createWorktree).
|
|
146
|
-
*/
|
|
147
|
-
function decideWorktreeReclaim(hasUncommittedChanges, commitsAhead, baseBranch) {
|
|
148
|
-
if (hasUncommittedChanges) return {
|
|
149
|
-
safe: false,
|
|
150
|
-
reason: "it has uncommitted changes that are NOT on the branch"
|
|
151
|
-
};
|
|
152
|
-
if (commitsAhead > 0) return {
|
|
153
|
-
safe: false,
|
|
154
|
-
reason: `its branch has ${String(commitsAhead)} commit(s) beyond ${baseBranch}`
|
|
155
|
-
};
|
|
156
|
-
if (commitsAhead < 0) return {
|
|
157
|
-
safe: false,
|
|
158
|
-
reason: "its state could not be verified as safe to discard"
|
|
159
|
-
};
|
|
160
|
-
return { safe: true };
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Decide whether removing a story worktree AND deleting its branch is safe
|
|
164
|
-
* (H0.3, field findings #17/#19). Unsafe when:
|
|
165
|
-
* - the worktree has uncommitted changes (removal destroys the only copy), or
|
|
166
|
-
* - the branch carries commits not reachable from the project's current HEAD
|
|
167
|
-
* (branch -D destroys them — this is where H0.1's wip checkpoints live), or
|
|
168
|
-
* - either state could not be determined (negative unmergedCommits).
|
|
169
|
-
*
|
|
170
|
-
* Pure + exported for testing (mirrors decideWorktreeReclaim; the git I/O that
|
|
171
|
-
* produces the inputs lives in inspectWorktreeRemovalSafety).
|
|
172
|
-
*/
|
|
173
|
-
function decideWorktreeRemoval(hasUncommittedChanges, uncommittedFiles, unmergedCommits, branchName) {
|
|
174
|
-
const reasons = [];
|
|
175
|
-
if (hasUncommittedChanges) {
|
|
176
|
-
const preview = uncommittedFiles.slice(0, 10).join(", ");
|
|
177
|
-
const more = uncommittedFiles.length > 10 ? ` (+${String(uncommittedFiles.length - 10)} more)` : "";
|
|
178
|
-
reasons.push(`the worktree has ${String(uncommittedFiles.length)} uncommitted change(s) that removal would destroy` + (preview.length > 0 ? `: ${preview}${more}` : ""));
|
|
179
|
-
}
|
|
180
|
-
if (unmergedCommits > 0) reasons.push(`branch ${branchName} carries ${String(unmergedCommits)} commit(s) not reachable from the current HEAD — deleting the branch would destroy them`);
|
|
181
|
-
if (unmergedCommits < 0) reasons.push("the branch state could not be verified as safe to discard");
|
|
182
|
-
return {
|
|
183
|
-
safe: reasons.length === 0,
|
|
184
|
-
reasons
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Gather the inputs for `decideWorktreeRemoval` from git. Thin I/O wrapper:
|
|
189
|
-
* - `git status --porcelain` inside the worktree (skipped when the directory
|
|
190
|
-
* is missing — nothing on disk to lose)
|
|
191
|
-
* - `git rev-list --count HEAD..<branch>` in the project root (commits the
|
|
192
|
-
* branch has that the current checkout does not; 0 when the branch is
|
|
193
|
-
* merged or absent, -1 when the count could not be determined)
|
|
194
|
-
*/
|
|
195
|
-
async function inspectWorktreeRemovalSafety(worktreePath, projectRoot, branchName) {
|
|
196
|
-
let hasUncommittedChanges = false;
|
|
197
|
-
let uncommittedFiles = [];
|
|
198
|
-
const worktreeOnDisk = await access$1(worktreePath).then(() => true).catch(() => false);
|
|
199
|
-
if (worktreeOnDisk) {
|
|
200
|
-
const statusResult = await spawnGit(["status", "--porcelain"], { cwd: worktreePath });
|
|
201
|
-
if (statusResult.code === 0) {
|
|
202
|
-
uncommittedFiles = statusResult.stdout.split("\n").filter((line) => line.trim().length > 0).map((line) => line.slice(3).trim());
|
|
203
|
-
hasUncommittedChanges = uncommittedFiles.length > 0;
|
|
204
|
-
} else return decideWorktreeRemoval(false, [], -1, branchName);
|
|
205
|
-
}
|
|
206
|
-
const branchExists = (await spawnGit([
|
|
207
|
-
"rev-parse",
|
|
208
|
-
"--verify",
|
|
209
|
-
branchName
|
|
210
|
-
], { cwd: projectRoot })).code === 0;
|
|
211
|
-
let unmergedCommits = 0;
|
|
212
|
-
if (branchExists) {
|
|
213
|
-
const aheadResult = await spawnGit([
|
|
214
|
-
"rev-list",
|
|
215
|
-
"--count",
|
|
216
|
-
`HEAD..${branchName}`
|
|
217
|
-
], { cwd: projectRoot });
|
|
218
|
-
unmergedCommits = aheadResult.code === 0 ? Number.parseInt(aheadResult.stdout.trim(), 10) || 0 : -1;
|
|
219
|
-
}
|
|
220
|
-
return decideWorktreeRemoval(hasUncommittedChanges, uncommittedFiles, unmergedCommits, branchName);
|
|
221
|
-
}
|
|
222
|
-
async function createWorktree(projectRoot, taskId, branchName, baseBranch, copyFiles = []) {
|
|
223
|
-
const worktreePath = path$5.join(projectRoot, ".substrate-worktrees", taskId);
|
|
224
|
-
const worktreeExists = await access$1(worktreePath).then(() => true).catch((err) => {
|
|
225
|
-
if (err.code === "ENOENT") return false;
|
|
226
|
-
throw err;
|
|
227
|
-
});
|
|
228
|
-
if (worktreeExists) {
|
|
229
|
-
const listResult = await spawnGit([
|
|
230
|
-
"worktree",
|
|
231
|
-
"list",
|
|
232
|
-
"--porcelain"
|
|
233
|
-
], { cwd: projectRoot });
|
|
234
|
-
const registeredPaths = listResult.stdout.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => line.slice(9).trim());
|
|
235
|
-
const isRegistered = registeredPaths.includes(worktreePath);
|
|
236
|
-
if (!isRegistered) await rm(worktreePath, {
|
|
237
|
-
recursive: true,
|
|
238
|
-
force: true
|
|
239
|
-
});
|
|
240
|
-
else {
|
|
241
|
-
const statusResult = await spawnGit(["status", "--porcelain"], { cwd: worktreePath });
|
|
242
|
-
const hasUncommittedChanges = statusResult.code === 0 && statusResult.stdout.trim().length > 0;
|
|
243
|
-
const aheadResult = await spawnGit([
|
|
244
|
-
"rev-list",
|
|
245
|
-
"--count",
|
|
246
|
-
`${baseBranch}..${branchName}`
|
|
247
|
-
], { cwd: projectRoot });
|
|
248
|
-
const commitsAhead = aheadResult.code === 0 ? Number.parseInt(aheadResult.stdout.trim(), 10) || 0 : -1;
|
|
249
|
-
const decision = decideWorktreeReclaim(hasUncommittedChanges, commitsAhead, baseBranch);
|
|
250
|
-
if (!decision.safe) throw new Error(`Worktree at ${worktreePath} is already registered (branch: ${branchName}) and ${decision.reason}.\nIt was preserved from a prior dispatch for inspection — inspect before removing.\n\nTo remove and re-dispatch:\n substrate worktrees --cleanup ${taskId}\n\nTo remove all substrate worktrees:\n substrate worktrees --cleanup`);
|
|
251
|
-
await spawnGit([
|
|
252
|
-
"worktree",
|
|
253
|
-
"remove",
|
|
254
|
-
"--force",
|
|
255
|
-
worktreePath
|
|
256
|
-
], { cwd: projectRoot });
|
|
257
|
-
await spawnGit([
|
|
258
|
-
"branch",
|
|
259
|
-
"-D",
|
|
260
|
-
branchName
|
|
261
|
-
], { cwd: projectRoot });
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const addResult = await spawnGit([
|
|
265
|
-
"worktree",
|
|
266
|
-
"add",
|
|
267
|
-
worktreePath,
|
|
268
|
-
"-b",
|
|
269
|
-
branchName,
|
|
270
|
-
baseBranch
|
|
271
|
-
], { cwd: projectRoot });
|
|
272
|
-
if (addResult.code !== 0) throw new Error(`git worktree add failed for task "${taskId}": ${addResult.stderr || addResult.stdout}`);
|
|
273
|
-
await copyFilesToWorktree(projectRoot, worktreePath, copyFiles);
|
|
274
|
-
return { worktreePath };
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Copy files from a source directory into a target worktree.
|
|
278
|
-
*
|
|
279
|
-
* Skips missing files silently (intentional — config like `[".env",
|
|
280
|
-
* ".env.local"]` should not blow up if `.env.local` doesn't exist in the
|
|
281
|
-
* parent checkout). Creates parent directories for nested paths.
|
|
282
|
-
*
|
|
283
|
-
* Exported for testability; not part of the public worktree API.
|
|
284
|
-
*/
|
|
285
|
-
async function copyFilesToWorktree(sourceRoot, worktreePath, files) {
|
|
286
|
-
if (files.length === 0) return;
|
|
287
|
-
for (const relativePath of files) {
|
|
288
|
-
if (path$5.isAbsolute(relativePath) || relativePath.split(path$5.sep).includes("..")) continue;
|
|
289
|
-
const sourcePath = path$5.join(sourceRoot, relativePath);
|
|
290
|
-
const destPath = path$5.join(worktreePath, relativePath);
|
|
291
|
-
try {
|
|
292
|
-
await access$1(sourcePath);
|
|
293
|
-
} catch {
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
const destDir = path$5.dirname(destPath);
|
|
297
|
-
if (destDir !== worktreePath) await mkdir$1(destDir, { recursive: true });
|
|
298
|
-
await copyFile(sourcePath, destPath);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Remove a git worktree by path.
|
|
303
|
-
*
|
|
304
|
-
* Uses `git worktree remove --force` to handle unclean worktrees.
|
|
305
|
-
*
|
|
306
|
-
* @param worktreePath - Absolute path to the worktree directory
|
|
307
|
-
* @param projectRoot - Absolute path to the git repository root
|
|
308
|
-
* @throws - Error if git command fails
|
|
309
|
-
*/
|
|
310
|
-
async function removeWorktree(worktreePath, projectRoot) {
|
|
311
|
-
const spawnOpts = {};
|
|
312
|
-
if (projectRoot !== void 0) spawnOpts.cwd = projectRoot;
|
|
313
|
-
const result = await spawnGit([
|
|
314
|
-
"worktree",
|
|
315
|
-
"remove",
|
|
316
|
-
"--force",
|
|
317
|
-
worktreePath
|
|
318
|
-
], spawnOpts);
|
|
319
|
-
if (result.code !== 0) throw new Error(`git worktree remove failed for "${worktreePath}": ${result.stderr || result.stdout}`);
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Delete a git branch using `git branch -D`.
|
|
323
|
-
*
|
|
324
|
-
* @param branchName - Branch name to delete (e.g., "substrate/story-abc123")
|
|
325
|
-
* @param projectRoot - Absolute path to the git repository root
|
|
326
|
-
* @returns - true if branch was successfully deleted, false otherwise
|
|
327
|
-
*/
|
|
328
|
-
async function removeBranch(branchName, projectRoot) {
|
|
329
|
-
const spawnOpts = {};
|
|
330
|
-
if (projectRoot !== void 0) spawnOpts.cwd = projectRoot;
|
|
331
|
-
const result = await spawnGit([
|
|
332
|
-
"branch",
|
|
333
|
-
"-D",
|
|
334
|
-
branchName
|
|
335
|
-
], spawnOpts);
|
|
336
|
-
if (result.code !== 0) return false;
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Scan the worktrees base directory and return paths of all found worktree directories.
|
|
341
|
-
*
|
|
342
|
-
* @param projectRoot - Absolute path to the git repository root
|
|
343
|
-
* @param baseDirectory - Relative directory name for worktrees (default: '.substrate-worktrees')
|
|
344
|
-
* @returns - Array of absolute worktree directory paths
|
|
345
|
-
*/
|
|
346
|
-
async function getOrphanedWorktrees(projectRoot, baseDirectory = ".substrate-worktrees") {
|
|
347
|
-
const worktreesDir = path$5.join(projectRoot, baseDirectory);
|
|
348
|
-
try {
|
|
349
|
-
await access$1(worktreesDir);
|
|
350
|
-
} catch {
|
|
351
|
-
return [];
|
|
352
|
-
}
|
|
353
|
-
let entries;
|
|
354
|
-
try {
|
|
355
|
-
entries = await readdir$1(worktreesDir, { withFileTypes: true });
|
|
356
|
-
} catch {
|
|
357
|
-
return [];
|
|
358
|
-
}
|
|
359
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) => path$5.join(worktreesDir, entry.name));
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Simulate a merge using git merge --no-commit --no-ff without committing.
|
|
363
|
-
*
|
|
364
|
-
* This runs in the target branch's working directory (the project root or
|
|
365
|
-
* worktree path). The simulation must be aborted after checking conflicts
|
|
366
|
-
* via abortMerge().
|
|
367
|
-
*
|
|
368
|
-
* @param branchName - The source branch to simulate merging
|
|
369
|
-
* @param cwd - Working directory (must be on the target branch)
|
|
370
|
-
* @returns - true if merge would be clean, false if there are conflicts
|
|
371
|
-
*/
|
|
372
|
-
async function simulateMerge(branchName, cwd) {
|
|
373
|
-
const result = await spawnGit([
|
|
374
|
-
"merge",
|
|
375
|
-
"--no-commit",
|
|
376
|
-
"--no-ff",
|
|
377
|
-
branchName
|
|
378
|
-
], { cwd });
|
|
379
|
-
if (result.code === 0) return true;
|
|
380
|
-
return false;
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Abort a merge in progress using git merge --abort.
|
|
384
|
-
*
|
|
385
|
-
* Should be called after simulateMerge() to clean up the merge state,
|
|
386
|
-
* regardless of whether conflicts were found.
|
|
387
|
-
*
|
|
388
|
-
* @param cwd - Working directory (same as used for simulateMerge)
|
|
389
|
-
*/
|
|
390
|
-
async function abortMerge(cwd) {
|
|
391
|
-
await spawnGit(["merge", "--abort"], { cwd });
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Get a list of files with conflicts during a merge.
|
|
395
|
-
*
|
|
396
|
-
* Must be called while a merge is in progress (after simulateMerge() and
|
|
397
|
-
* before abortMerge()).
|
|
398
|
-
*
|
|
399
|
-
* @param cwd - Working directory of the repository
|
|
400
|
-
* @returns - Array of conflicting file paths
|
|
401
|
-
*/
|
|
402
|
-
async function getConflictingFiles(cwd) {
|
|
403
|
-
const result = await spawnGit([
|
|
404
|
-
"diff",
|
|
405
|
-
"--name-only",
|
|
406
|
-
"--diff-filter=U"
|
|
407
|
-
], { cwd });
|
|
408
|
-
if (result.code !== 0) return [];
|
|
409
|
-
if (result.stdout.trim() === "") return [];
|
|
410
|
-
return result.stdout.trim().split("\n").filter((f$1) => f$1.trim().length > 0);
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Perform an actual merge using git merge --no-ff.
|
|
414
|
-
*
|
|
415
|
-
* Should only be called after detectConflicts() confirms there are no conflicts.
|
|
416
|
-
* Creates a merge commit even if fast-forward is possible (--no-ff ensures history).
|
|
417
|
-
*
|
|
418
|
-
* @param branchName - The source branch to merge
|
|
419
|
-
* @param cwd - Working directory (must be on the target branch)
|
|
420
|
-
* @returns - true if merge succeeded, false otherwise
|
|
421
|
-
*/
|
|
422
|
-
async function performMerge(branchName, cwd) {
|
|
423
|
-
const result = await spawnGit([
|
|
424
|
-
"merge",
|
|
425
|
-
"--no-ff",
|
|
426
|
-
branchName
|
|
427
|
-
], { cwd });
|
|
428
|
-
if (result.code !== 0) return false;
|
|
429
|
-
return true;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Get a list of files changed in the most recent merge.
|
|
433
|
-
*
|
|
434
|
-
* Uses git diff --name-only HEAD~1..HEAD to find files in the merge commit.
|
|
435
|
-
* Falls back to empty array if commit history is insufficient.
|
|
436
|
-
*
|
|
437
|
-
* @param cwd - Working directory of the repository
|
|
438
|
-
* @returns - Array of file paths that were merged
|
|
439
|
-
*/
|
|
440
|
-
async function getMergedFiles(cwd) {
|
|
441
|
-
const result = await spawnGit([
|
|
442
|
-
"diff",
|
|
443
|
-
"--name-only",
|
|
444
|
-
"HEAD~1..HEAD"
|
|
445
|
-
], { cwd });
|
|
446
|
-
if (result.code !== 0) {
|
|
447
|
-
const altResult = await spawnGit([
|
|
448
|
-
"show",
|
|
449
|
-
"--name-only",
|
|
450
|
-
"--format=",
|
|
451
|
-
"HEAD"
|
|
452
|
-
], { cwd });
|
|
453
|
-
if (altResult.code !== 0) return [];
|
|
454
|
-
return altResult.stdout.trim().split("\n").filter((f$1) => f$1.trim().length > 0);
|
|
455
|
-
}
|
|
456
|
-
if (result.stdout.trim() === "") return [];
|
|
457
|
-
return result.stdout.trim().split("\n").filter((f$1) => f$1.trim().length > 0);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
//#endregion
|
|
461
|
-
//#region packages/core/dist/git/git-worktree-manager-impl.js
|
|
462
|
-
/**
|
|
463
|
-
* Branch name prefix for substrate per-story branches.
|
|
464
|
-
*
|
|
465
|
-
* Exported as the canonical source of truth so consumers (orchestrator,
|
|
466
|
-
* integration tests, tooling) can compose branch names without
|
|
467
|
-
* independently encoding the prefix. v0.20.82 production bug:
|
|
468
|
-
* `orchestrator-impl.ts:4290` hardcoded `substrate/story-${storyKey}`
|
|
469
|
-
* while this module created `substrate/task-${taskId}` — the resulting
|
|
470
|
-
* merge-to-main looked at a non-existent branch. Recurrence prevention:
|
|
471
|
-
* all branch-name construction MUST import this constant.
|
|
472
|
-
*/
|
|
473
|
-
const BRANCH_PREFIX = "substrate/story-";
|
|
474
|
-
const DEFAULT_WORKTREE_BASE = ".substrate-worktrees";
|
|
475
|
-
var GitWorktreeManagerImpl = class {
|
|
476
|
-
_eventBus;
|
|
477
|
-
_projectRoot;
|
|
478
|
-
_baseDirectory;
|
|
479
|
-
_db;
|
|
480
|
-
_logger;
|
|
481
|
-
/** v0.20.109: files to copy from project root into each new worktree (e.g. `.env`). */
|
|
482
|
-
_copyFiles;
|
|
483
|
-
/** Bound listener references for cleanup in shutdown() */
|
|
484
|
-
_onTaskReady;
|
|
485
|
-
_onTaskComplete;
|
|
486
|
-
_onTaskFailed;
|
|
487
|
-
constructor(eventBus, projectRoot, baseDirectory = DEFAULT_WORKTREE_BASE, db = null, logger$27, copyFiles = []) {
|
|
488
|
-
this._eventBus = eventBus;
|
|
489
|
-
this._projectRoot = projectRoot;
|
|
490
|
-
this._baseDirectory = baseDirectory;
|
|
491
|
-
this._db = db;
|
|
492
|
-
this._logger = logger$27 ?? createStderrLogger("git-worktree-manager");
|
|
493
|
-
this._copyFiles = copyFiles;
|
|
494
|
-
this._onTaskReady = ({ taskId }) => {
|
|
495
|
-
this._handleTaskReady(taskId).catch((err) => {
|
|
496
|
-
this._logger.error({
|
|
497
|
-
taskId,
|
|
498
|
-
err
|
|
499
|
-
}, "Unhandled error in _handleTaskReady");
|
|
500
|
-
});
|
|
501
|
-
};
|
|
502
|
-
this._onTaskComplete = ({ taskId }) => {
|
|
503
|
-
this._handleTaskDone(taskId);
|
|
504
|
-
};
|
|
505
|
-
this._onTaskFailed = ({ taskId }) => {
|
|
506
|
-
this._handleTaskDone(taskId);
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
async initialize() {
|
|
510
|
-
this._logger.info({ projectRoot: this._projectRoot }, "GitWorktreeManager.initialize()");
|
|
511
|
-
await this.verifyGitVersion();
|
|
512
|
-
const cleaned = await this.cleanupAllWorktrees();
|
|
513
|
-
if (cleaned > 0) this._logger.info({ cleaned }, "Recovered orphaned worktrees on startup");
|
|
514
|
-
this._eventBus.on("task:ready", this._onTaskReady);
|
|
515
|
-
this._eventBus.on("task:complete", this._onTaskComplete);
|
|
516
|
-
this._eventBus.on("task:failed", this._onTaskFailed);
|
|
517
|
-
this._logger.info("GitWorktreeManager initialized");
|
|
518
|
-
}
|
|
519
|
-
async shutdown() {
|
|
520
|
-
this._logger.info("GitWorktreeManager.shutdown()");
|
|
521
|
-
this._eventBus.off("task:ready", this._onTaskReady);
|
|
522
|
-
this._eventBus.off("task:complete", this._onTaskComplete);
|
|
523
|
-
this._eventBus.off("task:failed", this._onTaskFailed);
|
|
524
|
-
await this.cleanupAllWorktrees();
|
|
525
|
-
this._logger.info("GitWorktreeManager shutdown complete");
|
|
526
|
-
}
|
|
527
|
-
async _handleTaskReady(taskId) {
|
|
528
|
-
this._logger.debug({ taskId }, "task:ready — creating worktree");
|
|
529
|
-
try {
|
|
530
|
-
await this.createWorktree(taskId);
|
|
531
|
-
} catch (err) {
|
|
532
|
-
this._logger.error({
|
|
533
|
-
taskId,
|
|
534
|
-
err
|
|
535
|
-
}, "Failed to create worktree for task");
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
async _handleTaskDone(taskId) {
|
|
539
|
-
this._logger.debug({ taskId }, "task done — cleaning up worktree");
|
|
540
|
-
try {
|
|
541
|
-
await this.cleanupWorktree(taskId);
|
|
542
|
-
} catch (err) {
|
|
543
|
-
this._logger.warn({
|
|
544
|
-
taskId,
|
|
545
|
-
err
|
|
546
|
-
}, "Failed to cleanup worktree for task");
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
async createWorktree(taskId, baseBranch = "main") {
|
|
550
|
-
if (!taskId || taskId.trim().length === 0) throw new Error("createWorktree: taskId must be a non-empty string");
|
|
551
|
-
const branchName = BRANCH_PREFIX + taskId;
|
|
552
|
-
const worktreePath = this.getWorktreePath(taskId);
|
|
553
|
-
this._logger.debug({
|
|
554
|
-
taskId,
|
|
555
|
-
branchName,
|
|
556
|
-
worktreePath,
|
|
557
|
-
baseBranch,
|
|
558
|
-
copyFiles: this._copyFiles
|
|
559
|
-
}, "createWorktree");
|
|
560
|
-
const copyFiles = this._copyFiles.includes(".substrate/project-profile.yaml") ? this._copyFiles : [...this._copyFiles, ".substrate/project-profile.yaml"];
|
|
561
|
-
await createWorktree(this._projectRoot, taskId, branchName, baseBranch, copyFiles);
|
|
562
|
-
const createdAt = new Date();
|
|
563
|
-
this._eventBus.emit("worktree:created", {
|
|
564
|
-
taskId,
|
|
565
|
-
branchName,
|
|
566
|
-
worktreePath,
|
|
567
|
-
createdAt
|
|
568
|
-
});
|
|
569
|
-
const info = {
|
|
570
|
-
taskId,
|
|
571
|
-
branchName,
|
|
572
|
-
worktreePath,
|
|
573
|
-
createdAt
|
|
574
|
-
};
|
|
575
|
-
this._logger.info({
|
|
576
|
-
taskId,
|
|
577
|
-
branchName,
|
|
578
|
-
worktreePath
|
|
579
|
-
}, "Worktree created");
|
|
580
|
-
return info;
|
|
581
|
-
}
|
|
582
|
-
async cleanupWorktree(taskId, opts) {
|
|
583
|
-
const branchName = BRANCH_PREFIX + taskId;
|
|
584
|
-
const worktreePath = this.getWorktreePath(taskId);
|
|
585
|
-
this._logger.debug({
|
|
586
|
-
taskId,
|
|
587
|
-
branchName,
|
|
588
|
-
worktreePath,
|
|
589
|
-
force: opts?.force === true
|
|
590
|
-
}, "cleanupWorktree");
|
|
591
|
-
if (opts?.force !== true) {
|
|
592
|
-
const decision = await inspectWorktreeRemovalSafety(worktreePath, this._projectRoot, branchName);
|
|
593
|
-
const blockingReasons = opts?.keepBranch === true ? decision.reasons.filter((r) => r.includes("uncommitted")) : decision.reasons;
|
|
594
|
-
if (blockingReasons.length > 0) throw new Error(`refusing to clean up worktree for "${taskId}": ${blockingReasons.join("; ")}.\nInspect first (git -C ${worktreePath} status; git log ${branchName} --oneline) or re-run with --force to discard.`);
|
|
595
|
-
}
|
|
596
|
-
let worktreeExists = false;
|
|
597
|
-
try {
|
|
598
|
-
await access$1(worktreePath);
|
|
599
|
-
worktreeExists = true;
|
|
600
|
-
} catch {
|
|
601
|
-
this._logger.debug({
|
|
602
|
-
taskId,
|
|
603
|
-
worktreePath
|
|
604
|
-
}, "cleanupWorktree: worktree does not exist, skipping removal");
|
|
605
|
-
}
|
|
606
|
-
if (worktreeExists) try {
|
|
607
|
-
await removeWorktree(worktreePath, this._projectRoot);
|
|
608
|
-
} catch (err) {
|
|
609
|
-
this._logger.warn({
|
|
610
|
-
taskId,
|
|
611
|
-
worktreePath,
|
|
612
|
-
err
|
|
613
|
-
}, "removeWorktree failed during cleanup");
|
|
614
|
-
}
|
|
615
|
-
if (opts?.keepBranch !== true) try {
|
|
616
|
-
await removeBranch(branchName, this._projectRoot);
|
|
617
|
-
} catch (err) {
|
|
618
|
-
this._logger.warn({
|
|
619
|
-
taskId,
|
|
620
|
-
branchName,
|
|
621
|
-
err
|
|
622
|
-
}, "removeBranch failed during cleanup");
|
|
623
|
-
}
|
|
624
|
-
this._eventBus.emit("worktree:removed", {
|
|
625
|
-
taskId,
|
|
626
|
-
branchName
|
|
627
|
-
});
|
|
628
|
-
this._logger.info({
|
|
629
|
-
taskId,
|
|
630
|
-
branchName
|
|
631
|
-
}, "Worktree cleaned up");
|
|
632
|
-
}
|
|
633
|
-
async cleanupAllWorktrees(opts) {
|
|
634
|
-
this._logger.debug({
|
|
635
|
-
projectRoot: this._projectRoot,
|
|
636
|
-
force: opts?.force === true
|
|
637
|
-
}, "cleanupAllWorktrees");
|
|
638
|
-
const orphanedPaths = await getOrphanedWorktrees(this._projectRoot, this._baseDirectory);
|
|
639
|
-
let cleaned = 0;
|
|
640
|
-
for (const worktreePath of orphanedPaths) {
|
|
641
|
-
const taskId = path$4.basename(worktreePath);
|
|
642
|
-
const branchGuardName = BRANCH_PREFIX + taskId;
|
|
643
|
-
const decision = opts?.force === true ? {
|
|
644
|
-
safe: true,
|
|
645
|
-
reasons: []
|
|
646
|
-
} : await inspectWorktreeRemovalSafety(worktreePath, this._projectRoot, branchGuardName);
|
|
647
|
-
if (!decision.safe) {
|
|
648
|
-
this._logger.warn({
|
|
649
|
-
taskId,
|
|
650
|
-
worktreePath,
|
|
651
|
-
reasons: decision.reasons
|
|
652
|
-
}, "cleanupAllWorktrees: preserving worktree — removal would destroy work (use `substrate worktrees cleanup --force` to discard)");
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
let worktreeRemoved = false;
|
|
656
|
-
try {
|
|
657
|
-
await removeWorktree(worktreePath, this._projectRoot);
|
|
658
|
-
worktreeRemoved = true;
|
|
659
|
-
this._logger.debug({
|
|
660
|
-
taskId,
|
|
661
|
-
worktreePath
|
|
662
|
-
}, "cleanupAllWorktrees: removed orphaned worktree");
|
|
663
|
-
} catch (err) {
|
|
664
|
-
this._logger.warn({
|
|
665
|
-
taskId,
|
|
666
|
-
worktreePath,
|
|
667
|
-
err
|
|
668
|
-
}, "cleanupAllWorktrees: failed to remove worktree");
|
|
669
|
-
}
|
|
670
|
-
const branchName = BRANCH_PREFIX + taskId;
|
|
671
|
-
try {
|
|
672
|
-
const branchRemoved = await removeBranch(branchName, this._projectRoot);
|
|
673
|
-
if (branchRemoved) this._logger.debug({
|
|
674
|
-
taskId,
|
|
675
|
-
branchName
|
|
676
|
-
}, "cleanupAllWorktrees: removed orphaned branch");
|
|
677
|
-
} catch (err) {
|
|
678
|
-
this._logger.warn({
|
|
679
|
-
taskId,
|
|
680
|
-
branchName,
|
|
681
|
-
err
|
|
682
|
-
}, "cleanupAllWorktrees: failed to remove branch");
|
|
683
|
-
}
|
|
684
|
-
if (worktreeRemoved) cleaned++;
|
|
685
|
-
}
|
|
686
|
-
if (cleaned > 0) this._logger.info({ cleaned }, "cleanupAllWorktrees: recovered orphaned worktrees");
|
|
687
|
-
return cleaned;
|
|
688
|
-
}
|
|
689
|
-
async detectConflicts(taskId, targetBranch = "main") {
|
|
690
|
-
if (!taskId || taskId.trim().length === 0) throw new Error("detectConflicts: taskId must be a non-empty string");
|
|
691
|
-
const branchName = BRANCH_PREFIX + taskId;
|
|
692
|
-
const worktreePath = this.getWorktreePath(taskId);
|
|
693
|
-
this._logger.debug({
|
|
694
|
-
taskId,
|
|
695
|
-
branchName,
|
|
696
|
-
targetBranch
|
|
697
|
-
}, "detectConflicts");
|
|
698
|
-
try {
|
|
699
|
-
await access$1(worktreePath);
|
|
700
|
-
} catch {
|
|
701
|
-
throw new Error(`detectConflicts: Worktree for task "${taskId}" not found at "${worktreePath}". The worktree may have already been cleaned up.`);
|
|
702
|
-
}
|
|
703
|
-
const mergeClean = await simulateMerge(branchName, this._projectRoot);
|
|
704
|
-
let conflictingFiles = [];
|
|
705
|
-
try {
|
|
706
|
-
if (!mergeClean) conflictingFiles = await getConflictingFiles(this._projectRoot);
|
|
707
|
-
} finally {
|
|
708
|
-
await abortMerge(this._projectRoot);
|
|
709
|
-
}
|
|
710
|
-
const report = {
|
|
711
|
-
hasConflicts: !mergeClean || conflictingFiles.length > 0,
|
|
712
|
-
conflictingFiles,
|
|
713
|
-
taskId,
|
|
714
|
-
targetBranch
|
|
715
|
-
};
|
|
716
|
-
if (report.hasConflicts) this._eventBus.emit("worktree:conflict", {
|
|
717
|
-
taskId,
|
|
718
|
-
branch: branchName,
|
|
719
|
-
conflictingFiles: report.conflictingFiles
|
|
720
|
-
});
|
|
721
|
-
this._logger.info({
|
|
722
|
-
taskId,
|
|
723
|
-
hasConflicts: report.hasConflicts,
|
|
724
|
-
conflictCount: conflictingFiles.length
|
|
725
|
-
}, "Conflict detection complete");
|
|
726
|
-
return report;
|
|
727
|
-
}
|
|
728
|
-
async mergeWorktree(taskId, targetBranch = "main") {
|
|
729
|
-
if (!taskId || taskId.trim().length === 0) throw new Error("mergeWorktree: taskId must be a non-empty string");
|
|
730
|
-
const branchName = BRANCH_PREFIX + taskId;
|
|
731
|
-
this._logger.debug({
|
|
732
|
-
taskId,
|
|
733
|
-
branchName,
|
|
734
|
-
targetBranch
|
|
735
|
-
}, "mergeWorktree");
|
|
736
|
-
const conflictReport = await this.detectConflicts(taskId, targetBranch);
|
|
737
|
-
if (conflictReport.hasConflicts) {
|
|
738
|
-
this._logger.info({
|
|
739
|
-
taskId,
|
|
740
|
-
conflictCount: conflictReport.conflictingFiles.length
|
|
741
|
-
}, "Merge skipped due to conflicts");
|
|
742
|
-
return {
|
|
743
|
-
success: false,
|
|
744
|
-
mergedFiles: [],
|
|
745
|
-
conflicts: conflictReport
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
const mergeSuccess = await performMerge(branchName, this._projectRoot);
|
|
749
|
-
if (!mergeSuccess) throw new Error(`mergeWorktree: git merge --no-ff failed for task "${taskId}" branch "${branchName}"`);
|
|
750
|
-
const mergedFiles = await getMergedFiles(this._projectRoot);
|
|
751
|
-
this._eventBus.emit("worktree:merged", {
|
|
752
|
-
taskId,
|
|
753
|
-
branch: branchName,
|
|
754
|
-
mergedFiles
|
|
755
|
-
});
|
|
756
|
-
const result = {
|
|
757
|
-
success: true,
|
|
758
|
-
mergedFiles
|
|
759
|
-
};
|
|
760
|
-
this._logger.info({
|
|
761
|
-
taskId,
|
|
762
|
-
branchName,
|
|
763
|
-
mergedFileCount: mergedFiles.length
|
|
764
|
-
}, "Worktree merged successfully");
|
|
765
|
-
return result;
|
|
766
|
-
}
|
|
767
|
-
async listWorktrees() {
|
|
768
|
-
this._logger.debug({
|
|
769
|
-
projectRoot: this._projectRoot,
|
|
770
|
-
baseDirectory: this._baseDirectory
|
|
771
|
-
}, "listWorktrees");
|
|
772
|
-
const worktreePaths = await getOrphanedWorktrees(this._projectRoot, this._baseDirectory);
|
|
773
|
-
const results = [];
|
|
774
|
-
for (const worktreePath of worktreePaths) {
|
|
775
|
-
const taskId = path$4.basename(worktreePath);
|
|
776
|
-
const branchName = BRANCH_PREFIX + taskId;
|
|
777
|
-
let createdAt;
|
|
778
|
-
try {
|
|
779
|
-
const { stat: stat$2 } = await import("node:fs/promises");
|
|
780
|
-
const stats = await stat$2(worktreePath);
|
|
781
|
-
createdAt = stats.birthtime ?? stats.ctime;
|
|
782
|
-
} catch {
|
|
783
|
-
createdAt = new Date();
|
|
784
|
-
}
|
|
785
|
-
results.push({
|
|
786
|
-
taskId,
|
|
787
|
-
branchName,
|
|
788
|
-
worktreePath,
|
|
789
|
-
createdAt
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
this._logger.debug({ count: results.length }, "listWorktrees: found worktrees");
|
|
793
|
-
return results;
|
|
794
|
-
}
|
|
795
|
-
getWorktreePath(taskId) {
|
|
796
|
-
return path$4.join(this._projectRoot, this._baseDirectory, taskId);
|
|
797
|
-
}
|
|
798
|
-
async verifyGitVersion() {
|
|
799
|
-
try {
|
|
800
|
-
await verifyGitVersion();
|
|
801
|
-
} catch (err) {
|
|
802
|
-
throw new Error(`GitWorktreeManager: git version check failed: ${String(err)}`);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
};
|
|
806
|
-
function createGitWorktreeManager(options) {
|
|
807
|
-
return new GitWorktreeManagerImpl(options.eventBus, options.projectRoot, options.baseDirectory, options.db ?? null, options.logger, options.copyFiles);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
//#endregion
|
|
811
35
|
//#region src/modules/methodology-pack/schemas.ts
|
|
812
36
|
/**
|
|
813
37
|
* A reference to a context value to inject into a step prompt.
|
|
@@ -2961,7 +2185,7 @@ These on-disk files back the new autonomy commands. External monitors (dashboard
|
|
|
2961
2185
|
- \`.substrate/current-run-id\` — plain text file containing the latest run ID; consulted by the canonical run-discovery chain.
|
|
2962
2186
|
- \`.substrate/notifications/<run-id>-<timestamp>.json\` — operator halt notifications written by the Recovery Engine when \`--halt-on\` triggers; deleted by \`substrate report\` after read.
|
|
2963
2187
|
- \`pending_proposals[]\` field in the run manifest — Recovery Engine Tier B re-scope proposals collected here for next-morning operator review. Back-pressure pauses dispatching at \`>= 2\` proposals (work-graph-aware) or \`>= 5\` (safety valve).
|
|
2964
|
-
-
|
|
2188
|
+
- Per-story git worktrees are created during dispatch on branch \`substrate/story-<key>\`. H4.2: the default base is EXTERNAL — \`~/.substrate/worktrees/<projectname>-<hash8>/<key>/\` — so agents have no parent repo above their worktree; set \`worktree.base: in-repo\` in .substrate/config.yaml to restore the legacy \`.substrate-worktrees/<key>/\` location (tooling that globbed the old path should use \`substrate worktrees list\`). **Substrate auto-commits** the dispatched agent's output to the branch after SHIP_IT with a \`feat(story-N-M): <title>\` message (v0.20.86+ — substrate does not rely on the agent committing). Pre-commit hooks fire on the substrate commit. The branch is then merged to the base branch and the worktree is removed. On verification failure or auto-commit failure, the worktree and branch are preserved for \`substrate reconcile-from-disk\` inspection. Use \`--no-worktree\` (or \`SUBSTRATE_NO_WORKTREE=1\`) to disable per-story worktrees entirely; dispatch then runs against the parent project tree.
|
|
2965
2189
|
|
|
2966
2190
|
## Environment Variables
|
|
2967
2191
|
|
|
@@ -3574,8 +2798,8 @@ var GrammarLoader = class {
|
|
|
3574
2798
|
throw err;
|
|
3575
2799
|
}
|
|
3576
2800
|
}
|
|
3577
|
-
_loadModule(path$
|
|
3578
|
-
return __require(path$
|
|
2801
|
+
_loadModule(path$4) {
|
|
2802
|
+
return __require(path$4);
|
|
3579
2803
|
}
|
|
3580
2804
|
};
|
|
3581
2805
|
|
|
@@ -7192,10 +6416,10 @@ function computeStoryFileFidelity(storyFileContent, namedPaths) {
|
|
|
7192
6416
|
};
|
|
7193
6417
|
const missing = [];
|
|
7194
6418
|
const present = [];
|
|
7195
|
-
for (const path$
|
|
7196
|
-
const basename$2 = path$
|
|
7197
|
-
if (storyFileContent.includes(path$
|
|
7198
|
-
else missing.push(path$
|
|
6419
|
+
for (const path$4 of namedPaths) {
|
|
6420
|
+
const basename$2 = path$4.includes("/") ? path$4.slice(path$4.lastIndexOf("/") + 1) : path$4;
|
|
6421
|
+
if (storyFileContent.includes(path$4) || storyFileContent.includes(basename$2)) present.push(path$4);
|
|
6422
|
+
else missing.push(path$4);
|
|
7199
6423
|
}
|
|
7200
6424
|
return {
|
|
7201
6425
|
missing,
|
|
@@ -8901,7 +8125,7 @@ function extractReferencedFiles(storyContent) {
|
|
|
8901
8125
|
const matches = storyContent.match(filePathRegex) ?? [];
|
|
8902
8126
|
const freq = new Map();
|
|
8903
8127
|
for (const m of matches) freq.set(m, (freq.get(m) ?? 0) + 1);
|
|
8904
|
-
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([path$
|
|
8128
|
+
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([path$4]) => path$4);
|
|
8905
8129
|
return sorted.filter((p) => !p.includes(".test."));
|
|
8906
8130
|
}
|
|
8907
8131
|
function extractFilesInScope(storyContent) {
|
|
@@ -9606,11 +8830,26 @@ const logger$16 = createLogger("compiled-workflows:merge-to-main");
|
|
|
9606
8830
|
*/
|
|
9607
8831
|
async function runMergeToMain(params) {
|
|
9608
8832
|
const { storyKey, branchName, startBranch, worktreeManager, eventBus, projectRoot } = params;
|
|
8833
|
+
const mergeStrategy = params.mergeStrategy ?? "ff-only";
|
|
9609
8834
|
logger$16.info({
|
|
9610
8835
|
storyKey,
|
|
9611
8836
|
branchName,
|
|
9612
|
-
startBranch
|
|
8837
|
+
startBranch,
|
|
8838
|
+
mergeStrategy
|
|
9613
8839
|
}, "Starting merge-to-main phase");
|
|
8840
|
+
const dirtiedFiles = listParentDirtyIntersection(branchName, startBranch, projectRoot);
|
|
8841
|
+
if (dirtiedFiles.length > 0) {
|
|
8842
|
+
logger$16.warn({
|
|
8843
|
+
storyKey,
|
|
8844
|
+
branchName,
|
|
8845
|
+
dirtiedFiles
|
|
8846
|
+
}, "parent working tree has uncommitted changes to files in the story diff — refusing to merge");
|
|
8847
|
+
return {
|
|
8848
|
+
success: false,
|
|
8849
|
+
reason: "parent-tree-dirtied-by-run",
|
|
8850
|
+
dirtiedFiles
|
|
8851
|
+
};
|
|
8852
|
+
}
|
|
9614
8853
|
const ffSuccess = tryFfMerge(branchName, projectRoot);
|
|
9615
8854
|
if (ffSuccess) {
|
|
9616
8855
|
logger$16.info({
|
|
@@ -9620,6 +8859,17 @@ async function runMergeToMain(params) {
|
|
|
9620
8859
|
await cleanupAfterSuccessfulMerge(storyKey, branchName, worktreeManager, projectRoot);
|
|
9621
8860
|
return { success: true };
|
|
9622
8861
|
}
|
|
8862
|
+
if (mergeStrategy === "ff-only") {
|
|
8863
|
+
logger$16.warn({
|
|
8864
|
+
storyKey,
|
|
8865
|
+
branchName,
|
|
8866
|
+
startBranch
|
|
8867
|
+
}, "fast-forward not possible (base moved since the story branched) and merge_strategy is ff-only — refusing 3-way merge. Set finalization.merge_strategy: three-way to allow it (required for concurrent multi-story runs), or integrate the branch manually.");
|
|
8868
|
+
return {
|
|
8869
|
+
success: false,
|
|
8870
|
+
reason: "ff-only-merge-not-possible"
|
|
8871
|
+
};
|
|
8872
|
+
}
|
|
9623
8873
|
logger$16.info({
|
|
9624
8874
|
storyKey,
|
|
9625
8875
|
branchName
|
|
@@ -9651,6 +8901,47 @@ async function runMergeToMain(params) {
|
|
|
9651
8901
|
};
|
|
9652
8902
|
}
|
|
9653
8903
|
/**
|
|
8904
|
+
* H3.3 (AC1): files that are both dirty in the parent working tree and part of
|
|
8905
|
+
* the story branch's diff against the start branch. Best-effort: if either
|
|
8906
|
+
* git query fails, returns [] (the merge itself will surface genuine issues).
|
|
8907
|
+
*/
|
|
8908
|
+
function listParentDirtyIntersection(branchName, startBranch, projectRoot) {
|
|
8909
|
+
try {
|
|
8910
|
+
const statusOut = execFileSync("git", ["status", "--porcelain"], {
|
|
8911
|
+
cwd: projectRoot,
|
|
8912
|
+
encoding: "utf-8",
|
|
8913
|
+
stdio: [
|
|
8914
|
+
"ignore",
|
|
8915
|
+
"pipe",
|
|
8916
|
+
"pipe"
|
|
8917
|
+
]
|
|
8918
|
+
});
|
|
8919
|
+
const dirty = new Set(statusOut.split("\n").filter((line) => line.trim().length > 0).map((line) => {
|
|
8920
|
+
const body = line.slice(3).trim();
|
|
8921
|
+
const arrow = body.indexOf(" -> ");
|
|
8922
|
+
return arrow === -1 ? body : body.slice(arrow + 4);
|
|
8923
|
+
}));
|
|
8924
|
+
if (dirty.size === 0) return [];
|
|
8925
|
+
const diffOut = execFileSync("git", [
|
|
8926
|
+
"diff",
|
|
8927
|
+
"--name-only",
|
|
8928
|
+
`${startBranch}...${branchName}`
|
|
8929
|
+
], {
|
|
8930
|
+
cwd: projectRoot,
|
|
8931
|
+
encoding: "utf-8",
|
|
8932
|
+
stdio: [
|
|
8933
|
+
"ignore",
|
|
8934
|
+
"pipe",
|
|
8935
|
+
"pipe"
|
|
8936
|
+
]
|
|
8937
|
+
});
|
|
8938
|
+
return diffOut.split("\n").filter((f$1) => f$1.trim().length > 0).filter((f$1) => dirty.has(f$1));
|
|
8939
|
+
} catch (err) {
|
|
8940
|
+
logger$16.warn({ err }, "parent-clean precondition check failed (best-effort) — proceeding to merge");
|
|
8941
|
+
return [];
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8944
|
+
/**
|
|
9654
8945
|
* Attempt a fast-forward merge of `branchName` into the current branch.
|
|
9655
8946
|
*
|
|
9656
8947
|
* @param branchName - Branch to merge
|
|
@@ -16746,6 +16037,63 @@ function createImplementationOrchestrator(deps) {
|
|
|
16746
16037
|
}, "merge-to-main pre-flight verification failed — proceeding with merge phase");
|
|
16747
16038
|
}
|
|
16748
16039
|
const finalizationMode = config.finalizationMode ?? "merge";
|
|
16040
|
+
const epicGateCommand = config.epicGateCommand;
|
|
16041
|
+
if (epicGateCommand !== void 0 && epicGateCommand.trim() !== "" && finalizationMode !== "branch") {
|
|
16042
|
+
const epicOf = (key) => key.includes("-") ? key.slice(0, key.lastIndexOf("-")) : key;
|
|
16043
|
+
const epicId = epicOf(storyKey);
|
|
16044
|
+
const TERMINAL_PHASES = ["COMPLETE", "ESCALATED"];
|
|
16045
|
+
const isLastOfEpic = [..._stories.entries()].filter(([key]) => key !== storyKey && epicOf(key) === epicId).every(([, st]) => TERMINAL_PHASES.includes(st.phase));
|
|
16046
|
+
if (isLastOfEpic) {
|
|
16047
|
+
logger$27.info({
|
|
16048
|
+
storyKey,
|
|
16049
|
+
epicId,
|
|
16050
|
+
epicGateCommand
|
|
16051
|
+
}, "H3.4: last story of epic — running epic gate command");
|
|
16052
|
+
let gateOutput = "";
|
|
16053
|
+
let gatePassed = false;
|
|
16054
|
+
try {
|
|
16055
|
+
gateOutput = execSync(epicGateCommand, {
|
|
16056
|
+
cwd: projectRoot,
|
|
16057
|
+
encoding: "utf-8",
|
|
16058
|
+
stdio: [
|
|
16059
|
+
"ignore",
|
|
16060
|
+
"pipe",
|
|
16061
|
+
"pipe"
|
|
16062
|
+
],
|
|
16063
|
+
timeout: 6e5,
|
|
16064
|
+
maxBuffer: 10 * 1024 * 1024
|
|
16065
|
+
});
|
|
16066
|
+
gatePassed = true;
|
|
16067
|
+
} catch (gateCmdErr) {
|
|
16068
|
+
const e = gateCmdErr;
|
|
16069
|
+
gateOutput = `${e.stdout ?? ""}${e.stderr ?? ""}`.trim() || (e.message ?? String(gateCmdErr));
|
|
16070
|
+
}
|
|
16071
|
+
if (!gatePassed) {
|
|
16072
|
+
logger$27.error({
|
|
16073
|
+
storyKey,
|
|
16074
|
+
epicId,
|
|
16075
|
+
output: gateOutput.slice(0, 500)
|
|
16076
|
+
}, "H3.4: epic gate command failed — halting finalization; branch preserved");
|
|
16077
|
+
updateStory(storyKey, {
|
|
16078
|
+
phase: "ESCALATED",
|
|
16079
|
+
error: "epic-gate-failed",
|
|
16080
|
+
completedAt: new Date().toISOString()
|
|
16081
|
+
});
|
|
16082
|
+
await emitEscalation({
|
|
16083
|
+
storyKey,
|
|
16084
|
+
lastVerdict: "epic-gate-failed",
|
|
16085
|
+
reviewCycles: completedReviewCycles,
|
|
16086
|
+
issues: [`epic gate command failed for epic ${epicId} (last story ${storyKey}): \`${epicGateCommand}\`\noutput (truncated):\n${gateOutput.slice(0, 4e3)}`]
|
|
16087
|
+
});
|
|
16088
|
+
await persistState();
|
|
16089
|
+
return "terminal";
|
|
16090
|
+
}
|
|
16091
|
+
logger$27.info({
|
|
16092
|
+
storyKey,
|
|
16093
|
+
epicId
|
|
16094
|
+
}, "H3.4: epic gate passed");
|
|
16095
|
+
}
|
|
16096
|
+
}
|
|
16749
16097
|
if (finalizationMode === "branch" || finalizationMode === "pr") {
|
|
16750
16098
|
let prUrl;
|
|
16751
16099
|
if (finalizationMode === "pr") try {
|
|
@@ -16822,7 +16170,8 @@ function createImplementationOrchestrator(deps) {
|
|
|
16822
16170
|
startBranch: _orchestratorStartBranch,
|
|
16823
16171
|
worktreeManager: _worktreeManager,
|
|
16824
16172
|
eventBus,
|
|
16825
|
-
projectRoot
|
|
16173
|
+
projectRoot,
|
|
16174
|
+
mergeStrategy: config.mergeStrategy ?? "ff-only"
|
|
16826
16175
|
});
|
|
16827
16176
|
} catch (mergeErr) {
|
|
16828
16177
|
const errMsg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
@@ -16845,21 +16194,28 @@ function createImplementationOrchestrator(deps) {
|
|
|
16845
16194
|
return "terminal";
|
|
16846
16195
|
}
|
|
16847
16196
|
if (!mergeResult.success) {
|
|
16197
|
+
const failReason = mergeResult.reason ?? "merge-conflict-detected";
|
|
16198
|
+
const issues = [];
|
|
16199
|
+
if (failReason === "parent-tree-dirtied-by-run") issues.push(`parent working tree has uncommitted changes to ${mergeResult.dirtiedFiles?.length ?? 0} file(s) the story also modified: ${(mergeResult.dirtiedFiles ?? []).join(", ")} — merging would entangle unreviewed parent edits with verified story content. Commit or stash the parent changes, then merge ${branchName} manually.`);
|
|
16200
|
+
else if (failReason === "ff-only-merge-not-possible") issues.push(`${_orchestratorStartBranch ?? "the start branch"} moved since ${branchName} was created and merge_strategy is ff-only — substrate will not synthesize a merge commit. Set finalization.merge_strategy: three-way (required for concurrent multi-story runs) or integrate the branch manually.`);
|
|
16201
|
+
else issues.push(`merge conflict in ${mergeResult.conflictingFiles?.length ?? 0} file(s): ${(mergeResult.conflictingFiles ?? []).join(", ")}`);
|
|
16848
16202
|
logger$27.warn({
|
|
16849
16203
|
storyKey,
|
|
16850
16204
|
branchName,
|
|
16851
|
-
|
|
16852
|
-
|
|
16205
|
+
reason: failReason,
|
|
16206
|
+
conflictingFiles: mergeResult.conflictingFiles,
|
|
16207
|
+
dirtiedFiles: mergeResult.dirtiedFiles
|
|
16208
|
+
}, `merge-to-main failed — escalating story with ${failReason}`);
|
|
16853
16209
|
updateStory(storyKey, {
|
|
16854
16210
|
phase: "ESCALATED",
|
|
16855
|
-
error:
|
|
16211
|
+
error: failReason,
|
|
16856
16212
|
completedAt: new Date().toISOString()
|
|
16857
16213
|
});
|
|
16858
16214
|
await emitEscalation({
|
|
16859
16215
|
storyKey,
|
|
16860
|
-
lastVerdict:
|
|
16216
|
+
lastVerdict: failReason,
|
|
16861
16217
|
reviewCycles: completedReviewCycles,
|
|
16862
|
-
issues
|
|
16218
|
+
issues
|
|
16863
16219
|
});
|
|
16864
16220
|
await persistState();
|
|
16865
16221
|
return "terminal";
|
|
@@ -18116,7 +17472,13 @@ function createImplementationOrchestrator(deps) {
|
|
|
18116
17472
|
}).trim();
|
|
18117
17473
|
logger$27.info({ orchestratorStartBranch: _orchestratorStartBranch }, "Captured orchestrator start branch for merge-to-main");
|
|
18118
17474
|
} catch (branchErr) {
|
|
18119
|
-
|
|
17475
|
+
if (config.noWorktree !== true) {
|
|
17476
|
+
logger$27.error({ err: branchErr }, "FATAL: failed to capture the orchestrator start branch (git rev-parse --abbrev-ref HEAD). Worktree finalization cannot work without it. Fix git in the project root or re-run with --no-worktree.");
|
|
17477
|
+
_state = "FAILED";
|
|
17478
|
+
_completedAt = new Date().toISOString();
|
|
17479
|
+
return getStatus();
|
|
17480
|
+
}
|
|
17481
|
+
logger$27.warn({ err: branchErr }, "Failed to capture orchestrator start branch — merge-to-main will skip worktree integration (--no-worktree run)");
|
|
18120
17482
|
}
|
|
18121
17483
|
for (const key of storyKeys) {
|
|
18122
17484
|
const pendingState = {
|
|
@@ -19841,8 +19203,8 @@ async function resolveContext(ref, deps, runId, params, stepOutputs) {
|
|
|
19841
19203
|
return params[key] ?? "";
|
|
19842
19204
|
}
|
|
19843
19205
|
if (source.startsWith("decision:")) {
|
|
19844
|
-
const path$
|
|
19845
|
-
const [phase, category] = path$
|
|
19206
|
+
const path$4 = source.slice(9);
|
|
19207
|
+
const [phase, category] = path$4.split(".");
|
|
19846
19208
|
if (!phase || !category) return "";
|
|
19847
19209
|
const decisions = await getDecisionsByPhaseForRun(deps.db, runId, phase);
|
|
19848
19210
|
const filtered = decisions.filter((d) => d.category === category);
|
|
@@ -19913,8 +19275,8 @@ async function runSteps(steps, deps, runId, phase, params) {
|
|
|
19913
19275
|
for (const ref of step.context) {
|
|
19914
19276
|
let value;
|
|
19915
19277
|
if (ref.source.startsWith("decision:")) {
|
|
19916
|
-
const path$
|
|
19917
|
-
const [decPhase, decCategory] = path$
|
|
19278
|
+
const path$4 = ref.source.slice(9);
|
|
19279
|
+
const [decPhase, decCategory] = path$4.split(".");
|
|
19918
19280
|
if (decPhase && decCategory) {
|
|
19919
19281
|
const decisions = await getDecisionsByPhaseForRun(deps.db, runId, decPhase);
|
|
19920
19282
|
const filtered = decisions.filter((d) => d.category === decCategory);
|
|
@@ -31002,8 +30364,8 @@ var require_uri_all = __commonJS({ "node_modules/uri-js/dist/es5/uri.all.js"(exp
|
|
|
31002
30364
|
wsComponents.secure = void 0;
|
|
31003
30365
|
}
|
|
31004
30366
|
if (wsComponents.resourceName) {
|
|
31005
|
-
var _wsComponents$resourc = wsComponents.resourceName.split("?"), _wsComponents$resourc2 = slicedToArray(_wsComponents$resourc, 2), path$
|
|
31006
|
-
wsComponents.path = path$
|
|
30367
|
+
var _wsComponents$resourc = wsComponents.resourceName.split("?"), _wsComponents$resourc2 = slicedToArray(_wsComponents$resourc, 2), path$4 = _wsComponents$resourc2[0], query = _wsComponents$resourc2[1];
|
|
30368
|
+
wsComponents.path = path$4 && path$4 !== "/" ? path$4 : void 0;
|
|
31007
30369
|
wsComponents.query = query;
|
|
31008
30370
|
wsComponents.resourceName = void 0;
|
|
31009
30371
|
}
|
|
@@ -31344,12 +30706,12 @@ var require_util = __commonJS({ "node_modules/ajv/lib/compile/util.js"(exports,
|
|
|
31344
30706
|
return "'" + escapeQuotes(str) + "'";
|
|
31345
30707
|
}
|
|
31346
30708
|
function getPathExpr(currentPath, expr, jsonPointers, isNumber) {
|
|
31347
|
-
var path$
|
|
31348
|
-
return joinPaths(currentPath, path$
|
|
30709
|
+
var path$4 = jsonPointers ? "'/' + " + expr + (isNumber ? "" : ".replace(/~/g, '~0').replace(/\\//g, '~1')") : isNumber ? "'[' + " + expr + " + ']'" : "'[\\'' + " + expr + " + '\\']'";
|
|
30710
|
+
return joinPaths(currentPath, path$4);
|
|
31349
30711
|
}
|
|
31350
30712
|
function getPath(currentPath, prop, jsonPointers) {
|
|
31351
|
-
var path$
|
|
31352
|
-
return joinPaths(currentPath, path$
|
|
30713
|
+
var path$4 = jsonPointers ? toQuotedString("/" + escapeJsonPointer(prop)) : toQuotedString(getProperty(prop));
|
|
30714
|
+
return joinPaths(currentPath, path$4);
|
|
31353
30715
|
}
|
|
31354
30716
|
var JSON_POINTER$1 = /^\/(?:[^~]|~0|~1)*$/;
|
|
31355
30717
|
var RELATIVE_JSON_POINTER$1 = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/;
|
|
@@ -35558,8 +34920,8 @@ var ToolRegistry = class {
|
|
|
35558
34920
|
if (!valid) {
|
|
35559
34921
|
const errors = validate$1.errors ?? [];
|
|
35560
34922
|
const messages = errors.map((e) => {
|
|
35561
|
-
const path$
|
|
35562
|
-
return `${path$
|
|
34923
|
+
const path$4 = e.instancePath ?? e.dataPath ?? "";
|
|
34924
|
+
return `${path$4} ${e.message ?? ""}`.trim();
|
|
35563
34925
|
}).join(", ");
|
|
35564
34926
|
return {
|
|
35565
34927
|
content: `Validation failed for tool '${name}': ${messages}`,
|
|
@@ -42265,10 +41627,10 @@ var PathBase = class {
|
|
|
42265
41627
|
/**
|
|
42266
41628
|
* Get the Path object referenced by the string path, resolved from this Path
|
|
42267
41629
|
*/
|
|
42268
|
-
resolve(path$
|
|
42269
|
-
if (!path$
|
|
42270
|
-
const rootPath = this.getRootString(path$
|
|
42271
|
-
const dir = path$
|
|
41630
|
+
resolve(path$4) {
|
|
41631
|
+
if (!path$4) return this;
|
|
41632
|
+
const rootPath = this.getRootString(path$4);
|
|
41633
|
+
const dir = path$4.substring(rootPath.length);
|
|
42272
41634
|
const dirParts = dir.split(this.splitSep);
|
|
42273
41635
|
const result = rootPath ? this.getRoot(rootPath).#resolveParts(dirParts) : this.#resolveParts(dirParts);
|
|
42274
41636
|
return result;
|
|
@@ -42923,8 +42285,8 @@ var PathWin32 = class PathWin32 extends PathBase {
|
|
|
42923
42285
|
/**
|
|
42924
42286
|
* @internal
|
|
42925
42287
|
*/
|
|
42926
|
-
getRootString(path$
|
|
42927
|
-
return win32.parse(path$
|
|
42288
|
+
getRootString(path$4) {
|
|
42289
|
+
return win32.parse(path$4).root;
|
|
42928
42290
|
}
|
|
42929
42291
|
/**
|
|
42930
42292
|
* @internal
|
|
@@ -42969,8 +42331,8 @@ var PathPosix = class PathPosix extends PathBase {
|
|
|
42969
42331
|
/**
|
|
42970
42332
|
* @internal
|
|
42971
42333
|
*/
|
|
42972
|
-
getRootString(path$
|
|
42973
|
-
return path$
|
|
42334
|
+
getRootString(path$4) {
|
|
42335
|
+
return path$4.startsWith("/") ? "/" : "";
|
|
42974
42336
|
}
|
|
42975
42337
|
/**
|
|
42976
42338
|
* @internal
|
|
@@ -43063,9 +42425,9 @@ var PathScurryBase = class {
|
|
|
43063
42425
|
/**
|
|
43064
42426
|
* Get the depth of a provided path, string, or the cwd
|
|
43065
42427
|
*/
|
|
43066
|
-
depth(path$
|
|
43067
|
-
if (typeof path$
|
|
43068
|
-
return path$
|
|
42428
|
+
depth(path$4 = this.cwd) {
|
|
42429
|
+
if (typeof path$4 === "string") path$4 = this.cwd.resolve(path$4);
|
|
42430
|
+
return path$4.depth();
|
|
43069
42431
|
}
|
|
43070
42432
|
/**
|
|
43071
42433
|
* Return the cache of child entries. Exposed so subclasses can create
|
|
@@ -43446,9 +42808,9 @@ var PathScurryBase = class {
|
|
|
43446
42808
|
process$1();
|
|
43447
42809
|
return results;
|
|
43448
42810
|
}
|
|
43449
|
-
chdir(path$
|
|
42811
|
+
chdir(path$4 = this.cwd) {
|
|
43450
42812
|
const oldCwd = this.cwd;
|
|
43451
|
-
this.cwd = typeof path$
|
|
42813
|
+
this.cwd = typeof path$4 === "string" ? this.cwd.resolve(path$4) : path$4;
|
|
43452
42814
|
this.cwd[setAsCwd](oldCwd);
|
|
43453
42815
|
}
|
|
43454
42816
|
};
|
|
@@ -43832,8 +43194,8 @@ var MatchRecord = class {
|
|
|
43832
43194
|
this.store.set(target, current === void 0 ? n$1 : n$1 & current);
|
|
43833
43195
|
}
|
|
43834
43196
|
entries() {
|
|
43835
|
-
return [...this.store.entries()].map(([path$
|
|
43836
|
-
path$
|
|
43197
|
+
return [...this.store.entries()].map(([path$4, n$1]) => [
|
|
43198
|
+
path$4,
|
|
43837
43199
|
!!(n$1 & 2),
|
|
43838
43200
|
!!(n$1 & 1)
|
|
43839
43201
|
]);
|
|
@@ -44009,9 +43371,9 @@ var GlobUtil = class {
|
|
|
44009
43371
|
signal;
|
|
44010
43372
|
maxDepth;
|
|
44011
43373
|
includeChildMatches;
|
|
44012
|
-
constructor(patterns, path$
|
|
43374
|
+
constructor(patterns, path$4, opts) {
|
|
44013
43375
|
this.patterns = patterns;
|
|
44014
|
-
this.path = path$
|
|
43376
|
+
this.path = path$4;
|
|
44015
43377
|
this.opts = opts;
|
|
44016
43378
|
this.#sep = !opts.posix && opts.platform === "win32" ? "\\" : "/";
|
|
44017
43379
|
this.includeChildMatches = opts.includeChildMatches !== false;
|
|
@@ -44032,11 +43394,11 @@ var GlobUtil = class {
|
|
|
44032
43394
|
});
|
|
44033
43395
|
}
|
|
44034
43396
|
}
|
|
44035
|
-
#ignored(path$
|
|
44036
|
-
return this.seen.has(path$
|
|
43397
|
+
#ignored(path$4) {
|
|
43398
|
+
return this.seen.has(path$4) || !!this.#ignore?.ignored?.(path$4);
|
|
44037
43399
|
}
|
|
44038
|
-
#childrenIgnored(path$
|
|
44039
|
-
return !!this.#ignore?.childrenIgnored?.(path$
|
|
43400
|
+
#childrenIgnored(path$4) {
|
|
43401
|
+
return !!this.#ignore?.childrenIgnored?.(path$4);
|
|
44040
43402
|
}
|
|
44041
43403
|
pause() {
|
|
44042
43404
|
this.paused = true;
|
|
@@ -44218,8 +43580,8 @@ var GlobUtil = class {
|
|
|
44218
43580
|
};
|
|
44219
43581
|
var GlobWalker = class extends GlobUtil {
|
|
44220
43582
|
matches = new Set();
|
|
44221
|
-
constructor(patterns, path$
|
|
44222
|
-
super(patterns, path$
|
|
43583
|
+
constructor(patterns, path$4, opts) {
|
|
43584
|
+
super(patterns, path$4, opts);
|
|
44223
43585
|
}
|
|
44224
43586
|
matchEmit(e) {
|
|
44225
43587
|
this.matches.add(e);
|
|
@@ -44246,8 +43608,8 @@ var GlobWalker = class extends GlobUtil {
|
|
|
44246
43608
|
};
|
|
44247
43609
|
var GlobStream = class extends GlobUtil {
|
|
44248
43610
|
results;
|
|
44249
|
-
constructor(patterns, path$
|
|
44250
|
-
super(patterns, path$
|
|
43611
|
+
constructor(patterns, path$4, opts) {
|
|
43612
|
+
super(patterns, path$4, opts);
|
|
44251
43613
|
this.results = new Minipass({
|
|
44252
43614
|
signal: this.signal,
|
|
44253
43615
|
objectMode: true
|
|
@@ -46953,6 +46315,8 @@ async function runRunAction(options) {
|
|
|
46953
46315
|
let meshProjectId;
|
|
46954
46316
|
let configRetryBudget;
|
|
46955
46317
|
let configFinalizationMode;
|
|
46318
|
+
let configMergeStrategy;
|
|
46319
|
+
let configEpicGateCommand;
|
|
46956
46320
|
let configWorktreeCopyFiles;
|
|
46957
46321
|
try {
|
|
46958
46322
|
const configSystem = createConfigSystem({ projectConfigDir: dbDir });
|
|
@@ -46980,6 +46344,8 @@ async function runRunAction(options) {
|
|
|
46980
46344
|
}
|
|
46981
46345
|
if (typeof cfg.retry_budget === "number") configRetryBudget = cfg.retry_budget;
|
|
46982
46346
|
if (cfg.finalization?.mode !== void 0) configFinalizationMode = cfg.finalization.mode;
|
|
46347
|
+
if (cfg.finalization?.merge_strategy !== void 0) configMergeStrategy = cfg.finalization.merge_strategy;
|
|
46348
|
+
if (cfg.finalization?.epic_gate_command !== void 0) configEpicGateCommand = cfg.finalization.epic_gate_command;
|
|
46983
46349
|
if (Array.isArray(cfg.worktree?.copy_files) && cfg.worktree.copy_files.length > 0) {
|
|
46984
46350
|
configWorktreeCopyFiles = [...cfg.worktree.copy_files];
|
|
46985
46351
|
logger.info({ copyFiles: configWorktreeCopyFiles }, "Loaded worktree.copy_files from config");
|
|
@@ -47080,6 +46446,8 @@ async function runRunAction(options) {
|
|
|
47080
46446
|
agentId,
|
|
47081
46447
|
...noWorktree === true ? { noWorktree: true } : {},
|
|
47082
46448
|
...finalization !== void 0 ? { finalizationMode: finalization } : configFinalizationMode !== void 0 ? { finalizationMode: configFinalizationMode } : {},
|
|
46449
|
+
...configMergeStrategy !== void 0 ? { mergeStrategy: configMergeStrategy } : {},
|
|
46450
|
+
...configEpicGateCommand !== void 0 ? { epicGateCommand: configEpicGateCommand } : {},
|
|
47083
46451
|
...configWorktreeCopyFiles !== void 0 ? { worktreeCopyFiles: configWorktreeCopyFiles } : {}
|
|
47084
46452
|
});
|
|
47085
46453
|
let storyKeys = [...parsedStoryKeys];
|
|
@@ -47644,6 +47012,8 @@ async function runRunAction(options) {
|
|
|
47644
47012
|
probeAuthorStateIntegrating: resolvedProbeAuthorStateIntegrating,
|
|
47645
47013
|
...noWorktree === true ? { noWorktree: true } : {},
|
|
47646
47014
|
...finalization !== void 0 ? { finalizationMode: finalization } : configFinalizationMode !== void 0 ? { finalizationMode: configFinalizationMode } : {},
|
|
47015
|
+
...configMergeStrategy !== void 0 ? { mergeStrategy: configMergeStrategy } : {},
|
|
47016
|
+
...configEpicGateCommand !== void 0 ? { epicGateCommand: configEpicGateCommand } : {},
|
|
47647
47017
|
...configWorktreeCopyFiles !== void 0 ? { worktreeCopyFiles: configWorktreeCopyFiles } : {}
|
|
47648
47018
|
},
|
|
47649
47019
|
projectRoot,
|
|
@@ -47867,7 +47237,7 @@ async function runRunAction(options) {
|
|
|
47867
47237
|
}
|
|
47868
47238
|
}
|
|
47869
47239
|
async function runFullPipeline(options) {
|
|
47870
|
-
const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, projectRoot, events: eventsFlag, skipUx, research: researchFlag, skipResearch: skipResearchFlag, skipPreflight, skipVerification, maxReviewCycles = 2, retryBudget, registry: injectedRegistry, tokenCeilings, stories: explicitStories, telemetryEnabled: fullTelemetryEnabled, telemetryPort: fullTelemetryPort, agentId, meshUrl: fpMeshUrl, meshProjectId: fpMeshProjectId, engineType: fpEngineType, probeAuthor, probeAuthorStateIntegrating: fpProbeAuthorStateIntegrating, noWorktree, worktreeCopyFiles: fpWorktreeCopyFiles, finalizationMode: fpFinalizationMode } = options;
|
|
47240
|
+
const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, projectRoot, events: eventsFlag, skipUx, research: researchFlag, skipResearch: skipResearchFlag, skipPreflight, skipVerification, maxReviewCycles = 2, retryBudget, registry: injectedRegistry, tokenCeilings, stories: explicitStories, telemetryEnabled: fullTelemetryEnabled, telemetryPort: fullTelemetryPort, agentId, meshUrl: fpMeshUrl, meshProjectId: fpMeshProjectId, engineType: fpEngineType, probeAuthor, probeAuthorStateIntegrating: fpProbeAuthorStateIntegrating, noWorktree, worktreeCopyFiles: fpWorktreeCopyFiles, finalizationMode: fpFinalizationMode, mergeStrategy: fpMergeStrategy, epicGateCommand: fpEpicGateCommand } = options;
|
|
47871
47241
|
if (!existsSync$1(dbDir)) mkdirSync$1(dbDir, { recursive: true });
|
|
47872
47242
|
let doltServerFull = null;
|
|
47873
47243
|
try {
|
|
@@ -48178,6 +47548,8 @@ async function runFullPipeline(options) {
|
|
|
48178
47548
|
...fpProbeAuthorStateIntegrating !== void 0 ? { probeAuthorStateIntegrating: fpProbeAuthorStateIntegrating } : {},
|
|
48179
47549
|
...noWorktree === true ? { noWorktree: true } : {},
|
|
48180
47550
|
...fpFinalizationMode !== void 0 ? { finalizationMode: fpFinalizationMode } : {},
|
|
47551
|
+
...fpMergeStrategy !== void 0 ? { mergeStrategy: fpMergeStrategy } : {},
|
|
47552
|
+
...fpEpicGateCommand !== void 0 ? { epicGateCommand: fpEpicGateCommand } : {},
|
|
48181
47553
|
...fpWorktreeCopyFiles !== void 0 ? { worktreeCopyFiles: fpWorktreeCopyFiles } : {}
|
|
48182
47554
|
},
|
|
48183
47555
|
projectRoot,
|
|
@@ -48348,7 +47720,7 @@ async function runFullPipeline(options) {
|
|
|
48348
47720
|
*/
|
|
48349
47721
|
async function emitPreDispatchVersionAdvisory(currentVersion) {
|
|
48350
47722
|
if (process.env["SUBSTRATE_NO_UPDATE_CHECK"] === "1") return;
|
|
48351
|
-
const { createVersionManager } = await import("./version-manager-impl-
|
|
47723
|
+
const { createVersionManager } = await import("./version-manager-impl-DizCvYy8.js");
|
|
48352
47724
|
const vm = createVersionManager();
|
|
48353
47725
|
const result = await vm.checkForUpdates(true);
|
|
48354
47726
|
const gap = classifyVersionGap(currentVersion, result.latestVersion);
|
|
@@ -48454,5 +47826,5 @@ function registerRunCommand(program, version = "0.0.0", projectRoot = process.cw
|
|
|
48454
47826
|
}
|
|
48455
47827
|
|
|
48456
47828
|
//#endregion
|
|
48457
|
-
export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GLOBSTAR$1 as GLOBSTAR, GitClient, GrammarLoader, Minimatch$1 as Minimatch, Minipass, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter,
|
|
48458
|
-
//# sourceMappingURL=run-
|
|
47829
|
+
export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GLOBSTAR$1 as GLOBSTAR, GitClient, GrammarLoader, Minimatch$1 as Minimatch, Minipass, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, escape$1 as escape, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveMaxReviewCycles, resolveProbeAuthorStateIntegrating, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runProbeAuthor, runRunAction, runSolutioningPhase, unescape$1 as unescape, validateStopAfterFromConflict, wireNdjsonEmitter };
|
|
47830
|
+
//# sourceMappingURL=run-BAUlPom3.js.map
|