cclaw-cli 6.12.0 → 6.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-linter/plan.js +60 -2
- package/dist/artifact-linter/shared.d.ts +9 -0
- package/dist/artifact-linter/spec.js +14 -0
- package/dist/artifact-linter/tdd.js +97 -3
- package/dist/artifact-linter.js +10 -1
- package/dist/content/hooks.js +48 -1
- package/dist/content/skills.js +5 -1
- package/dist/content/stages/plan.js +2 -1
- package/dist/content/stages/spec.js +2 -2
- package/dist/content/stages/tdd.js +2 -1
- package/dist/content/templates.js +10 -4
- package/dist/delegation.d.ts +73 -3
- package/dist/delegation.js +196 -6
- package/dist/flow-state.d.ts +20 -0
- package/dist/flow-state.js +7 -0
- package/dist/gate-evidence.d.ts +5 -0
- package/dist/gate-evidence.js +58 -1
- package/dist/install.js +64 -1
- package/dist/integration-fanin.d.ts +44 -0
- package/dist/integration-fanin.js +180 -0
- package/dist/internal/advance-stage/advance.js +16 -1
- package/dist/internal/advance-stage/start-flow.js +3 -1
- package/dist/internal/advance-stage.js +13 -4
- package/dist/internal/plan-split-waves.d.ts +39 -1
- package/dist/internal/plan-split-waves.js +190 -6
- package/dist/internal/set-worktree-mode.d.ts +10 -0
- package/dist/internal/set-worktree-mode.js +28 -0
- package/dist/managed-resources.js +2 -0
- package/dist/run-persistence.js +9 -0
- package/dist/worktree-manager.d.ts +50 -0
- package/dist/worktree-manager.js +136 -0
- package/dist/worktree-types.d.ts +36 -0
- package/dist/worktree-types.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { GitBaseRef, WorktreeLaneId } from "./worktree-types.js";
|
|
2
|
+
export interface CreateLaneOptions {
|
|
3
|
+
/** Repository root that owns `.cclaw/`. */
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
/** TDD slice id (e.g. `S-7`). */
|
|
6
|
+
sliceId: string;
|
|
7
|
+
/** Git ref to create the worktree from (e.g. `HEAD`, branch name). */
|
|
8
|
+
baseRef: GitBaseRef;
|
|
9
|
+
}
|
|
10
|
+
export interface CreateLaneResult {
|
|
11
|
+
laneId: WorktreeLaneId;
|
|
12
|
+
workdir: string;
|
|
13
|
+
branchName: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a dedicated git worktree for a slice under `.cclaw/worktrees/`.
|
|
17
|
+
* Uses branch namespace `cclaw/lane/<sliceId>-<suffix>`. Does not commit.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createLane(options: CreateLaneOptions): Promise<CreateLaneResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Assert the lane worktree exists, has a clean working tree, and matches
|
|
22
|
+
* the expected baseline ref (merge-base check with `baseRef`).
|
|
23
|
+
*/
|
|
24
|
+
export declare function verifyLaneClean(projectRoot: string, laneId: WorktreeLaneId, baseRef: GitBaseRef): Promise<{
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
reason: string;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Prepare the lane for interactive work (no-op placeholder for harness parity).
|
|
32
|
+
*/
|
|
33
|
+
export declare function attachLane(_laneId: WorktreeLaneId): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Release local harness attachment (no-op).
|
|
36
|
+
*/
|
|
37
|
+
export declare function detachLane(_laneId: WorktreeLaneId): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Remove the worktree directory and prune git metadata.
|
|
40
|
+
*/
|
|
41
|
+
export declare function cleanupLane(projectRoot: string, laneId: WorktreeLaneId, options?: {
|
|
42
|
+
force?: boolean;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
export interface PruneStaleLanesOptions {
|
|
45
|
+
olderThanHours: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Remove lane worktrees older than the threshold based on directory mtime.
|
|
49
|
+
*/
|
|
50
|
+
export declare function pruneStaleLanes(projectRoot: string, options: PruneStaleLanesOptions): Promise<string[]>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { exists } from "./fs-utils.js";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const WORKTREES_SEG = ".cclaw/worktrees";
|
|
8
|
+
const LANE_BRANCH_PREFIX = "cclaw/lane/";
|
|
9
|
+
function sanitizeSliceId(sliceId) {
|
|
10
|
+
return sliceId.replace(/[^A-Za-z0-9_-]/gu, "");
|
|
11
|
+
}
|
|
12
|
+
function safeLaneSuffix() {
|
|
13
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a dedicated git worktree for a slice under `.cclaw/worktrees/`.
|
|
17
|
+
* Uses branch namespace `cclaw/lane/<sliceId>-<suffix>`. Does not commit.
|
|
18
|
+
*/
|
|
19
|
+
export async function createLane(options) {
|
|
20
|
+
const { projectRoot, sliceId, baseRef } = options;
|
|
21
|
+
const slug = sanitizeSliceId(sliceId);
|
|
22
|
+
const laneId = `lane-${slug}-${safeLaneSuffix()}`;
|
|
23
|
+
const worktreesRoot = path.join(projectRoot, WORKTREES_SEG);
|
|
24
|
+
await fs.mkdir(worktreesRoot, { recursive: true });
|
|
25
|
+
const workdir = path.join(worktreesRoot, laneId);
|
|
26
|
+
const branchName = `${LANE_BRANCH_PREFIX}${slug}-${safeLaneSuffix()}`;
|
|
27
|
+
await execFileAsync("git", ["worktree", "add", "-b", branchName, workdir, baseRef], { cwd: projectRoot });
|
|
28
|
+
return { laneId, workdir, branchName };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Assert the lane worktree exists, has a clean working tree, and matches
|
|
32
|
+
* the expected baseline ref (merge-base check with `baseRef`).
|
|
33
|
+
*/
|
|
34
|
+
export async function verifyLaneClean(projectRoot, laneId, baseRef) {
|
|
35
|
+
const workdir = path.join(projectRoot, WORKTREES_SEG, laneId);
|
|
36
|
+
if (!(await exists(workdir))) {
|
|
37
|
+
return { ok: false, reason: `lane workdir missing: ${workdir}` };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const { stdout: status } = await execFileAsync("git", ["status", "--porcelain"], { cwd: workdir });
|
|
41
|
+
if (status.trim().length > 0) {
|
|
42
|
+
return { ok: false, reason: "lane working tree is dirty" };
|
|
43
|
+
}
|
|
44
|
+
await execFileAsync("git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], { cwd: workdir });
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return { ok: true };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Prepare the lane for interactive work (no-op placeholder for harness parity).
|
|
56
|
+
*/
|
|
57
|
+
export async function attachLane(_laneId) {
|
|
58
|
+
// Intentionally minimal: callers use workdir from createLane.
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Release local harness attachment (no-op).
|
|
62
|
+
*/
|
|
63
|
+
export async function detachLane(_laneId) { }
|
|
64
|
+
/**
|
|
65
|
+
* Remove the worktree directory and prune git metadata.
|
|
66
|
+
*/
|
|
67
|
+
export async function cleanupLane(projectRoot, laneId, options = {}) {
|
|
68
|
+
const workdir = path.join(projectRoot, WORKTREES_SEG, laneId);
|
|
69
|
+
if (!(await exists(workdir)))
|
|
70
|
+
return;
|
|
71
|
+
const insideSubmodules = await hasSubmoduleAtPath(projectRoot, workdir);
|
|
72
|
+
if (insideSubmodules && !options.force) {
|
|
73
|
+
throw new Error("cleanupLane: path appears inside a git submodule; pass { force: true } after manual review.");
|
|
74
|
+
}
|
|
75
|
+
await execFileAsync("git", ["worktree", "remove", "--force", workdir], {
|
|
76
|
+
cwd: projectRoot
|
|
77
|
+
}).catch(() => execFileAsync("git", ["worktree", "remove", workdir], { cwd: projectRoot }));
|
|
78
|
+
await fs.rm(workdir, { recursive: true, force: true });
|
|
79
|
+
await execFileAsync("git", ["worktree", "prune"], { cwd: projectRoot }).catch(() => undefined);
|
|
80
|
+
}
|
|
81
|
+
async function hasSubmoduleAtPath(projectRoot, targetPath) {
|
|
82
|
+
try {
|
|
83
|
+
const { stdout } = await execFileAsync("git", ["submodule", "status"], { cwd: projectRoot });
|
|
84
|
+
if (!stdout.trim())
|
|
85
|
+
return false;
|
|
86
|
+
// Conservative: if targetPath is under any registered submodule worktree, refuse.
|
|
87
|
+
const absTarget = path.resolve(targetPath);
|
|
88
|
+
const entries = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
89
|
+
for (const line of entries) {
|
|
90
|
+
const parts = line.split(/\s+/u);
|
|
91
|
+
const subPath = parts[1];
|
|
92
|
+
if (!subPath)
|
|
93
|
+
continue;
|
|
94
|
+
const absSub = path.resolve(projectRoot, subPath);
|
|
95
|
+
if (absTarget === absSub || absTarget.startsWith(`${absSub}${path.sep}`)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Remove lane worktrees older than the threshold based on directory mtime.
|
|
107
|
+
*/
|
|
108
|
+
export async function pruneStaleLanes(projectRoot, options) {
|
|
109
|
+
const worktreesRoot = path.join(projectRoot, WORKTREES_SEG);
|
|
110
|
+
if (!(await exists(worktreesRoot)))
|
|
111
|
+
return [];
|
|
112
|
+
const cutoff = Date.now() - options.olderThanHours * 3600 * 1000;
|
|
113
|
+
const removed = [];
|
|
114
|
+
let dirents = [];
|
|
115
|
+
try {
|
|
116
|
+
dirents = await fs.readdir(worktreesRoot, { withFileTypes: true });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return removed;
|
|
120
|
+
}
|
|
121
|
+
for (const ent of dirents) {
|
|
122
|
+
if (!ent.isDirectory())
|
|
123
|
+
continue;
|
|
124
|
+
if (!ent.name.startsWith("lane-"))
|
|
125
|
+
continue;
|
|
126
|
+
const abs = path.join(worktreesRoot, ent.name);
|
|
127
|
+
const st = await fs.stat(abs).catch(() => null);
|
|
128
|
+
if (!st)
|
|
129
|
+
continue;
|
|
130
|
+
if (st.mtimeMs < cutoff) {
|
|
131
|
+
await cleanupLane(projectRoot, ent.name, { force: false });
|
|
132
|
+
removed.push(ent.name);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return removed;
|
|
136
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git worktree lane identifiers and refs used by the v6.13 worktree-first
|
|
3
|
+
* multi-slice executor. Lanes isolate slice-implementer working trees from
|
|
4
|
+
* the integration checkout.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Stable lane id (e.g. `lane-S-3-a1b2c3`) under `.cclaw/worktrees/<laneId>/`.
|
|
8
|
+
*/
|
|
9
|
+
export type WorktreeLaneId = string;
|
|
10
|
+
/**
|
|
11
|
+
* Git ref (branch name, tag, or commit sha) used as the lane baseline.
|
|
12
|
+
*/
|
|
13
|
+
export type GitBaseRef = string;
|
|
14
|
+
/**
|
|
15
|
+
* Git ref describing the lane HEAD after local commits (often a branch tip).
|
|
16
|
+
*/
|
|
17
|
+
export type GitHeadRef = string;
|
|
18
|
+
/**
|
|
19
|
+
* Absolute or repo-root-relative working directory for a `git/worktree`.
|
|
20
|
+
*/
|
|
21
|
+
export type WorktreeWorkdir = string;
|
|
22
|
+
/**
|
|
23
|
+
* Metadata for one active lane tied to a TDD slice.
|
|
24
|
+
*/
|
|
25
|
+
export interface WorktreeLaneInfo {
|
|
26
|
+
/** Lane id; directory basename under `.cclaw/worktrees/`. */
|
|
27
|
+
laneId: WorktreeLaneId;
|
|
28
|
+
/** Slice this lane is bound to (e.g. `S-12`). */
|
|
29
|
+
sliceId: string;
|
|
30
|
+
/** Integration baseline the lane was created from. */
|
|
31
|
+
baseRef: GitBaseRef;
|
|
32
|
+
/** Optional named branch inside the lane worktree. */
|
|
33
|
+
branchName?: string;
|
|
34
|
+
/** Resolved filesystem path to the worktree root. */
|
|
35
|
+
workdir: WorktreeWorkdir;
|
|
36
|
+
}
|