cclaw-cli 0.48.2 → 0.48.3
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 +10 -3
- package/dist/cli.js +8 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +13 -3
- package/dist/content/contracts.d.ts +2 -2
- package/dist/content/contracts.js +2 -2
- package/dist/content/core-agents.d.ts +1 -1
- package/dist/content/core-agents.js +1 -1
- package/dist/content/hooks.js +16 -15
- package/dist/content/next-command.js +4 -2
- package/dist/content/observe.d.ts +2 -2
- package/dist/content/observe.js +83 -13
- package/dist/content/opencode-plugin.js +227 -45
- package/dist/content/stage-schema.js +1 -1
- package/dist/delegation.js +1 -1
- package/dist/doctor.js +35 -1
- package/dist/eval/runner.js +36 -4
- package/dist/feature-system.js +2 -2
- package/dist/fs-utils.d.ts +4 -1
- package/dist/fs-utils.js +9 -2
- package/dist/gate-evidence.js +1 -1
- package/dist/install.js +24 -22
- package/dist/internal/advance-stage.js +4 -2
- package/dist/knowledge-store.d.ts +8 -0
- package/dist/knowledge-store.js +113 -33
- package/dist/retro-gate.js +33 -23
- package/dist/run-archive.js +6 -9
- package/dist/run-persistence.js +1 -1
- package/dist/trace-matrix.js +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,8 +97,10 @@ scripted installs:
|
|
|
97
97
|
npx cclaw-cli init --harnesses=claude,cursor --no-interactive
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
That's the entire CLI interaction
|
|
101
|
-
inside your harness (Claude Code, Cursor,
|
|
100
|
+
That's the entire required CLI interaction for the normal workflow.
|
|
101
|
+
Everything day-to-day happens inside your harness (Claude Code, Cursor,
|
|
102
|
+
OpenCode, or Codex); optional maintenance commands are listed in the
|
|
103
|
+
CLI reference.
|
|
102
104
|
|
|
103
105
|
### What gets generated
|
|
104
106
|
|
|
@@ -140,9 +142,12 @@ Plus harness-specific shims:
|
|
|
140
142
|
`cclaw init` writes five keys, on purpose:
|
|
141
143
|
|
|
142
144
|
```yaml
|
|
143
|
-
version:
|
|
145
|
+
version: ${CCLAW_VERSION}
|
|
144
146
|
flowVersion: 1.0.0
|
|
145
147
|
harnesses:
|
|
148
|
+
- claude
|
|
149
|
+
- cursor
|
|
150
|
+
- opencode
|
|
146
151
|
- codex
|
|
147
152
|
strictness: advisory # advisory | strict — one knob for prompt-guard + TDD
|
|
148
153
|
gitHookGuards: false # opt in to managed .git/hooks/pre-commit + pre-push
|
|
@@ -471,7 +476,9 @@ your harness.
|
|
|
471
476
|
```bash
|
|
472
477
|
npx cclaw-cli # launches interactive setup (or prints
|
|
473
478
|
# a one-line status hint if already installed)
|
|
479
|
+
npx cclaw-cli sync # re-materialize generated runtime from config.yaml
|
|
474
480
|
npx cclaw-cli upgrade # refresh generated files; preserves .cclaw/config.yaml
|
|
481
|
+
npx cclaw-cli archive # archive current run and reset flow-state
|
|
475
482
|
npx cclaw-cli uninstall # remove .cclaw + generated harness shims
|
|
476
483
|
npx cclaw-cli eval … # maintainer surface (see docs/evals.md)
|
|
477
484
|
npx cclaw-cli --version
|
package/dist/cli.js
CHANGED
|
@@ -48,7 +48,12 @@ Commands:
|
|
|
48
48
|
init Bootstrap .cclaw runtime, state, and harness shims in this project.
|
|
49
49
|
Flags: --harnesses=<list> Comma list of harnesses (claude,cursor,opencode,codex).
|
|
50
50
|
--no-interactive Skip interactive prompts even on TTY (for CI/scripts).
|
|
51
|
+
sync Reconcile generated runtime files with the current config.
|
|
51
52
|
upgrade Refresh generated files in .cclaw. Preserves your config.yaml.
|
|
53
|
+
archive Archive the active run and reset flow state for next feature.
|
|
54
|
+
Flags: --name=<slug> Override archive folder suffix.
|
|
55
|
+
--skip-retro Skip retro gate only when runtime allows it.
|
|
56
|
+
--retro-reason=<txt> Required rationale with --skip-retro.
|
|
52
57
|
uninstall Remove .cclaw runtime and the generated harness shim files.
|
|
53
58
|
eval Run cclaw evals. Maintainer surface — see docs/evals.md.
|
|
54
59
|
Full flag reference: \`npx cclaw-cli eval --help\` or docs/evals.md.
|
|
@@ -60,6 +65,8 @@ Global flags:
|
|
|
60
65
|
Examples:
|
|
61
66
|
npx cclaw-cli
|
|
62
67
|
npx cclaw-cli init --harnesses=claude,cursor --no-interactive
|
|
68
|
+
npx cclaw-cli sync
|
|
69
|
+
npx cclaw-cli archive --name=my-feature
|
|
63
70
|
npx cclaw-cli upgrade
|
|
64
71
|
npx cclaw-cli eval --dry-run
|
|
65
72
|
|
|
@@ -1018,7 +1025,7 @@ async function runCommand(parsed, ctx) {
|
|
|
1018
1025
|
info(ctx, `Archived active artifacts to ${archived.archivePath}. Flow state reset to brainstorm.${snapshotSummary}`);
|
|
1019
1026
|
const k = archived.knowledge;
|
|
1020
1027
|
if (k.overThreshold) {
|
|
1021
|
-
info(ctx, `Knowledge curation recommended: ${k.knowledgePath} now has ${k.activeEntryCount} active entries (soft threshold ${k.softThreshold}). Run \`/cc-learn curate\` to plan a soft-archive of stale/duplicate entries to ${RUNTIME_ROOT}/knowledge.archive.
|
|
1028
|
+
info(ctx, `Knowledge curation recommended: ${k.knowledgePath} now has ${k.activeEntryCount} active entries (soft threshold ${k.softThreshold}). Run \`/cc-learn curate\` to plan a soft-archive of stale/duplicate entries to ${RUNTIME_ROOT}/knowledge.archive.jsonl.`);
|
|
1022
1029
|
}
|
|
1023
1030
|
else if (k.activeEntryCount > 0) {
|
|
1024
1031
|
info(ctx, `Knowledge: ${k.activeEntryCount}/${k.softThreshold} active entries. Run \`/cc-learn curate\` if you want a sweep before the next run.`);
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { CclawConfig, FlowTrack, HarnessId, LanguageRulePack } from "./types.js";
|
|
2
|
+
export declare class InvalidConfigError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
2
5
|
export declare function configPath(projectRoot: string): string;
|
|
3
6
|
/**
|
|
4
7
|
* Default test-path patterns used by workflow-guard.sh to classify TDD writes.
|
package/dist/config.js
CHANGED
|
@@ -65,8 +65,14 @@ function configFixExample() {
|
|
|
65
65
|
- claude
|
|
66
66
|
- cursor`;
|
|
67
67
|
}
|
|
68
|
+
export class InvalidConfigError extends Error {
|
|
69
|
+
constructor(message) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "InvalidConfigError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
68
74
|
function configValidationError(configFilePath, reason) {
|
|
69
|
-
return new
|
|
75
|
+
return new InvalidConfigError(`Invalid cclaw config at ${configFilePath}: ${reason}\n` +
|
|
70
76
|
`Supported harnesses: ${SUPPORTED_HARNESSES_TEXT}\n` +
|
|
71
77
|
`Supported tracks: ${SUPPORTED_TRACKS_TEXT}\n` +
|
|
72
78
|
`Supported languageRulePacks: ${SUPPORTED_LANGUAGE_RULE_PACKS_TEXT}\n` +
|
|
@@ -186,8 +192,12 @@ export async function readConfig(projectRoot) {
|
|
|
186
192
|
try {
|
|
187
193
|
parsedUnknown = parse(await fs.readFile(fullPath, "utf8"));
|
|
188
194
|
}
|
|
189
|
-
catch {
|
|
190
|
-
|
|
195
|
+
catch (error) {
|
|
196
|
+
const reason = error instanceof Error ? error.message : "unknown parse error";
|
|
197
|
+
throw configValidationError(fullPath, `failed to parse YAML (${reason})`);
|
|
198
|
+
}
|
|
199
|
+
if (parsedUnknown !== null && parsedUnknown !== undefined && typeof parsedUnknown !== "object") {
|
|
200
|
+
throw configValidationError(fullPath, "top-level config must be a YAML mapping/object");
|
|
191
201
|
}
|
|
192
202
|
const parsed = (parsedUnknown && typeof parsedUnknown === "object"
|
|
193
203
|
? parsedUnknown
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { FlowStage } from "../types.js";
|
|
2
|
-
export declare function stageCommandContract(stage: FlowStage): string;
|
|
1
|
+
import type { FlowStage, FlowTrack } from "../types.js";
|
|
2
|
+
export declare function stageCommandContract(stage: FlowStage, track?: FlowTrack): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stageSchema } from "./stage-schema.js";
|
|
2
2
|
import { stageSkillFolder } from "./skills.js";
|
|
3
|
-
export function stageCommandContract(stage) {
|
|
4
|
-
const schema = stageSchema(stage);
|
|
3
|
+
export function stageCommandContract(stage, track = "standard") {
|
|
4
|
+
const schema = stageSchema(stage, track);
|
|
5
5
|
const skillPath = `.cclaw/skills/${stageSkillFolder(stage)}/SKILL.md`;
|
|
6
6
|
const reads = schema.crossStageTrace.readsFrom;
|
|
7
7
|
const readsLine = reads.length > 0 ? reads.join(", ") : "(first stage)";
|
|
@@ -65,7 +65,7 @@ export declare const CCLAW_AGENTS: readonly [{
|
|
|
65
65
|
readonly body: string;
|
|
66
66
|
}, {
|
|
67
67
|
readonly name: "doc-updater";
|
|
68
|
-
readonly description: "MANDATORY at ship
|
|
68
|
+
readonly description: "MANDATORY only at ship; PROACTIVE during tdd/review whenever behavior, config, or public API changes. Keep docs and runbooks in lockstep with shipped behavior.";
|
|
69
69
|
readonly tools: ["Read", "Write", "Edit", "Grep", "Glob"];
|
|
70
70
|
readonly model: "fast";
|
|
71
71
|
readonly activation: "mandatory";
|
|
@@ -118,7 +118,7 @@ export const CCLAW_AGENTS = [
|
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
name: "doc-updater",
|
|
121
|
-
description: "MANDATORY at ship
|
|
121
|
+
description: "MANDATORY only at ship; PROACTIVE during tdd/review whenever behavior, config, or public API changes. Keep docs and runbooks in lockstep with shipped behavior.",
|
|
122
122
|
tools: ["Read", "Write", "Edit", "Grep", "Glob"],
|
|
123
123
|
model: "fast",
|
|
124
124
|
activation: "mandatory",
|
package/dist/content/hooks.js
CHANGED
|
@@ -845,7 +845,8 @@ if command -v cclaw >/dev/null 2>&1; then
|
|
|
845
845
|
exec cclaw internal advance-stage "$STAGE" "$@"
|
|
846
846
|
fi
|
|
847
847
|
|
|
848
|
-
|
|
848
|
+
printf '[cclaw] stage-complete: cclaw binary not found in PATH. Install cclaw CLI and rerun stage completion.\\n' >&2
|
|
849
|
+
exit 1
|
|
849
850
|
`;
|
|
850
851
|
}
|
|
851
852
|
export function preCompactScript() {
|
|
@@ -889,8 +890,8 @@ if [ -f "$STATE_FILE" ]; then
|
|
|
889
890
|
COMPLETED=$(jq -r '(.completedStages // []) | length' "$STATE_FILE" 2>/dev/null || echo "0")
|
|
890
891
|
SKIPPED=$(jq -r '(.skippedStages // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
|
|
891
892
|
ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
|
|
892
|
-
PASSED_GATES=$(jq -r --arg stage "$STAGE" '(.
|
|
893
|
-
BLOCKED_GATES=$(jq -r --arg stage "$STAGE" '(.
|
|
893
|
+
PASSED_GATES=$(jq -r --arg stage "$STAGE" '(.stageGateCatalog[$stage].passed // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
|
|
894
|
+
BLOCKED_GATES=$(jq -r --arg stage "$STAGE" '(.stageGateCatalog[$stage].blocked // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
|
|
894
895
|
elif command -v python3 >/dev/null 2>&1; then
|
|
895
896
|
OUTPUT=$(python3 - "$STATE_FILE" <<'PY'
|
|
896
897
|
import json, sys
|
|
@@ -904,7 +905,7 @@ track = data.get("track") or "standard"
|
|
|
904
905
|
completed = data.get("completedStages") or []
|
|
905
906
|
skipped = data.get("skippedStages") or []
|
|
906
907
|
run = data.get("activeRunId") or "none"
|
|
907
|
-
gates = (data.get("
|
|
908
|
+
gates = (data.get("stageGateCatalog") or {}).get(stage) or {}
|
|
908
909
|
passed = gates.get("passed") or []
|
|
909
910
|
blocked = gates.get("blocked") or []
|
|
910
911
|
print(stage)
|
|
@@ -965,20 +966,20 @@ TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
|
965
966
|
printf '# Session Digest\n'
|
|
966
967
|
printf '_Generated by pre-compact hook at %s_\n\n' "$TS"
|
|
967
968
|
printf '## Flow snapshot\n'
|
|
968
|
-
printf '- track: %s\n' "$TRACK"
|
|
969
|
-
printf '- current stage: %s\n' "$STAGE"
|
|
970
|
-
printf '- completed: %s stages\n' "$COMPLETED"
|
|
971
|
-
printf '- skipped: %s\n' "\${SKIPPED:-(none)}"
|
|
972
|
-
printf '- run: %s\n\n' "$ACTIVE_RUN"
|
|
969
|
+
printf -- '- track: %s\n' "$TRACK"
|
|
970
|
+
printf -- '- current stage: %s\n' "$STAGE"
|
|
971
|
+
printf -- '- completed: %s stages\n' "$COMPLETED"
|
|
972
|
+
printf -- '- skipped: %s\n' "\${SKIPPED:-(none)}"
|
|
973
|
+
printf -- '- run: %s\n\n' "$ACTIVE_RUN"
|
|
973
974
|
printf '## Gates (current stage)\n'
|
|
974
|
-
printf '- passed: %s\n' "\${PASSED_GATES:-(none)}"
|
|
975
|
-
printf '- blocked: %s\n\n' "\${BLOCKED_GATES:-(none)}"
|
|
975
|
+
printf -- '- passed: %s\n' "\${PASSED_GATES:-(none)}"
|
|
976
|
+
printf -- '- blocked: %s\n\n' "\${BLOCKED_GATES:-(none)}"
|
|
976
977
|
printf '## Outstanding delegations\n'
|
|
977
|
-
printf '- pending: %s\n\n' "\${DELEGATION_PENDING:-(none)}"
|
|
978
|
+
printf -- '- pending: %s\n\n' "\${DELEGATION_PENDING:-(none)}"
|
|
978
979
|
printf '## Git\n'
|
|
979
|
-
printf '- branch: %s\n' "\${GIT_BRANCH:-(unknown)}"
|
|
980
|
-
printf '- head: %s\n' "\${GIT_HEAD:-(unknown)}"
|
|
981
|
-
printf '- worktree: %s\n\n' "$GIT_DIRTY"
|
|
980
|
+
printf -- '- branch: %s\n' "\${GIT_BRANCH:-(unknown)}"
|
|
981
|
+
printf -- '- head: %s\n' "\${GIT_HEAD:-(unknown)}"
|
|
982
|
+
printf -- '- worktree: %s\n\n' "$GIT_DIRTY"
|
|
982
983
|
if [ -n "$KNOWLEDGE_TAIL" ]; then
|
|
983
984
|
printf '## Knowledge tail\n'
|
|
984
985
|
printf '%s\n' "$KNOWLEDGE_TAIL"
|
|
@@ -50,8 +50,9 @@ This is the only progression command the user needs to drive the entire flow. St
|
|
|
50
50
|
7. **Satisfied** for gate id \`g\`: \`g\` in \`catalog.passed\` and \`g\` not in \`catalog.blocked\`.
|
|
51
51
|
8. Let \`M\` = \`mandatoryDelegations\` for \`currentStage\`.
|
|
52
52
|
9. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if each mandatory agent is **completed** or **waived**.
|
|
53
|
-
10.
|
|
54
|
-
11. If
|
|
53
|
+
10. For each satisfied mandatory delegation row, verify \`evidenceRefs\` is a non-empty array (unless status is \`waived\` with rationale). Missing evidenceRefs means delegation is unresolved.
|
|
54
|
+
11. If any mandatory delegation is missing and no waiver exists: **STOP** and ask the user whether to dispatch now or waive with rationale. Do not mark gates passed while delegation is unresolved.
|
|
55
|
+
12. If \`currentStage === "review"\` and \`catalog.blocked\` includes \`review_criticals_resolved\`, treat this as a hard remediation branch: recommend \`/cc-ops rewind tdd "review_blocked_by_critical"\` with the blocking finding IDs, and do not attempt to advance toward ship.
|
|
55
56
|
|
|
56
57
|
### Path A: Current stage is NOT complete (any gate unmet or delegation missing)
|
|
57
58
|
|
|
@@ -161,6 +162,7 @@ For each gate id in \`requiredGates\` for \`currentStage\`:
|
|
|
161
162
|
- **Unmet** otherwise.
|
|
162
163
|
|
|
163
164
|
Check \`mandatoryDelegations\` via **\`${delegationPath}\`** — satisfied only if **completed** or **waived**.
|
|
165
|
+
Also verify each completed mandatory delegation row has non-empty \`evidenceRefs\` (waived rows must include rationale).
|
|
164
166
|
If a mandatory delegation is missing and no waiver exists, **STOP** and ask:
|
|
165
167
|
(A) dispatch now, (B) waive with rationale, (C) cancel stage advance.
|
|
166
168
|
|
|
@@ -25,8 +25,8 @@ export declare function cursorHooksJsonWithObservation(): string;
|
|
|
25
25
|
* - `SessionStart` matcher is limited to `startup|resume` — Codex does
|
|
26
26
|
* not emit `clear` or `compact` lifecycle phases.
|
|
27
27
|
* - `PreToolUse` / `PostToolUse` fire **only for the `Bash` tool**
|
|
28
|
-
* (documented Codex limitation, v0.114/v0.115). We
|
|
29
|
-
*
|
|
28
|
+
* (documented Codex limitation, v0.114/v0.115). We match both `Bash`
|
|
29
|
+
* and `bash` variants to tolerate casing drift across Codex builds.
|
|
30
30
|
* - `UserPromptSubmit` is supported and is the closest analogue to
|
|
31
31
|
* Cursor's `preToolUse` for non-Bash tooling — we run prompt-guard
|
|
32
32
|
* there so workflow/prompt checks still fire when the tool being
|
package/dist/content/observe.js
CHANGED
|
@@ -13,6 +13,7 @@ export function promptGuardScript(options = {}) {
|
|
|
13
13
|
# cclaw prompt guard hook — generated by cclaw sync
|
|
14
14
|
# Advisory-only guard for risky writes into ${RUNTIME_ROOT} runtime files.
|
|
15
15
|
set -uo pipefail
|
|
16
|
+
shopt -s globstar 2>/dev/null || true
|
|
16
17
|
PROMPT_GUARD_MODE="${promptGuardMode}"
|
|
17
18
|
|
|
18
19
|
HARNESS="codex"
|
|
@@ -166,6 +167,7 @@ export function workflowGuardScript(options = {}) {
|
|
|
166
167
|
# cclaw workflow guard hook — generated by cclaw sync
|
|
167
168
|
# Enforces stage-aware command discipline and recent flow-state read hygiene.
|
|
168
169
|
set -uo pipefail
|
|
170
|
+
shopt -s globstar 2>/dev/null || true
|
|
169
171
|
WORKFLOW_GUARD_MODE="\${CCLAW_WORKFLOW_GUARD_MODE:-${workflowGuardMode}}"
|
|
170
172
|
MAX_FLOW_READ_AGE_SEC="\${CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC:-1800}"
|
|
171
173
|
TDD_ENFORCEMENT_MODE="${tddEnforcementMode}"
|
|
@@ -408,10 +410,12 @@ verify_flow_state_candidate() {
|
|
|
408
410
|
return 1
|
|
409
411
|
}
|
|
410
412
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
if ! command -v cclaw >/dev/null 2>&1; then
|
|
414
|
+
rm -f "$tmp_file" 2>/dev/null || true
|
|
415
|
+
printf '[cclaw] workflow guard: cclaw binary is required to validate flow-state edits; install cclaw and re-run.\\n' >&2
|
|
416
|
+
return 1
|
|
414
417
|
fi
|
|
418
|
+
local verify_cmd=(cclaw internal verify-flow-state-diff --after-file="$tmp_file" --quiet)
|
|
415
419
|
|
|
416
420
|
if "\${verify_cmd[@]}" >/dev/null 2>&1; then
|
|
417
421
|
rm -f "$tmp_file" 2>/dev/null || true
|
|
@@ -579,10 +583,13 @@ tdd_cycle_counts() {
|
|
|
579
583
|
fi
|
|
580
584
|
local red_count="0"
|
|
581
585
|
local green_count="0"
|
|
582
|
-
if command -v jq >/dev/null 2>&1; then
|
|
586
|
+
if command -v jq >/dev/null 2>&1 && jq -n '1' >/dev/null 2>&1; then
|
|
583
587
|
red_count=$(jq -r --arg run "$CURRENT_RUN" 'select((.runId // $run) == $run and .phase == "red") | .phase' "$TDD_LOG_FILE" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
584
588
|
green_count=$(jq -r --arg run "$CURRENT_RUN" 'select((.runId // $run) == $run and .phase == "green") | .phase' "$TDD_LOG_FILE" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
585
|
-
elif command -v python3 >/dev/null 2>&1
|
|
589
|
+
elif command -v python3 >/dev/null 2>&1 && python3 - <<'PY' >/dev/null 2>&1
|
|
590
|
+
print("ok")
|
|
591
|
+
PY
|
|
592
|
+
then
|
|
586
593
|
red_count=$(python3 - "$TDD_LOG_FILE" "$CURRENT_RUN" <<'PY'
|
|
587
594
|
import json
|
|
588
595
|
import sys
|
|
@@ -630,8 +637,36 @@ print(count)
|
|
|
630
637
|
PY
|
|
631
638
|
)
|
|
632
639
|
else
|
|
633
|
-
|
|
634
|
-
|
|
640
|
+
if command -v awk >/dev/null 2>&1; then
|
|
641
|
+
local fallback_counts
|
|
642
|
+
fallback_counts=$(awk -v run="$CURRENT_RUN" '
|
|
643
|
+
BEGIN { red=0; green=0; }
|
|
644
|
+
{
|
|
645
|
+
line=$0;
|
|
646
|
+
line_run=run;
|
|
647
|
+
if (match(line, /"runId"[[:space:]]*:[[:space:]]*"[^"]+"/)) {
|
|
648
|
+
line_run=substr(line, RSTART, RLENGTH);
|
|
649
|
+
sub(/.*"/, "", line_run);
|
|
650
|
+
sub(/"$/, "", line_run);
|
|
651
|
+
}
|
|
652
|
+
if (line_run != run) next;
|
|
653
|
+
if (match(line, /"phase"[[:space:]]*:[[:space:]]*"[^"]+"/)) {
|
|
654
|
+
phase=substr(line, RSTART, RLENGTH);
|
|
655
|
+
sub(/.*"/, "", phase);
|
|
656
|
+
sub(/"$/, "", phase);
|
|
657
|
+
if (phase == "red") red += 1;
|
|
658
|
+
else if (phase == "green") green += 1;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
END { printf "%d:%d", red, green; }
|
|
662
|
+
' "$TDD_LOG_FILE" 2>/dev/null || true)
|
|
663
|
+
if printf '%s' "$fallback_counts" | grep -Eq '^[0-9]+:[0-9]+$'; then
|
|
664
|
+
printf '%s' "$fallback_counts"
|
|
665
|
+
return 0
|
|
666
|
+
fi
|
|
667
|
+
fi
|
|
668
|
+
printf '__UNAVAILABLE__'
|
|
669
|
+
return 0
|
|
635
670
|
fi
|
|
636
671
|
[ -n "$red_count" ] || red_count="0"
|
|
637
672
|
[ -n "$green_count" ] || green_count="0"
|
|
@@ -641,8 +676,14 @@ PY
|
|
|
641
676
|
has_open_red_cycle() {
|
|
642
677
|
local counts
|
|
643
678
|
counts=$(tdd_cycle_counts)
|
|
679
|
+
if [ "$counts" = "__UNAVAILABLE__" ]; then
|
|
680
|
+
return 2
|
|
681
|
+
fi
|
|
644
682
|
local red_count="\${counts%%:*}"
|
|
645
683
|
local green_count="\${counts##*:}"
|
|
684
|
+
if ! printf '%s' "$red_count:$green_count" | grep -Eq '^[0-9]+:[0-9]+$'; then
|
|
685
|
+
return 2
|
|
686
|
+
fi
|
|
646
687
|
if [ "$red_count" -gt "$green_count" ]; then
|
|
647
688
|
return 0
|
|
648
689
|
fi
|
|
@@ -652,8 +693,16 @@ has_open_red_cycle() {
|
|
|
652
693
|
tdd_cycle_state() {
|
|
653
694
|
local counts
|
|
654
695
|
counts=$(tdd_cycle_counts)
|
|
696
|
+
if [ "$counts" = "__UNAVAILABLE__" ]; then
|
|
697
|
+
printf '__UNAVAILABLE__'
|
|
698
|
+
return 0
|
|
699
|
+
fi
|
|
655
700
|
local red_count="\${counts%%:*}"
|
|
656
701
|
local green_count="\${counts##*:}"
|
|
702
|
+
if ! printf '%s' "$red_count:$green_count" | grep -Eq '^[0-9]+:[0-9]+$'; then
|
|
703
|
+
printf '__UNAVAILABLE__'
|
|
704
|
+
return 0
|
|
705
|
+
fi
|
|
657
706
|
if [ "$red_count" -le 0 ]; then
|
|
658
707
|
printf 'need_red'
|
|
659
708
|
return 0
|
|
@@ -740,7 +789,17 @@ if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
|
|
|
740
789
|
if has_open_red_cycle; then
|
|
741
790
|
TDD_CYCLE_STATE="red_open"
|
|
742
791
|
else
|
|
743
|
-
|
|
792
|
+
OPEN_RED_STATUS=$?
|
|
793
|
+
if [ "$OPEN_RED_STATUS" -eq 2 ]; then
|
|
794
|
+
TDD_CYCLE_STATE="counts_unavailable"
|
|
795
|
+
if [ -n "$REASONS" ]; then
|
|
796
|
+
REASONS="$REASONS,tdd_cycle_counts_unavailable"
|
|
797
|
+
else
|
|
798
|
+
REASONS="tdd_cycle_counts_unavailable"
|
|
799
|
+
fi
|
|
800
|
+
else
|
|
801
|
+
TDD_CYCLE_STATE=$(tdd_cycle_state)
|
|
802
|
+
fi
|
|
744
803
|
fi
|
|
745
804
|
if [ "$TDD_CYCLE_STATE" = "need_red" ]; then
|
|
746
805
|
if [ -n "$REASONS" ]; then
|
|
@@ -748,6 +807,12 @@ if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
|
|
|
748
807
|
else
|
|
749
808
|
REASONS="tdd_write_without_open_red"
|
|
750
809
|
fi
|
|
810
|
+
elif [ "$TDD_CYCLE_STATE" = "__UNAVAILABLE__" ]; then
|
|
811
|
+
if [ -n "$REASONS" ]; then
|
|
812
|
+
REASONS="$REASONS,tdd_cycle_counts_unavailable"
|
|
813
|
+
else
|
|
814
|
+
REASONS="tdd_cycle_counts_unavailable"
|
|
815
|
+
fi
|
|
751
816
|
fi
|
|
752
817
|
fi
|
|
753
818
|
fi
|
|
@@ -819,6 +884,8 @@ fi
|
|
|
819
884
|
if [ -n "$REASONS" ]; then
|
|
820
885
|
if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red'; then
|
|
821
886
|
NOTE="Cclaw workflow guard: Write a failing test first before editing production files during tdd stage (state=\${TDD_CYCLE_STATE})."
|
|
887
|
+
elif printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable'; then
|
|
888
|
+
NOTE="Cclaw workflow guard: unable to inspect run-scoped tdd-cycle counts (missing usable jq/python3/awk). Install one of these tools before writing production code in tdd."
|
|
822
889
|
elif printf '%s' "$REASONS" | grep -Eq 'direct_flow_state_edit'; then
|
|
823
890
|
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."
|
|
824
891
|
else
|
|
@@ -846,6 +913,9 @@ if [ -n "$REASONS" ]; then
|
|
|
846
913
|
if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red' && [ "$TDD_ENFORCEMENT_MODE" = "strict" ]; then
|
|
847
914
|
SHOULD_BLOCK="true"
|
|
848
915
|
fi
|
|
916
|
+
if printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable'; then
|
|
917
|
+
SHOULD_BLOCK="true"
|
|
918
|
+
fi
|
|
849
919
|
if [ "$WORKFLOW_GUARD_MODE" = "strict" ] || [ "$SHOULD_BLOCK" = "true" ]; then
|
|
850
920
|
printf '[cclaw] %s (blocked by workflow guard)\n' "$NOTE" >&2
|
|
851
921
|
exit 1
|
|
@@ -1177,8 +1247,8 @@ export function cursorHooksJsonWithObservation() {
|
|
|
1177
1247
|
* - `SessionStart` matcher is limited to `startup|resume` — Codex does
|
|
1178
1248
|
* not emit `clear` or `compact` lifecycle phases.
|
|
1179
1249
|
* - `PreToolUse` / `PostToolUse` fire **only for the `Bash` tool**
|
|
1180
|
-
* (documented Codex limitation, v0.114/v0.115). We
|
|
1181
|
-
*
|
|
1250
|
+
* (documented Codex limitation, v0.114/v0.115). We match both `Bash`
|
|
1251
|
+
* and `bash` variants to tolerate casing drift across Codex builds.
|
|
1182
1252
|
* - `UserPromptSubmit` is supported and is the closest analogue to
|
|
1183
1253
|
* Cursor's `preToolUse` for non-Bash tooling — we run prompt-guard
|
|
1184
1254
|
* there so workflow/prompt checks still fire when the tool being
|
|
@@ -1219,11 +1289,11 @@ export function codexHooksJsonWithObservation() {
|
|
|
1219
1289
|
command: hookDispatcherCommand("workflow-guard.sh")
|
|
1220
1290
|
}, {
|
|
1221
1291
|
type: "command",
|
|
1222
|
-
command: "bash -lc 'if command -v cclaw >/dev/null 2>&1; then cclaw
|
|
1292
|
+
command: "bash -lc 'if ! command -v cclaw >/dev/null 2>&1; then echo \"[cclaw] codex hook: cclaw binary is required for verify-current-state\" >&2; exit 1; fi; cclaw internal verify-current-state --quiet >/dev/null || true'"
|
|
1223
1293
|
}]
|
|
1224
1294
|
}],
|
|
1225
1295
|
PreToolUse: [{
|
|
1226
|
-
matcher: "Bash",
|
|
1296
|
+
matcher: "Bash|bash",
|
|
1227
1297
|
hooks: [{
|
|
1228
1298
|
type: "command",
|
|
1229
1299
|
command: hookDispatcherCommand("prompt-guard.sh")
|
|
@@ -1233,7 +1303,7 @@ export function codexHooksJsonWithObservation() {
|
|
|
1233
1303
|
}]
|
|
1234
1304
|
}],
|
|
1235
1305
|
PostToolUse: [{
|
|
1236
|
-
matcher: "Bash",
|
|
1306
|
+
matcher: "Bash|bash",
|
|
1237
1307
|
hooks: [{
|
|
1238
1308
|
type: "command",
|
|
1239
1309
|
command: hookDispatcherCommand("context-monitor.sh")
|