cclaw-cli 0.15.0 → 0.18.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.
Files changed (51) hide show
  1. package/dist/artifact-linter.js +154 -0
  2. package/dist/cli.js +2 -1
  3. package/dist/constants.d.ts +2 -2
  4. package/dist/constants.js +2 -3
  5. package/dist/content/contracts.js +1 -1
  6. package/dist/content/doctor-references.js +7 -6
  7. package/dist/content/feature-command.js +54 -51
  8. package/dist/content/harnesses-doc.js +2 -2
  9. package/dist/content/hooks.js +2 -2
  10. package/dist/content/learnings.d.ts +1 -1
  11. package/dist/content/learnings.js +22 -5
  12. package/dist/content/meta-skill.js +2 -2
  13. package/dist/content/next-command.js +2 -2
  14. package/dist/content/observe.js +3 -2
  15. package/dist/content/ops-command.js +1 -3
  16. package/dist/content/protocols.js +6 -34
  17. package/dist/content/rewind-command.d.ts +0 -1
  18. package/dist/content/rewind-command.js +19 -33
  19. package/dist/content/skills.js +2 -3
  20. package/dist/content/stage-schema.d.ts +2 -92
  21. package/dist/content/stage-schema.js +10 -1379
  22. package/dist/content/stages/brainstorm.d.ts +2 -0
  23. package/dist/content/stages/brainstorm.js +136 -0
  24. package/dist/content/stages/design.d.ts +2 -0
  25. package/dist/content/stages/design.js +215 -0
  26. package/dist/content/stages/index.d.ts +8 -0
  27. package/dist/content/stages/index.js +11 -0
  28. package/dist/content/stages/plan.d.ts +2 -0
  29. package/dist/content/stages/plan.js +157 -0
  30. package/dist/content/stages/review.d.ts +2 -0
  31. package/dist/content/stages/review.js +197 -0
  32. package/dist/content/stages/schema-types.d.ts +94 -0
  33. package/dist/content/stages/schema-types.js +1 -0
  34. package/dist/content/stages/scope.d.ts +2 -0
  35. package/dist/content/stages/scope.js +194 -0
  36. package/dist/content/stages/ship.d.ts +2 -0
  37. package/dist/content/stages/ship.js +142 -0
  38. package/dist/content/stages/spec.d.ts +2 -0
  39. package/dist/content/stages/spec.js +136 -0
  40. package/dist/content/stages/tdd.d.ts +2 -0
  41. package/dist/content/stages/tdd.js +185 -0
  42. package/dist/content/templates.js +105 -9
  43. package/dist/content/utility-skills.js +1 -1
  44. package/dist/delegation.d.ts +33 -3
  45. package/dist/delegation.js +56 -3
  46. package/dist/doctor.js +147 -88
  47. package/dist/feature-system.d.ts +22 -5
  48. package/dist/feature-system.js +267 -126
  49. package/dist/install.js +4 -8
  50. package/dist/policy.js +3 -4
  51. package/package.json +1 -1
@@ -2,17 +2,34 @@ export interface ActiveFeatureMeta {
2
2
  activeFeature: string;
3
3
  updatedAt: string;
4
4
  }
5
+ export type FeatureWorkspaceSource = "git-worktree" | "workspace" | "legacy-snapshot";
6
+ export interface FeatureWorkspaceEntry {
7
+ featureId: string;
8
+ branch: string;
9
+ path: string;
10
+ source: FeatureWorkspaceSource;
11
+ createdAt: string;
12
+ }
13
+ export interface FeatureWorktreeRegistry {
14
+ schemaVersion: 1;
15
+ updatedAt: string;
16
+ entries: FeatureWorkspaceEntry[];
17
+ }
18
+ export interface CreateFeatureOptions {
19
+ cloneActive?: boolean;
20
+ switchTo?: boolean;
21
+ }
5
22
  export declare function activeFeatureMetaPath(projectRoot: string): string;
23
+ export declare function worktreeRegistryPath(projectRoot: string): string;
6
24
  export declare function featureRootPath(projectRoot: string, featureId: string): string;
7
25
  export declare function featureArtifactsPath(projectRoot: string, featureId: string): string;
8
26
  export declare function featureStatePath(projectRoot: string, featureId: string): string;
27
+ export declare function resolveFeatureWorkspacePath(projectRoot: string, entry: FeatureWorkspaceEntry): string;
28
+ export declare function ensureFeatureSystem(projectRoot: string): Promise<ActiveFeatureMeta>;
29
+ export declare function readFeatureWorktreeRegistry(projectRoot: string): Promise<FeatureWorktreeRegistry>;
9
30
  export declare function readActiveFeature(projectRoot: string): Promise<string>;
