cclaw-cli 0.51.21 → 0.51.22
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/config.d.ts +8 -1
- package/dist/config.js +9 -6
- package/dist/content/hook-manifest.d.ts +2 -4
- package/dist/content/hook-manifest.js +4 -3
- package/dist/content/meta-skill.js +7 -9
- package/dist/content/next-command.js +2 -2
- package/dist/content/node-hooks.js +13 -3
- package/dist/content/review-loop.js +15 -5
- package/dist/content/review-prompts.js +1 -1
- package/dist/content/skills.js +3 -2
- package/dist/content/stage-schema.js +1 -0
- package/dist/content/stages/brainstorm.js +3 -3
- package/dist/content/stages/design.js +18 -17
- package/dist/content/stages/plan.js +2 -1
- package/dist/content/stages/review.js +10 -10
- package/dist/content/stages/scope.js +13 -13
- package/dist/content/stages/spec.js +7 -5
- package/dist/content/stages/tdd.js +2 -2
- package/dist/content/start-command.d.ts +4 -3
- package/dist/content/start-command.js +21 -17
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +48 -28
- package/dist/content/view-command.js +3 -1
- package/dist/delegation.js +28 -8
- package/dist/doctor.js +147 -21
- package/dist/gate-evidence.js +19 -7
- package/dist/harness-adapters.js +1 -5
- package/dist/install.js +87 -24
- package/dist/internal/advance-stage.js +90 -11
- package/dist/knowledge-store.d.ts +4 -1
- package/dist/knowledge-store.js +24 -14
- package/dist/retro-gate.d.ts +1 -0
- package/dist/retro-gate.js +9 -9
- package/dist/run-archive.js +19 -1
- package/dist/run-persistence.js +6 -2
- package/dist/tdd-cycle.js +6 -3
- package/package.json +1 -1
package/dist/delegation.js
CHANGED
|
@@ -103,6 +103,9 @@ async function detectReviewTriggers(projectRoot) {
|
|
|
103
103
|
return empty;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
function hasValidWaiverReason(value) {
|
|
107
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
108
|
+
}
|
|
106
109
|
function isDelegationTokenUsage(value) {
|
|
107
110
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
108
111
|
return false;
|
|
@@ -130,6 +133,7 @@ function isDelegationEntry(value) {
|
|
|
130
133
|
Number.isFinite(o.retryCount) &&
|
|
131
134
|
Number.isInteger(o.retryCount) &&
|
|
132
135
|
o.retryCount >= 0);
|
|
136
|
+
const waiverOk = o.status !== "waived" || hasValidWaiverReason(o.waiverReason);
|
|
133
137
|
return (typeof o.stage === "string" &&
|
|
134
138
|
typeof o.agent === "string" &&
|
|
135
139
|
modeOk &&
|
|
@@ -141,6 +145,7 @@ function isDelegationEntry(value) {
|
|
|
141
145
|
(o.endTs === undefined || typeof o.endTs === "string") &&
|
|
142
146
|
(o.taskId === undefined || typeof o.taskId === "string") &&
|
|
143
147
|
(o.waiverReason === undefined || typeof o.waiverReason === "string") &&
|
|
148
|
+
waiverOk &&
|
|
144
149
|
(o.runId === undefined || typeof o.runId === "string") &&
|
|
145
150
|
(o.fulfillmentMode === undefined ||
|
|
146
151
|
o.fulfillmentMode === "isolated" ||
|
|
@@ -165,8 +170,10 @@ function parseLedger(raw, runId) {
|
|
|
165
170
|
for (const item of entriesRaw) {
|
|
166
171
|
if (isDelegationEntry(item)) {
|
|
167
172
|
const ts = item.startTs ?? item.ts ?? new Date().toISOString();
|
|
168
|
-
const
|
|
169
|
-
|
|
173
|
+
const isLegacyCompletion = item.fulfillmentMode === undefined &&
|
|
174
|
+
item.schemaVersion === undefined &&
|
|
175
|
+
item.status === "completed";
|
|
176
|
+
const inferredFulfillmentMode = item.fulfillmentMode ?? (isLegacyCompletion ? "isolated" : undefined);
|
|
170
177
|
entries.push({
|
|
171
178
|
...item,
|
|
172
179
|
spanId: item.spanId ?? createSpanId(),
|
|
@@ -205,6 +212,9 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
205
212
|
const filePath = delegationLogPath(projectRoot);
|
|
206
213
|
const prior = await readDelegationLedger(projectRoot);
|
|
207
214
|
const startTs = entry.startTs ?? entry.ts ?? new Date().toISOString();
|
|
215
|
+
if (entry.status === "waived" && !hasValidWaiverReason(entry.waiverReason)) {
|
|
216
|
+
throw new Error("waived delegation entries require a non-empty waiverReason");
|
|
217
|
+
}
|
|
208
218
|
const stamped = { ...entry, runId: entry.runId ?? activeRunId };
|
|
209
219
|
stamped.spanId = entry.spanId ?? createSpanId();
|
|
210
220
|
stamped.startTs = startTs;
|
|
@@ -219,10 +229,19 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
219
229
|
stamped.evidenceRefs = [];
|
|
220
230
|
}
|
|
221
231
|
if (stamped.status === "completed" && stamped.fulfillmentMode === undefined) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
const activeFallback = process.env.CCLAW_ACTIVE_HARNESS
|
|
233
|
+
? HARNESS_ADAPTERS[process.env.CCLAW_ACTIVE_HARNESS]
|
|
234
|
+
?.capabilities.subagentFallback
|
|
235
|
+
: undefined;
|
|
236
|
+
if (activeFallback) {
|
|
237
|
+
stamped.fulfillmentMode = expectedFulfillmentMode([activeFallback]);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const config = await readConfig(projectRoot).catch(() => null);
|
|
241
|
+
const harnesses = config?.harnesses ?? [];
|
|
242
|
+
const fallbacks = harnesses.map((h) => HARNESS_ADAPTERS[h].capabilities.subagentFallback);
|
|
243
|
+
stamped.fulfillmentMode = expectedFulfillmentMode(fallbacks);
|
|
244
|
+
}
|
|
226
245
|
}
|
|
227
246
|
// Idempotency: if a caller (or a retried hook) tries to append a row
|
|
228
247
|
// with a spanId that already exists in the ledger, treat it as a no-op
|
|
@@ -256,10 +275,11 @@ export function expectedFulfillmentMode(fallbacks) {
|
|
|
256
275
|
return "harness-waiver";
|
|
257
276
|
}
|
|
258
277
|
export async function checkMandatoryDelegations(projectRoot, stage, options = {}) {
|
|
259
|
-
const
|
|
260
|
-
const { activeRunId } = await readFlowState(projectRoot, {
|
|
278
|
+
const flowState = await readFlowState(projectRoot, {
|
|
261
279
|
repairFeatureSystem: options.repairFeatureSystem
|
|
262
280
|
});
|
|
281
|
+
const mandatory = stageSchema(stage, flowState.track).mandatoryDelegations;
|
|
282
|
+
const { activeRunId } = flowState;
|
|
263
283
|
const ledger = await readDelegationLedger(projectRoot);
|
|
264
284
|
const forStage = ledger.entries.filter((e) => e.stage === stage);
|
|
265
285
|
const forRun = forStage.filter((e) => e.runId === activeRunId);
|
package/dist/doctor.js
CHANGED
|
@@ -25,6 +25,7 @@ import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_
|
|
|
25
25
|
import { validateHookDocument } from "./hook-schema.js";
|
|
26
26
|
import { validateKnowledgeEntry } from "./knowledge-store.js";
|
|
27
27
|
import { readSeedShelf } from "./content/seed-shelf.js";
|
|
28
|
+
import { evaluateRetroGate } from "./retro-gate.js";
|
|
28
29
|
const execFileAsync = promisify(execFile);
|
|
29
30
|
async function isGitRepo(projectRoot) {
|
|
30
31
|
try {
|
|
@@ -149,18 +150,30 @@ function knowledgeRoutingSurfaceIsDiscoverable(content) {
|
|
|
149
150
|
return ["trigger", "action", "origin_run"].every((term) => normalized.includes(term));
|
|
150
151
|
}
|
|
151
152
|
async function commandAvailable(command) {
|
|
153
|
+
const version = await commandVersion(command);
|
|
154
|
+
return version.available;
|
|
155
|
+
}
|
|
156
|
+
async function commandVersion(command, args = ["--version"]) {
|
|
152
157
|
try {
|
|
153
158
|
if (process.platform === "win32") {
|
|
154
159
|
await execFileAsync("where", [command]);
|
|
155
|
-
return true;
|
|
156
160
|
}
|
|
157
|
-
await execFileAsync(command,
|
|
158
|
-
return true;
|
|
161
|
+
const { stdout, stderr } = await execFileAsync(command, args);
|
|
162
|
+
return { available: true, output: `${stdout}${stderr}`.trim() };
|
|
159
163
|
}
|
|
160
164
|
catch {
|
|
161
|
-
return false;
|
|
165
|
+
return { available: false, output: "" };
|
|
162
166
|
}
|
|
163
167
|
}
|
|
168
|
+
function parseNodeMajor(versionOutput) {
|
|
169
|
+
const match = /v?(\d+)\./u.exec(versionOutput);
|
|
170
|
+
if (!match)
|
|
171
|
+
return null;
|
|
172
|
+
return Number(match[1]);
|
|
173
|
+
}
|
|
174
|
+
function gitVersionLooksUsable(versionOutput) {
|
|
175
|
+
return /git version \d+\.\d+/iu.test(versionOutput);
|
|
176
|
+
}
|
|
164
177
|
function stripJsonCommentsOutsideStrings(input) {
|
|
165
178
|
let out = "";
|
|
166
179
|
let i = 0;
|
|
@@ -308,6 +321,87 @@ async function opencodeRegistrationCheck(projectRoot) {
|
|
|
308
321
|
}
|
|
309
322
|
return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${expected}` };
|
|
310
323
|
}
|
|
324
|
+
async function initRecoveryCheck(projectRoot) {
|
|
325
|
+
const sentinelPath = path.join(projectRoot, RUNTIME_ROOT, "state", ".init-in-progress");
|
|
326
|
+
if (!(await exists(sentinelPath))) {
|
|
327
|
+
return { ok: true, details: "no partial init/sync sentinel found" };
|
|
328
|
+
}
|
|
329
|
+
let summary = `${RUNTIME_ROOT}/state/.init-in-progress sentinel present`;
|
|
330
|
+
try {
|
|
331
|
+
const parsed = JSON.parse(await fs.readFile(sentinelPath, "utf8"));
|
|
332
|
+
const operation = typeof parsed.operation === "string" ? parsed.operation : "unknown";
|
|
333
|
+
const startedAt = typeof parsed.startedAt === "string" ? parsed.startedAt : "unknown";
|
|
334
|
+
summary = `${summary} (operation=${operation}, startedAt=${startedAt})`;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
summary = `${summary} (unreadable sentinel payload)`;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
ok: false,
|
|
341
|
+
details: `${summary}. Fix: inspect generated runtime files, then rerun cclaw sync or remove the sentinel only after confirming the runtime is complete.`
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function archiveIntegrityCheck(projectRoot) {
|
|
345
|
+
const runsDir = path.join(projectRoot, RUNTIME_ROOT, "runs");
|
|
346
|
+
if (!(await exists(runsDir))) {
|
|
347
|
+
return { ok: true, details: `${RUNTIME_ROOT}/runs is absent; no archives to inspect yet` };
|
|
348
|
+
}
|
|
349
|
+
let entries;
|
|
350
|
+
try {
|
|
351
|
+
entries = await fs.readdir(runsDir, { withFileTypes: true });
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
355
|
+
return { ok: false, details: `unable to inspect ${RUNTIME_ROOT}/runs (${reason})` };
|
|
356
|
+
}
|
|
357
|
+
const problems = [];
|
|
358
|
+
for (const entry of entries) {
|
|
359
|
+
if (!entry.isDirectory())
|
|
360
|
+
continue;
|
|
361
|
+
const runId = entry.name;
|
|
362
|
+
const runPath = path.join(runsDir, runId);
|
|
363
|
+
const relRunPath = `${RUNTIME_ROOT}/runs/${runId}`;
|
|
364
|
+
if (await exists(path.join(runPath, ".archive-in-progress"))) {
|
|
365
|
+
problems.push(`${relRunPath}/.archive-in-progress sentinel present`);
|
|
366
|
+
}
|
|
367
|
+
const manifestPath = path.join(runPath, "archive-manifest.json");
|
|
368
|
+
if (!(await exists(manifestPath))) {
|
|
369
|
+
problems.push(`${relRunPath} missing archive-manifest.json`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
let manifest;
|
|
373
|
+
try {
|
|
374
|
+
manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
378
|
+
problems.push(`${relRunPath}/archive-manifest.json unreadable (${reason})`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const stateFiles = Array.isArray(manifest.snapshottedStateFiles)
|
|
382
|
+
? manifest.snapshottedStateFiles.filter((value) => typeof value === "string")
|
|
383
|
+
: [];
|
|
384
|
+
const stateDir = path.join(runPath, "state");
|
|
385
|
+
if (stateFiles.length > 0 && !(await exists(stateDir))) {
|
|
386
|
+
problems.push(`${relRunPath} manifest lists state snapshot files but state/ is missing`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
for (const stateFile of stateFiles) {
|
|
390
|
+
if (stateFile.endsWith("/"))
|
|
391
|
+
continue;
|
|
392
|
+
if (!(await exists(path.join(stateDir, stateFile)))) {
|
|
393
|
+
problems.push(`${relRunPath}/state missing ${stateFile} listed in manifest`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (problems.length === 0) {
|
|
398
|
+
return { ok: true, details: "no partial archive sentinels or incomplete archive snapshots found" };
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
ok: false,
|
|
402
|
+
details: `${problems.join("; ")}. Fix: inspect the archive directory, retry archive if the active run was restored, or recover/rollback artifacts and state from the snapshot before removing the sentinel.`
|
|
403
|
+
};
|
|
404
|
+
}
|
|
311
405
|
async function opencodePluginRuntimeShapeCheck(projectRoot) {
|
|
312
406
|
const pluginPath = path.join(projectRoot, ".opencode/plugins/cclaw-plugin.mjs");
|
|
313
407
|
if (!(await exists(pluginPath))) {
|
|
@@ -981,11 +1075,36 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
981
1075
|
details: registration.details
|
|
982
1076
|
});
|
|
983
1077
|
}
|
|
984
|
-
const
|
|
1078
|
+
const nodeVersion = await commandVersion("node");
|
|
1079
|
+
const nodeMajor = parseNodeMajor(nodeVersion.output);
|
|
985
1080
|
checks.push({
|
|
986
1081
|
name: "capability:required:node",
|
|
987
|
-
ok:
|
|
988
|
-
details:
|
|
1082
|
+
ok: nodeVersion.available,
|
|
1083
|
+
details: nodeVersion.available
|
|
1084
|
+
? `node binary available (${nodeVersion.output || "version unknown"})`
|
|
1085
|
+
: "node is required for cclaw runtime scripts and CLI wiring"
|
|
1086
|
+
});
|
|
1087
|
+
checks.push({
|
|
1088
|
+
name: "capability:required:node_version",
|
|
1089
|
+
ok: nodeVersion.available && nodeMajor !== null && nodeMajor >= 20,
|
|
1090
|
+
details: nodeVersion.available
|
|
1091
|
+
? `node >=20 required; detected ${nodeVersion.output || "unknown version"}`
|
|
1092
|
+
: "node version check skipped because node binary is unavailable"
|
|
1093
|
+
});
|
|
1094
|
+
const gitVersion = await commandVersion("git");
|
|
1095
|
+
checks.push({
|
|
1096
|
+
name: "capability:required:git",
|
|
1097
|
+
ok: gitVersion.available,
|
|
1098
|
+
details: gitVersion.available
|
|
1099
|
+
? `git binary available (${gitVersion.output || "version unknown"})`
|
|
1100
|
+
: "git is required for repository detection, hook setup, and doctor checks"
|
|
1101
|
+
});
|
|
1102
|
+
checks.push({
|
|
1103
|
+
name: "capability:required:git_version",
|
|
1104
|
+
ok: gitVersion.available && gitVersionLooksUsable(gitVersion.output),
|
|
1105
|
+
details: gitVersion.available
|
|
1106
|
+
? `git version output: ${gitVersion.output || "unknown version"}`
|
|
1107
|
+
: "git version check skipped because git binary is unavailable"
|
|
989
1108
|
});
|
|
990
1109
|
const windowsHookConfigCandidates = [
|
|
991
1110
|
path.join(projectRoot, ".claude/hooks/hooks.json"),
|
|
@@ -1083,12 +1202,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1083
1202
|
const key = `${trigger} => ${action}`;
|
|
1084
1203
|
triggerActionCounts.set(key, (triggerActionCounts.get(key) ?? 0) + 1);
|
|
1085
1204
|
}
|
|
1086
|
-
const missing = requiredV2Fields.some((field) =>
|
|
1087
|
-
if (field === "origin_run" && Object.prototype.hasOwnProperty.call(parsed, "origin_feature")) {
|
|
1088
|
-
return false;
|
|
1089
|
-
}
|
|
1090
|
-
return !Object.prototype.hasOwnProperty.call(parsed, field);
|
|
1091
|
-
});
|
|
1205
|
+
const missing = requiredV2Fields.some((field) => !Object.prototype.hasOwnProperty.call(parsed, field));
|
|
1092
1206
|
if (missing) {
|
|
1093
1207
|
missingSchemaV2Fields += 1;
|
|
1094
1208
|
}
|
|
@@ -1388,17 +1502,17 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1388
1502
|
? "no stale stages pending acknowledgement"
|
|
1389
1503
|
: `stale stages pending acknowledgement: ${staleStages.join(", ")}`
|
|
1390
1504
|
});
|
|
1391
|
-
const
|
|
1392
|
-
const retroComplete = !retroRequired ||
|
|
1393
|
-
(typeof flowState.retro.completedAt === "string" && flowState.retro.compoundEntries > 0);
|
|
1505
|
+
const retroGateStatus = await evaluateRetroGate(projectRoot, flowState);
|
|
1394
1506
|
checks.push({
|
|
1395
1507
|
name: "state:retro_gate",
|
|
1396
|
-
ok:
|
|
1397
|
-
details:
|
|
1398
|
-
?
|
|
1399
|
-
?
|
|
1508
|
+
ok: retroGateStatus.completed,
|
|
1509
|
+
details: retroGateStatus.completed
|
|
1510
|
+
? retroGateStatus.required
|
|
1511
|
+
? retroGateStatus.skipped
|
|
1512
|
+
? "retro gate complete (retro skipped with recorded closeout decision)"
|
|
1513
|
+
: `retro gate complete (${retroGateStatus.compoundEntries} compound entries)`
|
|
1400
1514
|
: "retro gate not required yet (ship not completed)"
|
|
1401
|
-
: "retro gate incomplete: ship flow requires recorded retrospective evidence."
|
|
1515
|
+
: "retro gate incomplete: ship flow requires recorded retrospective evidence or an explicit retro skip."
|
|
1402
1516
|
});
|
|
1403
1517
|
const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
|
|
1404
1518
|
const tddLogExists = await exists(tddLogPath);
|
|
@@ -1444,6 +1558,18 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1444
1558
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs")),
|
|
1445
1559
|
details: `${RUNTIME_ROOT}/runs must exist for archived run snapshots`
|
|
1446
1560
|
});
|
|
1561
|
+
const initRecovery = await initRecoveryCheck(projectRoot);
|
|
1562
|
+
checks.push({
|
|
1563
|
+
name: "state:init_recovery",
|
|
1564
|
+
ok: initRecovery.ok,
|
|
1565
|
+
details: initRecovery.details
|
|
1566
|
+
});
|
|
1567
|
+
const archiveIntegrity = await archiveIntegrityCheck(projectRoot);
|
|
1568
|
+
checks.push({
|
|
1569
|
+
name: "runs:archive_integrity",
|
|
1570
|
+
ok: archiveIntegrity.ok,
|
|
1571
|
+
details: archiveIntegrity.details
|
|
1572
|
+
});
|
|
1447
1573
|
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
|
|
1448
1574
|
repairFeatureSystem: false
|
|
1449
1575
|
});
|
package/dist/gate-evidence.js
CHANGED
|
@@ -336,11 +336,27 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
336
336
|
if (stage === "design") {
|
|
337
337
|
const researchGateRequired = schema.requiredGates.some((gate) => gate.id === "design_research_complete" && gate.tier === "required");
|
|
338
338
|
if (researchGateRequired) {
|
|
339
|
+
const designMarkdown = await readArtifactMarkdown(projectRoot, "03-design.md");
|
|
340
|
+
const inlineResearchBody = designMarkdown
|
|
341
|
+
? extractMarkdownSectionBody(designMarkdown, "Research Fleet Synthesis")
|
|
342
|
+
: null;
|
|
343
|
+
const inlineResearchLines = inlineResearchBody
|
|
344
|
+
? inlineResearchBody
|
|
345
|
+
.split(/\r?\n/gu)
|
|
346
|
+
.map((line) => line.trim())
|
|
347
|
+
.filter((line) => line.length > 0)
|
|
348
|
+
.filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line))
|
|
349
|
+
.filter((line) => !/\b(?:TODO|TBD|FIXME|pending)\b/iu.test(line) &&
|
|
350
|
+
!/<fill-in>/iu.test(line) &&
|
|
351
|
+
!/^>\s*Default path:/iu.test(line) &&
|
|
352
|
+
!/^\|\s*compact inline synthesis\s*\|\s*\|\s*\|\s*\|?\s*$/iu.test(line))
|
|
353
|
+
: [];
|
|
354
|
+
const inlineResearchComplete = inlineResearchLines.length > 0;
|
|
339
355
|
const researchMarkdown = await readArtifactMarkdown(projectRoot, "02a-research.md");
|
|
340
|
-
if (!researchMarkdown) {
|
|
341
|
-
issues.push("design research gate blocked (design_research_complete):
|
|
356
|
+
if (!inlineResearchComplete && !researchMarkdown) {
|
|
357
|
+
issues.push("design research gate blocked (design_research_complete): fill `Research Fleet Synthesis` in `.cclaw/artifacts/03-design.md`, or write `.cclaw/artifacts/02a-research.md` for deep/high-risk research.");
|
|
342
358
|
}
|
|
343
|
-
else {
|
|
359
|
+
else if (researchMarkdown) {
|
|
344
360
|
const missingSections = [];
|
|
345
361
|
for (const section of DESIGN_RESEARCH_REQUIRED_SECTIONS) {
|
|
346
362
|
const body = extractMarkdownSectionBody(researchMarkdown, section);
|
|
@@ -353,10 +369,6 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
353
369
|
.map((line) => line.trim())
|
|
354
370
|
.filter((line) => line.length > 0)
|
|
355
371
|
.filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line));
|
|
356
|
-
// `<fill-in>` needs its own check because `\b` does not match
|
|
357
|
-
// around `<`/`>` (non-word characters), so the previous combined
|
|
358
|
-
// pattern `\b(?:...|<fill-in>)\b` silently never matched placeholder
|
|
359
|
-
// templates that used angle-bracket form.
|
|
360
372
|
const nonPlaceholder = meaningfulLines.filter((line) => !/\b(?:TODO|TBD|FIXME|pending)\b/iu.test(line) &&
|
|
361
373
|
!/<fill-in>/iu.test(line));
|
|
362
374
|
if (nonPlaceholder.length === 0) {
|
package/dist/harness-adapters.js
CHANGED
|
@@ -134,11 +134,7 @@ export function harnessTier(harnessId) {
|
|
|
134
134
|
capabilities.hookSurface === "full") {
|
|
135
135
|
return "tier1";
|
|
136
136
|
}
|
|
137
|
-
if (capabilities.hookSurface
|
|
138
|
-
capabilities.hookSurface === "plugin" ||
|
|
139
|
-
capabilities.hookSurface === "limited" ||
|
|
140
|
-
capabilities.nativeSubagentDispatch === "generic" ||
|
|
141
|
-
capabilities.nativeSubagentDispatch === "partial") {
|
|
137
|
+
if (capabilities.hookSurface !== "none" || capabilities.nativeSubagentDispatch !== "none") {
|
|
142
138
|
return "tier2";
|
|
143
139
|
}
|
|
144
140
|
return "tier3";
|
package/dist/install.js
CHANGED
|
@@ -34,10 +34,17 @@ const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
|
|
|
34
34
|
const CURSOR_RULE_REL_PATH = ".cursor/rules/cclaw-workflow.mdc";
|
|
35
35
|
const GIT_HOOK_MANAGED_MARKER = "cclaw-managed-git-hook";
|
|
36
36
|
const GIT_HOOK_RUNTIME_REL_DIR = `${RUNTIME_ROOT}/hooks/git`;
|
|
37
|
+
const INIT_SENTINEL_FILE = ".init-in-progress";
|
|
37
38
|
const execFileAsync = promisify(execFile);
|
|
38
39
|
function runtimePath(projectRoot, ...segments) {
|
|
39
40
|
return path.join(projectRoot, RUNTIME_ROOT, ...segments);
|
|
40
41
|
}
|
|
42
|
+
async function writeInitSentinel(projectRoot, operation) {
|
|
43
|
+
const sentinelPath = runtimePath(projectRoot, "state", INIT_SENTINEL_FILE);
|
|
44
|
+
await ensureDir(path.dirname(sentinelPath));
|
|
45
|
+
await writeFileSafe(sentinelPath, `${JSON.stringify({ operation, startedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
46
|
+
return sentinelPath;
|
|
47
|
+
}
|
|
41
48
|
async function removeBestEffort(targetPath, recursive = false) {
|
|
42
49
|
try {
|
|
43
50
|
await fs.rm(targetPath, { recursive, force: true });
|
|
@@ -653,6 +660,54 @@ async function backupHookFile(projectRoot, hookFilePath, rawContent) {
|
|
|
653
660
|
await pruneOldHookBackups(backupsDir);
|
|
654
661
|
return backupPath;
|
|
655
662
|
}
|
|
663
|
+
function normalizeHookCommandForDedupe(command) {
|
|
664
|
+
return command.trim().replace(/\s+/gu, " ").replace(/\\/gu, "/");
|
|
665
|
+
}
|
|
666
|
+
function dedupeHookEntryByCommand(entry, seenCommands) {
|
|
667
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
668
|
+
return entry;
|
|
669
|
+
}
|
|
670
|
+
const obj = entry;
|
|
671
|
+
let changed = false;
|
|
672
|
+
if (typeof obj.command === "string") {
|
|
673
|
+
const normalized = normalizeHookCommandForDedupe(obj.command);
|
|
674
|
+
if (seenCommands.has(normalized)) {
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
seenCommands.add(normalized);
|
|
678
|
+
}
|
|
679
|
+
if (Array.isArray(obj.hooks)) {
|
|
680
|
+
const hooks = [];
|
|
681
|
+
for (const nested of obj.hooks) {
|
|
682
|
+
const deduped = dedupeHookEntryByCommand(nested, seenCommands);
|
|
683
|
+
if (deduped !== undefined) {
|
|
684
|
+
hooks.push(deduped);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
changed = true;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (hooks.length !== obj.hooks.length) {
|
|
691
|
+
changed = true;
|
|
692
|
+
}
|
|
693
|
+
if (hooks.length === 0 && typeof obj.command !== "string") {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
return changed ? { ...obj, hooks } : entry;
|
|
697
|
+
}
|
|
698
|
+
return entry;
|
|
699
|
+
}
|
|
700
|
+
function dedupeHookEntriesByCommand(entries) {
|
|
701
|
+
const seenCommands = new Set();
|
|
702
|
+
const deduped = [];
|
|
703
|
+
for (const entry of entries) {
|
|
704
|
+
const next = dedupeHookEntryByCommand(entry, seenCommands);
|
|
705
|
+
if (next !== undefined) {
|
|
706
|
+
deduped.push(next);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return deduped;
|
|
710
|
+
}
|
|
656
711
|
function mergeHookDocuments(existingDoc, generatedDoc) {
|
|
657
712
|
const generatedRoot = toObject(generatedDoc) ?? {};
|
|
658
713
|
const generatedHooks = toObject(generatedRoot.hooks) ?? {};
|
|
@@ -664,7 +719,7 @@ function mergeHookDocuments(existingDoc, generatedDoc) {
|
|
|
664
719
|
const existingEntries = existingHooks[eventName];
|
|
665
720
|
if (Array.isArray(generatedEntries)) {
|
|
666
721
|
const preservedEntries = Array.isArray(existingEntries) ? existingEntries : [];
|
|
667
|
-
mergedHooks[eventName] = [...generatedEntries, ...preservedEntries];
|
|
722
|
+
mergedHooks[eventName] = dedupeHookEntriesByCommand([...generatedEntries, ...preservedEntries]);
|
|
668
723
|
continue;
|
|
669
724
|
}
|
|
670
725
|
// Defensive: malformed generated event payload must not wipe user hooks.
|
|
@@ -944,26 +999,34 @@ async function cleanStaleFiles(projectRoot) {
|
|
|
944
999
|
// Keep user-owned custom assets under .cclaw/agents and .cclaw/skills.
|
|
945
1000
|
// Legacy managed removals happen in cleanLegacyArtifacts() with explicit paths.
|
|
946
1001
|
}
|
|
947
|
-
async function materializeRuntime(projectRoot, config, forceStateReset) {
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1002
|
+
async function materializeRuntime(projectRoot, config, forceStateReset, operation = "sync") {
|
|
1003
|
+
const sentinelPath = await writeInitSentinel(projectRoot, operation);
|
|
1004
|
+
try {
|
|
1005
|
+
const harnesses = config.harnesses;
|
|
1006
|
+
await ensureStructure(projectRoot);
|
|
1007
|
+
await cleanLegacyArtifacts(projectRoot);
|
|
1008
|
+
await cleanStaleFiles(projectRoot);
|
|
1009
|
+
await Promise.all([
|
|
1010
|
+
writeEntryCommands(projectRoot),
|
|
1011
|
+
writeSkills(projectRoot, config),
|
|
1012
|
+
writeArtifactTemplates(projectRoot),
|
|
1013
|
+
writeRulebook(projectRoot)
|
|
1014
|
+
]);
|
|
1015
|
+
await writeState(projectRoot, config, forceStateReset);
|
|
1016
|
+
await ensureRunSystem(projectRoot, { createIfMissing: false });
|
|
1017
|
+
await ensureKnowledgeStore(projectRoot);
|
|
1018
|
+
await writeHooks(projectRoot, config);
|
|
1019
|
+
await syncDisabledHarnessArtifacts(projectRoot, harnesses);
|
|
1020
|
+
await syncManagedGitHooks(projectRoot, config);
|
|
1021
|
+
await syncHarnessShims(projectRoot, harnesses);
|
|
1022
|
+
await writeCursorWorkflowRule(projectRoot, harnesses);
|
|
1023
|
+
await ensureGitignore(projectRoot);
|
|
1024
|
+
await fs.unlink(sentinelPath).catch(() => undefined);
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
// Leave the sentinel in place so doctor can surface the interrupted run.
|
|
1028
|
+
throw error;
|
|
1029
|
+
}
|
|
967
1030
|
}
|
|
968
1031
|
export async function initCclaw(options) {
|
|
969
1032
|
const baseConfig = createDefaultConfig(options.harnesses, options.track);
|
|
@@ -978,7 +1041,7 @@ export async function initCclaw(options) {
|
|
|
978
1041
|
// and only appear in the on-disk file when the user sets them explicitly
|
|
979
1042
|
// or a non-default value was detected (e.g. languageRulePacks).
|
|
980
1043
|
await writeConfig(options.projectRoot, config, { mode: "minimal" });
|
|
981
|
-
await materializeRuntime(options.projectRoot, config, true);
|
|
1044
|
+
await materializeRuntime(options.projectRoot, config, true, "init");
|
|
982
1045
|
}
|
|
983
1046
|
export async function syncCclaw(projectRoot) {
|
|
984
1047
|
const configExists = await exists(configPath(projectRoot));
|
|
@@ -996,7 +1059,7 @@ export async function syncCclaw(projectRoot) {
|
|
|
996
1059
|
await writeConfig(projectRoot, defaultConfig);
|
|
997
1060
|
config = defaultConfig;
|
|
998
1061
|
}
|
|
999
|
-
await materializeRuntime(projectRoot, config, false);
|
|
1062
|
+
await materializeRuntime(projectRoot, config, false, "sync");
|
|
1000
1063
|
}
|
|
1001
1064
|
/**
|
|
1002
1065
|
* Refresh generated files in `.cclaw/` without touching user-authored
|
|
@@ -1020,7 +1083,7 @@ export async function upgradeCclaw(projectRoot) {
|
|
|
1020
1083
|
mode: "minimal",
|
|
1021
1084
|
advancedKeysPresent
|
|
1022
1085
|
});
|
|
1023
|
-
await materializeRuntime(projectRoot, upgraded, false);
|
|
1086
|
+
await materializeRuntime(projectRoot, upgraded, false, "upgrade");
|
|
1024
1087
|
}
|
|
1025
1088
|
function stripManagedHookCommands(value) {
|
|
1026
1089
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|