cclaw-cli 7.1.1 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ import { RUNTIME_ROOT } from "../../constants.js";
3
3
  import { stageSchema } from "../../content/stage-schema.js";
4
4
  import { readFlowState } from "../../runs.js";
5
5
  import { TRACK_STAGES } from "../../types.js";
6
+ import { STACK_DISCOVERY_DIR_MARKERS, STACK_DISCOVERY_MARKERS } from "../../stack-detection.js";
6
7
  import { coerceCandidateFlowState } from "./flow-state-coercion.js";
7
8
  import { buildValidationReport } from "./advance.js";
8
9
  import fs from "node:fs/promises";
@@ -171,24 +172,11 @@ export async function discoverStartFlowContext(projectRoot) {
171
172
  lines.push(originFiles.length > 0
172
173
  ? `- Origin docs scanned: found ${originFiles.join(", ")}.`
173
174
  : "- Origin docs scanned: no PRD/RFC/ADR/design/spec files found in configured locations.");
174
- const stackMarkers = await listExistingFiles(projectRoot, [
175
- "package.json",
176
- "pyproject.toml",
177
- "requirements.txt",
178
- "requirements-dev.txt",
179
- ".python-version",
180
- "go.mod",
181
- "Cargo.toml",
182
- "pom.xml",
183
- "build.gradle",
184
- "build.gradle.kts",
185
- "Dockerfile",
186
- "docker-compose.yml",
187
- "docker-compose.yaml",
188
- ".gitlab-ci.yml"
189
- ]);
190
- if (await pathExists(projectRoot, ".github/workflows")) {
191
- stackMarkers.push(".github/workflows/");
175
+ const stackMarkers = await listExistingFiles(projectRoot, [...STACK_DISCOVERY_MARKERS]);
176
+ for (const markerDir of STACK_DISCOVERY_DIR_MARKERS) {
177
+ if (await pathExists(projectRoot, markerDir)) {
178
+ stackMarkers.push(`${markerDir}/`);
179
+ }
192
180
  }
193
181
  lines.push(stackMarkers.length > 0
194
182
  ? `- Stack markers scanned: found ${stackMarkers.join(", ")}.`
@@ -1,9 +1,10 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { promisify } from "node:util";
4
- import { readConfig, resolveTddCommitMode } from "../config.js";
4
+ import { readConfig, resolveTddCommitMode, resolveTddIsolationMode, resolveTddWorktreeRoot } from "../config.js";
5
5
  import { readDelegationLedger } from "../delegation.js";
6
6
  import { exists } from "../fs-utils.js";
7
+ import { cleanupWorktree, commitAndMergeBack, createSliceWorktree, WorktreeMergeConflictError, WorktreeUnsupportedError } from "../worktree-manager.js";
7
8
  const execFileAsync = promisify(execFile);
8
9
  function parseCsv(raw) {
9
10
  return raw
@@ -22,7 +23,9 @@ function parseSliceCommitArgs(tokens) {
22
23
  let taskId;
23
24
  let title;
24
25
  let runId;
26
+ let worktreePath;
25
27
  const claimedPaths = [];
28
+ let prepareWorktree = false;
26
29
  let json = false;
27
30
  let quiet = false;
28
31
  for (let i = 0; i < tokens.length; i += 1) {
@@ -45,6 +48,10 @@ function parseSliceCommitArgs(tokens) {
45
48
  quiet = true;
46
49
  continue;
47
50
  }
51
+ if (token === "--prepare-worktree") {
52
+ prepareWorktree = true;
53
+ continue;
54
+ }
48
55
  if (token.startsWith("--slice=") || token === "--slice") {
49
56
  sliceId = valueFrom("--slice").trim();
50
57
  continue;
@@ -65,6 +72,13 @@ function parseSliceCommitArgs(tokens) {
65
72
  runId = valueFrom("--run-id").trim();
66
73
  continue;
67
74
  }
75
+ if (token.startsWith("--worktree-path=") || token === "--worktree-path") {
76
+ const resolved = valueFrom("--worktree-path").trim();
77
+ if (resolved.length > 0) {
78
+ worktreePath = resolved;
79
+ }
80
+ continue;
81
+ }
68
82
  if (token.startsWith("--claimed-paths=") || token === "--claimed-paths") {
69
83
  claimedPaths.push(...parseCsv(valueFrom("--claimed-paths")));
70
84
  continue;
@@ -89,7 +103,9 @@ function parseSliceCommitArgs(tokens) {
89
103
  taskId,
90
104
  title,
91
105
  runId,
106
+ worktreePath,
92
107
  claimedPaths,
108
+ prepareWorktree,
93
109
  json,
94
110
  quiet
95
111
  };
@@ -132,6 +148,12 @@ function parsePorcelainPaths(raw) {
132
148
  }
133
149
  return [...new Set(out)];
134
150
  }
151
+ async function gitChangedPaths(cwd) {
152
+ const { stdout: statusRaw } = await execFileAsync("git", ["status", "--porcelain", "-uall"], {
153
+ cwd
154
+ });
155
+ return parsePorcelainPaths(statusRaw);
156
+ }
135
157
  function matchesClaimedPath(changedPath, claimedPaths) {
136
158
  const changed = normalizePathLike(changedPath);
137
159
  return claimedPaths.some((rawClaimed) => {
@@ -171,6 +193,67 @@ export async function runSliceCommitCommand(projectRoot, tokens, io) {
171
193
  }
172
194
  const config = await readConfig(projectRoot).catch(() => null);
173
195
  const commitMode = resolveTddCommitMode(config);
196
+ const isolationMode = resolveTddIsolationMode(config);
197
+ const worktreeRoot = resolveTddWorktreeRoot(config);
198
+ const gitPresent = await exists(path.join(projectRoot, ".git"));
199
+ if (args.prepareWorktree) {
200
+ if (!gitPresent) {
201
+ output(io, args, {
202
+ ok: true,
203
+ skipped: true,
204
+ reason: "no-git",
205
+ message: "slice-worktree skipped: .git is missing"
206
+ });
207
+ return 0;
208
+ }
209
+ if (isolationMode === "in-place") {
210
+ output(io, args, {
211
+ ok: true,
212
+ skipped: true,
213
+ reason: "isolation-in-place",
214
+ isolationMode,
215
+ message: "slice-worktree skipped: tdd.isolationMode=in-place"
216
+ });
217
+ return 0;
218
+ }
219
+ try {
220
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: projectRoot });
221
+ const prepared = await createSliceWorktree(args.sliceId, stdout.trim(), args.claimedPaths, {
222
+ projectRoot,
223
+ worktreeRoot
224
+ });
225
+ output(io, args, {
226
+ ok: true,
227
+ prepared: true,
228
+ sliceId: args.sliceId,
229
+ spanId: args.spanId,
230
+ worktreePath: prepared.path,
231
+ baseRef: prepared.ref
232
+ });
233
+ return 0;
234
+ }
235
+ catch (error) {
236
+ if (error instanceof WorktreeUnsupportedError) {
237
+ output(io, args, {
238
+ ok: true,
239
+ skipped: true,
240
+ reason: "worktree-unavailable",
241
+ degradedCommitMode: "agent-required",
242
+ message: error.message
243
+ });
244
+ return 0;
245
+ }
246
+ output(io, args, {
247
+ ok: false,
248
+ errorCode: "worktree_prepare_failed",
249
+ details: {
250
+ message: error instanceof Error ? error.message : String(error)
251
+ },
252
+ message: `worktree_prepare_failed: ${error instanceof Error ? error.message : String(error)}`
253
+ }, "stderr");
254
+ return 1;
255
+ }
256
+ }
174
257
  if (commitMode !== "managed-per-slice") {
175
258
  output(io, args, {
176
259
  ok: true,
@@ -181,7 +264,6 @@ export async function runSliceCommitCommand(projectRoot, tokens, io) {
181
264
  });
182
265
  return 0;
183
266
  }
184
- const gitPresent = await exists(path.join(projectRoot, ".git"));
185
267
  if (!gitPresent) {
186
268
  output(io, args, {
187
269
  ok: true,
@@ -206,11 +288,64 @@ export async function runSliceCommitCommand(projectRoot, tokens, io) {
206
288
  }, "stderr");
207
289
  return 2;
208
290
  }
209
- const { stdout: statusRaw } = await execFileAsync("git", ["status", "--porcelain", "-uall"], {
210
- cwd: projectRoot
211
- });
212
- const changedPaths = parsePorcelainPaths(statusRaw);
291
+ let managedWorktreePath = null;
292
+ let activeCwd = projectRoot;
293
+ let degradedToInPlace = false;
294
+ const requestedWorktreePath = typeof args.worktreePath === "string" && args.worktreePath.trim().length > 0
295
+ ? path.resolve(projectRoot, args.worktreePath.trim())
296
+ : null;
297
+ if (requestedWorktreePath && await exists(requestedWorktreePath)) {
298
+ managedWorktreePath = requestedWorktreePath;
299
+ activeCwd = requestedWorktreePath;
300
+ }
301
+ else if (isolationMode !== "in-place") {
302
+ try {
303
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: projectRoot });
304
+ const prepared = await createSliceWorktree(args.sliceId, stdout.trim(), claimedPaths, {
305
+ projectRoot,
306
+ worktreeRoot
307
+ });
308
+ managedWorktreePath = prepared.path;
309
+ activeCwd = prepared.path;
310
+ }
311
+ catch (error) {
312
+ if (error instanceof WorktreeUnsupportedError) {
313
+ output(io, args, {
314
+ ok: true,
315
+ skipped: true,
316
+ reason: "worktree-unavailable",
317
+ degradedCommitMode: "agent-required",
318
+ message: error.message
319
+ });
320
+ return 0;
321
+ }
322
+ output(io, args, {
323
+ ok: false,
324
+ errorCode: "worktree_prepare_failed",
325
+ details: {
326
+ message: error instanceof Error ? error.message : String(error)
327
+ },
328
+ message: `worktree_prepare_failed: ${error instanceof Error ? error.message : String(error)}`
329
+ }, "stderr");
330
+ return 1;
331
+ }
332
+ }
333
+ const cleanupManagedWorktree = async () => {
334
+ if (!managedWorktreePath)
335
+ return;
336
+ await cleanupWorktree(managedWorktreePath, { projectRoot }).catch(() => undefined);
337
+ };
338
+ let changedPaths = await gitChangedPaths(activeCwd);
339
+ if (changedPaths.length === 0 && managedWorktreePath && activeCwd !== projectRoot) {
340
+ const rootChangedPaths = await gitChangedPaths(projectRoot);
341
+ if (rootChangedPaths.length > 0) {
342
+ activeCwd = projectRoot;
343
+ changedPaths = rootChangedPaths;
344
+ degradedToInPlace = true;
345
+ }
346
+ }
213
347
  if (changedPaths.length === 0) {
348
+ await cleanupManagedWorktree();
214
349
  output(io, args, {
215
350
  ok: true,
216
351
  skipped: true,
@@ -236,6 +371,7 @@ export async function runSliceCommitCommand(projectRoot, tokens, io) {
236
371
  }
237
372
  const changedInClaim = changedPaths.filter((p) => matchesClaimedPath(p, claimedPaths));
238
373
  if (changedInClaim.length === 0) {
374
+ await cleanupManagedWorktree();
239
375
  output(io, args, {
240
376
  ok: true,
241
377
  skipped: true,
@@ -246,7 +382,7 @@ export async function runSliceCommitCommand(projectRoot, tokens, io) {
246
382
  }
247
383
  try {
248
384
  await execFileAsync("git", ["add", "--", ...claimedPaths], {
249
- cwd: projectRoot
385
+ cwd: activeCwd
250
386
  });
251
387
  const taskPart = args.taskId && args.taskId.length > 0 ? args.taskId : "task";
252
388
  const titlePart = args.title && args.title.length > 0 ? args.title : "slice update";
@@ -257,12 +393,13 @@ export async function runSliceCommitCommand(projectRoot, tokens, io) {
257
393
  "phase-cycle: red->green->refactor->doc"
258
394
  ].join("\n");
259
395
  await execFileAsync("git", ["commit", "-m", header, "-m", body], {
260
- cwd: projectRoot
396
+ cwd: activeCwd
261
397
  });
262
398
  }
263
399
  catch (err) {
264
400
  const message = err instanceof Error ? err.message : String(err);
265
401
  if (/nothing to commit/iu.test(message)) {
402
+ await cleanupManagedWorktree();
266
403
  output(io, args, {
267
404
  ok: true,
268
405
  skipped: true,
@@ -280,9 +417,39 @@ export async function runSliceCommitCommand(projectRoot, tokens, io) {
280
417
  return 1;
281
418
  }
282
419
  const { stdout: shaStdout } = await execFileAsync("git", ["rev-parse", "HEAD"], {
283
- cwd: projectRoot
420
+ cwd: activeCwd
284
421
  });
285
- const commitSha = shaStdout.trim();
422
+ let commitSha = shaStdout.trim();
423
+ if (managedWorktreePath && activeCwd !== projectRoot) {
424
+ try {
425
+ const merged = await commitAndMergeBack(activeCwd, `merge ${args.sliceId}`, { projectRoot });
426
+ commitSha = merged.commitSha;
427
+ }
428
+ catch (error) {
429
+ if (error instanceof WorktreeMergeConflictError) {
430
+ output(io, args, {
431
+ ok: false,
432
+ errorCode: "worktree_merge_conflict",
433
+ details: {
434
+ sliceId: args.sliceId,
435
+ spanId: args.spanId,
436
+ worktreePath: activeCwd,
437
+ message: error.message
438
+ },
439
+ message: error.message
440
+ }, "stderr");
441
+ return 2;
442
+ }
443
+ output(io, args, {
444
+ ok: false,
445
+ errorCode: "slice_commit_failed",
446
+ details: { message: error instanceof Error ? error.message : String(error) },
447
+ message: `slice_commit_failed: ${error instanceof Error ? error.message : String(error)}`
448
+ }, "stderr");
449
+ return 1;
450
+ }
451
+ }
452
+ await cleanupManagedWorktree();
286
453
  output(io, args, {
287
454
  ok: true,
288
455
  commitSha,
@@ -290,6 +457,8 @@ export async function runSliceCommitCommand(projectRoot, tokens, io) {
290
457
  spanId: args.spanId,
291
458
  claimedPaths,
292
459
  changedPaths: changedInClaim,
460
+ worktreePath: managedWorktreePath ?? undefined,
461
+ degradedToInPlace: degradedToInPlace || undefined,
293
462
  message: `slice commit created for ${args.sliceId}: ${commitSha}`
294
463
  });
295
464
  return 0;
@@ -40,6 +40,7 @@ var REQUIRED_DIRS = [
40
40
  `${RUNTIME_ROOT}/artifacts`,
41
41
  `${RUNTIME_ROOT}/wave-plans`,
42
42
  `${RUNTIME_ROOT}/archive`,
43
+ `${RUNTIME_ROOT}/worktrees`,
43
44
  `${RUNTIME_ROOT}/state`,
44
45
  `${RUNTIME_ROOT}/rules`,
45
46
  `${RUNTIME_ROOT}/agents`,
@@ -0,0 +1,22 @@
1
+ export interface StackReviewRouteProfile {
2
+ stack: string;
3
+ /**
4
+ * Signals shown in review routing documentation/skills.
5
+ * These are human-facing pointers, not strict parsers.
6
+ */
7
+ reviewSignals: string[];
8
+ /** Root-level markers used by start-flow context discovery. */
9
+ discoveryMarkers: string[];
10
+ focus: string;
11
+ }
12
+ export declare const STACK_REVIEW_ROUTE_PROFILES: readonly StackReviewRouteProfile[];
13
+ /**
14
+ * Unified root-marker list used by start-flow context discovery.
15
+ * Keep this in one place so stage skill routing and start-flow scanning
16
+ * evolve together.
17
+ */
18
+ export declare const STACK_DISCOVERY_MARKERS: readonly string[];
19
+ /**
20
+ * Directory markers (checked with pathExists) for stack discovery.
21
+ */
22
+ export declare const STACK_DISCOVERY_DIR_MARKERS: readonly string[];
@@ -0,0 +1,58 @@
1
+ export const STACK_REVIEW_ROUTE_PROFILES = [
2
+ {
3
+ stack: "TypeScript/JavaScript",
4
+ reviewSignals: ["package.json", "tsconfig.json"],
5
+ discoveryMarkers: ["package.json", "tsconfig.json", "jsconfig.json"],
6
+ focus: "type safety, package scripts, build/test config, dependency boundaries"
7
+ },
8
+ {
9
+ stack: "Python",
10
+ reviewSignals: ["pyproject.toml", "requirements.txt"],
11
+ discoveryMarkers: ["pyproject.toml", "requirements.txt", "requirements-dev.txt", ".python-version"],
12
+ focus: "packaging, virtualenv assumptions, typing, pytest or unittest evidence"
13
+ },
14
+ {
15
+ stack: "Ruby/Rails",
16
+ reviewSignals: ["Gemfile", "config/"],
17
+ discoveryMarkers: ["Gemfile"],
18
+ focus: "Rails conventions, migrations, routes/controllers, RSpec or Minitest evidence"
19
+ },
20
+ {
21
+ stack: "Go",
22
+ reviewSignals: ["go.mod"],
23
+ discoveryMarkers: ["go.mod"],
24
+ focus: "interfaces, concurrency, error handling, go test coverage"
25
+ },
26
+ {
27
+ stack: "Rust",
28
+ reviewSignals: ["Cargo.toml"],
29
+ discoveryMarkers: ["Cargo.toml"],
30
+ focus: "ownership, error/result handling, feature flags, cargo test coverage"
31
+ }
32
+ ];
33
+ const EXTRA_DISCOVERY_MARKERS = [
34
+ "pom.xml",
35
+ "build.gradle",
36
+ "build.gradle.kts",
37
+ "Dockerfile",
38
+ "docker-compose.yml",
39
+ "docker-compose.yaml",
40
+ ".gitlab-ci.yml"
41
+ ];
42
+ /**
43
+ * Unified root-marker list used by start-flow context discovery.
44
+ * Keep this in one place so stage skill routing and start-flow scanning
45
+ * evolve together.
46
+ */
47
+ export const STACK_DISCOVERY_MARKERS = [
48
+ ...new Set([
49
+ ...STACK_REVIEW_ROUTE_PROFILES.flatMap((profile) => profile.discoveryMarkers),
50
+ ...EXTRA_DISCOVERY_MARKERS
51
+ ])
52
+ ];
53
+ /**
54
+ * Directory markers (checked with pathExists) for stack discovery.
55
+ */
56
+ export const STACK_DISCOVERY_DIR_MARKERS = [
57
+ ".github/workflows"
58
+ ];
package/dist/types.d.ts CHANGED
@@ -162,6 +162,7 @@ export interface ReviewLoopConfig {
162
162
  }
163
163
  export type VcsMode = "git-with-remote" | "git-local-only" | "none";
164
164
  export type TddCommitMode = "managed-per-slice" | "agent-required" | "checkpoint-only" | "off";
165
+ export type TddIsolationMode = "worktree" | "in-place" | "auto";
165
166
  export interface TddConfig {
166
167
  /**
167
168
  * Commit ownership model for closed TDD slices.
@@ -171,6 +172,17 @@ export interface TddConfig {
171
172
  * - off: skip commit-shape enforcement.
172
173
  */
173
174
  commitMode?: TddCommitMode;
175
+ /**
176
+ * Slice execution isolation model.
177
+ * - worktree: default; allocate one git worktree per slice span.
178
+ * - in-place: run in the main working tree.
179
+ * - auto: prefer worktree, degrade to in-place when unavailable.
180
+ */
181
+ isolationMode?: TddIsolationMode;
182
+ /**
183
+ * Repo-relative root used for managed slice worktrees.
184
+ */
185
+ worktreeRoot?: string;
174
186
  }
175
187
  export interface CclawConfig {
176
188
  version: string;
@@ -0,0 +1,20 @@
1
+ export interface WorktreeManagerOptions {
2
+ projectRoot?: string;
3
+ worktreeRoot?: string;
4
+ }
5
+ export declare class WorktreeUnsupportedError extends Error {
6
+ readonly code = "worktree_unavailable";
7
+ constructor(message: string);
8
+ }
9
+ export declare class WorktreeMergeConflictError extends Error {
10
+ readonly code = "worktree_merge_conflict";
11
+ constructor(message: string);
12
+ }
13
+ export declare function createSliceWorktree(sliceId: string, baseRef: string, _claimedPaths: string[], options?: WorktreeManagerOptions): Promise<{
14
+ path: string;
15
+ ref: string;
16
+ }>;
17
+ export declare function commitAndMergeBack(worktreePath: string, _message: string, options?: WorktreeManagerOptions): Promise<{
18
+ commitSha: string;
19
+ }>;
20
+ export declare function cleanupWorktree(worktreePath: string, options?: WorktreeManagerOptions): Promise<void>;
@@ -0,0 +1,108 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { DEFAULT_TDD_WORKTREE_ROOT } from "./config.js";
6
+ import { exists } from "./fs-utils.js";
7
+ const execFileAsync = promisify(execFile);
8
+ export class WorktreeUnsupportedError extends Error {
9
+ code = "worktree_unavailable";
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "WorktreeUnsupportedError";
13
+ }
14
+ }
15
+ export class WorktreeMergeConflictError extends Error {
16
+ code = "worktree_merge_conflict";
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "WorktreeMergeConflictError";
20
+ }
21
+ }
22
+ function sanitizeSliceId(sliceId) {
23
+ return sliceId.trim().replace(/[^A-Za-z0-9._-]+/gu, "-");
24
+ }
25
+ function resolveProjectRoot(options) {
26
+ return options?.projectRoot ?? process.cwd();
27
+ }
28
+ function resolveWorktreeRoot(projectRoot, options) {
29
+ const root = typeof options?.worktreeRoot === "string" && options.worktreeRoot.trim().length > 0
30
+ ? options.worktreeRoot.trim()
31
+ : DEFAULT_TDD_WORKTREE_ROOT;
32
+ return path.resolve(projectRoot, root);
33
+ }
34
+ async function resolveMainRepoRootFromWorktree(worktreePath) {
35
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: worktreePath });
36
+ const commonDir = stdout.trim();
37
+ if (commonDir.length === 0) {
38
+ throw new Error(`Cannot resolve git common-dir from worktree ${worktreePath}`);
39
+ }
40
+ return path.dirname(commonDir);
41
+ }
42
+ export async function createSliceWorktree(sliceId, baseRef, _claimedPaths, options = {}) {
43
+ const projectRoot = resolveProjectRoot(options);
44
+ const ref = baseRef.trim().length > 0 ? baseRef.trim() : "HEAD";
45
+ const worktreeRoot = resolveWorktreeRoot(projectRoot, options);
46
+ const safeSliceId = sanitizeSliceId(sliceId);
47
+ if (safeSliceId.length === 0) {
48
+ throw new WorktreeUnsupportedError("Cannot create worktree: empty slice id.");
49
+ }
50
+ const worktreePath = path.join(worktreeRoot, safeSliceId);
51
+ try {
52
+ await execFileAsync("git", ["rev-parse", "--git-dir"], { cwd: projectRoot });
53
+ }
54
+ catch {
55
+ throw new WorktreeUnsupportedError("Cannot create worktree: repository has no .git metadata.");
56
+ }
57
+ await fs.mkdir(worktreeRoot, { recursive: true });
58
+ if (await exists(worktreePath)) {
59
+ if (await exists(path.join(worktreePath, ".git"))) {
60
+ return { path: worktreePath, ref };
61
+ }
62
+ await fs.rm(worktreePath, { recursive: true, force: true });
63
+ }
64
+ try {
65
+ await execFileAsync("git", ["worktree", "add", "--detach", worktreePath, ref], {
66
+ cwd: projectRoot
67
+ });
68
+ }
69
+ catch (error) {
70
+ throw new WorktreeUnsupportedError(`Cannot create worktree for ${sliceId}: ${error instanceof Error ? error.message : String(error)}`);
71
+ }
72
+ return { path: worktreePath, ref };
73
+ }
74
+ export async function commitAndMergeBack(worktreePath, _message, options = {}) {
75
+ const projectRoot = options.projectRoot ?? await resolveMainRepoRootFromWorktree(worktreePath);
76
+ try {
77
+ const { stdout: mainHeadStdout } = await execFileAsync("git", ["rev-parse", "HEAD"], {
78
+ cwd: projectRoot
79
+ });
80
+ const mainHead = mainHeadStdout.trim();
81
+ await execFileAsync("git", ["rebase", mainHead], { cwd: worktreePath });
82
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: worktreePath });
83
+ const commitSha = stdout.trim();
84
+ await execFileAsync("git", ["fetch", worktreePath, "HEAD"], { cwd: projectRoot });
85
+ await execFileAsync("git", ["merge", "--ff-only", "FETCH_HEAD"], { cwd: projectRoot });
86
+ return { commitSha };
87
+ }
88
+ catch (error) {
89
+ throw new WorktreeMergeConflictError(`worktree_merge_conflict: ${error instanceof Error ? error.message : String(error)}`);
90
+ }
91
+ }
92
+ export async function cleanupWorktree(worktreePath, options = {}) {
93
+ if (!(await exists(worktreePath)))
94
+ return;
95
+ const projectRoot = options.projectRoot ?? await resolveMainRepoRootFromWorktree(worktreePath).catch(() => null);
96
+ if (projectRoot) {
97
+ try {
98
+ await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], {
99
+ cwd: projectRoot
100
+ });
101
+ return;
102
+ }
103
+ catch {
104
+ // fall through to rm fallback below
105
+ }
106
+ }
107
+ await fs.rm(worktreePath, { recursive: true, force: true });
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "7.1.1",
3
+ "version": "7.3.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {