continuous-improvement 3.23.0 → 3.25.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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +37 -0
- package/QUICKSTART.md +19 -20
- package/README.md +71 -18
- package/SKILL.md +4 -0
- package/bin/check-invariant-count.mjs +144 -0
- package/bin/check-routing-targets.mjs +74 -4
- package/bin/check-test-count.mjs +135 -0
- package/bin/companion-preference-status.mjs +2 -5
- package/bin/generate-plugin-manifests.mjs +21 -2
- package/bin/harvest-friction.mjs +10 -8
- package/bin/install.mjs +4 -14
- package/bin/mcp-server.mjs +4 -9
- package/bin/observe.mjs +3 -3
- package/bin/reconcile-instinct-hashes.mjs +226 -0
- package/bin/refresh-third-party.mjs +180 -10
- package/commands/discipline.md +5 -2
- package/commands/reconcile.md +1 -1
- package/commands/superpowers.md +2 -2
- package/commands/verify-install.md +8 -3
- package/hooks/companion-preference.mjs +3 -10
- package/hooks/config-guard.mjs +94 -0
- package/hooks/gateguard.mjs +39 -39
- package/hooks/goal-drift-stop.mjs +2 -2
- package/hooks/query-cost-nudge.mjs +2 -2
- package/hooks/recall-briefing.mjs +2 -2
- package/hooks/route-prompt.mjs +2 -5
- package/hooks/session.mjs +2 -2
- package/hooks/workflow-distill.mjs +2 -2
- package/lib/config-guard-gate.mjs +243 -0
- package/lib/destructive-bash.mjs +216 -0
- package/lib/gateguard-state.mjs +5 -1
- package/lib/plugin-metadata.mjs +12 -1
- package/lib/skill-catalog.mjs +169 -0
- package/llms.txt +12 -1
- package/package.json +5 -3
- package/plugins/beginner.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
- package/plugins/continuous-improvement/README.md +1 -2
- package/plugins/continuous-improvement/bin/mcp-server.mjs +4 -9
- package/plugins/continuous-improvement/bin/observe.mjs +3 -3
- package/plugins/continuous-improvement/commands/discipline.md +5 -2
- package/plugins/continuous-improvement/commands/reconcile.md +1 -1
- package/plugins/continuous-improvement/commands/superpowers.md +2 -2
- package/plugins/continuous-improvement/commands/verify-install.md +8 -3
- package/plugins/continuous-improvement/hooks/companion-preference.mjs +3 -10
- package/plugins/continuous-improvement/hooks/config-guard.mjs +94 -0
- package/plugins/continuous-improvement/hooks/gateguard.mjs +39 -39
- package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +2 -2
- package/plugins/continuous-improvement/hooks/hooks.json +10 -0
- package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +2 -2
- package/plugins/continuous-improvement/hooks/recall-briefing.mjs +2 -2
- package/plugins/continuous-improvement/hooks/route-prompt.mjs +2 -5
- package/plugins/continuous-improvement/hooks/session.mjs +2 -2
- package/plugins/continuous-improvement/hooks/workflow-distill.mjs +2 -2
- package/plugins/continuous-improvement/lib/config-guard-gate.mjs +243 -0
- package/plugins/continuous-improvement/lib/destructive-bash.mjs +216 -0
- package/plugins/continuous-improvement/lib/gateguard-state.mjs +5 -1
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +12 -1
- package/plugins/continuous-improvement/scripts/route-recommendation.routes.json +2 -2
- package/plugins/continuous-improvement/skills/README.md +0 -1
- package/plugins/continuous-improvement/skills/continuous-improvement/SKILL.md +4 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +17 -2
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +0 -1
- package/plugins/continuous-improvement/skills/superpowers/SKILL.md +5 -6
- package/plugins/expert.json +1 -1
- package/scripts/route-recommendation.routes.json +2 -2
- package/skills/README.md +1 -2
- package/skills/deploy-receipt.md +1 -1
- package/skills/gateguard.md +17 -2
- package/skills/proceed-with-the-recommendation.md +2 -2
- package/skills/reconcile.md +0 -1
- package/skills/superpowers.md +5 -6
- package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +0 -77
- package/skills/safety-guard.md +0 -77
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// config-guard-gate.mts — Pure decision core for the config-guard PreToolUse hook.
|
|
2
|
+
//
|
|
3
|
+
// The guardrails this plugin ships are wired by a handful of files: the
|
|
4
|
+
// Claude Code settings files, the MCP config, hooks.json, the user hooks
|
|
5
|
+
// directory, the installed-plugins cache, and plugin manifests. Nothing stopped
|
|
6
|
+
// an agent from editing those files and switching every gate off, and
|
|
7
|
+
// hooks/gateguard.mjs is designed to be cleared per file, not to hard-deny. This
|
|
8
|
+
// module decides whether a tool call would mutate one of them. No I/O: the hook
|
|
9
|
+
// (src/hooks/config-guard.mts) wires stdin and the mode env var around it, so a
|
|
10
|
+
// table test covers every branch without spawning anything. Lives in lib/ so
|
|
11
|
+
// tests can import it (the test-imports-only invariant forbids hooks/).
|
|
12
|
+
//
|
|
13
|
+
// Idea ported from karanb192/claude-code-hooks `config-guard` (MIT); the code
|
|
14
|
+
// and the pattern list are ours. Fail-open by construction: an unrecognized mode
|
|
15
|
+
// resolves to warn, and a call that names no protected path is always allowed.
|
|
16
|
+
/**
|
|
17
|
+
* Path patterns, matched case-insensitively on the forward-slash form.
|
|
18
|
+
* A trailing "/" means a directory component anywhere in the path; otherwise
|
|
19
|
+
* the pattern must be the whole path or a trailing path segment, so
|
|
20
|
+
* `hooks.json.md` and `config/settings.json` are not matched.
|
|
21
|
+
*/
|
|
22
|
+
export const PROTECTED_PATTERNS = [
|
|
23
|
+
".claude/settings.json",
|
|
24
|
+
".claude/settings.local.json",
|
|
25
|
+
".mcp.json",
|
|
26
|
+
"hooks.json",
|
|
27
|
+
".claude/hooks/",
|
|
28
|
+
".claude/plugins/",
|
|
29
|
+
".claude-plugin/",
|
|
30
|
+
];
|
|
31
|
+
const ALLOW = { action: "allow", reason: "" };
|
|
32
|
+
export function parseMode(raw) {
|
|
33
|
+
const v = (raw ?? "warn").trim().toLowerCase();
|
|
34
|
+
return v === "block" || v === "off" ? v : "warn";
|
|
35
|
+
}
|
|
36
|
+
function normalizePath(p) {
|
|
37
|
+
return p.replace(/\\/g, "/").toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
export function matchProtectedPath(filePath) {
|
|
40
|
+
if (typeof filePath !== "string" || filePath === "")
|
|
41
|
+
return null;
|
|
42
|
+
const n = normalizePath(filePath);
|
|
43
|
+
for (const pattern of PROTECTED_PATTERNS) {
|
|
44
|
+
const lp = pattern.toLowerCase();
|
|
45
|
+
if (lp.endsWith("/")) {
|
|
46
|
+
if (n.includes(lp))
|
|
47
|
+
return pattern;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (n === lp || n.endsWith(`/${lp}`))
|
|
51
|
+
return pattern;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
// --- file tools ------------------------------------------------------------
|
|
56
|
+
function fileToolPaths(toolInput) {
|
|
57
|
+
const paths = [];
|
|
58
|
+
if (typeof toolInput.file_path === "string")
|
|
59
|
+
paths.push(toolInput.file_path);
|
|
60
|
+
if (typeof toolInput.notebook_path === "string")
|
|
61
|
+
paths.push(toolInput.notebook_path);
|
|
62
|
+
if (Array.isArray(toolInput.edits)) {
|
|
63
|
+
for (const edit of toolInput.edits) {
|
|
64
|
+
if (edit && typeof edit === "object" && typeof edit.file_path === "string") {
|
|
65
|
+
paths.push(edit.file_path);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return paths;
|
|
70
|
+
}
|
|
71
|
+
const FILE_MUTATING_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
72
|
+
// --- Bash --------------------------------------------------------------------
|
|
73
|
+
// Split at shell separators that sit outside single or double quotes.
|
|
74
|
+
function splitSimpleCommands(command) {
|
|
75
|
+
const segments = [];
|
|
76
|
+
let current = "";
|
|
77
|
+
let quote = null;
|
|
78
|
+
for (let i = 0; i < command.length; i++) {
|
|
79
|
+
const ch = command[i];
|
|
80
|
+
if (quote) {
|
|
81
|
+
current += ch;
|
|
82
|
+
if (ch === quote)
|
|
83
|
+
quote = null;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (ch === '"' || ch === "'") {
|
|
87
|
+
quote = ch;
|
|
88
|
+
current += ch;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (ch === "\n" || ch === ";") {
|
|
92
|
+
segments.push(current);
|
|
93
|
+
current = "";
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (ch === "|" || ch === "&") {
|
|
97
|
+
if (command[i + 1] === ch)
|
|
98
|
+
i++;
|
|
99
|
+
segments.push(current);
|
|
100
|
+
current = "";
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
current += ch;
|
|
104
|
+
}
|
|
105
|
+
segments.push(current);
|
|
106
|
+
return segments.map((s) => s.trim()).filter((s) => s !== "");
|
|
107
|
+
}
|
|
108
|
+
function tokenize(segment) {
|
|
109
|
+
return segment.split(/\s+/).filter((t) => t !== "");
|
|
110
|
+
}
|
|
111
|
+
function stripQuotes(token) {
|
|
112
|
+
return token.replace(/^['"]+|['"]+$/g, "");
|
|
113
|
+
}
|
|
114
|
+
function commandWord(tokens) {
|
|
115
|
+
let i = 0;
|
|
116
|
+
while (i < tokens.length) {
|
|
117
|
+
const t = tokens[i];
|
|
118
|
+
if (t === "sudo" || t === "env" || t === "command" || /^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) {
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return { cmd: (tokens[i] ?? "").toLowerCase(), rest: tokens.slice(i + 1) };
|
|
125
|
+
}
|
|
126
|
+
const COPY_MOVE = new Set(["cp", "mv", "copy-item", "move-item"]);
|
|
127
|
+
const OPERAND_WRITERS = new Set([
|
|
128
|
+
"rm",
|
|
129
|
+
"tee",
|
|
130
|
+
"truncate",
|
|
131
|
+
"remove-item",
|
|
132
|
+
"del",
|
|
133
|
+
"set-content",
|
|
134
|
+
"out-file",
|
|
135
|
+
"add-content",
|
|
136
|
+
"clear-content",
|
|
137
|
+
]);
|
|
138
|
+
// Mutation indicators for the generic scan (a protected path mentioned inside
|
|
139
|
+
// a scripting one-liner such as `node -e "writeFileSync('.claude/settings.json')"`).
|
|
140
|
+
const GENERIC_WRITE_RE = /writefilesync|writefile\(|appendfile|unlink|rename\(|copyfile|rmsync|truncate|set-content|out-file|add-content|remove-item|\bdel\b|\bsed\s+-i\b|\btee\b|>{1,2}/i;
|
|
141
|
+
const CLAUDE_CLI_MUTATIONS = {
|
|
142
|
+
plugin: new Set(["install", "uninstall", "enable", "disable", "update", "marketplace"]),
|
|
143
|
+
mcp: new Set(["add", "remove", "add-json", "add-from-claude-desktop", "reset-project-choices"]),
|
|
144
|
+
config: new Set(["set", "add", "remove", "reset", "rm"]),
|
|
145
|
+
};
|
|
146
|
+
function classifyBashSegment(segment) {
|
|
147
|
+
const tokens = tokenize(segment);
|
|
148
|
+
if (tokens.length === 0)
|
|
149
|
+
return null;
|
|
150
|
+
const { cmd, rest } = commandWord(tokens);
|
|
151
|
+
// `claude plugin|mcp|config <mutating-verb>`: the CLI edits the same files.
|
|
152
|
+
if (cmd === "claude") {
|
|
153
|
+
const area = (rest[0] ?? "").toLowerCase();
|
|
154
|
+
const verb = (rest[1] ?? "").toLowerCase();
|
|
155
|
+
const verbs = CLAUDE_CLI_MUTATIONS[area];
|
|
156
|
+
if (verbs && verbs.has(verb)) {
|
|
157
|
+
return { target: `claude ${area} ${verb}`, pattern: "claude-cli", via: "claude-cli" };
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
// cp / mv: only the destination (last operand) can be a protected write.
|
|
162
|
+
if (COPY_MOVE.has(cmd)) {
|
|
163
|
+
const operands = rest.filter((t) => !t.startsWith("-"));
|
|
164
|
+
const dest = operands[operands.length - 1];
|
|
165
|
+
const pattern = dest ? matchProtectedPath(stripQuotes(dest)) : null;
|
|
166
|
+
return pattern && dest ? { target: stripQuotes(dest), pattern, via: "bash" } : null;
|
|
167
|
+
}
|
|
168
|
+
// Redirects: `> path`, `>> path`, `2>path`, `>path`.
|
|
169
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
170
|
+
const t = tokens[i];
|
|
171
|
+
const m = /^(\d?>{1,2})(.*)$/.exec(t);
|
|
172
|
+
if (!m)
|
|
173
|
+
continue;
|
|
174
|
+
const target = m[2] !== "" ? m[2] : (tokens[i + 1] ?? "");
|
|
175
|
+
const pattern = matchProtectedPath(stripQuotes(target));
|
|
176
|
+
if (pattern)
|
|
177
|
+
return { target: stripQuotes(target), pattern, via: "bash" };
|
|
178
|
+
}
|
|
179
|
+
// Operand writers: any operand that is a protected path.
|
|
180
|
+
const sedInPlace = cmd === "sed" && rest.some((t) => /^-[a-zA-Z]*i/.test(t) || t === "--in-place");
|
|
181
|
+
if (OPERAND_WRITERS.has(cmd) || sedInPlace) {
|
|
182
|
+
for (const t of rest) {
|
|
183
|
+
if (t.startsWith("-"))
|
|
184
|
+
continue;
|
|
185
|
+
const clean = stripQuotes(t);
|
|
186
|
+
const pattern = matchProtectedPath(clean);
|
|
187
|
+
if (pattern)
|
|
188
|
+
return { target: clean, pattern, via: "bash" };
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
// Generic scan: a protected path quoted inside a scripting one-liner, next
|
|
193
|
+
// to a write indicator. Reads (`cat`, `grep`) carry no indicator and pass.
|
|
194
|
+
if (GENERIC_WRITE_RE.test(segment)) {
|
|
195
|
+
const candidates = [];
|
|
196
|
+
const quoted = /'([^']+)'|"([^"]+)"/g;
|
|
197
|
+
let q;
|
|
198
|
+
while ((q = quoted.exec(segment)) !== null)
|
|
199
|
+
candidates.push((q[1] ?? q[2]));
|
|
200
|
+
for (const t of tokens)
|
|
201
|
+
candidates.push(stripQuotes(t));
|
|
202
|
+
for (const c of candidates) {
|
|
203
|
+
const inner = /['"]([^'"]*?)['"]/.exec(c);
|
|
204
|
+
for (const candidate of [c, inner?.[1] ?? ""]) {
|
|
205
|
+
const pattern = matchProtectedPath(candidate);
|
|
206
|
+
if (pattern)
|
|
207
|
+
return { target: candidate, pattern, via: "bash" };
|
|
208
|
+
}
|
|
209
|
+
// A path embedded in a longer expression, e.g. writeFileSync('.claude/settings.json','{}')
|
|
210
|
+
const embedded = /(?:^|[('"\s=,])([~A-Za-z0-9_./\\:-]*(?:\.claude\/|\.claude-plugin\/|\.mcp\.json|hooks\.json)[A-Za-z0-9_./\\-]*)/i.exec(c);
|
|
211
|
+
if (embedded) {
|
|
212
|
+
const pattern = matchProtectedPath(embedded[1]);
|
|
213
|
+
if (pattern)
|
|
214
|
+
return { target: embedded[1], pattern, via: "bash" };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
/** Decide whether a tool call would mutate a guardrail-wiring file. Pure. */
|
|
221
|
+
export function classifyMutation(toolName, toolInput) {
|
|
222
|
+
if (FILE_MUTATING_TOOLS.has(toolName)) {
|
|
223
|
+
for (const p of fileToolPaths(toolInput)) {
|
|
224
|
+
const pattern = matchProtectedPath(p);
|
|
225
|
+
if (pattern)
|
|
226
|
+
return { target: p, pattern, via: "file" };
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
if (toolName === "Bash" && typeof toolInput.command === "string" && toolInput.command.trim() !== "") {
|
|
231
|
+
for (const segment of splitSimpleCommands(toolInput.command)) {
|
|
232
|
+
const hit = classifyBashSegment(segment);
|
|
233
|
+
if (hit)
|
|
234
|
+
return hit;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
export function decide(mode, gated, reason) {
|
|
240
|
+
if (!gated || mode === "off")
|
|
241
|
+
return ALLOW;
|
|
242
|
+
return { action: mode === "block" ? "block" : "warn", reason };
|
|
243
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destructive-Bash classifier for hooks/gateguard.mjs.
|
|
3
|
+
*
|
|
4
|
+
* Two layers, pure and dependency-free so a table test and a future
|
|
5
|
+
* `gateguard-explain` CLI can drive it without spawning the hook:
|
|
6
|
+
*
|
|
7
|
+
* 1. Structured rules: the command is split at unquoted separators
|
|
8
|
+
* (`|`, `||`, `&&`, `;`, newline) and each simple command is tokenized,
|
|
9
|
+
* so flag order and spelling stop mattering. `rm -r -f`, `rm -Rf` and
|
|
10
|
+
* `rm --recursive --force` are the same command; `git clean -fdx` and
|
|
11
|
+
* `git clean --force -d` are the same command. Each rule has a stable id
|
|
12
|
+
* the deny reason prints, so a block is explainable.
|
|
13
|
+
* 2. The original substring list, kept verbatim as the fallback so nothing
|
|
14
|
+
* that was caught before stops being caught.
|
|
15
|
+
*
|
|
16
|
+
* What is deliberately NOT here: plain file writes through Bash (`cat > x`,
|
|
17
|
+
* `sed -i`). Gating those would break the operator workflow that uses Bash
|
|
18
|
+
* precisely to write files; that limit is disclosed on the site and README.
|
|
19
|
+
*
|
|
20
|
+
* Message-flag carve-out: the VALUE of a commit message, PR body, title, or
|
|
21
|
+
* note is prose, never a command. It is blanked before any rule runs, so
|
|
22
|
+
* `git commit -m "rm -rf the old helper"` stays allowed. `-c` is not a message
|
|
23
|
+
* flag: `bash -c "rm -rf /"` carries a real command and still gates.
|
|
24
|
+
*/
|
|
25
|
+
export const DESTRUCTIVE_PATTERNS = [
|
|
26
|
+
"rm -rf",
|
|
27
|
+
"rm -fr",
|
|
28
|
+
"git reset --hard",
|
|
29
|
+
"git push --force",
|
|
30
|
+
"git push -f",
|
|
31
|
+
"--force-with-lease",
|
|
32
|
+
"git branch -D",
|
|
33
|
+
"drop table",
|
|
34
|
+
"drop database",
|
|
35
|
+
"drop schema",
|
|
36
|
+
"truncate ",
|
|
37
|
+
"mkfs",
|
|
38
|
+
"dd if=",
|
|
39
|
+
"format ",
|
|
40
|
+
"rmdir /s",
|
|
41
|
+
"del /f /q",
|
|
42
|
+
"del /q /f",
|
|
43
|
+
"Remove-Item -Recurse",
|
|
44
|
+
"Remove-Item -Force",
|
|
45
|
+
];
|
|
46
|
+
// Flags whose VALUE is human prose (a commit message, a PR body) or a filename —
|
|
47
|
+
// never a command to execute. Their contents must not trip the destructive scan.
|
|
48
|
+
const MESSAGE_FLAG_RE = /(^|\s)(-m|--message|-F|--file|--body|--body-file|--title|--notes|-C|--reuse-message)(=|\s+)('[^']*'|"[^"]*"|\S+)/g;
|
|
49
|
+
/** Blank the value of every message/body flag; the flag itself is preserved. */
|
|
50
|
+
export function stripMessageArgs(command) {
|
|
51
|
+
return command.replace(MESSAGE_FLAG_RE, (_match, lead, flag) => `${lead}${flag} `);
|
|
52
|
+
}
|
|
53
|
+
// Split at shell separators that sit outside single or double quotes.
|
|
54
|
+
function splitSimpleCommands(command) {
|
|
55
|
+
const segments = [];
|
|
56
|
+
let current = "";
|
|
57
|
+
let quote = null;
|
|
58
|
+
for (let i = 0; i < command.length; i++) {
|
|
59
|
+
const ch = command[i];
|
|
60
|
+
if (quote) {
|
|
61
|
+
current += ch;
|
|
62
|
+
if (ch === quote)
|
|
63
|
+
quote = null;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === '"' || ch === "'") {
|
|
67
|
+
quote = ch;
|
|
68
|
+
current += ch;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === "\n" || ch === ";") {
|
|
72
|
+
segments.push(current);
|
|
73
|
+
current = "";
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (ch === "|" || ch === "&") {
|
|
77
|
+
// `||`, `&&`, `|`, `&` all end the simple command; collapse a doubled char.
|
|
78
|
+
if (command[i + 1] === ch)
|
|
79
|
+
i++;
|
|
80
|
+
segments.push(current);
|
|
81
|
+
current = "";
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
current += ch;
|
|
85
|
+
}
|
|
86
|
+
segments.push(current);
|
|
87
|
+
return segments.map((s) => s.trim()).filter((s) => s !== "");
|
|
88
|
+
}
|
|
89
|
+
function tokenize(segment) {
|
|
90
|
+
return segment.split(/\s+/).filter((t) => t !== "");
|
|
91
|
+
}
|
|
92
|
+
// Drop `sudo`, `env`, and leading VAR=value assignments so the command word is
|
|
93
|
+
// the first token the rules look at.
|
|
94
|
+
function commandTokens(segment) {
|
|
95
|
+
const tokens = tokenize(segment);
|
|
96
|
+
let i = 0;
|
|
97
|
+
while (i < tokens.length) {
|
|
98
|
+
const t = tokens[i];
|
|
99
|
+
if (t === "sudo" || t === "env" || t === "command" || /^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) {
|
|
100
|
+
i++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return tokens.slice(i);
|
|
106
|
+
}
|
|
107
|
+
// For `git`, skip global options (`-C <dir>`, `-c k=v`, `--git-dir=…`) to find
|
|
108
|
+
// the subcommand and its own arguments.
|
|
109
|
+
function gitSubcommand(tokens) {
|
|
110
|
+
if (tokens[0] !== "git")
|
|
111
|
+
return null;
|
|
112
|
+
let i = 1;
|
|
113
|
+
while (i < tokens.length && tokens[i].startsWith("-")) {
|
|
114
|
+
const t = tokens[i];
|
|
115
|
+
i += t === "-C" || t === "-c" || t === "--git-dir" || t === "--work-tree" ? 2 : 1;
|
|
116
|
+
}
|
|
117
|
+
const sub = tokens[i];
|
|
118
|
+
if (!sub)
|
|
119
|
+
return null;
|
|
120
|
+
return { sub, args: tokens.slice(i + 1) };
|
|
121
|
+
}
|
|
122
|
+
const isShortFlag = (t) => /^-[A-Za-z]+$/.test(t);
|
|
123
|
+
const hasShortLetter = (t, letters) => isShortFlag(t) && letters.test(t.slice(1));
|
|
124
|
+
const STRUCTURED_RULES = [
|
|
125
|
+
{
|
|
126
|
+
id: "rm-recursive-force",
|
|
127
|
+
test: (tokens) => {
|
|
128
|
+
if (tokens[0] !== "rm")
|
|
129
|
+
return false;
|
|
130
|
+
const flags = tokens.slice(1).filter((t) => t.startsWith("-"));
|
|
131
|
+
const recursive = flags.some((t) => t === "--recursive" || hasShortLetter(t, /[rR]/));
|
|
132
|
+
const force = flags.some((t) => t === "--force" || hasShortLetter(t, /f/));
|
|
133
|
+
return recursive && force;
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "git-clean-force",
|
|
138
|
+
test: (tokens) => {
|
|
139
|
+
const git = gitSubcommand(tokens);
|
|
140
|
+
if (!git || git.sub !== "clean")
|
|
141
|
+
return false;
|
|
142
|
+
const dryRun = git.args.some((t) => t === "--dry-run" || hasShortLetter(t, /n/));
|
|
143
|
+
const force = git.args.some((t) => t === "--force" || hasShortLetter(t, /f/));
|
|
144
|
+
return force && !dryRun;
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "git-checkout-discard",
|
|
149
|
+
test: (tokens) => {
|
|
150
|
+
const git = gitSubcommand(tokens);
|
|
151
|
+
if (!git || git.sub !== "checkout")
|
|
152
|
+
return false;
|
|
153
|
+
const dash = git.args.indexOf("--");
|
|
154
|
+
if (dash !== -1)
|
|
155
|
+
return git.args.length > dash + 1; // `git checkout -- <pathspec>` discards
|
|
156
|
+
const first = git.args.find((t) => !t.startsWith("-"));
|
|
157
|
+
return first === "." || first === "./";
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "git-restore-discard",
|
|
162
|
+
test: (tokens) => {
|
|
163
|
+
const git = gitSubcommand(tokens);
|
|
164
|
+
if (!git || git.sub !== "restore")
|
|
165
|
+
return false;
|
|
166
|
+
const pathspec = git.args.some((t) => !t.startsWith("-"));
|
|
167
|
+
if (!pathspec)
|
|
168
|
+
return false;
|
|
169
|
+
const staged = git.args.some((t) => t === "--staged" || hasShortLetter(t, /S/));
|
|
170
|
+
const worktree = git.args.some((t) => t === "--worktree" || hasShortLetter(t, /W/));
|
|
171
|
+
return !staged || worktree; // `--staged` alone only unstages
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "find-delete",
|
|
176
|
+
test: (tokens) => tokens[0] === "find" && tokens.includes("-delete"),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "git-push-plus-refspec",
|
|
180
|
+
test: (tokens) => {
|
|
181
|
+
const git = gitSubcommand(tokens);
|
|
182
|
+
if (!git || git.sub !== "push")
|
|
183
|
+
return false;
|
|
184
|
+
return git.args.some((t) => t.startsWith("+") && t.length > 1);
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "git-stash-drop",
|
|
189
|
+
test: (tokens) => {
|
|
190
|
+
const git = gitSubcommand(tokens);
|
|
191
|
+
if (!git || git.sub !== "stash")
|
|
192
|
+
return false;
|
|
193
|
+
return git.args[0] === "drop" || git.args[0] === "clear";
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
/** Classify one Bash command string. Pure; never throws on odd input. */
|
|
198
|
+
export function classifyDestructiveBash(command) {
|
|
199
|
+
if (typeof command !== "string" || command.trim() === "")
|
|
200
|
+
return { destructive: false, rule: null };
|
|
201
|
+
const stripped = stripMessageArgs(command);
|
|
202
|
+
for (const segment of splitSimpleCommands(stripped)) {
|
|
203
|
+
const tokens = commandTokens(segment);
|
|
204
|
+
if (tokens.length === 0)
|
|
205
|
+
continue;
|
|
206
|
+
for (const rule of STRUCTURED_RULES) {
|
|
207
|
+
if (rule.test(tokens))
|
|
208
|
+
return { destructive: true, rule: rule.id };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const lower = stripped.toLowerCase();
|
|
212
|
+
const hit = DESTRUCTIVE_PATTERNS.find((p) => lower.includes(p.toLowerCase()));
|
|
213
|
+
if (hit !== undefined)
|
|
214
|
+
return { destructive: true, rule: `substring:${hit}` };
|
|
215
|
+
return { destructive: false, rule: null };
|
|
216
|
+
}
|
|
@@ -45,7 +45,7 @@ export function resolveSessionDir(sessionId) {
|
|
|
45
45
|
if (fromEnv)
|
|
46
46
|
return fromEnv;
|
|
47
47
|
const projectRoot = resolveProjectRoot();
|
|
48
|
-
const projectHash =
|
|
48
|
+
const projectHash = hashProjectRoot(projectRoot);
|
|
49
49
|
const base = join(resolveInstinctsRoot(), projectHash);
|
|
50
50
|
const scoped = sanitizeSessionId(sessionId);
|
|
51
51
|
return scoped ? join(base, "sessions", scoped) : base;
|
|
@@ -160,6 +160,10 @@ export function canonicalizeProjectRoot(p) {
|
|
|
160
160
|
export function canonicalizeFileKey(p) {
|
|
161
161
|
return canonicalizePath(p);
|
|
162
162
|
}
|
|
163
|
+
/** SHA-256[:12] of the canonical project root. C:/x, c:/x, and C:\\x share a bucket. */
|
|
164
|
+
export function hashProjectRoot(projectRoot) {
|
|
165
|
+
return createHash("sha256").update(canonicalizeProjectRoot(projectRoot)).digest("hex").slice(0, 12);
|
|
166
|
+
}
|
|
163
167
|
export function isFileCleared(state, filePath) {
|
|
164
168
|
return canonicalizeFileKey(filePath) in state.cleared_files;
|
|
165
169
|
}
|
|
@@ -26,7 +26,7 @@ const KEYWORDS = [
|
|
|
26
26
|
"transcript-linter",
|
|
27
27
|
];
|
|
28
28
|
const CLAUDE_PLUGIN_CATEGORY = "productivity";
|
|
29
|
-
const SHARED_PLUGIN_DESCRIPTION = "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as
|
|
29
|
+
const SHARED_PLUGIN_DESCRIPTION = "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 28 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.";
|
|
30
30
|
// Four vendored upstream companions registered alongside the CI plugin.
|
|
31
31
|
// Each entry points at a pinned-SHA snapshot under third-party/<name>/.
|
|
32
32
|
// See third-party/MANIFEST.md for refresh recipes and per-snapshot
|
|
@@ -474,6 +474,11 @@ export function getPluginHooksConfig() {
|
|
|
474
474
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-pack.mjs\"",
|
|
475
475
|
timeout: hookTimeoutSeconds,
|
|
476
476
|
};
|
|
477
|
+
const configGuardCommand = {
|
|
478
|
+
type: "command",
|
|
479
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/config-guard.mjs\"",
|
|
480
|
+
timeout: hookTimeoutSeconds,
|
|
481
|
+
};
|
|
477
482
|
const observeCommand = {
|
|
478
483
|
type: "command",
|
|
479
484
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/bin/observe.mjs\"",
|
|
@@ -538,6 +543,12 @@ export function getPluginHooksConfig() {
|
|
|
538
543
|
// Edit/Read/etc. Warn-default (CLAUDE_CI_HOOKPACK_GATE) — never blocks
|
|
539
544
|
// until the operator opts in.
|
|
540
545
|
{ matcher: "Bash", hooks: [hookPackCommand] },
|
|
546
|
+
// config-guard watches the files that wire the guardrails
|
|
547
|
+
// (.claude/settings*.json, .mcp.json, hooks.json, .claude/hooks/,
|
|
548
|
+
// .claude/plugins/, .claude-plugin/) and the `claude plugin|mcp|config`
|
|
549
|
+
// CLI forms. Matcher keeps read-only tools off the hot path.
|
|
550
|
+
// Warn-default (CI_CONFIG_GUARD) — denies only under block.
|
|
551
|
+
{ matcher: "Bash|Edit|MultiEdit|Write|NotebookEdit", hooks: [configGuardCommand] },
|
|
541
552
|
],
|
|
542
553
|
PostToolUse: [{ hooks: [observeCommand] }],
|
|
543
554
|
UserPromptSubmit: [{ hooks: [routePromptCommand, recallBriefingCommand] }],
|
|
@@ -191,14 +191,14 @@
|
|
|
191
191
|
{
|
|
192
192
|
"name": "Multi-session retrospective across a sprint",
|
|
193
193
|
"patterns": ["retrospective", "sprint review", "multi.session review", "what worked.*what failed"],
|
|
194
|
-
"preferred": ["
|
|
194
|
+
"preferred": ["learn-eval"],
|
|
195
195
|
"fallback": "What worked / what failed / what to do differently / 3 ranked next moves.",
|
|
196
196
|
"marker": "oh-my-claudecode"
|
|
197
197
|
},
|
|
198
198
|
{
|
|
199
199
|
"name": "Long autonomous run with quality gates",
|
|
200
200
|
"patterns": ["long autonomous", "ultrawork", "quality gates? (between|per) iteration"],
|
|
201
|
-
"preferred": ["oh-my-claudecode:
|
|
201
|
+
"preferred": ["oh-my-claudecode:ultragoal", "ralph"],
|
|
202
202
|
"fallback": "PRD-shaped autonomous loop with verify-between-iterations.",
|
|
203
203
|
"marker": "oh-my-claudecode"
|
|
204
204
|
},
|
|
@@ -34,7 +34,6 @@ skill set on disk.
|
|
|
34
34
|
- `reconcile` — Enforces Law 1 (Research Before Executing) of the 7 Laws of AI Agent Discipline. Establishes git ground truth — branch, status, stashes, worktrees, ahead/behind — before any mutation, halts on protected or destructive operations, then carries the known-good state through to a landed PR: stage by filename, commit one concern, push the feature branch, verify the push landed, open the PR, and after the PR merges fast-forward the default branch and check it out.
|
|
35
35
|
- `recovery-classification` — Enforces Law 4 (Verify Before Reporting) of the 7 Laws of AI Agent Discipline. After any failure in the verification ladder or auto-loop, classify the failure class before retrying — provider, tool-schema, deterministic-policy, git, worktree, runtime — so retry-vs-pause-vs-self-heal-vs-stop is an intentional decision, not a generic 'try again'.
|
|
36
36
|
- `roast` — Enforces Law 1 (Research Before Executing) of the 7 Laws of AI Agent Discipline. Convene a 5-persona adversarial council (Contrarian, Expansionist, Logician, Researcher, Buyer) that attacks an idea from every angle, then a Judge returns one GO / RESHAPE / KILL verdict plus the cheapest 48-hour test to de-risk it — so you pressure-test an idea before sinking time into building the wrong thing.
|
|
37
|
-
- `safety-guard` — Enforces Law 3 (One Thing at a Time) of the 7 Laws of AI Agent Discipline by scoping edits to a directory and blocking destructive shell commands. Use this skill to prevent destructive operations when working on production systems or running agents autonomously.
|
|
38
37
|
- `simplicity-review` — Enforces Law 4 (Verify Before Reporting) of the 7 Laws of AI Agent Discipline. Reviews the current diff for over-engineering (code that could reuse an existing file, a stdlib or native feature, or fewer lines) and reports trim findings without touching code, so 'it works' is never mistaken for 'it is the minimum that works'.
|
|
39
38
|
- `skill-distillation` — Enforces Law 7 (Learn From Every Session) of the 7 Laws of AI Agent Discipline. Distills repeated successful tool sequences into reusable draft instincts, so a pattern that worked three times becomes a captured recipe instead of being re-derived from scratch every session.
|
|
40
39
|
- `state-reconciliation` — Enforces Law 4 (Verify Before Reporting) of the 7 Laws of AI Agent Discipline. Pre-dispatch invariant: reconcile DB-vs-disk-vs-memory state before any unit runs, so a stale flag, missing artifact, or out-of-sync row never re-dispatches a unit that already completed or never started.
|
|
@@ -42,6 +42,8 @@ Before executing, state:
|
|
|
42
42
|
- Build passes
|
|
43
43
|
- You can explain what changed in one sentence
|
|
44
44
|
|
|
45
|
+
"Done" is an audit, not a hope: settle the actual result against the Verification you stipulated in Law 2.
|
|
46
|
+
|
|
45
47
|
## Law 5: Reflect After Every Session
|
|
46
48
|
|
|
47
49
|
After non-trivial tasks:
|
|
@@ -59,6 +61,8 @@ After non-trivial tasks:
|
|
|
59
61
|
|
|
60
62
|
The "Rule to add" field feeds Law 7 — it becomes an instinct with 0.6 starting confidence.
|
|
61
63
|
|
|
64
|
+
This block is the ledger: what worked and what failed are the session's gains and losses; the rule to add is what you carry into the session you will not be in.
|
|
65
|
+
|
|
62
66
|
The "Iteration — Next best recommendations" field feeds Law 6. List the **top 3 ranked** core-development moves based on the current code state — what to build, fix, refactor, or investigate next so the feature/system advances. Item #1 is the strongest recommendation; #2 and #3 are alternative directions the user can pick from. NOT git plumbing (commit, push, PR), NOT pure CI ceremony (run tests, type-check), NOT deploy steps. Those belong in the end-of-run summary, not here.
|
|
63
67
|
|
|
64
68
|
Format per item: `<verb> <object at path:line> (<why, one clause grounded in current context>)`.
|
|
@@ -157,7 +157,7 @@ The rollback command is **printed**, not run. The skill's job is to give the ope
|
|
|
157
157
|
- `finishing-a-development-branch` (vendored, third-party/superpowers/) — runs first; reports the merge. This skill runs after.
|
|
158
158
|
- `verification-loop` — same Law 4 family; this skill is the deploy-seam specialization
|
|
159
159
|
- `proceed-with-the-recommendation` — routing-table row for "Merge / close branch" should pair `finishing-a-development-branch` with this skill when the project is auto-deploy
|
|
160
|
-
- `
|
|
160
|
+
- `gateguard` — orthogonal; gateguard gates the destructive op, this verifies post-deploy state
|
|
161
161
|
|
|
162
162
|
## Close-the-Loop Rule
|
|
163
163
|
|
|
@@ -77,7 +77,7 @@ Before creating {file_path}, present these facts:
|
|
|
77
77
|
|
|
78
78
|
### Destructive Bash Gate (every destructive command)
|
|
79
79
|
|
|
80
|
-
Triggers on
|
|
80
|
+
Triggers on structured rules that ignore flag order and spelling — `rm` with any recursive plus any force flag (`rm -r -f`, `rm -Rf`, `rm --recursive --force`), `git clean` with a force flag and no dry run, `git checkout -- <path>` or `git checkout .`, `git restore <path>` unless it is `--staged` only, `find … -delete`, `git push` with a `+refspec`, `git stash drop|clear` — plus the original substring list (`rm -rf`, `git reset --hard`, `git push --force`, `git branch -D`, `drop table`, `truncate `, `Remove-Item -Recurse`, etc.). Each command is judged after `&&`, `||`, `|`, `;` splitting, and a commit message, PR body or title value is blanked first so prose never trips it. The deny reason prints `Matched rule: <id>` so a block is explainable; the classifier is `lib/destructive-bash.mjs`. Plain file writes through Bash (`cat > file`, `sed -i`) are deliberately not gated.
|
|
81
81
|
|
|
82
82
|
```
|
|
83
83
|
1. List all files/data this command will modify or delete
|
|
@@ -154,10 +154,24 @@ The inline `_gateguard_facts_presented: true` retry still works on harnesses tha
|
|
|
154
154
|
|
|
155
155
|
Set the `CI_GATEGUARD_EXCLUDE` environment variable to opt specific low-risk paths out of the gate entirely — an LLM-maintained prose wiki, a generated scratch directory, anything where the fact-forcing pause costs more than it saves. The value is a comma-separated list of path substrings, each matched case-insensitively against the forward-slash-normalized file path, so `/mywiki/` excludes `D:\Vault\MyWiki\notes\x.md`. Unset or empty (the default) changes nothing: every mutating file call is gated exactly as before, and a call that touches a mix of excluded and non-excluded paths still gates the non-excluded ones. Set it per project in `.claude/settings.json` under `env`, or globally in `~/.claude/settings.json`.
|
|
156
156
|
|
|
157
|
+
An exclusion is never silent. When every target of a call is excluded, `hooks/gateguard.mjs` still allows it but prints one stderr line naming the fragment that matched (`gateguard: skipped by CI_GATEGUARD_EXCLUDE (fragment "docs/wiki" matched docs/wiki/page.md)`). A catch-all fragment that every path contains (`/`, `.`, any single character) is honoured too, but the line says what it really is: the file gate is off for this session. Destructive Bash is never excluded. If `/verify-install` reports the probe write went through, check this variable before concluding the hook is not wired.
|
|
158
|
+
|
|
157
159
|
### Locking edits to the current repo
|
|
158
160
|
|
|
159
161
|
A fact-list can't catch a wrong-repo or wrong-worktree write — you can present perfect facts about the wrong file, in the wrong checkout. Set `CI_GATEGUARD_TARGET_LOCK=block` to make the runtime hook (`hooks/gateguard.mjs`) refuse any mutating call whose **absolute** target canonicalizes outside the session project root (`CLAUDE_PROJECT_DIR`, or the git toplevel). Relative paths resolve under the current directory (= the root) and always pass; only an absolute path into a different tree is denied, and the deny reason names both the stray target and the expected root. This runs before the fact gate and independent of clearance — a wrong-repo write is wrong even with facts. Unset (the default) checks nothing, so legitimate out-of-root edits (`~/.claude`, a `/tmp` scratch file, a sibling repo) are unaffected; turn it on per session in a multi-worktree or headless run where cross-repo writes are the real risk. Paths already covered by `CI_GATEGUARD_EXCLUDE` are never target-locked.
|
|
160
162
|
|
|
163
|
+
### Migrating from `safety-guard` (retired 2026-08-07)
|
|
164
|
+
|
|
165
|
+
`safety-guard` was a prose-only tier-2 skill describing three "modes" it never implemented — no hook, no command, no logger ever shipped with it. Every mode it described is already enforced here by the runtime hook (`hooks/gateguard.mjs`):
|
|
166
|
+
|
|
167
|
+
| Retired `safety-guard` mode | What enforces it now |
|
|
168
|
+
|---|---|
|
|
169
|
+
| Careful — warn on `rm -rf`, `git push --force`, `git reset --hard`, `DROP TABLE`, `chmod 777`, `--no-verify` … | [Destructive Bash Gate](#destructive-bash-gate-every-destructive-command). Its pattern set is a superset of the retired watch list, and it blocks rather than warns. |
|
|
170
|
+
| Freeze — restrict writes to one directory tree | `CI_GATEGUARD_TARGET_LOCK=block` (above). Canonicalizes the absolute target against the session project root and refuses strays — before the fact gate, independent of clearance. |
|
|
171
|
+
| Guard — careful + freeze together | Both of the above; they compose. Narrow the surface further with `CI_GATEGUARD_EXCLUDE`. |
|
|
172
|
+
|
|
173
|
+
If you previously opted into `safety-guard` for autonomous or production-adjacent runs, set `CI_GATEGUARD_TARGET_LOCK=block` for that session instead. That is a real refusal, not a checklist the agent can talk itself out of.
|
|
174
|
+
|
|
161
175
|
### Limitations and guarantees
|
|
162
176
|
|
|
163
177
|
- **Honor system.** Clearance is recorded by `ci_gateguard_clear`, the `gateguard-clear.mjs` CLI, a manual state-file write, or the inline `_gateguard_facts_presented` flag where the harness allows it (see "Clearing the gate" above). The hook can't verify the investigation actually happened; the 50-file cap — counted per session — bounds damage from stuck loops or rogue agents.
|
|
@@ -188,5 +202,6 @@ The standalone `gateguard-ai` Python/CLI package referenced in earlier drafts of
|
|
|
188
202
|
|
|
189
203
|
## Related Skills
|
|
190
204
|
|
|
191
|
-
- `safety
|
|
205
|
+
- `worktree-safety` — validates the worktree root before a source write; this skill gates the write itself
|
|
206
|
+
- `reconcile` — git ground truth before a destructive git action
|
|
192
207
|
- `code-reviewer` — Post-edit review (GateGuard is pre-edit investigation)
|
|
@@ -235,8 +235,8 @@ Rows whose **Preferred skill** is not bundled with the `continuous-improvement`
|
|
|
235
235
|
| Fan out N agents on isolated worktrees with shared contract | `superpowers:dispatching-parallel-agents` → `ruflo-swarm:swarm-init` | Use the swarm contract: fixed roles + base ref + shared contract test; reconcile results after. (Reference behavior — does not require `ruflo-swarm`.) |
|
|
236
236
|
| Stream live observation of long agent runs | `ruflo-swarm:monitor-stream` | Push-based event log; poll fallback if the MCP server is offline. (Reference behavior — does not require `ruflo-swarm`.) |
|
|
237
237
|
| Visual regression / browser-level diff | `oh-my-claudecode:visual-verdict` | Playwright screenshot diff against staging baseline. (Reference behavior — does not require `oh-my-claudecode`.) |
|
|
238
|
-
| Multi-session retrospective across a sprint | `
|
|
239
|
-
| Long autonomous run with quality gates | `oh-my-claudecode:
|
|
238
|
+
| Multi-session retrospective across a sprint | `learn-eval` | What worked / what failed / what to do differently / 3 ranked next moves. (Reference behavior — does not require `oh-my-claudecode`.) |
|
|
239
|
+
| Long autonomous run with quality gates | `oh-my-claudecode:ultragoal` → `ralph` | PRD-shaped autonomous loop with verify-between-iterations. (Reference behavior — does not require `oh-my-claudecode`.) |
|
|
240
240
|
| Product-management work (PRD, user stories, acceptance criteria, OKRs, experiments, personas, JTBD, lean canvas, market sizing, competitive analysis, meetings family, launch checklist) | Install phuryn/pm-skills via Claude Code marketplace — see docs/THIRD_PARTY.md | Out-of-band install (`claude plugin marketplace add phuryn/pm-skills` + the eight `pm-*@pm-skills` plugins). Inline fallback: keep the work shape (problem → user → goal → metric → scope; Given/When/Then per story; objective + 3-5 measurable KRs; we-believe / we'll-know hypothesis; TAM/SAM/SOM bottom-up; cross-cutting meetings agenda/brief/recap/synthesize) without depending on a specific routing target. |
|
|
241
241
|
|
|
242
242
|
## Phase 4: Verify (Law 4 — Verify Before Reporting)
|
|
@@ -177,6 +177,5 @@ git branch -d <type>/<slug> # delete the merged feature branch (safe
|
|
|
177
177
|
|
|
178
178
|
- **`recall`** (Law 1) — before a risky git op, recall whether the same operation failed on this repo before.
|
|
179
179
|
- **`gateguard`** (Law 1) — the runtime gate (`hooks/gateguard.mjs`); `reconcile` is the procedure you run once a destructive git action is in play.
|
|
180
|
-
- **`safety-guard`** — destructive-operation guardrails for production and autonomous runs.
|
|
181
180
|
- **`audit`** (Law 4) — when an audit ends in a fix, `reconcile` is the safe path from branch to landed PR.
|
|
182
181
|
- **`commit-commands:commit-push-pr`** — the external-plugin equivalent of the commit → push → PR tail; `reconcile` reimplements it inline so the flow works with no companion installed. For a TDD-gated single-defect variant, use `/ship`.
|