continuous-improvement 3.17.0 → 3.19.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/README.md +165 -99
- package/bin/audit-actions.mjs +433 -0
- package/bin/check-command-count.mjs +114 -0
- package/bin/generate-plugin-manifests.mjs +1 -0
- package/bin/portfolio-health.mjs +298 -0
- package/commands/reconcile.md +34 -7
- package/hooks/gateguard.mjs +137 -3
- package/hooks/typecheck-stop.mjs +117 -0
- package/lib/gateguard-state.mjs +5 -1
- package/lib/plugin-metadata.mjs +10 -2
- package/lib/typecheck-gate.mjs +62 -0
- package/package.json +12 -6
- package/plugins/beginner.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +1 -1
- package/plugins/continuous-improvement/commands/reconcile.md +34 -7
- package/plugins/continuous-improvement/hooks/gateguard.mjs +137 -3
- package/plugins/continuous-improvement/hooks/hooks.json +6 -1
- package/plugins/continuous-improvement/hooks/typecheck-stop.mjs +117 -0
- package/plugins/continuous-improvement/lib/gateguard-state.mjs +5 -1
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +10 -2
- package/plugins/continuous-improvement/lib/typecheck-gate.mjs +62 -0
- package/plugins/continuous-improvement/skills/README.md +1 -1
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +10 -0
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +52 -4
- package/plugins/expert.json +1 -1
- package/skills/gateguard.md +10 -0
- package/skills/reconcile.md +52 -4
- package/templates/actions_security_checklist.md +39 -0
- package/templates/experiment_template.md +38 -0
- package/templates/portfolio_event.schema.json +69 -0
- package/templates/release_receipt_template.md +37 -0
package/lib/gateguard-state.mjs
CHANGED
|
@@ -59,7 +59,11 @@ function sanitizeSessionId(sessionId) {
|
|
|
59
59
|
return "";
|
|
60
60
|
return sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
// Exported for the target-lock gate (RISA 2 / G2): the hook compares a mutating
|
|
63
|
+
// call's absolute target against this root to catch wrong-repo / wrong-worktree
|
|
64
|
+
// writes. Returns "global" when no CLAUDE_PROJECT_DIR and no git toplevel — the
|
|
65
|
+
// caller treats that as "no known root, do not guess".
|
|
66
|
+
export function resolveProjectRoot() {
|
|
63
67
|
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
64
68
|
if (fromEnv)
|
|
65
69
|
return fromEnv;
|
package/lib/plugin-metadata.mjs
CHANGED
|
@@ -497,6 +497,14 @@ export function getPluginHooksConfig() {
|
|
|
497
497
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/workflow-distill.mjs\"",
|
|
498
498
|
timeout: 5,
|
|
499
499
|
};
|
|
500
|
+
const typecheckStopCommand = {
|
|
501
|
+
type: "command",
|
|
502
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/typecheck-stop.mjs\"",
|
|
503
|
+
// Longer than the 5s hooks: tsc is slower. Opt-in via CLAUDE_TYPECHECK_GATE
|
|
504
|
+
// (off by default) and near-zero cost when off / no TS file changed; on an
|
|
505
|
+
// internal timeout it fails open (allow) rather than blocking.
|
|
506
|
+
timeout: 30,
|
|
507
|
+
};
|
|
500
508
|
const routePromptCommand = {
|
|
501
509
|
type: "command",
|
|
502
510
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/route-prompt.mjs\"",
|
|
@@ -508,7 +516,7 @@ export function getPluginHooksConfig() {
|
|
|
508
516
|
timeout: 5,
|
|
509
517
|
};
|
|
510
518
|
return {
|
|
511
|
-
description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
519
|
+
description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, opt-in typecheck Stop gate, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
512
520
|
hooks: {
|
|
513
521
|
// gateguard runs FIRST on PreToolUse so its block decision short-circuits
|
|
514
522
|
// before companion-preference sees the call. companion-preference runs
|
|
@@ -532,7 +540,7 @@ export function getPluginHooksConfig() {
|
|
|
532
540
|
UserPromptSubmit: [{ hooks: [routePromptCommand, recallBriefingCommand] }],
|
|
533
541
|
SessionStart: [{ hooks: [sessionCommand] }],
|
|
534
542
|
SessionEnd: [{ hooks: [sessionCommand] }],
|
|
535
|
-
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand] }],
|
|
543
|
+
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand, typecheckStopCommand] }],
|
|
536
544
|
},
|
|
537
545
|
};
|
|
538
546
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision helpers for the typecheck Stop gate (RISA 4 / G4).
|
|
3
|
+
*
|
|
4
|
+
* The hook (src/hooks/typecheck-stop.mts) does the I/O — git, spawning the
|
|
5
|
+
* typecheck, emitting the decision. Everything here is pure and unit-tested so
|
|
6
|
+
* the mode/selection/formatting logic is provable without spawning a compiler.
|
|
7
|
+
*
|
|
8
|
+
* Mode via CLAUDE_TYPECHECK_GATE: "off" (default) | "warn" | "block".
|
|
9
|
+
* - off : no-op. The default. The global ~/.claude typecheck-changed.sh
|
|
10
|
+
* already emits an advisory systemMessage; this hook stays silent
|
|
11
|
+
* until opted in, so there is no double-advisory and no regression.
|
|
12
|
+
* - warn : print a one-line notice to stderr; never blocks.
|
|
13
|
+
* - block : emit {"decision":"block","reason":...} so the failure re-enters
|
|
14
|
+
* model context — the fix headless/autonomous -p loops need.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveTypecheckMode(raw) {
|
|
17
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
18
|
+
return value === "block" || value === "warn" ? value : "off";
|
|
19
|
+
}
|
|
20
|
+
// Matches the extensions typecheck-changed.sh screens for. `.d.ts` ends in
|
|
21
|
+
// `.ts` so it counts — a changed ambient declaration can still break the build.
|
|
22
|
+
const TS_FILE_RE = /\.(ts|tsx|mts|cts)$/;
|
|
23
|
+
export function hasChangedTsFile(files) {
|
|
24
|
+
return files.some((file) => TS_FILE_RE.test(file.trim()));
|
|
25
|
+
}
|
|
26
|
+
// Split a `git diff --name-only` blob into a clean path list (drops blank lines).
|
|
27
|
+
export function parseChangedFiles(gitOutput) {
|
|
28
|
+
return (gitOutput ?? "")
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.filter((line) => line.length > 0);
|
|
32
|
+
}
|
|
33
|
+
// Prefer the project's own `typecheck` npm script (authoritative — it encodes
|
|
34
|
+
// the project's exact flags), else a local tsc, else null (a TS project with no
|
|
35
|
+
// runnable typecheck stays silent rather than guessing).
|
|
36
|
+
export function pickTypecheckKind(hasNpmTypecheckScript, hasLocalTsc) {
|
|
37
|
+
if (hasNpmTypecheckScript)
|
|
38
|
+
return "npm";
|
|
39
|
+
if (hasLocalTsc)
|
|
40
|
+
return "tsc";
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
export function formatTypecheckReason(output, maxLines = 15) {
|
|
44
|
+
const tail = (output ?? "")
|
|
45
|
+
.split(/\r?\n/)
|
|
46
|
+
.filter((line) => line.length > 0)
|
|
47
|
+
.slice(-maxLines)
|
|
48
|
+
.join("\n");
|
|
49
|
+
return `Typecheck FAILED on changed TS files — fix before ending the turn:\n${tail}`;
|
|
50
|
+
}
|
|
51
|
+
// The single decision point. `ranTypecheck` is false when the gate short-circuited
|
|
52
|
+
// (mode off, no TS project, no changed TS file, no runnable typecheck, or the run
|
|
53
|
+
// timed out) — all of which allow. A conclusive non-zero rc maps to the mode.
|
|
54
|
+
export function decideTypecheckAction(input) {
|
|
55
|
+
if (input.mode === "off")
|
|
56
|
+
return "allow";
|
|
57
|
+
if (!input.ranTypecheck)
|
|
58
|
+
return "allow";
|
|
59
|
+
if (input.rc === 0)
|
|
60
|
+
return "allow";
|
|
61
|
+
return input.mode;
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.19.0",
|
|
4
|
+
"description": "Claude Code that gets sharper every session: the persistent-memory and runtime-discipline layer built on the 7 Laws of AI Agent Discipline. It 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. Shipped as 27 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. Beginner: one /plugin install command. Expert: adds MCP tools and session hooks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
|
+
"claude-code-plugin",
|
|
7
8
|
"claude-code-skill",
|
|
9
|
+
"claude",
|
|
10
|
+
"anthropic",
|
|
8
11
|
"ai-agent",
|
|
12
|
+
"ai-agents",
|
|
9
13
|
"agent-skill",
|
|
10
14
|
"ai-discipline",
|
|
11
15
|
"mulahazah",
|
|
12
|
-
"instinct",
|
|
16
|
+
"instinct-learning",
|
|
13
17
|
"hooks",
|
|
14
18
|
"mcp",
|
|
15
|
-
"mcp-server",
|
|
16
19
|
"github-action",
|
|
17
20
|
"transcript-linter"
|
|
18
21
|
],
|
|
@@ -30,7 +33,9 @@
|
|
|
30
33
|
"continuous-improvement": "bin/install.mjs",
|
|
31
34
|
"ci-lint-transcript": "bin/lint-transcript.mjs",
|
|
32
35
|
"ci": "bin/unified-cli.mjs",
|
|
33
|
-
"ci-plan-pack": "bin/plan-pack.mjs"
|
|
36
|
+
"ci-plan-pack": "bin/plan-pack.mjs",
|
|
37
|
+
"ci-audit-actions": "bin/audit-actions.mjs",
|
|
38
|
+
"ci-portfolio-health": "bin/portfolio-health.mjs"
|
|
34
39
|
},
|
|
35
40
|
"scripts": {
|
|
36
41
|
"build": "tsc -p tsconfig.json && node bin/generate-plugin-manifests.mjs && node -e \"const fs=require('node:fs'); for (const f of fs.readdirSync('bin')) { if (f.endsWith('.mjs')) fs.chmodSync('bin/'+f, 0o755); } for (const f of fs.readdirSync('hooks')) { if (f.endsWith('.mjs')) fs.chmodSync('hooks/'+f, 0o755); } for (const f of fs.readdirSync('lib')) { if (f.endsWith('.mjs')) fs.chmodSync('lib/'+f, 0o755); } for (const f of fs.readdirSync('plugins/continuous-improvement/bin')) { if (f.endsWith('.mjs')) fs.chmodSync('plugins/continuous-improvement/bin/'+f, 0o755); } for (const f of fs.readdirSync('plugins/continuous-improvement/lib')) { if (f.endsWith('.mjs')) fs.chmodSync('plugins/continuous-improvement/lib/'+f, 0o755); } for (const f of fs.readdirSync('plugins/continuous-improvement/hooks')) { if (f.endsWith('.mjs')) fs.chmodSync('plugins/continuous-improvement/hooks/'+f, 0o755); } for (const f of fs.readdirSync('scripts')) { if (f.endsWith('.mjs')) fs.chmodSync('scripts/'+f, 0o755); } for (const f of fs.readdirSync('synthetic-checks')) { if (f.endsWith('.mjs')) fs.chmodSync('synthetic-checks/'+f, 0o755); } \"",
|
|
@@ -45,6 +50,7 @@
|
|
|
45
50
|
"verify:skill-law-tag": "node bin/check-skill-law-tag.mjs",
|
|
46
51
|
"verify:skill-count": "node bin/check-skill-count.mjs",
|
|
47
52
|
"verify:skill-count-prose": "node bin/check-skill-count-prose.mjs",
|
|
53
|
+
"verify:command-count": "node bin/check-command-count.mjs",
|
|
48
54
|
"verify:docs-substrings": "node bin/check-docs-substrings.mjs",
|
|
49
55
|
"verify:everything-mirror": "node bin/check-everything-mirror.mjs",
|
|
50
56
|
"verify:routing-targets": "node bin/check-routing-targets.mjs",
|
|
@@ -53,7 +59,7 @@
|
|
|
53
59
|
"verify:scripts-citation-drift": "node bin/check-scripts-citation-drift.mjs",
|
|
54
60
|
"verify:third-party-shape": "node bin/check-third-party-shape.mjs",
|
|
55
61
|
"verify:tool-count": "node bin/check-tool-count.mjs",
|
|
56
|
-
"verify:all": "npm run verify:skill-mirror && npm run verify:skill-tiers && npm run verify:skill-law-tag && npm run verify:skill-count && npm run verify:skill-count-prose && npm run verify:docs-substrings && npm run verify:everything-mirror && npm run verify:routing-targets && npm run verify:doc-runtime-claims && npm run verify:test-imports-only && npm run verify:scripts-citation-drift && npm run verify:third-party-shape && npm run verify:tool-count && npm run typecheck"
|
|
62
|
+
"verify:all": "npm run verify:skill-mirror && npm run verify:skill-tiers && npm run verify:skill-law-tag && npm run verify:skill-count && npm run verify:skill-count-prose && npm run verify:command-count && npm run verify:docs-substrings && npm run verify:everything-mirror && npm run verify:routing-targets && npm run verify:doc-runtime-claims && npm run verify:test-imports-only && npm run verify:scripts-citation-drift && npm run verify:third-party-shape && npm run verify:tool-count && npm run typecheck"
|
|
57
63
|
},
|
|
58
64
|
"files": [
|
|
59
65
|
".claude-plugin/",
|
package/plugins/beginner.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.0",
|
|
4
4
|
"mode": "beginner",
|
|
5
5
|
"description": "Beginner mode: see what your agent learned, list its instincts, and request a session reflection. Bundles three grounding skills (gateguard, tdd-workflow, verification-loop) so research, memory, tests, and verification happen by default — every edit starts from facts, not guesses.",
|
|
6
6
|
"tools": [
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "continuous-improvement",
|
|
10
10
|
"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 27 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.",
|
|
11
|
-
"version": "3.
|
|
11
|
+
"version": "3.19.0",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "naimkatiman"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.0",
|
|
4
4
|
"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 27 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.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "naimkatiman",
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reconcile
|
|
3
|
-
description: Establish git ground truth (branch, status, stashes, worktrees, ahead/behind) before any mutation, halt on protected or destructive operations,
|
|
3
|
+
description: Establish git ground truth (branch, status, stashes, worktrees, ahead/behind) before any mutation, halt on protected or destructive operations, then carry the known-good state through a single-concern commit, a push, an open PR, and — after the PR merges — a fast-forward of the default branch. Enforces Law 1 (Research Before Executing).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# /reconcile — Ground
|
|
6
|
+
# /reconcile — Ground Truth, Then Commit, Push, and Open the PR
|
|
7
7
|
|
|
8
|
-
Read the repo's real state before acting on it: a branch that shifted, a push that did not land, or another session mid-merge will burn a whole session if you assume instead of check.
|
|
8
|
+
Read the repo's real state before acting on it: a branch that shifted, a push that did not land, or another session mid-merge will burn a whole session if you assume instead of check. Once the state is known, `/reconcile` carries the work through to an open PR and back to an up-to-date default branch.
|
|
9
9
|
|
|
10
10
|
## What it does
|
|
11
11
|
|
|
12
|
-
Snapshots the full git state in one pass, detects a concurrent writer, classifies the upstream relationship, then acts only on the known state — stopping at every operation that is hard to reverse. Backed by the `reconcile` skill.
|
|
12
|
+
Snapshots the full git state in one pass, detects a concurrent writer, classifies the upstream relationship, then acts only on the known state — stopping at every operation that is hard to reverse. When work is ready, it stages by filename, commits one concern, pushes a feature branch, verifies the push landed, and opens a PR. After a human merges, it fast-forwards the default branch and checks it out. Backed by the `reconcile` skill.
|
|
13
13
|
|
|
14
14
|
## Establish ground truth
|
|
15
15
|
|
|
16
16
|
```
|
|
17
17
|
git branch --show-current
|
|
18
18
|
git status --porcelain=v1 # but trust git diff --stat for real drift (autocrlf)
|
|
19
|
-
git rev-list --left-right --count @{u}...HEAD # behind / ahead
|
|
19
|
+
git rev-list --left-right --count '@{u}...HEAD' # behind / ahead (quote the ref — bare @{u} trips the Bash parser)
|
|
20
20
|
git stash list
|
|
21
21
|
git worktree list
|
|
22
22
|
ls .git/MERGE_HEAD .git/rebase-merge .git/rebase-apply 2>/dev/null # in-progress op = another actor; do not race
|
|
@@ -31,7 +31,22 @@ behind -> git pull --ff-only
|
|
|
31
31
|
diverged -> rebase/merge deliberately; never blind --force
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
STOP for authorization before: pushing to a protected branch (this repo = feature branch + PR, never direct push to main), `--force` / `--force-with-lease`, `reset --hard`, `clean -fd`, or removing a dirty worktree. Never stage with `git add -A` on a Windows autocrlf tree (it commits phantom line-ending-only changes) — stage by explicit filename.
|
|
34
|
+
STOP for authorization before: pushing to a protected branch (this repo = feature branch + PR, never direct push to main), merging the PR you opened, `--force` / `--force-with-lease`, `reset --hard`, `clean -fd`, force-deleting a branch (`branch -D`), or removing a dirty worktree. Never stage with `git add -A` on a Windows autocrlf tree (it commits phantom line-ending-only changes) — stage by explicit filename.
|
|
35
|
+
|
|
36
|
+
## Commit and open the PR (self-contained)
|
|
37
|
+
|
|
38
|
+
Reimplements the commit → push → PR tail inline, so it works with no companion plugin installed:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
git switch main && git pull --ff-only origin main # branch from a fresh base
|
|
42
|
+
git switch -c <type>/<slug> # only if not already on a feature branch
|
|
43
|
+
git add path/one path/two # stage by name, one concern
|
|
44
|
+
git commit -m "feat(scope): <observable outcome>" # single-line -m; never a multi-line here-doc on Windows
|
|
45
|
+
git push -u origin <type>/<slug>
|
|
46
|
+
gh pr create --fill --base main # open one PR, then STOP — the merge is a human decision
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`/reconcile` never merges the PR, never uses `--admin` / `--force` / `--no-verify`, never auto-merges on green CI, and never deploys.
|
|
35
50
|
|
|
36
51
|
## Verify the push landed
|
|
37
52
|
|
|
@@ -39,9 +54,21 @@ STOP for authorization before: pushing to a protected branch (this repo = featur
|
|
|
39
54
|
git ls-remote origin refs/heads/<branch> # remote tip must equal local HEAD, else it did not land
|
|
40
55
|
```
|
|
41
56
|
|
|
57
|
+
## Sync the default branch after the PR merges
|
|
58
|
+
|
|
59
|
+
"Latest work on main" is true only once the PR merges, and on a protected branch that merge is a human action. After it lands:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
git switch main # or master
|
|
63
|
+
git pull --ff-only origin main # fast-forward only; if it will not ff, main diverged — re-survey, do not force
|
|
64
|
+
git rev-parse HEAD # confirm this equals the squash-merge SHA
|
|
65
|
+
git branch -d <type>/<slug> # delete the merged feature branch (safe -d, never -D)
|
|
66
|
+
```
|
|
67
|
+
|
|
42
68
|
## Pairs with
|
|
43
69
|
|
|
44
70
|
- **`reconcile`** skill — the discipline this command runs.
|
|
45
71
|
- **`gateguard`** / **`safety-guard`** — runtime + destructive-op guardrails.
|
|
46
72
|
- **`recall`** — recall whether the same git op failed here before.
|
|
47
|
-
- **`audit`** — the loop that often produces the fix
|
|
73
|
+
- **`audit`** — the loop that often produces the fix `/reconcile` then ships.
|
|
74
|
+
- **`/ship`** — the TDD-gated single-defect variant; `commit-commands:commit-push-pr` is the external-plugin equivalent of the commit → PR tail.
|
|
@@ -35,7 +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 { MAX_CLEARED_FILES, canonicalizeFileKey, isCapReached, isFileCleared, loadState, markFileCleared, resolveSessionDir, saveState, } from "../lib/gateguard-state.mjs";
|
|
38
|
+
import { MAX_CLEARED_FILES, canonicalizeFileKey, canonicalizeProjectRoot, isCapReached, isFileCleared, loadState, markFileCleared, resolveProjectRoot, resolveSessionDir, saveState, } from "../lib/gateguard-state.mjs";
|
|
39
39
|
const TOOL_ROUTE = {
|
|
40
40
|
Read: "allow",
|
|
41
41
|
Grep: "allow",
|
|
@@ -69,8 +69,20 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
69
69
|
"Remove-Item -Recurse",
|
|
70
70
|
"Remove-Item -Force",
|
|
71
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
|
+
}
|
|
72
84
|
function isDestructiveBash(command) {
|
|
73
|
-
const lower = command.toLowerCase();
|
|
85
|
+
const lower = stripMessageArgs(command).toLowerCase();
|
|
74
86
|
return DESTRUCTIVE_PATTERNS.some((p) => lower.includes(p.toLowerCase()));
|
|
75
87
|
}
|
|
76
88
|
function classifyTool(toolName, toolInput) {
|
|
@@ -95,6 +107,60 @@ function extractFilePaths(toolInput) {
|
|
|
95
107
|
return [toolInput.command];
|
|
96
108
|
return [];
|
|
97
109
|
}
|
|
110
|
+
// --- Path exclusions -------------------------------------------------------
|
|
111
|
+
// Opt-in: skip the fact-forcing gate for low-risk paths a user edits
|
|
112
|
+
// constantly (an LLM-maintained prose wiki, a generated scratch dir). Set the
|
|
113
|
+
// CI_GATEGUARD_EXCLUDE env var to a comma-separated list of path substrings;
|
|
114
|
+
// each is matched case-insensitively against the forward-slash-normalized file
|
|
115
|
+
// path. Unset/empty (the default) changes nothing — every mutating file call is
|
|
116
|
+
// gated exactly as before. A call whose targets mix excluded and non-excluded
|
|
117
|
+
// paths still gates the non-excluded ones.
|
|
118
|
+
const EXCLUDE_FRAGMENTS = String(process.env.CI_GATEGUARD_EXCLUDE ?? "")
|
|
119
|
+
.split(",")
|
|
120
|
+
.map((fragment) => fragment.trim().replace(/\\/g, "/").toLowerCase())
|
|
121
|
+
.filter((fragment) => fragment !== "");
|
|
122
|
+
function isExcludedPath(filePath) {
|
|
123
|
+
if (EXCLUDE_FRAGMENTS.length === 0 || typeof filePath !== "string" || filePath === "") {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const normalized = filePath.replace(/\\/g, "/").toLowerCase();
|
|
127
|
+
return EXCLUDE_FRAGMENTS.some((fragment) => normalized.includes(fragment));
|
|
128
|
+
}
|
|
129
|
+
// --- Target lock (opt-in) --------------------------------------------------
|
|
130
|
+
// A fact-list can't catch a wrong-repo / wrong-worktree write — you can present
|
|
131
|
+
// perfect facts about the wrong file. CI_GATEGUARD_TARGET_LOCK=block denies a
|
|
132
|
+
// mutating call whose ABSOLUTE target canonicalizes outside the session project
|
|
133
|
+
// root. Default (unset) checks nothing, so existing sessions — including
|
|
134
|
+
// legitimate out-of-root edits to ~/.claude or /tmp — are unaffected. This is
|
|
135
|
+
// the warn-first rollout: ship non-enforcing, flip to block per session.
|
|
136
|
+
const TARGET_LOCK_ON = String(process.env.CI_GATEGUARD_TARGET_LOCK ?? "").toLowerCase() === "block";
|
|
137
|
+
// Relative paths resolve under cwd (= the project root) and always pass; only an
|
|
138
|
+
// absolute path into a different tree can be out-of-root. Drive-letter (d:/,
|
|
139
|
+
// D:\), POSIX-absolute (/x), and UNC (\\host) forms all count as absolute.
|
|
140
|
+
function isAbsolutePathString(p) {
|
|
141
|
+
return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("/") || p.startsWith("\\\\");
|
|
142
|
+
}
|
|
143
|
+
function isTargetOutsideRoot(filePath, projectRoot) {
|
|
144
|
+
if (!isAbsolutePathString(filePath))
|
|
145
|
+
return false;
|
|
146
|
+
const root = canonicalizeProjectRoot(projectRoot);
|
|
147
|
+
if (root === "global" || root === "")
|
|
148
|
+
return false; // no known root — do not guess
|
|
149
|
+
const target = canonicalizeFileKey(filePath);
|
|
150
|
+
return target !== root && !target.startsWith(`${root}/`);
|
|
151
|
+
}
|
|
152
|
+
function buildTargetLockReason(strayPath, projectRoot) {
|
|
153
|
+
return [
|
|
154
|
+
`Target is outside the session project root — refusing a possible wrong-repo / wrong-worktree write.`,
|
|
155
|
+
"",
|
|
156
|
+
` Target: ${strayPath.replace(/\\/g, "/")}`,
|
|
157
|
+
` Session root: ${canonicalizeProjectRoot(projectRoot)}`,
|
|
158
|
+
"",
|
|
159
|
+
"If this is intentional, confirm you are in the right worktree (cwd / CLAUDE_PROJECT_DIR),",
|
|
160
|
+
"or unset CI_GATEGUARD_TARGET_LOCK for this session. Target lock is opt-in; it fires only",
|
|
161
|
+
"when CI_GATEGUARD_TARGET_LOCK=block.",
|
|
162
|
+
].join("\n");
|
|
163
|
+
}
|
|
98
164
|
// The call site only reads this inside the block branch, where at least one
|
|
99
165
|
// path is uncleared; an all-cleared batch returns "" and is never consumed.
|
|
100
166
|
function firstUnclearedFilePath(toolInput, state) {
|
|
@@ -144,6 +210,47 @@ function buildMutatingFileReason(toolName, filePaths, stateFilePath) {
|
|
|
144
210
|
" `_gateguard_facts_presented: true`; Claude Code's strict schema rejects that, so use A or B.)",
|
|
145
211
|
].join("\n");
|
|
146
212
|
}
|
|
213
|
+
function findUnquotedBraceRef(command) {
|
|
214
|
+
let quote = null;
|
|
215
|
+
for (let i = 0; i < command.length; i++) {
|
|
216
|
+
const ch = command[i];
|
|
217
|
+
if (quote) {
|
|
218
|
+
if (ch === quote)
|
|
219
|
+
quote = null;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (ch === '"' || ch === "'") {
|
|
223
|
+
quote = ch;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (ch === "@" && command[i + 1] === "{") {
|
|
227
|
+
// Expand to the whitespace-delimited word that carries this @{ ref, then
|
|
228
|
+
// single-quote that whole word in the suggested fix.
|
|
229
|
+
let wordStart = i;
|
|
230
|
+
while (wordStart > 0 && !/\s/.test(command[wordStart - 1]))
|
|
231
|
+
wordStart--;
|
|
232
|
+
let wordEnd = i;
|
|
233
|
+
while (wordEnd < command.length && !/\s/.test(command[wordEnd]))
|
|
234
|
+
wordEnd++;
|
|
235
|
+
const word = command.slice(wordStart, wordEnd);
|
|
236
|
+
const braceEnd = command.indexOf("}", i);
|
|
237
|
+
const ref = braceEnd === -1 ? command.slice(i, wordEnd) : command.slice(i, braceEnd + 1);
|
|
238
|
+
const fixed = `${command.slice(0, wordStart)}'${word}'${command.slice(wordEnd)}`;
|
|
239
|
+
return { ref, fixed };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
function buildBraceRefReason(hit) {
|
|
245
|
+
return [
|
|
246
|
+
`Unquoted git ref ${hit.ref} — Claude Code's Bash parser trips on the braces and blocks this`,
|
|
247
|
+
"post-hoc, costing a retry. Quote the ref and run the SAME command:",
|
|
248
|
+
"",
|
|
249
|
+
` ${hit.fixed}`,
|
|
250
|
+
"",
|
|
251
|
+
"Single quotes stop the shell from touching the braces; git reads the ref as-is.",
|
|
252
|
+
].join("\n");
|
|
253
|
+
}
|
|
147
254
|
function buildDestructiveBashReason(command) {
|
|
148
255
|
return [
|
|
149
256
|
`Destructive command requested: ${command}`,
|
|
@@ -199,6 +306,16 @@ function main() {
|
|
|
199
306
|
const toolName = typeof payload.tool_name === "string" ? payload.tool_name : "";
|
|
200
307
|
const toolInput = payload.tool_input ?? {};
|
|
201
308
|
const gate = classifyTool(toolName, toolInput);
|
|
309
|
+
// Unquoted @{…} refs trip the built-in Bash parser — catch them first, for
|
|
310
|
+
// both routine and destructive commands, so the quoted fix surfaces before the
|
|
311
|
+
// opaque post-hoc block (and before the destructive rollback demand).
|
|
312
|
+
if (toolName === "Bash" && typeof toolInput.command === "string") {
|
|
313
|
+
const braceHit = findUnquotedBraceRef(toolInput.command);
|
|
314
|
+
if (braceHit) {
|
|
315
|
+
emitDeny(buildBraceRefReason(braceHit));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
202
319
|
if (gate === "allow") {
|
|
203
320
|
emitAllow();
|
|
204
321
|
return;
|
|
@@ -215,7 +332,24 @@ function main() {
|
|
|
215
332
|
const sessionDir = resolveSessionDir(sessionId);
|
|
216
333
|
const stateFilePath = join(sessionDir, "gateguard-session.json");
|
|
217
334
|
const state = loadState(sessionDir);
|
|
218
|
-
const
|
|
335
|
+
const allTargetPaths = extractFilePaths(toolInput);
|
|
336
|
+
const filePaths = allTargetPaths.filter((path) => !isExcludedPath(path));
|
|
337
|
+
if (allTargetPaths.length > 0 && filePaths.length === 0) {
|
|
338
|
+
emitAllow(); // every target is under a CI_GATEGUARD_EXCLUDE path; skip the gate
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Target lock runs before the fact gate and independent of clearance: a
|
|
342
|
+
// wrong-repo write is wrong even with perfect facts. Excluded paths were
|
|
343
|
+
// already filtered out above, so an explicitly-excluded scratch dir outside
|
|
344
|
+
// the root is never target-locked.
|
|
345
|
+
if (TARGET_LOCK_ON) {
|
|
346
|
+
const projectRoot = resolveProjectRoot();
|
|
347
|
+
const stray = filePaths.find((path) => isTargetOutsideRoot(path, projectRoot));
|
|
348
|
+
if (stray) {
|
|
349
|
+
emitDeny(buildTargetLockReason(stray, projectRoot));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
219
353
|
const filePath = firstUnclearedFilePath(toolInput, state);
|
|
220
354
|
const factsFlagged = toolInput._gateguard_facts_presented === true;
|
|
221
355
|
const alreadyCleared = filePaths.length > 0 && filePaths.every((path) => isFileCleared(state, path));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
2
|
+
"description": "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, opt-in typecheck Stop gate, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
@@ -93,6 +93,11 @@
|
|
|
93
93
|
"type": "command",
|
|
94
94
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/workflow-distill.mjs\"",
|
|
95
95
|
"timeout": 5
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/typecheck-stop.mjs\"",
|
|
100
|
+
"timeout": 30
|
|
96
101
|
}
|
|
97
102
|
]
|
|
98
103
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* typecheck-stop.mts — Stop hook that runs the project typecheck on changed TS
|
|
4
|
+
* files and, when opted in, returns a block decision so the failure re-enters
|
|
5
|
+
* model context and the agent fixes it before ending the turn — in all modes,
|
|
6
|
+
* including headless `-p` runs where Stop still fires (RISA 4 / G4).
|
|
7
|
+
*
|
|
8
|
+
* Ports the proven detection logic of ~/.claude/scripts/typecheck-changed.sh
|
|
9
|
+
* (skip non-TS repos, skip TS repos with no changed TS file, prefer the npm
|
|
10
|
+
* `typecheck` script) and adds the block mechanism of goal-drift-stop.mts.
|
|
11
|
+
*
|
|
12
|
+
* Mode via CLAUDE_TYPECHECK_GATE: "off" (default) | "warn" | "block".
|
|
13
|
+
* off : no-op (the global script's advisory systemMessage stays the default
|
|
14
|
+
* layer; this hook is the opt-in enforcement layer — zero regression).
|
|
15
|
+
* warn : one-line stderr notice; never blocks.
|
|
16
|
+
* block : {"decision":"block","reason":...} to re-prompt the model.
|
|
17
|
+
*
|
|
18
|
+
* Fail-open by construction: any error, missing root, non-TS repo, no changed
|
|
19
|
+
* TS file, no runnable typecheck, or a run that times out exits 0 (allow). No
|
|
20
|
+
* network. The wiring gives this hook a longer timeout than the 5s hooks
|
|
21
|
+
* because `tsc` is slower; on the internal timeout it fails open.
|
|
22
|
+
*/
|
|
23
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
24
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { decideTypecheckAction, formatTypecheckReason, hasChangedTsFile, parseChangedFiles, pickTypecheckKind, resolveTypecheckMode, } from "../lib/typecheck-gate.mjs";
|
|
27
|
+
const SPAWN_TIMEOUT_MS = 25_000;
|
|
28
|
+
function resolveProjectRoot() {
|
|
29
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
30
|
+
if (fromEnv && fromEnv.trim())
|
|
31
|
+
return fromEnv.trim();
|
|
32
|
+
try {
|
|
33
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
36
|
+
}).trim();
|
|
37
|
+
return root || null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function collectChangedFiles(root) {
|
|
44
|
+
const run = (args) => {
|
|
45
|
+
try {
|
|
46
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const unstaged = run(["diff", "--name-only", "--diff-filter=ACMR"]);
|
|
53
|
+
const staged = run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
|
|
54
|
+
return [...parseChangedFiles(unstaged), ...parseChangedFiles(staged)];
|
|
55
|
+
}
|
|
56
|
+
function hasNpmTypecheckScript(root) {
|
|
57
|
+
try {
|
|
58
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
59
|
+
return typeof pkg.scripts?.typecheck === "string";
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function localTscPath(root) {
|
|
66
|
+
// node_modules/typescript/bin/tsc is a Node script — invoking it via `node`
|
|
67
|
+
// is cross-platform, unlike node_modules/.bin/tsc (a shell script / .cmd).
|
|
68
|
+
const path = join(root, "node_modules", "typescript", "bin", "tsc");
|
|
69
|
+
return existsSync(path) ? path : null;
|
|
70
|
+
}
|
|
71
|
+
function runTypecheck(root) {
|
|
72
|
+
const tscPath = localTscPath(root);
|
|
73
|
+
const kind = pickTypecheckKind(hasNpmTypecheckScript(root), tscPath !== null);
|
|
74
|
+
if (!kind)
|
|
75
|
+
return { ran: false, rc: 0, output: "" };
|
|
76
|
+
const result = kind === "npm"
|
|
77
|
+
? spawnSync("npm", ["run", "--silent", "typecheck"], {
|
|
78
|
+
cwd: root,
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
shell: true,
|
|
81
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
82
|
+
})
|
|
83
|
+
: spawnSync(process.execPath, [tscPath, "--noEmit"], {
|
|
84
|
+
cwd: root,
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
87
|
+
});
|
|
88
|
+
// A timeout or spawn failure leaves error set / status null — inconclusive, so
|
|
89
|
+
// fail open (do not block on a check that never produced a verdict).
|
|
90
|
+
if (result.error || typeof result.status !== "number")
|
|
91
|
+
return { ran: false, rc: 0, output: "" };
|
|
92
|
+
return { ran: true, rc: result.status, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
|
|
93
|
+
}
|
|
94
|
+
function main() {
|
|
95
|
+
const mode = resolveTypecheckMode(process.env.CLAUDE_TYPECHECK_GATE);
|
|
96
|
+
if (mode === "off")
|
|
97
|
+
return;
|
|
98
|
+
const root = resolveProjectRoot();
|
|
99
|
+
if (!root || !existsSync(join(root, "tsconfig.json")))
|
|
100
|
+
return; // not a TS project
|
|
101
|
+
if (!hasChangedTsFile(collectChangedFiles(root)))
|
|
102
|
+
return; // nothing TS changed — near-zero cost
|
|
103
|
+
const { ran, rc, output } = runTypecheck(root);
|
|
104
|
+
const action = decideTypecheckAction({ mode, ranTypecheck: ran, rc });
|
|
105
|
+
if (action === "block") {
|
|
106
|
+
process.stdout.write(`${JSON.stringify({ decision: "block", reason: formatTypecheckReason(output) })}\n`);
|
|
107
|
+
}
|
|
108
|
+
else if (action === "warn") {
|
|
109
|
+
process.stderr.write(`[continuous-improvement] typecheck: ${formatTypecheckReason(output)}\n`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
main();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// fail open — never trap a turn on a hook bug
|
|
117
|
+
}
|
|
@@ -59,7 +59,11 @@ function sanitizeSessionId(sessionId) {
|
|
|
59
59
|
return "";
|
|
60
60
|
return sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
// Exported for the target-lock gate (RISA 2 / G2): the hook compares a mutating
|
|
63
|
+
// call's absolute target against this root to catch wrong-repo / wrong-worktree
|
|
64
|
+
// writes. Returns "global" when no CLAUDE_PROJECT_DIR and no git toplevel — the
|
|
65
|
+
// caller treats that as "no known root, do not guess".
|
|
66
|
+
export function resolveProjectRoot() {
|
|
63
67
|
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
64
68
|
if (fromEnv)
|
|
65
69
|
return fromEnv;
|
|
@@ -497,6 +497,14 @@ export function getPluginHooksConfig() {
|
|
|
497
497
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/workflow-distill.mjs\"",
|
|
498
498
|
timeout: 5,
|
|
499
499
|
};
|
|
500
|
+
const typecheckStopCommand = {
|
|
501
|
+
type: "command",
|
|
502
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/typecheck-stop.mjs\"",
|
|
503
|
+
// Longer than the 5s hooks: tsc is slower. Opt-in via CLAUDE_TYPECHECK_GATE
|
|
504
|
+
// (off by default) and near-zero cost when off / no TS file changed; on an
|
|
505
|
+
// internal timeout it fails open (allow) rather than blocking.
|
|
506
|
+
timeout: 30,
|
|
507
|
+
};
|
|
500
508
|
const routePromptCommand = {
|
|
501
509
|
type: "command",
|
|
502
510
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/route-prompt.mjs\"",
|
|
@@ -508,7 +516,7 @@ export function getPluginHooksConfig() {
|
|
|
508
516
|
timeout: 5,
|
|
509
517
|
};
|
|
510
518
|
return {
|
|
511
|
-
description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
519
|
+
description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, opt-in typecheck Stop gate, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
512
520
|
hooks: {
|
|
513
521
|
// gateguard runs FIRST on PreToolUse so its block decision short-circuits
|
|
514
522
|
// before companion-preference sees the call. companion-preference runs
|
|
@@ -532,7 +540,7 @@ export function getPluginHooksConfig() {
|
|
|
532
540
|
UserPromptSubmit: [{ hooks: [routePromptCommand, recallBriefingCommand] }],
|
|
533
541
|
SessionStart: [{ hooks: [sessionCommand] }],
|
|
534
542
|
SessionEnd: [{ hooks: [sessionCommand] }],
|
|
535
|
-
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand] }],
|
|
543
|
+
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand, typecheckStopCommand] }],
|
|
536
544
|
},
|
|
537
545
|
};
|
|
538
546
|
}
|