cclaw-cli 0.15.1 → 0.21.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 (42) 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 +4 -3
  5. package/dist/content/compound-command.d.ts +2 -0
  6. package/dist/content/compound-command.js +72 -0
  7. package/dist/content/contracts.js +1 -1
  8. package/dist/content/doctor-references.js +7 -6
  9. package/dist/content/feature-command.js +54 -51
  10. package/dist/content/harnesses-doc.js +5 -3
  11. package/dist/content/hooks.js +2 -2
  12. package/dist/content/ideate-command.d.ts +2 -0
  13. package/dist/content/ideate-command.js +73 -0
  14. package/dist/content/learnings.d.ts +1 -1
  15. package/dist/content/learnings.js +22 -5
  16. package/dist/content/meta-skill.js +6 -3
  17. package/dist/content/next-command.js +5 -5
  18. package/dist/content/observe.js +3 -2
  19. package/dist/content/ops-command.js +4 -4
  20. package/dist/content/protocols.js +27 -38
  21. package/dist/content/retro-command.js +2 -1
  22. package/dist/content/rewind-command.d.ts +0 -1
  23. package/dist/content/rewind-command.js +19 -33
  24. package/dist/content/skills.js +14 -8
  25. package/dist/content/stage-schema.js +3 -38
  26. package/dist/content/stages/plan.js +16 -5
  27. package/dist/content/stages/review.js +20 -0
  28. package/dist/content/stages/scope.js +9 -3
  29. package/dist/content/stages/ship.js +1 -0
  30. package/dist/content/stages/tdd.js +5 -4
  31. package/dist/content/templates.js +105 -9
  32. package/dist/content/utility-skills.d.ts +3 -1
  33. package/dist/content/utility-skills.js +91 -1
  34. package/dist/delegation.d.ts +33 -3
  35. package/dist/delegation.js +56 -3
  36. package/dist/doctor.js +269 -88
  37. package/dist/feature-system.d.ts +22 -5
  38. package/dist/feature-system.js +267 -126
  39. package/dist/harness-adapters.js +17 -1
  40. package/dist/install.js +10 -8
  41. package/dist/policy.js +13 -4
  42. package/package.json +1 -1
package/dist/doctor.js CHANGED
@@ -14,7 +14,7 @@ import { readFlowState } from "./runs.js";
14
14
  import { skippedStagesForTrack } from "./flow-state.js";
15
15
  import { TRACK_STAGES } from "./types.js";
16
16
  import { checkMandatoryDelegations } from "./delegation.js";
17
- import { ensureFeatureSystem, featureRootPath, listFeatures, readActiveFeature } from "./feature-system.js";
17
+ import { ensureFeatureSystem, listFeatures, readActiveFeature, readFeatureWorktreeRegistry, resolveFeatureWorkspacePath, worktreeRegistryPath } from "./feature-system.js";
18
18
  import { buildTraceMatrix } from "./trace-matrix.js";
19
19
  import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
20
20
  import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
@@ -25,7 +25,6 @@ import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
25
25
  import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
26
26
  import { validateHookDocument } from "./hook-schema.js";
27
27
  const execFileAsync = promisify(execFile);
28
- const PREAMBLE_COOLDOWN_MS = 15 * 60 * 1000;
29
28
  async function isGitRepo(projectRoot) {
30
29
  try {
31
30
  await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: projectRoot });
@@ -35,6 +34,27 @@ async function isGitRepo(projectRoot) {
35
34
  return false;
36
35
  }
37
36
  }