10
31
  export declare function listFeatures(projectRoot: string): Promise<string[]>;
11
- export declare function ensureFeatureSystem(projectRoot: string): Promise<ActiveFeatureMeta>;
12
32
  export declare function syncActiveFeatureSnapshot(projectRoot: string): Promise<void>;
13
33
  export declare function switchActiveFeature(projectRoot: string, featureId: string): Promise<ActiveFeatureMeta>;
14
- export interface CreateFeatureOptions {
15
- cloneActive?: boolean;
16
- switchTo?: boolean;
17
- }
18
34
  export declare function createFeature(projectRoot: string, rawFeatureId: string, options?: CreateFeatureOptions): Promise<string>;
35
+ export declare function activeFeatureWorkspacePath(projectRoot: string): Promise<string>;
@@ -1,36 +1,43 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
3
5
  import { RUNTIME_ROOT } from "./constants.js";
4
6
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
5
- const FEATURES_DIR_REL_PATH = `${RUNTIME_ROOT}/features`;
7
+ const execFileAsync = promisify(execFile);
8
+ const WORKTREES_DIR_REL_PATH = `${RUNTIME_ROOT}/worktrees`;
9
+ const LEGACY_FEATURES_DIR_REL_PATH = `${RUNTIME_ROOT}/features`;
6
10
  const ACTIVE_FEATURE_META_REL_PATH = `${RUNTIME_ROOT}/state/active-feature.json`;
11
+ const WORKTREE_REGISTRY_REL_PATH = `${RUNTIME_ROOT}/state/worktrees.json`;
7
12
  const DEFAULT_FEATURE_ID = "default";
13
+ const WORKTREE_REGISTRY_SCHEMA_VERSION = 1;
8
14
  const FEATURE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
