continuous-improvement 3.22.1 → 3.24.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 +1 -1
- package/CHANGELOG.md +23 -0
- package/QUICKSTART.md +19 -20
- package/README.md +72 -19
- package/SKILL.md +4 -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 +208 -33
- package/bin/mcp-server.mjs +4 -9
- package/bin/observe.mjs +3 -3
- package/bin/reconcile-instinct-hashes.mjs +226 -0
- package/commands/discipline.md +5 -2
- package/commands/reconcile.md +1 -1
- package/commands/ship.md +5 -49
- package/commands/superpowers.md +1 -1
- package/commands/verify-install.md +8 -3
- package/hooks/companion-preference.mjs +2 -5
- 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 +11 -0
- package/package.json +1 -1
- package/plugins/beginner.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +1 -1
- 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/ship.md +5 -49
- package/plugins/continuous-improvement/commands/superpowers.md +1 -1
- package/plugins/continuous-improvement/commands/verify-install.md +8 -3
- package/plugins/continuous-improvement/hooks/companion-preference.mjs +2 -5
- 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/skills/README.md +1 -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/reconcile/SKILL.md +0 -1
- package/plugins/continuous-improvement/skills/ship/SKILL.md +139 -0
- package/plugins/expert.json +1 -1
- package/skills/README.md +2 -2
- package/skills/deploy-receipt.md +1 -1
- package/skills/gateguard.md +17 -2
- package/skills/reconcile.md +0 -1
- package/skills/ship.md +139 -0
- package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +0 -77
- package/skills/safety-guard.md +0 -77
package/commands/reconcile.md
CHANGED
|
@@ -95,7 +95,7 @@ git branch -d <type>/<slug> # delete the merged feature branch (safe
|
|
|
95
95
|
## Pairs with
|
|
96
96
|
|
|
97
97
|
- **`reconcile`** skill — the discipline this command runs.
|
|
98
|
-
- **`gateguard`**
|
|
98
|
+
- **`gateguard`** — the runtime gate for mutating tool calls and destructive shell.
|
|
99
99
|
- **`recall`** — recall whether the same git op failed here before.
|
|
100
100
|
- **`audit`** — the loop that often produces the fix `/reconcile` then ships.
|
|
101
101
|
- **`/ship`** — the TDD-gated single-defect variant; `commit-commands:commit-push-pr` is the external-plugin equivalent of the commit → PR tail.
|
package/commands/ship.md
CHANGED
|
@@ -1,57 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ship
|
|
3
|
-
description: "
|
|
3
|
+
description: "Compatibility entry point for the native ship skill. Passes one defect request through unchanged."
|
|
4
|
+
argument-hint: "[one-line defect description]"
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# /ship
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
Invoke the native `ship` skill with this complete defect request:
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
$ARGUMENTS
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
/ship <one-line description of the defect>
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
If the description is ambiguous or names more than one concern, `/ship` halts and asks you to narrow it — one defect per run.
|
|
19
|
-
|
|
20
|
-
## Behavior
|
|
21
|
-
|
|
22
|
-
In order, for the single defect:
|
|
23
|
-
|
|
24
|
-
1. **Ground truth** — `reconcile` (or its inline fallback): confirm the working tree is clean and on a feature branch cut from an up-to-date `origin/<base>`. If on a protected branch or a stale base, halt and ask.
|
|
25
|
-
2. **Reproduce (RED)** — `tdd-workflow`: write a failing test that reproduces the defect; watch it fail. Pre-test implementation code is deleted, not kept.
|
|
26
|
-
3. **Fix (GREEN)** — write the minimal change that makes the test pass; watch it pass. One concern only.
|
|
27
|
-
4. **Verify** — `verification-loop`: run the project's verify ladder (build, types, tests). Build-green is evidence of mechanism, not of the fix — confirm the defect itself no longer reproduces.
|
|
28
|
-
5. **Commit** — one commit, one concern, staged by explicit filename (never `git add -A`). Use a Windows-safe commit message: a single-line `-m` (repeat `-m` for paragraphs) or `git commit -F <tempfile>` — no multi-line here-docs/here-strings.
|
|
29
|
-
6. **Open PR** — `commit-commands:commit-push-pr` (or `gh pr create`): push the branch and open a single-concern PR that cites the plan or issue. **Stop here.** The merge is yours.
|
|
30
|
-
7. **Deploy receipt (advisory)** — after you merge, `deploy-receipt` verifies the deployed SHA matches the merge SHA. Advisory only; `/ship` does not deploy.
|
|
31
|
-
|
|
32
|
-
## Hard stops (halt and ask, never improvise)
|
|
33
|
-
|
|
34
|
-
- Ambiguous or multi-concern defect description.
|
|
35
|
-
- Working tree not clean, or branch is protected / cut from a stale base.
|
|
36
|
-
- Any verification step fails with a non-obvious fix.
|
|
37
|
-
- The fix would touch more than 15 non-generated files (that is no longer one concern — split it, or use `/release-train`).
|
|
38
|
-
- Push would target a protected branch.
|
|
39
|
-
|
|
40
|
-
## Anti-patterns this command refuses
|
|
41
|
-
|
|
42
|
-
- **Auto-merge.** Never merges the PR it opens, even when CI is green.
|
|
43
|
-
- **Deploy.** Never runs a deploy; `deploy-receipt` only verifies after you merge.
|
|
44
|
-
- **Bypass.** No `--admin`, `--force`, `--no-verify`.
|
|
45
|
-
- **Bundled concerns.** Will not fold an unrelated fix into the same commit; logs it as a deferred follow-up instead.
|
|
46
|
-
|
|
47
|
-
## Composition
|
|
48
|
-
|
|
49
|
-
Routes through, in order: `reconcile` → `tdd-workflow` → `verification-loop` → `commit-commands:commit-push-pr` → `deploy-receipt`. Each step falls back to its inline behavior when the preferred skill is not installed.
|
|
50
|
-
|
|
51
|
-
## Example
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
/ship registration form accepts a negative deposit amount
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Reconciles git state, writes a failing test asserting deposits must be positive, implements the guard, runs the verify ladder, commits one concern with a single-line message, opens the PR, and stops for your review.
|
|
13
|
+
Follow that skill exactly. Do not reconstruct or duplicate its Git, worktree, TDD, return, cleanup, merge, or deploy rules here. If the native skill is unavailable, stop and tell the operator to update or reinstall `continuous-improvement`.
|
package/commands/superpowers.md
CHANGED
|
@@ -13,7 +13,7 @@ The 7 Laws define *what* discipline must be applied. `/superpowers` decides *whi
|
|
|
13
13
|
|
|
14
14
|
| Source | Where it lives | Examples of what it routes to |
|
|
15
15
|
|---|---|---|
|
|
16
|
-
| `continuous-improvement` (this plugin) | bundled — always present | `gateguard` (Law 1), `tdd-workflow` (Law 3+4), `verification-loop` (Law 4), `wild-risa-balance` (Law 2), `
|
|
16
|
+
| `continuous-improvement` (this plugin) | bundled — always present | `gateguard` (Law 1), `tdd-workflow` (Law 3+4), `verification-loop` (Law 4), `wild-risa-balance` (Law 2), `proceed-with-the-recommendation` (all 7), `ralph` (Law 6), `workspace-surface-audit` (Law 1) |
|
|
17
17
|
| `obra/superpowers` (Jesse Vincent) | vendored at `third-party/superpowers/`, pinned SHA `f2cbfbe` (v5.1.0) | `superpowers:brainstorming`, `:writing-plans`, `:executing-plans`, `:test-driven-development`, `:systematic-debugging`, `:requesting-code-review`, `:receiving-code-review`, `:verification-before-completion`, `:dispatching-parallel-agents`, `:using-git-worktrees`, `:finishing-a-development-branch`, `:subagent-driven-development`, `:writing-skills`, `:using-superpowers` |
|
|
18
18
|
| `addyosmani/agent-skills` | vendored at `third-party/addy-agent-skills/`, pinned SHA `742dca5` (v1.0.0) | `spec-driven-development`, `source-driven-development`, `context-engineering`, `idea-refine`, `incremental-implementation`, `code-review-and-quality`, `code-simplification`, `security-and-hardening`, `debugging-and-error-recovery`, `performance-optimization`, `api-and-interface-design`, `frontend-ui-engineering`, `browser-testing-with-devtools`, `ci-cd-and-automation`, `deprecation-and-migration`, `documentation-and-adrs`, `git-workflow-and-versioning`, `planning-and-task-breakdown`, `shipping-and-launch` |
|
|
19
19
|
| `ruflo-swarm` (ruvnet) | vendored at `third-party/ruflo-swarm/`, pinned SHA `addb5cd` (v0.2.0) | `swarm-init`, `monitor-stream`; `swarm_*` and `agent_*` MCP tools; `/swarm`, `/watch` |
|
|
@@ -29,9 +29,14 @@ with the text `probe`) **without presenting any research first**.
|
|
|
29
29
|
|
|
30
30
|
- If the hook **blocks** the write with a fact-list reason — the runtime layer is
|
|
31
31
|
wired. Record `gateguard: ✓`. Do not retry the write; the block is the pass.
|
|
32
|
-
- If the write **goes through** with no pause — the hook did not load
|
|
33
|
-
`
|
|
34
|
-
|
|
32
|
+
- If the write **goes through** with no pause — either the hook did not load, or
|
|
33
|
+
`CI_GATEGUARD_EXCLUDE` is set to a fragment that matches the probe path (a
|
|
34
|
+
catch-all such as `/` or `.` matches every path and switches the file gate off;
|
|
35
|
+
the hook prints a one-line stderr notice when an exclusion fires). Run
|
|
36
|
+
`echo "$CI_GATEGUARD_EXCLUDE"` first. If it is empty, record
|
|
37
|
+
`gateguard: ✗ (hooks/gateguard.mjs not wired — see README → Troubleshooting install)`;
|
|
38
|
+
if it is set, record `gateguard: ✗ (excluded by CI_GATEGUARD_EXCLUDE=<value>; unset it or
|
|
39
|
+
narrow the fragment)`. Delete the probe file if it was created.
|
|
35
40
|
|
|
36
41
|
## Check 3 — observation capture recording
|
|
37
42
|
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
* suite or a follow-up audit walks the table against this map.
|
|
31
31
|
*/
|
|
32
32
|
import { execFileSync } from "node:child_process";
|
|
33
|
-
import { createHash } from "node:crypto";
|
|
34
33
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
35
34
|
import { homedir } from "node:os";
|
|
36
35
|
import { join } from "node:path";
|
|
36
|
+
import { hashProjectRoot } from "../lib/gateguard-state.mjs";
|
|
37
37
|
const OVERRIDES = {
|
|
38
38
|
"tdd-workflow": {
|
|
39
39
|
companion: "superpowers:test-driven-development",
|
|
@@ -125,10 +125,7 @@ function resolveProjectRoot() {
|
|
|
125
125
|
return "global";
|
|
126
126
|
}
|
|
127
127
|
function telemetryPath(home) {
|
|
128
|
-
const hash =
|
|
129
|
-
.update(resolveProjectRoot())
|
|
130
|
-
.digest("hex")
|
|
131
|
-
.slice(0, 12);
|
|
128
|
+
const hash = hashProjectRoot(resolveProjectRoot());
|
|
132
129
|
return join(home, ".claude", "instincts", hash, "companion-preference.jsonl");
|
|
133
130
|
}
|
|
134
131
|
/**
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Runtime PreToolUse config-guard hook.
|
|
4
|
+
*
|
|
5
|
+
* Stdin : JSON { tool_name, tool_input }
|
|
6
|
+
* Stdout : empty on allow / warn (warn prints to stderr); on block, the
|
|
7
|
+
* documented PreToolUse shape:
|
|
8
|
+
* { hookSpecificOutput: { hookEventName: "PreToolUse",
|
|
9
|
+
* permissionDecision: "deny", permissionDecisionReason } }
|
|
10
|
+
* Exit : 0 always (decision is in stdout; fail-open on any error).
|
|
11
|
+
*
|
|
12
|
+
* Guards the files that wire the guardrails: `.claude/settings*.json`,
|
|
13
|
+
* `.mcp.json`, `hooks.json`, `.claude/hooks/`, `.claude/plugins/`,
|
|
14
|
+
* `.claude-plugin/`, and the `claude plugin|mcp|config` CLI forms that edit
|
|
15
|
+
* them. Pure logic in ../lib/config-guard-gate.mjs.
|
|
16
|
+
*
|
|
17
|
+
* Mode via CI_CONFIG_GUARD: "warn" (default) | "block" | "off".
|
|
18
|
+
* warn : print a one-line notice to stderr; never blocks.
|
|
19
|
+
* block : emit the PreToolUse deny shape.
|
|
20
|
+
* off : no-op.
|
|
21
|
+
* One-call bypass: CI_CONFIG_GUARD_ALLOW=true allows the call and says so.
|
|
22
|
+
*
|
|
23
|
+
* Registered with a tool matcher (Bash|Edit|MultiEdit|Write|NotebookEdit) so
|
|
24
|
+
* read-only tools never spawn it. No network. No git. Fail-open.
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { classifyMutation, decide, parseMode } from "../lib/config-guard-gate.mjs";
|
|
28
|
+
function readStdinSync() {
|
|
29
|
+
try {
|
|
30
|
+
return readFileSync(0, "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function safeJsonParse(text) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function buildReason(toolName, hit, mode) {
|
|
45
|
+
const what = hit.via === "claude-cli"
|
|
46
|
+
? `\`${hit.target}\` edits the plugin / MCP / settings configuration`
|
|
47
|
+
: `${toolName} would modify ${hit.target} (matches "${hit.pattern}")`;
|
|
48
|
+
return [
|
|
49
|
+
`config-guard: ${what}, one of the files that wires the guardrails (settings, MCP config, hooks, installed plugins).`,
|
|
50
|
+
"If this is intended, rerun this one call with CI_CONFIG_GUARD_ALLOW=true, or set CI_CONFIG_GUARD=off for the session.",
|
|
51
|
+
mode === "block"
|
|
52
|
+
? "You are seeing a deny because CI_CONFIG_GUARD=block; the default is warn."
|
|
53
|
+
: "This is a warning (CI_CONFIG_GUARD=warn, the default); set CI_CONFIG_GUARD=block to deny instead.",
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
function main() {
|
|
57
|
+
const mode = parseMode(process.env.CI_CONFIG_GUARD);
|
|
58
|
+
if (mode === "off")
|
|
59
|
+
return;
|
|
60
|
+
const payload = safeJsonParse(readStdinSync());
|
|
61
|
+
if (!payload || typeof payload !== "object")
|
|
62
|
+
return;
|
|
63
|
+
const obj = payload;
|
|
64
|
+
const toolName = typeof obj.tool_name === "string" ? obj.tool_name : "";
|
|
65
|
+
const toolInput = obj.tool_input && typeof obj.tool_input === "object" ? obj.tool_input : {};
|
|
66
|
+
const hit = classifyMutation(toolName, toolInput);
|
|
67
|
+
if (!hit)
|
|
68
|
+
return;
|
|
69
|
+
if (String(process.env.CI_CONFIG_GUARD_ALLOW ?? "").trim().toLowerCase() === "true") {
|
|
70
|
+
process.stderr.write(`[continuous-improvement] config-guard: CI_CONFIG_GUARD_ALLOW=true let ${toolName} touch ${hit.target} on this call.\n`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const decision = decide(mode, true, buildReason(toolName, hit, mode));
|
|
74
|
+
if (decision.action === "block") {
|
|
75
|
+
process.stdout.write(`${JSON.stringify({
|
|
76
|
+
hookSpecificOutput: {
|
|
77
|
+
hookEventName: "PreToolUse",
|
|
78
|
+
permissionDecision: "deny",
|
|
79
|
+
permissionDecisionReason: decision.reason,
|
|
80
|
+
},
|
|
81
|
+
})}\n`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (decision.action === "warn") {
|
|
85
|
+
process.stderr.write(`[continuous-improvement] ${decision.reason.split("\n")[0]}\n`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
main();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// fail-open: never block a session on a hook error
|
|
93
|
+
}
|
|
94
|
+
process.exit(0);
|
package/hooks/gateguard.mjs
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import { readFileSync } from "node:fs";
|
|
36
36
|
import { dirname, join } from "node:path";
|
|
37
37
|
import { fileURLToPath } from "node:url";
|
|
38
|
+
import { classifyDestructiveBash } from "../lib/destructive-bash.mjs";
|
|
38
39
|
import { MAX_CLEARED_FILES, canonicalizeFileKey, canonicalizeProjectRoot, isCapReached, isFileCleared, loadState, markFileCleared, resolveProjectRoot, resolveSessionDir, saveState, } from "../lib/gateguard-state.mjs";
|
|
39
40
|
const TOOL_ROUTE = {
|
|
40
41
|
Read: "allow",
|
|
@@ -48,42 +49,14 @@ const TOOL_ROUTE = {
|
|
|
48
49
|
NotebookEdit: "mutating-file",
|
|
49
50
|
Bash: "allow",
|
|
50
51
|
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"--force-with-lease",
|
|
58
|
-
"git branch -D",
|
|
59
|
-
"drop table",
|
|
60
|
-
"drop database",
|
|
61
|
-
"drop schema",
|
|
62
|
-
"truncate ",
|
|
63
|
-
"mkfs",
|
|
64
|
-
"dd if=",
|
|
65
|
-
"format ",
|
|
66
|
-
"rmdir /s",
|
|
67
|
-
"del /f /q",
|
|
68
|
-
"del /q /f",
|
|
69
|
-
"Remove-Item -Recurse",
|
|
70
|
-
"Remove-Item -Force",
|
|
71
|
-
];
|
|
72
|
-
// Flags whose VALUE is human prose (a commit message, a PR body) or a filename —
|
|
73
|
-
// never a command to execute. Their contents must not trip the destructive scan:
|
|
74
|
-
// `git commit -m "drop the stale format helper"` and `gh pr create --body "…"`
|
|
75
|
-
// were stranding finished work on their own wording. `-c` is deliberately
|
|
76
|
-
// EXCLUDED — `bash -c "rm -rf /"` carries a real command and must still gate.
|
|
77
|
-
const MESSAGE_FLAG_RE = /(^|\s)(-m|--message|-F|--file|--body|--body-file|--title|--notes|-C|--reuse-message)(=|\s+)('[^']*'|"[^"]*"|\S+)/g;
|
|
78
|
-
// Blank the value of every message/body flag so only executable command syntax
|
|
79
|
-
// remains for the destructive-pattern scan. The flag itself is preserved so a
|
|
80
|
-
// flag like `-F` never accidentally merges with its neighbours.
|
|
81
|
-
function stripMessageArgs(command) {
|
|
82
|
-
return command.replace(MESSAGE_FLAG_RE, (_match, lead, flag) => `${lead}${flag} `);
|
|
83
|
-
}
|
|
52
|
+
// The destructive-Bash classifier lives in lib/destructive-bash.mjs: structured
|
|
53
|
+
// rules (flag order and spelling do not matter: `rm -r -f`, `git clean -fdx`,
|
|
54
|
+
// `git checkout -- .`, `git restore .`, `find -delete`, `git push +ref`,
|
|
55
|
+
// `git stash drop`) plus the original substring list as the fallback. The
|
|
56
|
+
// message-flag carve-out (`git commit -m "…"`) lives there too. Each rule has
|
|
57
|
+
// a stable id the deny reason prints, so a block is explainable.
|
|
84
58
|
function isDestructiveBash(command) {
|
|
85
|
-
|
|
86
|
-
return DESTRUCTIVE_PATTERNS.some((p) => lower.includes(p.toLowerCase()));
|
|
59
|
+
return classifyDestructiveBash(command).destructive;
|
|
87
60
|
}
|
|
88
61
|
function classifyTool(toolName, toolInput) {
|
|
89
62
|
const route = TOOL_ROUTE[toolName] ?? "allow";
|
|
@@ -119,12 +92,34 @@ const EXCLUDE_FRAGMENTS = String(process.env.CI_GATEGUARD_EXCLUDE ?? "")
|
|
|
119
92
|
.split(",")
|
|
120
93
|
.map((fragment) => fragment.trim().replace(/\\/g, "/").toLowerCase())
|
|
121
94
|
.filter((fragment) => fragment !== "");
|
|
122
|
-
function
|
|
95
|
+
function matchedExcludeFragment(filePath) {
|
|
123
96
|
if (EXCLUDE_FRAGMENTS.length === 0 || typeof filePath !== "string" || filePath === "") {
|
|
124
|
-
return
|
|
97
|
+
return null;
|
|
125
98
|
}
|
|
126
99
|
const normalized = filePath.replace(/\\/g, "/").toLowerCase();
|
|
127
|
-
return EXCLUDE_FRAGMENTS.
|
|
100
|
+
return EXCLUDE_FRAGMENTS.find((fragment) => normalized.includes(fragment)) ?? null;
|
|
101
|
+
}
|
|
102
|
+
function isExcludedPath(filePath) {
|
|
103
|
+
return matchedExcludeFragment(filePath) !== null;
|
|
104
|
+
}
|
|
105
|
+
// A fragment every path contains ("/", ".", any single character) is not an
|
|
106
|
+
// exclusion, it is an off switch for the whole file gate. It is still honoured —
|
|
107
|
+
// the operator set it — but silently honouring it is how a host ends up with the
|
|
108
|
+
// headline feature off and nobody noticing (this repo's own author's shell had
|
|
109
|
+
// CI_GATEGUARD_EXCLUDE="/,." for weeks). So every exclusion prints one stderr
|
|
110
|
+
// line, and a catch-all says plainly that the gate is off. stderr never changes
|
|
111
|
+
// the decision: allow stays empty stdout + exit 0.
|
|
112
|
+
function isCatchAllFragment(fragment) {
|
|
113
|
+
return fragment.length <= 1 || fragment === "./" || fragment === "..";
|
|
114
|
+
}
|
|
115
|
+
function buildExcludeNotice(paths, fragment) {
|
|
116
|
+
const shown = paths.map((p) => p.replace(/\\/g, "/")).join(", ");
|
|
117
|
+
if (isCatchAllFragment(fragment)) {
|
|
118
|
+
return (`[continuous-improvement] gateguard: CI_GATEGUARD_EXCLUDE fragment "${fragment}" matches every path, ` +
|
|
119
|
+
`so the file gate is off for this session (skipped ${shown}). ` +
|
|
120
|
+
"Narrow it to a directory, e.g. CI_GATEGUARD_EXCLUDE=docs/wiki, to get the gate back.");
|
|
121
|
+
}
|
|
122
|
+
return `[continuous-improvement] gateguard: skipped by CI_GATEGUARD_EXCLUDE (fragment "${fragment}" matched ${shown}).`;
|
|
128
123
|
}
|
|
129
124
|
// --- Target lock (opt-in) --------------------------------------------------
|
|
130
125
|
// A fact-list can't catch a wrong-repo / wrong-worktree write — you can present
|
|
@@ -271,6 +266,7 @@ function buildBraceRefReason(hit) {
|
|
|
271
266
|
].join("\n");
|
|
272
267
|
}
|
|
273
268
|
function buildDestructiveBashReason(command) {
|
|
269
|
+
const rule = classifyDestructiveBash(command).rule ?? "unknown";
|
|
274
270
|
return [
|
|
275
271
|
`Destructive command requested: ${command}`,
|
|
276
272
|
"",
|
|
@@ -278,6 +274,7 @@ function buildDestructiveBashReason(command) {
|
|
|
278
274
|
" 2. Write a one-line rollback procedure",
|
|
279
275
|
" 3. Quote the user's current instruction verbatim",
|
|
280
276
|
"",
|
|
277
|
+
`Matched rule: ${rule}`,
|
|
281
278
|
"Destructive Bash gates EVERY call — clearance is not cached.",
|
|
282
279
|
].join("\n");
|
|
283
280
|
}
|
|
@@ -354,7 +351,10 @@ function main() {
|
|
|
354
351
|
const allTargetPaths = extractFilePaths(toolInput);
|
|
355
352
|
const filePaths = allTargetPaths.filter((path) => !isExcludedPath(path));
|
|
356
353
|
if (allTargetPaths.length > 0 && filePaths.length === 0) {
|
|
357
|
-
|
|
354
|
+
// Every target is under a CI_GATEGUARD_EXCLUDE path; skip the gate, but say so.
|
|
355
|
+
const fragment = matchedExcludeFragment(allTargetPaths[0]) ?? EXCLUDE_FRAGMENTS[0];
|
|
356
|
+
process.stderr.write(`${buildExcludeNotice(allTargetPaths, fragment)}\n`);
|
|
357
|
+
emitAllow();
|
|
358
358
|
return;
|
|
359
359
|
}
|
|
360
360
|
// Target lock runs before the fact gate and independent of clearance: a
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
// construction: any error / missing goal / unreadable observations exits 0 and
|
|
18
18
|
// never blocks. No network. 5s hook budget.
|
|
19
19
|
import { execFileSync } from "node:child_process";
|
|
20
|
-
import { createHash } from "node:crypto";
|
|
21
20
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
21
|
import { join } from "node:path";
|
|
23
22
|
import { evaluateGoalDrift } from "../lib/goal-drift-gate.mjs";
|
|
23
|
+
import { hashProjectRoot } from "../lib/gateguard-state.mjs";
|
|
24
24
|
import { resolveHomeDir } from "../lib/resolve-home-dir.mjs";
|
|
25
25
|
function readStdinSync() {
|
|
26
26
|
try {
|
|
@@ -62,7 +62,7 @@ function resolveProjectRoot() {
|
|
|
62
62
|
return "global";
|
|
63
63
|
}
|
|
64
64
|
function projectHash(root) {
|
|
65
|
-
return
|
|
65
|
+
return hashProjectRoot(root);
|
|
66
66
|
}
|
|
67
67
|
function readGoalMarkdown(projectRoot, instinctsProjectDir) {
|
|
68
68
|
const candidates = [join(projectRoot, "task_plan.md"), join(instinctsProjectDir, "goal.md")];
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* the DB files stay dirty. Never blocks; fail-open on any error.
|
|
17
17
|
*/
|
|
18
18
|
import { execFileSync } from "node:child_process";
|
|
19
|
-
import {
|
|
19
|
+
import { hashProjectRoot } from "../lib/gateguard-state.mjs";
|
|
20
20
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
@@ -71,7 +71,7 @@ function markerKey(sessionId) {
|
|
|
71
71
|
return sanitized || `day-${new Date().toISOString().slice(0, 10)}`;
|
|
72
72
|
}
|
|
73
73
|
function markerPath(home, projectRoot, sessionId) {
|
|
74
|
-
const hash =
|
|
74
|
+
const hash = hashProjectRoot(projectRoot);
|
|
75
75
|
return join(home, ".claude", "instincts", hash, "query-cost-nudge", `${markerKey(sessionId)}.nudged`);
|
|
76
76
|
}
|
|
77
77
|
function main() {
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
* ~/.claude/instincts/<project-hash>/recall-briefing-session.json records which
|
|
24
24
|
* session_ids have been briefed so each session is briefed at most once.
|
|
25
25
|
*/
|
|
26
|
-
import { createHash } from "node:crypto";
|
|
27
26
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
27
|
import { homedir } from "node:os";
|
|
29
28
|
import { join } from "node:path";
|
|
30
29
|
import { execFileSync } from "node:child_process";
|
|
31
30
|
import { buildIndex, query } from "../lib/recall-index.mjs";
|
|
32
31
|
import { DEFAULT_MAX_HITS, decideBriefing } from "../lib/recall-briefing.mjs";
|
|
32
|
+
import { hashProjectRoot } from "../lib/gateguard-state.mjs";
|
|
33
33
|
const ENABLED_VALUES = new Set(["1", "on", "true", "yes"]);
|
|
34
34
|
const MAX_BRIEFED_KEYS = 1000;
|
|
35
35
|
function isEnabled() {
|
|
@@ -65,7 +65,7 @@ function resolveProjectRoot() {
|
|
|
65
65
|
return "global";
|
|
66
66
|
}
|
|
67
67
|
function instinctsDir(home) {
|
|
68
|
-
const hash =
|
|
68
|
+
const hash = hashProjectRoot(resolveProjectRoot());
|
|
69
69
|
return join(home, ".claude", "instincts", hash);
|
|
70
70
|
}
|
|
71
71
|
function readObservations(dir) {
|
package/hooks/route-prompt.mjs
CHANGED
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
* }
|
|
31
31
|
* Rows are evaluated in order; first match wins.
|
|
32
32
|
*/
|
|
33
|
-
import { createHash } from "node:crypto";
|
|
34
33
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
35
34
|
import { homedir } from "node:os";
|
|
36
35
|
import { dirname, join } from "node:path";
|
|
37
36
|
import { execFileSync } from "node:child_process";
|
|
38
37
|
import { fileURLToPath } from "node:url";
|
|
38
|
+
import { hashProjectRoot } from "../lib/gateguard-state.mjs";
|
|
39
39
|
function readStdin() {
|
|
40
40
|
try {
|
|
41
41
|
return readFileSync(0, "utf8");
|
|
@@ -65,10 +65,7 @@ function resolveProjectRoot() {
|
|
|
65
65
|
return "global";
|
|
66
66
|
}
|
|
67
67
|
function telemetryPath(home) {
|
|
68
|
-
const hash =
|
|
69
|
-
.update(resolveProjectRoot())
|
|
70
|
-
.digest("hex")
|
|
71
|
-
.slice(0, 12);
|
|
68
|
+
const hash = hashProjectRoot(resolveProjectRoot());
|
|
72
69
|
return join(home, ".claude", "instincts", hash, "route-prompt.jsonl");
|
|
73
70
|
}
|
|
74
71
|
function writeTelemetry(home, event) {
|
package/hooks/session.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
3
|
import { readFileSync, readdirSync } from "node:fs";
|
|
5
4
|
import { join } from "node:path";
|
|
5
|
+
import { hashProjectRoot } from "../lib/gateguard-state.mjs";
|
|
6
6
|
import { resolveHomeDir } from "../lib/resolve-home-dir.mjs";
|
|
7
7
|
function read(path) {
|
|
8
8
|
try {
|
|
@@ -62,7 +62,7 @@ function main() {
|
|
|
62
62
|
if (!home)
|
|
63
63
|
return;
|
|
64
64
|
const instinctsRoot = join(home, ".claude", "instincts");
|
|
65
|
-
const hash =
|
|
65
|
+
const hash = hashProjectRoot(projectRoot());
|
|
66
66
|
const projectDir = join(instinctsRoot, hash);
|
|
67
67
|
const files = [...yamlFiles(projectDir), ...yamlFiles(join(instinctsRoot, "global"))];
|
|
68
68
|
const observations = read(join(projectDir, "observations.jsonl")).split(/\r?\n/).filter(Boolean).length;
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
// name + verify command, so a run is nudged at most once. Fail-open by
|
|
25
25
|
// construction: any error exits 0 and never blocks. No network. 5s hook budget.
|
|
26
26
|
import { execFileSync } from "node:child_process";
|
|
27
|
-
import { createHash } from "node:crypto";
|
|
28
27
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
29
28
|
import { join } from "node:path";
|
|
29
|
+
import { hashProjectRoot } from "../lib/gateguard-state.mjs";
|
|
30
30
|
import { resolveHomeDir } from "../lib/resolve-home-dir.mjs";
|
|
31
31
|
import { workflowRunFromObservations } from "../lib/skill-distill.mjs";
|
|
32
32
|
function safeJsonParse(text) {
|
|
@@ -60,7 +60,7 @@ function resolveProjectRoot() {
|
|
|
60
60
|
return "global";
|
|
61
61
|
}
|
|
62
62
|
function projectHash(root) {
|
|
63
|
-
return
|
|
63
|
+
return hashProjectRoot(root);
|
|
64
64
|
}
|
|
65
65
|
function readObservations(instinctsProjectDir) {
|
|
66
66
|
const file = join(instinctsProjectDir, "observations.jsonl");
|