37
+ async function gitWorktreePaths(projectRoot) {
38
+ try {
39
+ const { stdout } = await execFileAsync("git", ["worktree", "list", "--porcelain"], {
40
+ cwd: projectRoot
41
+ });
42
+ const out = new Set();
43
+ for (const line of stdout.split(/\r?\n/)) {
44
+ const trimmed = line.trim();
45
+ if (!trimmed.startsWith("worktree "))
46
+ continue;
47
+ const rawPath = trimmed.slice("worktree ".length).trim();
48
+ if (!rawPath)
49
+ continue;
50
+ out.add(path.resolve(rawPath));
51
+ }
52
+ return out;
53
+ }
54
+ catch {
55
+ return new Set();
56
+ }
57
+ }
38
58
  async function resolveGitHooksDir(projectRoot) {
39
59
  try {
40
60
  const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", "hooks"], { cwd: projectRoot });
@@ -463,6 +483,7 @@ export async function doctorChecks(projectRoot, options = {}) {
463
483
  const hasMarkers = content.includes(CCLAW_MARKER_START) && content.includes(CCLAW_MARKER_END);
464
484
  const hasCcCommand = content.includes("/cc");
465
485
  const hasCcNext = content.includes("/cc-next");
486
+ const hasCcIdeate = content.includes("/cc-ideate");
466
487
  const hasCcLearn = content.includes("/cc-learn");
467
488
  const hasCcView = content.includes("/cc-view");
468
489
  const hasCcOps = content.includes("/cc-ops");
@@ -472,6 +493,7 @@ export async function doctorChecks(projectRoot, options = {}) {
472
493
  agentsBlockOk = hasMarkers
473
494
  && hasCcCommand
474
495
  && hasCcNext
496
+ && hasCcIdeate
475
497
  && hasCcLearn
476
498
  && hasCcView
477
499
  && hasCcOps
@@ -485,7 +507,7 @@ export async function doctorChecks(projectRoot, options = {}) {
485
507
  details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
486
508
  });
487
509
  // Utility commands
488
- for (const cmd of ["learn", "next", "status", "tree", "diff", "feature", "tdd-log", "retro", "rewind", "rewind-ack"]) {
510
+ for (const cmd of ["learn", "next", "ideate", "status", "tree", "diff", "feature", "tdd-log", "retro", "compound", "rewind"]) {
489
511
  const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
490
512
  checks.push({
491
513
  name: `utility_command:${cmd}`,
@@ -496,12 +518,16 @@ export async function doctorChecks(projectRoot, options = {}) {
496
518
  // Utility skills
497
519
  for (const [folder, label] of [
498
520
  ["learnings", "learnings"],
521
+ ["flow-ideate", "flow-ideate"],
499
522
  ["flow-tree", "flow-tree"],
500
523
  ["flow-diff", "flow-diff"],
501
- ["feature-workspaces", "feature-workspaces"],
524
+ ["using-git-worktrees", "using-git-worktrees"],
502
525
  ["tdd-cycle-log", "tdd-cycle-log"],
503
526
  ["flow-retro", "flow-retro"],
527
+ ["flow-compound", "flow-compound"],
504
528
  ["flow-rewind", "flow-rewind"],
529
+ ["verification-before-completion", "verification-before-completion"],
530
+ ["finishing-a-development-branch", "finishing-a-development-branch"],
505
531
  ["subagent-dev", "sdd"],
506
532
  ["parallel-dispatch", "parallel-agents"],
507
533
  ["session", "session"],
@@ -830,6 +856,102 @@ export async function doctorChecks(projectRoot, options = {}) {
830
856
  ? `legacy ${RUNTIME_ROOT}/knowledge.md must be removed — cclaw is JSONL-native`
831
857
  : `no legacy markdown store present`
832
858
  });
859
+ const knowledgePath = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
860
+ if (await exists(knowledgePath)) {
861
+ let malformedKnowledgeLines = 0;
862
+ let missingSchemaV2Fields = 0;
863
+ let parsedKnowledgeLines = 0;
864
+ let lowConfidenceLines = 0;
865
+ const triggerActionCounts = new Map();
866
+ const requiredV2Fields = [
867
+ "type",
868
+ "trigger",
869
+ "action",
870
+ "confidence",
871
+ "domain",
872
+ "stage",
873
+ "origin_stage",
874
+ "origin_feature",
875
+ "frequency",
876
+ "universality",
877
+ "maturity",
878
+ "created",
879
+ "first_seen_ts",
880
+ "last_seen_ts",
881
+ "project"
882
+ ];
883
+ try {
884
+ const raw = await fs.readFile(knowledgePath, "utf8");
885
+ const lines = raw
886
+ .split("\n")
887
+ .map((line) => line.trim())
888
+ .filter((line) => line.length > 0);
889
+ for (const line of lines) {
890
+ try {
891
+ const parsed = JSON.parse(line);
892
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
893
+ malformedKnowledgeLines += 1;
894
+ continue;
895
+ }
896
+ parsedKnowledgeLines += 1;
897
+ const confidence = typeof parsed.confidence === "string" ? parsed.confidence.toLowerCase() : "";
898
+ if (confidence === "low") {
899
+ lowConfidenceLines += 1;
900
+ }
901
+ const trigger = typeof parsed.trigger === "string" ? parsed.trigger.trim().toLowerCase() : "";
902
+ const action = typeof parsed.action === "string" ? parsed.action.trim().toLowerCase() : "";
903
+ if (trigger.length > 0 && action.length > 0) {
904
+ const key = `${trigger} => ${action}`;
905
+ triggerActionCounts.set(key, (triggerActionCounts.get(key) ?? 0) + 1);
906
+ }
907
+ const missing = requiredV2Fields.some((field) => !Object.prototype.hasOwnProperty.call(parsed, field));
908
+ if (missing) {
909
+ missingSchemaV2Fields += 1;
910
+ }
911
+ }
912
+ catch {
913
+ malformedKnowledgeLines += 1;
914
+ }
915
+ }
916
+ }
917
+ catch {
918
+ malformedKnowledgeLines += 1;
919
+ }
920
+ checks.push({
921
+ name: "knowledge:jsonl_parseable",
922
+ ok: malformedKnowledgeLines === 0,
923
+ details: malformedKnowledgeLines === 0
924
+ ? "knowledge.jsonl lines parse as JSON objects"
925
+ : `knowledge.jsonl contains ${malformedKnowledgeLines} malformed line(s)`
926
+ });
927
+ checks.push({
928
+ name: "warning:knowledge:schema_v2_fields",
929
+ ok: true,
930
+ details: parsedKnowledgeLines === 0
931
+ ? "knowledge.jsonl is empty"
932
+ : missingSchemaV2Fields === 0
933
+ ? `all ${parsedKnowledgeLines} knowledge line(s) include schema v2 fields`
934
+ : `warning: ${missingSchemaV2Fields}/${parsedKnowledgeLines} knowledge line(s) miss schema v2 fields (origin/maturity/frequency metadata)`
935
+ });
936
+ const lowConfidenceRatio = parsedKnowledgeLines === 0 ? 0 : lowConfidenceLines / parsedKnowledgeLines;
937
+ checks.push({
938
+ name: "warning:knowledge:low_confidence_density",
939
+ ok: true,
940
+ details: parsedKnowledgeLines === 0
941
+ ? "knowledge.jsonl is empty"
942
+ : lowConfidenceRatio <= 0.35
943
+ ? `low-confidence entries: ${lowConfidenceLines}/${parsedKnowledgeLines}`
944
+ : `warning: low-confidence entries are high (${lowConfidenceLines}/${parsedKnowledgeLines}, ${(lowConfidenceRatio * 100).toFixed(1)}%). Consider /cc-learn curate before adding more.`
945
+ });
946
+ const repeatedClusters = [...triggerActionCounts.entries()].filter(([, count]) => count >= 3);
947
+ checks.push({
948
+ name: "warning:knowledge:repeat_clusters",
949
+ ok: true,
950
+ details: repeatedClusters.length === 0
951
+ ? "no high-frequency repeated trigger/action clusters detected"
952
+ : `warning: ${repeatedClusters.length} repeated learning cluster(s) detected (>=3 repeats). Consider /cc-ops compound to lift them into rules/skills.`
953
+ });
954
+ }
833
955
  checks.push({
834
956
  name: "state:checkpoint_exists",
835
957
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "checkpoint.json")),
@@ -840,6 +962,54 @@ export async function doctorChecks(projectRoot, options = {}) {
840
962
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "stage-activity.jsonl")),
841
963
  details: `${RUNTIME_ROOT}/state/stage-activity.jsonl must exist`
842
964
  });
965
+ const stageActivityPath = path.join(projectRoot, RUNTIME_ROOT, "state", "stage-activity.jsonl");
966
+ if (await exists(stageActivityPath)) {
967
+ let malformedActivityLines = 0;
968
+ let missingSchemaVersion = 0;
969
+ let parsedActivityLines = 0;
970
+ try {
971
+ const raw = await fs.readFile(stageActivityPath, "utf8");
972
+ const lines = raw
973
+ .split("\n")
974
+ .map((line) => line.trim())
975
+ .filter((line) => line.length > 0);
976
+ for (const line of lines) {
977
+ try {
978
+ const parsed = JSON.parse(line);
979
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
980
+ malformedActivityLines += 1;
981
+ continue;
982
+ }
983
+ parsedActivityLines += 1;
984
+ if (parsed.schemaVersion !== 1) {
985
+ missingSchemaVersion += 1;
986
+ }
987
+ }
988
+ catch {
989
+ malformedActivityLines += 1;
990
+ }
991
+ }
992
+ }
993
+ catch {
994
+ malformedActivityLines += 1;
995
+ }
996
+ checks.push({
997
+ name: "state:stage_activity_jsonl_parseable",
998
+ ok: malformedActivityLines === 0,
999
+ details: malformedActivityLines === 0
1000
+ ? "stage-activity.jsonl lines parse as JSON objects"
1001
+ : `stage-activity.jsonl contains ${malformedActivityLines} malformed line(s)`
1002
+ });
1003
+ checks.push({
1004
+ name: "warning:state:stage_activity_schema_version",
1005
+ ok: true,
1006
+ details: parsedActivityLines === 0
1007
+ ? "stage-activity.jsonl is empty"
1008
+ : missingSchemaVersion === 0
1009
+ ? `all ${parsedActivityLines} stage-activity line(s) include schemaVersion=1`
1010
+ : `warning: ${missingSchemaVersion}/${parsedActivityLines} stage-activity line(s) missing schemaVersion=1`
1011
+ });
1012
+ }
843
1013
  checks.push({
844
1014
  name: "state:suggestion_memory_exists",
845
1015
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "suggestion-memory.json")),
@@ -880,81 +1050,6 @@ export async function doctorChecks(projectRoot, options = {}) {
880
1050
  details: modePath
881
1051
  });
882
1052
  }
883
- const preambleLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "preamble-log.jsonl");
884
- const preambleLogExists = await exists(preambleLogPath);
885
- checks.push({
886
- name: "state:preamble_log_exists",
887
- ok: preambleLogExists,
888
- details: `${RUNTIME_ROOT}/state/preamble-log.jsonl must exist for preamble budget tracking`
889
- });
890
- if (preambleLogExists) {
891
- let duplicateHits = 0;
892
- let parsedEntries = 0;
893
- let malformedEntries = 0;
894
- try {
895
- const now = Date.now();
896
- const byKey = new Map();
897
- const raw = await fs.readFile(preambleLogPath, "utf8");
898
- const lines = raw
899
- .split("\n")
900
- .map((line) => line.trim())
901
- .filter((line) => line.length > 0);
902
- for (const line of lines) {
903
- try {
904
- const parsed = JSON.parse(line);
905
- const tsRaw = parsed.ts;
906
- const stageRaw = parsed.stage;
907
- const triggerRaw = parsed.trigger;
908
- const hashRaw = parsed.hash;
909
- if (typeof tsRaw !== "string" ||
910
- typeof stageRaw !== "string" ||
911
- typeof triggerRaw !== "string" ||
912
- typeof hashRaw !== "string") {
913
- malformedEntries += 1;
914
- continue;
915
- }
916
- const stamp = Date.parse(tsRaw);
917
- if (!Number.isFinite(stamp)) {
918
- malformedEntries += 1;
919
- continue;
920
- }
921
- if (now - stamp > 24 * 60 * 60 * 1000) {
922
- continue;
923
- }
924
- parsedEntries += 1;
925
- const key = `${stageRaw}|${triggerRaw}|${hashRaw}`;
926
- const bucket = byKey.get(key) ?? [];
927
- bucket.push(stamp);
928
- byKey.set(key, bucket);
929
- }
930
- catch {
931
- malformedEntries += 1;
932
- }
933
- }
934
- for (const stamps of byKey.values()) {
935
- stamps.sort((a, b) => a - b);
936
- for (let i = 1; i < stamps.length; i += 1) {
937
- if (stamps[i] - stamps[i - 1] < PREAMBLE_COOLDOWN_MS) {
938
- duplicateHits += 1;
939
- }
940
- }
941
- }
942
- }
943
- catch {
944
- malformedEntries += 1;
945
- }
946
- checks.push({
947
- name: "warning:preamble:dedup",
948
- ok: true,
949
- details: duplicateHits > 0
950
- ? `warning: detected ${duplicateHits} repeated preamble emission(s) inside ${Math.floor(PREAMBLE_COOLDOWN_MS / 60000)}m cooldown window`
951
- : parsedEntries > 0
952
- ? `preamble budget healthy (${parsedEntries} recent preamble entry/entries checked)`
953
- : malformedEntries > 0
954
- ? `warning: preamble log exists but entries are malformed (${malformedEntries} line(s) ignored)`
955
- : "preamble log is empty; no recent preamble emissions recorded"
956
- });
957
- }
958
1053
  await ensureFeatureSystem(projectRoot);
959
1054
  const activeFeature = await readActiveFeature(projectRoot);
960
1055
  let flowState = await readFlowState(projectRoot);
@@ -1011,31 +1106,117 @@ export async function doctorChecks(projectRoot, options = {}) {
1011
1106
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "artifacts")),
1012
1107
  details: `${RUNTIME_ROOT}/artifacts must exist as the active artifact root`