9
- const FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT = new Set([
10
- "active-feature.json",
11
- ".flow-state.lock",
12
- ".delegation.lock"
13
- ]);
14
- function featuresRoot(projectRoot) {
15
- return path.join(projectRoot, FEATURES_DIR_REL_PATH);
15
+ function worktreesRoot(projectRoot) {
16
+ return path.join(projectRoot, WORKTREES_DIR_REL_PATH);
16
17
  }
17
- function runtimeArtifactsRoot(projectRoot) {
18
- return path.join(projectRoot, RUNTIME_ROOT, "artifacts");
19
- }
20
- function runtimeStateRoot(projectRoot) {
21
- return path.join(projectRoot, RUNTIME_ROOT, "state");
18
+ function legacyFeaturesRoot(projectRoot) {
19
+ return path.join(projectRoot, LEGACY_FEATURES_DIR_REL_PATH);
22
20
  }
23
21
  export function activeFeatureMetaPath(projectRoot) {
24
22
  return path.join(projectRoot, ACTIVE_FEATURE_META_REL_PATH);
25
23
  }
24
+ export function worktreeRegistryPath(projectRoot) {
25
+ return path.join(projectRoot, WORKTREE_REGISTRY_REL_PATH);
26
+ }
26
27
  export function featureRootPath(projectRoot, featureId) {
27
- return path.join(featuresRoot(projectRoot), featureId);
28
+ return path.join(worktreesRoot(projectRoot), normalizedFeatureId(featureId));
28
29
  }
29
30
  export function featureArtifactsPath(projectRoot, featureId) {
30
- return path.join(featureRootPath(projectRoot, featureId), "artifacts");
31
+ return path.join(featureRootPath(projectRoot, featureId), RUNTIME_ROOT, "artifacts");
31
32
  }
32
33
  export function featureStatePath(projectRoot, featureId) {
33
- return path.join(featureRootPath(projectRoot, featureId), "state");
34
+ return path.join(featureRootPath(projectRoot, featureId), RUNTIME_ROOT, "state");
35
+ }
36
+ export function resolveFeatureWorkspacePath(projectRoot, entry) {
37
+ if (entry.path === ".") {
38
+ return projectRoot;
39
+ }
40
+ return path.resolve(projectRoot, entry.path);
34
41
  }
35
42
  function normalizedFeatureId(value) {
36
43
  const candidate = value
@@ -45,64 +52,134 @@ function normalizedFeatureId(value) {
45
52
  const clipped = candidate.slice(0, 64);
46
53
  return FEATURE_ID_PATTERN.test(clipped) ? clipped : DEFAULT_FEATURE_ID;
47
54
  }
48
- async function clearDirectory(dirPath, preserveTargetEntries = new Set()) {
49
- await ensureDir(dirPath);
50
- let entries;
51
- try {
52
- entries = await fs.readdir(dirPath, { withFileTypes: true });
55
+ function toRelativePath(projectRoot, absolutePath) {
56
+ const rel = path.relative(projectRoot, absolutePath);
57
+ if (!rel || rel.trim().length === 0) {
58
+ return ".";
53
59
  }
54
- catch {
55
- return;
60
+ return rel.split(path.sep).join("/");
61
+ }
62
+ function sanitizeWorkspaceSource(value) {
63
+ if (value === "git-worktree" || value === "workspace" || value === "legacy-snapshot") {
64
+ return value;
65
+ }
66
+ return "workspace";
67
+ }
68
+ function sanitizeRegistryEntry(raw) {
69
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
70
+ return null;
56
71
  }
72
+ const typed = raw;
73
+ const featureIdRaw = typeof typed.featureId === "string" ? typed.featureId : "";
74
+ const featureId = normalizedFeatureId(featureIdRaw);
75
+ if (!FEATURE_ID_PATTERN.test(featureId)) {
76
+ return null;
77
+ }
78
+ const branch = typeof typed.branch === "string" && typed.branch.trim().length > 0
79
+ ? typed.branch.trim()
80
+ : (featureId === DEFAULT_FEATURE_ID ? "workspace/default" : `workspace/${featureId}`);
81
+ const pathRaw = typeof typed.path === "string" ? typed.path.trim() : "";
82
+ const workspacePath = pathRaw.length > 0 ? pathRaw : ".";
83
+ const createdAt = typeof typed.createdAt === "string" && typed.createdAt.trim().length > 0
84
+ ? typed.createdAt.trim()
85
+ : new Date().toISOString();
86
+ return {
87
+ featureId,
88
+ branch,
89
+ path: workspacePath,
90
+ source: sanitizeWorkspaceSource(typed.source),
91
+ createdAt
92
+ };
93
+ }
94
+ function dedupeEntries(entries) {
95
+ const byId = new Map();
57
96
  for (const entry of entries) {
58
- if (preserveTargetEntries.has(entry.name)) {
59
- continue;
97
+ if (!byId.has(entry.featureId)) {
98
+ byId.set(entry.featureId, entry);
60
99
  }
61
- await fs.rm(path.join(dirPath, entry.name), { recursive: true, force: true });
62
100
  }
101
+ return [...byId.values()].sort((a, b) => a.featureId.localeCompare(b.featureId));
63
102
  }
64
- async function copyDirectoryContents(sourceDir, targetDir, options = {}) {
65
- const exclude = options.exclude ?? new Set();
66
- const preserveTargetEntries = options.preserveTargetEntries ?? new Set();
67
- await ensureDir(targetDir);
68
- await clearDirectory(targetDir, preserveTargetEntries);
69
- if (!(await exists(sourceDir))) {
70
- return;
71
- }
72
- let entries;
103
+ async function runGit(projectRoot, args) {
73
104
  try {
74
- entries = await fs.readdir(sourceDir, { withFileTypes: true });
105
+ const { stdout, stderr } = await execFileAsync("git", args, { cwd: projectRoot });
106
+ return { ok: true, stdout: stdout.trim(), stderr: stderr.trim() };
75
107
  }
76
- catch {
77
- return;
108
+ catch (error) {
109
+ const err = error;
110
+ return {
111
+ ok: false,
112
+ stdout: typeof err.stdout === "string" ? err.stdout.trim() : "",
113
+ stderr: typeof err.stderr === "string" && err.stderr.trim().length > 0
114
+ ? err.stderr.trim()
115
+ : (err.message ?? "git command failed")
116
+ };
78
117
  }
79
- for (const entry of entries) {
80
- if (exclude.has(entry.name)) {
81
- continue;
82
- }
83
- const from = path.join(sourceDir, entry.name);
84
- const to = path.join(targetDir, entry.name);
85
- if (entry.isDirectory()) {
86
- await fs.cp(from, to, { recursive: true, force: true });
87
- continue;
88
- }
89
- if (entry.isFile()) {
90
- await fs.copyFile(from, to);
91
- }
118
+ }
119
+ async function isGitRepository(projectRoot) {
120
+ const result = await runGit(projectRoot, ["rev-parse", "--is-inside-work-tree"]);
121
+ return result.ok && result.stdout === "true";
122
+ }
123
+ async function currentBranch(projectRoot) {
124
+ const result = await runGit(projectRoot, ["rev-parse", "--abbrev-ref", "HEAD"]);
125
+ return result.ok && result.stdout.length > 0 ? result.stdout : "HEAD";
126
+ }
127
+ async function defaultStartPoint(projectRoot) {
128
+ const remoteHead = await runGit(projectRoot, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
129
+ if (remoteHead.ok && remoteHead.stdout.length > 0) {
130
+ return remoteHead.stdout.replace(/^origin\//u, "");
92
131
  }
132
+ return currentBranch(projectRoot);
133
+ }
134
+ function buildDefaultEntry(source, branch) {
135
+ return {
136
+ featureId: DEFAULT_FEATURE_ID,
137
+ branch,
138
+ path: ".",
139
+ source,
140
+ createdAt: new Date().toISOString()
141
+ };
93
142
  }
94
- async function dirHasEntries(dirPath, exclude = new Set()) {
95
- if (!(await exists(dirPath))) {
96
- return false;
143
+ async function readRegistry(projectRoot) {
144
+ const filePath = worktreeRegistryPath(projectRoot);
145
+ if (!(await exists(filePath))) {
146
+ return {
147
+ schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
148
+ updatedAt: new Date().toISOString(),
149
+ entries: []
150
+ };
97
151
  }
98
152
  try {
99
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
100
- return entries.some((entry) => !exclude.has(entry.name));
153
+ const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
154
+ const entriesRaw = Array.isArray(parsed.entries) ? parsed.entries : [];
155
+ const entries = dedupeEntries(entriesRaw
156
+ .map((entry) => sanitizeRegistryEntry(entry))
157
+ .filter((entry) => entry !== null));
158
+ const updatedAt = typeof parsed.updatedAt === "string" && parsed.updatedAt.trim().length > 0
159
+ ? parsed.updatedAt.trim()
160
+ : new Date().toISOString();
161
+ return {
162
+ schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
163
+ updatedAt,
164
+ entries
165
+ };
101
166
  }
102
167
  catch {
103
- return false;
168
+ return {
169
+ schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
170
+ updatedAt: new Date().toISOString(),
171
+ entries: []
172
+ };
104
173
  }
105
174
  }
175
+ async function writeRegistry(projectRoot, registry) {
176
+ const normalized = {
177
+ schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
178
+ updatedAt: registry.updatedAt,
179
+ entries: dedupeEntries(registry.entries)
180
+ };
181
+ await writeFileSafe(worktreeRegistryPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n`);
182
+ }
106
183
  async function readActiveFeatureMetaInternal(projectRoot) {
107
184
  const filePath = activeFeatureMetaPath(projectRoot);
108
185
  if (!(await exists(filePath))) {
@@ -138,84 +215,94 @@ async function writeActiveFeatureMeta(projectRoot, meta) {
138
215
  };
139
216
  await writeFileSafe(activeFeatureMetaPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n`);
140
217
  }
141
- async function ensureFeatureSnapshot(projectRoot, featureId) {
142
- const id = normalizedFeatureId(featureId);
143
- await ensureDir(featureArtifactsPath(projectRoot, id));
144
- await ensureDir(featureStatePath(projectRoot, id));
218
+ function registryHasFeature(registry, featureId) {
219
+ return registry.entries.some((entry) => entry.featureId === featureId);
145
220
  }
146
- export async function readActiveFeature(projectRoot) {
147
- const meta = await readActiveFeatureMetaInternal(projectRoot);
148
- return normalizedFeatureId(meta.activeFeature);
221
+ function findEntry(registry, featureId) {
222
+ return registry.entries.find((entry) => entry.featureId === featureId);
149
223
  }
150
- export async function listFeatures(projectRoot) {
151
- const root = featuresRoot(projectRoot);
224
+ async function listLegacySnapshotIds(projectRoot) {
225
+ const root = legacyFeaturesRoot(projectRoot);
152
226
  if (!(await exists(root))) {
153
227
  return [];
154
228
  }
155
- let entries;
156
229
  try {
157
- entries = await fs.readdir(root, { withFileTypes: true });
230
+ const entries = await fs.readdir(root, { withFileTypes: true });
231
+ return entries
232
+ .filter((entry) => entry.isDirectory() && FEATURE_ID_PATTERN.test(entry.name))
233
+ .map((entry) => entry.name)
234
+ .sort((a, b) => a.localeCompare(b));
158
235
  }
159
236
  catch {
160
237
  return [];
161
238
  }
162
- return entries
163
- .filter((entry) => entry.isDirectory() && FEATURE_ID_PATTERN.test(entry.name))
164
- .map((entry) => entry.name)
165
- .sort((a, b) => a.localeCompare(b));
166
239
  }
167
- export async function ensureFeatureSystem(projectRoot) {
168
- await ensureDir(featuresRoot(projectRoot));
169
- await ensureDir(runtimeArtifactsRoot(projectRoot));
170
- await ensureDir(runtimeStateRoot(projectRoot));
171
- const existing = await readActiveFeatureMetaInternal(projectRoot);
172
- const activeFeature = normalizedFeatureId(existing.activeFeature);
173
- await ensureFeatureSnapshot(projectRoot, activeFeature);
174
- const runtimeArtifactsHasData = await dirHasEntries(runtimeArtifactsRoot(projectRoot));
175
- const runtimeStateHasData = await dirHasEntries(runtimeStateRoot(projectRoot), new Set(["active-feature.json"]));
176
- const featureArtifactsHasData = await dirHasEntries(featureArtifactsPath(projectRoot, activeFeature));
177
- const featureStateHasData = await dirHasEntries(featureStatePath(projectRoot, activeFeature));
178
- if ((runtimeArtifactsHasData || runtimeStateHasData) && !featureArtifactsHasData && !featureStateHasData) {
179
- await copyDirectoryContents(runtimeArtifactsRoot(projectRoot), featureArtifactsPath(projectRoot, activeFeature));
180
- await copyDirectoryContents(runtimeStateRoot(projectRoot), featureStatePath(projectRoot, activeFeature), { exclude: FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT });
181
- }
182
- else if ((!runtimeArtifactsHasData && !runtimeStateHasData) && (featureArtifactsHasData || featureStateHasData)) {
183
- await copyDirectoryContents(featureArtifactsPath(projectRoot, activeFeature), runtimeArtifactsRoot(projectRoot));
184
- await copyDirectoryContents(featureStatePath(projectRoot, activeFeature), runtimeStateRoot(projectRoot), { preserveTargetEntries: new Set(["active-feature.json"]) });
240
+ async function ensureRegistryState(projectRoot) {
241
+ await ensureDir(path.join(projectRoot, RUNTIME_ROOT, "state"));
242
+ await ensureDir(worktreesRoot(projectRoot));
243
+ const gitRepo = await isGitRepository(projectRoot);
244
+ const source = gitRepo ? "git-worktree" : "workspace";
245
+ const branch = gitRepo ? await currentBranch(projectRoot) : "workspace/default";
246
+ const currentRegistry = await readRegistry(projectRoot);
247
+ const entries = [...currentRegistry.entries];
248
+ if (!entries.some((entry) => entry.featureId === DEFAULT_FEATURE_ID)) {
249
+ entries.push(buildDefaultEntry(source, branch));
185
250
  }
186
- const normalized = {
187
- activeFeature,
251
+ const legacyFeatureIds = await listLegacySnapshotIds(projectRoot);
252
+ for (const legacyId of legacyFeatureIds) {
253
+ if (entries.some((entry) => entry.featureId === legacyId)) {
254
+ continue;
255
+ }
256
+ entries.push({
257
+ featureId: legacyId,
258
+ branch: `legacy/${legacyId}`,
259
+ path: `${LEGACY_FEATURES_DIR_REL_PATH}/${legacyId}`,
260
+ source: "legacy-snapshot",
261
+ createdAt: new Date().toISOString()
262
+ });
263
+ }
264
+ const registry = {
265
+ schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
266
+ updatedAt: new Date().toISOString(),
267
+ entries: dedupeEntries(entries)
268
+ };
269
+ await writeRegistry(projectRoot, registry);
270
+ const active = await readActiveFeatureMetaInternal(projectRoot);
271
+ const normalizedActive = registryHasFeature(registry, active.activeFeature)
272
+ ? active.activeFeature
273
+ : DEFAULT_FEATURE_ID;
274
+ const activeMeta = {
275
+ activeFeature: normalizedActive,
188
276
  updatedAt: new Date().toISOString()
189
277
  };
190
- await writeActiveFeatureMeta(projectRoot, normalized);
191
- return normalized;
278
+ await writeActiveFeatureMeta(projectRoot, activeMeta);
279
+ return { registry, activeMeta };
280
+ }
281
+ export async function ensureFeatureSystem(projectRoot) {
282
+ const { activeMeta } = await ensureRegistryState(projectRoot);
283
+ return activeMeta;
284
+ }
285
+ export async function readFeatureWorktreeRegistry(projectRoot) {
286
+ const { registry } = await ensureRegistryState(projectRoot);
287
+ return registry;
288
+ }
289
+ export async function readActiveFeature(projectRoot) {
290
+ const meta = await ensureFeatureSystem(projectRoot);
291
+ return normalizedFeatureId(meta.activeFeature);
292
+ }
293
+ export async function listFeatures(projectRoot) {
294
+ const registry = await readFeatureWorktreeRegistry(projectRoot);
295
+ return registry.entries.map((entry) => entry.featureId).sort((a, b) => a.localeCompare(b));
192
296
  }
193
297
  export async function syncActiveFeatureSnapshot(projectRoot) {
194
- const activeFeature = await readActiveFeature(projectRoot);
195
- await ensureFeatureSnapshot(projectRoot, activeFeature);
196
- await copyDirectoryContents(runtimeArtifactsRoot(projectRoot), featureArtifactsPath(projectRoot, activeFeature));
197
- await copyDirectoryContents(runtimeStateRoot(projectRoot), featureStatePath(projectRoot, activeFeature), {
198
- exclude: FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT
199
- });
298
+ await ensureFeatureSystem(projectRoot);
200
299
  }
201
300
  export async function switchActiveFeature(projectRoot, featureId) {
202
- await ensureFeatureSystem(projectRoot);
203
- const current = await readActiveFeature(projectRoot);
301
+ const registry = await readFeatureWorktreeRegistry(projectRoot);
204
302
  const target = normalizedFeatureId(featureId);
205
- if (current === target) {
206
- const unchanged = {
207
- activeFeature: current,
208
- updatedAt: new Date().toISOString()
209
- };
210
- await writeActiveFeatureMeta(projectRoot, unchanged);
211
- return unchanged;
212
- }
213
- await syncActiveFeatureSnapshot(projectRoot);
214
- await ensureFeatureSnapshot(projectRoot, target);
215
- await copyDirectoryContents(featureArtifactsPath(projectRoot, target), runtimeArtifactsRoot(projectRoot));
216
- await copyDirectoryContents(featureStatePath(projectRoot, target), runtimeStateRoot(projectRoot), {
217
- preserveTargetEntries: new Set(["active-feature.json"])
218
- });
303
+ if (!registryHasFeature(registry, target)) {
304
+ throw new Error(`Feature "${target}" is not registered. Create it first with /cc-ops feature new ${target}.`);
305
+ }
219
306
  const nextMeta = {
220
307
  activeFeature: target,
221
308
  updatedAt: new Date().toISOString()
@@ -224,24 +311,78 @@ export async function switchActiveFeature(projectRoot, featureId) {
224
311
  return nextMeta;
225
312
  }
226
313
  export async function createFeature(projectRoot, rawFeatureId, options = {}) {
227
- await ensureFeatureSystem(projectRoot);
314
+ const registry = await readFeatureWorktreeRegistry(projectRoot);
228
315
  const featureId = normalizedFeatureId(rawFeatureId);
229
- if (featureId === DEFAULT_FEATURE_ID && rawFeatureId.trim().length > 0 && rawFeatureId.trim().toLowerCase() !== "default") {
316
+ if (featureId === DEFAULT_FEATURE_ID &&
317
+ rawFeatureId.trim().length > 0 &&
318
+ rawFeatureId.trim().toLowerCase() !== "default") {
230
319
  throw new Error(`Unable to create feature from "${rawFeatureId}" — use letters, numbers, and dashes.`);
231
320
  }
232
- const featureDir = featureRootPath(projectRoot, featureId);
233
- if (await exists(featureDir)) {
321
+ if (registryHasFeature(registry, featureId)) {
234
322
  throw new Error(`Feature "${featureId}" already exists.`);
235
323
  }
236
- await ensureFeatureSnapshot(projectRoot, featureId);
237
- if (options.cloneActive === true) {
238
- const activeFeature = await readActiveFeature(projectRoot);
239
- await syncActiveFeatureSnapshot(projectRoot);
240
- await copyDirectoryContents(featureArtifactsPath(projectRoot, activeFeature), featureArtifactsPath(projectRoot, featureId));
241
- await copyDirectoryContents(featureStatePath(projectRoot, activeFeature), featureStatePath(projectRoot, featureId));
324
+ const isGit = await isGitRepository(projectRoot);
325
+ let entry;
326
+ if (isGit) {
327
+ const workspacePath = featureRootPath(projectRoot, featureId);
328
+ if (await exists(workspacePath)) {
329
+ throw new Error(`Worktree path already exists: ${workspacePath}`);
330
+ }
331
+ await ensureDir(path.dirname(workspacePath));
332
+ const branch = `feature/${featureId}`;
333
+ const localBranchRef = `refs/heads/${branch}`;
334
+ const branchCheck = await runGit(projectRoot, ["show-ref", "--verify", "--quiet", localBranchRef]);
335
+ if (branchCheck.ok) {
336
+ const addExisting = await runGit(projectRoot, ["worktree", "add", workspacePath, branch]);
337
+ if (!addExisting.ok) {
338
+ throw new Error(`Unable to attach worktree for branch "${branch}": ${addExisting.stderr}`);
339
+ }
340
+ }
341
+ else {
342
+ const startPoint = options.cloneActive === false
343
+ ? await defaultStartPoint(projectRoot)
344
+ : "HEAD";
345
+ const addNew = await runGit(projectRoot, ["worktree", "add", "-b", branch, workspacePath, startPoint]);
346
+ if (!addNew.ok) {
347
+ throw new Error(`Unable to create worktree "${featureId}" on branch "${branch}": ${addNew.stderr}`);
348
+ }
349
+ }
350
+ entry = {
351
+ featureId,
352
+ branch,
353
+ path: toRelativePath(projectRoot, workspacePath),
354
+ source: "git-worktree",
355
+ createdAt: new Date().toISOString()
356
+ };
242
357
  }
358
+ else {
359
+ const workspacePath = featureRootPath(projectRoot, featureId);
360
+ await ensureDir(workspacePath);
361
+ entry = {
362
+ featureId,
363
+ branch: `workspace/${featureId}`,
364
+ path: toRelativePath(projectRoot, workspacePath),
365
+ source: "workspace",
366
+ createdAt: new Date().toISOString()
367
+ };
368
+ }
369
+ const nextRegistry = {
370
+ schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
371
+ updatedAt: new Date().toISOString(),
372
+ entries: dedupeEntries([...registry.entries, entry])
373
+ };
374
+ await writeRegistry(projectRoot, nextRegistry);
243
375
  if (options.switchTo === true) {
244
376
  await switchActiveFeature(projectRoot, featureId);
245
377
  }
246
378
  return featureId;
247
379
  }
380
+ export async function activeFeatureWorkspacePath(projectRoot) {
381
+ const registry = await readFeatureWorktreeRegistry(projectRoot);
382
+ const active = await readActiveFeature(projectRoot);
383
+ const entry = findEntry(registry, active);
384
+ if (!entry) {
385
+ return projectRoot;
386
+ }
387
+ return resolveFeatureWorkspacePath(projectRoot, entry);
388
+ }
package/dist/install.js CHANGED
@@ -18,7 +18,7 @@ import { featureCommandContract, featureCommandSkillMarkdown } from "./content/f
18
18
  import { tddLogCommandContract, tddLogCommandSkillMarkdown } from "./content/tdd-log-command.js";
19
19
  import { retroCommandContract, retroCommandSkillMarkdown } from "./content/retro-command.js";
20
20
  import { archiveCommandContract, archiveCommandSkillMarkdown } from "./content/archive-command.js";
21
- import { rewindAcknowledgeCommandContract, rewindCommandContract, rewindCommandSkillMarkdown } from "./content/rewind-command.js";
21
+ import { rewindCommandContract, rewindCommandSkillMarkdown } from "./content/rewind-command.js";
22
22
  import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
23
23
  import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
24
24
  import { sessionStartScript, stopCheckpointScript, preCompactScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
@@ -210,7 +210,7 @@ async function writeSkills(projectRoot, config) {
210
210
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-tree", "SKILL.md"), treeCommandSkillMarkdown());
211
211
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-diff", "SKILL.md"), diffCommandSkillMarkdown());
212
212
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-ops", "SKILL.md"), opsCommandSkillMarkdown());
213
- await writeFileSafe(runtimePath(projectRoot, "skills", "feature-workspaces", "SKILL.md"), featureCommandSkillMarkdown());
213
+ await writeFileSafe(runtimePath(projectRoot, "skills", "using-git-worktrees", "SKILL.md"), featureCommandSkillMarkdown());
214
214
  await writeFileSafe(runtimePath(projectRoot, "skills", "tdd-cycle-log", "SKILL.md"), tddLogCommandSkillMarkdown());
215
215
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-retro", "SKILL.md"), retroCommandSkillMarkdown());
216
216
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-rewind", "SKILL.md"), rewindCommandSkillMarkdown());
@@ -277,7 +277,6 @@ async function writeUtilityCommands(projectRoot) {
277
277
  await writeFileSafe(runtimePath(projectRoot, "commands", "retro.md"), retroCommandContract());
278
278
  await writeFileSafe(runtimePath(projectRoot, "commands", "archive.md"), archiveCommandContract());
279
279
  await writeFileSafe(runtimePath(projectRoot, "commands", "rewind.md"), rewindCommandContract());
280
- await writeFileSafe(runtimePath(projectRoot, "commands", "rewind-ack.md"), rewindAcknowledgeCommandContract());
281
280
  }
282
281
  function toObject(value) {
283
282
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -827,10 +826,6 @@ async function ensureSessionStateFiles(projectRoot) {
827
826
  if (!(await exists(knowledgeDigestPath))) {
828
827
  await writeFileSafe(knowledgeDigestPath, "# Knowledge digest (auto-generated)\n\n(no entries yet)\n");
829
828
  }
830
- const preambleLogPath = path.join(stateDir, "preamble-log.jsonl");
831
- if (!(await exists(preambleLogPath))) {
832
- await writeFileSafe(preambleLogPath, "");
833
- }
834
829
  const tddCycleLogPath = path.join(stateDir, "tdd-cycle-log.jsonl");
835
830
  if (!(await exists(tddCycleLogPath))) {
836
831
  await writeFileSafe(tddCycleLogPath, "");
@@ -945,7 +940,8 @@ async function cleanLegacyArtifacts(projectRoot) {
945
940
  "session-guidelines",
946
941
  "security-review",
947
942
  "documentation",
948
- "browser-qa-testing"
943
+ "browser-qa-testing",
944
+ "feature-workspaces"
949
945
  ]) {
950
946
  try {
951
947
  await fs.rm(runtimePath(projectRoot, "skills", legacyFolder), {
package/dist/policy.js CHANGED
@@ -86,7 +86,7 @@ export async function policyChecks(projectRoot, options = {}) {
86
86
  const utilitySkillChecks = [
87
87
  { file: runtimeFile("skills/learnings/SKILL.md"), needle: "strict JSONL schema", name: "utility_skill:learnings:jsonl_schema" },
88
88
  { file: runtimeFile("skills/learnings/SKILL.md"), needle: "knowledge.jsonl", name: "utility_skill:learnings:jsonl_store" },
89
- { file: runtimeFile("skills/learnings/SKILL.md"), needle: "type, trigger, action, confidence, domain, stage, created, project", name: "utility_skill:learnings:field_order" },
89
+ { file: runtimeFile("skills/learnings/SKILL.md"), needle: "type, trigger, action, confidence, domain, stage, origin_stage, origin_feature, frequency, universality, maturity, created, first_seen_ts, last_seen_ts, project", name: "utility_skill:learnings:field_order" },
90
90
  { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Subcommands", name: "utility_skill:learnings:subcommands" },
91
91
  { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:learnings:hard_gate" },
92
92
  { file: runtimeFile("commands/learn.md"), needle: "## Subcommands", name: "utility_command:learn:subcommands" },
@@ -99,8 +99,8 @@ export async function policyChecks(projectRoot, options = {}) {
99
99
  { file: runtimeFile("skills/flow-diff/SKILL.md"), needle: "## Protocol", name: "utility_skill:diff:protocol" },
100
100
  { file: runtimeFile("skills/flow-diff/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:diff:hard_gate" },
101
101
  { file: runtimeFile("commands/feature.md"), needle: "## Subcommands", name: "utility_command:feature:subcommands" },
102
- { file: runtimeFile("skills/feature-workspaces/SKILL.md"), needle: "## Protocol", name: "utility_skill:feature:protocol" },
103
- { file: runtimeFile("skills/feature-workspaces/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:feature:hard_gate" },
102
+ { file: runtimeFile("skills/using-git-worktrees/SKILL.md"), needle: "## Protocol", name: "utility_skill:feature:protocol" },
103
+ { file: runtimeFile("skills/using-git-worktrees/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:feature:hard_gate" },
104
104
  { file: runtimeFile("commands/tdd-log.md"), needle: "## Subcommands", name: "utility_command:tdd_log:subcommands" },
105
105
  { file: runtimeFile("skills/tdd-cycle-log/SKILL.md"), needle: "## Protocol", name: "utility_skill:tdd_log:protocol" },
106
106
  { file: runtimeFile("skills/tdd-cycle-log/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:tdd_log:hard_gate" },
@@ -108,7 +108,6 @@ export async function policyChecks(projectRoot, options = {}) {
108
108
  { file: runtimeFile("skills/flow-retro/SKILL.md"), needle: "## Protocol", name: "utility_skill:retro:protocol" },
109
109
  { file: runtimeFile("skills/flow-retro/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:retro:hard_gate" },
110
110
  { file: runtimeFile("commands/rewind.md"), needle: "## Algorithm", name: "utility_command:rewind:algorithm" },
111
- { file: runtimeFile("commands/rewind-ack.md"), needle: "## Algorithm", name: "utility_command:rewind_ack:algorithm" },
112
111
  { file: runtimeFile("skills/flow-rewind/SKILL.md"), needle: "## Protocol", name: "utility_skill:rewind:protocol" },
113
112
  { file: runtimeFile("skills/flow-rewind/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:rewind:hard_gate" },
114
113
  { file: runtimeFile("skills/subagent-dev/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:sdd:hard_gate" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.15.0",
3
+ "version": "0.18.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {