substrate-ai 0.20.148 → 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 +24 -16
- package/dist/{decisions-DSDqOEN4.js → decisions-De43bvYq.js} +1 -1
- package/dist/{dist-CZk5SeSt.js → dist-C2CAZDep.js} +894 -17
- package/dist/{errors-_tbIg6Cv.js → errors-Db8EIM9w.js} +2 -2
- package/dist/{experimenter-RnaEiLUD.js → experimenter-Dm5jUPO6.js} +1 -1
- package/dist/{health-Aqc5cghP.js → health-ByQadq3J.js} +3 -3
- package/dist/{health-Cm0aQCQL.js → health-WGcvcIYK.js} +3 -3
- package/dist/{index-CvNLhgKV.d.ts → index-DxeIRBBf.d.ts} +5 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +3 -3
- package/dist/{interactive-prompt-BLkX0iE_.js → interactive-prompt-CMBlj6UF.js} +2 -2
- package/dist/{manifest-read-D6NAZgt8.js → manifest-read-knD2uWkz.js} +2 -2
- package/dist/modules/interactive-prompt/index.js +3 -3
- package/dist/{routing-QBUaShFv.js → routing-B3sgH5MZ.js} +1 -1
- package/dist/{run-B9DV9sro.js → run-BAUlPom3.js} +53 -829
- package/dist/{run-BX-__EJm.js → run-CtIc7_rK.js} +5 -5
- package/dist/src/modules/recovery-engine/index.d.ts +1 -1
- package/dist/{upgrade-emlrK3YW.js → upgrade-B3iamwjO.js} +2 -2
- package/dist/{upgrade-BEfZJU4K.js → upgrade-DOmKCyOY.js} +2 -2
- package/dist/{version-manager-impl-_x1n4xZk.js → version-manager-impl-DizCvYy8.js} +1 -1
- package/package.json +1 -1
- package/dist/adapter-registry-hLwwiJGM.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) {
|
|
@@ -19979,8 +19203,8 @@ async function resolveContext(ref, deps, runId, params, stepOutputs) {
|
|
|
19979
19203
|
return params[key] ?? "";
|
|
19980
19204
|
}
|
|
19981
19205
|
if (source.startsWith("decision:")) {
|
|
19982
|
-
const path$
|
|
19983
|
-
const [phase, category] = path$
|
|
19206
|
+
const path$4 = source.slice(9);
|
|
19207
|
+
const [phase, category] = path$4.split(".");
|
|
19984
19208
|
if (!phase || !category) return "";
|
|
19985
19209
|
const decisions = await getDecisionsByPhaseForRun(deps.db, runId, phase);
|
|
19986
19210
|
const filtered = decisions.filter((d) => d.category === category);
|
|
@@ -20051,8 +19275,8 @@ async function runSteps(steps, deps, runId, phase, params) {
|
|
|
20051
19275
|
for (const ref of step.context) {
|
|
20052
19276
|
let value;
|
|
20053
19277
|
if (ref.source.startsWith("decision:")) {
|
|
20054
|
-
const path$
|
|
20055
|
-
const [decPhase, decCategory] = path$
|
|
19278
|
+
const path$4 = ref.source.slice(9);
|
|
19279
|
+
const [decPhase, decCategory] = path$4.split(".");
|
|
20056
19280
|
if (decPhase && decCategory) {
|
|
20057
19281
|
const decisions = await getDecisionsByPhaseForRun(deps.db, runId, decPhase);
|
|
20058
19282
|
const filtered = decisions.filter((d) => d.category === decCategory);
|
|
@@ -31140,8 +30364,8 @@ var require_uri_all = __commonJS({ "node_modules/uri-js/dist/es5/uri.all.js"(exp
|
|
|
31140
30364
|
wsComponents.secure = void 0;
|
|
31141
30365
|
}
|
|
31142
30366
|
if (wsComponents.resourceName) {
|
|
31143
|
-
var _wsComponents$resourc = wsComponents.resourceName.split("?"), _wsComponents$resourc2 = slicedToArray(_wsComponents$resourc, 2), path$
|
|
31144
|
-
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;
|
|
31145
30369
|
wsComponents.query = query;
|
|
31146
30370
|
wsComponents.resourceName = void 0;
|
|
31147
30371
|
}
|
|
@@ -31482,12 +30706,12 @@ var require_util = __commonJS({ "node_modules/ajv/lib/compile/util.js"(exports,
|
|
|
31482
30706
|
return "'" + escapeQuotes(str) + "'";
|
|
31483
30707
|
}
|
|
31484
30708
|
function getPathExpr(currentPath, expr, jsonPointers, isNumber) {
|
|
31485
|
-
var path$
|
|
31486
|
-
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);
|
|
31487
30711
|
}
|
|
31488
30712
|
function getPath(currentPath, prop, jsonPointers) {
|
|
31489
|
-
var path$
|
|
31490
|
-
return joinPaths(currentPath, path$
|
|
30713
|
+
var path$4 = jsonPointers ? toQuotedString("/" + escapeJsonPointer(prop)) : toQuotedString(getProperty(prop));
|
|
30714
|
+
return joinPaths(currentPath, path$4);
|
|
31491
30715
|
}
|
|
31492
30716
|
var JSON_POINTER$1 = /^\/(?:[^~]|~0|~1)*$/;
|
|
31493
30717
|
var RELATIVE_JSON_POINTER$1 = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/;
|
|
@@ -35696,8 +34920,8 @@ var ToolRegistry = class {
|
|
|
35696
34920
|
if (!valid) {
|
|
35697
34921
|
const errors = validate$1.errors ?? [];
|
|
35698
34922
|
const messages = errors.map((e) => {
|
|
35699
|
-
const path$
|
|
35700
|
-
return `${path$
|
|
34923
|
+
const path$4 = e.instancePath ?? e.dataPath ?? "";
|
|
34924
|
+
return `${path$4} ${e.message ?? ""}`.trim();
|
|
35701
34925
|
}).join(", ");
|
|
35702
34926
|
return {
|
|
35703
34927
|
content: `Validation failed for tool '${name}': ${messages}`,
|
|
@@ -42403,10 +41627,10 @@ var PathBase = class {
|
|
|
42403
41627
|
/**
|
|
42404
41628
|
* Get the Path object referenced by the string path, resolved from this Path
|
|
42405
41629
|
*/
|
|
42406
|
-
resolve(path$
|
|
42407
|
-
if (!path$
|
|
42408
|
-
const rootPath = this.getRootString(path$
|
|
42409
|
-
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);
|
|
42410
41634
|
const dirParts = dir.split(this.splitSep);
|
|
42411
41635
|
const result = rootPath ? this.getRoot(rootPath).#resolveParts(dirParts) : this.#resolveParts(dirParts);
|
|
42412
41636
|
return result;
|
|
@@ -43061,8 +42285,8 @@ var PathWin32 = class PathWin32 extends PathBase {
|
|
|
43061
42285
|
/**
|
|
43062
42286
|
* @internal
|
|
43063
42287
|
*/
|
|
43064
|
-
getRootString(path$
|
|
43065
|
-
return win32.parse(path$
|
|
42288
|
+
getRootString(path$4) {
|
|
42289
|
+
return win32.parse(path$4).root;
|
|
43066
42290
|
}
|
|
43067
42291
|
/**
|
|
43068
42292
|
* @internal
|
|
@@ -43107,8 +42331,8 @@ var PathPosix = class PathPosix extends PathBase {
|
|
|
43107
42331
|
/**
|
|
43108
42332
|
* @internal
|
|
43109
42333
|
*/
|
|
43110
|
-
getRootString(path$
|
|
43111
|
-
return path$
|
|
42334
|
+
getRootString(path$4) {
|
|
42335
|
+
return path$4.startsWith("/") ? "/" : "";
|
|
43112
42336
|
}
|
|
43113
42337
|
/**
|
|
43114
42338
|
* @internal
|
|
@@ -43201,9 +42425,9 @@ var PathScurryBase = class {
|
|
|
43201
42425
|
/**
|
|
43202
42426
|
* Get the depth of a provided path, string, or the cwd
|
|
43203
42427
|
*/
|
|
43204
|
-
depth(path$
|
|
43205
|
-
if (typeof path$
|
|
43206
|
-
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();
|
|
43207
42431
|
}
|
|
43208
42432
|
/**
|
|
43209
42433
|
* Return the cache of child entries. Exposed so subclasses can create
|
|
@@ -43584,9 +42808,9 @@ var PathScurryBase = class {
|
|
|
43584
42808
|
process$1();
|
|
43585
42809
|
return results;
|
|
43586
42810
|
}
|
|
43587
|
-
chdir(path$
|
|
42811
|
+
chdir(path$4 = this.cwd) {
|
|
43588
42812
|
const oldCwd = this.cwd;
|
|
43589
|
-
this.cwd = typeof path$
|
|
42813
|
+
this.cwd = typeof path$4 === "string" ? this.cwd.resolve(path$4) : path$4;
|
|
43590
42814
|
this.cwd[setAsCwd](oldCwd);
|
|
43591
42815
|
}
|
|
43592
42816
|
};
|
|
@@ -43970,8 +43194,8 @@ var MatchRecord = class {
|
|
|
43970
43194
|
this.store.set(target, current === void 0 ? n$1 : n$1 & current);
|
|
43971
43195
|
}
|
|
43972
43196
|
entries() {
|
|
43973
|
-
return [...this.store.entries()].map(([path$
|
|
43974
|
-
path$
|
|
43197
|
+
return [...this.store.entries()].map(([path$4, n$1]) => [
|
|
43198
|
+
path$4,
|
|
43975
43199
|
!!(n$1 & 2),
|
|
43976
43200
|
!!(n$1 & 1)
|
|
43977
43201
|
]);
|
|
@@ -44147,9 +43371,9 @@ var GlobUtil = class {
|
|
|
44147
43371
|
signal;
|
|
44148
43372
|
maxDepth;
|
|
44149
43373
|
includeChildMatches;
|
|
44150
|
-
constructor(patterns, path$
|
|
43374
|
+
constructor(patterns, path$4, opts) {
|
|
44151
43375
|
this.patterns = patterns;
|
|
44152
|
-
this.path = path$
|
|
43376
|
+
this.path = path$4;
|
|
44153
43377
|
this.opts = opts;
|
|
44154
43378
|
this.#sep = !opts.posix && opts.platform === "win32" ? "\\" : "/";
|
|
44155
43379
|
this.includeChildMatches = opts.includeChildMatches !== false;
|
|
@@ -44170,11 +43394,11 @@ var GlobUtil = class {
|
|
|
44170
43394
|
});
|
|
44171
43395
|
}
|
|
44172
43396
|
}
|
|
44173
|
-
#ignored(path$
|
|
44174
|
-
return this.seen.has(path$
|
|
43397
|
+
#ignored(path$4) {
|
|
43398
|
+
return this.seen.has(path$4) || !!this.#ignore?.ignored?.(path$4);
|
|
44175
43399
|
}
|
|
44176
|
-
#childrenIgnored(path$
|
|
44177
|
-
return !!this.#ignore?.childrenIgnored?.(path$
|
|
43400
|
+
#childrenIgnored(path$4) {
|
|
43401
|
+
return !!this.#ignore?.childrenIgnored?.(path$4);
|
|
44178
43402
|
}
|
|
44179
43403
|
pause() {
|
|
44180
43404
|
this.paused = true;
|
|
@@ -44356,8 +43580,8 @@ var GlobUtil = class {
|
|
|
44356
43580
|
};
|
|
44357
43581
|
var GlobWalker = class extends GlobUtil {
|
|
44358
43582
|
matches = new Set();
|
|
44359
|
-
constructor(patterns, path$
|
|
44360
|
-
super(patterns, path$
|
|
43583
|
+
constructor(patterns, path$4, opts) {
|
|
43584
|
+
super(patterns, path$4, opts);
|
|
44361
43585
|
}
|
|
44362
43586
|
matchEmit(e) {
|
|
44363
43587
|
this.matches.add(e);
|
|
@@ -44384,8 +43608,8 @@ var GlobWalker = class extends GlobUtil {
|
|
|
44384
43608
|
};
|
|
44385
43609
|
var GlobStream = class extends GlobUtil {
|
|
44386
43610
|
results;
|
|
44387
|
-
constructor(patterns, path$
|
|
44388
|
-
super(patterns, path$
|
|
43611
|
+
constructor(patterns, path$4, opts) {
|
|
43612
|
+
super(patterns, path$4, opts);
|
|
44389
43613
|
this.results = new Minipass({
|
|
44390
43614
|
signal: this.signal,
|
|
44391
43615
|
objectMode: true
|
|
@@ -48496,7 +47720,7 @@ async function runFullPipeline(options) {
|
|
|
48496
47720
|
*/
|
|
48497
47721
|
async function emitPreDispatchVersionAdvisory(currentVersion) {
|
|
48498
47722
|
if (process.env["SUBSTRATE_NO_UPDATE_CHECK"] === "1") return;
|
|
48499
|
-
const { createVersionManager } = await import("./version-manager-impl-
|
|
47723
|
+
const { createVersionManager } = await import("./version-manager-impl-DizCvYy8.js");
|
|
48500
47724
|
const vm = createVersionManager();
|
|
48501
47725
|
const result = await vm.checkForUpdates(true);
|
|
48502
47726
|
const gap = classifyVersionGap(currentVersion, result.latestVersion);
|
|
@@ -48602,5 +47826,5 @@ function registerRunCommand(program, version = "0.0.0", projectRoot = process.cw
|
|
|
48602
47826
|
}
|
|
48603
47827
|
|
|
48604
47828
|
//#endregion
|
|
48605
|
-
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,
|
|
48606
|
-
//# 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
|