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.
@@ -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
- typeof o.ts === "string" &&
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
- entries.push(item);
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, 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 });
@@ -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", "rewind-ack"]) {
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
- ["feature-workspaces", "feature-workspaces"],
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}/features`
1025
- : `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/features`
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 snapshot(s): ${features.join(", ")}`
1032
- : `no feature snapshots found under ${RUNTIME_ROOT}/features`
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: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`
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-ack: ${staleStages.join(", ")}`
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 ||
@@ -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>;