1013
1108
  });
1109
+ const artifactsRoot = path.join(projectRoot, RUNTIME_ROOT, "artifacts");
1110
+ let artifactPlaceholderHits = [];
1111
+ if (await exists(artifactsRoot)) {
1112
+ try {
1113
+ const entries = await fs.readdir(artifactsRoot, { withFileTypes: true });
1114
+ const placeholderPattern = /\b(?:TODO|TBD|FIXME)\b|<fill-in>|<your-.*-here>/giu;
1115
+ for (const entry of entries) {
1116
+ if (!entry.isFile() || !entry.name.endsWith(".md"))
1117
+ continue;
1118
+ const filePath = path.join(artifactsRoot, entry.name);
1119
+ const content = await fs.readFile(filePath, "utf8");
1120
+ const matchCount = (content.match(placeholderPattern) ?? []).length;
1121
+ if (matchCount > 0) {
1122
+ artifactPlaceholderHits.push(`${entry.name}:${matchCount}`);
1123
+ }
1124
+ }
1125
+ }
1126
+ catch {
1127
+ artifactPlaceholderHits = [];
1128
+ }
1129
+ }
1130
+ checks.push({
1131
+ name: "warning:artifacts:stale_placeholders",
1132
+ ok: true,
1133
+ details: artifactPlaceholderHits.length === 0
1134
+ ? "no TODO/TBD/FIXME placeholder markers found in active artifacts"
1135
+ : `warning: placeholder markers detected in active artifacts (${artifactPlaceholderHits.join(", ")}). Clear before marking completion.`
1136
+ });
1014
1137
  const features = await listFeatures(projectRoot);
