cclaw-cli 0.48.31 → 0.48.33
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/artifact-linter.js +609 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +82 -4
- package/dist/content/examples.js +23 -6
- package/dist/content/hook-inline-snippets.d.ts +80 -0
- package/dist/content/hook-inline-snippets.js +270 -0
- package/dist/content/ideate-command.d.ts +6 -2
- package/dist/content/ideate-command.js +43 -16
- package/dist/content/ideate-frames.d.ts +31 -0
- package/dist/content/ideate-frames.js +140 -0
- package/dist/content/ideate-ranking.d.ts +25 -0
- package/dist/content/ideate-ranking.js +65 -0
- package/dist/content/node-hooks.js +9 -197
- package/dist/content/review-loop.d.ts +192 -0
- package/dist/content/review-loop.js +689 -0
- package/dist/content/seed-shelf.d.ts +36 -0
- package/dist/content/seed-shelf.js +236 -0
- package/dist/content/skills.js +77 -4
- package/dist/content/stage-schema.d.ts +1 -1
- package/dist/content/stage-schema.js +18 -2
- package/dist/content/stages/brainstorm.js +20 -4
- package/dist/content/stages/design.js +36 -8
- package/dist/content/stages/plan.js +5 -0
- package/dist/content/stages/review.js +5 -0
- package/dist/content/stages/schema-types.d.ts +29 -0
- package/dist/content/stages/scope.js +22 -6
- package/dist/content/stages/ship.js +6 -0
- package/dist/content/stages/spec.js +6 -0
- package/dist/content/stages/tdd.js +6 -0
- package/dist/content/start-command.js +24 -18
- package/dist/content/templates.js +108 -4
- package/dist/internal/advance-stage.js +143 -1
- package/dist/trace-matrix.d.ts +14 -0
- package/dist/trace-matrix.js +55 -1
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare function readConfig(projectRoot: string): Promise<CclawConfig>;
|
|
|
42
42
|
* the user set them explicitly. Keeps the default template small and honest:
|
|
43
43
|
* only knobs a new user would meaningfully flip show up.
|
|
44
44
|
*/
|
|
45
|
-
type AdvancedConfigKey = "tddTestGlobs" | "tdd" | "compound" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview" | "ironLaws";
|
|
45
|
+
type AdvancedConfigKey = "tddTestGlobs" | "tdd" | "compound" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview" | "ironLaws" | "optInAudits" | "reviewLoop";
|
|
46
46
|
/**
|
|
47
47
|
* Options controlling the serialisation shape of `config.yaml`.
|
|
48
48
|
*
|
package/dist/config.js
CHANGED
|
@@ -25,7 +25,9 @@ const ALLOWED_CONFIG_KEYS = new Set([
|
|
|
25
25
|
"languageRulePacks",
|
|
26
26
|
"trackHeuristics",
|
|
27
27
|
"sliceReview",
|
|
28
|
-
"ironLaws"
|
|
28
|
+
"ironLaws",
|
|
29
|
+
"optInAudits",
|
|
30
|
+
"reviewLoop"
|
|
29
31
|
]);
|
|
30
32
|
/**
|
|
31
33
|
* Config keys removed in the advisory-by-default consolidation. Kept here so
|
|
@@ -429,6 +431,76 @@ export async function readConfig(projectRoot) {
|
|
|
429
431
|
else {
|
|
430
432
|
ironLaws = { strictLaws: [] };
|
|
431
433
|
}
|
|
434
|
+
const optInAuditsRaw = parsed.optInAudits;
|
|
435
|
+
let optInAudits = undefined;
|
|
436
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "optInAudits")) {
|
|
437
|
+
if (!isRecord(optInAuditsRaw)) {
|
|
438
|
+
throw configValidationError(fullPath, `"optInAudits" must be an object`);
|
|
439
|
+
}
|
|
440
|
+
const unknownOptInAuditKeys = Object.keys(optInAuditsRaw).filter((key) => key !== "scopePreAudit" && key !== "staleDiagramAudit");
|
|
441
|
+
if (unknownOptInAuditKeys.length > 0) {
|
|
442
|
+
throw configValidationError(fullPath, `"optInAudits" has unknown key(s): ${unknownOptInAuditKeys.join(", ")}`);
|
|
443
|
+
}
|
|
444
|
+
if (optInAuditsRaw.scopePreAudit !== undefined &&
|
|
445
|
+
typeof optInAuditsRaw.scopePreAudit !== "boolean") {
|
|
446
|
+
throw configValidationError(fullPath, `"optInAudits.scopePreAudit" must be a boolean`);
|
|
447
|
+
}
|
|
448
|
+
if (optInAuditsRaw.staleDiagramAudit !== undefined &&
|
|
449
|
+
typeof optInAuditsRaw.staleDiagramAudit !== "boolean") {
|
|
450
|
+
throw configValidationError(fullPath, `"optInAudits.staleDiagramAudit" must be a boolean`);
|
|
451
|
+
}
|
|
452
|
+
optInAudits = {
|
|
453
|
+
scopePreAudit: typeof optInAuditsRaw.scopePreAudit === "boolean"
|
|
454
|
+
? optInAuditsRaw.scopePreAudit
|
|
455
|
+
: false,
|
|
456
|
+
staleDiagramAudit: typeof optInAuditsRaw.staleDiagramAudit === "boolean"
|
|
457
|
+
? optInAuditsRaw.staleDiagramAudit
|
|
458
|
+
: false
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const reviewLoopRaw = parsed.reviewLoop;
|
|
462
|
+
let reviewLoop = undefined;
|
|
463
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "reviewLoop")) {
|
|
464
|
+
if (!isRecord(reviewLoopRaw)) {
|
|
465
|
+
throw configValidationError(fullPath, `"reviewLoop" must be an object`);
|
|
466
|
+
}
|
|
467
|
+
const unknownReviewLoopKeys = Object.keys(reviewLoopRaw).filter((key) => key !== "externalSecondOpinion");
|
|
468
|
+
if (unknownReviewLoopKeys.length > 0) {
|
|
469
|
+
throw configValidationError(fullPath, `"reviewLoop" has unknown key(s): ${unknownReviewLoopKeys.join(", ")}`);
|
|
470
|
+
}
|
|
471
|
+
const externalRaw = reviewLoopRaw.externalSecondOpinion;
|
|
472
|
+
let externalSecondOpinion = undefined;
|
|
473
|
+
if (externalRaw !== undefined) {
|
|
474
|
+
if (!isRecord(externalRaw)) {
|
|
475
|
+
throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion" must be an object`);
|
|
476
|
+
}
|
|
477
|
+
const unknownExternalKeys = Object.keys(externalRaw).filter((key) => key !== "enabled" && key !== "model" && key !== "scoreDeltaThreshold");
|
|
478
|
+
if (unknownExternalKeys.length > 0) {
|
|
479
|
+
throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion" has unknown key(s): ${unknownExternalKeys.join(", ")}`);
|
|
480
|
+
}
|
|
481
|
+
if (externalRaw.enabled !== undefined && typeof externalRaw.enabled !== "boolean") {
|
|
482
|
+
throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.enabled" must be a boolean`);
|
|
483
|
+
}
|
|
484
|
+
if (externalRaw.model !== undefined && typeof externalRaw.model !== "string") {
|
|
485
|
+
throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.model" must be a string`);
|
|
486
|
+
}
|
|
487
|
+
if (externalRaw.scoreDeltaThreshold !== undefined &&
|
|
488
|
+
(typeof externalRaw.scoreDeltaThreshold !== "number" ||
|
|
489
|
+
Number.isNaN(externalRaw.scoreDeltaThreshold) ||
|
|
490
|
+
externalRaw.scoreDeltaThreshold < 0 ||
|
|
491
|
+
externalRaw.scoreDeltaThreshold > 1)) {
|
|
492
|
+
throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.scoreDeltaThreshold" must be a number between 0 and 1`);
|
|
493
|
+
}
|
|
494
|
+
externalSecondOpinion = {
|
|
495
|
+
enabled: externalRaw.enabled === true,
|
|
496
|
+
model: typeof externalRaw.model === "string" ? externalRaw.model : undefined,
|
|
497
|
+
scoreDeltaThreshold: typeof externalRaw.scoreDeltaThreshold === "number"
|
|
498
|
+
? externalRaw.scoreDeltaThreshold
|
|
499
|
+
: 0.2
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
reviewLoop = { externalSecondOpinion };
|
|
503
|
+
}
|
|
432
504
|
return {
|
|
433
505
|
version: parsed.version ?? CCLAW_VERSION,
|
|
434
506
|
flowVersion: parsed.flowVersion ?? FLOW_VERSION,
|
|
@@ -447,7 +519,9 @@ export async function readConfig(projectRoot) {
|
|
|
447
519
|
languageRulePacks,
|
|
448
520
|
trackHeuristics,
|
|
449
521
|
sliceReview,
|
|
450
|
-
ironLaws
|
|
522
|
+
ironLaws,
|
|
523
|
+
optInAudits,
|
|
524
|
+
reviewLoop
|
|
451
525
|
};
|
|
452
526
|
}
|
|
453
527
|
function isMinimalKey(key) {
|
|
@@ -470,7 +544,9 @@ function buildSerializableConfig(config, options = {}) {
|
|
|
470
544
|
"languageRulePacks",
|
|
471
545
|
"trackHeuristics",
|
|
472
546
|
"sliceReview",
|
|
473
|
-
"ironLaws"
|
|
547
|
+
"ironLaws",
|
|
548
|
+
"optInAudits",
|
|
549
|
+
"reviewLoop"
|
|
474
550
|
];
|
|
475
551
|
for (const key of ordered) {
|
|
476
552
|
const value = config[key];
|
|
@@ -523,7 +599,9 @@ export async function detectAdvancedKeys(projectRoot) {
|
|
|
523
599
|
"languageRulePacks",
|
|
524
600
|
"trackHeuristics",
|
|
525
601
|
"sliceReview",
|
|
526
|
-
"ironLaws"
|
|
602
|
+
"ironLaws",
|
|
603
|
+
"optInAudits",
|
|
604
|
+
"reviewLoop"
|
|
527
605
|
];
|
|
528
606
|
const present = new Set();
|
|
529
607
|
for (const key of advancedCandidates) {
|
package/dist/content/examples.js
CHANGED
|
@@ -18,18 +18,35 @@ const STAGE_EXAMPLES = {
|
|
|
18
18
|
| 2 | Should the validation logic live in a reusable module or stay as shell scripts? | Reusable module. | Architecture: shared TypeScript module imported by CI and local tooling, not duplicated shell scripts. |
|
|
19
19
|
| 3 | For v1, prioritize rapid delivery or maximum configurability? | Rapid delivery. | Minimal deterministic validation surface; defer plugin/config system to v2. |
|
|
20
20
|
|
|
21
|
+
## Approach Tier
|
|
22
|
+
|
|
23
|
+
- **Tier:** Standard
|
|
24
|
+
- **Why this tier:** Change spans CI + local release workflow and shared module boundaries, but remains bounded to one subsystem.
|
|
25
|
+
|
|
26
|
+
## Short-Circuit Decision
|
|
27
|
+
|
|
28
|
+
- **Status:** bypassed
|
|
29
|
+
- **Why:** Core requirements were not concrete enough initially; we still needed options + trade-off conversation.
|
|
30
|
+
- **Scope handoff:** Continue full brainstorm flow before scope.
|
|
31
|
+
|
|
21
32
|
## Approaches
|
|
22
33
|
|
|
23
|
-
| Approach | Architecture | Trade-offs | Recommendation |
|
|
24
|
-
| --- | --- | --- | --- |
|
|
25
|
-
| A: Reusable validation module | Shared TS module with typed validators, imported by CI scripts and local CLI. Existing \`pre-publish.sh\` calls the module. | Medium upfront effort, high reuse. Requires test coverage for the module. | **Recommended** — best balance of reuse and delivery speed. |
|
|
26
|
-
| B: Hardened shell scripts | Keep existing script approach, add stricter checks and error messages. | Lowest effort. Weak reuse, CI/local divergence risk grows over time. | Viable fallback if TS module is blocked. |
|
|
27
|
-
| C: Full release framework | New release orchestrator with plugin system, config files, rollback commands. | Maximum flexibility. High risk, delivery delay, over-engineered for current needs. | Not recommended for v1. |
|
|
34
|
+
| Approach | Role | Architecture | Trade-offs | Recommendation |
|
|
35
|
+
| --- | --- | --- | --- | --- |
|
|
36
|
+
| A: Reusable validation module | baseline | Shared TS module with typed validators, imported by CI scripts and local CLI. Existing \`pre-publish.sh\` calls the module. | Medium upfront effort, high reuse. Requires test coverage for the module. | **Recommended** — best balance of reuse and delivery speed. |
|
|
37
|
+
| B: Hardened shell scripts | fallback | Keep existing script approach, add stricter checks and error messages. | Lowest effort. Weak reuse, CI/local divergence risk grows over time. | Viable fallback if TS module is blocked. |
|
|
38
|
+
| C: Full release framework | challenger: higher-upside | New release orchestrator with plugin system, config files, rollback commands. | Maximum flexibility. High risk, delivery delay, over-engineered for current needs. | Not recommended for v1. |
|
|
39
|
+
|
|
40
|
+
## Approach Reaction
|
|
41
|
+
|
|
42
|
+
- **Closest option:** A (reusable validation module).
|
|
43
|
+
- **Concerns:** User wanted to avoid framework-level overbuild and keep v1 delivery speed high.
|
|
44
|
+
- **What changed after reaction:** Recommendation stayed on A, but added explicit fallback path via existing shell entrypoint to reduce migration risk.
|
|
28
45
|
|
|
29
46
|
## Selected Direction
|
|
30
47
|
|
|
31
48
|
- **Approach:** A — Reusable validation module
|
|
32
|
-
- **Rationale:** shared TS module gives consistent behavior in CI
|
|
49
|
+
- **Rationale:** based on user reaction favoring fast delivery and lower complexity, shared TS module gives consistent behavior in CI/local, avoids script duplication, and stays within the no-new-dependency constraint.
|
|
33
50
|
- **Approval:** approved
|
|
34
51
|
|
|
35
52
|
## Design
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hook-inline-snippets.ts
|
|
3
|
+
*
|
|
4
|
+
* Runtime `.cclaw/hooks/run-hook.mjs` is a **standalone Node script** that
|
|
5
|
+
* cannot import from `cclaw-cli` — it must work inside the end-user's
|
|
6
|
+
* project even when the CLI is not installed. Two derived computations,
|
|
7
|
+
* though, must remain 1:1 with the canonical TS implementations:
|
|
8
|
+
*
|
|
9
|
+
* 1. `computeCompoundReadinessInline` mirrors
|
|
10
|
+
* `src/knowledge-store.ts::computeCompoundReadiness`.
|
|
11
|
+
* 2. `computeRalphLoopStatusInline` mirrors
|
|
12
|
+
* `src/tdd-cycle.ts::computeRalphLoopStatus`.
|
|
13
|
+
*
|
|
14
|
+
* Previously those bodies lived inline in `src/content/node-hooks.ts` — a
|
|
15
|
+
* ~2000-line file — next to unrelated hook-handler code. Any silent drift
|
|
16
|
+
* only surfaced when someone remembered to update both sides.
|
|
17
|
+
*
|
|
18
|
+
* This module centralizes the inline JavaScript snippets so:
|
|
19
|
+
*
|
|
20
|
+
* - There is exactly **one place** (this file) that holds each inline
|
|
21
|
+
* JS body.
|
|
22
|
+
* - Each snippet carries an explicit "mirrors X, parity enforced by Y"
|
|
23
|
+
* header comment and is emitted into `run-hook.mjs` verbatim.
|
|
24
|
+
* - `src/content/node-hooks.ts` only interpolates the snippets, it no
|
|
25
|
+
* longer owns their source code.
|
|
26
|
+
*
|
|
27
|
+
* Parity with the TypeScript canonical implementations is enforced by
|
|
28
|
+
* `tests/unit/ralph-loop-parity.test.ts`. Any structural change to the
|
|
29
|
+
* canonical TS code MUST:
|
|
30
|
+
*
|
|
31
|
+
* 1. Update the matching snippet below.
|
|
32
|
+
* 2. Re-run `npm test tests/unit/ralph-loop-parity.test.ts`.
|
|
33
|
+
*
|
|
34
|
+
* DO NOT inline tests here — keep the parity check in its dedicated test
|
|
35
|
+
* file.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Inline JS helpers used by both compound-readiness and ralph-loop
|
|
39
|
+
* snippets. Kept small and locked: they are shared across the two inline
|
|
40
|
+
* routines and must not grow into a hidden utility namespace.
|
|
41
|
+
*
|
|
42
|
+
* - `normalizeCompoundLastUpdatedAt` produces a stable ISO-8601 UTC
|
|
43
|
+
* timestamp so the hook-written `compound-readiness.json` is byte-equal
|
|
44
|
+
* to the CLI-written version for the same input.
|
|
45
|
+
* - `countArchivedRunsInline` counts immediate subdirectories of
|
|
46
|
+
* `<root>/.cclaw/runs/` so both the hook and the CLI see the same
|
|
47
|
+
* `archivedRunsCount` for the small-project relaxation.
|
|
48
|
+
*/
|
|
49
|
+
export declare const HOOK_INLINE_SHARED_HELPERS = "\nfunction normalizeCompoundLastUpdatedAt(date) {\n return date.toISOString().replace(/\\.\\d{3}Z$/u, \"Z\");\n}\n\n// Count archived runs as sub-directories under `.cclaw/runs/`. Missing\n// dir returns 0; unexpected errors return undefined so the caller can\n// skip the small-project relaxation rather than guess.\nasync function countArchivedRunsInline(root) {\n const dir = path.join(root, RUNTIME_ROOT, \"runs\");\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n return entries.filter((entry) => entry.isDirectory()).length;\n } catch (error) {\n const code = error && typeof error === \"object\" && \"code\" in error ? error.code : null;\n if (code === \"ENOENT\") return 0;\n return undefined;\n }\n}\n";
|
|
50
|
+
/**
|
|
51
|
+
* Inline mirror of `src/knowledge-store.ts::computeCompoundReadiness`.
|
|
52
|
+
*
|
|
53
|
+
* Parity enforced by
|
|
54
|
+
* `tests/unit/ralph-loop-parity.test.ts::compound-readiness parity`.
|
|
55
|
+
*
|
|
56
|
+
* Signature contract:
|
|
57
|
+
* async function computeCompoundReadinessInline(root, options) -> CompoundReadiness
|
|
58
|
+
*
|
|
59
|
+
* Accepted options (all optional):
|
|
60
|
+
* - prereadRaw: string | undefined — pre-read `knowledge.jsonl` contents.
|
|
61
|
+
* - threshold: integer >= 1 — default recurrence threshold.
|
|
62
|
+
* - archivedRunsCount: integer >= 0 — enables small-project relaxation.
|
|
63
|
+
* - maxReady: integer >= 1 — cap on returned `ready` cluster count
|
|
64
|
+
* (default 10).
|
|
65
|
+
*
|
|
66
|
+
* Depends on: `SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD`,
|
|
67
|
+
* `SMALL_PROJECT_RECURRENCE_THRESHOLD`, `COMPOUND_RECURRENCE_THRESHOLD`,
|
|
68
|
+
* and `HOOK_INLINE_SHARED_HELPERS` being in the same runtime scope.
|
|
69
|
+
*/
|
|
70
|
+
export declare const COMPOUND_READINESS_INLINE_SOURCE = "\nasync function computeCompoundReadinessInline(root, options) {\n const filePath = path.join(root, RUNTIME_ROOT, \"knowledge.jsonl\");\n // Caller may supply pre-read raw to avoid double-reading knowledge.jsonl.\n const raw = typeof (options && options.prereadRaw) === \"string\"\n ? options.prereadRaw\n : await readTextFile(filePath, \"\");\n const baseThresholdRaw = options && options.threshold;\n const baseThreshold = Number.isInteger(baseThresholdRaw) && baseThresholdRaw >= 1\n ? baseThresholdRaw\n : COMPOUND_RECURRENCE_THRESHOLD;\n const archivedRunsCount =\n typeof (options && options.archivedRunsCount) === \"number\" &&\n Number.isFinite(options.archivedRunsCount) &&\n options.archivedRunsCount >= 0\n ? Math.floor(options.archivedRunsCount)\n : undefined;\n const smallProjectRelaxationApplied =\n archivedRunsCount !== undefined &&\n archivedRunsCount < SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD &&\n baseThreshold > SMALL_PROJECT_RECURRENCE_THRESHOLD;\n const threshold = smallProjectRelaxationApplied\n ? SMALL_PROJECT_RECURRENCE_THRESHOLD\n : baseThreshold;\n const maxReady = Number.isInteger(options && options.maxReady) && options.maxReady >= 1\n ? options.maxReady\n : 10;\n const normalize = (value) => String(value == null ? \"\" : value).trim().replace(/\\s+/gu, \" \").toLowerCase();\n const severityWeight = (sev) => {\n if (sev === \"critical\") return 3;\n if (sev === \"important\") return 2;\n if (sev === \"suggestion\") return 1;\n return 0;\n };\n const buckets = new Map();\n for (const rawLine of raw.split(/\\r?\\n/gu)) {\n const line = rawLine.trim();\n if (line.length === 0) continue;\n let row;\n try { row = JSON.parse(line); } catch { continue; }\n if (!row || typeof row !== \"object\" || Array.isArray(row)) continue;\n if (row.maturity === \"lifted-to-enforcement\") continue;\n const type = typeof row.type === \"string\" ? row.type : \"\";\n const trigger = typeof row.trigger === \"string\" ? row.trigger : \"\";\n const action = typeof row.action === \"string\" ? row.action : \"\";\n if (type.length === 0 || trigger.length === 0 || action.length === 0) continue;\n const key = type + \"||\" + normalize(trigger) + \"||\" + normalize(action);\n const frequency = Number.isInteger(row.frequency) && row.frequency > 0 ? Math.floor(row.frequency) : 1;\n const lastSeen = typeof row.last_seen_ts === \"string\" ? row.last_seen_ts : \"\";\n let bucket = buckets.get(key);\n if (!bucket) {\n bucket = {\n trigger,\n action,\n recurrence: frequency,\n entryCount: 1,\n severity: typeof row.severity === \"string\" ? row.severity : undefined,\n lastSeenTs: lastSeen,\n types: new Set([type]),\n maturity: new Set([typeof row.maturity === \"string\" ? row.maturity : \"raw\"])\n };\n buckets.set(key, bucket);\n continue;\n }\n bucket.recurrence += frequency;\n bucket.entryCount += 1;\n bucket.types.add(type);\n bucket.maturity.add(typeof row.maturity === \"string\" ? row.maturity : \"raw\");\n if (row.severity === \"critical\") {\n bucket.severity = \"critical\";\n } else if (row.severity === \"important\" && bucket.severity !== \"critical\") {\n bucket.severity = \"important\";\n }\n if (lastSeen && Date.parse(lastSeen) > Date.parse(bucket.lastSeenTs || \"0\")) {\n bucket.lastSeenTs = lastSeen;\n }\n }\n const ready = [];\n for (const bucket of buckets.values()) {\n const criticalOverride = bucket.severity === \"critical\";\n const meetsRecurrence = bucket.recurrence >= threshold;\n if (!criticalOverride && !meetsRecurrence) continue;\n ready.push({\n trigger: bucket.trigger,\n action: bucket.action,\n recurrence: bucket.recurrence,\n entryCount: bucket.entryCount,\n qualification: criticalOverride && !meetsRecurrence ? \"critical_override\" : \"recurrence\",\n ...(bucket.severity ? { severity: bucket.severity } : {}),\n lastSeenTs: bucket.lastSeenTs,\n types: Array.from(bucket.types).sort(),\n maturity: Array.from(bucket.maturity).sort()\n });\n }\n ready.sort((a, b) => {\n const sevDiff = severityWeight(b.severity) - severityWeight(a.severity);\n if (sevDiff !== 0) return sevDiff;\n if (b.recurrence !== a.recurrence) return b.recurrence - a.recurrence;\n const recencyDiff = Date.parse(b.lastSeenTs || \"0\") - Date.parse(a.lastSeenTs || \"0\");\n if (!Number.isNaN(recencyDiff) && recencyDiff !== 0) return recencyDiff;\n return String(a.trigger).localeCompare(String(b.trigger));\n });\n return {\n schemaVersion: 2,\n threshold,\n baseThreshold,\n ...(archivedRunsCount !== undefined ? { archivedRunsCount } : {}),\n smallProjectRelaxationApplied,\n clusterCount: buckets.size,\n readyCount: ready.length,\n ready: ready.slice(0, maxReady),\n lastUpdatedAt: normalizeCompoundLastUpdatedAt(new Date())\n };\n}\n";
|
|
71
|
+
/**
|
|
72
|
+
* Inline mirror of `src/tdd-cycle.ts::computeRalphLoopStatus`.
|
|
73
|
+
*
|
|
74
|
+
* Parity enforced by
|
|
75
|
+
* `tests/unit/ralph-loop-parity.test.ts::ralph-loop parity`.
|
|
76
|
+
*
|
|
77
|
+
* Signature contract:
|
|
78
|
+
* async function computeRalphLoopStatusInline(stateDir, runId) -> RalphLoopStatus
|
|
79
|
+
*/
|
|
80
|
+
export declare const RALPH_LOOP_INLINE_SOURCE = "\nasync function computeRalphLoopStatusInline(stateDir, runId) {\n const filePath = path.join(stateDir, \"tdd-cycle-log.jsonl\");\n const raw = await readTextFile(filePath, \"\");\n const sliceMap = new Map();\n const acClosed = new Set();\n const redOpenSlices = [];\n let loopIteration = 0;\n for (const rawLine of raw.split(/\\r?\\n/gu)) {\n const line = rawLine.trim();\n if (line.length === 0) continue;\n let row;\n try { row = JSON.parse(line); } catch { continue; }\n if (!row || typeof row !== \"object\" || Array.isArray(row)) continue;\n const rowRun = typeof row.runId === \"string\" && row.runId.length > 0 ? row.runId : runId;\n if (rowRun !== runId) continue;\n const slice = typeof row.slice === \"string\" && row.slice.length > 0 ? row.slice : \"S-unknown\";\n let state = sliceMap.get(slice);\n if (!state) {\n state = { slice, redCount: 0, greenCount: 0, refactorCount: 0, redOpen: false, acIds: [] };\n sliceMap.set(slice, state);\n }\n const exitCode = typeof row.exitCode === \"number\" ? row.exitCode : undefined;\n if (row.phase === \"red\") {\n state.redCount += 1;\n if (exitCode !== undefined && exitCode !== 0) state.redOpen = true;\n } else if (row.phase === \"green\") {\n state.greenCount += 1;\n state.redOpen = false;\n loopIteration += 1;\n if (Array.isArray(row.acIds)) {\n for (const acId of row.acIds) {\n if (typeof acId !== \"string\" || acId.length === 0) continue;\n acClosed.add(acId);\n if (!state.acIds.includes(acId)) state.acIds.push(acId);\n }\n }\n } else if (row.phase === \"refactor\") {\n state.refactorCount += 1;\n }\n }\n for (const state of sliceMap.values()) {\n if (state.redOpen) redOpenSlices.push(state.slice);\n }\n const slices = Array.from(sliceMap.values()).sort((a, b) => a.slice.localeCompare(b.slice, \"en\"));\n return {\n schemaVersion: 1,\n runId,\n loopIteration,\n redOpen: redOpenSlices.length > 0,\n redOpenSlices,\n acClosed: Array.from(acClosed).sort(),\n sliceCount: slices.length,\n slices,\n lastUpdatedAt: new Date().toISOString()\n };\n}\n";
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hook-inline-snippets.ts
|
|
3
|
+
*
|
|
4
|
+
* Runtime `.cclaw/hooks/run-hook.mjs` is a **standalone Node script** that
|
|
5
|
+
* cannot import from `cclaw-cli` — it must work inside the end-user's
|
|
6
|
+
* project even when the CLI is not installed. Two derived computations,
|
|
7
|
+
* though, must remain 1:1 with the canonical TS implementations:
|
|
8
|
+
*
|
|
9
|
+
* 1. `computeCompoundReadinessInline` mirrors
|
|
10
|
+
* `src/knowledge-store.ts::computeCompoundReadiness`.
|
|
11
|
+
* 2. `computeRalphLoopStatusInline` mirrors
|
|
12
|
+
* `src/tdd-cycle.ts::computeRalphLoopStatus`.
|
|
13
|
+
*
|
|
14
|
+
* Previously those bodies lived inline in `src/content/node-hooks.ts` — a
|
|
15
|
+
* ~2000-line file — next to unrelated hook-handler code. Any silent drift
|
|
16
|
+
* only surfaced when someone remembered to update both sides.
|
|
17
|
+
*
|
|
18
|
+
* This module centralizes the inline JavaScript snippets so:
|
|
19
|
+
*
|
|
20
|
+
* - There is exactly **one place** (this file) that holds each inline
|
|
21
|
+
* JS body.
|
|
22
|
+
* - Each snippet carries an explicit "mirrors X, parity enforced by Y"
|
|
23
|
+
* header comment and is emitted into `run-hook.mjs` verbatim.
|
|
24
|
+
* - `src/content/node-hooks.ts` only interpolates the snippets, it no
|
|
25
|
+
* longer owns their source code.
|
|
26
|
+
*
|
|
27
|
+
* Parity with the TypeScript canonical implementations is enforced by
|
|
28
|
+
* `tests/unit/ralph-loop-parity.test.ts`. Any structural change to the
|
|
29
|
+
* canonical TS code MUST:
|
|
30
|
+
*
|
|
31
|
+
* 1. Update the matching snippet below.
|
|
32
|
+
* 2. Re-run `npm test tests/unit/ralph-loop-parity.test.ts`.
|
|
33
|
+
*
|
|
34
|
+
* DO NOT inline tests here — keep the parity check in its dedicated test
|
|
35
|
+
* file.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Inline JS helpers used by both compound-readiness and ralph-loop
|
|
39
|
+
* snippets. Kept small and locked: they are shared across the two inline
|
|
40
|
+
* routines and must not grow into a hidden utility namespace.
|
|
41
|
+
*
|
|
42
|
+
* - `normalizeCompoundLastUpdatedAt` produces a stable ISO-8601 UTC
|
|
43
|
+
* timestamp so the hook-written `compound-readiness.json` is byte-equal
|
|
44
|
+
* to the CLI-written version for the same input.
|
|
45
|
+
* - `countArchivedRunsInline` counts immediate subdirectories of
|
|
46
|
+
* `<root>/.cclaw/runs/` so both the hook and the CLI see the same
|
|
47
|
+
* `archivedRunsCount` for the small-project relaxation.
|
|
48
|
+
*/
|
|
49
|
+
export const HOOK_INLINE_SHARED_HELPERS = `
|
|
50
|
+
function normalizeCompoundLastUpdatedAt(date) {
|
|
51
|
+
return date.toISOString().replace(/\\.\\d{3}Z$/u, "Z");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Count archived runs as sub-directories under \`.cclaw/runs/\`. Missing
|
|
55
|
+
// dir returns 0; unexpected errors return undefined so the caller can
|
|
56
|
+
// skip the small-project relaxation rather than guess.
|
|
57
|
+
async function countArchivedRunsInline(root) {
|
|
58
|
+
const dir = path.join(root, RUNTIME_ROOT, "runs");
|
|
59
|
+
try {
|
|
60
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
61
|
+
return entries.filter((entry) => entry.isDirectory()).length;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
64
|
+
if (code === "ENOENT") return 0;
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
/**
|
|
70
|
+
* Inline mirror of `src/knowledge-store.ts::computeCompoundReadiness`.
|
|
71
|
+
*
|
|
72
|
+
* Parity enforced by
|
|
73
|
+
* `tests/unit/ralph-loop-parity.test.ts::compound-readiness parity`.
|
|
74
|
+
*
|
|
75
|
+
* Signature contract:
|
|
76
|
+
* async function computeCompoundReadinessInline(root, options) -> CompoundReadiness
|
|
77
|
+
*
|
|
78
|
+
* Accepted options (all optional):
|
|
79
|
+
* - prereadRaw: string | undefined — pre-read `knowledge.jsonl` contents.
|
|
80
|
+
* - threshold: integer >= 1 — default recurrence threshold.
|
|
81
|
+
* - archivedRunsCount: integer >= 0 — enables small-project relaxation.
|
|
82
|
+
* - maxReady: integer >= 1 — cap on returned `ready` cluster count
|
|
83
|
+
* (default 10).
|
|
84
|
+
*
|
|
85
|
+
* Depends on: `SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD`,
|
|
86
|
+
* `SMALL_PROJECT_RECURRENCE_THRESHOLD`, `COMPOUND_RECURRENCE_THRESHOLD`,
|
|
87
|
+
* and `HOOK_INLINE_SHARED_HELPERS` being in the same runtime scope.
|
|
88
|
+
*/
|
|
89
|
+
export const COMPOUND_READINESS_INLINE_SOURCE = `
|
|
90
|
+
async function computeCompoundReadinessInline(root, options) {
|
|
91
|
+
const filePath = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
92
|
+
// Caller may supply pre-read raw to avoid double-reading knowledge.jsonl.
|
|
93
|
+
const raw = typeof (options && options.prereadRaw) === "string"
|
|
94
|
+
? options.prereadRaw
|
|
95
|
+
: await readTextFile(filePath, "");
|
|
96
|
+
const baseThresholdRaw = options && options.threshold;
|
|
97
|
+
const baseThreshold = Number.isInteger(baseThresholdRaw) && baseThresholdRaw >= 1
|
|
98
|
+
? baseThresholdRaw
|
|
99
|
+
: COMPOUND_RECURRENCE_THRESHOLD;
|
|
100
|
+
const archivedRunsCount =
|
|
101
|
+
typeof (options && options.archivedRunsCount) === "number" &&
|
|
102
|
+
Number.isFinite(options.archivedRunsCount) &&
|
|
103
|
+
options.archivedRunsCount >= 0
|
|
104
|
+
? Math.floor(options.archivedRunsCount)
|
|
105
|
+
: undefined;
|
|
106
|
+
const smallProjectRelaxationApplied =
|
|
107
|
+
archivedRunsCount !== undefined &&
|
|
108
|
+
archivedRunsCount < SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD &&
|
|
109
|
+
baseThreshold > SMALL_PROJECT_RECURRENCE_THRESHOLD;
|
|
110
|
+
const threshold = smallProjectRelaxationApplied
|
|
111
|
+
? SMALL_PROJECT_RECURRENCE_THRESHOLD
|
|
112
|
+
: baseThreshold;
|
|
113
|
+
const maxReady = Number.isInteger(options && options.maxReady) && options.maxReady >= 1
|
|
114
|
+
? options.maxReady
|
|
115
|
+
: 10;
|
|
116
|
+
const normalize = (value) => String(value == null ? "" : value).trim().replace(/\\s+/gu, " ").toLowerCase();
|
|
117
|
+
const severityWeight = (sev) => {
|
|
118
|
+
if (sev === "critical") return 3;
|
|
119
|
+
if (sev === "important") return 2;
|
|
120
|
+
if (sev === "suggestion") return 1;
|
|
121
|
+
return 0;
|
|
122
|
+
};
|
|
123
|
+
const buckets = new Map();
|
|
124
|
+
for (const rawLine of raw.split(/\\r?\\n/gu)) {
|
|
125
|
+
const line = rawLine.trim();
|
|
126
|
+
if (line.length === 0) continue;
|
|
127
|
+
let row;
|
|
128
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
129
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
130
|
+
if (row.maturity === "lifted-to-enforcement") continue;
|
|
131
|
+
const type = typeof row.type === "string" ? row.type : "";
|
|
132
|
+
const trigger = typeof row.trigger === "string" ? row.trigger : "";
|
|
133
|
+
const action = typeof row.action === "string" ? row.action : "";
|
|
134
|
+
if (type.length === 0 || trigger.length === 0 || action.length === 0) continue;
|
|
135
|
+
const key = type + "||" + normalize(trigger) + "||" + normalize(action);
|
|
136
|
+
const frequency = Number.isInteger(row.frequency) && row.frequency > 0 ? Math.floor(row.frequency) : 1;
|
|
137
|
+
const lastSeen = typeof row.last_seen_ts === "string" ? row.last_seen_ts : "";
|
|
138
|
+
let bucket = buckets.get(key);
|
|
139
|
+
if (!bucket) {
|
|
140
|
+
bucket = {
|
|
141
|
+
trigger,
|
|
142
|
+
action,
|
|
143
|
+
recurrence: frequency,
|
|
144
|
+
entryCount: 1,
|
|
145
|
+
severity: typeof row.severity === "string" ? row.severity : undefined,
|
|
146
|
+
lastSeenTs: lastSeen,
|
|
147
|
+
types: new Set([type]),
|
|
148
|
+
maturity: new Set([typeof row.maturity === "string" ? row.maturity : "raw"])
|
|
149
|
+
};
|
|
150
|
+
buckets.set(key, bucket);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
bucket.recurrence += frequency;
|
|
154
|
+
bucket.entryCount += 1;
|
|
155
|
+
bucket.types.add(type);
|
|
156
|
+
bucket.maturity.add(typeof row.maturity === "string" ? row.maturity : "raw");
|
|
157
|
+
if (row.severity === "critical") {
|
|
158
|
+
bucket.severity = "critical";
|
|
159
|
+
} else if (row.severity === "important" && bucket.severity !== "critical") {
|
|
160
|
+
bucket.severity = "important";
|
|
161
|
+
}
|
|
162
|
+
if (lastSeen && Date.parse(lastSeen) > Date.parse(bucket.lastSeenTs || "0")) {
|
|
163
|
+
bucket.lastSeenTs = lastSeen;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const ready = [];
|
|
167
|
+
for (const bucket of buckets.values()) {
|
|
168
|
+
const criticalOverride = bucket.severity === "critical";
|
|
169
|
+
const meetsRecurrence = bucket.recurrence >= threshold;
|
|
170
|
+
if (!criticalOverride && !meetsRecurrence) continue;
|
|
171
|
+
ready.push({
|
|
172
|
+
trigger: bucket.trigger,
|
|
173
|
+
action: bucket.action,
|
|
174
|
+
recurrence: bucket.recurrence,
|
|
175
|
+
entryCount: bucket.entryCount,
|
|
176
|
+
qualification: criticalOverride && !meetsRecurrence ? "critical_override" : "recurrence",
|
|
177
|
+
...(bucket.severity ? { severity: bucket.severity } : {}),
|
|
178
|
+
lastSeenTs: bucket.lastSeenTs,
|
|
179
|
+
types: Array.from(bucket.types).sort(),
|
|
180
|
+
maturity: Array.from(bucket.maturity).sort()
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
ready.sort((a, b) => {
|
|
184
|
+
const sevDiff = severityWeight(b.severity) - severityWeight(a.severity);
|
|
185
|
+
if (sevDiff !== 0) return sevDiff;
|
|
186
|
+
if (b.recurrence !== a.recurrence) return b.recurrence - a.recurrence;
|
|
187
|
+
const recencyDiff = Date.parse(b.lastSeenTs || "0") - Date.parse(a.lastSeenTs || "0");
|
|
188
|
+
if (!Number.isNaN(recencyDiff) && recencyDiff !== 0) return recencyDiff;
|
|
189
|
+
return String(a.trigger).localeCompare(String(b.trigger));
|
|
190
|
+
});
|
|
191
|
+
return {
|
|
192
|
+
schemaVersion: 2,
|
|
193
|
+
threshold,
|
|
194
|
+
baseThreshold,
|
|
195
|
+
...(archivedRunsCount !== undefined ? { archivedRunsCount } : {}),
|
|
196
|
+
smallProjectRelaxationApplied,
|
|
197
|
+
clusterCount: buckets.size,
|
|
198
|
+
readyCount: ready.length,
|
|
199
|
+
ready: ready.slice(0, maxReady),
|
|
200
|
+
lastUpdatedAt: normalizeCompoundLastUpdatedAt(new Date())
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
/**
|
|
205
|
+
* Inline mirror of `src/tdd-cycle.ts::computeRalphLoopStatus`.
|
|
206
|
+
*
|
|
207
|
+
* Parity enforced by
|
|
208
|
+
* `tests/unit/ralph-loop-parity.test.ts::ralph-loop parity`.
|
|
209
|
+
*
|
|
210
|
+
* Signature contract:
|
|
211
|
+
* async function computeRalphLoopStatusInline(stateDir, runId) -> RalphLoopStatus
|
|
212
|
+
*/
|
|
213
|
+
export const RALPH_LOOP_INLINE_SOURCE = `
|
|
214
|
+
async function computeRalphLoopStatusInline(stateDir, runId) {
|
|
215
|
+
const filePath = path.join(stateDir, "tdd-cycle-log.jsonl");
|
|
216
|
+
const raw = await readTextFile(filePath, "");
|
|
217
|
+
const sliceMap = new Map();
|
|
218
|
+
const acClosed = new Set();
|
|
219
|
+
const redOpenSlices = [];
|
|
220
|
+
let loopIteration = 0;
|
|
221
|
+
for (const rawLine of raw.split(/\\r?\\n/gu)) {
|
|
222
|
+
const line = rawLine.trim();
|
|
223
|
+
if (line.length === 0) continue;
|
|
224
|
+
let row;
|
|
225
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
226
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
227
|
+
const rowRun = typeof row.runId === "string" && row.runId.length > 0 ? row.runId : runId;
|
|
228
|
+
if (rowRun !== runId) continue;
|
|
229
|
+
const slice = typeof row.slice === "string" && row.slice.length > 0 ? row.slice : "S-unknown";
|
|
230
|
+
let state = sliceMap.get(slice);
|
|
231
|
+
if (!state) {
|
|
232
|
+
state = { slice, redCount: 0, greenCount: 0, refactorCount: 0, redOpen: false, acIds: [] };
|
|
233
|
+
sliceMap.set(slice, state);
|
|
234
|
+
}
|
|
235
|
+
const exitCode = typeof row.exitCode === "number" ? row.exitCode : undefined;
|
|
236
|
+
if (row.phase === "red") {
|
|
237
|
+
state.redCount += 1;
|
|
238
|
+
if (exitCode !== undefined && exitCode !== 0) state.redOpen = true;
|
|
239
|
+
} else if (row.phase === "green") {
|
|
240
|
+
state.greenCount += 1;
|
|
241
|
+
state.redOpen = false;
|
|
242
|
+
loopIteration += 1;
|
|
243
|
+
if (Array.isArray(row.acIds)) {
|
|
244
|
+
for (const acId of row.acIds) {
|
|
245
|
+
if (typeof acId !== "string" || acId.length === 0) continue;
|
|
246
|
+
acClosed.add(acId);
|
|
247
|
+
if (!state.acIds.includes(acId)) state.acIds.push(acId);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} else if (row.phase === "refactor") {
|
|
251
|
+
state.refactorCount += 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const state of sliceMap.values()) {
|
|
255
|
+
if (state.redOpen) redOpenSlices.push(state.slice);
|
|
256
|
+
}
|
|
257
|
+
const slices = Array.from(sliceMap.values()).sort((a, b) => a.slice.localeCompare(b.slice, "en"));
|
|
258
|
+
return {
|
|
259
|
+
schemaVersion: 1,
|
|
260
|
+
runId,
|
|
261
|
+
loopIteration,
|
|
262
|
+
redOpen: redOpenSlices.length > 0,
|
|
263
|
+
redOpenSlices,
|
|
264
|
+
acClosed: Array.from(acClosed).sort(),
|
|
265
|
+
sliceCount: slices.length,
|
|
266
|
+
slices,
|
|
267
|
+
lastUpdatedAt: new Date().toISOString()
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
`;
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
import { type IdeateFrameId } from "./ideate-frames.js";
|
|
2
|
+
export interface IdeateCommandOptions {
|
|
3
|
+
frameIds?: readonly IdeateFrameId[];
|
|
4
|
+
}
|
|
5
|
+
export declare function ideateCommandContract(options?: IdeateCommandOptions): string;
|
|
6
|
+
export declare function ideateCommandSkillMarkdown(options?: IdeateCommandOptions): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
2
|
+
import { resolveIdeateFrames } from "./ideate-frames.js";
|
|
2
3
|
const IDEATE_SKILL_FOLDER = "flow-ideate";
|
|
3
4
|
const IDEATE_SKILL_NAME = "flow-ideate";
|
|
4
5
|
/**
|
|
@@ -18,7 +19,20 @@ const STRUCTURED_ASK_TOOLS = "`AskUserQuestion` on Claude, `AskQuestion` on Curs
|
|
|
18
19
|
"`question` on OpenCode when `permission.question: \"allow\"` is set, " +
|
|
19
20
|
"`request_user_input` on Codex in Plan / Collaboration mode; " +
|
|
20
21
|
"fall back to a plain-text lettered list when the tool is hidden or errors";
|
|
21
|
-
|
|
22
|
+
function renderFrameBullets(frameIds) {
|
|
23
|
+
return resolveIdeateFrames(frameIds)
|
|
24
|
+
.map((frame) => ` - ${frame.label} (\`${frame.id}\`)`)
|
|
25
|
+
.join("\n");
|
|
26
|
+
}
|
|
27
|
+
function renderFrameNames(frameIds) {
|
|
28
|
+
return resolveIdeateFrames(frameIds)
|
|
29
|
+
.map((frame) => frame.label)
|
|
30
|
+
.join(", ");
|
|
31
|
+
}
|
|
32
|
+
export function ideateCommandContract(options = {}) {
|
|
33
|
+
const frames = resolveIdeateFrames(options.frameIds);
|
|
34
|
+
const frameBullets = renderFrameBullets(options.frameIds);
|
|
35
|
+
const minimumDistinctFrames = Math.min(4, frames.length);
|
|
22
36
|
return `# /cc-ideate
|
|
23
37
|
|
|
24
38
|
## Purpose
|
|
@@ -51,17 +65,24 @@ same session, or save/discard the backlog.
|
|
|
51
65
|
repetition scan.
|
|
52
66
|
- Elsewhere-software: docs-first grounding (Context7 and official docs).
|
|
53
67
|
- Elsewhere-non-software: constraints and objective grounding.
|
|
54
|
-
4. **Divergent ideation frames (parallel).** Generate candidates with
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
4. **Divergent ideation frames (parallel).** Generate candidates with
|
|
69
|
+
configured frames (${frames.length} total):
|
|
70
|
+
${frameBullets}
|
|
71
|
+
Keep at least ${minimumDistinctFrames} distinct frame outputs in every run.
|
|
57
72
|
5. **Adversarial critique pass.** For each candidate, write the strongest
|
|
58
73
|
counter-argument, kill weak ideas, and keep survivors only.
|
|
59
74
|
6. **Produce 5-10 survivors** with impact (High/Medium/Low),
|
|
60
75
|
effort (S/M/L), confidence (High/Medium/Low), and one evidence path per
|
|
61
76
|
survivor.
|
|
62
|
-
7. **Rank by impact/effort
|
|
77
|
+
7. **Rank by impact/effort/confidence** using
|
|
78
|
+
\`(impact points / effort cost) * confidence multiplier\` and recommend
|
|
79
|
+
the top survivor.
|
|
63
80
|
8. **Write the artifact** at
|
|
64
81
|
\`${IDEATE_ARTIFACT_PATTERN}\` using the schema in the skill.
|
|
82
|
+
8.5 **Seed shelf (optional).** For critiqued-out or deferred ideas that still
|
|
83
|
+
show upside, write seed notes to
|
|
84
|
+
\`${RUNTIME_ROOT}/seeds/SEED-<YYYY-MM-DD>-<slug>.md\` with
|
|
85
|
+
\`trigger_when\`, hypothesis, and suggested action.
|
|
65
86
|
9. **Present the handoff prompt** with four concrete options — not A/B/C
|
|
66
87
|
letters. Default = "Start /cc on the top recommendation".
|
|
67
88
|
|
|
@@ -78,10 +99,14 @@ Validate envelopes with:
|
|
|
78
99
|
|
|
79
100
|
## Primary skill
|
|
80
101
|
|
|
81
|
-
**${RUNTIME_ROOT}/skills/${IDEATE_SKILL_FOLDER}/SKILL.md**
|
|
102
|
+
**${RUNTIME_ROOT}/skills/${IDEATE_SKILL_FOLDER}/SKILL.md**
|
|
82
103
|
`;
|
|
83
104
|
}
|
|
84
|
-
export function ideateCommandSkillMarkdown() {
|
|
105
|
+
export function ideateCommandSkillMarkdown(options = {}) {
|
|
106
|
+
const frames = resolveIdeateFrames(options.frameIds);
|
|
107
|
+
const frameBullets = renderFrameBullets(options.frameIds);
|
|
108
|
+
const minimumDistinctFrames = Math.min(4, frames.length);
|
|
109
|
+
const frameNames = renderFrameNames(options.frameIds);
|
|
85
110
|
return `---
|
|
86
111
|
name: ${IDEATE_SKILL_NAME}
|
|
87
112
|
description: "Repository ideate mode: detect and rank high-leverage improvements, persist a backlog artifact, and hand off to /cc or save/discard."
|
|
@@ -150,14 +175,9 @@ Record each finding with exact evidence (path, command, or doc source).
|
|
|
150
175
|
|
|
151
176
|
Generate candidate ideas by frame, in parallel when possible:
|
|
152
177
|
|
|
153
|
-
|
|
154
|
-
- inversion
|
|
155
|
-
- assumption-break
|
|
156
|
-
- leverage
|
|
157
|
-
- cross-domain analogy
|
|
158
|
-
- constraint-flip
|
|
178
|
+
${frameBullets}
|
|
159
179
|
|
|
160
|
-
Require at least
|
|
180
|
+
Require at least ${minimumDistinctFrames} distinct frames in every run. Avoid frame-collapse
|
|
161
181
|
(same idea rewritten 6 times). Keep raw outputs for auditability.
|
|
162
182
|
|
|
163
183
|
### Phase 3 — Critique all, keep survivors
|
|
@@ -182,7 +202,8 @@ Only survivors advance to ranking.
|
|
|
182
202
|
- **Evidence** — path(s) or command output, inline if short
|
|
183
203
|
- **Counter-argument** — strongest concern that survived
|
|
184
204
|
- **Proposed handoff** — exact \`/cc <phrase>\`
|
|
185
|
-
3. Sort by impact/effort
|
|
205
|
+
3. Sort by score \`(impact points / effort cost) * confidence multiplier\`
|
|
206
|
+
and break ties with rationale strength.
|
|
186
207
|
4. Compute the artifact filename:
|
|
187
208
|
- \`slug\` = first 3–5 words of the top recommendation, lowercase,
|
|
188
209
|
non-alphanumeric collapsed to \`-\`, trimmed. When ideate mode is
|
|
@@ -231,7 +252,11 @@ Only survivors advance to ranking.
|
|
|
231
252
|
### I-2 — …
|
|
232
253
|
\`\`\`
|
|
233
254
|
|
|
234
|
-
6.
|
|
255
|
+
6. Optional: for promising non-selected ideas, write
|
|
256
|
+
\`${RUNTIME_ROOT}/seeds/SEED-<YYYY-MM-DD>-<slug>.md\` entries with:
|
|
257
|
+
\`title\`, \`trigger_when\`, \`hypothesis\`, \`action\`, and
|
|
258
|
+
\`source_artifact\` = ideate artifact path.
|
|
259
|
+
7. Confirm in chat: "Wrote <path>."
|
|
235
260
|
|
|
236
261
|
### Phase 5 — Handoff prompt
|
|
237
262
|
|
|
@@ -270,5 +295,7 @@ lettered list with the same four labels. Do not invent extra options.
|
|
|
270
295
|
- Do not mutate \`.cclaw/state/flow-state.json\` at any phase.
|
|
271
296
|
- Do not end the turn with an ungrounded "pick one" question — every
|
|
272
297
|
option in the handoff prompt must reference a concrete command.
|
|
298
|
+
- Do not collapse all ideas into one frame; distribute across:
|
|
299
|
+
${frameNames}.
|
|
273
300
|
`;
|
|
274
301
|
}
|