cclaw-cli 0.5.16 → 0.6.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.d.ts +13 -0
- package/dist/artifact-linter.js +182 -13
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +18 -4
- package/dist/config.d.ts +2 -2
- package/dist/config.js +19 -5
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +3 -2
- package/dist/content/agents.js +2 -2
- package/dist/content/examples.js +71 -62
- package/dist/content/hooks.d.ts +1 -0
- package/dist/content/hooks.js +145 -0
- package/dist/content/learnings.js +25 -5
- package/dist/content/meta-skill.js +12 -0
- package/dist/content/next-command.js +8 -0
- package/dist/content/observe.js +18 -0
- package/dist/content/session-hooks.js +1 -1
- package/dist/content/stage-schema.js +12 -2
- package/dist/content/status-command.d.ts +9 -0
- package/dist/content/status-command.js +132 -0
- package/dist/content/templates.js +18 -19
- package/dist/content/utility-skills.d.ts +6 -2
- package/dist/content/utility-skills.js +431 -3
- package/dist/delegation.d.ts +6 -0
- package/dist/delegation.js +12 -4
- package/dist/doctor.js +37 -1
- package/dist/flow-state.d.ts +16 -4
- package/dist/flow-state.js +50 -11
- package/dist/gate-evidence.d.ts +14 -0
- package/dist/gate-evidence.js +65 -3
- package/dist/harness-adapters.js +1 -0
- package/dist/install.d.ts +2 -1
- package/dist/install.js +107 -6
- package/dist/runs.d.ts +13 -1
- package/dist/runs.js +73 -7
- package/dist/types.d.ts +13 -0
- package/dist/types.js +13 -0
- package/package.json +1 -1
package/dist/flow-state.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { COMMAND_FILE_ORDER } from "./constants.js";
|
|
2
2
|
import { buildTransitionRules, orderedStageSchemas, stageGateIds } from "./content/stage-schema.js";
|
|
3
|
+
import { FLOW_STAGES, FLOW_TRACKS, TRACK_STAGES } from "./types.js";
|
|
3
4
|
export const TRANSITION_RULES = buildTransitionRules();
|
|
4
|
-
export function
|
|
5
|
+
export function isFlowTrack(value) {
|
|
6
|
+
return typeof value === "string" && FLOW_TRACKS.includes(value);
|
|
7
|
+
}
|
|
8
|
+
export function trackStages(track) {
|
|
9
|
+
return [...TRACK_STAGES[track]];
|
|
10
|
+
}
|
|
11
|
+
export function skippedStagesForTrack(track) {
|
|
12
|
+
const inTrack = new Set(TRACK_STAGES[track]);
|
|
13
|
+
return FLOW_STAGES.filter((stage) => !inTrack.has(stage));
|
|
14
|
+
}
|
|
15
|
+
export function firstStageForTrack(track) {
|
|
16
|
+
const stages = TRACK_STAGES[track];
|
|
17
|
+
return stages[0] ?? "brainstorm";
|
|
18
|
+
}
|
|
19
|
+
export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTrack) {
|
|
20
|
+
const options = typeof activeRunIdOrOptions === "string"
|
|
21
|
+
? { activeRunId: activeRunIdOrOptions, track: maybeTrack }
|
|
22
|
+
: activeRunIdOrOptions;
|
|
23
|
+
const activeRunId = options.activeRunId ?? "active";
|
|
24
|
+
const track = options.track ?? "standard";
|
|
25
|
+
const skippedStages = skippedStagesForTrack(track);
|
|
5
26
|
const stageGateCatalog = {};
|
|
6
27
|
for (const schema of orderedStageSchemas()) {
|
|
7
28
|
stageGateCatalog[schema.stage] = {
|
|
@@ -12,10 +33,12 @@ export function createInitialFlowState(activeRunId = "active") {
|
|
|
12
33
|
}
|
|
13
34
|
return {
|
|
14
35
|
activeRunId,
|
|
15
|
-
currentStage:
|
|
36
|
+
currentStage: firstStageForTrack(track),
|
|
16
37
|
completedStages: [],
|
|
17
38
|
guardEvidence: {},
|
|
18
|
-
stageGateCatalog
|
|
39
|
+
stageGateCatalog,
|
|
40
|
+
track,
|
|
41
|
+
skippedStages
|
|
19
42
|
};
|
|
20
43
|
}
|
|
21
44
|
export function canTransition(from, to) {
|
|
@@ -25,17 +48,33 @@ export function getTransitionGuards(from, to) {
|
|
|
25
48
|
const match = TRANSITION_RULES.find((rule) => rule.from === from && rule.to === to);
|
|
26
49
|
return match ? [...match.guards] : [];
|
|
27
50
|
}
|
|
28
|
-
export function nextStage(stage) {
|
|
29
|
-
const
|
|
30
|
-
|
|
51
|
+
export function nextStage(stage, track = "standard") {
|
|
52
|
+
const ordered = TRACK_STAGES[track];
|
|
53
|
+
const index = ordered.indexOf(stage);
|
|
54
|
+
if (index < 0) {
|
|
55
|
+
const fallback = COMMAND_FILE_ORDER.indexOf(stage);
|
|
56
|
+
if (fallback < 0 || fallback === COMMAND_FILE_ORDER.length - 1) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return COMMAND_FILE_ORDER[fallback + 1];
|
|
60
|
+
}
|
|
61
|
+
if (index === ordered.length - 1) {
|
|
31
62
|
return null;
|
|
32
63
|
}
|
|
33
|
-
return
|
|
64
|
+
return ordered[index + 1];
|
|
34
65
|
}
|
|
35
|
-
export function previousStage(stage) {
|
|
36
|
-
const
|
|
37
|
-
|
|
66
|
+
export function previousStage(stage, track = "standard") {
|
|
67
|
+
const ordered = TRACK_STAGES[track];
|
|
68
|
+
const index = ordered.indexOf(stage);
|
|
69
|
+
if (index === 0) {
|
|
38
70
|
return null;
|
|
39
71
|
}
|
|
40
|
-
|
|
72
|
+
if (index < 0) {
|
|
73
|
+
const fallback = COMMAND_FILE_ORDER.indexOf(stage);
|
|
74
|
+
if (fallback <= 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return COMMAND_FILE_ORDER[fallback - 1];
|
|
78
|
+
}
|
|
79
|
+
return ordered[index - 1];
|
|
41
80
|
}
|
package/dist/gate-evidence.d.ts
CHANGED
|
@@ -7,8 +7,22 @@ export interface GateEvidenceCheckResult {
|
|
|
7
7
|
requiredCount: number;
|
|
8
8
|
passedCount: number;
|
|
9
9
|
blockedCount: number;
|
|
10
|
+
/** True only when every required gate for the stage is in `passed` and none are `blocked`. */
|
|
11
|
+
complete: boolean;
|
|
12
|
+
/** Required gate ids that are neither passed nor blocked. */
|
|
13
|
+
missingRequired: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface CompletedStagesClosureResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
issues: string[];
|
|
18
|
+
openStages: Array<{
|
|
19
|
+
stage: FlowStage;
|
|
20
|
+
missingRequired: string[];
|
|
21
|
+
blocked: string[];
|
|
22
|
+
}>;
|
|
10
23
|
}
|
|
11
24
|
export declare function verifyCurrentStageGateEvidence(projectRoot: string, flowState: FlowState): Promise<GateEvidenceCheckResult>;
|
|
25
|
+
export declare function verifyCompletedStagesGateClosure(flowState: FlowState): CompletedStagesClosureResult;
|
|
12
26
|
export interface GateReconciliationResult {
|
|
13
27
|
stage: FlowStage;
|
|
14
28
|
changed: boolean;
|
package/dist/gate-evidence.js
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { checkReviewVerdictConsistency, lintArtifact, validateReviewArmy } from "./artifact-linter.js";
|
|
4
|
+
import { RUNTIME_ROOT } from "./constants.js";
|
|
2
5
|
import { stageSchema } from "./content/stage-schema.js";
|
|
6
|
+
import { exists } from "./fs-utils.js";
|
|
3
7
|
import { readFlowState, writeFlowState } from "./runs.js";
|
|
8
|
+
async function currentStageArtifactExists(projectRoot, stage) {
|
|
9
|
+
const artifactFile = stageSchema(stage).artifactFile;
|
|
10
|
+
const candidates = [
|
|
11
|
+
path.join(projectRoot, RUNTIME_ROOT, "artifacts", artifactFile),
|
|
12
|
+
path.join(projectRoot, artifactFile)
|
|
13
|
+
];
|
|
14
|
+
for (const candidate of candidates) {
|
|
15
|
+
if (await exists(candidate))
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
// Artifact-linter also accepts the file under current working directory fallback; stat once more.
|
|
19
|
+
try {
|
|
20
|
+
await fs.access(path.join(projectRoot, artifactFile));
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
4
27
|
function unique(values) {
|
|
5
28
|
return [...new Set(values)];
|
|
6
29
|
}
|
|
@@ -44,7 +67,8 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
44
67
|
issues.push(`blocked gate "${gateId}" is not defined for stage "${stage}".`);
|
|
45
68
|
}
|
|
46
69
|
}
|
|
47
|
-
const
|
|
70
|
+
const artifactPresent = await currentStageArtifactExists(projectRoot, stage);
|
|
71
|
+
const shouldValidateArtifact = artifactPresent || catalog.passed.length > 0 || flowState.completedStages.includes(stage);
|
|
48
72
|
if (shouldValidateArtifact) {
|
|
49
73
|
const lint = await lintArtifact(projectRoot, stage);
|
|
50
74
|
if (!lint.passed) {
|
|
@@ -60,6 +84,21 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
60
84
|
if (!reviewArmy.valid) {
|
|
61
85
|
issues.push(`review-army validation failed: ${reviewArmy.errors.join("; ")}`);
|
|
62
86
|
}
|
|
87
|
+
const verdictConsistency = await checkReviewVerdictConsistency(projectRoot);
|
|
88
|
+
if (!verdictConsistency.ok) {
|
|
89
|
+
issues.push(`review verdict inconsistency: ${verdictConsistency.errors.join("; ")}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const passedSet = new Set(catalog.passed);
|
|
94
|
+
const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
|
|
95
|
+
const complete = missingRequired.length === 0 && catalog.blocked.length === 0;
|
|
96
|
+
if (flowState.completedStages.includes(stage) && !complete) {
|
|
97
|
+
if (missingRequired.length > 0) {
|
|
98
|
+
issues.push(`stage "${stage}" is marked completed but required gates are not passed: ${missingRequired.join(", ")}.`);
|
|
99
|
+
}
|
|
100
|
+
if (catalog.blocked.length > 0) {
|
|
101
|
+
issues.push(`stage "${stage}" is marked completed but has blocked gates: ${catalog.blocked.join(", ")}.`);
|
|
63
102
|
}
|
|
64
103
|
}
|
|
65
104
|
return {
|
|
@@ -68,9 +107,32 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
68
107
|
issues,
|
|
69
108
|
requiredCount: required.length,
|
|
70
109
|
passedCount: catalog.passed.length,
|
|
71
|
-
blockedCount: catalog.blocked.length
|
|
110
|
+
blockedCount: catalog.blocked.length,
|
|
111
|
+
complete,
|
|
112
|
+
missingRequired
|
|
72
113
|
};
|
|
73
114
|
}
|
|
115
|
+
export function verifyCompletedStagesGateClosure(flowState) {
|
|
116
|
+
const issues = [];
|
|
117
|
+
const openStages = [];
|
|
118
|
+
for (const stage of flowState.completedStages) {
|
|
119
|
+
const schema = stageSchema(stage);
|
|
120
|
+
const catalog = flowState.stageGateCatalog[stage];
|
|
121
|
+
const required = schema.requiredGates.map((gate) => gate.id);
|
|
122
|
+
const passedSet = new Set(catalog.passed);
|
|
123
|
+
const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
|
|
124
|
+
if (missingRequired.length > 0 || catalog.blocked.length > 0) {
|
|
125
|
+
openStages.push({ stage, missingRequired, blocked: [...catalog.blocked] });
|
|
126
|
+
if (missingRequired.length > 0) {
|
|
127
|
+
issues.push(`completed stage "${stage}" has unpassed required gates: ${missingRequired.join(", ")}.`);
|
|
128
|
+
}
|
|
129
|
+
if (catalog.blocked.length > 0) {
|
|
130
|
+
issues.push(`completed stage "${stage}" still has blocked gates: ${catalog.blocked.join(", ")}.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { ok: openStages.length === 0, issues, openStages };
|
|
135
|
+
}
|
|
74
136
|
export function reconcileCurrentStageGateCatalog(flowState) {
|
|
75
137
|
const stage = flowState.currentStage;
|
|
76
138
|
const required = stageSchema(stage).requiredGates.map((gate) => gate.id);
|
package/dist/harness-adapters.js
CHANGED
|
@@ -124,6 +124,7 @@ export async function syncHarnessShims(projectRoot, harnesses) {
|
|
|
124
124
|
await writeFileSafe(path.join(commandDir, "cc.md"), utilityShimContent(harness, "cc", "flow-start", "start.md"));
|
|
125
125
|
await writeFileSafe(path.join(commandDir, "cc-next.md"), utilityShimContent(harness, "next", "flow-next-step", "next.md"));
|
|
126
126
|
await writeFileSafe(path.join(commandDir, "cc-learn.md"), utilityShimContent(harness, "learn", "learnings", "learn.md"));
|
|
127
|
+
await writeFileSafe(path.join(commandDir, "cc-status.md"), utilityShimContent(harness, "status", "flow-status", "status.md"));
|
|
127
128
|
}
|
|
128
129
|
await syncAgentFiles(projectRoot);
|
|
129
130
|
await syncAgentsMd(projectRoot);
|
package/dist/install.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { HarnessId } from "./types.js";
|
|
1
|
+
import type { FlowTrack, HarnessId } from "./types.js";
|
|
2
2
|
export interface InitOptions {
|
|
3
3
|
projectRoot: string;
|
|
4
4
|
harnesses?: HarnessId[];
|
|
5
|
+
track?: FlowTrack;
|
|
5
6
|
}
|
|
6
7
|
export declare function initCclaw(options: InitOptions): Promise<void>;
|
|
7
8
|
export declare function syncCclaw(projectRoot: string): Promise<void>;
|
package/dist/install.js
CHANGED
|
@@ -9,9 +9,10 @@ import { contextModeFiles, createInitialContextModeState } from "./content/conte
|
|
|
9
9
|
import { learnSkillMarkdown, learnCommandContract } from "./content/learnings.js";
|
|
10
10
|
import { nextCommandContract, nextCommandSkillMarkdown } from "./content/next-command.js";
|
|
11
11
|
import { startCommandContract, startCommandSkillMarkdown } from "./content/start-command.js";
|
|
12
|
+
import { statusCommandContract, statusCommandSkillMarkdown } from "./content/status-command.js";
|
|
12
13
|
import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
|
|
13
14
|
import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
|
|
14
|
-
import { sessionStartScript, stopCheckpointScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
|
|
15
|
+
import { sessionStartScript, stopCheckpointScript, preCompactScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
|
|
15
16
|
import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
|
|
16
17
|
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
17
18
|
import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
|
|
@@ -173,6 +174,7 @@ async function writeSkills(projectRoot) {
|
|
|
173
174
|
await writeFileSafe(runtimePath(projectRoot, "skills", "learnings", "SKILL.md"), learnSkillMarkdown());
|
|
174
175
|
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-next-step", "SKILL.md"), nextCommandSkillMarkdown());
|
|
175
176
|
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-start", "SKILL.md"), startCommandSkillMarkdown());
|
|
177
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-status", "SKILL.md"), statusCommandSkillMarkdown());
|
|
176
178
|
await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
|
|
177
179
|
await writeFileSafe(runtimePath(projectRoot, "skills", "parallel-dispatch", "SKILL.md"), parallelAgentsSkill());
|
|
178
180
|
await writeFileSafe(runtimePath(projectRoot, "skills", "session", "SKILL.md"), sessionHooksSkillMarkdown());
|
|
@@ -186,6 +188,7 @@ async function writeUtilityCommands(projectRoot) {
|
|
|
186
188
|
await writeFileSafe(runtimePath(projectRoot, "commands", "learn.md"), learnCommandContract());
|
|
187
189
|
await writeFileSafe(runtimePath(projectRoot, "commands", "next.md"), nextCommandContract());
|
|
188
190
|
await writeFileSafe(runtimePath(projectRoot, "commands", "start.md"), startCommandContract());
|
|
191
|
+
await writeFileSafe(runtimePath(projectRoot, "commands", "status.md"), statusCommandContract());
|
|
189
192
|
}
|
|
190
193
|
function toObject(value) {
|
|
191
194
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -482,6 +485,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
482
485
|
await ensureDir(hooksDir);
|
|
483
486
|
await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
|
|
484
487
|
await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
|
|
488
|
+
await writeFileSafe(path.join(hooksDir, "pre-compact.sh"), preCompactScript());
|
|
485
489
|
await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript({
|
|
486
490
|
strictMode: config.promptGuardMode === "strict"
|
|
487
491
|
}));
|
|
@@ -493,6 +497,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
493
497
|
for (const script of [
|
|
494
498
|
"session-start.sh",
|
|
495
499
|
"stop-checkpoint.sh",
|
|
500
|
+
"pre-compact.sh",
|
|
496
501
|
"prompt-guard.sh",
|
|
497
502
|
"workflow-guard.sh",
|
|
498
503
|
"context-monitor.sh",
|
|
@@ -542,6 +547,101 @@ async function ensureKnowledgeStore(projectRoot) {
|
|
|
542
547
|
await writeFileSafe(storePath, "# Project Knowledge\n\n");
|
|
543
548
|
}
|
|
544
549
|
}
|
|
550
|
+
async function ensureCustomSkillsScaffold(projectRoot) {
|
|
551
|
+
const customDir = runtimePath(projectRoot, "custom-skills");
|
|
552
|
+
await ensureDir(customDir);
|
|
553
|
+
const readmePath = path.join(customDir, "README.md");
|
|
554
|
+
if (!(await exists(readmePath))) {
|
|
555
|
+
await writeFileSafe(readmePath, CUSTOM_SKILLS_README);
|
|
556
|
+
}
|
|
557
|
+
const examplePath = path.join(customDir, "example", "SKILL.md");
|
|
558
|
+
if (!(await exists(examplePath))) {
|
|
559
|
+
await writeFileSafe(examplePath, CUSTOM_SKILLS_EXAMPLE);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const CUSTOM_SKILLS_README = `# Custom Skills (sync-safe)
|
|
563
|
+
|
|
564
|
+
This directory is **never overwritten** by \`cclaw sync\` or \`cclaw upgrade\`. Use it
|
|
565
|
+
to add project-specific skills that complement the managed skills under
|
|
566
|
+
\`.cclaw/skills/\`.
|
|
567
|
+
|
|
568
|
+
## When to add a custom skill
|
|
569
|
+
|
|
570
|
+
- A repeatable lens specific to **this project** (e.g. "billing-domain", "kafka-message-contracts").
|
|
571
|
+
- A team convention you want every agent session to load.
|
|
572
|
+
- A domain checklist that does not generalize to other projects.
|
|
573
|
+
|
|
574
|
+
If the skill is general (security, performance, accessibility, etc.) prefer
|
|
575
|
+
contributing it upstream instead — the managed skills receive maintenance.
|
|
576
|
+
|
|
577
|
+
## File format
|
|
578
|
+
|
|
579
|
+
Each skill lives at \`.cclaw/custom-skills/<folder>/SKILL.md\` with frontmatter:
|
|
580
|
+
|
|
581
|
+
\`\`\`markdown
|
|
582
|
+
---
|
|
583
|
+
name: <kebab-case-skill-name>
|
|
584
|
+
description: "One sentence describing when this skill applies. Triggers semantic routing."
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
# <Skill title>
|
|
588
|
+
|
|
589
|
+
## When to use
|
|
590
|
+
- ...
|
|
591
|
+
|
|
592
|
+
## HARD-GATE (optional)
|
|
593
|
+
A non-skippable rule, if any. Phrase it as a refusal, not a recommendation.
|
|
594
|
+
|
|
595
|
+
## Algorithm / checklist
|
|
596
|
+
1. ...
|
|
597
|
+
2. ...
|
|
598
|
+
|
|
599
|
+
## Anti-patterns
|
|
600
|
+
- ...
|
|
601
|
+
\`\`\`
|
|
602
|
+
|
|
603
|
+
## Routing
|
|
604
|
+
|
|
605
|
+
Custom skills are surfaced via the \`using-cclaw\` meta-skill at session start.
|
|
606
|
+
Mention the skill name in your prompt or let the agent semantic-route to it
|
|
607
|
+
based on the description.
|
|
608
|
+
|
|
609
|
+
## Removing or replacing
|
|
610
|
+
|
|
611
|
+
Custom skills are user-owned. Delete or edit them at any time — \`cclaw sync\`
|
|
612
|
+
will not touch them.
|
|
613
|
+
`;
|
|
614
|
+
const CUSTOM_SKILLS_EXAMPLE = `---
|
|
615
|
+
name: example-custom-skill
|
|
616
|
+
description: "Replace this with a one-sentence description that triggers when the skill should be used. Delete or rename this folder when you add a real skill."
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
# Example Custom Skill
|
|
620
|
+
|
|
621
|
+
This is a placeholder. Use it as a starting template, then delete or rename
|
|
622
|
+
the \`example/\` folder.
|
|
623
|
+
|
|
624
|
+
## When to use
|
|
625
|
+
|
|
626
|
+
- A real, repeatable situation in **this** project that needs a consistent lens.
|
|
627
|
+
|
|
628
|
+
## HARD-GATE (optional)
|
|
629
|
+
|
|
630
|
+
Drop this section if no hard rule applies. Keep it crisp:
|
|
631
|
+
|
|
632
|
+
> Do not <X> while <Y>.
|
|
633
|
+
|
|
634
|
+
## Algorithm
|
|
635
|
+
|
|
636
|
+
1. Step one — observable, file:line evidence required.
|
|
637
|
+
2. Step two — produce a named artifact, not a vibe.
|
|
638
|
+
3. Step three — escalate / hand off / record knowledge entry.
|
|
639
|
+
|
|
640
|
+
## Anti-patterns
|
|
641
|
+
|
|
642
|
+
- Treating this skill as advisory when the situation matches the trigger.
|
|
643
|
+
- Loading this skill when the situation clearly does not match (context bloat).
|
|
644
|
+
`;
|
|
545
645
|
async function ensureSessionStateFiles(projectRoot) {
|
|
546
646
|
const stateDir = runtimePath(projectRoot, "state");
|
|
547
647
|
await ensureDir(stateDir);
|
|
@@ -623,12 +723,12 @@ async function syncDisabledHarnessArtifacts(projectRoot, harnesses) {
|
|
|
623
723
|
await removeManagedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
|
|
624
724
|
}
|
|
625
725
|
}
|
|
626
|
-
async function writeState(projectRoot, forceReset = false) {
|
|
726
|
+
async function writeState(projectRoot, config, forceReset = false) {
|
|
627
727
|
const statePath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
628
728
|
if (!forceReset && (await exists(statePath))) {
|
|
629
729
|
return;
|
|
630
730
|
}
|
|
631
|
-
const state = createInitialFlowState();
|
|
731
|
+
const state = createInitialFlowState("active", config.defaultTrack ?? "standard");
|
|
632
732
|
await writeFileSafe(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
633
733
|
}
|
|
634
734
|
async function writeAdapterManifest(projectRoot, harnesses) {
|
|
@@ -740,11 +840,12 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
|
|
|
740
840
|
await writeContextModes(projectRoot);
|
|
741
841
|
await writeArtifactTemplates(projectRoot);
|
|
742
842
|
await writeRulebook(projectRoot);
|
|
743
|
-
await writeState(projectRoot, forceStateReset);
|
|
843
|
+
await writeState(projectRoot, config, forceStateReset);
|
|
744
844
|
await ensureRunSystem(projectRoot, { createIfMissing: false });
|
|
745
845
|
await ensureSessionStateFiles(projectRoot);
|
|
746
846
|
await writeAdapterManifest(projectRoot, harnesses);
|
|
747
847
|
await ensureKnowledgeStore(projectRoot);
|
|
848
|
+
await ensureCustomSkillsScaffold(projectRoot);
|
|
748
849
|
await writeHooks(projectRoot, config);
|
|
749
850
|
await syncDisabledHarnessArtifacts(projectRoot, harnesses);
|
|
750
851
|
await syncManagedGitHooks(projectRoot, config);
|
|
@@ -753,7 +854,7 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
|
|
|
753
854
|
await ensureGitignore(projectRoot);
|
|
754
855
|
}
|
|
755
856
|
export async function initCclaw(options) {
|
|
756
|
-
const config = createDefaultConfig(options.harnesses);
|
|
857
|
+
const config = createDefaultConfig(options.harnesses, options.track);
|
|
757
858
|
await writeConfig(options.projectRoot, config);
|
|
758
859
|
await materializeRuntime(options.projectRoot, config, true);
|
|
759
860
|
}
|
|
@@ -828,7 +929,7 @@ function stripManagedHookCommands(value) {
|
|
|
828
929
|
}
|
|
829
930
|
function isManagedRuntimeHookCommand(command) {
|
|
830
931
|
const normalized = command.trim().replace(/\s+/gu, " ");
|
|
831
|
-
return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized);
|
|
932
|
+
return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized);
|
|
832
933
|
}
|
|
833
934
|
async function removeManagedHookEntries(hookFilePath) {
|
|
834
935
|
if (!(await exists(hookFilePath)))
|
package/dist/runs.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { type FlowState } from "./flow-state.js";
|
|
2
2
|
import type { FlowStage } from "./types.js";
|
|
3
|
+
export declare class InvalidStageTransitionError extends Error {
|
|
4
|
+
readonly from: FlowStage;
|
|
5
|
+
readonly to: FlowStage;
|
|
6
|
+
constructor(from: FlowStage, to: FlowStage, message: string);
|
|
7
|
+
}
|
|
8
|
+
export interface WriteFlowStateOptions {
|
|
9
|
+
/**
|
|
10
|
+
* When true, skip prior-state validation. Used for run archival, initial
|
|
11
|
+
* bootstrap, or explicit recovery; never set from normal stage handlers.
|
|
12
|
+
*/
|
|
13
|
+
allowReset?: boolean;
|
|
14
|
+
}
|
|
3
15
|
export interface CclawRunMeta {
|
|
4
16
|
id: string;
|
|
5
17
|
title: string;
|
|
@@ -32,7 +44,7 @@ export declare class CorruptFlowStateError extends Error {
|
|
|
32
44
|
constructor(statePath: string, quarantinedPath: string, cause: unknown);
|
|
33
45
|
}
|
|
34
46
|
export declare function readFlowState(projectRoot: string): Promise<FlowState>;
|
|
35
|
-
export declare function writeFlowState(projectRoot: string, state: FlowState): Promise<void>;
|
|
47
|
+
export declare function writeFlowState(projectRoot: string, state: FlowState, options?: WriteFlowStateOptions): Promise<void>;
|
|
36
48
|
export declare function ensureRunSystem(projectRoot: string, _options?: EnsureRunSystemOptions): Promise<FlowState>;
|
|
37
49
|
export declare function listRuns(projectRoot: string): Promise<CclawRunMeta[]>;
|
|
38
50
|
export declare function archiveRun(projectRoot: string, featureName?: string): Promise<ArchiveRunResult>;
|
package/dist/runs.js
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { COMMAND_FILE_ORDER, RUNTIME_ROOT } from "./constants.js";
|
|
4
|
-
import { createInitialFlowState } from "./flow-state.js";
|
|
4
|
+
import { canTransition, createInitialFlowState, isFlowTrack, skippedStagesForTrack } from "./flow-state.js";
|
|
5
5
|
import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
|
|
6
|
+
export class InvalidStageTransitionError extends Error {
|
|
7
|
+
from;
|
|
8
|
+
to;
|
|
9
|
+
constructor(from, to, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.from = from;
|
|
12
|
+
this.to = to;
|
|
13
|
+
this.name = "InvalidStageTransitionError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function validateFlowTransition(prev, next) {
|
|
17
|
+
if (prev.activeRunId !== next.activeRunId) {
|
|
18
|
+
// New run — only reset paths may change the runId, but those set allowReset.
|
|
19
|
+
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change activeRunId from "${prev.activeRunId}" to "${next.activeRunId}" without allowReset.`);
|
|
20
|
+
}
|
|
21
|
+
for (const completed of prev.completedStages) {
|
|
22
|
+
if (!next.completedStages.includes(completed)) {
|
|
23
|
+
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `completedStages must be monotonic: stage "${completed}" was previously completed but is missing from the new state.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (prev.currentStage === next.currentStage) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!canTransition(prev.currentStage, next.currentStage)) {
|
|
30
|
+
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `no transition rule allows "${prev.currentStage}" -> "${next.currentStage}". Use /cc-next to advance stages or archive the run to reset.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
6
33
|
const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
|
|
7
34
|
const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
|
|
8
35
|
const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
|
|
@@ -129,8 +156,27 @@ function sanitizeStageGateCatalog(value, fallback) {
|
|
|
129
156
|
}
|
|
130
157
|
return next;
|
|
131
158
|
}
|
|
159
|
+
function coerceTrack(value) {
|
|
160
|
+
return isFlowTrack(value) ? value : "standard";
|
|
161
|
+
}
|
|
162
|
+
function sanitizeSkippedStages(value, track) {
|
|
163
|
+
const trackDefault = skippedStagesForTrack(track);
|
|
164
|
+
if (!Array.isArray(value)) {
|
|
165
|
+
return trackDefault;
|
|
166
|
+
}
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const raw of value) {
|
|
170
|
+
if (isFlowStage(raw) && !seen.has(raw)) {
|
|
171
|
+
seen.add(raw);
|
|
172
|
+
out.push(raw);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out.length > 0 ? out : trackDefault;
|
|
176
|
+
}
|
|
132
177
|
function coerceFlowState(parsed) {
|
|
133
|
-
const
|
|
178
|
+
const track = coerceTrack(parsed.track);
|
|
179
|
+
const next = createInitialFlowState("active", track);
|
|
134
180
|
const activeRunIdRaw = parsed.activeRunId;
|
|
135
181
|
const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
|
|
136
182
|
? activeRunIdRaw.trim()
|
|
@@ -140,7 +186,9 @@ function coerceFlowState(parsed) {
|
|
|
140
186
|
currentStage: isFlowStage(parsed.currentStage) ? parsed.currentStage : next.currentStage,
|
|
141
187
|
completedStages: sanitizeCompletedStages(parsed.completedStages),
|
|
142
188
|
guardEvidence: sanitizeGuardEvidence(parsed.guardEvidence),
|
|
143
|
-
stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog)
|
|
189
|
+
stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog),
|
|
190
|
+
track,
|
|
191
|
+
skippedStages: sanitizeSkippedStages(parsed.skippedStages, track)
|
|
144
192
|
};
|
|
145
193
|
}
|
|
146
194
|
function toArchiveDate(date = new Date()) {
|
|
@@ -251,10 +299,28 @@ export async function readFlowState(projectRoot) {
|
|
|
251
299
|
}
|
|
252
300
|
return coerceFlowState(parsed);
|
|
253
301
|
}
|
|
254
|
-
export async function writeFlowState(projectRoot, state) {
|
|
302
|
+
export async function writeFlowState(projectRoot, state, options = {}) {
|
|
255
303
|
await withDirectoryLock(flowStateLockPath(projectRoot), async () => {
|
|
304
|
+
const statePath = flowStatePath(projectRoot);
|
|
305
|
+
if (!options.allowReset && (await exists(statePath))) {
|
|
306
|
+
try {
|
|
307
|
+
const raw = await fs.readFile(statePath, "utf8");
|
|
308
|
+
const parsed = JSON.parse(raw);
|
|
309
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
310
|
+
const prev = coerceFlowState(parsed);
|
|
311
|
+
validateFlowTransition(prev, state);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
if (err instanceof InvalidStageTransitionError) {
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
// A corrupt prior file is surfaced by readFlowState elsewhere; don't
|
|
319
|
+
// block a legitimate write attempt on parse errors here.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
256
322
|
const safe = coerceFlowState({ ...state });
|
|
257
|
-
await writeFileSafe(
|
|
323
|
+
await writeFileSafe(statePath, `${JSON.stringify(safe, null, 2)}\n`);
|
|
258
324
|
});
|
|
259
325
|
}
|
|
260
326
|
export async function ensureRunSystem(projectRoot, _options = {}) {
|
|
@@ -263,7 +329,7 @@ export async function ensureRunSystem(projectRoot, _options = {}) {
|
|
|
263
329
|
const statePath = flowStatePath(projectRoot);
|
|
264
330
|
const state = await readFlowState(projectRoot);
|
|
265
331
|
if (!(await exists(statePath))) {
|
|
266
|
-
await writeFlowState(projectRoot, state);
|
|
332
|
+
await writeFlowState(projectRoot, state, { allowReset: true });
|
|
267
333
|
}
|
|
268
334
|
return state;
|
|
269
335
|
}
|
|
@@ -315,7 +381,7 @@ export async function archiveRun(projectRoot, featureName) {
|
|
|
315
381
|
const archiveStatePath = path.join(archivePath, "state");
|
|
316
382
|
const snapshottedStateFiles = await snapshotStateDirectory(projectRoot, archiveStatePath);
|
|
317
383
|
const resetState = createInitialFlowState();
|
|
318
|
-
await writeFlowState(projectRoot, resetState);
|
|
384
|
+
await writeFlowState(projectRoot, resetState, { allowReset: true });
|
|
319
385
|
const archivedAt = new Date().toISOString();
|
|
320
386
|
const manifest = {
|
|
321
387
|
version: 1,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
export declare const FLOW_STAGES: readonly ["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"];
|
|
2
2
|
export type FlowStage = (typeof FLOW_STAGES)[number];
|
|
3
|
+
export declare const FLOW_TRACKS: readonly ["quick", "standard"];
|
|
4
|
+
export type FlowTrack = (typeof FLOW_TRACKS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* Ordered stages that make up each flow track.
|
|
7
|
+
*
|
|
8
|
+
* - `standard` runs the full 8-stage pipeline (default — same as before tracks existed).
|
|
9
|
+
* - `quick` skips the upstream product stages (brainstorm/scope/design/plan) for
|
|
10
|
+
* small bug fixes or single-purpose changes where the spec is already known.
|
|
11
|
+
* It still keeps the non-negotiable safety gates: spec → tdd → review → ship.
|
|
12
|
+
*/
|
|
13
|
+
export declare const TRACK_STAGES: Record<FlowTrack, readonly FlowStage[]>;
|
|
3
14
|
export declare const HARNESS_IDS: readonly ["claude", "cursor", "opencode", "codex"];
|
|
4
15
|
export type HarnessId = (typeof HARNESS_IDS)[number];
|
|
5
16
|
export interface VibyConfig {
|
|
@@ -12,6 +23,8 @@ export interface VibyConfig {
|
|
|
12
23
|
promptGuardMode?: "advisory" | "strict";
|
|
13
24
|
/** When true, cclaw installs managed git pre-commit/pre-push wrappers. */
|
|
14
25
|
gitHookGuards?: boolean;
|
|
26
|
+
/** Default flow track for new runs (quick = shortened path, standard = full pipeline). */
|
|
27
|
+
defaultTrack?: FlowTrack;
|
|
15
28
|
}
|
|
16
29
|
export interface TransitionRule {
|
|
17
30
|
from: FlowStage;
|
package/dist/types.js
CHANGED
|
@@ -8,4 +8,17 @@ export const FLOW_STAGES = [
|
|
|
8
8
|
"review",
|
|
9
9
|
"ship"
|
|
10
10
|
];
|
|
11
|
+
export const FLOW_TRACKS = ["quick", "standard"];
|
|
12
|
+
/**
|
|
13
|
+
* Ordered stages that make up each flow track.
|
|
14
|
+
*
|
|
15
|
+
* - `standard` runs the full 8-stage pipeline (default — same as before tracks existed).
|
|
16
|
+
* - `quick` skips the upstream product stages (brainstorm/scope/design/plan) for
|
|
17
|
+
* small bug fixes or single-purpose changes where the spec is already known.
|
|
18
|
+
* It still keeps the non-negotiable safety gates: spec → tdd → review → ship.
|
|
19
|
+
*/
|
|
20
|
+
export const TRACK_STAGES = {
|
|
21
|
+
standard: FLOW_STAGES,
|
|
22
|
+
quick: ["spec", "tdd", "review", "ship"]
|
|
23
|
+
};
|
|
11
24
|
export const HARNESS_IDS = ["claude", "cursor", "opencode", "codex"];
|