cclaw-cli 0.48.2 → 0.48.3
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 +10 -3
- package/dist/cli.js +8 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +13 -3
- package/dist/content/contracts.d.ts +2 -2
- package/dist/content/contracts.js +2 -2
- package/dist/content/core-agents.d.ts +1 -1
- package/dist/content/core-agents.js +1 -1
- package/dist/content/hooks.js +16 -15
- package/dist/content/next-command.js +4 -2
- package/dist/content/observe.d.ts +2 -2
- package/dist/content/observe.js +83 -13
- package/dist/content/opencode-plugin.js +227 -45
- package/dist/content/stage-schema.js +1 -1
- package/dist/delegation.js +1 -1
- package/dist/doctor.js +35 -1
- package/dist/eval/runner.js +36 -4
- package/dist/feature-system.js +2 -2
- package/dist/fs-utils.d.ts +4 -1
- package/dist/fs-utils.js +9 -2
- package/dist/gate-evidence.js +1 -1
- package/dist/install.js +24 -22
- package/dist/internal/advance-stage.js +4 -2
- package/dist/knowledge-store.d.ts +8 -0
- package/dist/knowledge-store.js +113 -33
- package/dist/retro-gate.js +33 -23
- package/dist/run-archive.js +6 -9
- package/dist/run-persistence.js +1 -1
- package/dist/trace-matrix.js +7 -7
- package/package.json +1 -1
|
@@ -2,7 +2,8 @@ import { RUNTIME_ROOT } from "../constants.js";
|
|
|
2
2
|
import { META_SKILL_NAME } from "./meta-skill.js";
|
|
3
3
|
export function opencodePluginJs(_options = {}) {
|
|
4
4
|
return `// cclaw OpenCode plugin — generated by cclaw sync
|
|
5
|
-
import { existsSync, mkdirSync
|
|
5
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { readFile, stat } from "node:fs/promises";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
|
|
8
9
|
export default function cclawPlugin(ctx) {
|
|
@@ -33,9 +34,9 @@ export default function cclawPlugin(ctx) {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function readFlowState() {
|
|
37
|
+
async function readFlowState() {
|
|
37
38
|
try {
|
|
38
|
-
const raw =
|
|
39
|
+
const raw = await readFile(flowStatePath, "utf8");
|
|
39
40
|
const state = JSON.parse(raw);
|
|
40
41
|
return {
|
|
41
42
|
stage: typeof state.currentStage === "string" ? state.currentStage : "none",
|
|
@@ -47,23 +48,23 @@ export default function cclawPlugin(ctx) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function readFileText(filePath) {
|
|
51
|
+
async function readFileText(filePath) {
|
|
51
52
|
try {
|
|
52
|
-
return
|
|
53
|
+
return await readFile(filePath, "utf8");
|
|
53
54
|
} catch {
|
|
54
55
|
return "";
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
function readTailLines(filePath, maxLines) {
|
|
59
|
-
const text = readFileText(filePath).trim();
|
|
59
|
+
async function readTailLines(filePath, maxLines) {
|
|
60
|
+
const text = (await readFileText(filePath)).trim();
|
|
60
61
|
if (!text) return [];
|
|
61
62
|
return text.split(/\\r?\\n/).slice(-maxLines);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function readCheckpointSummary() {
|
|
65
|
+
async function readCheckpointSummary() {
|
|
65
66
|
try {
|
|
66
|
-
const raw = readFileText(checkpointPath);
|
|
67
|
+
const raw = await readFileText(checkpointPath);
|
|
67
68
|
if (!raw) return "";
|
|
68
69
|
const cp = JSON.parse(raw);
|
|
69
70
|
return \`Checkpoint: stage=\${cp.stage || "none"}, status=\${cp.status || "unknown"}, run=\${cp.runId || "none"}, at=\${cp.timestamp || "unknown"}\`;
|
|
@@ -72,10 +73,10 @@ export default function cclawPlugin(ctx) {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
function readContextMode() {
|
|
76
|
+
async function readContextMode() {
|
|
76
77
|
let mode = "default";
|
|
77
78
|
try {
|
|
78
|
-
const parsed = JSON.parse(readFileText(contextModePath));
|
|
79
|
+
const parsed = JSON.parse(await readFileText(contextModePath));
|
|
79
80
|
if (parsed && typeof parsed.activeMode === "string" && parsed.activeMode.trim().length > 0) {
|
|
80
81
|
mode = parsed.activeMode.trim();
|
|
81
82
|
}
|
|
@@ -87,9 +88,9 @@ export default function cclawPlugin(ctx) {
|
|
|
87
88
|
return { mode, guide };
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
function readRecentActivity() {
|
|
91
|
+
async function readRecentActivity() {
|
|
91
92
|
try {
|
|
92
|
-
const lines = readTailLines(activityPath, 5);
|
|
93
|
+
const lines = await readTailLines(activityPath, 5);
|
|
93
94
|
if (lines.length === 0) return [];
|
|
94
95
|
return lines
|
|
95
96
|
.map((line) => {
|
|
@@ -106,9 +107,9 @@ export default function cclawPlugin(ctx) {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
function readLatestContextWarning() {
|
|
110
|
+
async function readLatestContextWarning() {
|
|
110
111
|
try {
|
|
111
|
-
const line = readTailLines(contextWarningsPath, 1)[0];
|
|
112
|
+
const line = (await readTailLines(contextWarningsPath, 1))[0];
|
|
112
113
|
if (!line) return "";
|
|
113
114
|
try {
|
|
114
115
|
const parsed = JSON.parse(line);
|
|
@@ -122,8 +123,8 @@ export default function cclawPlugin(ctx) {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
function readKnowledgeDigest() {
|
|
126
|
-
const digest = readFileText(knowledgeDigestPath).trim();
|
|
126
|
+
async function readKnowledgeDigest() {
|
|
127
|
+
const digest = (await readFileText(knowledgeDigestPath)).trim();
|
|
127
128
|
if (!digest) {
|
|
128
129
|
return readTailLines(knowledgePath, 12);
|
|
129
130
|
}
|
|
@@ -134,68 +135,219 @@ export default function cclawPlugin(ctx) {
|
|
|
134
135
|
.filter((line) => !line.startsWith("#"));
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
const BOOTSTRAP_MARKER = "<!-- cclaw-bootstrap-v1 -->";
|
|
139
|
+
|
|
140
|
+
async function buildBootstrap() {
|
|
141
|
+
const flow = await readFlowState();
|
|
139
142
|
const parts = [
|
|
143
|
+
BOOTSTRAP_MARKER,
|
|
140
144
|
\`cclaw loaded. Flow: stage=\${flow.stage} (\${flow.completed}/8 completed, run=\${flow.activeRunId}). Active artifacts: ${RUNTIME_ROOT}/artifacts/\`
|
|
141
145
|
];
|
|
142
|
-
const contextMode = readContextMode();
|
|
146
|
+
const contextMode = await readContextMode();
|
|
143
147
|
parts.push(
|
|
144
148
|
contextMode.guide
|
|
145
149
|
? \`Context mode: \${contextMode.mode} (guide: \${contextMode.guide})\`
|
|
146
150
|
: \`Context mode: \${contextMode.mode}\`
|
|
147
151
|
);
|
|
148
152
|
|
|
149
|
-
const checkpoint = readCheckpointSummary();
|
|
153
|
+
const checkpoint = await readCheckpointSummary();
|
|
150
154
|
if (checkpoint) parts.push(checkpoint);
|
|
151
155
|
|
|
152
|
-
const digest = readFileText(sessionDigestPath).trim();
|
|
156
|
+
const digest = (await readFileText(sessionDigestPath)).trim();
|
|
153
157
|
if (digest) parts.push("Last session:", digest);
|
|
154
158
|
|
|
155
|
-
const activity = readRecentActivity();
|
|
159
|
+
const activity = await readRecentActivity();
|
|
156
160
|
if (activity.length > 0) parts.push("Recent stage activity:", ...activity);
|
|
157
161
|
|
|
158
|
-
const warning = readLatestContextWarning();
|
|
162
|
+
const warning = await readLatestContextWarning();
|
|
159
163
|
if (warning) parts.push("Latest context warning:", warning);
|
|
160
164
|
|
|
161
|
-
const knowledge = readKnowledgeDigest();
|
|
165
|
+
const knowledge = await readKnowledgeDigest();
|
|
162
166
|
if (knowledge.length > 0) parts.push("Knowledge digest (top relevant entries):", ...knowledge);
|
|
163
167
|
|
|
164
168
|
parts.push(
|
|
165
169
|
"If you discover a non-obvious rule or pattern, append one strict-schema JSON line to .cclaw/knowledge.jsonl using type: rule, pattern, lesson, or compound."
|
|
166
170
|
);
|
|
167
171
|
|
|
168
|
-
const meta = readFileText(metaSkillPath).trim();
|
|
172
|
+
const meta = (await readFileText(metaSkillPath)).trim();
|
|
169
173
|
if (meta) parts.push("", meta);
|
|
170
174
|
return parts.join("\\n");
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
let bootstrapCache = "";
|
|
178
|
+
let bootstrapMtimes = new Map();
|
|
179
|
+
let bootstrapRefreshPromise = null;
|
|
180
|
+
const BOOTSTRAP_SOURCE_PATHS = [
|
|
181
|
+
flowStatePath,
|
|
182
|
+
checkpointPath,
|
|
183
|
+
activityPath,
|
|
184
|
+
contextWarningsPath,
|
|
185
|
+
contextModePath,
|
|
186
|
+
sessionDigestPath,
|
|
187
|
+
knowledgePath,
|
|
188
|
+
knowledgeDigestPath,
|
|
189
|
+
metaSkillPath
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
async function readMtimeMs(filePath) {
|
|
193
|
+
try {
|
|
194
|
+
const st = await stat(filePath);
|
|
195
|
+
return Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0;
|
|
196
|
+
} catch {
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function snapshotBootstrapMtimes() {
|
|
202
|
+
const next = new Map();
|
|
203
|
+
for (const filePath of BOOTSTRAP_SOURCE_PATHS) {
|
|
204
|
+
next.set(filePath, await readMtimeMs(filePath));
|
|
205
|
+
}
|
|
206
|
+
return next;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function bootstrapNeedsRefresh() {
|
|
210
|
+
if (!bootstrapCache) return true;
|
|
211
|
+
for (const filePath of BOOTSTRAP_SOURCE_PATHS) {
|
|
212
|
+
const prev = bootstrapMtimes.get(filePath) ?? 0;
|
|
213
|
+
const now = await readMtimeMs(filePath);
|
|
214
|
+
if (prev !== now) return true;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
174
218
|
|
|
175
|
-
function refreshBootstrapCache() {
|
|
176
|
-
|
|
219
|
+
async function refreshBootstrapCache(force = false) {
|
|
220
|
+
if (!force && !(await bootstrapNeedsRefresh())) {
|
|
221
|
+
return bootstrapCache;
|
|
222
|
+
}
|
|
223
|
+
if (bootstrapRefreshPromise) {
|
|
224
|
+
return bootstrapRefreshPromise;
|
|
225
|
+
}
|
|
226
|
+
bootstrapRefreshPromise = (async () => {
|
|
227
|
+
const nextBootstrap = await buildBootstrap();
|
|
228
|
+
const nextMtimes = await snapshotBootstrapMtimes();
|
|
229
|
+
bootstrapCache = nextBootstrap;
|
|
230
|
+
bootstrapMtimes = nextMtimes;
|
|
231
|
+
return bootstrapCache;
|
|
232
|
+
})();
|
|
233
|
+
try {
|
|
234
|
+
return await bootstrapRefreshPromise;
|
|
235
|
+
} finally {
|
|
236
|
+
bootstrapRefreshPromise = null;
|
|
237
|
+
}
|
|
177
238
|
}
|
|
178
239
|
|
|
179
240
|
function getBootstrap() {
|
|
180
|
-
if (!bootstrapCache) refreshBootstrapCache();
|
|
181
241
|
return bootstrapCache;
|
|
182
242
|
}
|
|
183
243
|
|
|
244
|
+
const MAX_CONCURRENT_HOOKS = 2;
|
|
245
|
+
let runningHookTasks = 0;
|
|
246
|
+
const pendingHookTasks = [];
|
|
247
|
+
|
|
248
|
+
function runNextHookTask() {
|
|
249
|
+
if (runningHookTasks >= MAX_CONCURRENT_HOOKS) return;
|
|
250
|
+
const queued = pendingHookTasks.shift();
|
|
251
|
+
if (!queued) return;
|
|
252
|
+
runningHookTasks += 1;
|
|
253
|
+
queued()
|
|
254
|
+
.catch(() => false)
|
|
255
|
+
.finally(() => {
|
|
256
|
+
runningHookTasks -= 1;
|
|
257
|
+
runNextHookTask();
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function scheduleHookTask(task) {
|
|
262
|
+
return new Promise((resolve) => {
|
|
263
|
+
const wrapped = async () => {
|
|
264
|
+
try {
|
|
265
|
+
resolve(await task());
|
|
266
|
+
} catch {
|
|
267
|
+
resolve(false);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
pendingHookTasks.push(wrapped);
|
|
271
|
+
runNextHookTask();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
184
275
|
async function runHookScript(scriptFileName, payload = {}) {
|
|
185
|
-
const {
|
|
276
|
+
const { spawn } = await import("node:child_process");
|
|
186
277
|
const scriptPath = join(root, "${RUNTIME_ROOT}/hooks/" + scriptFileName);
|
|
187
278
|
const input = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
279
|
+
return scheduleHookTask(() => new Promise((resolve) => {
|
|
280
|
+
let stderr = "";
|
|
281
|
+
let settled = false;
|
|
282
|
+
const finish = (ok) => {
|
|
283
|
+
if (settled) return;
|
|
284
|
+
settled = true;
|
|
285
|
+
resolve(ok);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
let child;
|
|
289
|
+
try {
|
|
290
|
+
child = spawn("bash", [scriptPath], {
|
|
291
|
+
cwd: root,
|
|
292
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
293
|
+
});
|
|
294
|
+
} catch {
|
|
295
|
+
finish(false);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const timer = setTimeout(() => {
|
|
300
|
+
child.kill("SIGKILL");
|
|
301
|
+
if (stderr.length > 0) {
|
|
302
|
+
console.error("[cclaw] opencode hook timeout: " + scriptFileName + " stderr=" + stderr.slice(-1200));
|
|
303
|
+
}
|
|
304
|
+
finish(false);
|
|
305
|
+
}, 20_000);
|
|
306
|
+
|
|
307
|
+
child.stderr?.on("data", (chunk) => {
|
|
308
|
+
stderr += String(chunk ?? "");
|
|
309
|
+
if (stderr.length > 4000) {
|
|
310
|
+
stderr = stderr.slice(-4000);
|
|
311
|
+
}
|
|
194
312
|
});
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
313
|
+
child.on("error", () => {
|
|
314
|
+
clearTimeout(timer);
|
|
315
|
+
finish(false);
|
|
316
|
+
});
|
|
317
|
+
child.on("close", (code) => {
|
|
318
|
+
clearTimeout(timer);
|
|
319
|
+
const ok = code === 0;
|
|
320
|
+
if (!ok && stderr.length > 0) {
|
|
321
|
+
console.error("[cclaw] opencode hook failed: " + scriptFileName + " stderr=" + stderr.slice(-1200));
|
|
322
|
+
}
|
|
323
|
+
finish(ok);
|
|
324
|
+
});
|
|
325
|
+
if (child.stdin) {
|
|
326
|
+
child.stdin.on("error", (error) => {
|
|
327
|
+
const code =
|
|
328
|
+
error && typeof error === "object" && "code" in error
|
|
329
|
+
? String(error.code)
|
|
330
|
+
: "";
|
|
331
|
+
if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
finish(false);
|
|
336
|
+
});
|
|
337
|
+
try {
|
|
338
|
+
child.stdin.end(input);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const code =
|
|
341
|
+
error && typeof error === "object" && "code" in error
|
|
342
|
+
? String(error.code)
|
|
343
|
+
: "";
|
|
344
|
+
if (code !== "EPIPE" && code !== "ERR_STREAM_DESTROYED") {
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
finish(false);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}));
|
|
199
351
|
}
|
|
200
352
|
|
|
201
353
|
function normalizeToolPayload(input, output) {
|
|
@@ -207,9 +359,15 @@ export default function cclawPlugin(ctx) {
|
|
|
207
359
|
if (typeof payload === "string") return payload;
|
|
208
360
|
if (payload && typeof payload === "object") {
|
|
209
361
|
if (typeof payload.type === "string") return payload.type;
|
|
362
|
+
if (typeof payload.eventType === "string") return payload.eventType;
|
|
363
|
+
if (typeof payload.kind === "string") return payload.kind;
|
|
364
|
+
if (typeof payload.topic === "string") return payload.topic;
|
|
210
365
|
if (typeof payload.name === "string") return payload.name;
|
|
211
366
|
if (payload.event && typeof payload.event === "object") {
|
|
212
367
|
if (typeof payload.event.type === "string") return payload.event.type;
|
|
368
|
+
if (typeof payload.event.eventType === "string") return payload.event.eventType;
|
|
369
|
+
if (typeof payload.event.kind === "string") return payload.event.kind;
|
|
370
|
+
if (typeof payload.event.topic === "string") return payload.event.topic;
|
|
213
371
|
if (typeof payload.event.name === "string") return payload.event.name;
|
|
214
372
|
}
|
|
215
373
|
}
|
|
@@ -217,18 +375,40 @@ export default function cclawPlugin(ctx) {
|
|
|
217
375
|
}
|
|
218
376
|
|
|
219
377
|
function resolveEventData(payload) {
|
|
220
|
-
if (payload && typeof payload === "object"
|
|
221
|
-
|
|
378
|
+
if (payload && typeof payload === "object") {
|
|
379
|
+
if (payload.event && typeof payload.event === "object") {
|
|
380
|
+
if (payload.event.data && typeof payload.event.data === "object") {
|
|
381
|
+
return payload.event.data;
|
|
382
|
+
}
|
|
383
|
+
if (payload.event.payload && typeof payload.event.payload === "object") {
|
|
384
|
+
return payload.event.payload;
|
|
385
|
+
}
|
|
386
|
+
return payload.event;
|
|
387
|
+
}
|
|
388
|
+
if (payload.data && typeof payload.data === "object") {
|
|
389
|
+
return payload.data;
|
|
390
|
+
}
|
|
391
|
+
if (payload.payload && typeof payload.payload === "object") {
|
|
392
|
+
return payload.payload;
|
|
393
|
+
}
|
|
222
394
|
}
|
|
223
395
|
return payload;
|
|
224
396
|
}
|
|
225
397
|
|
|
226
398
|
ensureRuntimeDirs();
|
|
399
|
+
void refreshBootstrapCache(true);
|
|
227
400
|
|
|
228
401
|
return {
|
|
229
402
|
event: async (payload) => {
|
|
230
403
|
const eventType = resolveEventType(payload);
|
|
231
404
|
const eventData = resolveEventData(payload);
|
|
405
|
+
if (!eventType) {
|
|
406
|
+
const keys =
|
|
407
|
+
payload && typeof payload === "object"
|
|
408
|
+
? Object.keys(payload).slice(0, 10).join(", ")
|
|
409
|
+
: typeof payload;
|
|
410
|
+
console.error("[cclaw] opencode unknown event payload keys: " + keys);
|
|
411
|
+
}
|
|
232
412
|
if (
|
|
233
413
|
eventType === "session.created" ||
|
|
234
414
|
eventType === "session.resumed" ||
|
|
@@ -242,7 +422,7 @@ export default function cclawPlugin(ctx) {
|
|
|
242
422
|
// session.updated covers config reloads and artifact/rules edits
|
|
243
423
|
// that happen mid-session; without it the cache would stay stale
|
|
244
424
|
// until the next compaction or restart.
|
|
245
|
-
refreshBootstrapCache();
|
|
425
|
+
await refreshBootstrapCache(true);
|
|
246
426
|
}
|
|
247
427
|
if (eventType === "session.compacted") {
|
|
248
428
|
await runHookScript("pre-compact.sh", eventData ?? {});
|
|
@@ -264,14 +444,16 @@ export default function cclawPlugin(ctx) {
|
|
|
264
444
|
"tool.execute.after": async (input, output) => {
|
|
265
445
|
const payload = normalizeToolPayload(input, output);
|
|
266
446
|
await runHookScript("context-monitor.sh", payload);
|
|
447
|
+
void refreshBootstrapCache(false);
|
|
267
448
|
},
|
|
268
449
|
"experimental.chat.system.transform": (payload) => {
|
|
269
450
|
const bootstrap = getBootstrap();
|
|
451
|
+
if (!bootstrap) return payload;
|
|
270
452
|
if (typeof payload === "string") {
|
|
271
|
-
return payload.includes(
|
|
453
|
+
return payload.includes(BOOTSTRAP_MARKER) ? payload : \`\${payload}\\n\\n\${bootstrap}\`;
|
|
272
454
|
}
|
|
273
455
|
if (payload && typeof payload === "object" && typeof payload.system === "string") {
|
|
274
|
-
if (payload.system.includes(
|
|
456
|
+
if (payload.system.includes(BOOTSTRAP_MARKER)) return payload;
|
|
275
457
|
return { ...payload, system: \`\${payload.system}\\n\\n\${bootstrap}\` };
|
|
276
458
|
}
|
|
277
459
|
return payload;
|
|
@@ -198,7 +198,7 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
|
|
|
198
198
|
{
|
|
199
199
|
agent: "doc-updater",
|
|
200
200
|
mode: "proactive",
|
|
201
|
-
when: "
|
|
201
|
+
when: "Proactive in tdd when public behavior, APIs, or config surfaces change.",
|
|
202
202
|
purpose: "Prevent code/docs drift before review and ship.",
|
|
203
203
|
requiresUserGate: false
|
|
204
204
|
}
|
package/dist/delegation.js
CHANGED
|
@@ -238,7 +238,7 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
238
238
|
runId: activeRunId,
|
|
239
239
|
entries: [...prior.entries, stamped]
|
|
240
240
|
};
|
|
241
|
-
await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n
|
|
241
|
+
await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
|
|
242
242
|
});
|
|
243
243
|
}
|
|
244
244
|
/**
|
package/dist/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { REQUIRED_DIRS, RUNTIME_ROOT, UTILITY_COMMANDS } from "./constants.js";
|
|
7
7
|
import { CCLAW_AGENTS } from "./content/core-agents.js";
|
|
8
|
-
import { detectAdvancedKeys, readConfig } from "./config.js";
|
|
8
|
+
import { detectAdvancedKeys, InvalidConfigError, readConfig } from "./config.js";
|
|
9
9
|
import { exists } from "./fs-utils.js";
|
|
10
10
|
import { gitignoreHasRequiredPatterns } from "./gitignore.js";
|
|
11
11
|
import { HARNESS_ADAPTERS, CCLAW_MARKER_START, CCLAW_MARKER_END, harnessShimFileNames, harnessShimSkillNames } from "./harness-adapters.js";
|
|
@@ -209,6 +209,15 @@ async function readJsonObjectStatus(filePath) {
|
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
+
async function readPermissionBits(filePath) {
|
|
213
|
+
try {
|
|
214
|
+
const stat = await fs.stat(filePath);
|
|
215
|
+
return stat.mode & 0o777;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
212
221
|
function normalizeOpenCodePluginEntry(entry) {
|
|
213
222
|
if (typeof entry === "string" && entry.trim().length > 0)
|
|
214
223
|
return entry.trim();
|
|
@@ -457,6 +466,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
457
466
|
checks.push({
|
|
458
467
|
name: "config:valid",
|
|
459
468
|
ok: false,
|
|
469
|
+
severity: error instanceof InvalidConfigError ? "error" : "warning",
|
|
460
470
|
details: error instanceof Error ? error.message : "Invalid config"
|
|
461
471
|
});
|
|
462
472
|
}
|
|
@@ -1373,6 +1383,30 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1373
1383
|
ok: activeRunId.length > 0,
|
|
1374
1384
|
details: `${RUNTIME_ROOT}/state/flow-state.json must include activeRunId`
|
|
1375
1385
|
});
|
|
1386
|
+
const sensitivePermissionTargets = [
|
|
1387
|
+
path.join(projectRoot, RUNTIME_ROOT, "state", "flow-state.json"),
|
|
1388
|
+
path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-log.json"),
|
|
1389
|
+
path.join(projectRoot, RUNTIME_ROOT, "state", "reconciliation-notices.json"),
|
|
1390
|
+
path.join(projectRoot, RUNTIME_ROOT, "state", "worktrees.json"),
|
|
1391
|
+
path.join(projectRoot, RUNTIME_ROOT, "state", "active-feature.json"),
|
|
1392
|
+
path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl")
|
|
1393
|
+
];
|
|
1394
|
+
const permissiveStateFiles = [];
|
|
1395
|
+
for (const targetPath of sensitivePermissionTargets) {
|
|
1396
|
+
const bits = await readPermissionBits(targetPath);
|
|
1397
|
+
if (bits === null)
|
|
1398
|
+
continue;
|
|
1399
|
+
if (bits > 0o640) {
|
|
1400
|
+
permissiveStateFiles.push(`${path.relative(projectRoot, targetPath)}:${bits.toString(8)}`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
checks.push({
|
|
1404
|
+
name: "warning:state:file_permissions",
|
|
1405
|
+
ok: true,
|
|
1406
|
+
details: permissiveStateFiles.length === 0
|
|
1407
|
+
? "sensitive state files are <=0640 permissions"
|
|
1408
|
+
: `warning: sensitive state files are overly permissive (${permissiveStateFiles.join(", ")}). Run \`chmod 600 .cclaw/state/*.json .cclaw/state/*.jsonl .cclaw/knowledge.jsonl\` if this machine is multi-user.`
|
|
1409
|
+
});
|
|
1376
1410
|
const reconciliationNotices = await readReconciliationNotices(projectRoot);
|
|
1377
1411
|
checks.push({
|
|
1378
1412
|
name: "state:reconciliation_notices_parse",
|
package/dist/eval/runner.js
CHANGED
|
@@ -524,6 +524,34 @@ function stagesInResults(caseResults) {
|
|
|
524
524
|
set.add(c.stage);
|
|
525
525
|
return FLOW_STAGES.filter((s) => set.has(s));
|
|
526
526
|
}
|
|
527
|
+
const MAX_PARALLEL_CASES = 4;
|
|
528
|
+
async function runCasesWithBoundedConcurrency(items, concurrency, worker) {
|
|
529
|
+
if (items.length === 0) {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
533
|
+
if (limit === 1) {
|
|
534
|
+
const results = [];
|
|
535
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
536
|
+
results.push(await worker(items[i], i));
|
|
537
|
+
}
|
|
538
|
+
return results;
|
|
539
|
+
}
|
|
540
|
+
const results = new Array(items.length);
|
|
541
|
+
let cursor = 0;
|
|
542
|
+
const runners = Array.from({ length: limit }, async () => {
|
|
543
|
+
while (true) {
|
|
544
|
+
const index = cursor;
|
|
545
|
+
cursor += 1;
|
|
546
|
+
if (index >= items.length) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
results[index] = await worker(items[index], index);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
await Promise.all(runners);
|
|
553
|
+
return results;
|
|
554
|
+
}
|
|
527
555
|
/**
|
|
528
556
|
* Main eval runner. Dispatches between fixture-backed verification, the
|
|
529
557
|
* single-stage agent-with-tools loop, and the multi-stage workflow
|
|
@@ -653,8 +681,11 @@ export async function runEval(options) {
|
|
|
653
681
|
}
|
|
654
682
|
}
|
|
655
683
|
else {
|
|
656
|
-
|
|
657
|
-
|
|
684
|
+
// Only parallelize fixture/rules verification passes that do not depend on
|
|
685
|
+
// LLM judge/agent loops. Those modes touch cost guards and retries where
|
|
686
|
+
// ordered execution is safer.
|
|
687
|
+
const caseConcurrency = flags.runJudge || flags.runAgent ? 1 : MAX_PARALLEL_CASES;
|
|
688
|
+
const results = await runCasesWithBoundedConcurrency(corpus, caseConcurrency, async (item, i) => {
|
|
658
689
|
progress.emit({
|
|
659
690
|
kind: "case-start",
|
|
660
691
|
caseId: item.id,
|
|
@@ -682,8 +713,9 @@ export async function runEval(options) {
|
|
|
682
713
|
durationMs: result.durationMs,
|
|
683
714
|
...(result.costUsd !== undefined ? { costUsd: result.costUsd } : {})
|
|
684
715
|
});
|
|
685
|
-
|
|
686
|
-
}
|
|
716
|
+
return result;
|
|
717
|
+
});
|
|
718
|
+
caseResults.push(...results);
|
|
687
719
|
}
|
|
688
720
|
const stages = stagesInResults(caseResults);
|
|
689
721
|
const baselines = await loadBaselinesByStage(options.projectRoot, stages);
|
package/dist/feature-system.js
CHANGED
|
@@ -178,7 +178,7 @@ async function writeRegistry(projectRoot, registry) {
|
|
|
178
178
|
updatedAt: registry.updatedAt,
|
|
179
179
|
entries: dedupeEntries(registry.entries)
|
|
180
180
|
};
|
|
181
|
-
await writeFileSafe(worktreeRegistryPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n
|
|
181
|
+
await writeFileSafe(worktreeRegistryPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
|
|
182
182
|
}
|
|
183
183
|
async function readActiveFeatureMetaInternal(projectRoot) {
|
|
184
184
|
const filePath = activeFeatureMetaPath(projectRoot);
|
|
@@ -213,7 +213,7 @@ async function writeActiveFeatureMeta(projectRoot, meta) {
|
|
|
213
213
|
activeFeature: normalizedFeatureId(meta.activeFeature),
|
|
214
214
|
updatedAt: meta.updatedAt
|
|
215
215
|
};
|
|
216
|
-
await writeFileSafe(activeFeatureMetaPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n
|
|
216
|
+
await writeFileSafe(activeFeatureMetaPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
|
|
217
217
|
}
|
|
218
218
|
function registryHasFeature(registry, featureId) {
|
|
219
219
|
return registry.entries.some((entry) => entry.featureId === featureId);
|
package/dist/fs-utils.d.ts
CHANGED
|
@@ -18,7 +18,10 @@ export interface DirectoryLockOptions {
|
|
|
18
18
|
* The lock is removed in a finally block.
|
|
19
19
|
*/
|
|
20
20
|
export declare function withDirectoryLock<T>(lockPath: string, fn: () => Promise<T>, options?: DirectoryLockOptions): Promise<T>;
|
|
21
|
-
export
|
|
21
|
+
export interface WriteFileSafeOptions {
|
|
22
|
+
mode?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function writeFileSafe(filePath: string, content: string, options?: WriteFileSafeOptions): Promise<void>;
|
|
22
25
|
export declare function exists(filePath: string): Promise<boolean>;
|
|
23
26
|
export declare function removeIfExists(targetPath: string): Promise<void>;
|
|
24
27
|
export declare function resolveProjectPath(cwd: string, relativePath: string): string;
|
package/dist/fs-utils.js
CHANGED
|
@@ -70,12 +70,16 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
export async function writeFileSafe(filePath, content) {
|
|
73
|
+
export async function writeFileSafe(filePath, content, options = {}) {
|
|
74
74
|
await ensureDir(path.dirname(filePath));
|
|
75
75
|
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
76
|
-
|
|
76
|
+
const targetMode = options.mode;
|
|
77
|
+
await fs.writeFile(tempPath, content, { encoding: "utf8", ...(targetMode !== undefined ? { mode: targetMode } : {}) });
|
|
77
78
|
try {
|
|
78
79
|
await fs.rename(tempPath, filePath);
|
|
80
|
+
if (targetMode !== undefined) {
|
|
81
|
+
await fs.chmod(filePath, targetMode).catch(() => undefined);
|
|
82
|
+
}
|
|
79
83
|
}
|
|
80
84
|
catch (error) {
|
|
81
85
|
const code = error?.code;
|
|
@@ -87,6 +91,9 @@ export async function writeFileSafe(filePath, content) {
|
|
|
87
91
|
if (code === "EXDEV") {
|
|
88
92
|
try {
|
|
89
93
|
await fs.copyFile(tempPath, filePath);
|
|
94
|
+
if (targetMode !== undefined) {
|
|
95
|
+
await fs.chmod(filePath, targetMode).catch(() => undefined);
|
|
96
|
+
}
|
|
90
97
|
}
|
|
91
98
|
finally {
|
|
92
99
|
await fs.unlink(tempPath).catch(() => undefined);
|
package/dist/gate-evidence.js
CHANGED
|
@@ -133,7 +133,7 @@ async function writeReconciliationNotices(projectRoot, payload) {
|
|
|
133
133
|
await writeFileSafe(filePath, `${JSON.stringify({
|
|
134
134
|
schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
|
|
135
135
|
notices: payload.notices
|
|
136
|
-
}, null, 2)}\n
|
|
136
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
137
137
|
}
|
|
138
138
|
export function classifyReconciliationNotices(flowState, notices) {
|
|
139
139
|
const activeBlocked = [];
|