cclaw-cli 0.48.14 → 0.48.15

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.
@@ -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\`, ...). |
@@ -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.
@@ -12,6 +12,7 @@ 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";
15
16
  import { runEnvelopeValidateCommand } from "./envelope-validate.js";
16
17
  import { runKnowledgeDigestCommand } from "./knowledge-digest.js";
17
18
  import { runTddLoopStatusCommand } from "./tdd-loop-status.js";
@@ -673,7 +674,7 @@ async function runHookCommand(projectRoot, args, io) {
673
674
  export async function runInternalCommand(projectRoot, argv, io) {
674
675
  const [subcommand, ...tokens] = argv;
675
676
  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");
677
+ 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\n");
677
678
  return 1;
678
679
  }
679
680
  try {
@@ -698,10 +699,13 @@ export async function runInternalCommand(projectRoot, argv, io) {
698
699
  if (subcommand === "tdd-loop-status") {
699
700
  return await runTddLoopStatusCommand(projectRoot, tokens, io);
700
701
  }
702
+ if (subcommand === "compound-readiness") {
703
+ return await runCompoundReadinessCommand(projectRoot, tokens, io);
704
+ }
701
705
  if (subcommand === "hook") {
702
706
  return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
703
707
  }
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`);
708
+ 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\n`);
705
709
  return 1;
706
710
  }
707
711
  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
+ }
@@ -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[];
@@ -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" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.14",
3
+ "version": "0.48.15",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {