cclaw-cli 0.11.0 → 0.13.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 +4 -3
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +311 -10
- package/dist/config.js +19 -0
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +13 -1
- package/dist/content/core-agents.d.ts +44 -0
- package/dist/content/core-agents.js +225 -0
- package/dist/content/diff-command.d.ts +2 -0
- package/dist/content/diff-command.js +83 -0
- package/dist/content/doctor-references.d.ts +2 -0
- package/dist/content/doctor-references.js +144 -0
- package/dist/content/examples.js +1 -1
- package/dist/content/feature-command.d.ts +2 -0
- package/dist/content/feature-command.js +120 -0
- package/dist/content/harnesses-doc.d.ts +1 -0
- package/dist/content/harnesses-doc.js +103 -0
- package/dist/content/hook-events.d.ts +4 -0
- package/dist/content/hook-events.js +42 -0
- package/dist/content/hooks.js +47 -1
- package/dist/content/meta-skill.js +3 -2
- package/dist/content/next-command.js +8 -6
- package/dist/content/observe.d.ts +5 -1
- package/dist/content/observe.js +134 -2
- package/dist/content/protocols.js +34 -6
- package/dist/content/research-playbooks.d.ts +8 -0
- package/dist/content/research-playbooks.js +135 -0
- package/dist/content/retro-command.d.ts +2 -0
- package/dist/content/retro-command.js +77 -0
- package/dist/content/rewind-command.d.ts +3 -0
- package/dist/content/rewind-command.js +120 -0
- package/dist/content/skills.js +20 -0
- package/dist/content/stage-schema.d.ts +3 -1
- package/dist/content/stage-schema.js +20 -51
- package/dist/content/status-command.js +43 -35
- package/dist/content/subagents.d.ts +1 -1
- package/dist/content/subagents.js +23 -38
- package/dist/content/tdd-log-command.d.ts +2 -0
- package/dist/content/tdd-log-command.js +75 -0
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +84 -16
- package/dist/content/tree-command.d.ts +2 -0
- package/dist/content/tree-command.js +91 -0
- package/dist/delegation.d.ts +1 -0
- package/dist/delegation.js +27 -1
- package/dist/doctor-registry.d.ts +8 -0
- package/dist/doctor-registry.js +127 -0
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +261 -7
- package/dist/feature-system.d.ts +18 -0
- package/dist/feature-system.js +247 -0
- package/dist/flow-state.d.ts +25 -0
- package/dist/flow-state.js +8 -1
- package/dist/harness-adapters.d.ts +7 -0
- package/dist/harness-adapters.js +127 -13
- package/dist/init-detect.d.ts +2 -0
- package/dist/init-detect.js +45 -0
- package/dist/install.js +98 -3
- package/dist/policy.js +27 -0
- package/dist/runs.d.ts +33 -1
- package/dist/runs.js +365 -6
- package/dist/tdd-cycle.d.ts +22 -0
- package/dist/tdd-cycle.js +82 -0
- package/dist/types.d.ts +4 -0
- package/package.json +2 -1
- package/dist/content/agents.d.ts +0 -48
- package/dist/content/agents.js +0 -411
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { HARNESS_ADAPTERS, harnessTier } from "../harness-adapters.js";
|
|
2
|
+
import { HOOK_EVENTS_BY_HARNESS, HOOK_SEMANTIC_EVENTS } from "./hook-events.js";
|
|
3
|
+
function harnessTitle(harness) {
|
|
4
|
+
switch (harness) {
|
|
5
|
+
case "claude":
|
|
6
|
+
return "Claude Code";
|
|
7
|
+
case "cursor":
|
|
8
|
+
return "Cursor";
|
|
9
|
+
case "opencode":
|
|
10
|
+
return "OpenCode";
|
|
11
|
+
case "codex":
|
|
12
|
+
return "OpenAI Codex";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function tierDescription(tier) {
|
|
16
|
+
if (tier === "tier1")
|
|
17
|
+
return "full native automation";
|
|
18
|
+
if (tier === "tier2")
|
|
19
|
+
return "partial automation with waivers";
|
|
20
|
+
return "manual fallback only";
|
|
21
|
+
}
|
|
22
|
+
export function harnessIntegrationDocMarkdown() {
|
|
23
|
+
const harnesses = Object.keys(HARNESS_ADAPTERS);
|
|
24
|
+
const capabilityRows = harnesses
|
|
25
|
+
.map((harness) => {
|
|
26
|
+
const adapter = HARNESS_ADAPTERS[harness];
|
|
27
|
+
const tier = harnessTier(harness);
|
|
28
|
+
return `| ${harnessTitle(harness)} | \`${harness}\` | \`${tier}\` (${tierDescription(tier)}) | ${adapter.capabilities.nativeSubagentDispatch} | ${adapter.capabilities.hookSurface} | ${adapter.capabilities.structuredAsk} |`;
|
|
29
|
+
})
|
|
30
|
+
.join("\n");
|
|
31
|
+
const hookRows = HOOK_SEMANTIC_EVENTS.map((eventName) => {
|
|
32
|
+
const columns = harnesses
|
|
33
|
+
.map((harness) => {
|
|
34
|
+
const mapping = HOOK_EVENTS_BY_HARNESS[harness][eventName];
|
|
35
|
+
return mapping ?? "missing";
|
|
36
|
+
})
|
|
37
|
+
.join(" | ");
|
|
38
|
+
return `| \`${eventName}\` | ${columns} |`;
|
|
39
|
+
}).join("\n");
|
|
40
|
+
return `# Harness Integration Matrix
|
|
41
|
+
|
|
42
|
+
Generated from \`src/harness-adapters.ts\` capabilities and hook event mappings.
|
|
43
|
+
|
|
44
|
+
## Capability tiers
|
|
45
|
+
|
|
46
|
+
| Harness | ID | Tier | Native subagent dispatch | Hook surface | Structured ask |
|
|
47
|
+
|---|---|---|---|---|---|
|
|
48
|
+
${capabilityRows}
|
|
49
|
+
|
|
50
|
+
## Semantic hook event coverage
|
|
51
|
+
|
|
52
|
+
| Event | Claude | Cursor | OpenCode | Codex |
|
|
53
|
+
|---|---|---|---|---|
|
|
54
|
+
${hookRows}
|
|
55
|
+
|
|
56
|
+
## Interpretation
|
|
57
|
+
|
|
58
|
+
- \`tier1\`: full native delegation + structured asks + full hook surface.
|
|
59
|
+
- \`tier2\`: usable flow with capability gaps; mandatory delegation can require waivers.
|
|
60
|
+
- \`tier3\`: manual-only fallback; no native automation guarantees.
|
|
61
|
+
|
|
62
|
+
## Shared command contract
|
|
63
|
+
|
|
64
|
+
All harnesses receive the same utility commands:
|
|
65
|
+
|
|
66
|
+
- \`/cc\` - flow entry and resume
|
|
67
|
+
- \`/cc-next\` - stage progression
|
|
68
|
+
- \`/cc-learn\` - knowledge capture/lookup
|
|
69
|
+
- \`/cc-status\` - read-only visual flow snapshot
|
|
70
|
+
- \`/cc-tree\` - deep flow tree (stages, artifacts, stale markers)
|
|
71
|
+
- \`/cc-diff\` - before/after flow-state diff map
|
|
72
|
+
- \`/cc-feature\` - multi-feature workspace management
|
|
73
|
+
- \`/cc-tdd-log\` - explicit RED/GREEN/REFACTOR evidence log
|
|
74
|
+
- \`/cc-retro\` - mandatory retrospective gate before archive
|
|
75
|
+
- \`/cc-rewind\` - rewind flow and invalidate downstream stages
|
|
76
|
+
- \`/cc-rewind-ack\` - clear stale stage markers after redo
|
|
77
|
+
|
|
78
|
+
Stage order remains canonical:
|
|
79
|
+
\`brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship\`
|
|
80
|
+
|
|
81
|
+
## Install surfaces
|
|
82
|
+
|
|
83
|
+
Always generated:
|
|
84
|
+
|
|
85
|
+
- \`.cclaw/commands/*.md\`
|
|
86
|
+
- \`.cclaw/skills/*/SKILL.md\`
|
|
87
|
+
- \`.cclaw/references/**\`
|
|
88
|
+
- \`.cclaw/state/*.json|*.jsonl\`
|
|
89
|
+
- \`AGENTS.md\` managed block
|
|
90
|
+
|
|
91
|
+
Harness-specific additions:
|
|
92
|
+
|
|
93
|
+
- \`claude\`: \`.claude/commands/cc*.md\`, \`.claude/hooks/hooks.json\`
|
|
94
|
+
- \`cursor\`: \`.cursor/commands/cc*.md\`, \`.cursor/hooks.json\`, \`.cursor/rules/cclaw-workflow.mdc\`
|
|
95
|
+
- \`opencode\`: \`.opencode/commands/cc*.md\`, \`.opencode/plugins/cclaw-plugin.mjs\`, opencode plugin registration
|
|
96
|
+
- \`codex\`: \`.codex/commands/cc*.md\`, \`.codex/hooks.json\`
|
|
97
|
+
|
|
98
|
+
## Runtime observability
|
|
99
|
+
|
|
100
|
+
- \`.cclaw/state/harness-gaps.json\` captures per-harness capability gaps for the active config.
|
|
101
|
+
- \`cclaw doctor\` validates shim, hook, and lifecycle surfaces against this capability model.
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HarnessId } from "../types.js";
|
|
2
|
+
export declare const HOOK_SEMANTIC_EVENTS: readonly ["session_rehydrate", "pre_tool_prompt_guard", "pre_tool_workflow_guard", "post_tool_context_monitor", "stop_checkpoint", "precompact_digest"];
|
|
3
|
+
export type HookSemanticEvent = (typeof HOOK_SEMANTIC_EVENTS)[number];
|
|
4
|
+
export declare const HOOK_EVENTS_BY_HARNESS: Record<HarnessId, Partial<Record<HookSemanticEvent, string>>>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const HOOK_SEMANTIC_EVENTS = [
|
|
2
|
+
"session_rehydrate",
|
|
3
|
+
"pre_tool_prompt_guard",
|
|
4
|
+
"pre_tool_workflow_guard",
|
|
5
|
+
"post_tool_context_monitor",
|
|
6
|
+
"stop_checkpoint",
|
|
7
|
+
"precompact_digest"
|
|
8
|
+
];
|
|
9
|
+
export const HOOK_EVENTS_BY_HARNESS = {
|
|
10
|
+
claude: {
|
|
11
|
+
session_rehydrate: "SessionStart matcher startup|resume|clear|compact",
|
|
12
|
+
pre_tool_prompt_guard: "PreToolUse -> prompt-guard.sh",
|
|
13
|
+
pre_tool_workflow_guard: "PreToolUse -> workflow-guard.sh",
|
|
14
|
+
post_tool_context_monitor: "PostToolUse -> context-monitor.sh",
|
|
15
|
+
stop_checkpoint: "Stop -> stop-checkpoint.sh",
|
|
16
|
+
precompact_digest: "PreCompact -> pre-compact.sh"
|
|
17
|
+
},
|
|
18
|
+
cursor: {
|
|
19
|
+
session_rehydrate: "sessionStart/sessionResume/sessionClear/sessionCompact",
|
|
20
|
+
pre_tool_prompt_guard: "preToolUse -> prompt-guard.sh",
|
|
21
|
+
pre_tool_workflow_guard: "preToolUse -> workflow-guard.sh",
|
|
22
|
+
post_tool_context_monitor: "postToolUse -> context-monitor.sh",
|
|
23
|
+
stop_checkpoint: "stop -> stop-checkpoint.sh",
|
|
24
|
+
precompact_digest: "sessionCompact -> pre-compact.sh"
|
|
25
|
+
},
|
|
26
|
+
opencode: {
|
|
27
|
+
session_rehydrate: "plugin event handlers + transform rehydration",
|
|
28
|
+
pre_tool_prompt_guard: "plugin tool.execute.before -> prompt-guard.sh",
|
|
29
|
+
pre_tool_workflow_guard: "plugin tool.execute.before -> workflow-guard.sh",
|
|
30
|
+
post_tool_context_monitor: "plugin tool.execute.after -> context-monitor.sh",
|
|
31
|
+
stop_checkpoint: "plugin session.idle -> stop-checkpoint.sh",
|
|
32
|
+
precompact_digest: "plugin session.cleared/session.resumed hooks"
|
|
33
|
+
},
|
|
34
|
+
codex: {
|
|
35
|
+
session_rehydrate: "SessionStart matcher startup|resume|clear|compact",
|
|
36
|
+
pre_tool_prompt_guard: "PreToolUse -> prompt-guard.sh",
|
|
37
|
+
pre_tool_workflow_guard: "PreToolUse -> workflow-guard.sh",
|
|
38
|
+
post_tool_context_monitor: "PostToolUse -> context-monitor.sh",
|
|
39
|
+
stop_checkpoint: "Stop -> stop-checkpoint.sh",
|
|
40
|
+
precompact_digest: "PreCompact -> pre-compact.sh"
|
|
41
|
+
}
|
|
42
|
+
};
|
package/dist/content/hooks.js
CHANGED
|
@@ -45,6 +45,7 @@ set -euo pipefail
|
|
|
45
45
|
${DETECT_ROOT}
|
|
46
46
|
|
|
47
47
|
STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
|
|
48
|
+
ACTIVE_FEATURE_FILE="$ROOT/${RUNTIME_ROOT}/state/active-feature.json"
|
|
48
49
|
CHECKPOINT_FILE="$ROOT/${RUNTIME_ROOT}/state/checkpoint.json"
|
|
49
50
|
ACTIVITY_FILE="$ROOT/${RUNTIME_ROOT}/state/stage-activity.jsonl"
|
|
50
51
|
SUGGESTION_MEMORY_FILE="$ROOT/${RUNTIME_ROOT}/state/suggestion-memory.json"
|
|
@@ -59,13 +60,16 @@ META_SKILL="$ROOT/${RUNTIME_ROOT}/skills/${META_SKILL_NAME}/SKILL.md"
|
|
|
59
60
|
STAGE="none"
|
|
60
61
|
COMPLETED="0"
|
|
61
62
|
ACTIVE_RUN="none"
|
|
63
|
+
ACTIVE_FEATURE="default"
|
|
62
64
|
ACTIVE_CONTEXT_MODE="default"
|
|
65
|
+
STALE_STAGES=""
|
|
63
66
|
CONTEXT_MODE_NOTE=""
|
|
64
67
|
if [ -f "$STATE_FILE" ]; then
|
|
65
68
|
if command -v jq >/dev/null 2>&1; then
|
|
66
69
|
STAGE=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
|
|
67
70
|
COMPLETED=$(jq -r '(.completedStages | length) // 0' "$STATE_FILE" 2>/dev/null || echo "0")
|
|
68
71
|
ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
|
|
72
|
+
STALE_STAGES=$(jq -r '(.staleStages // {} | keys | join(", "))' "$STATE_FILE" 2>/dev/null || echo "")
|
|
69
73
|
else
|
|
70
74
|
if command -v python3 >/dev/null 2>&1; then
|
|
71
75
|
STAGE=$(python3 - "$STATE_FILE" <<'PY'
|
|
@@ -115,6 +119,22 @@ except Exception:
|
|
|
115
119
|
pass
|
|
116
120
|
print(run)
|
|
117
121
|
PY
|
|
122
|
+
)
|
|
123
|
+
STALE_STAGES=$(python3 - "$STATE_FILE" <<'PY'
|
|
124
|
+
import json
|
|
125
|
+
import sys
|
|
126
|
+
value = ""
|
|
127
|
+
try:
|
|
128
|
+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
129
|
+
data = json.load(fh)
|
|
130
|
+
stale = data.get("staleStages", {})
|
|
131
|
+
if isinstance(stale, dict):
|
|
132
|
+
keys = [k for k, v in stale.items() if isinstance(v, dict)]
|
|
133
|
+
value = ", ".join(keys)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
print(value)
|
|
137
|
+
PY
|
|
118
138
|
)
|
|
119
139
|
else
|
|
120
140
|
STAGE=$(grep -o '"currentStage"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "none")
|
|
@@ -129,6 +149,28 @@ PY
|
|
|
129
149
|
fi
|
|
130
150
|
fi
|
|
131
151
|
|
|
152
|
+
if [ -f "$ACTIVE_FEATURE_FILE" ]; then
|
|
153
|
+
if command -v jq >/dev/null 2>&1; then
|
|
154
|
+
ACTIVE_FEATURE=$(jq -r '.activeFeature // "default"' "$ACTIVE_FEATURE_FILE" 2>/dev/null || echo "default")
|
|
155
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
156
|
+
ACTIVE_FEATURE=$(python3 - "$ACTIVE_FEATURE_FILE" <<'PY'
|
|
157
|
+
import json
|
|
158
|
+
import sys
|
|
159
|
+
feature = "default"
|
|
160
|
+
try:
|
|
161
|
+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
162
|
+
data = json.load(fh)
|
|
163
|
+
value = data.get("activeFeature")
|
|
164
|
+
if isinstance(value, str) and value:
|
|
165
|
+
feature = value
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
print(feature)
|
|
169
|
+
PY
|
|
170
|
+
)
|
|
171
|
+
fi
|
|
172
|
+
fi
|
|
173
|
+
|
|
132
174
|
if [ -f "$CONTEXT_MODE_FILE" ]; then
|
|
133
175
|
if command -v jq >/dev/null 2>&1; then
|
|
134
176
|
ACTIVE_CONTEXT_MODE=$(jq -r '.activeMode // "default"' "$CONTEXT_MODE_FILE" 2>/dev/null || echo "default")
|
|
@@ -415,7 +457,7 @@ if [ -n "$ROUTING_MISSING" ]; then
|
|
|
415
457
|
fi
|
|
416
458
|
|
|
417
459
|
# --- Build context message ---
|
|
418
|
-
CTX="cclaw loaded. Flow: stage=$STAGE ($COMPLETED/8 completed, run=$ACTIVE_RUN). Active artifacts: ${RUNTIME_ROOT}/artifacts/. Learnings: $LEARNINGS_COUNT entries."
|
|
460
|
+
CTX="cclaw loaded. Flow: stage=$STAGE ($COMPLETED/8 completed, run=$ACTIVE_RUN, feature=$ACTIVE_FEATURE). Active artifacts: ${RUNTIME_ROOT}/artifacts/. Feature snapshots: ${RUNTIME_ROOT}/features/$ACTIVE_FEATURE/. Learnings: $LEARNINGS_COUNT entries."
|
|
419
461
|
if [ -n "$VERSION_NOTE" ]; then
|
|
420
462
|
CTX="$CTX
|
|
421
463
|
$VERSION_NOTE"
|
|
@@ -452,6 +494,10 @@ if [ -n "$STAGE_SUGGESTION" ]; then
|
|
|
452
494
|
$STAGE_SUGGESTION
|
|
453
495
|
To disable suggestions persistently set ${RUNTIME_ROOT}/state/suggestion-memory.json -> enabled=false."
|
|
454
496
|
fi
|
|
497
|
+
if [ -n "$STALE_STAGES" ]; then
|
|
498
|
+
CTX="$CTX
|
|
499
|
+
Stale stages pending acknowledgement: $STALE_STAGES (use /cc-rewind-ack <stage> after redo)."
|
|
500
|
+
fi
|
|
455
501
|
if [ -n "$KNOWLEDGE_DIGEST" ]; then
|
|
456
502
|
CTX="$CTX
|
|
457
503
|
Knowledge digest (top relevant entries):
|
|
@@ -3,7 +3,7 @@ export const META_SKILL_NAME = "using-cclaw";
|
|
|
3
3
|
export function usingCclawSkillMarkdown() {
|
|
4
4
|
return `---
|
|
5
5
|
name: using-cclaw
|
|
6
|
-
description: "Routing brain for cclaw. Decide whether to start/resume a stage, answer directly, or use /cc-learn."
|
|
6
|
+
description: "Routing brain for cclaw. Decide whether to start/resume a stage, answer directly, or use utility commands like /cc-learn, /cc-status, /cc-tree, and /cc-diff."
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Using Cclaw
|
|
@@ -26,7 +26,8 @@ Task arrives
|
|
|
26
26
|
├─ Pure question / non-software ask? -> answer directly (no stage)
|
|
27
27
|
├─ New software work? -> /cc <idea>
|
|
28
28
|
├─ Resume existing flow? -> /cc or /cc-next
|
|
29
|
-
|
|
29
|
+
├─ Knowledge operation? -> /cc-learn
|
|
30
|
+
└─ Workspace operation? -> /cc-status, /cc-tree, /cc-diff, /cc-feature, /cc-tdd-log, /cc-retro, /cc-rewind
|
|
30
31
|
\`\`\`
|
|
31
32
|
|
|
32
33
|
## Task classification
|
|
@@ -39,11 +39,12 @@ This is the only progression command the user needs to drive the entire flow. St
|
|
|
39
39
|
|
|
40
40
|
1. Read **\`${flowPath}\`**. If missing → **BLOCKED** (state missing).
|
|
41
41
|
2. Parse JSON. Capture \`currentStage\` and \`stageGateCatalog[currentStage]\`.
|
|
42
|
-
3.
|
|
43
|
-
4. Let \`
|
|
44
|
-
5.
|
|
45
|
-
6.
|
|
46
|
-
7.
|
|
42
|
+
3. If \`staleStages[currentStage]\` exists, do not advance automatically. Re-run the stage artifact work, then clear the marker with \`/cc-rewind-ack <currentStage>\`.
|
|
43
|
+
4. Let \`G\` = \`requiredGates\` for **\`currentStage\`** from the stage schema.
|
|
44
|
+
5. Let \`catalog\` = \`stageGateCatalog[currentStage]\` from flow state.
|
|
45
|
+
6. **Satisfied** for gate id \`g\`: \`g\` in \`catalog.passed\` and \`g\` not in \`catalog.blocked\`.
|
|
46
|
+
7. Let \`M\` = \`mandatoryDelegations\` for \`currentStage\`.
|
|
47
|
+
8. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if the agent is **completed** or **waived**.
|
|
47
48
|
|
|
48
49
|
### Path A: Current stage is NOT complete (any gate unmet or delegation missing)
|
|
49
50
|
|
|
@@ -120,7 +121,8 @@ Do **not** mark gates satisfied from memory alone. Cite **artifact evidence** (p
|
|
|
120
121
|
|
|
121
122
|
1. Open **\`${flowPath}\`**.
|
|
122
123
|
2. Record \`currentStage\` and \`stageGateCatalog[currentStage]\`.
|
|
123
|
-
3. If
|
|
124
|
+
3. If \`staleStages[currentStage]\` exists, re-run the stage and clear marker via \`/cc-rewind-ack <currentStage>\` before advancing.
|
|
125
|
+
4. If the file is missing or invalid JSON → **BLOCKED** (report and stop).
|
|
124
126
|
|
|
125
127
|
### Step 2: Evaluate gates
|
|
126
128
|
|
|
@@ -9,7 +9,11 @@ export interface PromptGuardOptions {
|
|
|
9
9
|
strictMode?: boolean;
|
|
10
10
|
}
|
|
11
11
|
export declare function promptGuardScript(options?: PromptGuardOptions): string;
|
|
12
|
-
export
|
|
12
|
+
export interface WorkflowGuardOptions {
|
|
13
|
+
tddEnforcementMode?: "advisory" | "strict";
|
|
14
|
+
tddTestGlobs?: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function workflowGuardScript(options?: WorkflowGuardOptions): string;
|
|
13
17
|
export declare function observeScript(): string;
|
|
14
18
|
export declare function contextMonitorScript(): string;
|
|
15
19
|
export declare function summarizeObservationsRuntimeModule(): string;
|
package/dist/content/observe.js
CHANGED
|
@@ -153,18 +153,25 @@ fi
|
|
|
153
153
|
exit 0
|
|
154
154
|
`;
|
|
155
155
|
}
|
|
156
|
-
export function workflowGuardScript() {
|
|
156
|
+
export function workflowGuardScript(options = {}) {
|
|
157
|
+
const tddEnforcementMode = options.tddEnforcementMode === "strict" ? "strict" : "advisory";
|
|
158
|
+
const tddTestGlobs = options.tddTestGlobs && options.tddTestGlobs.length > 0
|
|
159
|
+
? options.tddTestGlobs.join(",")
|
|
160
|
+
: "**/*.test.*,**/*.spec.*,**/test/**";
|
|
157
161
|
return `#!/usr/bin/env bash
|
|
158
162
|
# cclaw workflow guard hook — generated by cclaw sync
|
|
159
163
|
# Enforces stage-aware command discipline and recent flow-state read hygiene.
|
|
160
164
|
set -uo pipefail
|
|
161
165
|
WORKFLOW_GUARD_MODE="\${CCLAW_WORKFLOW_GUARD_MODE:-advisory}"
|
|
162
166
|
MAX_FLOW_READ_AGE_SEC="\${CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC:-1800}"
|
|
167
|
+
TDD_ENFORCEMENT_MODE="${tddEnforcementMode}"
|
|
168
|
+
TDD_TEST_GLOBS="${tddTestGlobs}"
|
|
163
169
|
|
|
164
170
|
${RUNTIME_SHELL_DETECT_ROOT}
|
|
165
171
|
|
|
166
172
|
STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
|
|
167
173
|
FLOW_STATE_FILE="$STATE_DIR/flow-state.json"
|
|
174
|
+
TDD_LOG_FILE="$STATE_DIR/tdd-cycle-log.jsonl"
|
|
168
175
|
GUARD_STATE_FILE="$STATE_DIR/workflow-guard.json"
|
|
169
176
|
GUARD_LOG="$STATE_DIR/workflow-guard.jsonl"
|
|
170
177
|
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
@@ -234,9 +241,11 @@ NOW_EPOCH=$(date +%s 2>/dev/null || echo "0")
|
|
|
234
241
|
REASONS=""
|
|
235
242
|
|
|
236
243
|
CURRENT_STAGE="none"
|
|
244
|
+
CURRENT_RUN="active"
|
|
237
245
|
if [ -f "$FLOW_STATE_FILE" ]; then
|
|
238
246
|
if command -v jq >/dev/null 2>&1; then
|
|
239
247
|
CURRENT_STAGE=$(jq -r '.currentStage // "none"' "$FLOW_STATE_FILE" 2>/dev/null || echo "none")
|
|
248
|
+
CURRENT_RUN=$(jq -r '.activeRunId // "active"' "$FLOW_STATE_FILE" 2>/dev/null || echo "active")
|
|
240
249
|
elif command -v python3 >/dev/null 2>&1; then
|
|
241
250
|
CURRENT_STAGE=$(python3 - "$FLOW_STATE_FILE" <<'PY'
|
|
242
251
|
import json
|
|
@@ -252,6 +261,21 @@ except Exception:
|
|
|
252
261
|
pass
|
|
253
262
|
print(stage)
|
|
254
263
|
PY
|
|
264
|
+
)
|
|
265
|
+
CURRENT_RUN=$(python3 - "$FLOW_STATE_FILE" <<'PY'
|
|
266
|
+
import json
|
|
267
|
+
import sys
|
|
268
|
+
run_id = "active"
|
|
269
|
+
try:
|
|
270
|
+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
271
|
+
parsed = json.load(fh)
|
|
272
|
+
value = parsed.get("activeRunId")
|
|
273
|
+
if isinstance(value, str) and value:
|
|
274
|
+
run_id = value
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
print(run_id)
|
|
278
|
+
PY
|
|
255
279
|
)
|
|
256
280
|
fi
|
|
257
281
|
fi
|
|
@@ -325,6 +349,99 @@ is_preimplementation_stage() {
|
|
|
325
349
|
esac
|
|
326
350
|
}
|
|
327
351
|
|
|
352
|
+
is_tdd_test_payload() {
|
|
353
|
+
local text="$1"
|
|
354
|
+
if printf '%s' "$text" | grep -Eq '/tests?/|\\.test\\.|\\.spec\\.'; then
|
|
355
|
+
return 0
|
|
356
|
+
fi
|
|
357
|
+
if printf '%s' "$TDD_TEST_GLOBS" | grep -Eq '.' && printf '%s' "$text" | grep -Eq '(test|spec)'; then
|
|
358
|
+
return 0
|
|
359
|
+
fi
|
|
360
|
+
return 1
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
is_tdd_runtime_write_payload() {
|
|
364
|
+
local text="$1"
|
|
365
|
+
if printf '%s' "$text" | grep -Eq '\\.cclaw/'; then
|
|
366
|
+
return 1
|
|
367
|
+
fi
|
|
368
|
+
if ! printf '%s' "$text" | grep -Eq '\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)'; then
|
|
369
|
+
return 1
|
|
370
|
+
fi
|
|
371
|
+
if is_tdd_test_payload "$text"; then
|
|
372
|
+
return 1
|
|
373
|
+
fi
|
|
374
|
+
return 0
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
has_open_red_cycle() {
|
|
378
|
+
if [ ! -f "$TDD_LOG_FILE" ] || [ ! -s "$TDD_LOG_FILE" ]; then
|
|
379
|
+
return 1
|
|
380
|
+
fi
|
|
381
|
+
local red_count="0"
|
|
382
|
+
local green_count="0"
|
|
383
|
+
if command -v jq >/dev/null 2>&1; then
|
|
384
|
+
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")
|
|
385
|
+
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")
|
|
386
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
387
|
+
red_count=$(python3 - "$TDD_LOG_FILE" "$CURRENT_RUN" <<'PY'
|
|
388
|
+
import json
|
|
389
|
+
import sys
|
|
390
|
+
count = 0
|
|
391
|
+
run_id = sys.argv[2]
|
|
392
|
+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
393
|
+
for raw in fh:
|
|
394
|
+
raw = raw.strip()
|
|
395
|
+
if not raw:
|
|
396
|
+
continue
|
|
397
|
+
try:
|
|
398
|
+
parsed = json.loads(raw)
|
|
399
|
+
except Exception:
|
|
400
|
+
continue
|
|
401
|
+
if not isinstance(parsed, dict):
|
|
402
|
+
continue
|
|
403
|
+
if str(parsed.get("runId", run_id)) != run_id:
|
|
404
|
+
continue
|
|
405
|
+
if parsed.get("phase") == "red":
|
|
406
|
+
count += 1
|
|
407
|
+
print(count)
|
|
408
|
+
PY
|
|
409
|
+
)
|
|
410
|
+
green_count=$(python3 - "$TDD_LOG_FILE" "$CURRENT_RUN" <<'PY'
|
|
411
|
+
import json
|
|
412
|
+
import sys
|
|
413
|
+
count = 0
|
|
414
|
+
run_id = sys.argv[2]
|
|
415
|
+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
416
|
+
for raw in fh:
|
|
417
|
+
raw = raw.strip()
|
|
418
|
+
if not raw:
|
|
419
|
+
continue
|
|
420
|
+
try:
|
|
421
|
+
parsed = json.loads(raw)
|
|
422
|
+
except Exception:
|
|
423
|
+
continue
|
|
424
|
+
if not isinstance(parsed, dict):
|
|
425
|
+
continue
|
|
426
|
+
if str(parsed.get("runId", run_id)) != run_id:
|
|
427
|
+
continue
|
|
428
|
+
if parsed.get("phase") == "green":
|
|
429
|
+
count += 1
|
|
430
|
+
print(count)
|
|
431
|
+
PY
|
|
432
|
+
)
|
|
433
|
+
else
|
|
434
|
+
red_count=$(grep -ci '"phase"[[:space:]]*:[[:space:]]*"red"' "$TDD_LOG_FILE" 2>/dev/null || echo "0")
|
|
435
|
+
green_count=$(grep -ci '"phase"[[:space:]]*:[[:space:]]*"green"' "$TDD_LOG_FILE" 2>/dev/null || echo "0")
|
|
436
|
+
fi
|
|
437
|
+
[ -n "$red_count" ] || red_count="0"
|
|
438
|
+
[ -n "$green_count" ] || green_count="0"
|
|
439
|
+
if [ "$red_count" -gt "$green_count" ]; then
|
|
440
|
+
return 0
|
|
441
|
+
fi
|
|
442
|
+
return 1
|
|
443
|
+
}
|
|
444
|
+
|
|
328
445
|
detect_target_stage() {
|
|
329
446
|
local text="$1"
|
|
330
447
|
for stage in brainstorm scope design spec plan tdd review ship; do
|
|
@@ -373,6 +490,18 @@ if is_preimplementation_stage "$CURRENT_STAGE" && is_mutating_tool "$TOOL_LOWER"
|
|
|
373
490
|
fi
|
|
374
491
|
fi
|
|
375
492
|
|
|
493
|
+
if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
|
|
494
|
+
if is_tdd_runtime_write_payload "$PAYLOAD_LOWER"; then
|
|
495
|
+
if ! has_open_red_cycle; then
|
|
496
|
+
if [ -n "$REASONS" ]; then
|
|
497
|
+
REASONS="$REASONS,tdd_write_without_open_red"
|
|
498
|
+
else
|
|
499
|
+
REASONS="tdd_write_without_open_red"
|
|
500
|
+
fi
|
|
501
|
+
fi
|
|
502
|
+
fi
|
|
503
|
+
fi
|
|
504
|
+
|
|
376
505
|
if is_preimplementation_stage "$CURRENT_STAGE" && ! is_plan_mode_safe_tool "$TOOL_LOWER"; then
|
|
377
506
|
if ! is_mutating_tool "$TOOL_LOWER"; then
|
|
378
507
|
if ! printf '%s' "$PAYLOAD_LOWER" | grep -Eq '\.cclaw/' && ! is_cclaw_cli_payload "$PAYLOAD_LOWER"; then
|
|
@@ -438,7 +567,7 @@ PY
|
|
|
438
567
|
fi
|
|
439
568
|
|
|
440
569
|
if [ -n "$REASONS" ]; then
|
|
441
|
-
NOTE="Cclaw workflow guard: detected potential flow violation (\${REASONS}). Re-read ${RUNTIME_ROOT}/state/flow-state.json, avoid source edits before tdd stage, and
|
|
570
|
+
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."
|
|
442
571
|
if command -v jq >/dev/null 2>&1; then
|
|
443
572
|
ENTRY=$(jq -n -c \
|
|
444
573
|
--arg ts "$TS" \
|
|
@@ -458,6 +587,9 @@ if [ -n "$REASONS" ]; then
|
|
|
458
587
|
if printf '%s' "$REASONS" | grep -Eq 'implementation_write_before_'; then
|
|
459
588
|
SHOULD_BLOCK="true"
|
|
460
589
|
fi
|
|
590
|
+
if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red' && [ "$TDD_ENFORCEMENT_MODE" = "strict" ]; then
|
|
591
|
+
SHOULD_BLOCK="true"
|
|
592
|
+
fi
|
|
461
593
|
if [ "$WORKFLOW_GUARD_MODE" = "strict" ] || [ "$SHOULD_BLOCK" = "true" ]; then
|
|
462
594
|
printf '[cclaw] %s (blocked by workflow guard)\n' "$NOTE" >&2
|
|
463
595
|
exit 1
|
|
@@ -78,14 +78,42 @@ Before adding new code/templates/rules:
|
|
|
78
78
|
- Evidence beats volume.
|
|
79
79
|
- Keep stage output concrete and testable.
|
|
80
80
|
|
|
81
|
-
## Preamble
|
|
81
|
+
## Preamble budget
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- stage transition,
|
|
86
|
-
- or multi-step operation where drift risk is real.
|
|
83
|
+
This section is the single source of truth for preamble behavior.
|
|
84
|
+
Do not duplicate preamble rules in AGENTS.md, harness adapters, or stage-local docs.
|
|
87
85
|
|
|
88
|
-
|
|
86
|
+
### Emit when
|
|
87
|
+
|
|
88
|
+
| Trigger | Machine-verifiable condition |
|
|
89
|
+
|---|---|
|
|
90
|
+
| Stage transition | \`flow-state.currentStage\` changes in this turn |
|
|
91
|
+
| Non-trivial implementation turn | agent is about to run source-editing tools outside \`.cclaw/\` |
|
|
92
|
+
| Multi-step risky operation | planned sequence contains 2+ commands with rollback/risk potential |
|
|
93
|
+
|
|
94
|
+
### Skip when
|
|
95
|
+
|
|
96
|
+
| Skip reason | Condition |
|
|
97
|
+
|---|---|
|
|
98
|
+
| Pure Q&A | no filesystem or runtime mutation planned |
|
|
99
|
+
| Trivial change | single low-risk edit with no stage or plan drift |
|
|
100
|
+
| Subagent dispatch payload | prompt is for spawned agent/tool call only |
|
|
101
|
+
| Cooldown hit | same stage + same trigger emitted within cooldown window |
|
|
102
|
+
|
|
103
|
+
### Form contract (max 4 lines)
|
|
104
|
+
|
|
105
|
+
1. \`Stage:\` current stage id
|
|
106
|
+
2. \`Goal:\` concrete objective for this turn
|
|
107
|
+
3. \`Plan:\` next 1-3 actions
|
|
108
|
+
4. \`Guardrails:\` key constraints / non-goals
|
|
109
|
+
|
|
110
|
+
### Cooldown
|
|
111
|
+
|
|
112
|
+
- Record each emitted preamble in \`.cclaw/state/preamble-log.jsonl\` as JSON line:
|
|
113
|
+
\`{"ts","stage","runId","trigger","hash"}\`.
|
|
114
|
+
- Default cooldown: 15 minutes for identical \`stage + trigger + hash\`.
|
|
115
|
+
- TDD wave mode uses stricter dedupe: one preamble per wave unless scope changes.
|
|
116
|
+
- If the plan changes materially, a new preamble is allowed inside cooldown.
|
|
89
117
|
|
|
90
118
|
## Operational learning
|
|
91
119
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-thread research playbooks.
|
|
3
|
+
*
|
|
4
|
+
* These files intentionally have no YAML frontmatter and are not standalone
|
|
5
|
+
* delegated personas. The primary agent loads and executes them directly.
|
|
6
|
+
*/
|
|
7
|
+
export declare const RESEARCH_PLAYBOOKS: Record<string, string>;
|
|
8
|
+
export declare const RESEARCH_PLAYBOOK_FILES: string[];
|