1138
+ const worktreeRegistry = await readFeatureWorktreeRegistry(projectRoot);
1139
+ const activeFeatureEntry = worktreeRegistry.entries.find((entry) => entry.featureId === activeFeature);
1140
+ const activeFeatureWorkspacePath = activeFeatureEntry
1141
+ ? resolveFeatureWorkspacePath(projectRoot, activeFeatureEntry)
1142
+ : "";
1015
1143
  checks.push({
1016
1144
  name: "state:active_feature_meta",
1017
1145
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "active-feature.json")),
1018
1146
  details: `${RUNTIME_ROOT}/state/active-feature.json must exist`
1019
1147
  });
1148
+ checks.push({
1149
+ name: "state:worktree_registry_exists",
1150
+ ok: await exists(worktreeRegistryPath(projectRoot)),
1151
+ details: `${RUNTIME_ROOT}/state/worktrees.json must exist and track feature->worktree mapping`
1152
+ });
1020
1153
  checks.push({
1021
1154
  name: "state:active_feature_exists",
1022
1155
  ok: features.includes(activeFeature),
1023
1156
  details: features.includes(activeFeature)
1024
- ? `active feature "${activeFeature}" is present in ${RUNTIME_ROOT}/features`
1025
- : `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/features`
1157
+ ? `active feature "${activeFeature}" is present in ${RUNTIME_ROOT}/state/worktrees.json`
1158
+ : `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/state/worktrees.json`
1026
1159
  });
