cclaw-cli 0.48.22 → 0.48.24
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/content/node-hooks.js +57 -8
- package/dist/doctor-registry.js +16 -1
- package/dist/fs-utils.js +13 -1
- package/package.json +1 -1
|
@@ -103,11 +103,20 @@ async function withDirectoryLockInline(lockPath, fn, options = {}) {
|
|
|
103
103
|
}
|
|
104
104
|
try {
|
|
105
105
|
const stat = await fs.stat(lockPath);
|
|
106
|
+
if (!stat.isDirectory()) {
|
|
107
|
+
throw new Error("Lock path exists but is not a directory: " + lockPath);
|
|
108
|
+
}
|
|
106
109
|
if (Date.now() - stat.mtimeMs > staleAfterMs) {
|
|
107
110
|
await fs.rm(lockPath, { recursive: true, force: true });
|
|
108
111
|
continue;
|
|
109
112
|
}
|
|
110
|
-
} catch {
|
|
113
|
+
} catch (statError) {
|
|
114
|
+
if (
|
|
115
|
+
statError instanceof Error &&
|
|
116
|
+
statError.message.startsWith("Lock path exists but is not a directory")
|
|
117
|
+
) {
|
|
118
|
+
throw statError;
|
|
119
|
+
}
|
|
111
120
|
// lock vanished between retries
|
|
112
121
|
}
|
|
113
122
|
await hookSleep(retryDelayMs);
|
|
@@ -254,6 +263,24 @@ async function readTextFile(filePath, fallback = "") {
|
|
|
254
263
|
}
|
|
255
264
|
}
|
|
256
265
|
|
|
266
|
+
// CLI-compatible knowledge lock. Must match
|
|
267
|
+
// src/knowledge-store.ts::knowledgeLockPath exactly so the hook and the
|
|
268
|
+
// CLI serialize on the same mutex when reading / appending
|
|
269
|
+
// knowledge.jsonl. Drift here re-introduces the race we just closed.
|
|
270
|
+
function knowledgeLockPathInline(root) {
|
|
271
|
+
return path.join(root, RUNTIME_ROOT, "state", ".knowledge.lock");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function readTextFileLocked(lockPath, filePath, fallback = "") {
|
|
275
|
+
return withDirectoryLockInline(lockPath, async () => {
|
|
276
|
+
try {
|
|
277
|
+
return await fs.readFile(filePath, "utf8");
|
|
278
|
+
} catch {
|
|
279
|
+
return fallback;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
257
284
|
async function appendJsonLine(filePath, value) {
|
|
258
285
|
const payload = JSON.stringify(value) + "\\n";
|
|
259
286
|
await withDirectoryLockInline(lockPathFor(filePath), async () => {
|
|
@@ -904,10 +931,16 @@ async function handleSessionStart(runtime) {
|
|
|
904
931
|
const sessionDigest = (await readTextFile(sessionDigestFile, "")).trim();
|
|
905
932
|
const activitySummary = await readRecentActivityLines(activityFile);
|
|
906
933
|
const contextWarning = await readLatestContextWarningLine(contextWarningsFile);
|
|
907
|
-
// Read knowledge.jsonl exactly once per session-start
|
|
908
|
-
//
|
|
934
|
+
// Read knowledge.jsonl exactly once per session-start while holding the
|
|
935
|
+
// SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
|
|
936
|
+
// see a partial (mid-write) snapshot. Both the digest and
|
|
937
|
+
// compound-readiness derive from this single read.
|
|
909
938
|
const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
910
|
-
const knowledgeRaw = await
|
|
939
|
+
const knowledgeRaw = await readTextFileLocked(
|
|
940
|
+
knowledgeLockPathInline(runtime.root),
|
|
941
|
+
knowledgeFilePath,
|
|
942
|
+
""
|
|
943
|
+
);
|
|
911
944
|
const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
|
|
912
945
|
|
|
913
946
|
// Refresh Ralph Loop status each session-start so /cc-next and the model
|
|
@@ -925,8 +958,16 @@ async function handleSessionStart(runtime) {
|
|
|
925
958
|
", slices=" + String(ralphStatus.sliceCount) +
|
|
926
959
|
", acClosed=" + String(ralphStatus.acClosed.length) +
|
|
927
960
|
", redOpen=" + redOpen;
|
|
928
|
-
} catch (
|
|
929
|
-
//
|
|
961
|
+
} catch (err) {
|
|
962
|
+
// Best-effort — a malformed cycle log should never break
|
|
963
|
+
// session-start. But we DO leave a breadcrumb in
|
|
964
|
+
// hook-errors.jsonl so \`cclaw doctor\` can surface chronic
|
|
965
|
+
// failures (previously this was a silent swallow).
|
|
966
|
+
await recordHookError(
|
|
967
|
+
runtime.root,
|
|
968
|
+
"session-start:ralph-loop",
|
|
969
|
+
err instanceof Error ? err.message : String(err)
|
|
970
|
+
);
|
|
930
971
|
}
|
|
931
972
|
}
|
|
932
973
|
|
|
@@ -952,8 +993,16 @@ async function handleSessionStart(runtime) {
|
|
|
952
993
|
", ready=" + String(readiness.readyCount) + criticalSuffix;
|
|
953
994
|
}
|
|
954
995
|
}
|
|
955
|
-
} catch (
|
|
956
|
-
//
|
|
996
|
+
} catch (err) {
|
|
997
|
+
// Best-effort — a malformed knowledge.jsonl must never break
|
|
998
|
+
// session-start. But we DO leave a breadcrumb in
|
|
999
|
+
// hook-errors.jsonl so config/IO problems become visible in
|
|
1000
|
+
// \`cclaw doctor\` instead of silently degrading readiness output.
|
|
1001
|
+
await recordHookError(
|
|
1002
|
+
runtime.root,
|
|
1003
|
+
"session-start:compound-readiness",
|
|
1004
|
+
err instanceof Error ? err.message : String(err)
|
|
1005
|
+
);
|
|
957
1006
|
}
|
|
958
1007
|
|
|
959
1008
|
const suggestionMemory = toObject(await readJsonFile(suggestionMemoryFile, {})) || {};
|
package/dist/doctor-registry.js
CHANGED
|
@@ -85,7 +85,7 @@ const RULES = [
|
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
|
-
test: /^(meta_skill:|protocol:|stage_skill:|context_mode
|
|
88
|
+
test: /^(meta_skill:|protocol:|stage_skill:|context_mode:)/,
|
|
89
89
|
metadata: {
|
|
90
90
|
severity: "error",
|
|
91
91
|
summary: "Routing skill and protocol integrity check.",
|
|
@@ -93,6 +93,21 @@ const RULES = [
|
|
|
93
93
|
docRef: ref("harness-and-routing.md")
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
|
+
{
|
|
97
|
+
// `reference:*` checks (flow-map.md and similar overview documents)
|
|
98
|
+
// are useful to detect drift from the generated baseline, but they
|
|
99
|
+
// document the surface rather than gate it. A missing section here
|
|
100
|
+
// means the map is out of date, not that a runtime contract is
|
|
101
|
+
// broken — so they report as a warning instead of hard-failing
|
|
102
|
+
// doctor / CI. `cclaw sync` rewrites the file.
|
|
103
|
+
test: /^reference:/,
|
|
104
|
+
metadata: {
|
|
105
|
+
severity: "warning",
|
|
106
|
+
summary: "Reference/overview doc integrity (non-blocking).",
|
|
107
|
+
fix: "Run `cclaw sync` to regenerate the reference doc from the canonical source.",
|
|
108
|
+
docRef: ref("harness-and-routing.md")
|
|
109
|
+
}
|
|
110
|
+
},
|
|
96
111
|
{
|
|
97
112
|
test: /^delegation:/,
|
|
98
113
|
metadata: {
|
package/dist/fs-utils.js
CHANGED
|
@@ -42,12 +42,24 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
try {
|
|
44
44
|
const stat = await fs.stat(lockPath);
|
|
45
|
+
if (!stat.isDirectory()) {
|
|
46
|
+
// A non-directory lives at the lock path (e.g. a stray file).
|
|
47
|
+
// Retrying mkdir() will keep returning EEXIST forever, and no
|
|
48
|
+
// other cclaw process is holding the lock - the path is simply
|
|
49
|
+
// unusable. Fail loudly rather than burning the full retry
|
|
50
|
+
// budget, so the caller sees a deterministic error.
|
|
51
|
+
throw new Error(`Lock path exists but is not a directory: ${lockPath}`);
|
|
52
|
+
}
|
|
45
53
|
if (Date.now() - stat.mtimeMs > staleAfterMs) {
|
|
46
54
|
await fs.rm(lockPath, { recursive: true, force: true });
|
|
47
55
|
continue;
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
|
-
catch {
|
|
58
|
+
catch (statError) {
|
|
59
|
+
if (statError instanceof Error &&
|
|
60
|
+
statError.message.startsWith("Lock path exists but is not a directory")) {
|
|
61
|
+
throw statError;
|
|
62
|
+
}
|
|
51
63
|
// Lock directory disappeared between retries.
|
|
52
64
|
}
|
|
53
65
|
await sleep(retryDelayMs);
|