cclaw-cli 0.1.0 → 0.2.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/README.md +28 -0
- package/dist/artifact-linter.d.ts +20 -0
- package/dist/artifact-linter.js +368 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +22 -5
- package/dist/config.d.ts +4 -4
- package/dist/config.js +55 -3
- package/dist/constants.d.ts +4 -4
- package/dist/constants.js +6 -3
- package/dist/content/autoplan.js +51 -4
- package/dist/content/contexts.d.ts +9 -0
- package/dist/content/contexts.js +65 -0
- package/dist/content/hooks.d.ts +6 -2
- package/dist/content/hooks.js +448 -16
- package/dist/content/meta-skill.js +26 -0
- package/dist/content/next-command.d.ts +9 -0
- package/dist/content/next-command.js +138 -0
- package/dist/content/observe.d.ts +5 -1
- package/dist/content/observe.js +506 -24
- package/dist/content/skills.js +126 -0
- package/dist/content/stage-schema.d.ts +7 -0
- package/dist/content/stage-schema.js +70 -12
- package/dist/content/subagents.js +33 -0
- package/dist/content/templates.d.ts +1 -0
- package/dist/content/templates.js +182 -77
- package/dist/content/utility-skills.d.ts +5 -1
- package/dist/content/utility-skills.js +208 -2
- package/dist/delegation.d.ts +21 -0
- package/dist/delegation.js +94 -0
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +274 -29
- package/dist/fs-utils.d.ts +10 -0
- package/dist/fs-utils.js +47 -0
- package/dist/gate-evidence.d.ts +26 -0
- package/dist/gate-evidence.js +157 -0
- package/dist/harness-adapters.js +10 -26
- package/dist/hook-schema.d.ts +6 -0
- package/dist/hook-schema.js +45 -0
- package/dist/hook-schemas/claude-hooks.v1.json +12 -0
- package/dist/hook-schemas/codex-hooks.v1.json +12 -0
- package/dist/hook-schemas/cursor-hooks.v1.json +15 -0
- package/dist/install.js +395 -15
- package/dist/policy.d.ts +5 -1
- package/dist/policy.js +52 -1
- package/dist/runs.js +8 -3
- package/dist/trace-matrix.d.ts +13 -0
- package/dist/trace-matrix.js +182 -0
- package/dist/types.d.ts +11 -1
- package/package.json +2 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { lintArtifact, validateReviewArmy } from "./artifact-linter.js";
|
|
2
|
+
import { stageSchema } from "./content/stage-schema.js";
|
|
3
|
+
import { readFlowState, writeFlowState } from "./runs.js";
|
|
4
|
+
function unique(values) {
|
|
5
|
+
return [...new Set(values)];
|
|
6
|
+
}
|
|
7
|
+
function sameStringArray(a, b) {
|
|
8
|
+
if (a.length !== b.length)
|
|
9
|
+
return false;
|
|
10
|
+
return a.every((value, index) => value === b[index]);
|
|
11
|
+
}
|
|
12
|
+
export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
13
|
+
const stage = flowState.currentStage;
|
|
14
|
+
const schema = stageSchema(stage);
|
|
15
|
+
const catalog = flowState.stageGateCatalog[stage];
|
|
16
|
+
const required = schema.requiredGates.map((gate) => gate.id);
|
|
17
|
+
const requiredSet = new Set(required);
|
|
18
|
+
const issues = [];
|
|
19
|
+
const catalogRequired = unique(catalog.required);
|
|
20
|
+
const missingInCatalog = required.filter((gateId) => !catalogRequired.includes(gateId));
|
|
21
|
+
const unexpectedInCatalog = catalogRequired.filter((gateId) => !requiredSet.has(gateId));
|
|
22
|
+
for (const gateId of missingInCatalog) {
|
|
23
|
+
issues.push(`gate "${gateId}" missing from stageGateCatalog.required for stage "${stage}".`);
|
|
24
|
+
}
|
|
25
|
+
for (const gateId of unexpectedInCatalog) {
|
|
26
|
+
issues.push(`unexpected gate "${gateId}" found in stageGateCatalog.required for stage "${stage}".`);
|
|
27
|
+
}
|
|
28
|
+
const blockedSet = new Set(catalog.blocked);
|
|
29
|
+
for (const gateId of catalog.passed) {
|
|
30
|
+
if (!requiredSet.has(gateId)) {
|
|
31
|
+
issues.push(`passed gate "${gateId}" is not defined for stage "${stage}".`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (blockedSet.has(gateId)) {
|
|
35
|
+
issues.push(`gate "${gateId}" cannot be both passed and blocked.`);
|
|
36
|
+
}
|
|
37
|
+
const evidence = flowState.guardEvidence[gateId];
|
|
38
|
+
if (typeof evidence !== "string" || evidence.trim().length === 0) {
|
|
39
|
+
issues.push(`passed gate "${gateId}" is missing guardEvidence entry.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const gateId of catalog.blocked) {
|
|
43
|
+
if (!requiredSet.has(gateId)) {
|
|
44
|
+
issues.push(`blocked gate "${gateId}" is not defined for stage "${stage}".`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const shouldValidateArtifact = catalog.passed.length > 0 || flowState.completedStages.includes(stage);
|
|
48
|
+
if (shouldValidateArtifact) {
|
|
49
|
+
const lint = await lintArtifact(projectRoot, stage);
|
|
50
|
+
if (!lint.passed) {
|
|
51
|
+
const failedRequired = lint.findings
|
|
52
|
+
.filter((finding) => finding.required && !finding.found)
|
|
53
|
+
.map((finding) => finding.section);
|
|
54
|
+
if (failedRequired.length > 0) {
|
|
55
|
+
issues.push(`artifact validation failed for required sections: ${failedRequired.join(", ")}.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (stage === "review") {
|
|
59
|
+
const reviewArmy = await validateReviewArmy(projectRoot);
|
|
60
|
+
if (!reviewArmy.valid) {
|
|
61
|
+
issues.push(`review-army validation failed: ${reviewArmy.errors.join("; ")}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
ok: issues.length === 0,
|
|
67
|
+
stage,
|
|
68
|
+
issues,
|
|
69
|
+
requiredCount: required.length,
|
|
70
|
+
passedCount: catalog.passed.length,
|
|
71
|
+
blockedCount: catalog.blocked.length
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function reconcileCurrentStageGateCatalog(flowState) {
|
|
75
|
+
const stage = flowState.currentStage;
|
|
76
|
+
const required = stageSchema(stage).requiredGates.map((gate) => gate.id);
|
|
77
|
+
const requiredSet = new Set(required);
|
|
78
|
+
const catalog = flowState.stageGateCatalog[stage];
|
|
79
|
+
const notes = [];
|
|
80
|
+
const before = {
|
|
81
|
+
required: [...catalog.required],
|
|
82
|
+
passed: [...catalog.passed],
|
|
83
|
+
blocked: [...catalog.blocked]
|
|
84
|
+
};
|
|
85
|
+
const passedSet = new Set(unique(catalog.passed).filter((gateId) => {
|
|
86
|
+
const keep = requiredSet.has(gateId);
|
|
87
|
+
if (!keep) {
|
|
88
|
+
notes.push(`removed unknown passed gate "${gateId}"`);
|
|
89
|
+
}
|
|
90
|
+
return keep;
|
|
91
|
+
}));
|
|
92
|
+
const blockedSet = new Set(unique(catalog.blocked).filter((gateId) => {
|
|
93
|
+
const keep = requiredSet.has(gateId);
|
|
94
|
+
if (!keep) {
|
|
95
|
+
notes.push(`removed unknown blocked gate "${gateId}"`);
|
|
96
|
+
}
|
|
97
|
+
return keep;
|
|
98
|
+
}));
|
|
99
|
+
for (const gateId of [...passedSet]) {
|
|
100
|
+
if (!blockedSet.has(gateId))
|
|
101
|
+
continue;
|
|
102
|
+
const evidence = flowState.guardEvidence[gateId];
|
|
103
|
+
if (typeof evidence === "string" && evidence.trim().length > 0) {
|
|
104
|
+
blockedSet.delete(gateId);
|
|
105
|
+
notes.push(`resolved overlap for "${gateId}" in favor of passed (evidence present)`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
passedSet.delete(gateId);
|
|
109
|
+
notes.push(`resolved overlap for "${gateId}" in favor of blocked (missing evidence)`);
|
|
110
|
+
}
|
|
111
|
+
for (const gateId of [...passedSet]) {
|
|
112
|
+
const evidence = flowState.guardEvidence[gateId];
|
|
113
|
+
if (typeof evidence === "string" && evidence.trim().length > 0)
|
|
114
|
+
continue;
|
|
115
|
+
passedSet.delete(gateId);
|
|
116
|
+
blockedSet.add(gateId);
|
|
117
|
+
notes.push(`moved "${gateId}" from passed to blocked (missing evidence)`);
|
|
118
|
+
}
|
|
119
|
+
const after = {
|
|
120
|
+
required: [...required],
|
|
121
|
+
passed: required.filter((gateId) => passedSet.has(gateId)),
|
|
122
|
+
blocked: required.filter((gateId) => blockedSet.has(gateId) && !passedSet.has(gateId))
|
|
123
|
+
};
|
|
124
|
+
const changed = !sameStringArray(before.required, after.required) ||
|
|
125
|
+
!sameStringArray(before.passed, after.passed) ||
|
|
126
|
+
!sameStringArray(before.blocked, after.blocked);
|
|
127
|
+
const nextState = changed
|
|
128
|
+
? {
|
|
129
|
+
...flowState,
|
|
130
|
+
stageGateCatalog: {
|
|
131
|
+
...flowState.stageGateCatalog,
|
|
132
|
+
[stage]: after
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
: flowState;
|
|
136
|
+
return {
|
|
137
|
+
nextState,
|
|
138
|
+
reconciliation: {
|
|
139
|
+
stage,
|
|
140
|
+
changed,
|
|
141
|
+
before,
|
|
142
|
+
after,
|
|
143
|
+
notes
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export async function reconcileAndWriteCurrentStageGateCatalog(projectRoot) {
|
|
148
|
+
const state = await readFlowState(projectRoot);
|
|
149
|
+
const { nextState, reconciliation } = reconcileCurrentStageGateCatalog(state);
|
|
150
|
+
if (reconciliation.changed) {
|
|
151
|
+
await writeFlowState(projectRoot, nextState);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
...reconciliation,
|
|
155
|
+
wrote: reconciliation.changed
|
|
156
|
+
};
|
|
157
|
+
}
|
package/dist/harness-adapters.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
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 { CCLAW_AGENTS, agentMarkdown
|
|
5
|
-
import { autoplanAgentsMdBlock } from "./content/autoplan.js";
|
|
6
|
-
import { learningsAgentsMdBlock } from "./content/learnings.js";
|
|
7
|
-
import { sessionHooksAgentsMdBlock } from "./content/session-hooks.js";
|
|
8
|
-
import { hooksAgentsMdBlock } from "./content/hooks.js";
|
|
9
|
-
import { subagentsAgentsMdBlock } from "./content/subagents.js";
|
|
4
|
+
import { CCLAW_AGENTS, agentMarkdown } from "./content/agents.js";
|
|
10
5
|
import { stageSkillFolder } from "./content/skills.js";
|
|
11
6
|
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
12
7
|
export const CCLAW_MARKER_START = "<!-- cclaw-start -->";
|
|
@@ -48,9 +43,10 @@ Do not skip required handoff gates.
|
|
|
48
43
|
function agentsMdBlock() {
|
|
49
44
|
const stageList = COMMAND_FILE_ORDER.map((s) => `| \`/cc-${s}\` | \`.cclaw/skills/${stageSkillFolder(s)}/SKILL.md\` + \`.cclaw/commands/${s}.md\` |`).join("\n");
|
|
50
45
|
return `${CCLAW_MARKER_START}
|
|
51
|
-
## Cclaw —
|
|
46
|
+
## Cclaw — Workflow Adapter
|
|
52
47
|
|
|
53
|
-
> Auto-generated by \`cclaw sync\`. Do not edit
|
|
48
|
+
> Auto-generated by \`cclaw sync\`. Do not edit this managed block manually.
|
|
49
|
+
> Existing project rules in this repository take precedence over cclaw defaults.
|
|
54
50
|
|
|
55
51
|
### Activation Rule
|
|
56
52
|
|
|
@@ -66,32 +62,19 @@ Before responding to a coding request:
|
|
|
66
62
|
${stageList}
|
|
67
63
|
| \`/cc-learn\` | \`.cclaw/skills/learnings/SKILL.md\` + \`.cclaw/commands/learn.md\` |
|
|
68
64
|
| \`/cc-autoplan\` | \`.cclaw/skills/autoplan/SKILL.md\` + \`.cclaw/commands/autoplan.md\` |
|
|
65
|
+
| \`/cc-next\` | \`.cclaw/skills/flow-next-step/SKILL.md\` + \`.cclaw/commands/next.md\` |
|
|
69
66
|
|
|
70
67
|
**Stage order:** brainstorm > scope > design > spec > plan > test > build > review > ship.
|
|
71
|
-
One stage per invocation. Gates must pass before handoff.
|
|
68
|
+
One stage per invocation. Gates must pass before handoff.
|
|
72
69
|
|
|
73
70
|
### Verification Discipline
|
|
74
71
|
|
|
75
72
|
No completion claims without fresh evidence. No "Done" / "All good" / "Tests pass" without running the command in this message.
|
|
76
73
|
|
|
77
|
-
###
|
|
74
|
+
### Detail Level
|
|
78
75
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
| \`.cclaw/commands/*.md\` | Stage commands (thin orchestrators) |
|
|
82
|
-
| \`.cclaw/skills/*/SKILL.md\` | Full stage instructions |
|
|
83
|
-
| \`.cclaw/rules/\` | RULES.md + rules.json |
|
|
84
|
-
| \`.cclaw/state/flow-state.json\` | Flow state & gate tracking |
|
|
85
|
-
| \`.cclaw/artifacts/*.md\` | Stage evidence artifacts |
|
|
86
|
-
| \`.cclaw/agents/*.md\` | Specialist agent personas |
|
|
87
|
-
| \`.cclaw/learnings.jsonl\` | Project knowledge base |
|
|
88
|
-
|
|
89
|
-
${learningsAgentsMdBlock()}
|
|
90
|
-
${autoplanAgentsMdBlock()}
|
|
91
|
-
${agentsAgentsMdBlock()}
|
|
92
|
-
${subagentsAgentsMdBlock()}
|
|
93
|
-
${sessionHooksAgentsMdBlock()}
|
|
94
|
-
${hooksAgentsMdBlock()}
|
|
76
|
+
- This managed AGENTS block is intentionally minimal for cross-project use.
|
|
77
|
+
- Detailed operating procedures live in \`.cclaw/skills/using-cclaw/SKILL.md\`.
|
|
95
78
|
${CCLAW_MARKER_END}`;
|
|
96
79
|
}
|
|
97
80
|
/** Removes the cclaw AGENTS.md block. */
|
|
@@ -169,6 +152,7 @@ export async function syncHarnessShims(projectRoot, harnesses) {
|
|
|
169
152
|
// Utility command shims
|
|
170
153
|
await writeFileSafe(path.join(commandDir, "cc-learn.md"), utilityShimContent(harness, "learn", "learnings", "learn.md"));
|
|
171
154
|
await writeFileSafe(path.join(commandDir, "cc-autoplan.md"), utilityShimContent(harness, "autoplan", "autoplan", "autoplan.md"));
|
|
155
|
+
await writeFileSafe(path.join(commandDir, "cc-next.md"), utilityShimContent(harness, "next", "flow-next-step", "next.md"));
|
|
172
156
|
}
|
|
173
157
|
await syncAgentFiles(projectRoot);
|
|
174
158
|
await syncAgentsMd(projectRoot);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type HookSchemaHarness = "claude" | "cursor" | "codex";
|
|
2
|
+
export interface HookSchemaValidationResult {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function validateHookDocument(harness: HookSchemaHarness, document: unknown): HookSchemaValidationResult;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import claudeHooksSchema from "./hook-schemas/claude-hooks.v1.json" with { type: "json" };
|
|
2
|
+
import codexHooksSchema from "./hook-schemas/codex-hooks.v1.json" with { type: "json" };
|
|
3
|
+
import cursorHooksSchema from "./hook-schemas/cursor-hooks.v1.json" with { type: "json" };
|
|
4
|
+
const SCHEMA_MAP = {
|
|
5
|
+
claude: claudeHooksSchema,
|
|
6
|
+
cursor: cursorHooksSchema,
|
|
7
|
+
codex: codexHooksSchema
|
|
8
|
+
};
|
|
9
|
+
function toObject(value) {
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export function validateHookDocument(harness, document) {
|
|
16
|
+
const descriptor = SCHEMA_MAP[harness];
|
|
17
|
+
const root = toObject(document);
|
|
18
|
+
if (!root) {
|
|
19
|
+
return { ok: false, errors: ["hook document must be a JSON object"] };
|
|
20
|
+
}
|
|
21
|
+
const errors = [];
|
|
22
|
+
const version = root.cclawHookSchemaVersion;
|
|
23
|
+
if (version !== descriptor.schemaVersion) {
|
|
24
|
+
errors.push(`expected cclawHookSchemaVersion=${descriptor.schemaVersion}, got ${JSON.stringify(version)}`);
|
|
25
|
+
}
|
|
26
|
+
if (harness === "cursor" && root.version !== 1) {
|
|
27
|
+
errors.push(`cursor hooks require version=1, got ${JSON.stringify(root.version)}`);
|
|
28
|
+
}
|
|
29
|
+
const hooks = toObject(root.hooks);
|
|
30
|
+
if (!hooks) {
|
|
31
|
+
errors.push("missing hooks object");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
for (const eventName of descriptor.requiredEvents) {
|
|
35
|
+
const eventValue = hooks[eventName];
|
|
36
|
+
if (!Array.isArray(eventValue) || eventValue.length === 0) {
|
|
37
|
+
errors.push(`missing required event array "${eventName}"`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ok: errors.length === 0,
|
|
43
|
+
errors
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "cclaw://hooks/cursor/v1",
|
|
4
|
+
"harness": "cursor",
|
|
5
|
+
"schemaVersion": 1,
|
|
6
|
+
"requiredEvents": [
|
|
7
|
+
"sessionStart",
|
|
8
|
+
"sessionResume",
|
|
9
|
+
"sessionClear",
|
|
10
|
+
"sessionCompact",
|
|
11
|
+
"preToolUse",
|
|
12
|
+
"postToolUse",
|
|
13
|
+
"stop"
|
|
14
|
+
]
|
|
15
|
+
}
|