1027
1160
  checks.push({
1028
1161
  name: "state:features_nonempty",
1029
1162
  ok: features.length > 0,
1030
1163
  details: features.length > 0
1031
- ? `${features.length} feature snapshot(s): ${features.join(", ")}`
1032
- : `no feature snapshots found under ${RUNTIME_ROOT}/features`
1164
+ ? `${features.length} registered feature workspace(s): ${features.join(", ")}`
1165
+ : `no feature workspaces found in ${RUNTIME_ROOT}/state/worktrees.json`
1166
+ });
1167
+ checks.push({
1168
+ name: "state:active_feature_workspace_path",
1169
+ ok: activeFeatureEntry ? await exists(activeFeatureWorkspacePath) : false,
1170
+ details: activeFeatureEntry
1171
+ ? `active feature "${activeFeature}" maps to workspace path ${activeFeatureEntry.path} (${activeFeatureEntry.source})`
1172
+ : `active feature "${activeFeature}" has no worktree registry entry`
1173
+ });
1174
+ const missingRegistryPaths = [];
1175
+ for (const entry of worktreeRegistry.entries) {
1176
+ const workspacePath = resolveFeatureWorkspacePath(projectRoot, entry);
1177
+ if (!(await exists(workspacePath))) {
1178
+ missingRegistryPaths.push(`${entry.featureId}:${entry.path}`);
1179
+ }
1180
+ }
1181
+ checks.push({
1182
+ name: "state:worktree_registry_paths_exist",
1183
+ ok: missingRegistryPaths.length === 0,
1184
+ details: missingRegistryPaths.length === 0
1185
+ ? "all worktree registry entries resolve to existing paths"
1186
+ : `missing worktree paths for registry entries: ${missingRegistryPaths.join(", ")}`
1187
+ });
1188
+ const gitTrackedPaths = await gitWorktreePaths(projectRoot);
1189
+ const registryGitPaths = worktreeRegistry.entries
1190
+ .filter((entry) => entry.source === "git-worktree")
1191
+ .map((entry) => resolveFeatureWorkspacePath(projectRoot, entry));
1192
+ const missingFromGitList = registryGitPaths.filter((workspacePath) => !gitTrackedPaths.has(path.resolve(workspacePath)));
1193
+ checks.push({
1194
+ name: "warning:state:worktree_registry_git_drift",
1195
+ ok: true,
1196
+ details: missingFromGitList.length === 0
1197
+ ? "git-worktree registry entries align with `git worktree list`"
1198
+ : `warning: ${missingFromGitList.length} registry worktree path(s) are missing from \`git worktree list\`: ${missingFromGitList.join(", ")}`
1199
+ });
1200
+ const managedWorktreeRoot = path.join(projectRoot, RUNTIME_ROOT, "worktrees");
1201
+ const unregisteredManagedWorktrees = [...gitTrackedPaths]
1202
+ .filter((workspacePath) => workspacePath.startsWith(path.resolve(managedWorktreeRoot)))
1203
+ .filter((workspacePath) => !registryGitPaths.some((registeredPath) => path.resolve(registeredPath) === workspacePath));
1204
+ checks.push({
1205
+ name: "warning:state:worktree_unregistered_paths",
1206
+ ok: true,
1207
+ details: unregisteredManagedWorktrees.length === 0
1208
+ ? "no unmanaged git worktrees under .cclaw/worktrees"
1209
+ : `warning: unregistered git worktree paths detected: ${unregisteredManagedWorktrees.map((value) => path.relative(projectRoot, value)).join(", ")}`
1033
1210
  });
