cclaw-cli 0.46.14 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -11
- package/dist/artifact-linter.d.ts +2 -0
- package/dist/artifact-linter.js +137 -9
- package/dist/config.d.ts +12 -7
- package/dist/config.js +79 -10
- package/dist/content/compound-command.d.ts +5 -2
- package/dist/content/compound-command.js +47 -16
- package/dist/content/contracts.js +1 -1
- package/dist/content/examples.d.ts +1 -0
- package/dist/content/examples.js +13 -0
- package/dist/content/harnesses-doc.js +11 -0
- package/dist/content/learnings.d.ts +2 -1
- package/dist/content/learnings.js +5 -3
- package/dist/content/observe.d.ts +2 -1
- package/dist/content/observe.js +174 -14
- package/dist/content/stage-schema.js +21 -8
- package/dist/content/stages/design.js +0 -2
- package/dist/content/utility-skills.js +1 -1
- package/dist/delegation.js +19 -4
- package/dist/gate-evidence.js +6 -1
- package/dist/install.d.ts +3 -3
- package/dist/install.js +13 -8
- package/dist/internal/advance-stage.js +53 -16
- package/dist/knowledge-store.d.ts +3 -0
- package/dist/knowledge-store.js +11 -1
- package/dist/retro-gate.js +11 -1
- package/dist/run-persistence.js +11 -1
- package/dist/tdd-cycle.js +19 -1
- package/dist/types.d.ts +30 -2
- package/package.json +1 -1
package/dist/content/examples.js
CHANGED
|
@@ -713,6 +713,14 @@ const DOMAIN_LABELS = {
|
|
|
713
713
|
library: "Library / SDK",
|
|
714
714
|
"data-pipeline": "Data pipeline / ETL"
|
|
715
715
|
};
|
|
716
|
+
export const RESEARCH_FLEET_USAGE_EXAMPLE = [
|
|
717
|
+
"Before drafting `03-design.md`, run `research/research-fleet.md` once and",
|
|
718
|
+
"capture all four lenses in `.cclaw/artifacts/02a-research.md`.",
|
|
719
|
+
"Dispatch semantics by harness: Claude/Cursor = parallel subagents in one turn;",
|
|
720
|
+
"OpenCode/Codex = sequential role-switch with explicit announcements.",
|
|
721
|
+
"Design must include a `Research Fleet Synthesis` section that maps each",
|
|
722
|
+
"lens to concrete architecture decisions and risks."
|
|
723
|
+
].join(" ");
|
|
716
724
|
const STAGE_DOMAIN_SAMPLES = {
|
|
717
725
|
brainstorm: [
|
|
718
726
|
{
|
|
@@ -759,6 +767,11 @@ const STAGE_DOMAIN_SAMPLES = {
|
|
|
759
767
|
}
|
|
760
768
|
],
|
|
761
769
|
design: [
|
|
770
|
+
{
|
|
771
|
+
domain: "web",
|
|
772
|
+
label: "Parallel research fleet handoff",
|
|
773
|
+
body: RESEARCH_FLEET_USAGE_EXAMPLE
|
|
774
|
+
},
|
|
762
775
|
{
|
|
763
776
|
domain: "web",
|
|
764
777
|
label: "Architecture note",
|
|
@@ -58,6 +58,17 @@ Fallback legend:
|
|
|
58
58
|
- \`role-switch\` — in-session role announce + delegation-log entry with evidenceRefs (OpenCode, Codex).
|
|
59
59
|
- \`waiver\` — no parity path; reserved for harnesses that cannot role-switch (none shipped).
|
|
60
60
|
|
|
61
|
+
## Parallel research dispatch semantics
|
|
62
|
+
|
|
63
|
+
Design-stage research fleet uses the same parity model:
|
|
64
|
+
|
|
65
|
+
- **Claude / Cursor**: dispatch all four research lenses in one turn
|
|
66
|
+
(stack, features, architecture, pitfalls) and synthesize into
|
|
67
|
+
\`.cclaw/artifacts/02a-research.md\`.
|
|
68
|
+
- **OpenCode / Codex**: execute the same four lenses via sequential
|
|
69
|
+
role-switch, each with explicit announce -> execute -> evidence trail.
|
|
70
|
+
This preserves auditability when native parallel dispatch is unavailable.
|
|
71
|
+
|
|
61
72
|
## Semantic hook event coverage
|
|
62
73
|
|
|
63
74
|
| Event | Claude | Cursor | OpenCode | Codex |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Canonical required JSONL field order (matches strict validator keys).
|
|
3
|
-
* Optional keys (for now: `source`) may be appended after these
|
|
3
|
+
* Optional keys (for now: `source`, `severity`) may be appended after these
|
|
4
|
+
* required fields.
|
|
4
5
|
* Exported for tests and any programmatic writer that wants a stable base shape.
|
|
5
6
|
*/
|
|
6
7
|
export declare const KNOWLEDGE_JSONL_FIELDS: readonly ["type", "trigger", "action", "confidence", "domain", "stage", "origin_stage", "origin_feature", "frequency", "universality", "maturity", "created", "first_seen_ts", "last_seen_ts", "project"];
|
|
@@ -11,7 +11,8 @@ const LEARN_SKILL_NAME = "learnings";
|
|
|
11
11
|
const LEARN_SKILL_DESCRIPTION = "Project-scoped knowledge store: append and query rule/pattern/lesson/compound entries in the canonical JSONL file at .cclaw/knowledge.jsonl. Strict schema, append-only, machine-queryable.";
|
|
12
12
|
/**
|
|
13
13
|
* Canonical required JSONL field order (matches strict validator keys).
|
|
14
|
-
* Optional keys (for now: `source`) may be appended after these
|
|
14
|
+
* Optional keys (for now: `source`, `severity`) may be appended after these
|
|
15
|
+
* required fields.
|
|
15
16
|
* Exported for tests and any programmatic writer that wants a stable base shape.
|
|
16
17
|
*/
|
|
17
18
|
export const KNOWLEDGE_JSONL_FIELDS = [
|
|
@@ -74,7 +75,7 @@ Do not invent alternate stores (no markdown mirror, no SQLite, no per-stage file
|
|
|
74
75
|
|
|
75
76
|
Exactly one JSON object per line. Required fields must appear in the order:
|
|
76
77
|
\`type, trigger, action, confidence, domain, stage, origin_stage, origin_feature, frequency, universality, maturity, created, first_seen_ts, last_seen_ts, project\`.
|
|
77
|
-
Optional
|
|
78
|
+
Optional fields \`source\` and \`severity\` may be appended after \`project\`.
|
|
78
79
|
|
|
79
80
|
\`\`\`json
|
|
80
81
|
{"type":"pattern","trigger":"when reviewing external payloads","action":"parse through zod before touching service layer","confidence":"high","domain":"api","stage":"review","origin_stage":"review","origin_feature":"payload-hardening","frequency":1,"universality":"project","maturity":"raw","created":"2026-04-14T12:00:00Z","first_seen_ts":"2026-04-14T12:00:00Z","last_seen_ts":"2026-04-14T12:00:00Z","project":"cclaw"}
|
|
@@ -98,6 +99,7 @@ Optional field \`source\` may be appended after \`project\`.
|
|
|
98
99
|
| \`last_seen_ts\` | ISO 8601 UTC string | yes | Last re-confirmed timestamp. |
|
|
99
100
|
| \`project\` | string \\| null | yes | Repo or scope name. Use \`null\` when the entry crosses projects. |
|
|
100
101
|
| \`source\` | \`"stage" \\| "retro" \\| "compound" \\| "ideate" \\| "manual" \\| null\` | no | Origin channel for the entry when known. |
|
|
102
|
+
| \`severity\` | \`"critical" \\| "important" \\| "suggestion"\` | no | Priority signal for compound lifts; \`critical\` enables single-hit override in \`/cc-ops compound\`. |
|
|
101
103
|
|
|
102
104
|
Rules:
|
|
103
105
|
- No other fields beyond the table above. Extra keys are forbidden and MUST be rejected by any writer.
|
|
@@ -170,7 +172,7 @@ Do not edit source code from this command. Only operate on \`${KNOWLEDGE_PATH}\`
|
|
|
170
172
|
|---|---|---|
|
|
171
173
|
| (default) | — | Show recent knowledge entries (tail of JSONL, pretty-printed). |
|
|
172
174
|
| \`search\` | \`<query>\` | Stream-filter the JSONL for matching \`trigger\`, \`action\`, \`domain\`, \`project\`. |
|
|
173
|
-
| \`add\` | — | Append one JSON line (\`rule\` / \`pattern\` / \`lesson\` / \`compound\`) with the strict JSONL schema (15 required fields + optional \`source\`). |
|
|
175
|
+
| \`add\` | — | Append one JSON line (\`rule\` / \`pattern\` / \`lesson\` / \`compound\`) with the strict JSONL schema (15 required fields + optional \`source\` / \`severity\`). |
|
|
174
176
|
| \`curate\` | — | Hand off to the **knowledge-curation** skill: read-only audit + soft-archive plan when the file exceeds the curation threshold. |
|
|
175
177
|
`;
|
|
176
178
|
}
|
|
@@ -12,7 +12,8 @@ export declare function promptGuardScript(options?: PromptGuardOptions): string;
|
|
|
12
12
|
export interface WorkflowGuardOptions {
|
|
13
13
|
workflowGuardMode?: "advisory" | "strict";
|
|
14
14
|
tddEnforcementMode?: "advisory" | "strict";
|
|
15
|
-
|
|
15
|
+
tddTestPathPatterns?: string[];
|
|
16
|
+
tddProductionPathPatterns?: string[];
|
|
16
17
|
}
|
|
17
18
|
export declare function workflowGuardScript(options?: WorkflowGuardOptions): string;
|
|
18
19
|
export declare function contextMonitorScript(): string;
|
package/dist/content/observe.js
CHANGED
|
@@ -156,9 +156,12 @@ exit 0
|
|
|
156
156
|
export function workflowGuardScript(options = {}) {
|
|
157
157
|
const workflowGuardMode = options.workflowGuardMode === "strict" ? "strict" : "advisory";
|
|
158
158
|
const tddEnforcementMode = options.tddEnforcementMode === "strict" ? "strict" : "advisory";
|
|
159
|
-
const
|
|
160
|
-
? options.
|
|
161
|
-
: "**/*.test
|
|
159
|
+
const tddTestPathPatterns = options.tddTestPathPatterns && options.tddTestPathPatterns.length > 0
|
|
160
|
+
? options.tddTestPathPatterns.join(",")
|
|
161
|
+
: "**/*.test.*,**/tests/**,**/__tests__/**";
|
|
162
|
+
const tddProductionPathPatterns = options.tddProductionPathPatterns && options.tddProductionPathPatterns.length > 0
|
|
163
|
+
? options.tddProductionPathPatterns.join(",")
|
|
164
|
+
: "";
|
|
162
165
|
return `#!/usr/bin/env bash
|
|
163
166
|
# cclaw workflow guard hook — generated by cclaw sync
|
|
164
167
|
# Enforces stage-aware command discipline and recent flow-state read hygiene.
|
|
@@ -166,7 +169,8 @@ set -uo pipefail
|
|
|
166
169
|
WORKFLOW_GUARD_MODE="\${CCLAW_WORKFLOW_GUARD_MODE:-${workflowGuardMode}}"
|
|
167
170
|
MAX_FLOW_READ_AGE_SEC="\${CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC:-1800}"
|
|
168
171
|
TDD_ENFORCEMENT_MODE="${tddEnforcementMode}"
|
|
169
|
-
|
|
172
|
+
TDD_TEST_PATH_PATTERNS="${tddTestPathPatterns}"
|
|
173
|
+
TDD_PRODUCTION_PATH_PATTERNS="${tddProductionPathPatterns}"
|
|
170
174
|
|
|
171
175
|
${RUNTIME_SHELL_DETECT_ROOT}
|
|
172
176
|
|
|
@@ -425,34 +429,153 @@ is_preimplementation_stage() {
|
|
|
425
429
|
esac
|
|
426
430
|
}
|
|
427
431
|
|
|
432
|
+
normalize_payload_path() {
|
|
433
|
+
local raw="$1"
|
|
434
|
+
local normalized="$raw"
|
|
435
|
+
normalized=$(printf '%s' "$normalized" | tr '\\\\' '/')
|
|
436
|
+
normalized=$(printf '%s' "$normalized" | tr '[:upper:]' '[:lower:]')
|
|
437
|
+
normalized="\${normalized#./}"
|
|
438
|
+
printf '%s' "$normalized"
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
extract_payload_paths() {
|
|
442
|
+
if command -v jq >/dev/null 2>&1; then
|
|
443
|
+
printf '%s' "$INPUT" | jq -r '
|
|
444
|
+
[.. | objects | (.path?, .file_path?, .filepath?) | select(type == "string" and length > 0)]
|
|
445
|
+
| unique
|
|
446
|
+
| .[]
|
|
447
|
+
' 2>/dev/null || printf ''
|
|
448
|
+
return 0
|
|
449
|
+
fi
|
|
450
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
451
|
+
INPUT_JSON="$INPUT" python3 - <<'PY'
|
|
452
|
+
import json
|
|
453
|
+
import os
|
|
454
|
+
|
|
455
|
+
def visit(node, acc):
|
|
456
|
+
if isinstance(node, dict):
|
|
457
|
+
for key in ("path", "file_path", "filepath"):
|
|
458
|
+
value = node.get(key)
|
|
459
|
+
if isinstance(value, str) and value.strip():
|
|
460
|
+
acc.add(value.strip())
|
|
461
|
+
for value in node.values():
|
|
462
|
+
visit(value, acc)
|
|
463
|
+
elif isinstance(node, list):
|
|
464
|
+
for value in node:
|
|
465
|
+
visit(value, acc)
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
payload = json.loads(os.environ.get("INPUT_JSON", "{}"))
|
|
469
|
+
except Exception:
|
|
470
|
+
payload = {}
|
|
471
|
+
|
|
472
|
+
items = set()
|
|
473
|
+
visit(payload, items)
|
|
474
|
+
for value in sorted(items):
|
|
475
|
+
print(value)
|
|
476
|
+
PY
|
|
477
|
+
return 0
|
|
478
|
+
fi
|
|
479
|
+
printf ''
|
|
480
|
+
return 0
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
matches_path_patterns() {
|
|
484
|
+
local candidate="$1"
|
|
485
|
+
local patterns_csv="$2"
|
|
486
|
+
[ -n "$candidate" ] || return 1
|
|
487
|
+
[ -n "$patterns_csv" ] || return 1
|
|
488
|
+
local old_ifs="$IFS"
|
|
489
|
+
IFS=','
|
|
490
|
+
for pattern in $patterns_csv; do
|
|
491
|
+
local normalized_pattern
|
|
492
|
+
normalized_pattern=$(normalize_payload_path "$pattern")
|
|
493
|
+
[ -n "$normalized_pattern" ] || continue
|
|
494
|
+
case "$candidate" in
|
|
495
|
+
$normalized_pattern)
|
|
496
|
+
IFS="$old_ifs"
|
|
497
|
+
return 0
|
|
498
|
+
;;
|
|
499
|
+
esac
|
|
500
|
+
done
|
|
501
|
+
IFS="$old_ifs"
|
|
502
|
+
return 1
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
is_code_like_path() {
|
|
506
|
+
local candidate="$1"
|
|
507
|
+
printf '%s' "$candidate" | grep -Eq '\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)$'
|
|
508
|
+
}
|
|
509
|
+
|
|
428
510
|
is_tdd_test_payload() {
|
|
429
511
|
local text="$1"
|
|
430
|
-
|
|
431
|
-
|
|
512
|
+
local payload_paths="$2"
|
|
513
|
+
if [ -n "$payload_paths" ]; then
|
|
514
|
+
while IFS= read -r raw_path; do
|
|
515
|
+
[ -n "$raw_path" ] || continue
|
|
516
|
+
local normalized
|
|
517
|
+
normalized=$(normalize_payload_path "$raw_path")
|
|
518
|
+
if matches_path_patterns "$normalized" "$TDD_TEST_PATH_PATTERNS"; then
|
|
519
|
+
return 0
|
|
520
|
+
fi
|
|
521
|
+
done <<< "$payload_paths"
|
|
432
522
|
fi
|
|
433
|
-
if printf '%s' "$
|
|
523
|
+
if printf '%s' "$text" | grep -Eq '/tests?/|/__tests__/|\\.test\\.'; then
|
|
434
524
|
return 0
|
|
435
525
|
fi
|
|
436
526
|
return 1
|
|
437
527
|
}
|
|
438
528
|
|
|
439
|
-
|
|
529
|
+
is_tdd_production_path() {
|
|
530
|
+
local normalized="$1"
|
|
531
|
+
[ -n "$normalized" ] || return 1
|
|
532
|
+
if printf '%s' "$normalized" | grep -Eq '(^|/)\\.cclaw/'; then
|
|
533
|
+
return 1
|
|
534
|
+
fi
|
|
535
|
+
if matches_path_patterns "$normalized" "$TDD_TEST_PATH_PATTERNS"; then
|
|
536
|
+
return 1
|
|
537
|
+
fi
|
|
538
|
+
if [ -n "$TDD_PRODUCTION_PATH_PATTERNS" ]; then
|
|
539
|
+
matches_path_patterns "$normalized" "$TDD_PRODUCTION_PATH_PATTERNS"
|
|
540
|
+
return $?
|
|
541
|
+
fi
|
|
542
|
+
is_code_like_path "$normalized"
|
|
543
|
+
return $?
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
is_tdd_production_write_payload() {
|
|
440
547
|
local text="$1"
|
|
548
|
+
local payload_paths="$2"
|
|
549
|
+
if [ -n "$payload_paths" ]; then
|
|
550
|
+
while IFS= read -r raw_path; do
|
|
551
|
+
[ -n "$raw_path" ] || continue
|
|
552
|
+
local normalized
|
|
553
|
+
normalized=$(normalize_payload_path "$raw_path")
|
|
554
|
+
if is_tdd_production_path "$normalized"; then
|
|
555
|
+
return 0
|
|
556
|
+
fi
|
|
557
|
+
done <<< "$payload_paths"
|
|
558
|
+
return 1
|
|
559
|
+
fi
|
|
560
|
+
if [ -n "$TDD_PRODUCTION_PATH_PATTERNS" ]; then
|
|
561
|
+
return 1
|
|
562
|
+
fi
|
|
441
563
|
if printf '%s' "$text" | grep -Eq '\\.cclaw/'; then
|
|
442
564
|
return 1
|
|
443
565
|
fi
|
|
444
566
|
if ! printf '%s' "$text" | grep -Eq '\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)'; then
|
|
445
567
|
return 1
|
|
446
568
|
fi
|
|
447
|
-
if is_tdd_test_payload "$text"; then
|
|
569
|
+
if is_tdd_test_payload "$text" ""; then
|
|
448
570
|
return 1
|
|
449
571
|
fi
|
|
450
572
|
return 0
|
|
451
573
|
}
|
|
452
574
|
|
|
453
|
-
|
|
575
|
+
tdd_cycle_counts() {
|
|
454
576
|
if [ ! -f "$TDD_LOG_FILE" ] || [ ! -s "$TDD_LOG_FILE" ]; then
|
|
455
|
-
|
|
577
|
+
printf '0:0'
|
|
578
|
+
return 0
|
|
456
579
|
fi
|
|
457
580
|
local red_count="0"
|
|
458
581
|
local green_count="0"
|
|
@@ -512,12 +635,37 @@ PY
|
|
|
512
635
|
fi
|
|
513
636
|
[ -n "$red_count" ] || red_count="0"
|
|
514
637
|
[ -n "$green_count" ] || green_count="0"
|
|
638
|
+
printf '%s:%s' "$red_count" "$green_count"
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
has_open_red_cycle() {
|
|
642
|
+
local counts
|
|
643
|
+
counts=$(tdd_cycle_counts)
|
|
644
|
+
local red_count="\${counts%%:*}"
|
|
645
|
+
local green_count="\${counts##*:}"
|
|
515
646
|
if [ "$red_count" -gt "$green_count" ]; then
|
|
516
647
|
return 0
|
|
517
648
|
fi
|
|
518
649
|
return 1
|
|
519
650
|
}
|
|
520
651
|
|
|
652
|
+
tdd_cycle_state() {
|
|
653
|
+
local counts
|
|
654
|
+
counts=$(tdd_cycle_counts)
|
|
655
|
+
local red_count="\${counts%%:*}"
|
|
656
|
+
local green_count="\${counts##*:}"
|
|
657
|
+
if [ "$red_count" -le 0 ]; then
|
|
658
|
+
printf 'need_red'
|
|
659
|
+
return 0
|
|
660
|
+
fi
|
|
661
|
+
if [ "$red_count" -gt "$green_count" ]; then
|
|
662
|
+
printf 'red_open'
|
|
663
|
+
return 0
|
|
664
|
+
fi
|
|
665
|
+
printf 'green_done'
|
|
666
|
+
return 0
|
|
667
|
+
}
|
|
668
|
+
|
|
521
669
|
detect_target_stage() {
|
|
522
670
|
local text="$1"
|
|
523
671
|
for stage in brainstorm scope design spec plan tdd review ship; do
|
|
@@ -546,6 +694,11 @@ FLOW_COMMAND_INVOKED=0
|
|
|
546
694
|
if is_flow_progression_command "$PAYLOAD_LOWER"; then
|
|
547
695
|
FLOW_COMMAND_INVOKED=1
|
|
548
696
|
fi
|
|
697
|
+
MUTATION_PATHS=""
|
|
698
|
+
if is_mutating_tool "$TOOL_LOWER"; then
|
|
699
|
+
MUTATION_PATHS=$(extract_payload_paths)
|
|
700
|
+
fi
|
|
701
|
+
TDD_CYCLE_STATE="unknown"
|
|
549
702
|
if [ -n "$TARGET_STAGE" ] && [ "$CURRENT_STAGE" != "none" ]; then
|
|
550
703
|
CURRENT_IDX=$(stage_index "$CURRENT_STAGE")
|
|
551
704
|
TARGET_IDX=$(stage_index "$TARGET_STAGE")
|
|
@@ -583,8 +736,13 @@ if is_preimplementation_stage "$CURRENT_STAGE" && is_mutating_tool "$TOOL_LOWER"
|
|
|
583
736
|
fi
|
|
584
737
|
|
|
585
738
|
if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
|
|
586
|
-
if
|
|
587
|
-
if
|
|
739
|
+
if is_tdd_production_write_payload "$PAYLOAD_LOWER" "$MUTATION_PATHS"; then
|
|
740
|
+
if has_open_red_cycle; then
|
|
741
|
+
TDD_CYCLE_STATE="red_open"
|
|
742
|
+
else
|
|
743
|
+
TDD_CYCLE_STATE=$(tdd_cycle_state)
|
|
744
|
+
fi
|
|
745
|
+
if [ "$TDD_CYCLE_STATE" = "need_red" ]; then
|
|
588
746
|
if [ -n "$REASONS" ]; then
|
|
589
747
|
REASONS="$REASONS,tdd_write_without_open_red"
|
|
590
748
|
else
|
|
@@ -659,7 +817,9 @@ PY
|
|
|
659
817
|
fi
|
|
660
818
|
|
|
661
819
|
if [ -n "$REASONS" ]; then
|
|
662
|
-
if printf '%s' "$REASONS" | grep -Eq '
|
|
820
|
+
if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red'; then
|
|
821
|
+
NOTE="Cclaw workflow guard: Write a failing test first before editing production files during tdd stage (state=\${TDD_CYCLE_STATE})."
|
|
822
|
+
elif printf '%s' "$REASONS" | grep -Eq 'direct_flow_state_edit'; then
|
|
663
823
|
NOTE="Cclaw workflow guard: direct flow-state edit bypasses the canonical stage-complete helper (\${REASONS}). Prefer: bash ${RUNTIME_ROOT}/hooks/stage-complete.sh <stage>. In strict mode this is blocked."
|
|
664
824
|
else
|
|
665
825
|
NOTE="Cclaw workflow guard: detected potential flow violation (\${REASONS}). Re-read ${RUNTIME_ROOT}/state/flow-state.json, avoid source edits before tdd stage, and enforce RED -> GREEN -> REFACTOR discipline inside tdd."
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { COMMAND_FILE_ORDER } from "../constants.js";
|
|
2
|
+
import { FLOW_TRACKS, TRACK_STAGES } from "../types.js";
|
|
2
3
|
import { BRAINSTORM, SCOPE, DESIGN, SPEC, PLAN, TDD, REVIEW, SHIP } from "./stages/index.js";
|
|
3
4
|
import { tddStageForTrack } from "./stages/tdd.js";
|
|
4
5
|
const REQUIRED_GATE_IDS = {
|
|
@@ -266,15 +267,27 @@ export function nextCclawCommand(stage) {
|
|
|
266
267
|
}
|
|
267
268
|
export function buildTransitionRules() {
|
|
268
269
|
const rules = [];
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
const seen = new Set();
|
|
271
|
+
// Derive transitions from every track so medium/quick (which skip stages)
|
|
272
|
+
// get their neighbour edges registered alongside the standard chain.
|
|
273
|
+
// Previously only the standard track produced rules, so `canTransition`
|
|
274
|
+
// returned false for legitimate medium/quick transitions (e.g. brainstorm
|
|
275
|
+
// -> spec on medium) even though `nextStage` correctly advanced them.
|
|
276
|
+
for (const track of FLOW_TRACKS) {
|
|
277
|
+
const ordered = TRACK_STAGES[track];
|
|
278
|
+
for (let i = 0; i < ordered.length - 1; i += 1) {
|
|
279
|
+
const from = ordered[i];
|
|
280
|
+
const to = ordered[i + 1];
|
|
281
|
+
const key = `${from}->${to}`;
|
|
282
|
+
if (seen.has(key))
|
|
283
|
+
continue;
|
|
284
|
+
seen.add(key);
|
|
285
|
+
rules.push({
|
|
286
|
+
from,
|
|
287
|
+
to,
|
|
288
|
+
guards: stageGateIds(from, track)
|
|
289
|
+
});
|
|
272
290
|
}
|
|
273
|
-
rules.push({
|
|
274
|
-
from: schema.stage,
|
|
275
|
-
to: schema.next,
|
|
276
|
-
guards: stageGateIds(schema.stage)
|
|
277
|
-
});
|
|
278
291
|
}
|
|
279
292
|
// Review can explicitly route back to TDD when the verdict is BLOCKED.
|
|
280
293
|
rules.push({
|
|
@@ -118,9 +118,7 @@ export const DESIGN = {
|
|
|
118
118
|
"No performance budget for critical path",
|
|
119
119
|
"Batching multiple design issues into one question",
|
|
120
120
|
"Agreeing with user's architecture choice without evaluating alternatives",
|
|
121
|
-
"Hedging every recommendation with 'it depends' instead of taking a position",
|
|
122
121
|
"No NOT-in-scope output section",
|
|
123
|
-
"No What-already-exists output section",
|
|
124
122
|
"Design decisions made without reading the actual code first"
|
|
125
123
|
],
|
|
126
124
|
policyNeedles: [
|
|
@@ -1469,7 +1469,7 @@ For each lens, write either a knowledge entry **or** the explicit string
|
|
|
1469
1469
|
## Output protocol
|
|
1470
1470
|
|
|
1471
1471
|
For every harvested insight, append one strict-schema JSON line to
|
|
1472
|
-
\`.cclaw/knowledge.jsonl\` (fields: \`type, trigger, action, confidence, domain, stage, origin_stage, origin_feature, frequency, universality, maturity, created, first_seen_ts, last_seen_ts, project\`).
|
|
1472
|
+
\`.cclaw/knowledge.jsonl\` (fields: \`type, trigger, action, confidence, domain, stage, origin_stage, origin_feature, frequency, universality, maturity, created, first_seen_ts, last_seen_ts, project\`; optional: \`source\`, \`severity\`).
|
|
1473
1473
|
See the \`learnings\` skill for the canonical shape. Choose \`type\`:
|
|
1474
1474
|
|
|
1475
1475
|
- \`compound\` for process/speed accelerators.
|
package/dist/delegation.js
CHANGED
|
@@ -126,6 +126,13 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
126
126
|
if (!Array.isArray(stamped.evidenceRefs)) {
|
|
127
127
|
stamped.evidenceRefs = [];
|
|
128
128
|
}
|
|
129
|
+
// Idempotency: if a caller (or a retried hook) tries to append a row
|
|
130
|
+
// with a spanId that already exists in the ledger, treat it as a no-op
|
|
131
|
+
// instead of growing the log with duplicate entries that subsequent
|
|
132
|
+
// delegation checks would mis-count.
|
|
133
|
+
if (prior.entries.some((existing) => existing.spanId === stamped.spanId)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
129
136
|
const ledger = {
|
|
130
137
|
runId: activeRunId,
|
|
131
138
|
entries: [...prior.entries, stamped]
|
|
@@ -201,11 +208,19 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
|
|
|
201
208
|
if (hasWaived) {
|
|
202
209
|
waived.push(agent);
|
|
203
210
|
}
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
211
|
+
// Evidence gating for `completed` rows has two triggers:
|
|
212
|
+
// 1. The aggregate expected mode is role-switch (no isolated harness
|
|
213
|
+
// available), so every completion implicitly ran as role-switch.
|
|
214
|
+
// 2. Any completed row is explicitly stamped `fulfillmentMode:
|
|
215
|
+
// "role-switch"` — even in a mixed install. This closes the loop
|
|
216
|
+
// where a Codex session logs a role-switch completion inside a
|
|
217
|
+
// claude+codex project: the aggregate expectedMode is "isolated"
|
|
218
|
+
// (claude wins), so the role-switch row would previously sail
|
|
219
|
+
// through without evidenceRefs.
|
|
220
|
+
const hasExplicitRoleSwitchRow = completedRows.some((e) => e.fulfillmentMode === "role-switch");
|
|
221
|
+
const evidenceRequired = expectedMode === "role-switch" || hasExplicitRoleSwitchRow;
|
|
207
222
|
if (hasCompleted &&
|
|
208
|
-
|
|
223
|
+
evidenceRequired &&
|
|
209
224
|
!completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
|
|
210
225
|
missingEvidence.push(agent);
|
|
211
226
|
}
|
package/dist/gate-evidence.js
CHANGED
|
@@ -266,7 +266,12 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
266
266
|
.map((line) => line.trim())
|
|
267
267
|
.filter((line) => line.length > 0)
|
|
268
268
|
.filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line));
|
|
269
|
-
|
|
269
|
+
// `<fill-in>` needs its own check because `\b` does not match
|
|
270
|
+
// around `<`/`>` (non-word characters), so the previous combined
|
|
271
|
+
// pattern `\b(?:...|<fill-in>)\b` silently never matched placeholder
|
|
272
|
+
// templates that used angle-bracket form.
|
|
273
|
+
const nonPlaceholder = meaningfulLines.filter((line) => !/\b(?:TODO|TBD|FIXME|pending)\b/iu.test(line) &&
|
|
274
|
+
!/<fill-in>/iu.test(line));
|
|
270
275
|
if (nonPlaceholder.length === 0) {
|
|
271
276
|
missingSections.push(`${section} (empty or placeholder)`);
|
|
272
277
|
}
|
package/dist/install.d.ts
CHANGED
|
@@ -12,9 +12,9 @@ export declare function syncCclaw(projectRoot: string): Promise<void>;
|
|
|
12
12
|
* stamps are rewritten so the on-disk config reflects the installed CLI.
|
|
13
13
|
*
|
|
14
14
|
* Shape preservation: if the user previously hand-authored advanced keys
|
|
15
|
-
* (e.g. `
|
|
16
|
-
* yaml. If their existing config is minimal, the upgrade keeps it
|
|
17
|
-
* advanced knobs are never silently added.
|
|
15
|
+
* (e.g. `tdd`, `compound`, `trackHeuristics`, `sliceReview`), those stay in
|
|
16
|
+
* the yaml. If their existing config is minimal, the upgrade keeps it
|
|
17
|
+
* minimal — advanced knobs are never silently added.
|
|
18
18
|
*/
|
|
19
19
|
export declare function upgradeCclaw(projectRoot: string): Promise<void>;
|
|
20
20
|
export declare function uninstallCclaw(projectRoot: string): Promise<void>;
|
package/dist/install.js
CHANGED
|
@@ -245,7 +245,9 @@ async function writeSkills(projectRoot, config) {
|
|
|
245
245
|
await writeFileSafe(runtimePath(projectRoot, "skills", "using-git-worktrees", "SKILL.md"), featureCommandSkillMarkdown());
|
|
246
246
|
await writeFileSafe(runtimePath(projectRoot, "skills", "tdd-cycle-log", "SKILL.md"), tddLogCommandSkillMarkdown());
|
|
247
247
|
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-retro", "SKILL.md"), retroCommandSkillMarkdown());
|
|
248
|
-
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-compound", "SKILL.md"), compoundCommandSkillMarkdown(
|
|
248
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-compound", "SKILL.md"), compoundCommandSkillMarkdown({
|
|
249
|
+
recurrenceThreshold: config?.compound?.recurrenceThreshold
|
|
250
|
+
}));
|
|
249
251
|
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-rewind", "SKILL.md"), rewindCommandSkillMarkdown());
|
|
250
252
|
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-archive", "SKILL.md"), archiveCommandSkillMarkdown());
|
|
251
253
|
await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
|
|
@@ -306,7 +308,7 @@ async function writeSkills(projectRoot, config) {
|
|
|
306
308
|
await writeFileSafe(runtimePath(projectRoot, ...playbookDirSegments, harnessPlaybookFileName(harness)), harnessPlaybookMarkdown(harness));
|
|
307
309
|
}
|
|
308
310
|
}
|
|
309
|
-
async function writeUtilityCommands(projectRoot) {
|
|
311
|
+
async function writeUtilityCommands(projectRoot, config) {
|
|
310
312
|
await writeFileSafe(runtimePath(projectRoot, "commands", "learn.md"), learnCommandContract());
|
|
311
313
|
await writeFileSafe(runtimePath(projectRoot, "commands", "next.md"), nextCommandContract());
|
|
312
314
|
await writeFileSafe(runtimePath(projectRoot, "commands", "ideate.md"), ideateCommandContract());
|
|
@@ -319,7 +321,9 @@ async function writeUtilityCommands(projectRoot) {
|
|
|
319
321
|
await writeFileSafe(runtimePath(projectRoot, "commands", "feature.md"), featureCommandContract());
|
|
320
322
|
await writeFileSafe(runtimePath(projectRoot, "commands", "tdd-log.md"), tddLogCommandContract());
|
|
321
323
|
await writeFileSafe(runtimePath(projectRoot, "commands", "retro.md"), retroCommandContract());
|
|
322
|
-
await writeFileSafe(runtimePath(projectRoot, "commands", "compound.md"), compoundCommandContract(
|
|
324
|
+
await writeFileSafe(runtimePath(projectRoot, "commands", "compound.md"), compoundCommandContract({
|
|
325
|
+
recurrenceThreshold: config.compound?.recurrenceThreshold
|
|
326
|
+
}));
|
|
323
327
|
await writeFileSafe(runtimePath(projectRoot, "commands", "archive.md"), archiveCommandContract());
|
|
324
328
|
await writeFileSafe(runtimePath(projectRoot, "commands", "rewind.md"), rewindCommandContract());
|
|
325
329
|
}
|
|
@@ -627,7 +631,8 @@ async function writeHooks(projectRoot, config) {
|
|
|
627
631
|
await writeFileSafe(path.join(hooksDir, "workflow-guard.sh"), workflowGuardScript({
|
|
628
632
|
workflowGuardMode: config.strictness ?? "advisory",
|
|
629
633
|
tddEnforcementMode: config.tddEnforcement ?? "advisory",
|
|
630
|
-
|
|
634
|
+
tddTestPathPatterns: config.tdd?.testPathPatterns ?? config.tddTestGlobs,
|
|
635
|
+
tddProductionPathPatterns: config.tdd?.productionPathPatterns
|
|
631
636
|
}));
|
|
632
637
|
await writeFileSafe(path.join(hooksDir, "context-monitor.sh"), contextMonitorScript());
|
|
633
638
|
const opencodePluginSource = opencodePluginJs();
|
|
@@ -1146,7 +1151,7 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
|
|
|
1146
1151
|
await cleanLegacyArtifacts(projectRoot);
|
|
1147
1152
|
await cleanStaleFiles(projectRoot);
|
|
1148
1153
|
await writeCommandContracts(projectRoot);
|
|
1149
|
-
await writeUtilityCommands(projectRoot);
|
|
1154
|
+
await writeUtilityCommands(projectRoot, config);
|
|
1150
1155
|
await writeSkills(projectRoot, config);
|
|
1151
1156
|
await writeContextModes(projectRoot);
|
|
1152
1157
|
await writeArtifactTemplates(projectRoot);
|
|
@@ -1195,9 +1200,9 @@ export async function syncCclaw(projectRoot) {
|
|
|
1195
1200
|
* stamps are rewritten so the on-disk config reflects the installed CLI.
|
|
1196
1201
|
*
|
|
1197
1202
|
* Shape preservation: if the user previously hand-authored advanced keys
|
|
1198
|
-
* (e.g. `
|
|
1199
|
-
* yaml. If their existing config is minimal, the upgrade keeps it
|
|
1200
|
-
* advanced knobs are never silently added.
|
|
1203
|
+
* (e.g. `tdd`, `compound`, `trackHeuristics`, `sliceReview`), those stay in
|
|
1204
|
+
* the yaml. If their existing config is minimal, the upgrade keeps it
|
|
1205
|
+
* minimal — advanced knobs are never silently added.
|
|
1201
1206
|
*/
|
|
1202
1207
|
export async function upgradeCclaw(projectRoot) {
|
|
1203
1208
|
const advancedKeysPresent = await detectAdvancedKeys(projectRoot);
|