cclaw-cli 0.1.1 → 0.2.1
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 +20 -0
- package/dist/artifact-linter.js +368 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +8 -2
- package/dist/config.d.ts +4 -4
- package/dist/config.js +56 -5
- 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 -23
- 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 +2 -0
- 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 +431 -16
- 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 +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FlowStage } from "./types.js";
|
|
2
|
+
export interface LintFinding {
|
|
3
|
+
section: string;
|
|
4
|
+
required: boolean;
|
|
5
|
+
rule: string;
|
|
6
|
+
found: boolean;
|
|
7
|
+
details: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LintResult {
|
|
10
|
+
stage: string;
|
|
11
|
+
file: string;
|
|
12
|
+
passed: boolean;
|
|
13
|
+
findings: LintFinding[];
|
|
14
|
+
}
|
|
15
|
+
export declare function lintArtifact(projectRoot: string, stage: FlowStage): Promise<LintResult>;
|
|
16
|
+
export declare function lintAllArtifacts(projectRoot: string): Promise<LintResult[]>;
|
|
17
|
+
export declare function validateReviewArmy(projectRoot: string): Promise<{
|
|
18
|
+
valid: boolean;
|
|
19
|
+
errors: string[];
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
|
+
import { exists } from "./fs-utils.js";
|
|
5
|
+
import { orderedStageSchemas, stageSchema } from "./content/stage-schema.js";
|
|
6
|
+
import { readFlowState } from "./runs.js";
|
|
7
|
+
async function resolveArtifactPath(projectRoot, fileName) {
|
|
8
|
+
const fallbackRelPath = path.join(RUNTIME_ROOT, "artifacts", fileName);
|
|
9
|
+
const fallbackAbsPath = path.join(projectRoot, fallbackRelPath);
|
|
10
|
+
const { activeRunId } = await readFlowState(projectRoot);
|
|
11
|
+
const runId = activeRunId.trim();
|
|
12
|
+
if (runId.length > 0) {
|
|
13
|
+
const canonicalRelPath = path.join(RUNTIME_ROOT, "runs", runId, "artifacts", fileName);
|
|
14
|
+
const canonicalAbsPath = path.join(projectRoot, canonicalRelPath);
|
|
15
|
+
if (await exists(canonicalAbsPath)) {
|
|
16
|
+
return { absPath: canonicalAbsPath, relPath: canonicalRelPath };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { absPath: fallbackAbsPath, relPath: fallbackRelPath };
|
|
20
|
+
}
|
|
21
|
+
function normalizeHeadingTitle(title) {
|
|
22
|
+
return title.trim().replace(/\s+/g, " ");
|
|
23
|
+
}
|
|
24
|
+
/** Collect H2 sections and body content (`## Section Name`). */
|
|
25
|
+
function extractH2Sections(markdown) {
|
|
26
|
+
const sections = new Map();
|
|
27
|
+
const lines = markdown.split(/\r?\n/);
|
|
28
|
+
let currentHeading = null;
|
|
29
|
+
let buffer = [];
|
|
30
|
+
const flush = () => {
|
|
31
|
+
if (currentHeading === null)
|
|
32
|
+
return;
|
|
33
|
+
sections.set(currentHeading, buffer.join("\n"));
|
|
34
|
+
};
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const match = /^##\s+(.+)$/u.exec(line);
|
|
37
|
+
if (match) {
|
|
38
|
+
flush();
|
|
39
|
+
currentHeading = normalizeHeadingTitle(match[1] ?? "");
|
|
40
|
+
buffer = [];
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (currentHeading !== null) {
|
|
44
|
+
buffer.push(line);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
flush();
|
|
48
|
+
return sections;
|
|
49
|
+
}
|
|
50
|
+
function headingPresent(sections, section) {
|
|
51
|
+
const want = normalizeHeadingTitle(section).toLowerCase();
|
|
52
|
+
for (const h of sections.keys()) {
|
|
53
|
+
if (h.toLowerCase() === want) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
function sectionBodyByName(sections, section) {
|
|
60
|
+
const want = normalizeHeadingTitle(section).toLowerCase();
|
|
61
|
+
for (const [heading, body] of sections.entries()) {
|
|
62
|
+
if (heading.toLowerCase() === want) {
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function meaningfulLineCount(sectionBody) {
|
|
69
|
+
return sectionBody
|
|
70
|
+
.split(/\r?\n/)
|
|
71
|
+
.map((line) => line.trim())
|
|
72
|
+
.filter((line) => line.length > 0)
|
|
73
|
+
.filter((line) => !line.startsWith("<!--"))
|
|
74
|
+
.filter((line) => !/^[-:| ]+$/u.test(line))
|
|
75
|
+
.filter((line) => /[A-Za-z0-9]/u.test(line))
|
|
76
|
+
.length;
|
|
77
|
+
}
|
|
78
|
+
function lineHasToken(line, token) {
|
|
79
|
+
return new RegExp(`\\b${token}\\b`, "u").test(line);
|
|
80
|
+
}
|
|
81
|
+
function countListItems(sectionBody) {
|
|
82
|
+
const lines = sectionBody.split(/\r?\n/).map((line) => line.trim());
|
|
83
|
+
const bullets = lines.filter((line) => /^[-*]\s+\S+/u.test(line)).length;
|
|
84
|
+
const tableRows = lines.filter((line) => /^\|.*\|$/u.test(line) && !/^\|[-:| ]+\|$/u.test(line));
|
|
85
|
+
const tableDataRows = tableRows.length > 0 ? Math.max(0, tableRows.length - 1) : 0;
|
|
86
|
+
return Math.max(bullets, tableDataRows);
|
|
87
|
+
}
|
|
88
|
+
function extractMinItemsFromRule(rule) {
|
|
89
|
+
const match = /at least\s+(\d+)/iu.exec(rule);
|
|
90
|
+
if (!match)
|
|
91
|
+
return null;
|
|
92
|
+
const parsed = Number.parseInt(match[1] ?? "", 10);
|
|
93
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
94
|
+
}
|
|
95
|
+
function tokensFromRule(rule) {
|
|
96
|
+
const allCaps = rule.match(/\b[A-Z][A-Z0-9_]{2,}\b/g) ?? [];
|
|
97
|
+
if (allCaps.length > 0) {
|
|
98
|
+
return [...new Set(allCaps)];
|
|
99
|
+
}
|
|
100
|
+
if (/finalization enum token/iu.test(rule)) {
|
|
101
|
+
return [
|
|
102
|
+
"FINALIZE_MERGE_LOCAL",
|
|
103
|
+
"FINALIZE_OPEN_PR",
|
|
104
|
+
"FINALIZE_KEEP_BRANCH",
|
|
105
|
+
"FINALIZE_DISCARD_BRANCH"
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
if (/final verdict/iu.test(rule)) {
|
|
109
|
+
return ["APPROVED", "APPROVED_WITH_CONCERNS", "BLOCKED"];
|
|
110
|
+
}
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
function validateSectionBody(sectionBody, rule) {
|
|
114
|
+
const bodyLines = sectionBody.split(/\r?\n/).map((line) => line.trim());
|
|
115
|
+
const meaningful = meaningfulLineCount(sectionBody);
|
|
116
|
+
if (meaningful === 0) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
details: "Section exists but has no meaningful content yet."
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const minItems = extractMinItemsFromRule(rule);
|
|
123
|
+
if (minItems !== null) {
|
|
124
|
+
const count = countListItems(sectionBody);
|
|
125
|
+
if (count < minItems) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
details: `Rule expects at least ${minItems} item(s), found ${count}.`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (/exactly one/iu.test(rule)) {
|
|
133
|
+
const tokens = tokensFromRule(rule);
|
|
134
|
+
if (tokens.length > 0) {
|
|
135
|
+
const selected = new Set();
|
|
136
|
+
const tokenLines = [];
|
|
137
|
+
for (const line of bodyLines) {
|
|
138
|
+
if (!line)
|
|
139
|
+
continue;
|
|
140
|
+
for (const token of tokens) {
|
|
141
|
+
if (!lineHasToken(line, token))
|
|
142
|
+
continue;
|
|
143
|
+
tokenLines.push({ line, token });
|
|
144
|
+
if (/\[x\]/iu.test(line) || /selected|verdict|enum|execution result|status/iu.test(line)) {
|
|
145
|
+
selected.add(token);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (selected.size === 0 && tokenLines.length === 1 && !tokenLines[0].line.includes("|")) {
|
|
150
|
+
selected.add(tokenLines[0].token);
|
|
151
|
+
}
|
|
152
|
+
if (selected.size !== 1) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
details: `Rule expects exactly one selected token (${tokens.join(", ")}); found ${selected.size}.`
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
details: "Section heading and content satisfy lint heuristics."
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export async function lintArtifact(projectRoot, stage) {
|
|
166
|
+
const schema = stageSchema(stage);
|
|
167
|
+
const { absPath: absFile, relPath: relFile } = await resolveArtifactPath(projectRoot, schema.artifactFile);
|
|
168
|
+
const findings = [];
|
|
169
|
+
if (!(await exists(absFile))) {
|
|
170
|
+
for (const v of schema.artifactValidation) {
|
|
171
|
+
findings.push({
|
|
172
|
+
section: v.section,
|
|
173
|
+
required: v.required,
|
|
174
|
+
rule: v.validationRule,
|
|
175
|
+
found: false,
|
|
176
|
+
details: `Artifact file missing: ${relFile}`
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
stage,
|
|
181
|
+
file: relFile,
|
|
182
|
+
passed: schema.artifactValidation.every((v) => !v.required),
|
|
183
|
+
findings
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const raw = await fs.readFile(absFile, "utf8");
|
|
187
|
+
const sections = extractH2Sections(raw);
|
|
188
|
+
for (const v of schema.artifactValidation) {
|
|
189
|
+
const hasHeading = headingPresent(sections, v.section);
|
|
190
|
+
const body = hasHeading ? sectionBodyByName(sections, v.section) : null;
|
|
191
|
+
const validation = body === null
|
|
192
|
+
? { ok: false, details: `No ## heading matching required section "${v.section}".` }
|
|
193
|
+
: validateSectionBody(body, v.validationRule);
|
|
194
|
+
const found = hasHeading && validation.ok;
|
|
195
|
+
findings.push({
|
|
196
|
+
section: v.section,
|
|
197
|
+
required: v.required,
|
|
198
|
+
rule: v.validationRule,
|
|
199
|
+
found,
|
|
200
|
+
details: found
|
|
201
|
+
? validation.details
|
|
202
|
+
: validation.details
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const passed = findings.every((f) => !f.required || f.found);
|
|
206
|
+
return { stage, file: relFile, passed, findings };
|
|
207
|
+
}
|
|
208
|
+
export async function lintAllArtifacts(projectRoot) {
|
|
209
|
+
const out = [];
|
|
210
|
+
for (const schema of orderedStageSchemas()) {
|
|
211
|
+
out.push(await lintArtifact(projectRoot, schema.stage));
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
function isNonEmptyString(v) {
|
|
216
|
+
return typeof v === "string" && v.length > 0;
|
|
217
|
+
}
|
|
218
|
+
function isFiniteNumber(v) {
|
|
219
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
220
|
+
}
|
|
221
|
+
function isNonNegativeInteger(v) {
|
|
222
|
+
return Number.isInteger(v) && v >= 0;
|
|
223
|
+
}
|
|
224
|
+
function isStringArray(v) {
|
|
225
|
+
return Array.isArray(v) && v.every((item) => typeof item === "string");
|
|
226
|
+
}
|
|
227
|
+
export async function validateReviewArmy(projectRoot) {
|
|
228
|
+
const errors = [];
|
|
229
|
+
const { absPath, relPath } = await resolveArtifactPath(projectRoot, "07-review-army.json");
|
|
230
|
+
if (!(await exists(absPath))) {
|
|
231
|
+
return { valid: false, errors: [`Missing file: ${relPath}`] };
|
|
232
|
+
}
|
|
233
|
+
let parsed;
|
|
234
|
+
try {
|
|
235
|
+
parsed = JSON.parse(await fs.readFile(absPath, "utf8"));
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
239
|
+
return { valid: false, errors: [`Invalid JSON: ${msg}`] };
|
|
240
|
+
}
|
|
241
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
242
|
+
return { valid: false, errors: ["Root value must be a JSON object."] };
|
|
243
|
+
}
|
|
244
|
+
const root = parsed;
|
|
245
|
+
if (!("version" in root) || !isFiniteNumber(root.version) || root.version < 1) {
|
|
246
|
+
errors.push('Field "version" must be a finite number >= 1.');
|
|
247
|
+
}
|
|
248
|
+
if (!isNonEmptyString(root.generatedAt)) {
|
|
249
|
+
errors.push('Field "generatedAt" must be a non-empty string.');
|
|
250
|
+
}
|
|
251
|
+
if (!("scope" in root) || root.scope === null || typeof root.scope !== "object" || Array.isArray(root.scope)) {
|
|
252
|
+
errors.push('Field "scope" must be an object.');
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
const scope = root.scope;
|
|
256
|
+
if (!isNonEmptyString(scope.base)) {
|
|
257
|
+
errors.push("scope.base must be a non-empty string.");
|
|
258
|
+
}
|
|
259
|
+
if (!isNonEmptyString(scope.head)) {
|
|
260
|
+
errors.push("scope.head must be a non-empty string.");
|
|
261
|
+
}
|
|
262
|
+
if (!isStringArray(scope.files)) {
|
|
263
|
+
errors.push("scope.files must be an array of strings.");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const severitySet = new Set(["Critical", "Important", "Suggestion"]);
|
|
267
|
+
const statusSet = new Set(["open", "accepted", "resolved"]);
|
|
268
|
+
const findingIds = new Set();
|
|
269
|
+
const openCriticalIds = new Set();
|
|
270
|
+
if (!Array.isArray(root.findings)) {
|
|
271
|
+
errors.push('Field "findings" must be an array.');
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
root.findings.forEach((f, i) => {
|
|
275
|
+
if (f === null || typeof f !== "object" || Array.isArray(f)) {
|
|
276
|
+
errors.push(`findings[${i}] must be an object.`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const o = f;
|
|
280
|
+
if (!isNonEmptyString(o.id)) {
|
|
281
|
+
errors.push(`findings[${i}].id must be a non-empty string.`);
|
|
282
|
+
}
|
|
283
|
+
else if (findingIds.has(o.id)) {
|
|
284
|
+
errors.push(`findings[${i}].id must be unique.`);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
findingIds.add(o.id);
|
|
288
|
+
}
|
|
289
|
+
if (!isNonEmptyString(o.severity) || !severitySet.has(o.severity)) {
|
|
290
|
+
errors.push(`findings[${i}].severity must be one of: Critical, Important, Suggestion.`);
|
|
291
|
+
}
|
|
292
|
+
if (!isNonEmptyString(o.status) || !statusSet.has(o.status)) {
|
|
293
|
+
errors.push(`findings[${i}].status must be one of: open, accepted, resolved.`);
|
|
294
|
+
}
|
|
295
|
+
if (!isNonEmptyString(o.fingerprint)) {
|
|
296
|
+
errors.push(`findings[${i}].fingerprint must be a non-empty string.`);
|
|
297
|
+
}
|
|
298
|
+
if (!isFiniteNumber(o.confidence) || o.confidence < 1 || o.confidence > 10) {
|
|
299
|
+
errors.push(`findings[${i}].confidence must be a number in [1,10].`);
|
|
300
|
+
}
|
|
301
|
+
if (!isStringArray(o.reportedBy) || o.reportedBy.length === 0) {
|
|
302
|
+
errors.push(`findings[${i}].reportedBy must be a non-empty string array.`);
|
|
303
|
+
}
|
|
304
|
+
if (o.location !== undefined) {
|
|
305
|
+
if (o.location === null || typeof o.location !== "object" || Array.isArray(o.location)) {
|
|
306
|
+
errors.push(`findings[${i}].location must be an object when present.`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const loc = o.location;
|
|
310
|
+
if (!isNonEmptyString(loc.file)) {
|
|
311
|
+
errors.push(`findings[${i}].location.file must be a non-empty string.`);
|
|
312
|
+
}
|
|
313
|
+
if (!isFiniteNumber(loc.line) || loc.line < 1) {
|
|
314
|
+
errors.push(`findings[${i}].location.line must be a positive number.`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (o.recommendation !== undefined && !isNonEmptyString(o.recommendation)) {
|
|
319
|
+
errors.push(`findings[${i}].recommendation must be a non-empty string when present.`);
|
|
320
|
+
}
|
|
321
|
+
if (o.severity === "Critical" && o.status === "open" && !isNonEmptyString(o.recommendation)) {
|
|
322
|
+
errors.push(`findings[${i}] open Critical finding must include recommendation.`);
|
|
323
|
+
}
|
|
324
|
+
if (o.id && o.severity === "Critical" && o.status === "open" && typeof o.id === "string") {
|
|
325
|
+
openCriticalIds.add(o.id);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (!("reconciliation" in root) || root.reconciliation === null || typeof root.reconciliation !== "object") {
|
|
330
|
+
errors.push('Field "reconciliation" must be an object.');
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
const rec = root.reconciliation;
|
|
334
|
+
if (!isNonNegativeInteger(rec.duplicatesCollapsed)) {
|
|
335
|
+
errors.push("reconciliation.duplicatesCollapsed must be a non-negative integer.");
|
|
336
|
+
}
|
|
337
|
+
if (!Array.isArray(rec.conflicts)) {
|
|
338
|
+
errors.push("reconciliation.conflicts must be an array.");
|
|
339
|
+
}
|
|
340
|
+
if (!isStringArray(rec.multiSpecialistConfirmed)) {
|
|
341
|
+
errors.push("reconciliation.multiSpecialistConfirmed must be an array of finding ids.");
|
|
342
|
+
}
|
|
343
|
+
if (!isStringArray(rec.shipBlockers)) {
|
|
344
|
+
errors.push("reconciliation.shipBlockers must be an array of finding ids.");
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const blockers = new Set(rec.shipBlockers);
|
|
348
|
+
for (const id of rec.shipBlockers) {
|
|
349
|
+
if (!findingIds.has(id)) {
|
|
350
|
+
errors.push(`reconciliation.shipBlockers references unknown finding id "${id}".`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
for (const criticalId of openCriticalIds) {
|
|
354
|
+
if (!blockers.has(criticalId)) {
|
|
355
|
+
errors.push(`reconciliation.shipBlockers must include open Critical finding "${criticalId}".`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (isStringArray(rec.multiSpecialistConfirmed)) {
|
|
360
|
+
for (const id of rec.multiSpecialistConfirmed) {
|
|
361
|
+
if (!findingIds.has(id)) {
|
|
362
|
+
errors.push(`reconciliation.multiSpecialistConfirmed references unknown finding id "${id}".`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return { valid: errors.length === 0, errors };
|
|
368
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ type CommandName = "init" | "sync" | "doctor" | "upgrade" | "uninstall";
|
|
|
4
4
|
interface ParsedArgs {
|
|
5
5
|
command?: CommandName;
|
|
6
6
|
harnesses?: HarnessId[];
|
|
7
|
+
reconcileGates?: boolean;
|
|
7
8
|
}
|
|
8
9
|
declare function parseHarnesses(raw: string): HarnessId[];
|
|
9
10
|
declare function parseArgs(argv: string[]): ParsedArgs;
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ function usage() {
|
|
|
14
14
|
Usage:
|
|
15
15
|
cclaw init [--harnesses=claude,cursor,opencode,codex]
|
|
16
16
|
cclaw sync
|
|
17
|
-
cclaw doctor
|
|
17
|
+
cclaw doctor [--reconcile-gates]
|
|
18
18
|
cclaw upgrade
|
|
19
19
|
cclaw uninstall
|
|
20
20
|
`;
|
|
@@ -39,6 +39,10 @@ function parseArgs(argv) {
|
|
|
39
39
|
for (const flag of flags) {
|
|
40
40
|
if (flag.startsWith("--harnesses=")) {
|
|
41
41
|
parsed.harnesses = parseHarnesses(flag.replace("--harnesses=", ""));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (flag === "--reconcile-gates") {
|
|
45
|
+
parsed.reconcileGates = true;
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
return parsed;
|
|
@@ -63,7 +67,9 @@ async function runCommand(parsed, ctx) {
|
|
|
63
67
|
return 0;
|
|
64
68
|
}
|
|
65
69
|
if (command === "doctor") {
|
|
66
|
-
const checks = await doctorChecks(ctx.cwd
|
|
70
|
+
const checks = await doctorChecks(ctx.cwd, {
|
|
71
|
+
reconcileCurrentStageGates: parsed.reconcileGates === true
|
|
72
|
+
});
|
|
67
73
|
for (const check of checks) {
|
|
68
74
|
ctx.stdout.write(`${check.ok ? "PASS" : "FAIL"} ${check.name} :: ${check.details}\n`);
|
|
69
75
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { HarnessId,
|
|
1
|
+
import type { HarnessId, VibyConfig } from "./types.js";
|
|
2
2
|
export declare function configPath(projectRoot: string): string;
|
|
3
|
-
export declare function createDefaultConfig(harnesses?: HarnessId[]):
|
|
4
|
-
export declare function readConfig(projectRoot: string): Promise<
|
|
5
|
-
export declare function writeConfig(projectRoot: string, config:
|
|
3
|
+
export declare function createDefaultConfig(harnesses?: HarnessId[]): VibyConfig;
|
|
4
|
+
export declare function readConfig(projectRoot: string): Promise<VibyConfig>;
|
|
5
|
+
export declare function writeConfig(projectRoot: string, config: VibyConfig): Promise<void>;
|
package/dist/config.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse, stringify } from "yaml";
|
|
4
|
-
import { DEFAULT_HARNESSES, FLOW_VERSION, RUNTIME_ROOT
|
|
4
|
+
import { CCLAW_VERSION, DEFAULT_HARNESSES, FLOW_VERSION, RUNTIME_ROOT } from "./constants.js";
|
|
5
5
|
import { exists, writeFileSafe } from "./fs-utils.js";
|
|
6
6
|
import { HARNESS_IDS } from "./types.js";
|
|
7
7
|
const CONFIG_PATH = `${RUNTIME_ROOT}/config.yaml`;
|
|
8
8
|
const HARNESS_ID_SET = new Set(HARNESS_IDS);
|
|
9
9
|
const SUPPORTED_HARNESSES_TEXT = HARNESS_IDS.join(", ");
|
|
10
|
+
const ALLOWED_CONFIG_KEYS = new Set([
|
|
11
|
+
"version",
|
|
12
|
+
"flowVersion",
|
|
13
|
+
"harnesses",
|
|
14
|
+
"autoAdvance",
|
|
15
|
+
"globalLearnings",
|
|
16
|
+
"globalLearningsPath",
|
|
17
|
+
"promptGuardMode",
|
|
18
|
+
"gitHookGuards"
|
|
19
|
+
]);
|
|
10
20
|
function configFixExample() {
|
|
11
21
|
return `harnesses:
|
|
12
22
|
- claude
|
|
13
|
-
- cursor
|
|
14
|
-
`;
|
|
23
|
+
- cursor`;
|
|
15
24
|
}
|
|
16
25
|
function configValidationError(configFilePath, reason) {
|
|
17
26
|
return new Error(`Invalid cclaw config at ${configFilePath}: ${reason}\n` +
|
|
@@ -26,7 +35,11 @@ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES) {
|
|
|
26
35
|
return {
|
|
27
36
|
version: CCLAW_VERSION,
|
|
28
37
|
flowVersion: FLOW_VERSION,
|
|
29
|
-
harnesses
|
|
38
|
+
harnesses,
|
|
39
|
+
autoAdvance: false,
|
|
40
|
+
globalLearnings: false,
|
|
41
|
+
promptGuardMode: "advisory",
|
|
42
|
+
gitHookGuards: false
|
|
30
43
|
};
|
|
31
44
|
}
|
|
32
45
|
export async function readConfig(projectRoot) {
|
|
@@ -44,6 +57,10 @@ export async function readConfig(projectRoot) {
|
|
|
44
57
|
const parsed = (parsedUnknown && typeof parsedUnknown === "object"
|
|
45
58
|
? parsedUnknown
|
|
46
59
|
: {});
|
|
60
|
+
const unknownKeys = Object.keys(parsed).filter((key) => !ALLOWED_CONFIG_KEYS.has(key));
|
|
61
|
+
if (unknownKeys.length > 0) {
|
|
62
|
+
throw configValidationError(fullPath, `unknown top-level key(s): ${unknownKeys.join(", ")}`);
|
|
63
|
+
}
|
|
47
64
|
const hasHarnessesField = Object.prototype.hasOwnProperty.call(parsed, "harnesses");
|
|
48
65
|
if (hasHarnessesField && !Array.isArray(parsed.harnesses)) {
|
|
49
66
|
throw configValidationError(fullPath, `"harnesses" must be an array`);
|
|
@@ -60,10 +77,44 @@ export async function readConfig(projectRoot) {
|
|
|
60
77
|
const harnesses = hasHarnessesField
|
|
61
78
|
? [...new Set(validatedHarnesses)]
|
|
62
79
|
: DEFAULT_HARNESSES;
|
|
80
|
+
const autoAdvanceRaw = parsed.autoAdvance;
|
|
81
|
+
const autoAdvance = typeof autoAdvanceRaw === "boolean" ? autoAdvanceRaw : false;
|
|
82
|
+
const globalLearningsRaw = parsed.globalLearnings;
|
|
83
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "globalLearnings") &&
|
|
84
|
+
typeof globalLearningsRaw !== "boolean") {
|
|
85
|
+
throw configValidationError(fullPath, `"globalLearnings" must be a boolean`);
|
|
86
|
+
}
|
|
87
|
+
const globalLearnings = typeof globalLearningsRaw === "boolean" ? globalLearningsRaw : false;
|
|
88
|
+
const globalLearningsPathRaw = parsed.globalLearningsPath;
|
|
89
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "globalLearningsPath") &&
|
|
90
|
+
typeof globalLearningsPathRaw !== "string") {
|
|
91
|
+
throw configValidationError(fullPath, `"globalLearningsPath" must be a string`);
|
|
92
|
+
}
|
|
93
|
+
const globalLearningsPath = typeof globalLearningsPathRaw === "string" && globalLearningsPathRaw.trim().length > 0
|
|
94
|
+
? globalLearningsPathRaw.trim()
|
|
95
|
+
: undefined;
|
|
96
|
+
const promptGuardModeRaw = parsed.promptGuardMode;
|
|
97
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "promptGuardMode") &&
|
|
98
|
+
promptGuardModeRaw !== "advisory" &&
|
|
99
|
+
promptGuardModeRaw !== "strict") {
|
|
100
|
+
throw configValidationError(fullPath, `"promptGuardMode" must be "advisory" or "strict"`);
|
|
101
|
+
}
|
|
102
|
+
const promptGuardMode = promptGuardModeRaw === "strict" ? "strict" : "advisory";
|
|
103
|
+
const gitHookGuardsRaw = parsed.gitHookGuards;
|
|
104
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "gitHookGuards") &&
|
|
105
|
+
typeof gitHookGuardsRaw !== "boolean") {
|
|
106
|
+
throw configValidationError(fullPath, `"gitHookGuards" must be a boolean`);
|
|
107
|
+
}
|
|
108
|
+
const gitHookGuards = typeof gitHookGuardsRaw === "boolean" ? gitHookGuardsRaw : false;
|
|
63
109
|
return {
|
|
64
110
|
version: parsed.version ?? CCLAW_VERSION,
|
|
65
111
|
flowVersion: parsed.flowVersion ?? FLOW_VERSION,
|
|
66
|
-
harnesses
|
|
112
|
+
harnesses,
|
|
113
|
+
autoAdvance,
|
|
114
|
+
globalLearnings,
|
|
115
|
+
globalLearningsPath,
|
|
116
|
+
promptGuardMode,
|
|
117
|
+
gitHookGuards
|
|
67
118
|
};
|
|
68
119
|
}
|
|
69
120
|
export async function writeConfig(projectRoot, config) {
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { FlowStage, HarnessId } from "./types.js";
|
|
2
2
|
/** Hidden runtime directory at project root (dot-prefixed). */
|
|
3
3
|
export declare const RUNTIME_ROOT = ".cclaw";
|
|
4
|
-
export declare const CCLAW_VERSION = "0.1.
|
|
4
|
+
export declare const CCLAW_VERSION = "0.1.1";
|
|
5
5
|
export declare const FLOW_VERSION = "1.0.0";
|
|
6
6
|
export declare const DEFAULT_HARNESSES: HarnessId[];
|
|
7
|
-
export declare const REQUIRED_DIRS: readonly [".cclaw", ".cclaw/commands", ".cclaw/skills", ".cclaw/templates", ".cclaw/artifacts", ".cclaw/state", ".cclaw/runs", ".cclaw/rules", ".cclaw/adapters", ".cclaw/agents", ".cclaw/hooks"];
|
|
8
|
-
export declare const REQUIRED_GITIGNORE_PATTERNS: readonly ["# cclaw generated artifacts", ".cclaw/", ".claude/commands/cc-*.md", ".cursor/commands/cc-*.md", ".opencode/commands/cc-*.md", ".codex/commands/cc-*.md", ".claude/hooks/hooks.json", ".cursor/hooks.json", ".codex/hooks.json"];
|
|
7
|
+
export declare const REQUIRED_DIRS: readonly [".cclaw", ".cclaw/commands", ".cclaw/skills", ".cclaw/contexts", ".cclaw/templates", ".cclaw/artifacts", ".cclaw/state", ".cclaw/runs", ".cclaw/rules", ".cclaw/adapters", ".cclaw/agents", ".cclaw/hooks"];
|
|
8
|
+
export declare const REQUIRED_GITIGNORE_PATTERNS: readonly ["# cclaw generated artifacts", ".cclaw/", ".claude/commands/cc-*.md", ".cursor/commands/cc-*.md", ".opencode/commands/cc-*.md", ".codex/commands/cc-*.md", ".claude/hooks/hooks.json", ".cursor/hooks.json", ".codex/hooks.json", ".opencode/plugins/cclaw-plugin.mjs", ".cursor/rules/cclaw-workflow.mdc"];
|
|
9
9
|
export declare const COMMAND_FILE_ORDER: FlowStage[];
|
|
10
|
-
export declare const UTILITY_COMMANDS: readonly ["learn", "autoplan"];
|
|
10
|
+
export declare const UTILITY_COMMANDS: readonly ["learn", "autoplan", "next"];
|
|
11
11
|
export declare const SUBAGENT_SKILL_FOLDERS: readonly ["subagent-dev", "parallel-dispatch"];
|
|
12
12
|
export type UtilityCommand = (typeof UTILITY_COMMANDS)[number];
|
package/dist/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Hidden runtime directory at project root (dot-prefixed). */
|
|
2
2
|
export const RUNTIME_ROOT = ".cclaw";
|
|
3
|
-
export const CCLAW_VERSION = "0.1.
|
|
3
|
+
export const CCLAW_VERSION = "0.1.1";
|
|
4
4
|
export const FLOW_VERSION = "1.0.0";
|
|
5
5
|
export const DEFAULT_HARNESSES = [
|
|
6
6
|
"claude",
|
|
@@ -12,6 +12,7 @@ export const REQUIRED_DIRS = [
|
|
|
12
12
|
RUNTIME_ROOT,
|
|
13
13
|
`${RUNTIME_ROOT}/commands`,
|
|
14
14
|
`${RUNTIME_ROOT}/skills`,
|
|
15
|
+
`${RUNTIME_ROOT}/contexts`,
|
|
15
16
|
`${RUNTIME_ROOT}/templates`,
|
|
16
17
|
`${RUNTIME_ROOT}/artifacts`,
|
|
17
18
|
`${RUNTIME_ROOT}/state`,
|
|
@@ -30,7 +31,9 @@ export const REQUIRED_GITIGNORE_PATTERNS = [
|
|
|
30
31
|
".codex/commands/cc-*.md",
|
|
31
32
|
".claude/hooks/hooks.json",
|
|
32
33
|
".cursor/hooks.json",
|
|
33
|
-
".codex/hooks.json"
|
|
34
|
+
".codex/hooks.json",
|
|
35
|
+
".opencode/plugins/cclaw-plugin.mjs",
|
|
36
|
+
".cursor/rules/cclaw-workflow.mdc"
|
|
34
37
|
];
|
|
35
38
|
export const COMMAND_FILE_ORDER = [
|
|
36
39
|
"brainstorm",
|
|
@@ -43,7 +46,7 @@ export const COMMAND_FILE_ORDER = [
|
|
|
43
46
|
"review",
|
|
44
47
|
"ship"
|
|
45
48
|
];
|
|
46
|
-
export const UTILITY_COMMANDS = ["learn", "autoplan"];
|
|
49
|
+
export const UTILITY_COMMANDS = ["learn", "autoplan", "next"];
|
|
47
50
|
export const SUBAGENT_SKILL_FOLDERS = [
|
|
48
51
|
"subagent-dev",
|
|
49
52
|
"parallel-dispatch"
|