1211
+ const legacyWorkspaceEntries = worktreeRegistry.entries
1212
+ .filter((entry) => entry.source === "legacy-snapshot")
1213
+ .map((entry) => entry.featureId);
1034
1214
  checks.push({
1035
- name: "state:active_feature_snapshot_dirs",
1036
- ok: await exists(path.join(featureRootPath(projectRoot, activeFeature), "artifacts")) &&
1037
- await exists(path.join(featureRootPath(projectRoot, activeFeature), "state")),
1038
- details: `${RUNTIME_ROOT}/features/${activeFeature}/artifacts and /state must exist`
1215
+ name: "warning:state:legacy_feature_snapshots",
1216
+ ok: legacyWorkspaceEntries.length === 0,
1217
+ details: legacyWorkspaceEntries.length === 0
1218
+ ? "no legacy .cclaw/features snapshot entries remain"
1219
+ : `legacy snapshot entries still present (read-only): ${legacyWorkspaceEntries.join(", ")}`
1039
1220
  });
1040
1221
  const staleStages = Object.keys(flowState.staleStages).filter((value) => COMMAND_FILE_ORDER.includes(value));
1041
1222
  checks.push({
@@ -1043,7 +1224,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1043
1224
  ok: staleStages.length === 0,
1044
1225
  details: staleStages.length === 0
1045
1226
  ? "no stale stages pending acknowledgement"
1046
- : `stale stages must be acknowledged via /cc-ops rewind-ack: ${staleStages.join(", ")}`
1227
+ : `stale stages must be acknowledged via /cc-ops rewind --ack <stage>: ${staleStages.join(", ")}`
1047
1228
  });
1048
1229
  const retroRequired = flowState.completedStages.includes("ship");
1049
1230
  const retroComplete = !retroRequired ||
@@ -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>;