cclaw-cli 0.15.1 → 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.
- package/dist/artifact-linter.js +154 -0
- package/dist/cli.js +2 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +2 -3
- 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 +2 -2
- package/dist/content/hooks.js +2 -2
- package/dist/content/learnings.d.ts +1 -1
- package/dist/content/learnings.js +22 -5
- package/dist/content/meta-skill.js +2 -2
- package/dist/content/next-command.js +2 -2
- package/dist/content/observe.js +3 -2
- package/dist/content/ops-command.js +1 -3
- package/dist/content/protocols.js +6 -34
- package/dist/content/rewind-command.d.ts +0 -1
- package/dist/content/rewind-command.js +19 -33
- package/dist/content/skills.js +2 -3
- package/dist/content/stage-schema.js +3 -38
- package/dist/content/stages/plan.js +16 -5
- package/dist/content/stages/scope.js +9 -3
- package/dist/content/stages/tdd.js +5 -4
- package/dist/content/templates.js +105 -9
- package/dist/content/utility-skills.js +1 -1
- package/dist/delegation.d.ts +33 -3
- package/dist/delegation.js +56 -3
- package/dist/doctor.js +147 -88
- package/dist/feature-system.d.ts +22 -5
- package/dist/feature-system.js +267 -126
- package/dist/install.js +4 -8
- package/dist/policy.js +3 -4
- package/package.json +1 -1
package/dist/delegation.js
CHANGED
|
@@ -12,6 +12,20 @@ function delegationLogPath(projectRoot) {
|
|
|
12
12
|
function delegationLockPath(projectRoot) {
|
|
13
13
|
return path.join(projectRoot, RUNTIME_ROOT, "state", ".delegation.lock");
|
|
14
14
|
}
|
|
15
|
+
function createSpanId() {
|
|
16
|
+
return `dspan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
17
|
+
}
|
|
18
|
+
function isDelegationTokenUsage(value) {
|
|
19
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
20
|
+
return false;
|
|
21
|
+
const o = value;
|
|
22
|
+
return (typeof o.input === "number" &&
|
|
23
|
+
Number.isFinite(o.input) &&
|
|
24
|
+
typeof o.output === "number" &&
|
|
25
|
+
Number.isFinite(o.output) &&
|
|
26
|
+
typeof o.model === "string" &&
|
|
27
|
+
o.model.trim().length > 0);
|
|
28
|
+
}
|
|
15
29
|
function isDelegationEntry(value) {
|
|
16
30
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
17
31
|
return false;
|
|
@@ -21,15 +35,30 @@ function isDelegationEntry(value) {
|
|
|
21
35
|
o.status === "completed" ||
|
|
22
36
|
o.status === "failed" ||
|
|
23
37
|
o.status === "waived";
|
|
38
|
+
const timestampOk = typeof o.ts === "string" ||
|
|
39
|
+
typeof o.startTs === "string";
|
|
40
|
+
const retryOk = o.retryCount === undefined ||
|
|
41
|
+
(typeof o.retryCount === "number" &&
|
|
42
|
+
Number.isFinite(o.retryCount) &&
|
|
43
|
+
Number.isInteger(o.retryCount) &&
|
|
44
|
+
o.retryCount >= 0);
|
|
24
45
|
return (typeof o.stage === "string" &&
|
|
25
46
|
typeof o.agent === "string" &&
|
|
26
47
|
modeOk &&
|
|
27
48
|
statusOk &&
|
|
28
|
-
|
|
49
|
+
timestampOk &&
|
|
50
|
+
(o.spanId === undefined || typeof o.spanId === "string") &&
|
|
51
|
+
(o.parentSpanId === undefined || typeof o.parentSpanId === "string") &&
|
|
52
|
+
(o.startTs === undefined || typeof o.startTs === "string") &&
|
|
53
|
+
(o.endTs === undefined || typeof o.endTs === "string") &&
|
|
29
54
|
(o.taskId === undefined || typeof o.taskId === "string") &&
|
|
30
55
|
(o.waiverReason === undefined || typeof o.waiverReason === "string") &&
|
|
31
56
|
(o.runId === undefined || typeof o.runId === "string") &&
|
|
32
|
-
(o.conditionTrigger === undefined || typeof o.conditionTrigger === "string")
|
|
57
|
+
(o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
|
|
58
|
+
(o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
|
|
59
|
+
retryOk &&
|
|
60
|
+
(o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
|
|
61
|
+
(o.schemaVersion === undefined || o.schemaVersion === 1));
|
|
33
62
|
}
|
|
34
63
|
function parseLedger(raw, runId) {
|
|
35
64
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -41,7 +70,18 @@ function parseLedger(raw, runId) {
|
|
|
41
70
|
if (Array.isArray(entriesRaw)) {
|
|
42
71
|
for (const item of entriesRaw) {
|
|
43
72
|
if (isDelegationEntry(item)) {
|
|
44
|
-
|
|
73
|
+
const ts = item.startTs ?? item.ts ?? new Date().toISOString();
|
|
74
|
+
entries.push({
|
|
75
|
+
...item,
|
|
76
|
+
spanId: item.spanId ?? createSpanId(),
|
|
77
|
+
startTs: ts,
|
|
78
|
+
ts,
|
|
79
|
+
retryCount: typeof item.retryCount === "number" && Number.isInteger(item.retryCount) && item.retryCount >= 0
|
|
80
|
+
? item.retryCount
|
|
81
|
+
: 0,
|
|
82
|
+
evidenceRefs: Array.isArray(item.evidenceRefs) ? item.evidenceRefs : [],
|
|
83
|
+
schemaVersion: 1
|
|
84
|
+
});
|
|
45
85
|
}
|
|
46
86
|
}
|
|
47
87
|
}
|
|
@@ -67,7 +107,20 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
67
107
|
await withDirectoryLock(delegationLockPath(projectRoot), async () => {
|
|
68
108
|
const filePath = delegationLogPath(projectRoot);
|
|
69
109
|
const prior = await readDelegationLedger(projectRoot);
|
|
110
|
+
const startTs = entry.startTs ?? entry.ts ?? new Date().toISOString();
|
|
70
111
|
const stamped = { ...entry, runId: entry.runId ?? activeRunId };
|
|
112
|
+
stamped.spanId = entry.spanId ?? createSpanId();
|
|
113
|
+
stamped.startTs = startTs;
|
|
114
|
+
stamped.ts = startTs;
|
|
115
|
+
stamped.schemaVersion = 1;
|
|
116
|
+
if (stamped.retryCount === undefined ||
|
|
117
|
+
!Number.isInteger(stamped.retryCount) ||
|
|
118
|
+
stamped.retryCount < 0) {
|
|
119
|
+
stamped.retryCount = 0;
|
|
120
|
+
}
|
|
121
|
+
if (!Array.isArray(stamped.evidenceRefs)) {
|
|
122
|
+
stamped.evidenceRefs = [];
|
|
123
|
+
}
|
|
71
124
|
const ledger = {
|
|
72
125
|
runId: activeRunId,
|
|
73
126
|
entries: [...prior.entries, stamped]
|
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 });
|
|
@@ -485,7 +484,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
485
484
|
details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
|
|
486
485
|
});
|
|
487
486
|
// Utility commands
|
|
488
|
-
for (const cmd of ["learn", "next", "status", "tree", "diff", "feature", "tdd-log", "retro", "rewind"
|
|
487
|
+
for (const cmd of ["learn", "next", "status", "tree", "diff", "feature", "tdd-log", "retro", "rewind"]) {
|
|
489
488
|
const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
|
|
490
489
|
checks.push({
|
|
491
490
|
name: `utility_command:${cmd}`,
|
|
@@ -498,7 +497,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
498
497
|
["learnings", "learnings"],
|
|
499
498
|
["flow-tree", "flow-tree"],
|
|
500
499
|
["flow-diff", "flow-diff"],
|
|
501
|
-
["
|
|
500
|
+
["using-git-worktrees", "using-git-worktrees"],
|
|
502
501
|
["tdd-cycle-log", "tdd-cycle-log"],
|
|
503
502
|
["flow-retro", "flow-retro"],
|
|
504
503
|
["flow-rewind", "flow-rewind"],
|
|
@@ -830,6 +829,72 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
830
829
|
? `legacy ${RUNTIME_ROOT}/knowledge.md must be removed — cclaw is JSONL-native`
|
|
831
830
|
: `no legacy markdown store present`
|
|
832
831
|
});
|
|
832
|
+
const knowledgePath = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
|
|
833
|
+
if (await exists(knowledgePath)) {
|
|
834
|
+
let malformedKnowledgeLines = 0;
|
|
835
|
+
let missingSchemaV2Fields = 0;
|
|
836
|
+
let parsedKnowledgeLines = 0;
|
|
837
|
+
const requiredV2Fields = [
|
|
838
|
+
"type",
|
|
839
|
+
"trigger",
|
|
840
|
+
"action",
|
|
841
|
+
"confidence",
|
|
842
|
+
"domain",
|
|
843
|
+
"stage",
|
|
844
|
+
"origin_stage",
|
|
845
|
+
"origin_feature",
|
|
846
|
+
"frequency",
|
|
847
|
+
"universality",
|
|
848
|
+
"maturity",
|
|
849
|
+
"created",
|
|
850
|
+
"first_seen_ts",
|
|
851
|
+
"last_seen_ts",
|
|
852
|
+
"project"
|
|
853
|
+
];
|
|
854
|
+
try {
|
|
855
|
+
const raw = await fs.readFile(knowledgePath, "utf8");
|
|
856
|
+
const lines = raw
|
|
857
|
+
.split("\n")
|
|
858
|
+
.map((line) => line.trim())
|
|
859
|
+
.filter((line) => line.length > 0);
|
|
860
|
+
for (const line of lines) {
|
|
861
|
+
try {
|
|
862
|
+
const parsed = JSON.parse(line);
|
|
863
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
864
|
+
malformedKnowledgeLines += 1;
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
parsedKnowledgeLines += 1;
|
|
868
|
+
const missing = requiredV2Fields.some((field) => !Object.prototype.hasOwnProperty.call(parsed, field));
|
|
869
|
+
if (missing) {
|
|
870
|
+
missingSchemaV2Fields += 1;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
malformedKnowledgeLines += 1;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
malformedKnowledgeLines += 1;
|
|
880
|
+
}
|
|
881
|
+
checks.push({
|
|
882
|
+
name: "knowledge:jsonl_parseable",
|
|
883
|
+
ok: malformedKnowledgeLines === 0,
|
|
884
|
+
details: malformedKnowledgeLines === 0
|
|
885
|
+
? "knowledge.jsonl lines parse as JSON objects"
|
|
886
|
+
: `knowledge.jsonl contains ${malformedKnowledgeLines} malformed line(s)`
|
|
887
|
+
});
|
|
888
|
+
checks.push({
|
|
889
|
+
name: "warning:knowledge:schema_v2_fields",
|
|
890
|
+
ok: true,
|
|
891
|
+
details: parsedKnowledgeLines === 0
|
|
892
|
+
? "knowledge.jsonl is empty"
|
|
893
|
+
: missingSchemaV2Fields === 0
|
|
894
|
+
? `all ${parsedKnowledgeLines} knowledge line(s) include schema v2 fields`
|
|
895
|
+
: `warning: ${missingSchemaV2Fields}/${parsedKnowledgeLines} knowledge line(s) miss schema v2 fields (origin/maturity/frequency metadata)`
|
|
896
|
+
});
|
|
897
|
+
}
|
|
833
898
|
checks.push({
|
|
834
899
|
name: "state:checkpoint_exists",
|
|
835
900
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "checkpoint.json")),
|
|
@@ -840,6 +905,54 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
840
905
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "stage-activity.jsonl")),
|
|
841
906
|
details: `${RUNTIME_ROOT}/state/stage-activity.jsonl must exist`
|
|
842
907
|
});
|
|
908
|
+
const stageActivityPath = path.join(projectRoot, RUNTIME_ROOT, "state", "stage-activity.jsonl");
|
|
909
|
+
if (await exists(stageActivityPath)) {
|
|
910
|
+
let malformedActivityLines = 0;
|
|
911
|
+
let missingSchemaVersion = 0;
|
|
912
|
+
let parsedActivityLines = 0;
|
|
913
|
+
try {
|
|
914
|
+
const raw = await fs.readFile(stageActivityPath, "utf8");
|
|
915
|
+
const lines = raw
|
|
916
|
+
.split("\n")
|
|
917
|
+
.map((line) => line.trim())
|
|
918
|
+
.filter((line) => line.length > 0);
|
|
919
|
+
for (const line of lines) {
|
|
920
|
+
try {
|
|
921
|
+
const parsed = JSON.parse(line);
|
|
922
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
923
|
+
malformedActivityLines += 1;
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
parsedActivityLines += 1;
|
|
927
|
+
if (parsed.schemaVersion !== 1) {
|
|
928
|
+
missingSchemaVersion += 1;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
malformedActivityLines += 1;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
malformedActivityLines += 1;
|
|
938
|
+
}
|
|
939
|
+
checks.push({
|
|
940
|
+
name: "state:stage_activity_jsonl_parseable",
|
|
941
|
+
ok: malformedActivityLines === 0,
|
|
942
|
+
details: malformedActivityLines === 0
|
|
943
|
+
? "stage-activity.jsonl lines parse as JSON objects"
|
|
944
|
+
: `stage-activity.jsonl contains ${malformedActivityLines} malformed line(s)`
|
|
945
|
+
});
|
|
946
|
+
checks.push({
|
|
947
|
+
name: "warning:state:stage_activity_schema_version",
|
|
948
|
+
ok: true,
|
|
949
|
+
details: parsedActivityLines === 0
|
|
950
|
+
? "stage-activity.jsonl is empty"
|
|
951
|
+
: missingSchemaVersion === 0
|
|
952
|
+
? `all ${parsedActivityLines} stage-activity line(s) include schemaVersion=1`
|
|
953
|
+
: `warning: ${missingSchemaVersion}/${parsedActivityLines} stage-activity line(s) missing schemaVersion=1`
|
|
954
|
+
});
|
|
955
|
+
}
|
|
843
956
|
checks.push({
|
|
844
957
|
name: "state:suggestion_memory_exists",
|
|
845
958
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "suggestion-memory.json")),
|
|
@@ -880,81 +993,6 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
880
993
|
details: modePath
|
|
881
994
|
});
|
|
882
995
|
}
|
|
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
996
|
await ensureFeatureSystem(projectRoot);
|
|
959
997
|
const activeFeature = await readActiveFeature(projectRoot);
|
|
960
998
|
let flowState = await readFlowState(projectRoot);
|
|
@@ -1012,30 +1050,51 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1012
1050
|
details: `${RUNTIME_ROOT}/artifacts must exist as the active artifact root`
|
|
1013
1051
|
});
|
|
1014
1052
|
const features = await listFeatures(projectRoot);
|
|
1053
|
+
const worktreeRegistry = await readFeatureWorktreeRegistry(projectRoot);
|
|
1054
|
+
const activeFeatureEntry = worktreeRegistry.entries.find((entry) => entry.featureId === activeFeature);
|
|
1055
|
+
const activeFeatureWorkspacePath = activeFeatureEntry
|
|
1056
|
+
? resolveFeatureWorkspacePath(projectRoot, activeFeatureEntry)
|
|
1057
|
+
: "";
|
|
1015
1058
|
checks.push({
|
|
1016
1059
|
name: "state:active_feature_meta",
|
|
1017
1060
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "active-feature.json")),
|
|
1018
1061
|
details: `${RUNTIME_ROOT}/state/active-feature.json must exist`
|
|
1019
1062
|
});
|
|
1063
|
+
checks.push({
|
|
1064
|
+
name: "state:worktree_registry_exists",
|
|
1065
|
+
ok: await exists(worktreeRegistryPath(projectRoot)),
|
|
1066
|
+
details: `${RUNTIME_ROOT}/state/worktrees.json must exist and track feature->worktree mapping`
|
|
1067
|
+
});
|
|
1020
1068
|
checks.push({
|
|
1021
1069
|
name: "state:active_feature_exists",
|
|
1022
1070
|
ok: features.includes(activeFeature),
|
|
1023
1071
|
details: features.includes(activeFeature)
|
|
1024
|
-
? `active feature "${activeFeature}" is present in ${RUNTIME_ROOT}/
|
|
1025
|
-
: `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/
|
|
1072
|
+
? `active feature "${activeFeature}" is present in ${RUNTIME_ROOT}/state/worktrees.json`
|
|
1073
|
+
: `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/state/worktrees.json`
|
|
1026
1074
|
});
|
|
1027
1075
|
checks.push({
|
|
1028
1076
|
name: "state:features_nonempty",
|
|
1029
1077
|
ok: features.length > 0,
|
|
1030
1078
|
details: features.length > 0
|
|
1031
|
-
? `${features.length} feature
|
|
1032
|
-
: `no feature
|
|
1079
|
+
? `${features.length} registered feature workspace(s): ${features.join(", ")}`
|
|
1080
|
+
: `no feature workspaces found in ${RUNTIME_ROOT}/state/worktrees.json`
|
|
1081
|
+
});
|
|
1082
|
+
checks.push({
|
|
1083
|
+
name: "state:active_feature_workspace_path",
|
|
1084
|
+
ok: activeFeatureEntry ? await exists(activeFeatureWorkspacePath) : false,
|
|
1085
|
+
details: activeFeatureEntry
|
|
1086
|
+
? `active feature "${activeFeature}" maps to workspace path ${activeFeatureEntry.path} (${activeFeatureEntry.source})`
|
|
1087
|
+
: `active feature "${activeFeature}" has no worktree registry entry`
|
|
1033
1088
|
});
|
|
1089
|
+
const legacyWorkspaceEntries = worktreeRegistry.entries
|
|
1090
|
+
.filter((entry) => entry.source === "legacy-snapshot")
|
|
1091
|
+
.map((entry) => entry.featureId);
|
|
1034
1092
|
checks.push({
|
|
1035
|
-
name: "state:
|
|
1036
|
-
ok:
|
|
1037
|
-
|
|
1038
|
-
|
|
1093
|
+
name: "warning:state:legacy_feature_snapshots",
|
|
1094
|
+
ok: legacyWorkspaceEntries.length === 0,
|
|
1095
|
+
details: legacyWorkspaceEntries.length === 0
|
|
1096
|
+
? "no legacy .cclaw/features snapshot entries remain"
|
|
1097
|
+
: `legacy snapshot entries still present (read-only): ${legacyWorkspaceEntries.join(", ")}`
|
|
1039
1098
|
});
|
|
1040
1099
|
const staleStages = Object.keys(flowState.staleStages).filter((value) => COMMAND_FILE_ORDER.includes(value));
|
|
1041
1100
|
checks.push({
|
|
@@ -1043,7 +1102,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1043
1102
|
ok: staleStages.length === 0,
|
|
1044
1103
|
details: staleStages.length === 0
|
|
1045
1104
|
? "no stale stages pending acknowledgement"
|
|
1046
|
-
: `stale stages must be acknowledged via /cc-ops rewind
|
|
1105
|
+
: `stale stages must be acknowledged via /cc-ops rewind --ack <stage>: ${staleStages.join(", ")}`
|
|
1047
1106
|
});
|
|
1048
1107
|
const retroRequired = flowState.completedStages.includes("ship");
|
|
1049
1108
|
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>;
|