cclaw-cli 0.12.0 → 0.14.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/cli.d.ts +2 -0
- package/dist/cli.js +25 -1
- package/dist/config.js +19 -8
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +16 -1
- package/dist/content/archive-command.d.ts +2 -0
- package/dist/content/archive-command.js +98 -0
- package/dist/content/contracts.js +1 -1
- package/dist/content/diff-command.d.ts +2 -0
- package/dist/content/diff-command.js +83 -0
- package/dist/content/feature-command.d.ts +2 -0
- package/dist/content/feature-command.js +120 -0
- package/dist/content/harnesses-doc.js +11 -0
- package/dist/content/hooks.js +48 -2
- package/dist/content/learnings.d.ts +0 -2
- package/dist/content/learnings.js +4 -33
- package/dist/content/meta-skill.js +4 -2
- package/dist/content/next-command.js +18 -9
- package/dist/content/observe.d.ts +5 -1
- package/dist/content/observe.js +134 -2
- package/dist/content/ops-command.d.ts +2 -0
- package/dist/content/ops-command.js +60 -0
- package/dist/content/protocols.js +14 -2
- package/dist/content/retro-command.d.ts +2 -0
- package/dist/content/retro-command.js +77 -0
- package/dist/content/rewind-command.d.ts +3 -0
- package/dist/content/rewind-command.js +120 -0
- package/dist/content/skills.js +2 -0
- package/dist/content/stage-common-guidance.js +2 -1
- package/dist/content/status-command.js +43 -35
- package/dist/content/tdd-log-command.d.ts +2 -0
- package/dist/content/tdd-log-command.js +75 -0
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +36 -6
- package/dist/content/tree-command.d.ts +2 -0
- package/dist/content/tree-command.js +91 -0
- package/dist/content/utility-skills.js +1 -1
- package/dist/content/view-command.d.ts +2 -0
- package/dist/content/view-command.js +57 -0
- package/dist/doctor-registry.js +3 -3
- package/dist/doctor.js +149 -3
- package/dist/feature-system.d.ts +18 -0
- package/dist/feature-system.js +247 -0
- package/dist/flow-state.d.ts +25 -0
- package/dist/flow-state.js +8 -1
- package/dist/harness-adapters.js +95 -4
- package/dist/install.js +44 -2
- package/dist/policy.js +22 -0
- package/dist/runs.d.ts +33 -1
- package/dist/runs.js +365 -6
- package/dist/tdd-cycle.d.ts +22 -0
- package/dist/tdd-cycle.js +82 -0
- package/dist/types.d.ts +4 -2
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -14,8 +14,10 @@ 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
18
|
import { buildTraceMatrix } from "./trace-matrix.js";
|
|
18
19
|
import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
|
|
20
|
+
import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
|
|
19
21
|
import { stageSkillFolder } from "./content/skills.js";
|
|
20
22
|
import { doctorCheckMetadata } from "./doctor-registry.js";
|
|
21
23
|
import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
|
|
@@ -445,7 +447,19 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
445
447
|
});
|
|
446
448
|
continue;
|
|
447
449
|
}
|
|
448
|
-
for (const shim of [
|
|
450
|
+
for (const shim of [
|
|
451
|
+
"cc.md",
|
|
452
|
+
"cc-next.md",
|
|
453
|
+
"cc-learn.md",
|
|
454
|
+
"cc-status.md",
|
|
455
|
+
"cc-tree.md",
|
|
456
|
+
"cc-diff.md",
|
|
457
|
+
"cc-feature.md",
|
|
458
|
+
"cc-tdd-log.md",
|
|
459
|
+
"cc-retro.md",
|
|
460
|
+
"cc-rewind.md",
|
|
461
|
+
"cc-rewind-ack.md"
|
|
462
|
+
]) {
|
|
449
463
|
const shimPath = path.join(projectRoot, adapter.commandDir, shim);
|
|
450
464
|
checks.push({
|
|
451
465
|
name: `shim:${harness}:${shim.replace(".md", "")}`,
|
|
@@ -462,10 +476,32 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
462
476
|
const hasCcCommand = content.includes("/cc");
|
|
463
477
|
const hasCcNext = content.includes("/cc-next");
|
|
464
478
|
const hasCcLearn = content.includes("/cc-learn");
|
|
479
|
+
const hasCcStatus = content.includes("/cc-status");
|
|
480
|
+
const hasCcTree = content.includes("/cc-tree");
|
|
481
|
+
const hasCcDiff = content.includes("/cc-diff");
|
|
482
|
+
const hasCcFeature = content.includes("/cc-feature");
|
|
483
|
+
const hasCcTddLog = content.includes("/cc-tdd-log");
|
|
484
|
+
const hasCcRetro = content.includes("/cc-retro");
|
|
485
|
+
const hasCcRewind = content.includes("/cc-rewind");
|
|
486
|
+
const hasCcRewindAck = content.includes("/cc-rewind-ack");
|
|
465
487
|
const hasVerification = content.includes("Verification Discipline");
|
|
466
488
|
const hasMinimalMarker = content.includes("intentionally minimal for cross-project use");
|
|
467
489
|
const hasMetaSkillPointer = content.includes(".cclaw/skills/using-cclaw/SKILL.md");
|
|
468
|
-
agentsBlockOk = hasMarkers
|
|
490
|
+
agentsBlockOk = hasMarkers
|
|
491
|
+
&& hasCcCommand
|
|
492
|
+
&& hasCcNext
|
|
493
|
+
&& hasCcLearn
|
|
494
|
+
&& hasCcStatus
|
|
495
|
+
&& hasCcTree
|
|
496
|
+
&& hasCcDiff
|
|
497
|
+
&& hasCcFeature
|
|
498
|
+
&& hasCcTddLog
|
|
499
|
+
&& hasCcRetro
|
|
500
|
+
&& hasCcRewind
|
|
501
|
+
&& hasCcRewindAck
|
|
502
|
+
&& hasVerification
|
|
503
|
+
&& hasMinimalMarker
|
|
504
|
+
&& hasMetaSkillPointer;
|
|
469
505
|
}
|
|
470
506
|
checks.push({
|
|
471
507
|
name: "agents:cclaw_block",
|
|
@@ -473,7 +509,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
473
509
|
details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
|
|
474
510
|
});
|
|
475
511
|
// Utility commands
|
|
476
|
-
for (const cmd of ["learn"]) {
|
|
512
|
+
for (const cmd of ["learn", "next", "status", "tree", "diff", "feature", "tdd-log", "retro", "rewind", "rewind-ack"]) {
|
|
477
513
|
const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
|
|
478
514
|
checks.push({
|
|
479
515
|
name: `utility_command:${cmd}`,
|
|
@@ -484,6 +520,12 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
484
520
|
// Utility skills
|
|
485
521
|
for (const [folder, label] of [
|
|
486
522
|
["learnings", "learnings"],
|
|
523
|
+
["flow-tree", "flow-tree"],
|
|
524
|
+
["flow-diff", "flow-diff"],
|
|
525
|
+
["feature-workspaces", "feature-workspaces"],
|
|
526
|
+
["tdd-cycle-log", "tdd-cycle-log"],
|
|
527
|
+
["flow-retro", "flow-retro"],
|
|
528
|
+
["flow-rewind", "flow-rewind"],
|
|
487
529
|
["subagent-dev", "sdd"],
|
|
488
530
|
["parallel-dispatch", "parallel-agents"],
|
|
489
531
|
["session", "session"],
|
|
@@ -937,6 +979,8 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
937
979
|
: "preamble log is empty; no recent preamble emissions recorded"
|
|
938
980
|
});
|
|
939
981
|
}
|
|
982
|
+
await ensureFeatureSystem(projectRoot);
|
|
983
|
+
const activeFeature = await readActiveFeature(projectRoot);
|
|
940
984
|
let flowState = await readFlowState(projectRoot);
|
|
941
985
|
if (options.reconcileCurrentStageGates === true) {
|
|
942
986
|
const reconciliation = await reconcileAndWriteCurrentStageGateCatalog(projectRoot);
|
|
@@ -991,6 +1035,108 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
991
1035
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "artifacts")),
|
|
992
1036
|
details: `${RUNTIME_ROOT}/artifacts must exist as the active artifact root`
|
|
993
1037
|
});
|
|
1038
|
+
const features = await listFeatures(projectRoot);
|
|
1039
|
+
checks.push({
|
|
1040
|
+
name: "state:active_feature_meta",
|
|
1041
|
+
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "active-feature.json")),
|
|
1042
|
+
details: `${RUNTIME_ROOT}/state/active-feature.json must exist`
|
|
1043
|
+
});
|
|
1044
|
+
checks.push({
|
|
1045
|
+
name: "state:active_feature_exists",
|
|
1046
|
+
ok: features.includes(activeFeature),
|
|
1047
|
+
details: features.includes(activeFeature)
|
|
1048
|
+
? `active feature "${activeFeature}" is present in ${RUNTIME_ROOT}/features`
|
|
1049
|
+
: `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/features`
|
|
1050
|
+
});
|
|
1051
|
+
checks.push({
|
|
1052
|
+
name: "state:features_nonempty",
|
|
1053
|
+
ok: features.length > 0,
|
|
1054
|
+
details: features.length > 0
|
|
1055
|
+
? `${features.length} feature snapshot(s): ${features.join(", ")}`
|
|
1056
|
+
: `no feature snapshots found under ${RUNTIME_ROOT}/features`
|
|
1057
|
+
});
|
|
1058
|
+
checks.push({
|
|
1059
|
+
name: "state:active_feature_snapshot_dirs",
|
|
1060
|
+
ok: await exists(path.join(featureRootPath(projectRoot, activeFeature), "artifacts")) &&
|
|
1061
|
+
await exists(path.join(featureRootPath(projectRoot, activeFeature), "state")),
|
|
1062
|
+
details: `${RUNTIME_ROOT}/features/${activeFeature}/artifacts and /state must exist`
|
|
1063
|
+
});
|
|
1064
|
+
const staleStages = Object.keys(flowState.staleStages).filter((value) => COMMAND_FILE_ORDER.includes(value));
|
|
1065
|
+
checks.push({
|
|
1066
|
+
name: "state:stale_stages_resolved",
|
|
1067
|
+
ok: staleStages.length === 0,
|
|
1068
|
+
details: staleStages.length === 0
|
|
1069
|
+
? "no stale stages pending acknowledgement"
|
|
1070
|
+
: `stale stages must be acknowledged via /cc-rewind-ack: ${staleStages.join(", ")}`
|
|
1071
|
+
});
|
|
1072
|
+
const retroRequired = flowState.completedStages.includes("ship");
|
|
1073
|
+
const retroComplete = !retroRequired ||
|
|
1074
|
+
(typeof flowState.retro.completedAt === "string" && flowState.retro.compoundEntries > 0);
|
|
1075
|
+
checks.push({
|
|
1076
|
+
name: "state:retro_gate",
|
|
1077
|
+
ok: retroComplete,
|
|
1078
|
+
details: retroComplete
|
|
1079
|
+
? retroRequired
|
|
1080
|
+
? `retro gate complete (${flowState.retro.compoundEntries} compound entries)`
|
|
1081
|
+
: "retro gate not required yet (ship not completed)"
|
|
1082
|
+
: "retro gate incomplete: run /cc-retro and record at least one compound knowledge entry"
|
|
1083
|
+
});
|
|
1084
|
+
const flowSnapshotPath = path.join(projectRoot, RUNTIME_ROOT, "state", "flow-state.snapshot.json");
|
|
1085
|
+
const flowSnapshotExists = await exists(flowSnapshotPath);
|
|
1086
|
+
let flowSnapshotValid = flowSnapshotExists;
|
|
1087
|
+
if (flowSnapshotExists) {
|
|
1088
|
+
try {
|
|
1089
|
+
JSON.parse(await fs.readFile(flowSnapshotPath, "utf8"));
|
|
1090
|
+
flowSnapshotValid = true;
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
flowSnapshotValid = false;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
checks.push({
|
|
1097
|
+
name: "state:flow_snapshot",
|
|
1098
|
+
ok: flowSnapshotExists && flowSnapshotValid,
|
|
1099
|
+
details: flowSnapshotExists
|
|
1100
|
+
? flowSnapshotValid
|
|
1101
|
+
? `${RUNTIME_ROOT}/state/flow-state.snapshot.json exists and is valid JSON`
|
|
1102
|
+
: `${RUNTIME_ROOT}/state/flow-state.snapshot.json exists but is invalid JSON`
|
|
1103
|
+
: `${RUNTIME_ROOT}/state/flow-state.snapshot.json is missing`
|
|
1104
|
+
});
|
|
1105
|
+
const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
|
|
1106
|
+
const tddLogExists = await exists(tddLogPath);
|
|
1107
|
+
checks.push({
|
|
1108
|
+
name: "state:tdd_cycle_log_exists",
|
|
1109
|
+
ok: tddLogExists,
|
|
1110
|
+
details: `${RUNTIME_ROOT}/state/tdd-cycle-log.jsonl must exist`
|
|
1111
|
+
});
|
|
1112
|
+
const tddCompleted = flowState.completedStages.includes("tdd")
|
|
1113
|
+
|| (flowState.currentStage === "review" || flowState.currentStage === "ship");
|
|
1114
|
+
if (tddLogExists) {
|
|
1115
|
+
const tddLogRaw = await fs.readFile(tddLogPath, "utf8");
|
|
1116
|
+
const parsedCycles = parseTddCycleLog(tddLogRaw);
|
|
1117
|
+
const validation = validateTddCycleOrder(parsedCycles, { runId: activeRunId || undefined });
|
|
1118
|
+
const hasCoverage = validation.sliceCount > 0;
|
|
1119
|
+
checks.push({
|
|
1120
|
+
name: "state:tdd_cycle_order",
|
|
1121
|
+
ok: validation.ok && (!tddCompleted || hasCoverage),
|
|
1122
|
+
details: validation.ok
|
|
1123
|
+
? tddCompleted && !hasCoverage
|
|
1124
|
+
? "tdd stage complete but no RED/GREEN cycle evidence logged"
|
|
1125
|
+
: `tdd cycle log valid (${validation.sliceCount} slice(s), open_red=${validation.openRedSlices.length})`
|
|
1126
|
+
: `tdd cycle order issues: ${validation.issues.join("; ")}${validation.openRedSlices.length > 0
|
|
1127
|
+
? ` | open red slices: ${validation.openRedSlices.join(", ")}`
|
|
1128
|
+
: ""}`
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
checks.push({
|
|
1133
|
+
name: "state:tdd_cycle_order",
|
|
1134
|
+
ok: !tddCompleted,
|
|
1135
|
+
details: tddCompleted
|
|
1136
|
+
? "tdd stage complete but tdd-cycle-log.jsonl is missing"
|
|
1137
|
+
: "tdd cycle order deferred until tdd stage evidence is generated"
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
994
1140
|
checks.push({
|
|
995
1141
|
name: "runs:archive_root",
|
|
996
1142
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs")),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ActiveFeatureMeta {
|
|
2
|
+
activeFeature: string;
|
|
3
|
+
updatedAt: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function activeFeatureMetaPath(projectRoot: string): string;
|
|
6
|
+
export declare function featureRootPath(projectRoot: string, featureId: string): string;
|
|
7
|
+
export declare function featureArtifactsPath(projectRoot: string, featureId: string): string;
|
|
8
|
+
export declare function featureStatePath(projectRoot: string, featureId: string): string;
|
|
9
|
+
export declare function readActiveFeature(projectRoot: string): Promise<string>;
|
|
10
|
+
export declare function listFeatures(projectRoot: string): Promise<string[]>;
|
|
11
|
+
export declare function ensureFeatureSystem(projectRoot: string): Promise<ActiveFeatureMeta>;
|
|
12
|
+
export declare function syncActiveFeatureSnapshot(projectRoot: string): Promise<void>;
|
|
13
|
+
export declare function switchActiveFeature(projectRoot: string, featureId: string): Promise<ActiveFeatureMeta>;
|
|
14
|
+
export interface CreateFeatureOptions {
|
|
15
|
+
cloneActive?: boolean;
|
|
16
|
+
switchTo?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function createFeature(projectRoot: string, rawFeatureId: string, options?: CreateFeatureOptions): Promise<string>;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
|
+
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
5
|
+
const FEATURES_DIR_REL_PATH = `${RUNTIME_ROOT}/features`;
|
|
6
|
+
const ACTIVE_FEATURE_META_REL_PATH = `${RUNTIME_ROOT}/state/active-feature.json`;
|
|
7
|
+
const DEFAULT_FEATURE_ID = "default";
|
|
8
|
+
const FEATURE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
|
|
9
|
+
const FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT = new Set([
|
|
10
|
+
"active-feature.json",
|
|
11
|
+
".flow-state.lock",
|
|
12
|
+
".delegation.lock"
|
|
13
|
+
]);
|
|
14
|
+
function featuresRoot(projectRoot) {
|
|
15
|
+
return path.join(projectRoot, FEATURES_DIR_REL_PATH);
|
|
16
|
+
}
|
|
17
|
+
function runtimeArtifactsRoot(projectRoot) {
|
|
18
|
+
return path.join(projectRoot, RUNTIME_ROOT, "artifacts");
|
|
19
|
+
}
|
|
20
|
+
function runtimeStateRoot(projectRoot) {
|
|
21
|
+
return path.join(projectRoot, RUNTIME_ROOT, "state");
|
|
22
|
+
}
|
|
23
|
+
export function activeFeatureMetaPath(projectRoot) {
|
|
24
|
+
return path.join(projectRoot, ACTIVE_FEATURE_META_REL_PATH);
|
|
25
|
+
}
|
|
26
|
+
export function featureRootPath(projectRoot, featureId) {
|
|
27
|
+
return path.join(featuresRoot(projectRoot), featureId);
|
|
28
|
+
}
|
|
29
|
+
export function featureArtifactsPath(projectRoot, featureId) {
|
|
30
|
+
return path.join(featureRootPath(projectRoot, featureId), "artifacts");
|
|
31
|
+
}
|
|
32
|
+
export function featureStatePath(projectRoot, featureId) {
|
|
33
|
+
return path.join(featureRootPath(projectRoot, featureId), "state");
|
|
34
|
+
}
|
|
35
|
+
function normalizedFeatureId(value) {
|
|
36
|
+
const candidate = value
|
|
37
|
+
.trim()
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
40
|
+
.replace(/^-+/u, "")
|
|
41
|
+
.replace(/-+$/u, "");
|
|
42
|
+
if (!candidate) {
|
|
43
|
+
return DEFAULT_FEATURE_ID;
|
|
44
|
+
}
|
|
45
|
+
const clipped = candidate.slice(0, 64);
|
|
46
|
+
return FEATURE_ID_PATTERN.test(clipped) ? clipped : DEFAULT_FEATURE_ID;
|
|
47
|
+
}
|
|
48
|
+
async function clearDirectory(dirPath, preserveTargetEntries = new Set()) {
|
|
49
|
+
await ensureDir(dirPath);
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (preserveTargetEntries.has(entry.name)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
await fs.rm(path.join(dirPath, entry.name), { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function copyDirectoryContents(sourceDir, targetDir, options = {}) {
|
|
65
|
+
const exclude = options.exclude ?? new Set();
|
|
66
|
+
const preserveTargetEntries = options.preserveTargetEntries ?? new Set();
|
|
67
|
+
await ensureDir(targetDir);
|
|
68
|
+
await clearDirectory(targetDir, preserveTargetEntries);
|
|
69
|
+
if (!(await exists(sourceDir))) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (exclude.has(entry.name)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const from = path.join(sourceDir, entry.name);
|
|
84
|
+
const to = path.join(targetDir, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
await fs.cp(from, to, { recursive: true, force: true });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (entry.isFile()) {
|
|
90
|
+
await fs.copyFile(from, to);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function dirHasEntries(dirPath, exclude = new Set()) {
|
|
95
|
+
if (!(await exists(dirPath))) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
100
|
+
return entries.some((entry) => !exclude.has(entry.name));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function readActiveFeatureMetaInternal(projectRoot) {
|
|
107
|
+
const filePath = activeFeatureMetaPath(projectRoot);
|
|
108
|
+
if (!(await exists(filePath))) {
|
|
109
|
+
return {
|
|
110
|
+
activeFeature: DEFAULT_FEATURE_ID,
|
|
111
|
+
updatedAt: new Date().toISOString()
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
116
|
+
const activeFeatureRaw = typeof parsed.activeFeature === "string"
|
|
117
|
+
? parsed.activeFeature
|
|
118
|
+
: DEFAULT_FEATURE_ID;
|
|
119
|
+
const updatedAtRaw = typeof parsed.updatedAt === "string"
|
|
120
|
+
? parsed.updatedAt
|
|
121
|
+
: new Date().toISOString();
|
|
122
|
+
return {
|
|
123
|
+
activeFeature: normalizedFeatureId(activeFeatureRaw),
|
|
124
|
+
updatedAt: updatedAtRaw
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return {
|
|
129
|
+
activeFeature: DEFAULT_FEATURE_ID,
|
|
130
|
+
updatedAt: new Date().toISOString()
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function writeActiveFeatureMeta(projectRoot, meta) {
|
|
135
|
+
const normalized = {
|
|
136
|
+
activeFeature: normalizedFeatureId(meta.activeFeature),
|
|
137
|
+
updatedAt: meta.updatedAt
|
|
138
|
+
};
|
|
139
|
+
await writeFileSafe(activeFeatureMetaPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n`);
|
|
140
|
+
}
|
|
141
|
+
async function ensureFeatureSnapshot(projectRoot, featureId) {
|
|
142
|
+
const id = normalizedFeatureId(featureId);
|
|
143
|
+
await ensureDir(featureArtifactsPath(projectRoot, id));
|
|
144
|
+
await ensureDir(featureStatePath(projectRoot, id));
|
|
145
|
+
}
|
|
146
|
+
export async function readActiveFeature(projectRoot) {
|
|
147
|
+
const meta = await readActiveFeatureMetaInternal(projectRoot);
|
|
148
|
+
return normalizedFeatureId(meta.activeFeature);
|
|
149
|
+
}
|
|
150
|
+
export async function listFeatures(projectRoot) {
|
|
151
|
+
const root = featuresRoot(projectRoot);
|
|
152
|
+
if (!(await exists(root))) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = await fs.readdir(root, { withFileTypes: true });
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
return entries
|
|
163
|
+
.filter((entry) => entry.isDirectory() && FEATURE_ID_PATTERN.test(entry.name))
|
|
164
|
+
.map((entry) => entry.name)
|
|
165
|
+
.sort((a, b) => a.localeCompare(b));
|
|
166
|
+
}
|
|
167
|
+
export async function ensureFeatureSystem(projectRoot) {
|
|
168
|
+
await ensureDir(featuresRoot(projectRoot));
|
|
169
|
+
await ensureDir(runtimeArtifactsRoot(projectRoot));
|
|
170
|
+
await ensureDir(runtimeStateRoot(projectRoot));
|
|
171
|
+
const existing = await readActiveFeatureMetaInternal(projectRoot);
|
|
172
|
+
const activeFeature = normalizedFeatureId(existing.activeFeature);
|
|
173
|
+
await ensureFeatureSnapshot(projectRoot, activeFeature);
|
|
174
|
+
const runtimeArtifactsHasData = await dirHasEntries(runtimeArtifactsRoot(projectRoot));
|
|
175
|
+
const runtimeStateHasData = await dirHasEntries(runtimeStateRoot(projectRoot), new Set(["active-feature.json"]));
|
|
176
|
+
const featureArtifactsHasData = await dirHasEntries(featureArtifactsPath(projectRoot, activeFeature));
|
|
177
|
+
const featureStateHasData = await dirHasEntries(featureStatePath(projectRoot, activeFeature));
|
|
178
|
+
if ((runtimeArtifactsHasData || runtimeStateHasData) && !featureArtifactsHasData && !featureStateHasData) {
|
|
179
|
+
await copyDirectoryContents(runtimeArtifactsRoot(projectRoot), featureArtifactsPath(projectRoot, activeFeature));
|
|
180
|
+
await copyDirectoryContents(runtimeStateRoot(projectRoot), featureStatePath(projectRoot, activeFeature), { exclude: FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT });
|
|
181
|
+
}
|
|
182
|
+
else if ((!runtimeArtifactsHasData && !runtimeStateHasData) && (featureArtifactsHasData || featureStateHasData)) {
|
|
183
|
+
await copyDirectoryContents(featureArtifactsPath(projectRoot, activeFeature), runtimeArtifactsRoot(projectRoot));
|
|
184
|
+
await copyDirectoryContents(featureStatePath(projectRoot, activeFeature), runtimeStateRoot(projectRoot), { preserveTargetEntries: new Set(["active-feature.json"]) });
|
|
185
|
+
}
|
|
186
|
+
const normalized = {
|
|
187
|
+
activeFeature,
|
|
188
|
+
updatedAt: new Date().toISOString()
|
|
189
|
+
};
|
|
190
|
+
await writeActiveFeatureMeta(projectRoot, normalized);
|
|
191
|
+
return normalized;
|
|
192
|
+
}
|
|
193
|
+
export async function syncActiveFeatureSnapshot(projectRoot) {
|
|
194
|
+
const activeFeature = await readActiveFeature(projectRoot);
|
|
195
|
+
await ensureFeatureSnapshot(projectRoot, activeFeature);
|
|
196
|
+
await copyDirectoryContents(runtimeArtifactsRoot(projectRoot), featureArtifactsPath(projectRoot, activeFeature));
|
|
197
|
+
await copyDirectoryContents(runtimeStateRoot(projectRoot), featureStatePath(projectRoot, activeFeature), {
|
|
198
|
+
exclude: FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export async function switchActiveFeature(projectRoot, featureId) {
|
|
202
|
+
await ensureFeatureSystem(projectRoot);
|
|
203
|
+
const current = await readActiveFeature(projectRoot);
|
|
204
|
+
const target = normalizedFeatureId(featureId);
|
|
205
|
+
if (current === target) {
|
|
206
|
+
const unchanged = {
|
|
207
|
+
activeFeature: current,
|
|
208
|
+
updatedAt: new Date().toISOString()
|
|
209
|
+
};
|
|
210
|
+
await writeActiveFeatureMeta(projectRoot, unchanged);
|
|
211
|
+
return unchanged;
|
|
212
|
+
}
|
|
213
|
+
await syncActiveFeatureSnapshot(projectRoot);
|
|
214
|
+
await ensureFeatureSnapshot(projectRoot, target);
|
|
215
|
+
await copyDirectoryContents(featureArtifactsPath(projectRoot, target), runtimeArtifactsRoot(projectRoot));
|
|
216
|
+
await copyDirectoryContents(featureStatePath(projectRoot, target), runtimeStateRoot(projectRoot), {
|
|
217
|
+
preserveTargetEntries: new Set(["active-feature.json"])
|
|
218
|
+
});
|
|
219
|
+
const nextMeta = {
|
|
220
|
+
activeFeature: target,
|
|
221
|
+
updatedAt: new Date().toISOString()
|
|
222
|
+
};
|
|
223
|
+
await writeActiveFeatureMeta(projectRoot, nextMeta);
|
|
224
|
+
return nextMeta;
|
|
225
|
+
}
|
|
226
|
+
export async function createFeature(projectRoot, rawFeatureId, options = {}) {
|
|
227
|
+
await ensureFeatureSystem(projectRoot);
|
|
228
|
+
const featureId = normalizedFeatureId(rawFeatureId);
|
|
229
|
+
if (featureId === DEFAULT_FEATURE_ID && rawFeatureId.trim().length > 0 && rawFeatureId.trim().toLowerCase() !== "default") {
|
|
230
|
+
throw new Error(`Unable to create feature from "${rawFeatureId}" — use letters, numbers, and dashes.`);
|
|
231
|
+
}
|
|
232
|
+
const featureDir = featureRootPath(projectRoot, featureId);
|
|
233
|
+
if (await exists(featureDir)) {
|
|
234
|
+
throw new Error(`Feature "${featureId}" already exists.`);
|
|
235
|
+
}
|
|
236
|
+
await ensureFeatureSnapshot(projectRoot, featureId);
|
|
237
|
+
if (options.cloneActive === true) {
|
|
238
|
+
const activeFeature = await readActiveFeature(projectRoot);
|
|
239
|
+
await syncActiveFeatureSnapshot(projectRoot);
|
|
240
|
+
await copyDirectoryContents(featureArtifactsPath(projectRoot, activeFeature), featureArtifactsPath(projectRoot, featureId));
|
|
241
|
+
await copyDirectoryContents(featureStatePath(projectRoot, activeFeature), featureStatePath(projectRoot, featureId));
|
|
242
|
+
}
|
|
243
|
+
if (options.switchTo === true) {
|
|
244
|
+
await switchActiveFeature(projectRoot, featureId);
|
|
245
|
+
}
|
|
246
|
+
return featureId;
|
|
247
|
+
}
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -9,6 +9,25 @@ export interface StageGateState {
|
|
|
9
9
|
passed: string[];
|
|
10
10
|
blocked: string[];
|
|
11
11
|
}
|
|
12
|
+
export interface RewindRecord {
|
|
13
|
+
id: string;
|
|
14
|
+
fromStage: FlowStage;
|
|
15
|
+
toStage: FlowStage;
|
|
16
|
+
reason: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
invalidatedStages: FlowStage[];
|
|
19
|
+
}
|
|
20
|
+
export interface StaleStageMarker {
|
|
21
|
+
rewindId: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
markedAt: string;
|
|
24
|
+
acknowledgedAt?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface RetroState {
|
|
27
|
+
required: boolean;
|
|
28
|
+
completedAt?: string;
|
|
29
|
+
compoundEntries: number;
|
|
30
|
+
}
|
|
12
31
|
export interface FlowState {
|
|
13
32
|
activeRunId: string;
|
|
14
33
|
currentStage: FlowStage;
|
|
@@ -19,6 +38,12 @@ export interface FlowState {
|
|
|
19
38
|
track: FlowTrack;
|
|
20
39
|
/** Stages explicitly skipped for this track (empty for standard; populated for quick). */
|
|
21
40
|
skippedStages: FlowStage[];
|
|
41
|
+
/** Stages invalidated by rewind operations and awaiting explicit acknowledgement. */
|
|
42
|
+
staleStages: Partial<Record<FlowStage, StaleStageMarker>>;
|
|
43
|
+
/** Chronological rewind operations for the active run. */
|
|
44
|
+
rewinds: RewindRecord[];
|
|
45
|
+
/** Mandatory retrospective gate status before archive. */
|
|
46
|
+
retro: RetroState;
|
|
22
47
|
}
|
|
23
48
|
export interface InitialFlowStateOptions {
|
|
24
49
|
activeRunId?: string;
|
package/dist/flow-state.js
CHANGED
|
@@ -41,7 +41,14 @@ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTra
|
|
|
41
41
|
guardEvidence: {},
|
|
42
42
|
stageGateCatalog,
|
|
43
43
|
track,
|
|
44
|
-
skippedStages
|
|
44
|
+
skippedStages,
|
|
45
|
+
staleStages: {},
|
|
46
|
+
rewinds: [],
|
|
47
|
+
retro: {
|
|
48
|
+
required: false,
|
|
49
|
+
completedAt: undefined,
|
|
50
|
+
compoundEntries: 0
|
|
51
|
+
}
|
|
45
52
|
};
|
|
46
53
|
}
|
|
47
54
|
export function canTransition(from, to) {
|
package/dist/harness-adapters.js
CHANGED
|
@@ -11,6 +11,86 @@ function escapeRegExp(value) {
|
|
|
11
11
|
const RUNTIME_AGENTS_BLOCK_SOURCE = `${escapeRegExp(CCLAW_MARKER_START)}[\\s\\S]*?${escapeRegExp(CCLAW_MARKER_END)}`;
|
|
12
12
|
const RUNTIME_AGENTS_BLOCK_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOURCE, "u");
|
|
13
13
|
const RUNTIME_AGENTS_BLOCK_GLOBAL_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOURCE, "gu");
|
|
14
|
+
const UTILITY_SHIMS = [
|
|
15
|
+
{
|
|
16
|
+
fileName: "cc-next.md",
|
|
17
|
+
command: "next",
|
|
18
|
+
skillFolder: "flow-next-step",
|
|
19
|
+
commandFile: "next.md"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
fileName: "cc-view.md",
|
|
23
|
+
command: "view",
|
|
24
|
+
skillFolder: "flow-view",
|
|
25
|
+
commandFile: "view.md"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
fileName: "cc-learn.md",
|
|
29
|
+
command: "learn",
|
|
30
|
+
skillFolder: "learnings",
|
|
31
|
+
commandFile: "learn.md"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
fileName: "cc-status.md",
|
|
35
|
+
command: "status",
|
|
36
|
+
skillFolder: "flow-status",
|
|
37
|
+
commandFile: "status.md"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
fileName: "cc-tree.md",
|
|
41
|
+
command: "tree",
|
|
42
|
+
skillFolder: "flow-tree",
|
|
43
|
+
commandFile: "tree.md"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
fileName: "cc-diff.md",
|
|
47
|
+
command: "diff",
|
|
48
|
+
skillFolder: "flow-diff",
|
|
49
|
+
commandFile: "diff.md"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
fileName: "cc-ops.md",
|
|
53
|
+
command: "ops",
|
|
54
|
+
skillFolder: "flow-ops",
|
|
55
|
+
commandFile: "ops.md"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
fileName: "cc-feature.md",
|
|
59
|
+
command: "feature",
|
|
60
|
+
skillFolder: "feature-workspaces",
|
|
61
|
+
commandFile: "feature.md"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
fileName: "cc-tdd-log.md",
|
|
65
|
+
command: "tdd-log",
|
|
66
|
+
skillFolder: "tdd-cycle-log",
|
|
67
|
+
commandFile: "tdd-log.md"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
fileName: "cc-retro.md",
|
|
71
|
+
command: "retro",
|
|
72
|
+
skillFolder: "flow-retro",
|
|
73
|
+
commandFile: "retro.md"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
fileName: "cc-archive.md",
|
|
77
|
+
command: "archive",
|
|
78
|
+
skillFolder: "flow-archive",
|
|
79
|
+
commandFile: "archive.md"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
fileName: "cc-rewind.md",
|
|
83
|
+
command: "rewind",
|
|
84
|
+
skillFolder: "flow-rewind",
|
|
85
|
+
commandFile: "rewind.md"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
fileName: "cc-rewind-ack.md",
|
|
89
|
+
command: "rewind-ack",
|
|
90
|
+
skillFolder: "flow-rewind",
|
|
91
|
+
commandFile: "rewind-ack.md"
|
|
92
|
+
}
|
|
93
|
+
];
|
|
14
94
|
export const HARNESS_ADAPTERS = {
|
|
15
95
|
claude: {
|
|
16
96
|
id: "claude",
|
|
@@ -98,13 +178,24 @@ When in doubt, prefer **non-trivial** — the quick track is opt-in and only saf
|
|
|
98
178
|
5. Contextual utility skills.
|
|
99
179
|
6. Training priors.
|
|
100
180
|
|
|
101
|
-
### Commands
|
|
181
|
+
### Commands
|
|
102
182
|
|
|
103
183
|
| Command | Purpose |
|
|
104
184
|
|---|---|
|
|
105
185
|
| \`/cc\` | **Entry point.** No args = resume current stage. With prompt = classify task and start the right flow. |
|
|
106
186
|
| \`/cc-next\` | **Progression.** Advances to the next stage when current is complete. |
|
|
187
|
+
| \`/cc-view\` | **Read-only router.** Unified entry for status/tree/diff views. |
|
|
107
188
|
| \`/cc-learn\` | **Cross-cutting.** Capture or review project knowledge (append-only JSONL). |
|
|
189
|
+
| \`/cc-status\` | **Read-only.** Visual snapshot with progress bar, gate delta, and delegations. |
|
|
190
|
+
| \`/cc-tree\` | **Read-only.** Deep flow tree for stages, artifacts, and stale markers. |
|
|
191
|
+
| \`/cc-diff\` | **Delta map.** Compare current flow-state with saved baseline snapshot. |
|
|
192
|
+
| \`/cc-ops\` | **Operations router.** Unified entry for feature/tdd-log/retro/archive/rewind actions. |
|
|
193
|
+
| \`/cc-feature\` | **Workspace.** Manage active feature snapshots for parallel tracks. |
|
|
194
|
+
| \`/cc-tdd-log\` | **Evidence.** Record RED/GREEN/REFACTOR cycle events for enforcement. |
|
|
195
|
+
| \`/cc-retro\` | **Learning gate.** Mandatory retrospective before archive after ship. |
|
|
196
|
+
| \`/cc-archive\` | **Run finalization.** Archive active flow into \`.cclaw/runs/\` and reset runtime. |
|
|
197
|
+
| \`/cc-rewind\` | **Recovery.** Rewind flow to an earlier stage and invalidate downstream work. |
|
|
198
|
+
| \`/cc-rewind-ack\` | **Recovery.** Clear stale-stage markers after redo. |
|
|
108
199
|
|
|
109
200
|
**Stage order:** brainstorm > scope > design > spec > plan > tdd > review > ship.
|
|
110
201
|
\`/cc-next\` loads the right stage skill automatically. Gates must pass before handoff.
|
|
@@ -213,9 +304,9 @@ export async function syncHarnessShims(projectRoot, harnesses) {
|
|
|
213
304
|
const commandDir = path.join(projectRoot, adapter.commandDir);
|
|
214
305
|
await ensureDir(commandDir);
|
|
215
306
|
await writeFileSafe(path.join(commandDir, "cc.md"), utilityShimContent(harness, "cc", "flow-start", "start.md"));
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
307
|
+
for (const shim of UTILITY_SHIMS) {
|
|
308
|
+
await writeFileSafe(path.join(commandDir, shim.fileName), utilityShimContent(harness, shim.command, shim.skillFolder, shim.commandFile));
|
|
309
|
+
}
|
|
219
310
|
}
|
|
220
311
|
await syncAgentFiles(projectRoot);
|
|
221
312
|
await syncAgentsMd(projectRoot, harnesses);
|