cclaw-cli 0.48.14 → 0.48.16
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/flow-map.js +17 -0
- package/dist/content/hook-events.d.ts +7 -2
- package/dist/content/hook-events.js +30 -46
- package/dist/content/hook-manifest.d.ts +82 -0
- package/dist/content/hook-manifest.js +208 -0
- package/dist/content/node-hooks.js +123 -0
- package/dist/content/observe.d.ts +16 -0
- package/dist/content/observe.js +65 -120
- package/dist/internal/advance-stage.js +10 -2
- package/dist/internal/compound-readiness.d.ts +15 -0
- package/dist/internal/compound-readiness.js +76 -0
- package/dist/internal/hook-manifest.d.ts +16 -0
- package/dist/internal/hook-manifest.js +77 -0
- package/dist/knowledge-store.d.ts +57 -0
- package/dist/knowledge-store.js +107 -0
- package/dist/policy.js +1 -0
- package/package.json +1 -1
package/dist/content/flow-map.js
CHANGED
|
@@ -82,6 +82,17 @@ worth acting on:
|
|
|
82
82
|
Ralph Loop is a signal, not a gate. Stage advancement still runs
|
|
83
83
|
through the normal \`flow-state.json\` gate catalog.
|
|
84
84
|
|
|
85
|
+
## Compound readiness (auto-promotion signal)
|
|
86
|
+
|
|
87
|
+
SessionStart also refreshes
|
|
88
|
+
\`${RUNTIME_ROOT}/state/compound-readiness.json\` from \`knowledge.jsonl\`.
|
|
89
|
+
The file lists clusters whose summed \`frequency\` reaches
|
|
90
|
+
\`compound.recurrenceThreshold\` (default 3) or whose severity is
|
|
91
|
+
\`critical\` (override). It surfaces a one-line nudge in the session
|
|
92
|
+
digest only during \`review\` and \`ship\`, where lift-to-rule is in
|
|
93
|
+
scope; earlier stages refresh the file silently. Promotion itself stays
|
|
94
|
+
manual via \`/cc-ops compound\` so the signal never blocks flow.
|
|
95
|
+
|
|
85
96
|
## Key state files
|
|
86
97
|
|
|
87
98
|
| Path | What it holds |
|
|
@@ -90,6 +101,7 @@ through the normal \`flow-state.json\` gate catalog.
|
|
|
90
101
|
| \`${RUNTIME_ROOT}/state/delegation-log.json\` | Per-stage mandatory agent status + fulfillmentMode + evidenceRefs. |
|
|
91
102
|
| \`${RUNTIME_ROOT}/state/tdd-cycle-log.jsonl\` | Append-only RED/GREEN/REFACTOR entries (source of Ralph Loop). |
|
|
92
103
|
| \`${RUNTIME_ROOT}/state/ralph-loop.json\` | Derived Ralph Loop status (TDD-only). |
|
|
104
|
+
| \`${RUNTIME_ROOT}/state/compound-readiness.json\` | Derived compound-promotion readiness (refreshed each SessionStart). |
|
|
93
105
|
| \`${RUNTIME_ROOT}/state/stage-activity.jsonl\` | Append-only stage-enter/exit and gate-pass signals. |
|
|
94
106
|
| \`${RUNTIME_ROOT}/state/checkpoint.json\` | Latest session checkpoint (stage + timestamp). |
|
|
95
107
|
| \`${RUNTIME_ROOT}/state/context-mode.json\` | Active context mode (\`default\`, \`headless\`, ...). |
|
|
@@ -106,6 +118,11 @@ Hook-driven guards respect the \`strictness\` field in
|
|
|
106
118
|
|
|
107
119
|
Override per-session with \`CCLAW_STRICTNESS=advisory|strict\`.
|
|
108
120
|
|
|
121
|
+
Hook wiring itself comes from a **single manifest** (\`src/content/hook-manifest.ts\`):
|
|
122
|
+
the per-harness documents at \`.claude/hooks/hooks.json\`, \`.cursor/hooks.json\`,
|
|
123
|
+
\`.codex/hooks.json\` are all derived from it. Inspect the live bindings with
|
|
124
|
+
\`cclaw internal hook-manifest\` (add \`--json\` for machine-readable output).
|
|
125
|
+
|
|
109
126
|
## When in doubt
|
|
110
127
|
|
|
111
128
|
1. Read \`${RUNTIME_ROOT}/state/flow-state.json\` to know where you are.
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { HarnessId } from "../types.js";
|
|
2
|
-
|
|
3
|
-
export type HookSemanticEvent
|
|
2
|
+
import { type HookSemanticEvent } from "./hook-manifest.js";
|
|
3
|
+
export { HOOK_SEMANTIC_EVENTS, type HookSemanticEvent } from "./hook-manifest.js";
|
|
4
|
+
/**
|
|
5
|
+
* Public semantic coverage map derived from `HOOK_MANIFEST` for
|
|
6
|
+
* claude/cursor/codex, plus the static OpenCode descriptor. Consumers
|
|
7
|
+
* should treat this as read-only.
|
|
8
|
+
*/
|
|
4
9
|
export declare const HOOK_EVENTS_BY_HARNESS: Record<HarnessId, Partial<Record<HookSemanticEvent, string>>>;
|
|
@@ -1,47 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
session_rehydrate: "sessionStart/sessionResume/sessionClear/sessionCompact",
|
|
20
|
-
pre_tool_prompt_guard: "preToolUse -> prompt-guard",
|
|
21
|
-
pre_tool_workflow_guard: "preToolUse -> workflow-guard",
|
|
22
|
-
post_tool_context_monitor: "postToolUse -> context-monitor",
|
|
23
|
-
stop_checkpoint: "stop -> stop-checkpoint",
|
|
24
|
-
precompact_digest: "sessionCompact -> pre-compact"
|
|
25
|
-
},
|
|
26
|
-
opencode: {
|
|
27
|
-
session_rehydrate: "plugin event handlers + transform rehydration",
|
|
28
|
-
pre_tool_prompt_guard: "plugin tool.execute.before -> prompt-guard",
|
|
29
|
-
pre_tool_workflow_guard: "plugin tool.execute.before -> workflow-guard",
|
|
30
|
-
post_tool_context_monitor: "plugin tool.execute.after -> context-monitor",
|
|
31
|
-
stop_checkpoint: "plugin session.idle -> stop-checkpoint",
|
|
32
|
-
precompact_digest: "plugin session.compacted -> pre-compact"
|
|
33
|
-
},
|
|
34
|
-
codex: {
|
|
35
|
-
// Codex CLI v0.114+ exposes lifecycle hooks via `.codex/hooks.json`,
|
|
36
|
-
// gated by `[features] codex_hooks = true` in `~/.codex/config.toml`.
|
|
37
|
-
// SessionStart, Stop, and UserPromptSubmit fire for every turn;
|
|
38
|
-
// PreToolUse/PostToolUse are **Bash-only** (Write/Edit/WebSearch/MCP
|
|
39
|
-
// calls do not trigger them). `precompact_digest` is unmapped —
|
|
40
|
-
// Codex has no PreCompact event; cclaw covers it via `/cc-ops retro`.
|
|
41
|
-
session_rehydrate: "SessionStart matcher startup|resume",
|
|
42
|
-
pre_tool_prompt_guard: "PreToolUse matcher Bash -> prompt-guard (plus UserPromptSubmit for non-Bash prompts)",
|
|
43
|
-
pre_tool_workflow_guard: "PreToolUse matcher Bash -> workflow-guard (Bash-only)",
|
|
44
|
-
post_tool_context_monitor: "PostToolUse matcher Bash -> context-monitor (Bash-only)",
|
|
45
|
-
stop_checkpoint: "Stop -> stop-checkpoint"
|
|
46
|
-
}
|
|
1
|
+
import { HOOK_MANIFEST_HARNESSES, semanticEventCoverage } from "./hook-manifest.js";
|
|
2
|
+
export { HOOK_SEMANTIC_EVENTS } from "./hook-manifest.js";
|
|
3
|
+
function isManifestHarness(value) {
|
|
4
|
+
return HOOK_MANIFEST_HARNESSES.includes(value);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* OpenCode is covered by the inline plugin (`opencode-plugin.ts`), not
|
|
8
|
+
* by the generated `run-hook.mjs` dispatcher. We keep its semantic
|
|
9
|
+
* coverage table here as an explicit descriptor, since it does not
|
|
10
|
+
* flow through the hook manifest.
|
|
11
|
+
*/
|
|
12
|
+
const OPENCODE_SEMANTIC_COVERAGE = {
|
|
13
|
+
session_rehydrate: "plugin event handlers + transform rehydration",
|
|
14
|
+
pre_tool_prompt_guard: "plugin tool.execute.before -> prompt-guard",
|
|
15
|
+
pre_tool_workflow_guard: "plugin tool.execute.before -> workflow-guard",
|
|
16
|
+
post_tool_context_monitor: "plugin tool.execute.after -> context-monitor",
|
|
17
|
+
stop_checkpoint: "plugin session.idle -> stop-checkpoint",
|
|
18
|
+
precompact_digest: "plugin session.compacted -> pre-compact"
|
|
47
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Public semantic coverage map derived from `HOOK_MANIFEST` for
|
|
22
|
+
* claude/cursor/codex, plus the static OpenCode descriptor. Consumers
|
|
23
|
+
* should treat this as read-only.
|
|
24
|
+
*/
|
|
25
|
+
export const HOOK_EVENTS_BY_HARNESS = Object.freeze({
|
|
26
|
+
claude: semanticEventCoverage("claude"),
|
|
27
|
+
cursor: semanticEventCoverage("cursor"),
|
|
28
|
+
codex: semanticEventCoverage("codex"),
|
|
29
|
+
opencode: OPENCODE_SEMANTIC_COVERAGE
|
|
30
|
+
});
|
|
31
|
+
void isManifestHarness;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical operational manifest for cclaw hooks.
|
|
3
|
+
*
|
|
4
|
+
* This is the **single source of truth** for:
|
|
5
|
+
*
|
|
6
|
+
* - the per-harness JSON generators in `./observe.ts`
|
|
7
|
+
* (claude/cursor/codex hook documents),
|
|
8
|
+
* - the semantic coverage map in `./hook-events.ts` (docs + doctor),
|
|
9
|
+
* - the `requiredEvents` list embedded in `src/hook-schemas/*.v1.json`
|
|
10
|
+
* (enforced by a parity test).
|
|
11
|
+
*
|
|
12
|
+
* When adding a new hook handler or rerouting an existing one, edit
|
|
13
|
+
* this file and let the downstream modules rebuild from it. Never
|
|
14
|
+
* hard-code a `SessionStart`, `PreToolUse`, etc. mapping outside of
|
|
15
|
+
* this manifest.
|
|
16
|
+
*
|
|
17
|
+
* OpenCode is deliberately out of scope here: its plugin
|
|
18
|
+
* (`opencode-plugin.ts`) implements the handlers inline rather than
|
|
19
|
+
* dispatching through `run-hook.mjs`, so its coverage is tracked
|
|
20
|
+
* separately in `HOOK_EVENTS_BY_HARNESS`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const HOOK_MANIFEST_HARNESSES: readonly ["claude", "cursor", "codex"];
|
|
23
|
+
export type HookManifestHarness = (typeof HOOK_MANIFEST_HARNESSES)[number];
|
|
24
|
+
export declare const HOOK_HANDLERS: readonly ["session-start", "prompt-guard", "workflow-guard", "context-monitor", "stop-checkpoint", "pre-compact", "verify-current-state"];
|
|
25
|
+
export type HookHandlerId = (typeof HOOK_HANDLERS)[number];
|
|
26
|
+
export interface HookBinding {
|
|
27
|
+
/**
|
|
28
|
+
* Harness-native event name (exact string; PascalCase for
|
|
29
|
+
* claude/codex, camelCase for cursor). Do not normalize casing.
|
|
30
|
+
*/
|
|
31
|
+
event: string;
|
|
32
|
+
matcher?: string;
|
|
33
|
+
timeout?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Within a single (harness, event) group, entries are sorted by
|
|
36
|
+
* `priority` ASC, ties broken by manifest-declaration order. Use
|
|
37
|
+
* this to express "this handler must run BEFORE/AFTER that handler
|
|
38
|
+
* on the same event" (e.g. pre-compact must run before session-start
|
|
39
|
+
* on cursor `sessionCompact`). Default `0`.
|
|
40
|
+
*/
|
|
41
|
+
priority?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface HookHandlerSpec {
|
|
44
|
+
handler: HookHandlerId;
|
|
45
|
+
description: string;
|
|
46
|
+
/**
|
|
47
|
+
* Semantic event id used by `HOOK_EVENTS_BY_HARNESS` / docs.
|
|
48
|
+
* `null` means this handler contributes no semantic coverage row
|
|
49
|
+
* (e.g. `verify-current-state` on codex is a supplementary guard,
|
|
50
|
+
* not a top-level semantic event).
|
|
51
|
+
*/
|
|
52
|
+
semantic: HookSemanticEvent | null;
|
|
53
|
+
bindings: Partial<Record<HookManifestHarness, HookBinding[]>>;
|
|
54
|
+
}
|
|
55
|
+
export declare const HOOK_SEMANTIC_EVENTS: readonly ["session_rehydrate", "pre_tool_prompt_guard", "pre_tool_workflow_guard", "post_tool_context_monitor", "stop_checkpoint", "precompact_digest"];
|
|
56
|
+
export type HookSemanticEvent = (typeof HOOK_SEMANTIC_EVENTS)[number];
|
|
57
|
+
export declare const HOOK_MANIFEST: readonly HookHandlerSpec[];
|
|
58
|
+
export interface EventGroup {
|
|
59
|
+
event: string;
|
|
60
|
+
/**
|
|
61
|
+
* Entries sorted by (priority ASC, declaration order). Default
|
|
62
|
+
* priority is 0. Stable — ties preserve manifest order.
|
|
63
|
+
*/
|
|
64
|
+
entries: Array<{
|
|
65
|
+
handler: HookHandlerId;
|
|
66
|
+
matcher?: string;
|
|
67
|
+
timeout?: number;
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Group manifest bindings by harness-native event name. This is the
|
|
72
|
+
* core projection that observe.ts generators consume to emit the
|
|
73
|
+
* harness-specific JSON document.
|
|
74
|
+
*/
|
|
75
|
+
export declare function groupBindingsByEvent(harness: HookManifestHarness): EventGroup[];
|
|
76
|
+
/** Distinct harness-native event names covered by the manifest. */
|
|
77
|
+
export declare function requiredEventsFor(harness: HookManifestHarness): string[];
|
|
78
|
+
/**
|
|
79
|
+
* Human-readable per-harness semantic coverage used by docs and by
|
|
80
|
+
* the doctor's `harness-gaps.json` synthesis.
|
|
81
|
+
*/
|
|
82
|
+
export declare function semanticEventCoverage(harness: HookManifestHarness): Partial<Record<HookSemanticEvent, string>>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical operational manifest for cclaw hooks.
|
|
3
|
+
*
|
|
4
|
+
* This is the **single source of truth** for:
|
|
5
|
+
*
|
|
6
|
+
* - the per-harness JSON generators in `./observe.ts`
|
|
7
|
+
* (claude/cursor/codex hook documents),
|
|
8
|
+
* - the semantic coverage map in `./hook-events.ts` (docs + doctor),
|
|
9
|
+
* - the `requiredEvents` list embedded in `src/hook-schemas/*.v1.json`
|
|
10
|
+
* (enforced by a parity test).
|
|
11
|
+
*
|
|
12
|
+
* When adding a new hook handler or rerouting an existing one, edit
|
|
13
|
+
* this file and let the downstream modules rebuild from it. Never
|
|
14
|
+
* hard-code a `SessionStart`, `PreToolUse`, etc. mapping outside of
|
|
15
|
+
* this manifest.
|
|
16
|
+
*
|
|
17
|
+
* OpenCode is deliberately out of scope here: its plugin
|
|
18
|
+
* (`opencode-plugin.ts`) implements the handlers inline rather than
|
|
19
|
+
* dispatching through `run-hook.mjs`, so its coverage is tracked
|
|
20
|
+
* separately in `HOOK_EVENTS_BY_HARNESS`.
|
|
21
|
+
*/
|
|
22
|
+
export const HOOK_MANIFEST_HARNESSES = ["claude", "cursor", "codex"];
|
|
23
|
+
export const HOOK_HANDLERS = [
|
|
24
|
+
"session-start",
|
|
25
|
+
"prompt-guard",
|
|
26
|
+
"workflow-guard",
|
|
27
|
+
"context-monitor",
|
|
28
|
+
"stop-checkpoint",
|
|
29
|
+
"pre-compact",
|
|
30
|
+
"verify-current-state"
|
|
31
|
+
];
|
|
32
|
+
export const HOOK_SEMANTIC_EVENTS = [
|
|
33
|
+
"session_rehydrate",
|
|
34
|
+
"pre_tool_prompt_guard",
|
|
35
|
+
"pre_tool_workflow_guard",
|
|
36
|
+
"post_tool_context_monitor",
|
|
37
|
+
"stop_checkpoint",
|
|
38
|
+
"precompact_digest"
|
|
39
|
+
];
|
|
40
|
+
export const HOOK_MANIFEST = [
|
|
41
|
+
{
|
|
42
|
+
handler: "session-start",
|
|
43
|
+
description: "Rehydrate flow state, refresh Ralph Loop + compound readiness, emit bootstrap digest.",
|
|
44
|
+
semantic: "session_rehydrate",
|
|
45
|
+
bindings: {
|
|
46
|
+
claude: [{ event: "SessionStart", matcher: "startup|resume|clear|compact" }],
|
|
47
|
+
cursor: [
|
|
48
|
+
{ event: "sessionStart" },
|
|
49
|
+
{ event: "sessionResume" },
|
|
50
|
+
{ event: "sessionClear" },
|
|
51
|
+
{ event: "sessionCompact" }
|
|
52
|
+
],
|
|
53
|
+
codex: [{ event: "SessionStart", matcher: "startup|resume" }]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
handler: "prompt-guard",
|
|
58
|
+
description: "Stage-aware prompt gate (iron-laws + strictness).",
|
|
59
|
+
semantic: "pre_tool_prompt_guard",
|
|
60
|
+
bindings: {
|
|
61
|
+
claude: [{ event: "PreToolUse", matcher: "*" }],
|
|
62
|
+
cursor: [{ event: "preToolUse", matcher: "*" }],
|
|
63
|
+
codex: [
|
|
64
|
+
{ event: "UserPromptSubmit" },
|
|
65
|
+
{ event: "PreToolUse", matcher: "Bash|bash" }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
handler: "workflow-guard",
|
|
71
|
+
description: "TDD and workflow gate on Write/Edit/Bash style tool invocations.",
|
|
72
|
+
semantic: "pre_tool_workflow_guard",
|
|
73
|
+
bindings: {
|
|
74
|
+
claude: [{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash" }],
|
|
75
|
+
cursor: [{ event: "preToolUse", matcher: "*" }],
|
|
76
|
+
codex: [
|
|
77
|
+
{ event: "UserPromptSubmit" },
|
|
78
|
+
{ event: "PreToolUse", matcher: "Bash|bash" }
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
handler: "context-monitor",
|
|
84
|
+
description: "Post-tool context usage + stage signal monitor.",
|
|
85
|
+
semantic: "post_tool_context_monitor",
|
|
86
|
+
bindings: {
|
|
87
|
+
claude: [{ event: "PostToolUse", matcher: "*" }],
|
|
88
|
+
cursor: [{ event: "postToolUse", matcher: "*" }],
|
|
89
|
+
codex: [{ event: "PostToolUse", matcher: "Bash|bash" }]
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
handler: "stop-checkpoint",
|
|
94
|
+
description: "Persist checkpoint with stage + run context on session stop.",
|
|
95
|
+
semantic: "stop_checkpoint",
|
|
96
|
+
bindings: {
|
|
97
|
+
claude: [{ event: "Stop", timeout: 10 }],
|
|
98
|
+
cursor: [{ event: "stop", timeout: 10 }],
|
|
99
|
+
codex: [{ event: "Stop", timeout: 10 }]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
handler: "pre-compact",
|
|
104
|
+
description: "Write pre-compact digest (Claude+Cursor have a native event; Codex has no PreCompact — covered by `/cc-ops retro`).",
|
|
105
|
+
semantic: "precompact_digest",
|
|
106
|
+
bindings: {
|
|
107
|
+
claude: [{ event: "PreCompact", matcher: "manual|auto", timeout: 10 }],
|
|
108
|
+
// pre-compact must capture the digest BEFORE session-start
|
|
109
|
+
// rehydrates on cursor `sessionCompact`.
|
|
110
|
+
cursor: [{ event: "sessionCompact", priority: -10 }]
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
handler: "verify-current-state",
|
|
115
|
+
description: "Supplementary codex guard that runs on UserPromptSubmit to assert the live state matches the flow.",
|
|
116
|
+
semantic: null,
|
|
117
|
+
bindings: {
|
|
118
|
+
codex: [{ event: "UserPromptSubmit" }]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
/** Sanity: every harness in HOOK_MANIFEST_HARNESSES must be a HarnessId. */
|
|
123
|
+
const _harnessIdCheck = HOOK_MANIFEST_HARNESSES;
|
|
124
|
+
void _harnessIdCheck;
|
|
125
|
+
/**
|
|
126
|
+
* Group manifest bindings by harness-native event name. This is the
|
|
127
|
+
* core projection that observe.ts generators consume to emit the
|
|
128
|
+
* harness-specific JSON document.
|
|
129
|
+
*/
|
|
130
|
+
export function groupBindingsByEvent(harness) {
|
|
131
|
+
const order = [];
|
|
132
|
+
const byEvent = new Map();
|
|
133
|
+
let seq = 0;
|
|
134
|
+
for (const spec of HOOK_MANIFEST) {
|
|
135
|
+
const bindings = spec.bindings[harness];
|
|
136
|
+
if (!bindings)
|
|
137
|
+
continue;
|
|
138
|
+
for (const binding of bindings) {
|
|
139
|
+
let group = byEvent.get(binding.event);
|
|
140
|
+
if (!group) {
|
|
141
|
+
group = { event: binding.event, entries: [] };
|
|
142
|
+
byEvent.set(binding.event, group);
|
|
143
|
+
order.push(binding.event);
|
|
144
|
+
}
|
|
145
|
+
group.entries.push({
|
|
146
|
+
handler: spec.handler,
|
|
147
|
+
...(binding.matcher !== undefined ? { matcher: binding.matcher } : {}),
|
|
148
|
+
...(binding.timeout !== undefined ? { timeout: binding.timeout } : {}),
|
|
149
|
+
priority: binding.priority ?? 0,
|
|
150
|
+
seq: seq++
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return order.map((event) => {
|
|
155
|
+
const group = byEvent.get(event);
|
|
156
|
+
const sorted = [...group.entries].sort((a, b) => {
|
|
157
|
+
if (a.priority !== b.priority)
|
|
158
|
+
return a.priority - b.priority;
|
|
159
|
+
return a.seq - b.seq;
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
event: group.event,
|
|
163
|
+
entries: sorted.map(({ priority: _p, seq: _s, ...entry }) => entry)
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Distinct harness-native event names covered by the manifest. */
|
|
168
|
+
export function requiredEventsFor(harness) {
|
|
169
|
+
const seen = new Set();
|
|
170
|
+
const ordered = [];
|
|
171
|
+
for (const spec of HOOK_MANIFEST) {
|
|
172
|
+
for (const binding of spec.bindings[harness] ?? []) {
|
|
173
|
+
if (seen.has(binding.event))
|
|
174
|
+
continue;
|
|
175
|
+
seen.add(binding.event);
|
|
176
|
+
ordered.push(binding.event);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return ordered;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Human-readable per-harness semantic coverage used by docs and by
|
|
183
|
+
* the doctor's `harness-gaps.json` synthesis.
|
|
184
|
+
*/
|
|
185
|
+
export function semanticEventCoverage(harness) {
|
|
186
|
+
const out = {};
|
|
187
|
+
for (const spec of HOOK_MANIFEST) {
|
|
188
|
+
if (spec.semantic === null)
|
|
189
|
+
continue;
|
|
190
|
+
const bindings = spec.bindings[harness];
|
|
191
|
+
if (!bindings || bindings.length === 0)
|
|
192
|
+
continue;
|
|
193
|
+
out[spec.semantic] = describeBindings(bindings);
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
function describeBindings(bindings) {
|
|
198
|
+
return bindings
|
|
199
|
+
.map((binding) => {
|
|
200
|
+
const pieces = [binding.event];
|
|
201
|
+
if (binding.matcher)
|
|
202
|
+
pieces.push(`matcher=${binding.matcher}`);
|
|
203
|
+
if (binding.timeout)
|
|
204
|
+
pieces.push(`timeout=${binding.timeout}s`);
|
|
205
|
+
return pieces.join(" ");
|
|
206
|
+
})
|
|
207
|
+
.join(" | ");
|
|
208
|
+
}
|
|
@@ -725,6 +725,28 @@ async function handleSessionStart(runtime) {
|
|
|
725
725
|
}
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
// Keep compound-readiness.json fresh on every session-start (cheap derived
|
|
729
|
+
// summary). Surface a one-line nudge only from review and ship stages
|
|
730
|
+
// where lifting becomes relevant; earlier stages update the file silently.
|
|
731
|
+
let compoundReadinessLine = "";
|
|
732
|
+
try {
|
|
733
|
+
const readiness = await computeCompoundReadinessInline(runtime.root, {});
|
|
734
|
+
await writeJsonFile(path.join(stateDir, "compound-readiness.json"), readiness);
|
|
735
|
+
if (state.currentStage === "review" || state.currentStage === "ship") {
|
|
736
|
+
if (readiness.readyCount === 0) {
|
|
737
|
+
compoundReadinessLine = "Compound readiness: no candidates (clusters=" +
|
|
738
|
+
String(readiness.clusterCount) + ", threshold=" + String(readiness.threshold) + ")";
|
|
739
|
+
} else {
|
|
740
|
+
const critical = readiness.ready.filter((entry) => entry.severity === "critical").length;
|
|
741
|
+
const criticalSuffix = critical > 0 ? " (critical=" + String(critical) + ")" : "";
|
|
742
|
+
compoundReadinessLine = "Compound readiness: clusters=" + String(readiness.clusterCount) +
|
|
743
|
+
", ready=" + String(readiness.readyCount) + criticalSuffix;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
} catch (_err) {
|
|
747
|
+
// best-effort — a malformed knowledge.jsonl must never break session-start.
|
|
748
|
+
}
|
|
749
|
+
|
|
728
750
|
const suggestionMemory = toObject(await readJsonFile(suggestionMemoryFile, {})) || {};
|
|
729
751
|
const suggestionsEnabled = suggestionMemory.enabled !== false;
|
|
730
752
|
const mutedStages = Array.isArray(suggestionMemory.mutedStages)
|
|
@@ -792,6 +814,9 @@ async function handleSessionStart(runtime) {
|
|
|
792
814
|
if (ralphLoopLine.length > 0) {
|
|
793
815
|
parts.push(ralphLoopLine);
|
|
794
816
|
}
|
|
817
|
+
if (compoundReadinessLine.length > 0) {
|
|
818
|
+
parts.push(compoundReadinessLine);
|
|
819
|
+
}
|
|
795
820
|
if (contextWarning.length > 0) {
|
|
796
821
|
parts.push("Latest context warning:\\n" + contextWarning);
|
|
797
822
|
}
|
|
@@ -1112,6 +1137,104 @@ async function tddCycleCounts(stateDir, runId) {
|
|
|
1112
1137
|
return { red, green };
|
|
1113
1138
|
}
|
|
1114
1139
|
|
|
1140
|
+
// Mirrors src/knowledge-store.ts::computeCompoundReadiness — kept inline so
|
|
1141
|
+
// SessionStart can refresh compound-readiness.json without the CLI binary.
|
|
1142
|
+
// Any schema change must update src/knowledge-store.ts::computeCompoundReadiness
|
|
1143
|
+
// and src/internal/compound-readiness.ts in lockstep.
|
|
1144
|
+
async function computeCompoundReadinessInline(root, options) {
|
|
1145
|
+
const filePath = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
1146
|
+
const raw = await readTextFile(filePath, "");
|
|
1147
|
+
const threshold = Number.isInteger(options && options.threshold) && options.threshold >= 1
|
|
1148
|
+
? options.threshold
|
|
1149
|
+
: 3;
|
|
1150
|
+
const maxReady = Number.isInteger(options && options.maxReady) && options.maxReady >= 1
|
|
1151
|
+
? options.maxReady
|
|
1152
|
+
: 10;
|
|
1153
|
+
const normalize = (value) => String(value == null ? "" : value).trim().replace(/\\s+/gu, " ").toLowerCase();
|
|
1154
|
+
const severityWeight = (sev) => {
|
|
1155
|
+
if (sev === "critical") return 3;
|
|
1156
|
+
if (sev === "important") return 2;
|
|
1157
|
+
if (sev === "suggestion") return 1;
|
|
1158
|
+
return 0;
|
|
1159
|
+
};
|
|
1160
|
+
const buckets = new Map();
|
|
1161
|
+
for (const rawLine of raw.split(/\\r?\\n/gu)) {
|
|
1162
|
+
const line = rawLine.trim();
|
|
1163
|
+
if (line.length === 0) continue;
|
|
1164
|
+
let row;
|
|
1165
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
1166
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
1167
|
+
if (row.maturity === "lifted-to-enforcement") continue;
|
|
1168
|
+
const type = typeof row.type === "string" ? row.type : "";
|
|
1169
|
+
const trigger = typeof row.trigger === "string" ? row.trigger : "";
|
|
1170
|
+
const action = typeof row.action === "string" ? row.action : "";
|
|
1171
|
+
if (type.length === 0 || trigger.length === 0 || action.length === 0) continue;
|
|
1172
|
+
const key = type + "||" + normalize(trigger) + "||" + normalize(action);
|
|
1173
|
+
const frequency = Number.isInteger(row.frequency) && row.frequency > 0 ? Math.floor(row.frequency) : 1;
|
|
1174
|
+
const lastSeen = typeof row.last_seen_ts === "string" ? row.last_seen_ts : "";
|
|
1175
|
+
let bucket = buckets.get(key);
|
|
1176
|
+
if (!bucket) {
|
|
1177
|
+
bucket = {
|
|
1178
|
+
trigger,
|
|
1179
|
+
action,
|
|
1180
|
+
recurrence: frequency,
|
|
1181
|
+
entryCount: 1,
|
|
1182
|
+
severity: typeof row.severity === "string" ? row.severity : undefined,
|
|
1183
|
+
lastSeenTs: lastSeen,
|
|
1184
|
+
types: new Set([type]),
|
|
1185
|
+
maturity: new Set([typeof row.maturity === "string" ? row.maturity : "raw"])
|
|
1186
|
+
};
|
|
1187
|
+
buckets.set(key, bucket);
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
bucket.recurrence += frequency;
|
|
1191
|
+
bucket.entryCount += 1;
|
|
1192
|
+
bucket.types.add(type);
|
|
1193
|
+
bucket.maturity.add(typeof row.maturity === "string" ? row.maturity : "raw");
|
|
1194
|
+
if (row.severity === "critical") {
|
|
1195
|
+
bucket.severity = "critical";
|
|
1196
|
+
} else if (row.severity === "important" && bucket.severity !== "critical") {
|
|
1197
|
+
bucket.severity = "important";
|
|
1198
|
+
}
|
|
1199
|
+
if (lastSeen && Date.parse(lastSeen) > Date.parse(bucket.lastSeenTs || "0")) {
|
|
1200
|
+
bucket.lastSeenTs = lastSeen;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const ready = [];
|
|
1204
|
+
for (const bucket of buckets.values()) {
|
|
1205
|
+
const criticalOverride = bucket.severity === "critical";
|
|
1206
|
+
const meetsRecurrence = bucket.recurrence >= threshold;
|
|
1207
|
+
if (!criticalOverride && !meetsRecurrence) continue;
|
|
1208
|
+
ready.push({
|
|
1209
|
+
trigger: bucket.trigger,
|
|
1210
|
+
action: bucket.action,
|
|
1211
|
+
recurrence: bucket.recurrence,
|
|
1212
|
+
entryCount: bucket.entryCount,
|
|
1213
|
+
qualification: criticalOverride && !meetsRecurrence ? "critical_override" : "recurrence",
|
|
1214
|
+
...(bucket.severity ? { severity: bucket.severity } : {}),
|
|
1215
|
+
lastSeenTs: bucket.lastSeenTs,
|
|
1216
|
+
types: Array.from(bucket.types).sort(),
|
|
1217
|
+
maturity: Array.from(bucket.maturity).sort()
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
ready.sort((a, b) => {
|
|
1221
|
+
const sevDiff = severityWeight(b.severity) - severityWeight(a.severity);
|
|
1222
|
+
if (sevDiff !== 0) return sevDiff;
|
|
1223
|
+
if (b.recurrence !== a.recurrence) return b.recurrence - a.recurrence;
|
|
1224
|
+
const recencyDiff = Date.parse(b.lastSeenTs || "0") - Date.parse(a.lastSeenTs || "0");
|
|
1225
|
+
if (!Number.isNaN(recencyDiff) && recencyDiff !== 0) return recencyDiff;
|
|
1226
|
+
return String(a.trigger).localeCompare(String(b.trigger));
|
|
1227
|
+
});
|
|
1228
|
+
return {
|
|
1229
|
+
schemaVersion: 1,
|
|
1230
|
+
threshold,
|
|
1231
|
+
clusterCount: buckets.size,
|
|
1232
|
+
readyCount: ready.length,
|
|
1233
|
+
ready: ready.slice(0, maxReady),
|
|
1234
|
+
lastUpdatedAt: new Date().toISOString()
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1115
1238
|
// Mirrors src/tdd-cycle.ts::computeRalphLoopStatus — kept inline so the
|
|
1116
1239
|
// SessionStart hook can write ralph-loop.json without depending on the CLI
|
|
1117
1240
|
// binary being installed globally. Any schema change must update both copies.
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
import { type HookHandlerId, type HookManifestHarness } from "./hook-manifest.js";
|
|
1
2
|
export declare function claudeHooksJsonWithObservation(): string;
|
|
2
3
|
export declare function cursorHooksJsonWithObservation(): string;
|
|
3
4
|
export declare function codexHooksJsonWithObservation(): string;
|
|
5
|
+
/**
|
|
6
|
+
* Public accessor so diagnostic CLIs and tests can inspect the
|
|
7
|
+
* manifest without importing the private generator helpers.
|
|
8
|
+
*/
|
|
9
|
+
export declare function hookManifestSnapshot(): Array<{
|
|
10
|
+
harness: HookManifestHarness;
|
|
11
|
+
events: Array<{
|
|
12
|
+
event: string;
|
|
13
|
+
entries: Array<{
|
|
14
|
+
handler: HookHandlerId;
|
|
15
|
+
matcher?: string;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}>;
|
|
18
|
+
}>;
|
|
19
|
+
}>;
|
package/dist/content/observe.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
2
|
+
import { HOOK_MANIFEST, groupBindingsByEvent } from "./hook-manifest.js";
|
|
2
3
|
function hookDispatcherCommand(hookName) {
|
|
3
4
|
// RUNTIME_ROOT is a relative path (".cclaw") that currently contains no
|
|
4
5
|
// whitespace, so quoting is unnecessary inside the JSON-encoded command
|
|
@@ -6,139 +7,83 @@ function hookDispatcherCommand(hookName) {
|
|
|
6
7
|
// JSON.stringify to survive spaces.
|
|
7
8
|
return `node ${RUNTIME_ROOT}/hooks/run-hook.mjs ${hookName}`;
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Claude / Codex share the same outer envelope: each event is an
|
|
12
|
+
* array of `{matcher?, hooks: [{type: "command", command, timeout?}]}`
|
|
13
|
+
* objects. Entries with the same `matcher` are merged into a single
|
|
14
|
+
* outer entry so we emit one `{matcher: "..."}` block with multiple
|
|
15
|
+
* inner hook commands.
|
|
16
|
+
*/
|
|
17
|
+
function buildClaudeLikeEvents(harness) {
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const group of groupBindingsByEvent(harness)) {
|
|
20
|
+
const mergedByMatcher = new Map();
|
|
21
|
+
const order = [];
|
|
22
|
+
for (const entry of group.entries) {
|
|
23
|
+
const matcherKey = entry.matcher ?? "__no_matcher__";
|
|
24
|
+
let bucket = mergedByMatcher.get(matcherKey);
|
|
25
|
+
if (!bucket) {
|
|
26
|
+
bucket = {
|
|
27
|
+
...(entry.matcher !== undefined ? { matcher: entry.matcher } : {}),
|
|
28
|
+
hooks: []
|
|
29
|
+
};
|
|
30
|
+
mergedByMatcher.set(matcherKey, bucket);
|
|
31
|
+
order.push(matcherKey);
|
|
32
|
+
}
|
|
33
|
+
const hookEntry = {
|
|
34
|
+
type: "command",
|
|
35
|
+
command: hookDispatcherCommand(entry.handler),
|
|
36
|
+
...(entry.timeout !== undefined ? { timeout: entry.timeout } : {})
|
|
37
|
+
};
|
|
38
|
+
bucket.hooks.push(hookEntry);
|
|
39
|
+
}
|
|
40
|
+
out[group.event] = order.map((key) => mergedByMatcher.get(key));
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Cursor uses a flat shape: each event maps directly to an array of
|
|
46
|
+
* `{command, matcher?, timeout?}` entries — no inner `hooks` array.
|
|
47
|
+
*/
|
|
48
|
+
function buildCursorEvents() {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const group of groupBindingsByEvent("cursor")) {
|
|
51
|
+
out[group.event] = group.entries.map((entry) => ({
|
|
52
|
+
command: hookDispatcherCommand(entry.handler),
|
|
53
|
+
...(entry.matcher !== undefined ? { matcher: entry.matcher } : {}),
|
|
54
|
+
...(entry.timeout !== undefined ? { timeout: entry.timeout } : {})
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
9
59
|
export function claudeHooksJsonWithObservation() {
|
|
10
60
|
return JSON.stringify({
|
|
11
61
|
cclawHookSchemaVersion: 1,
|
|
12
|
-
hooks:
|
|
13
|
-
SessionStart: [{
|
|
14
|
-
matcher: "startup|resume|clear|compact",
|
|
15
|
-
hooks: [{
|
|
16
|
-
type: "command",
|
|
17
|
-
command: hookDispatcherCommand("session-start")
|
|
18
|
-
}]
|
|
19
|
-
}],
|
|
20
|
-
PreToolUse: [{
|
|
21
|
-
matcher: "*",
|
|
22
|
-
hooks: [{
|
|
23
|
-
type: "command",
|
|
24
|
-
command: hookDispatcherCommand("prompt-guard")
|
|
25
|
-
}]
|
|
26
|
-
}, {
|
|
27
|
-
matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash",
|
|
28
|
-
hooks: [{
|
|
29
|
-
type: "command",
|
|
30
|
-
command: hookDispatcherCommand("workflow-guard")
|
|
31
|
-
}]
|
|
32
|
-
}],
|
|
33
|
-
PostToolUse: [{
|
|
34
|
-
matcher: "*",
|
|
35
|
-
hooks: [{
|
|
36
|
-
type: "command",
|
|
37
|
-
command: hookDispatcherCommand("context-monitor")
|
|
38
|
-
}]
|
|
39
|
-
}],
|
|
40
|
-
Stop: [{
|
|
41
|
-
hooks: [{
|
|
42
|
-
type: "command",
|
|
43
|
-
command: hookDispatcherCommand("stop-checkpoint"),
|
|
44
|
-
timeout: 10
|
|
45
|
-
}]
|
|
46
|
-
}],
|
|
47
|
-
PreCompact: [{
|
|
48
|
-
matcher: "manual|auto",
|
|
49
|
-
hooks: [{
|
|
50
|
-
type: "command",
|
|
51
|
-
command: hookDispatcherCommand("pre-compact"),
|
|
52
|
-
timeout: 10
|
|
53
|
-
}]
|
|
54
|
-
}]
|
|
55
|
-
}
|
|
62
|
+
hooks: buildClaudeLikeEvents("claude")
|
|
56
63
|
}, null, 2);
|
|
57
64
|
}
|
|
58
65
|
export function cursorHooksJsonWithObservation() {
|
|
59
66
|
return JSON.stringify({
|
|
60
67
|
cclawHookSchemaVersion: 1,
|
|
61
68
|
version: 1,
|
|
62
|
-
hooks:
|
|
63
|
-
sessionStart: [{
|
|
64
|
-
command: hookDispatcherCommand("session-start")
|
|
65
|
-
}],
|
|
66
|
-
sessionResume: [{
|
|
67
|
-
command: hookDispatcherCommand("session-start")
|
|
68
|
-
}],
|
|
69
|
-
sessionClear: [{
|
|
70
|
-
command: hookDispatcherCommand("session-start")
|
|
71
|
-
}],
|
|
72
|
-
sessionCompact: [{
|
|
73
|
-
command: hookDispatcherCommand("pre-compact")
|
|
74
|
-
}, {
|
|
75
|
-
command: hookDispatcherCommand("session-start")
|
|
76
|
-
}],
|
|
77
|
-
preToolUse: [{
|
|
78
|
-
matcher: "*",
|
|
79
|
-
command: hookDispatcherCommand("prompt-guard")
|
|
80
|
-
}, {
|
|
81
|
-
matcher: "*",
|
|
82
|
-
command: hookDispatcherCommand("workflow-guard")
|
|
83
|
-
}],
|
|
84
|
-
postToolUse: [{
|
|
85
|
-
matcher: "*",
|
|
86
|
-
command: hookDispatcherCommand("context-monitor")
|
|
87
|
-
}],
|
|
88
|
-
stop: [{
|
|
89
|
-
command: hookDispatcherCommand("stop-checkpoint"),
|
|
90
|
-
timeout: 10
|
|
91
|
-
}]
|
|
92
|
-
}
|
|
69
|
+
hooks: buildCursorEvents()
|
|
93
70
|
}, null, 2);
|
|
94
71
|
}
|
|
95
72
|
export function codexHooksJsonWithObservation() {
|
|
96
73
|
return JSON.stringify({
|
|
97
74
|
cclawHookSchemaVersion: 1,
|
|
98
|
-
hooks:
|
|
99
|
-
SessionStart: [{
|
|
100
|
-
matcher: "startup|resume",
|
|
101
|
-
hooks: [{
|
|
102
|
-
type: "command",
|
|
103
|
-
command: hookDispatcherCommand("session-start")
|
|
104
|
-
}]
|
|
105
|
-
}],
|
|
106
|
-
UserPromptSubmit: [{
|
|
107
|
-
hooks: [{
|
|
108
|
-
type: "command",
|
|
109
|
-
command: hookDispatcherCommand("prompt-guard")
|
|
110
|
-
}, {
|
|
111
|
-
type: "command",
|
|
112
|
-
command: hookDispatcherCommand("workflow-guard")
|
|
113
|
-
}, {
|
|
114
|
-
type: "command",
|
|
115
|
-
command: hookDispatcherCommand("verify-current-state")
|
|
116
|
-
}]
|
|
117
|
-
}],
|
|
118
|
-
PreToolUse: [{
|
|
119
|
-
matcher: "Bash|bash",
|
|
120
|
-
hooks: [{
|
|
121
|
-
type: "command",
|
|
122
|
-
command: hookDispatcherCommand("prompt-guard")
|
|
123
|
-
}, {
|
|
124
|
-
type: "command",
|
|
125
|
-
command: hookDispatcherCommand("workflow-guard")
|
|
126
|
-
}]
|
|
127
|
-
}],
|
|
128
|
-
PostToolUse: [{
|
|
129
|
-
matcher: "Bash|bash",
|
|
130
|
-
hooks: [{
|
|
131
|
-
type: "command",
|
|
132
|
-
command: hookDispatcherCommand("context-monitor")
|
|
133
|
-
}]
|
|
134
|
-
}],
|
|
135
|
-
Stop: [{
|
|
136
|
-
hooks: [{
|
|
137
|
-
type: "command",
|
|
138
|
-
command: hookDispatcherCommand("stop-checkpoint"),
|
|
139
|
-
timeout: 10
|
|
140
|
-
}]
|
|
141
|
-
}]
|
|
142
|
-
}
|
|
75
|
+
hooks: buildClaudeLikeEvents("codex")
|
|
143
76
|
}, null, 2);
|
|
144
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Public accessor so diagnostic CLIs and tests can inspect the
|
|
80
|
+
* manifest without importing the private generator helpers.
|
|
81
|
+
*/
|
|
82
|
+
export function hookManifestSnapshot() {
|
|
83
|
+
return (HOOK_MANIFEST.length === 0
|
|
84
|
+
? ["claude", "cursor", "codex"]
|
|
85
|
+
: ["claude", "cursor", "codex"]).map((harness) => ({
|
|
86
|
+
harness,
|
|
87
|
+
events: groupBindingsByEvent(harness)
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
@@ -12,6 +12,8 @@ import { getAvailableTransitions, getTransitionGuards, isFlowTrack } from "../fl
|
|
|
12
12
|
import { appendKnowledge } from "../knowledge-store.js";
|
|
13
13
|
import { readFlowState, writeFlowState } from "../runs.js";
|
|
14
14
|
import { FLOW_STAGES } from "../types.js";
|
|
15
|
+
import { runCompoundReadinessCommand } from "./compound-readiness.js";
|
|
16
|
+
import { runHookManifestCommand } from "./hook-manifest.js";
|
|
15
17
|
import { runEnvelopeValidateCommand } from "./envelope-validate.js";
|
|
16
18
|
import { runKnowledgeDigestCommand } from "./knowledge-digest.js";
|
|
17
19
|
import { runTddLoopStatusCommand } from "./tdd-loop-status.js";
|
|
@@ -673,7 +675,7 @@ async function runHookCommand(projectRoot, args, io) {
|
|
|
673
675
|
export async function runInternalCommand(projectRoot, argv, io) {
|
|
674
676
|
const [subcommand, ...tokens] = argv;
|
|
675
677
|
if (!subcommand) {
|
|
676
|
-
io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | tdd-loop-status | hook\n");
|
|
678
|
+
io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n");
|
|
677
679
|
return 1;
|
|
678
680
|
}
|
|
679
681
|
try {
|
|
@@ -698,10 +700,16 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
698
700
|
if (subcommand === "tdd-loop-status") {
|
|
699
701
|
return await runTddLoopStatusCommand(projectRoot, tokens, io);
|
|
700
702
|
}
|
|
703
|
+
if (subcommand === "compound-readiness") {
|
|
704
|
+
return await runCompoundReadinessCommand(projectRoot, tokens, io);
|
|
705
|
+
}
|
|
706
|
+
if (subcommand === "hook-manifest") {
|
|
707
|
+
return await runHookManifestCommand(projectRoot, tokens, io);
|
|
708
|
+
}
|
|
701
709
|
if (subcommand === "hook") {
|
|
702
710
|
return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
|
|
703
711
|
}
|
|
704
|
-
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | tdd-loop-status | hook\n`);
|
|
712
|
+
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n`);
|
|
705
713
|
return 1;
|
|
706
714
|
}
|
|
707
715
|
catch (err) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
import { type CompoundReadiness } from "../knowledge-store.js";
|
|
3
|
+
interface InternalIo {
|
|
4
|
+
stdout: Writable;
|
|
5
|
+
stderr: Writable;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Compact one-liner for session-digest / bootstrap surfaces.
|
|
9
|
+
*
|
|
10
|
+
* Example: `Compound readiness: clusters=12, ready=2 (critical=1)`.
|
|
11
|
+
* When `ready === 0`, emit `Compound readiness: no candidates`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatCompoundReadinessLine(status: CompoundReadiness): string;
|
|
14
|
+
export declare function runCompoundReadinessCommand(projectRoot: string, argv: string[], io: InternalIo): Promise<number>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { RUNTIME_ROOT } from "../constants.js";
|
|
3
|
+
import { readConfig } from "../config.js";
|
|
4
|
+
import { writeFileSafe } from "../fs-utils.js";
|
|
5
|
+
import { computeCompoundReadiness, readKnowledgeSafely } from "../knowledge-store.js";
|
|
6
|
+
function parseArgs(tokens) {
|
|
7
|
+
const args = { json: false, quiet: false, write: true };
|
|
8
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
9
|
+
const token = tokens[i];
|
|
10
|
+
if (token === "--json")
|
|
11
|
+
args.json = true;
|
|
12
|
+
else if (token === "--quiet")
|
|
13
|
+
args.quiet = true;
|
|
14
|
+
else if (token === "--no-write")
|
|
15
|
+
args.write = false;
|
|
16
|
+
else if (token === "--write")
|
|
17
|
+
args.write = true;
|
|
18
|
+
else if (token === "--threshold") {
|
|
19
|
+
const value = tokens[i + 1];
|
|
20
|
+
if (!value)
|
|
21
|
+
throw new Error("--threshold requires a numeric value");
|
|
22
|
+
const parsed = Number.parseInt(value, 10);
|
|
23
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
24
|
+
throw new Error(`--threshold must be a positive integer, got ${value}`);
|
|
25
|
+
}
|
|
26
|
+
args.threshold = parsed;
|
|
27
|
+
i += 1;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
throw new Error(`Unknown compound-readiness flag: ${token}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return args;
|
|
34
|
+
}
|
|
35
|
+
function stateDir(projectRoot) {
|
|
36
|
+
return path.join(projectRoot, RUNTIME_ROOT, "state");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Compact one-liner for session-digest / bootstrap surfaces.
|
|
40
|
+
*
|
|
41
|
+
* Example: `Compound readiness: clusters=12, ready=2 (critical=1)`.
|
|
42
|
+
* When `ready === 0`, emit `Compound readiness: no candidates`.
|
|
43
|
+
*/
|
|
44
|
+
export function formatCompoundReadinessLine(status) {
|
|
45
|
+
if (status.readyCount === 0) {
|
|
46
|
+
return `Compound readiness: no candidates (clusters=${status.clusterCount}, threshold=${status.threshold})`;
|
|
47
|
+
}
|
|
48
|
+
const critical = status.ready.filter((cluster) => cluster.severity === "critical").length;
|
|
49
|
+
const criticalSuffix = critical > 0 ? ` (critical=${critical})` : "";
|
|
50
|
+
return `Compound readiness: clusters=${status.clusterCount}, ready=${status.readyCount}${criticalSuffix}`;
|
|
51
|
+
}
|
|
52
|
+
export async function runCompoundReadinessCommand(projectRoot, argv, io) {
|
|
53
|
+
const args = parseArgs(argv);
|
|
54
|
+
const config = await readConfig(projectRoot).catch(() => null);
|
|
55
|
+
const threshold = args.threshold ??
|
|
56
|
+
(typeof config?.compound?.recurrenceThreshold === "number"
|
|
57
|
+
? config.compound.recurrenceThreshold
|
|
58
|
+
: undefined);
|
|
59
|
+
const { entries } = await readKnowledgeSafely(projectRoot, { lockAware: false });
|
|
60
|
+
const status = computeCompoundReadiness(entries, {
|
|
61
|
+
...(typeof threshold === "number" ? { threshold } : {})
|
|
62
|
+
});
|
|
63
|
+
if (args.write) {
|
|
64
|
+
const target = path.join(stateDir(projectRoot), "compound-readiness.json");
|
|
65
|
+
await writeFileSafe(target, `${JSON.stringify(status, null, 2)}\n`);
|
|
66
|
+
}
|
|
67
|
+
if (!args.quiet) {
|
|
68
|
+
if (args.json) {
|
|
69
|
+
io.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
io.stdout.write(`${formatCompoundReadinessLine(status)}\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
interface InternalIo {
|
|
3
|
+
stdout: Writable;
|
|
4
|
+
stderr: Writable;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `cclaw internal hook-manifest` — diagnostic command that prints
|
|
8
|
+
* the resolved manifest. Primary use cases:
|
|
9
|
+
*
|
|
10
|
+
* - debugging "which handler fires for event X on harness Y",
|
|
11
|
+
* - migration tooling that needs a machine-readable view,
|
|
12
|
+
* - parity verification between the source-of-truth manifest and
|
|
13
|
+
* per-harness generated documents.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runHookManifestCommand(_projectRoot: string, argv: string[], io: InternalIo): Promise<number>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { HOOK_MANIFEST, HOOK_MANIFEST_HARNESSES, groupBindingsByEvent, requiredEventsFor } from "../content/hook-manifest.js";
|
|
2
|
+
function parseArgs(tokens) {
|
|
3
|
+
const args = { json: false };
|
|
4
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
5
|
+
const token = tokens[i];
|
|
6
|
+
if (token === "--json")
|
|
7
|
+
args.json = true;
|
|
8
|
+
else if (token === "--harness") {
|
|
9
|
+
const value = tokens[i + 1];
|
|
10
|
+
if (value !== "claude" && value !== "cursor" && value !== "codex") {
|
|
11
|
+
throw new Error(`--harness must be one of claude|cursor|codex, got ${String(value)}`);
|
|
12
|
+
}
|
|
13
|
+
args.harness = value;
|
|
14
|
+
i += 1;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
throw new Error(`Unknown hook-manifest flag: ${token}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return args;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `cclaw internal hook-manifest` — diagnostic command that prints
|
|
24
|
+
* the resolved manifest. Primary use cases:
|
|
25
|
+
*
|
|
26
|
+
* - debugging "which handler fires for event X on harness Y",
|
|
27
|
+
* - migration tooling that needs a machine-readable view,
|
|
28
|
+
* - parity verification between the source-of-truth manifest and
|
|
29
|
+
* per-harness generated documents.
|
|
30
|
+
*/
|
|
31
|
+
export async function runHookManifestCommand(_projectRoot, argv, io) {
|
|
32
|
+
const args = parseArgs(argv);
|
|
33
|
+
const harnesses = args.harness ? [args.harness] : [...HOOK_MANIFEST_HARNESSES];
|
|
34
|
+
if (args.json) {
|
|
35
|
+
const payload = {
|
|
36
|
+
handlers: HOOK_MANIFEST.map((spec) => ({
|
|
37
|
+
handler: spec.handler,
|
|
38
|
+
description: spec.description,
|
|
39
|
+
semantic: spec.semantic,
|
|
40
|
+
bindings: spec.bindings
|
|
41
|
+
})),
|
|
42
|
+
byHarness: Object.fromEntries(harnesses.map((harness) => [
|
|
43
|
+
harness,
|
|
44
|
+
{
|
|
45
|
+
requiredEvents: requiredEventsFor(harness),
|
|
46
|
+
events: groupBindingsByEvent(harness)
|
|
47
|
+
}
|
|
48
|
+
]))
|
|
49
|
+
};
|
|
50
|
+
io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
const lines = [];
|
|
54
|
+
lines.push("cclaw hook manifest");
|
|
55
|
+
for (const harness of harnesses) {
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push(`## ${harness}`);
|
|
58
|
+
const groups = groupBindingsByEvent(harness);
|
|
59
|
+
if (groups.length === 0) {
|
|
60
|
+
lines.push(" (no bindings)");
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
for (const group of groups) {
|
|
64
|
+
lines.push(` ${group.event}:`);
|
|
65
|
+
for (const entry of group.entries) {
|
|
66
|
+
const parts = [entry.handler];
|
|
67
|
+
if (entry.matcher)
|
|
68
|
+
parts.push(`matcher=${entry.matcher}`);
|
|
69
|
+
if (entry.timeout)
|
|
70
|
+
parts.push(`timeout=${entry.timeout}s`);
|
|
71
|
+
lines.push(` - ${parts.join(" ")}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
io.stdout.write(`${lines.join("\n")}\n`);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
@@ -72,6 +72,63 @@ export interface SelectRelevantLearningsOptions {
|
|
|
72
72
|
openGates?: string[];
|
|
73
73
|
limit?: number;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* One clustered (trigger, action) group ready for compound lift.
|
|
77
|
+
*
|
|
78
|
+
* A cluster "qualifies" when its recurrence count meets the configured
|
|
79
|
+
* threshold **or** any contributing entry is marked `severity: "critical"`.
|
|
80
|
+
* The skill surface exposes this for nudging — it is not a gate.
|
|
81
|
+
*/
|
|
82
|
+
export interface CompoundReadinessCluster {
|
|
83
|
+
trigger: string;
|
|
84
|
+
action: string;
|
|
85
|
+
/**
|
|
86
|
+
* Sum of `frequency` across entries in the cluster — matches the
|
|
87
|
+
* recurrence count used by `/cc-ops compound`.
|
|
88
|
+
*/
|
|
89
|
+
recurrence: number;
|
|
90
|
+
/** Distinct entry lines contributing to this cluster. */
|
|
91
|
+
entryCount: number;
|
|
92
|
+
qualification: "recurrence" | "critical_override";
|
|
93
|
+
severity?: KnowledgeEntrySeverity;
|
|
94
|
+
lastSeenTs: string;
|
|
95
|
+
/** Entry types observed (rule/pattern/lesson/compound). */
|
|
96
|
+
types: KnowledgeEntryType[];
|
|
97
|
+
/** Distinct maturity values observed across the cluster. */
|
|
98
|
+
maturity: KnowledgeEntryMaturity[];
|
|
99
|
+
}
|
|
100
|
+
export interface CompoundReadiness {
|
|
101
|
+
schemaVersion: 1;
|
|
102
|
+
/** Effective recurrence threshold applied to this computation. */
|
|
103
|
+
threshold: number;
|
|
104
|
+
/** Total number of (trigger, action) clusters seen, regardless of threshold. */
|
|
105
|
+
clusterCount: number;
|
|
106
|
+
/** Number of clusters that passed the threshold or critical override. */
|
|
107
|
+
readyCount: number;
|
|
108
|
+
/**
|
|
109
|
+
* Top ready clusters (sorted by qualification severity / recurrence /
|
|
110
|
+
* recency). Capped by `maxReady` to keep the artifact small.
|
|
111
|
+
*/
|
|
112
|
+
ready: CompoundReadinessCluster[];
|
|
113
|
+
lastUpdatedAt: string;
|
|
114
|
+
}
|
|
115
|
+
export interface ComputeCompoundReadinessOptions {
|
|
116
|
+
threshold?: number;
|
|
117
|
+
/** Hard cap on `ready[]` to keep the surface digest concise. Default 10. */
|
|
118
|
+
maxReady?: number;
|
|
119
|
+
now?: Date;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Pure function — no filesystem side effects. Callers pass entries from
|
|
123
|
+
* `readKnowledgeSafely` and get a derived readiness snapshot suitable
|
|
124
|
+
* for persisting to `.cclaw/state/compound-readiness.json`.
|
|
125
|
+
*
|
|
126
|
+
* Clustering key: `(type, normalizeText(trigger), normalizeText(action))`
|
|
127
|
+
* which mirrors the clustering used by the `/cc-ops compound` skill.
|
|
128
|
+
* Entries with `maturity === "lifted-to-enforcement"` are excluded —
|
|
129
|
+
* they were already promoted and should not re-appear as ready.
|
|
130
|
+
*/
|
|
131
|
+
export declare function computeCompoundReadiness(entries: KnowledgeEntry[], options?: ComputeCompoundReadinessOptions): CompoundReadiness;
|
|
75
132
|
export declare function validateKnowledgeEntry(entry: unknown): {
|
|
76
133
|
ok: boolean;
|
|
77
134
|
errors: string[];
|
package/dist/knowledge-store.js
CHANGED
|
@@ -1,8 +1,115 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { DEFAULT_COMPOUND_RECURRENCE_THRESHOLD } from "./config.js";
|
|
3
4
|
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
5
|
import { stripBom, withDirectoryLock } from "./fs-utils.js";
|
|
5
6
|
import { FLOW_STAGES } from "./types.js";
|
|
7
|
+
const DEFAULT_COMPOUND_READINESS_MAX_READY = 10;
|
|
8
|
+
/**
|
|
9
|
+
* Pure function — no filesystem side effects. Callers pass entries from
|
|
10
|
+
* `readKnowledgeSafely` and get a derived readiness snapshot suitable
|
|
11
|
+
* for persisting to `.cclaw/state/compound-readiness.json`.
|
|
12
|
+
*
|
|
13
|
+
* Clustering key: `(type, normalizeText(trigger), normalizeText(action))`
|
|
14
|
+
* which mirrors the clustering used by the `/cc-ops compound` skill.
|
|
15
|
+
* Entries with `maturity === "lifted-to-enforcement"` are excluded —
|
|
16
|
+
* they were already promoted and should not re-appear as ready.
|
|
17
|
+
*/
|
|
18
|
+
export function computeCompoundReadiness(entries, options = {}) {
|
|
19
|
+
const thresholdRaw = options.threshold ?? DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
20
|
+
const threshold = Number.isInteger(thresholdRaw) && thresholdRaw >= 1
|
|
21
|
+
? thresholdRaw
|
|
22
|
+
: DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
23
|
+
const maxReadyRaw = options.maxReady ?? DEFAULT_COMPOUND_READINESS_MAX_READY;
|
|
24
|
+
const maxReady = Number.isInteger(maxReadyRaw) && maxReadyRaw >= 1
|
|
25
|
+
? maxReadyRaw
|
|
26
|
+
: DEFAULT_COMPOUND_READINESS_MAX_READY;
|
|
27
|
+
const now = options.now ?? new Date();
|
|
28
|
+
const buckets = new Map();
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (entry.maturity === "lifted-to-enforcement")
|
|
31
|
+
continue;
|
|
32
|
+
const key = [
|
|
33
|
+
entry.type,
|
|
34
|
+
normalizeText(entry.trigger),
|
|
35
|
+
normalizeText(entry.action)
|
|
36
|
+
].join("||");
|
|
37
|
+
const frequency = Math.max(1, Math.floor(entry.frequency));
|
|
38
|
+
const bucket = buckets.get(key);
|
|
39
|
+
if (!bucket) {
|
|
40
|
+
buckets.set(key, {
|
|
41
|
+
trigger: entry.trigger,
|
|
42
|
+
action: entry.action,
|
|
43
|
+
recurrence: frequency,
|
|
44
|
+
entryCount: 1,
|
|
45
|
+
severity: entry.severity,
|
|
46
|
+
lastSeenTs: entry.last_seen_ts,
|
|
47
|
+
types: new Set([entry.type]),
|
|
48
|
+
maturity: new Set([entry.maturity])
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
bucket.recurrence += frequency;
|
|
53
|
+
bucket.entryCount += 1;
|
|
54
|
+
bucket.types.add(entry.type);
|
|
55
|
+
bucket.maturity.add(entry.maturity);
|
|
56
|
+
if (entry.severity === "critical") {
|
|
57
|
+
bucket.severity = "critical";
|
|
58
|
+
}
|
|
59
|
+
else if (entry.severity === "important" && bucket.severity !== "critical") {
|
|
60
|
+
bucket.severity = "important";
|
|
61
|
+
}
|
|
62
|
+
if (Date.parse(entry.last_seen_ts) > Date.parse(bucket.lastSeenTs)) {
|
|
63
|
+
bucket.lastSeenTs = entry.last_seen_ts;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const ready = [];
|
|
67
|
+
for (const bucket of buckets.values()) {
|
|
68
|
+
const criticalOverride = bucket.severity === "critical";
|
|
69
|
+
const meetsRecurrence = bucket.recurrence >= threshold;
|
|
70
|
+
if (!criticalOverride && !meetsRecurrence)
|
|
71
|
+
continue;
|
|
72
|
+
ready.push({
|
|
73
|
+
trigger: bucket.trigger,
|
|
74
|
+
action: bucket.action,
|
|
75
|
+
recurrence: bucket.recurrence,
|
|
76
|
+
entryCount: bucket.entryCount,
|
|
77
|
+
qualification: criticalOverride && !meetsRecurrence ? "critical_override" : "recurrence",
|
|
78
|
+
...(bucket.severity ? { severity: bucket.severity } : {}),
|
|
79
|
+
lastSeenTs: bucket.lastSeenTs,
|
|
80
|
+
types: Array.from(bucket.types).sort(),
|
|
81
|
+
maturity: Array.from(bucket.maturity).sort()
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
ready.sort((a, b) => {
|
|
85
|
+
const severityWeight = (sev) => {
|
|
86
|
+
if (sev === "critical")
|
|
87
|
+
return 3;
|
|
88
|
+
if (sev === "important")
|
|
89
|
+
return 2;
|
|
90
|
+
if (sev === "suggestion")
|
|
91
|
+
return 1;
|
|
92
|
+
return 0;
|
|
93
|
+
};
|
|
94
|
+
const severityDiff = severityWeight(b.severity) - severityWeight(a.severity);
|
|
95
|
+
if (severityDiff !== 0)
|
|
96
|
+
return severityDiff;
|
|
97
|
+
if (b.recurrence !== a.recurrence)
|
|
98
|
+
return b.recurrence - a.recurrence;
|
|
99
|
+
const recencyDiff = Date.parse(b.lastSeenTs) - Date.parse(a.lastSeenTs);
|
|
100
|
+
if (!Number.isNaN(recencyDiff) && recencyDiff !== 0)
|
|
101
|
+
return recencyDiff;
|
|
102
|
+
return a.trigger.localeCompare(b.trigger);
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
threshold,
|
|
107
|
+
clusterCount: buckets.size,
|
|
108
|
+
readyCount: ready.length,
|
|
109
|
+
ready: ready.slice(0, maxReady),
|
|
110
|
+
lastUpdatedAt: normalizeUtcIso(now.toISOString())
|
|
111
|
+
};
|
|
112
|
+
}
|
|
6
113
|
const KNOWLEDGE_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
|
|
7
114
|
const KNOWLEDGE_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
|
|
8
115
|
const KNOWLEDGE_SEVERITY_SET = new Set(["critical", "important", "suggestion"]);
|
package/dist/policy.js
CHANGED
|
@@ -146,6 +146,7 @@ export async function policyChecks(projectRoot, options = {}) {
|
|
|
146
146
|
{ file: runtimeFile("references/flow-map.md"), needle: "## Stages (8)", name: "reference:flow_map:stages" },
|
|
147
147
|
{ file: runtimeFile("references/flow-map.md"), needle: "## Ralph Loop", name: "reference:flow_map:ralph_loop" },
|
|
148
148
|
{ file: runtimeFile("references/flow-map.md"), needle: "## Key state files", name: "reference:flow_map:state_files" },
|
|
149
|
+
{ file: runtimeFile("references/flow-map.md"), needle: "## Compound readiness", name: "reference:flow_map:compound_readiness" },
|
|
149
150
|
{ file: runtimeFile("skills/session/SKILL.md"), needle: "## Session Resume Protocol", name: "utility_skill:session:resume" },
|
|
150
151
|
{ file: runtimeFile("skills/brainstorming/SKILL.md"), needle: "common-guidance.md", name: "stage_skill:shared_guidance_reference" },
|
|
151
152
|
{ file: runtimeFile("skills/security/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:security:hard_gate" },
|