valent-pipeline 0.19.57 → 0.19.59
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/package.json +1 -1
- package/pipeline/steps/qa-a/write-spec.md +2 -0
- package/pipeline/steps/uxa/translate-spec.md +16 -0
- package/skills/valent-configure/SKILL.md +1 -0
- package/skills/valent-run-story-workflow/SKILL.md +1 -1
- package/src/commands/git-flow.js +5 -3
- package/src/commands/init.js +5 -1
- package/src/lib/config-schema.js +6 -0
- package/src/lib/git-flow.js +35 -6
package/package.json
CHANGED
|
@@ -87,6 +87,8 @@ For each NFR-sensitive path: `[NFR-PERF]` response time + load patterns; `[NFR-S
|
|
|
87
87
|
|
|
88
88
|
**Sanctioning a checkpoint `N/A (sanction)` — the visual gate covers browser-testable *static renders* only.** The PMCP visual gate drives a **stateless** browser-automation MCP: it can navigate, snapshot, and assert rendered DOM, but it CANNOT drive a persistent WebSocket event (`career.created`, `career.resumed`), sever a connection mid-session, or catch an in-flight transient. A checkpoint whose pass criteria *require* such a flow will false-FAIL the visual gate even when the feature works. For exactly those checkpoints, set the checklist's **`N/A (sanction)`** field to `yes — <rationale>`, where the rationale (a) states why the browser MCP cannot drive it and (b) **names the acceptance/integration test that DOES validate it** — that test MUST exist; an `na` with no validating test is a coverage hole, not a sanction. PMCP then records the checkpoint N/A (not FAIL) and JUDGE treats it as non-blocking. Keep every browser-testable static render (default/empty/error/success states, element presence, layout) as a normal PASS/FAIL checkpoint — only WS-event-driven / non-render flows are eligible. When in doubt, do NOT sanction: a real render that fails must stay a FAIL.
|
|
89
89
|
|
|
90
|
+
**The validating test a WS-driven sanction names MUST be a true end-to-end reachability case — never a service-layer test that bypasses the live event path.** When the UXA spec carries a Step 6b reachability contract for a WS-driven screen, author a REQUIRED `e2e` acceptance case that **drives the real triggering event over the actual connection** (register/login → issue the WS command → assert the screen leaves loading and renders), then sanction the matching visual checkpoint to it. A test that calls the service/handler layer directly (e.g. asserts `POST /api/careers` or the service method) can pass green while the **live WS command path is structurally broken** — a missing initial-state push, or a bootstrap command that violates a pre-event invariant (writing an FK row keyed on an id that does not exist until the event lands). That gap stays invisible until the visual gate a full attempt later. The reachability case is what forces the live path; the service-layer test is necessary but NOT sufficient to back a sanction.
|
|
91
|
+
|
|
90
92
|
## Step 9b: Test Quality Bar — make every case implementable AND falsifiable
|
|
91
93
|
|
|
92
94
|
The dev agents implement exactly what you specify, and CRITIC rejects tests that are weak, unfalsifiable, or bound to the wrong artifact. Spec the bar IN so it is built right the first time (each rule below traces to a real rework cycle):
|
|
@@ -37,6 +37,21 @@ For every page, document all five states. No exceptions.
|
|
|
37
37
|
|
|
38
38
|
Each state: condition (when), behavior (what user sees/can do).
|
|
39
39
|
|
|
40
|
+
## Step 6b: WS-Driven Screen Reachability Contract (Conditional — real-time / WebSocket / SSE UIs)
|
|
41
|
+
|
|
42
|
+
A screen whose data arrives over a **persistent connection** (WebSocket/SSE) — not an HTTP page load — is NOT reachable just because its component renders. It becomes reachable only once its **triggering event fires AND pushes the initial state** the screen needs to leave its loading state. Miss that push and the screen hangs in Loading forever: the component is correct, the screen is unreachable. This is the single most common late-caught UI defect on real-time apps — the acceptance/visual gate catches it at green, a full attempt late. Spec it HERE so the dev wires the push first-try.
|
|
43
|
+
|
|
44
|
+
For EACH WS-driven screen, document its **reachability contract**:
|
|
45
|
+
|
|
46
|
+
| Field | What to specify |
|
|
47
|
+
|-------|-----------------|
|
|
48
|
+
| **Triggering event** | The server→client event that makes this screen reachable (e.g. `career.created`, `career.resumed`) |
|
|
49
|
+
| **Initial-state-push payload** | The exact state fields the event MUST push so the screen's loading state resolves to active — name them (e.g. `school_name`, `season`, `current_phase`); "sends state" is not enough |
|
|
50
|
+
| **Pre-event session state** | What exists BEFORE the event (e.g. session keyed on a player/user id, no record yet) — flags bootstrap commands that must NOT assume post-event invariants (FK rows, ids that do not exist until the event lands) |
|
|
51
|
+
| **Unreachable-if** | The concrete failure: which screen hangs in which state if the push (or the bootstrap command) is missing/broken |
|
|
52
|
+
|
|
53
|
+
Each contract becomes a REQUIRED end-to-end reachability acceptance case in QA-A (drive the real event → assert the screen renders), and the matching PMCP visual checkpoint is sanctioned `N/A` (the stateless browser MCP cannot drive a persistent WS event — see QA-A Step 9). A screen reachable via a plain HTTP load only is out of scope — this is for event-driven reachability.
|
|
54
|
+
|
|
40
55
|
## Step 7: Accessibility Checklist (WCAG AA)
|
|
41
56
|
|
|
42
57
|
- [ ] ARIA labels for interactive components
|
|
@@ -71,6 +86,7 @@ A developer with zero project context must build every page from this spec alone
|
|
|
71
86
|
| Components have explicit dimensions/spacing/layout | | |
|
|
72
87
|
| Interactive elements have behavior for all actions | | |
|
|
73
88
|
| All five states documented per page | | |
|
|
89
|
+
| WS-driven screens carry a reachability contract (triggering event + named initial-state-push payload) — Step 6b | | |
|
|
74
90
|
| Area labels consistent and follow convention | | |
|
|
75
91
|
| No ambiguous language ("appropriate", "as needed") | | |
|
|
76
92
|
| Multi-language considerations documented | | |
|
|
@@ -107,6 +107,7 @@ the config — surface it explicitly:
|
|
|
107
107
|
| `git.parallelism.max_stories` | `2` | Concurrent story cap |
|
|
108
108
|
| `git.parallelism.worktree_dir` | `.valent-worktrees` | Worktree container (ignored via `.git/info/exclude`) |
|
|
109
109
|
| `git.parallelism.setup_commands` | `[]` | Commands run in each fresh worktree (env files, per-story ports, non-node installs) — MUST be idempotent (setup re-runs on resume) |
|
|
110
|
+
| `git.parallelism.teardown_commands` | `[]` | Commands run IN the worktree just before removal (on ship or rollover) — symmetric cleanup (e.g. `docker compose down` on a per-story stack) so a stale stack can't leak into the next story. Best-effort: a failure is logged but never blocks the ship/rollover |
|
|
110
111
|
|
|
111
112
|
### Step 4a: Evidence Gate (machine-evidence layer)
|
|
112
113
|
|
|
@@ -29,7 +29,7 @@ If no argument is provided, resolve the next work item from the backlog (see Ste
|
|
|
29
29
|
|
|
30
30
|
### Step 1: Load Pipeline Config
|
|
31
31
|
|
|
32
|
-
Read `.valent-pipeline/pipeline-config.yaml` and resolve the variables you need below (project type, paths, quality caps, `tech_stack`, `knowledge`). Capture `project.type` — it feeds `--project-type` in Step 1b, and the resolved candidate object then carries `projectType`, `profiles`, and the execution-mode flags for Step 4 (when the user gave an explicit story argument instead, run the Step 1b resolver command anyway and select that story's candidate object from its buckets). Also capture the entire `models` section (the `{ opus:[...], sonnet:[...], haiku:[...] }` tier→roles map) — pass it as the `models` arg so per-agent model tiers stay config-driven (editable via `/valent-configure`). If the config has no `models` section, omit the arg and the workflow uses its baked-in default assignment. Likewise capture the `reasoning` section (the `{ ultrathink:[...], 'think-harder':[...], 'think-hard':[...], think:[...] }` level→roles map) and pass it as the `reasoning` arg; it is blank by default (injects nothing). Omit the arg if the section is absent or all levels are empty. Also capture the `pre_critic_gate` section (deterministic lint/type/static checks) and pass it as the `preCriticGate` arg; omit it if absent or its `commands` list is empty (the gate is then a no-op). Likewise capture the `pack_gate` section (run-level pack-time freshness checks — they run ONCE at sprint intake, before the story spends tokens) and pass it as the `packGate` arg; omit it if absent or its `commands` list is empty. Capture the `atdd` section and pass it as the `atdd` arg — it drives the RED gate (pre-implementation acceptance-suite run that must fail) and the GREEN gate (post-CRITIC re-run that must pass with the acceptance sources unchanged); omit it if absent or its `command` is blank (both gates are then a loud no-op). Capture the `git` section and pass it as the `git` arg `{ enabled: <git.enabled ?? true>, targetBranch: <git.target_branch or ''>, storyBranchPrefix: <git.story_branch_prefix or 'story/'>, worktreeIsolation: <git.worktree_isolation ?? false>, parallelism: <git.parallelism or omit> }` — it drives the story git flow (branch off the target, full commit trail, merge --no-ff on SHIP); `worktreeIsolation` builds each story in its own worktree even when serial (parallel mode forces it on); `parallelism` ({ enabled, max_stories, worktree_dir, setup_commands }) additionally enables concurrent stories in worktrees (off by default). Omit the whole arg only to accept the defaults (flow ON, parallelism OFF, graceful no-op outside a git repo). Capture the `visual` section together with `tech_stack.browser_automation_mcp` and pass them as the `visual` arg `{ onMissingMcp: <visual.on_missing_mcp or 'block'>, browserMcp: <tech_stack.browser_automation_mcp or 'playwright-mcp'> }` — this drives the Visual (PMCP) phase for `ui`-profile stories.
|
|
32
|
+
Read `.valent-pipeline/pipeline-config.yaml` and resolve the variables you need below (project type, paths, quality caps, `tech_stack`, `knowledge`). Capture `project.type` — it feeds `--project-type` in Step 1b, and the resolved candidate object then carries `projectType`, `profiles`, and the execution-mode flags for Step 4 (when the user gave an explicit story argument instead, run the Step 1b resolver command anyway and select that story's candidate object from its buckets). Also capture the entire `models` section (the `{ opus:[...], sonnet:[...], haiku:[...] }` tier→roles map) — pass it as the `models` arg so per-agent model tiers stay config-driven (editable via `/valent-configure`). If the config has no `models` section, omit the arg and the workflow uses its baked-in default assignment. Likewise capture the `reasoning` section (the `{ ultrathink:[...], 'think-harder':[...], 'think-hard':[...], think:[...] }` level→roles map) and pass it as the `reasoning` arg; it is blank by default (injects nothing). Omit the arg if the section is absent or all levels are empty. Also capture the `pre_critic_gate` section (deterministic lint/type/static checks) and pass it as the `preCriticGate` arg; omit it if absent or its `commands` list is empty (the gate is then a no-op). Likewise capture the `pack_gate` section (run-level pack-time freshness checks — they run ONCE at sprint intake, before the story spends tokens) and pass it as the `packGate` arg; omit it if absent or its `commands` list is empty. Capture the `atdd` section and pass it as the `atdd` arg — it drives the RED gate (pre-implementation acceptance-suite run that must fail) and the GREEN gate (post-CRITIC re-run that must pass with the acceptance sources unchanged); omit it if absent or its `command` is blank (both gates are then a loud no-op). Capture the `git` section and pass it as the `git` arg `{ enabled: <git.enabled ?? true>, targetBranch: <git.target_branch or ''>, storyBranchPrefix: <git.story_branch_prefix or 'story/'>, worktreeIsolation: <git.worktree_isolation ?? false>, parallelism: <git.parallelism or omit> }` — it drives the story git flow (branch off the target, full commit trail, merge --no-ff on SHIP); `worktreeIsolation` builds each story in its own worktree even when serial (parallel mode forces it on); `parallelism` ({ enabled, max_stories, worktree_dir, setup_commands, teardown_commands }) additionally enables concurrent stories in worktrees (off by default); `setup_commands`/`teardown_commands` run in each worktree on create/remove (the git CLI reads them from config on disk). Omit the whole arg only to accept the defaults (flow ON, parallelism OFF, graceful no-op outside a git repo). Capture the `visual` section together with `tech_stack.browser_automation_mcp` and pass them as the `visual` arg `{ onMissingMcp: <visual.on_missing_mcp or 'block'>, browserMcp: <tech_stack.browser_automation_mcp or 'playwright-mcp'> }` — this drives the Visual (PMCP) phase for `ui`-profile stories.
|
|
33
33
|
|
|
34
34
|
### Step 1b: Resolve Next Work Item (when no argument provided)
|
|
35
35
|
|
package/src/commands/git-flow.js
CHANGED
|
@@ -36,6 +36,7 @@ function resolveOpts(options) {
|
|
|
36
36
|
worktree: options.worktree === true,
|
|
37
37
|
worktreeDir: options.worktreeDir || par.worktree_dir || DEFAULT_WORKTREE_DIR,
|
|
38
38
|
setupCommands: Array.isArray(par.setup_commands) ? par.setup_commands : [],
|
|
39
|
+
teardownCommands: Array.isArray(par.teardown_commands) ? par.teardown_commands : [],
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -78,7 +79,7 @@ export async function gitCommitPhaseCmd(options) {
|
|
|
78
79
|
|
|
79
80
|
export async function gitShipStoryCmd(options) {
|
|
80
81
|
try {
|
|
81
|
-
const { root, targetBranch, prefix, worktree } = resolveOpts(options);
|
|
82
|
+
const { root, targetBranch, prefix, worktree, teardownCommands } = resolveOpts(options);
|
|
82
83
|
// Ship-time evidence binding (review pass-3 #7): the clamp config rides pipeline-config.yaml
|
|
83
84
|
// (evidence_gate.ship_clamp) — no CLI bypass flag exists by design.
|
|
84
85
|
const { evidenceShipConfig } = await import('./evidence.js');
|
|
@@ -87,6 +88,7 @@ export async function gitShipStoryCmd(options) {
|
|
|
87
88
|
root, story: options.story, verdict: options.verdict || 'ship', targetBranch, prefix, worktree,
|
|
88
89
|
into: options.into || null,
|
|
89
90
|
shipClamp: shipCfg.mode === 'off' ? null : { mode: shipCfg.mode, label: shipCfg.label },
|
|
91
|
+
teardownCommands,
|
|
90
92
|
});
|
|
91
93
|
if (result.shipClampWarning) {
|
|
92
94
|
console.error(`git-flow WARNING: ship-time evidence binding failed (${result.shipClampWarning}) — proceeding (evidence_gate.ship_clamp: warn)`);
|
|
@@ -135,8 +137,8 @@ export async function gitStoryStatusCmd(options) {
|
|
|
135
137
|
|
|
136
138
|
export async function gitLeaveStoryCmd(options) {
|
|
137
139
|
try {
|
|
138
|
-
const { root, targetBranch, prefix, worktree } = resolveOpts(options);
|
|
139
|
-
finish(leaveStory({ root, story: options.story, reason: options.reason || 'rejected', targetBranch, prefix, worktree }));
|
|
140
|
+
const { root, targetBranch, prefix, worktree, teardownCommands } = resolveOpts(options);
|
|
141
|
+
finish(leaveStory({ root, story: options.story, reason: options.reason || 'rejected', targetBranch, prefix, worktree, teardownCommands }));
|
|
140
142
|
} catch (e) {
|
|
141
143
|
fail(e);
|
|
142
144
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -554,12 +554,16 @@ git:
|
|
|
554
554
|
# worktree under worktree_dir; the main checkout stays on the target, merges are serialized, and
|
|
555
555
|
# each ship gets a post-merge acceptance verify (ATDD armed). OFF by default — sequential sprint.
|
|
556
556
|
# setup_commands run in each fresh worktree (node_modules is linked + pipeline.db copied for you;
|
|
557
|
-
# add project-specific needs here, e.g. env files or per-story ports).
|
|
557
|
+
# add project-specific needs here, e.g. env files or per-story ports). teardown_commands run in
|
|
558
|
+
# the worktree just before it is removed (on ship or rollover) — the symmetric cleanup (e.g.
|
|
559
|
+
# "docker compose down" on a per-story stack) so a stale stack can't leak into the next story.
|
|
560
|
+
# Best-effort: a teardown failure is logged but never blocks the ship/rollover.
|
|
558
561
|
parallelism:
|
|
559
562
|
enabled: ${config.git.parallelism?.enabled ?? false}
|
|
560
563
|
max_stories: ${config.git.parallelism?.max_stories ?? 2}
|
|
561
564
|
worktree_dir: "${config.git.parallelism?.worktree_dir ?? '.valent-worktrees'}"
|
|
562
565
|
setup_commands: [${(config.git.parallelism?.setup_commands ?? []).map((c) => JSON.stringify(c)).join(', ')}]
|
|
566
|
+
teardown_commands: [${(config.git.parallelism?.teardown_commands ?? []).map((c) => JSON.stringify(c)).join(', ')}]
|
|
563
567
|
|
|
564
568
|
communication:
|
|
565
569
|
handoff_format: "${config.communication.handoff_format}"
|
package/src/lib/config-schema.js
CHANGED
|
@@ -545,6 +545,12 @@ export const defaults = {
|
|
|
545
545
|
// Commands run in each fresh worktree after node_modules linking + pipeline.db copy —
|
|
546
546
|
// project-specific setup (env files, per-story ports, non-node package installs).
|
|
547
547
|
setup_commands: [],
|
|
548
|
+
// Commands run IN the worktree just before it is removed (on ship or rollover) — the
|
|
549
|
+
// symmetric complement to setup_commands. Use to tear down per-worktree side effects a setup
|
|
550
|
+
// command created (e.g. `docker compose down` on a per-story stack) so a stale stack can't
|
|
551
|
+
// leak into the next story and pin a shared host port. Best-effort: a failure is logged but
|
|
552
|
+
// never blocks the ship/rollover.
|
|
553
|
+
teardown_commands: [],
|
|
548
554
|
},
|
|
549
555
|
},
|
|
550
556
|
communication: {
|
package/src/lib/git-flow.js
CHANGED
|
@@ -276,12 +276,41 @@ function worktreeForBranch(root, branch) {
|
|
|
276
276
|
return null;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Best-effort teardown (config git.parallelism.teardown_commands), run IN the worktree just before
|
|
281
|
+
* it is removed — the symmetric complement to setupWorktree's setup_commands. Use it to tear down
|
|
282
|
+
* per-worktree side effects a setup command created (e.g. `docker compose down` on a per-story
|
|
283
|
+
* stack) so a stale stack can't leak into the next story and pin a shared host port. UNLIKE setup,
|
|
284
|
+
* a teardown failure NEVER throws: the worktree is being discarded regardless, and a failed cleanup
|
|
285
|
+
* must not strand a shipped/rolled-over story — it is reported loudly on stderr and removal still
|
|
286
|
+
* proceeds. Runs in the worktree's cwd so a command can read its compose project name / .env.test.
|
|
287
|
+
*/
|
|
288
|
+
function runWorktreeTeardown(wtPath, teardownCommands = []) {
|
|
289
|
+
if (!Array.isArray(teardownCommands) || !existsSync(wtPath)) return { ran: [], failed: [] };
|
|
290
|
+
const ran = [], failed = [];
|
|
291
|
+
for (const cmd of teardownCommands) {
|
|
292
|
+
if (typeof cmd !== 'string' || !cmd.trim()) continue;
|
|
293
|
+
try {
|
|
294
|
+
execSync(cmd, { cwd: wtPath, stdio: ['ignore', 'pipe', 'pipe'], timeout: 300000 });
|
|
295
|
+
ran.push(cmd);
|
|
296
|
+
} catch (e) {
|
|
297
|
+
const stderr = e.stderr ? e.stderr.toString().slice(-2000) : '';
|
|
298
|
+
const why = e.killed ? ' (timed out after 300s)' : '';
|
|
299
|
+
failed.push(cmd);
|
|
300
|
+
console.error(`git-flow WARNING: worktree teardown command failed${why} (best-effort, removal proceeds): ${cmd}${stderr ? `\n${stderr}` : ''}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { ran, failed };
|
|
304
|
+
}
|
|
305
|
+
|
|
279
306
|
/**
|
|
280
307
|
* Remove a worktree AFTER its sweep commit. Plain remove refuses while untracked/ignored files
|
|
281
308
|
* remain — post-sweep the only leftovers are ignored setup artifacts (the node_modules link, the
|
|
282
|
-
* pipeline.db copy), so the --force fallback discards nothing of value.
|
|
309
|
+
* pipeline.db copy), so the --force fallback discards nothing of value. Teardown commands run FIRST
|
|
310
|
+
* (in the worktree, while its files still exist), best-effort.
|
|
283
311
|
*/
|
|
284
|
-
function removeWorktreeDir(root, wtPath) {
|
|
312
|
+
function removeWorktreeDir(root, wtPath, teardownCommands = []) {
|
|
313
|
+
runWorktreeTeardown(wtPath, teardownCommands);
|
|
285
314
|
try {
|
|
286
315
|
git(root, ['worktree', 'remove', wtPath]);
|
|
287
316
|
return true;
|
|
@@ -691,7 +720,7 @@ export function commitPhase({ root, story, phase, summary = null, gate = null })
|
|
|
691
720
|
* shipped; the branch retains every commit. Idempotent: an already-merged branch reports
|
|
692
721
|
* { merged:true, already:true }.
|
|
693
722
|
*/
|
|
694
|
-
export function shipStory({ root, story, verdict = 'ship', targetBranch = '', prefix = 'story/', worktree = false, into = null, shipClamp = null }) {
|
|
723
|
+
export function shipStory({ root, story, verdict = 'ship', targetBranch = '', prefix = 'story/', worktree = false, into = null, shipClamp = null, teardownCommands = [] }) {
|
|
695
724
|
if (!isRepoWithCommits(root)) return NOT_REPO;
|
|
696
725
|
const branch = storyBranchName(prefix, story);
|
|
697
726
|
if (!branchExists(root, branch)) throw new Error(`story branch "${branch}" does not exist — was start-story run?`);
|
|
@@ -735,7 +764,7 @@ export function shipStory({ root, story, verdict = 'ship', targetBranch = '', pr
|
|
|
735
764
|
sweep = commitAll(root, buildCommitMessage({ storyId: story, phase: 'ship', trailers: { Verdict: verdict } }));
|
|
736
765
|
}
|
|
737
766
|
|
|
738
|
-
const removeWorktree = () => (wt ? removeWorktreeDir(root, wt) : false);
|
|
767
|
+
const removeWorktree = () => (wt ? removeWorktreeDir(root, wt, teardownCommands) : false);
|
|
739
768
|
|
|
740
769
|
if (isMergedInto(root, branch, target)) {
|
|
741
770
|
if (currentBranch(root) !== target) git(root, ['checkout', target]);
|
|
@@ -898,7 +927,7 @@ export function syncStory({ root, story, targetBranch = '', prefix = 'story/', c
|
|
|
898
927
|
* must survive for the inline fix or next-sprint retry), checkout the target, branch retained.
|
|
899
928
|
* Safe to call from any branch state; idempotent.
|
|
900
929
|
*/
|
|
901
|
-
export function leaveStory({ root, story, reason = 'rejected', targetBranch = '', prefix = 'story/', worktree = false }) {
|
|
930
|
+
export function leaveStory({ root, story, reason = 'rejected', targetBranch = '', prefix = 'story/', worktree = false, teardownCommands = [] }) {
|
|
902
931
|
if (!isRepoWithCommits(root)) return NOT_REPO;
|
|
903
932
|
const branch = storyBranchName(prefix, story);
|
|
904
933
|
|
|
@@ -910,7 +939,7 @@ export function leaveStory({ root, story, reason = 'rejected', targetBranch = ''
|
|
|
910
939
|
let removed = false;
|
|
911
940
|
if (wt) {
|
|
912
941
|
sweep = commitAll(wt, buildCommitMessage({ storyId: story, phase: 'rollover', summary: `roll over (${reason})` }));
|
|
913
|
-
removed = removeWorktreeDir(root, wt);
|
|
942
|
+
removed = removeWorktreeDir(root, wt, teardownCommands);
|
|
914
943
|
}
|
|
915
944
|
return { left: true, branch, target: targetBranch || currentBranch(root), sweepSha: sweep.sha || null, reason, worktreeRemoved: removed };
|
|
916
945
|
}
|