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.
- package/dist/artifact-linter.js +154 -0
- package/dist/cli.js +2 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +4 -3
- package/dist/content/compound-command.d.ts +2 -0
- package/dist/content/compound-command.js +72 -0
- package/dist/content/contracts.js +1 -1
- package/dist/content/doctor-references.js +7 -6
- package/dist/content/feature-command.js +54 -51
- package/dist/content/harnesses-doc.js +5 -3
- package/dist/content/hooks.js +2 -2
- package/dist/content/ideate-command.d.ts +2 -0
- package/dist/content/ideate-command.js +73 -0
- package/dist/content/learnings.d.ts +1 -1
- package/dist/content/learnings.js +22 -5
- package/dist/content/meta-skill.js +6 -3
- package/dist/content/next-command.js +5 -5
- package/dist/content/observe.js +3 -2
- package/dist/content/ops-command.js +4 -4
- package/dist/content/protocols.js +27 -38
- package/dist/content/retro-command.js +2 -1
- package/dist/content/rewind-command.d.ts +0 -1
- package/dist/content/rewind-command.js +19 -33
- package/dist/content/skills.js +14 -8
- package/dist/content/stage-schema.js +3 -38
- package/dist/content/stages/plan.js +16 -5
- package/dist/content/stages/review.js +20 -0
- package/dist/content/stages/scope.js +9 -3
- package/dist/content/stages/ship.js +1 -0
- package/dist/content/stages/tdd.js +5 -4
- package/dist/content/templates.js +105 -9
- package/dist/content/utility-skills.d.ts +3 -1
- package/dist/content/utility-skills.js +91 -1
- package/dist/delegation.d.ts +33 -3
- package/dist/delegation.js +56 -3
- package/dist/doctor.js +269 -88
- package/dist/feature-system.d.ts +22 -5
- package/dist/feature-system.js +267 -126
- package/dist/harness-adapters.js +17 -1
- package/dist/install.js +10 -8
- package/dist/policy.js +13 -4
- 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,
|
|
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", "
|
|
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
|
-
["
|
|
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}/
|
|
1025
|
-
: `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/
|
|
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
|
|
1032
|
-
: `no feature
|
|
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:
|
|
1036
|
-
ok:
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
|
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 ||
|
package/dist/feature-system.d.ts
CHANGED
|
@@ -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>;
|