continuous-improvement 3.12.3 → 3.16.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 +45 -1
- package/LICENSE +21 -21
- package/QUICKSTART.md +1 -1
- package/README.md +29 -8
- package/bin/analyze.sh +161 -161
- package/bin/backfill.mjs +19 -19
- package/bin/check-doc-runtime-claims.mjs +0 -0
- package/bin/check-docs-substrings.mjs +0 -0
- package/bin/check-everything-mirror.mjs +0 -0
- package/bin/check-routing-targets.mjs +0 -0
- package/bin/check-scripts-citation-drift.mjs +0 -0
- package/bin/check-skill-count-prose.mjs +168 -0
- package/bin/check-skill-count.mjs +32 -1
- package/bin/check-skill-law-tag.mjs +0 -0
- package/bin/check-skill-mirror.mjs +0 -0
- package/bin/check-skill-tiers.mjs +0 -0
- package/bin/check-test-imports-only.mjs +1 -1
- package/bin/check-third-party-shape.mjs +0 -0
- package/bin/check-tool-count.mjs +0 -0
- package/bin/companion-preference-status.mjs +0 -0
- package/bin/gateguard-clear.mjs +0 -0
- package/bin/generate-plugin-manifests.mjs +2 -1
- package/bin/harvest-friction.mjs +0 -0
- package/bin/hook-stats.mjs +21 -21
- package/bin/install.mjs +120 -2
- package/bin/lint-transcript.mjs +15 -3
- package/bin/mcp-server.mjs +66 -8
- package/bin/observe.mjs +0 -0
- package/bin/plan-pack.mjs +77 -0
- package/bin/pre-commit-block-strays.sh +0 -0
- package/bin/refresh-third-party.mjs +0 -0
- package/bin/unified-cli.mjs +55 -410
- package/commands/continuous-improvement.md +115 -115
- package/commands/dashboard.md +56 -56
- package/commands/model-forward.md +13 -0
- package/commands/production-readiness-review.md +53 -0
- package/commands/roast.md +34 -0
- package/commands/seven-laws.md +16 -16
- package/commands/ship.md +57 -0
- package/commands/workspace-surface-audit.md +77 -77
- package/hooks/companion-preference.mjs +31 -19
- package/hooks/gateguard.mjs +38 -16
- package/hooks/goal-drift-stop.mjs +0 -0
- package/hooks/hook-pack.mjs +110 -0
- package/hooks/observe.sh +0 -0
- package/hooks/recall-briefing.mjs +0 -0
- package/hooks/route-prompt.mjs +0 -0
- package/hooks/session.sh +106 -106
- package/hooks/three-section-close.mjs +0 -0
- package/hooks/workflow-distill.mjs +145 -0
- package/instinct-packs/go.json +58 -58
- package/instinct-packs/meta.json +16 -16
- package/instinct-packs/python.json +58 -58
- package/instinct-packs/react.json +58 -58
- package/lib/cli-anything.mjs +0 -0
- package/lib/gateguard-state.mjs +62 -13
- package/lib/goal-drift-gate.mjs +0 -0
- package/lib/goal-state.mjs +8 -4
- package/lib/hook-pack-gate.mjs +65 -0
- package/lib/install-targets.mjs +121 -0
- package/lib/observe-event.mjs +0 -0
- package/lib/plan-review-packet.mjs +96 -0
- package/lib/plugin-metadata.mjs +30 -4
- package/lib/pm-marketplace.mjs +0 -0
- package/lib/recall-briefing.mjs +0 -0
- package/lib/recall-index.mjs +0 -0
- package/lib/resolve-home-dir.mjs +0 -0
- package/lib/skill-distill.mjs +141 -0
- package/lib/skill-tiers.mjs +0 -0
- package/lib/version-check.mjs +115 -0
- package/llms.txt +2 -2
- package/package.json +6 -4
- package/plugins/beginner.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
- package/plugins/continuous-improvement/LICENSE +21 -21
- package/plugins/continuous-improvement/README.md +1 -1
- package/plugins/continuous-improvement/bin/backfill.mjs +19 -19
- package/plugins/continuous-improvement/bin/gateguard-clear.mjs +0 -0
- package/plugins/continuous-improvement/bin/mcp-server.mjs +66 -8
- package/plugins/continuous-improvement/bin/observe.mjs +0 -0
- package/plugins/continuous-improvement/commands/continuous-improvement.md +115 -115
- package/plugins/continuous-improvement/commands/dashboard.md +56 -56
- package/plugins/continuous-improvement/commands/model-forward.md +13 -0
- package/plugins/continuous-improvement/commands/production-readiness-review.md +53 -0
- package/plugins/continuous-improvement/commands/roast.md +34 -0
- package/plugins/continuous-improvement/commands/seven-laws.md +16 -16
- package/plugins/continuous-improvement/commands/ship.md +57 -0
- package/plugins/continuous-improvement/commands/workspace-surface-audit.md +77 -77
- package/plugins/continuous-improvement/hooks/companion-preference.mjs +31 -19
- package/plugins/continuous-improvement/hooks/gateguard.mjs +38 -16
- package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +0 -0
- package/plugins/continuous-improvement/hooks/hook-pack.mjs +110 -0
- package/plugins/continuous-improvement/hooks/hooks.json +16 -1
- package/plugins/continuous-improvement/hooks/observe.sh +0 -0
- package/plugins/continuous-improvement/hooks/recall-briefing.mjs +0 -0
- package/plugins/continuous-improvement/hooks/route-prompt.mjs +0 -0
- package/plugins/continuous-improvement/hooks/session.sh +106 -106
- package/plugins/continuous-improvement/hooks/three-section-close.mjs +0 -0
- package/plugins/continuous-improvement/hooks/workflow-distill.mjs +145 -0
- package/plugins/continuous-improvement/instinct-packs/go.json +58 -58
- package/plugins/continuous-improvement/instinct-packs/meta.json +16 -16
- package/plugins/continuous-improvement/instinct-packs/python.json +58 -58
- package/plugins/continuous-improvement/instinct-packs/react.json +58 -58
- package/plugins/continuous-improvement/lib/gateguard-state.mjs +62 -13
- package/plugins/continuous-improvement/lib/goal-drift-gate.mjs +0 -0
- package/plugins/continuous-improvement/lib/goal-state.mjs +8 -4
- package/plugins/continuous-improvement/lib/hook-pack-gate.mjs +65 -0
- package/plugins/continuous-improvement/lib/observe-event.mjs +0 -0
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +30 -4
- package/plugins/continuous-improvement/lib/recall-briefing.mjs +0 -0
- package/plugins/continuous-improvement/lib/recall-index.mjs +0 -0
- package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +0 -0
- package/plugins/continuous-improvement/lib/skill-distill.mjs +141 -0
- package/plugins/continuous-improvement/skills/README.md +2 -1
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +4 -4
- package/plugins/continuous-improvement/skills/goal-monitor/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/handoff/SKILL.md +0 -1
- package/plugins/continuous-improvement/skills/model-forward/SKILL.md +44 -0
- package/plugins/continuous-improvement/skills/roast/SKILL.md +108 -0
- package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +12 -32
- package/plugins/continuous-improvement/skills/tdd-workflow/SKILL.md +411 -411
- package/plugins/expert.json +5 -1
- package/skills/README.md +5 -3
- package/skills/gateguard.md +4 -4
- package/skills/goal-monitor.md +1 -1
- package/skills/handoff.md +0 -1
- package/skills/model-forward.md +44 -0
- package/skills/roast.md +108 -0
- package/skills/strategic-compact.md +12 -32
- package/skills/tdd-workflow.md +411 -411
- package/lib/compound-engineering.mjs +0 -831
- package/lib/pm-skills.mjs +0 -1274
- package/lib/unified-plugin.mjs +0 -924
- package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +0 -108
- package/skills/para-memory-files.md +0 -108
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"id": "react-check-existing-components",
|
|
4
|
-
"trigger": "when creating a new React component",
|
|
5
|
-
"body": "Search the codebase for existing components that solve the same problem before creating new ones. Check shared/, components/, and ui/ directories.",
|
|
6
|
-
"confidence": 0.65,
|
|
7
|
-
"domain": "workflow"
|
|
8
|
-
},
|
|
9
|
-
{
|
|
10
|
-
"id": "react-prefer-server-components",
|
|
11
|
-
"trigger": "when creating components in Next.js App Router",
|
|
12
|
-
"body": "Default to Server Components. Only add 'use client' when the component needs useState, useEffect, event handlers, or browser APIs.",
|
|
13
|
-
"confidence": 0.7,
|
|
14
|
-
"domain": "patterns"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"id": "react-key-prop-lists",
|
|
18
|
-
"trigger": "when rendering lists with .map()",
|
|
19
|
-
"body": "Always use a stable, unique key prop. Never use array index as key unless the list is static and never reordered.",
|
|
20
|
-
"confidence": 0.7,
|
|
21
|
-
"domain": "patterns"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"id": "react-effect-cleanup",
|
|
25
|
-
"trigger": "when writing useEffect with subscriptions or timers",
|
|
26
|
-
"body": "Always return a cleanup function from useEffect when setting up subscriptions, event listeners, or timers to prevent memory leaks.",
|
|
27
|
-
"confidence": 0.7,
|
|
28
|
-
"domain": "patterns"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"id": "react-memo-expensive",
|
|
32
|
-
"trigger": "when a component re-renders with expensive calculations",
|
|
33
|
-
"body": "Use useMemo for expensive computations and React.memo for components that receive the same props frequently. Don't memo everything — only what's measurably slow.",
|
|
34
|
-
"confidence": 0.6,
|
|
35
|
-
"domain": "patterns"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"id": "react-form-validation",
|
|
39
|
-
"trigger": "when building forms",
|
|
40
|
-
"body": "Check if react-hook-form or zod is already in the project before building custom form validation. Prefer library solutions over hand-rolled validation.",
|
|
41
|
-
"confidence": 0.65,
|
|
42
|
-
"domain": "tooling"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"id": "react-error-boundary",
|
|
46
|
-
"trigger": "when adding a new page or route",
|
|
47
|
-
"body": "Ensure error boundaries exist around new pages/routes. In Next.js App Router, add error.tsx. In other React apps, wrap with ErrorBoundary component.",
|
|
48
|
-
"confidence": 0.6,
|
|
49
|
-
"domain": "patterns"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"id": "react-test-user-behavior",
|
|
53
|
-
"trigger": "when writing React component tests",
|
|
54
|
-
"body": "Test user behavior, not implementation details. Use @testing-library/react. Query by role, label, or text — not by class name or test ID.",
|
|
55
|
-
"confidence": 0.65,
|
|
56
|
-
"domain": "testing"
|
|
57
|
-
}
|
|
58
|
-
]
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "react-check-existing-components",
|
|
4
|
+
"trigger": "when creating a new React component",
|
|
5
|
+
"body": "Search the codebase for existing components that solve the same problem before creating new ones. Check shared/, components/, and ui/ directories.",
|
|
6
|
+
"confidence": 0.65,
|
|
7
|
+
"domain": "workflow"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "react-prefer-server-components",
|
|
11
|
+
"trigger": "when creating components in Next.js App Router",
|
|
12
|
+
"body": "Default to Server Components. Only add 'use client' when the component needs useState, useEffect, event handlers, or browser APIs.",
|
|
13
|
+
"confidence": 0.7,
|
|
14
|
+
"domain": "patterns"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "react-key-prop-lists",
|
|
18
|
+
"trigger": "when rendering lists with .map()",
|
|
19
|
+
"body": "Always use a stable, unique key prop. Never use array index as key unless the list is static and never reordered.",
|
|
20
|
+
"confidence": 0.7,
|
|
21
|
+
"domain": "patterns"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "react-effect-cleanup",
|
|
25
|
+
"trigger": "when writing useEffect with subscriptions or timers",
|
|
26
|
+
"body": "Always return a cleanup function from useEffect when setting up subscriptions, event listeners, or timers to prevent memory leaks.",
|
|
27
|
+
"confidence": 0.7,
|
|
28
|
+
"domain": "patterns"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "react-memo-expensive",
|
|
32
|
+
"trigger": "when a component re-renders with expensive calculations",
|
|
33
|
+
"body": "Use useMemo for expensive computations and React.memo for components that receive the same props frequently. Don't memo everything — only what's measurably slow.",
|
|
34
|
+
"confidence": 0.6,
|
|
35
|
+
"domain": "patterns"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "react-form-validation",
|
|
39
|
+
"trigger": "when building forms",
|
|
40
|
+
"body": "Check if react-hook-form or zod is already in the project before building custom form validation. Prefer library solutions over hand-rolled validation.",
|
|
41
|
+
"confidence": 0.65,
|
|
42
|
+
"domain": "tooling"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "react-error-boundary",
|
|
46
|
+
"trigger": "when adding a new page or route",
|
|
47
|
+
"body": "Ensure error boundaries exist around new pages/routes. In Next.js App Router, add error.tsx. In other React apps, wrap with ErrorBoundary component.",
|
|
48
|
+
"confidence": 0.6,
|
|
49
|
+
"domain": "patterns"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "react-test-user-behavior",
|
|
53
|
+
"trigger": "when writing React component tests",
|
|
54
|
+
"body": "Test user behavior, not implementation details. Use @testing-library/react. Query by role, label, or text — not by class name or test ID.",
|
|
55
|
+
"confidence": 0.65,
|
|
56
|
+
"domain": "testing"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
@@ -2,20 +2,25 @@
|
|
|
2
2
|
* Gateguard per-session state.
|
|
3
3
|
*
|
|
4
4
|
* State file: <sessionDir>/gateguard-session.json. sessionDir resolves to
|
|
5
|
-
* GATEGUARD_SESSION_DIR (env override, used by tests) or
|
|
6
|
-
* ~/.claude/instincts/<projectHash>/
|
|
5
|
+
* GATEGUARD_SESSION_DIR (env override, used by tests), or in production to
|
|
6
|
+
* ~/.claude/instincts/<projectHash>/sessions/<sessionId>/ when the hook passes
|
|
7
|
+
* the stdin session_id, falling back to ~/.claude/instincts/<projectHash>/ when
|
|
8
|
+
* no session id is available. Per-session scoping is what stops the clearance
|
|
9
|
+
* cap from bleeding across concurrent same-day sessions on a multi-Claude host.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
11
|
+
* Limitations and guarantees:
|
|
9
12
|
* - Honor system: clearance is granted whenever the agent sets
|
|
10
13
|
* `_gateguard_facts_presented: true` in tool_input or has a prior per-file
|
|
11
14
|
* marker. The hook cannot verify that real investigation occurred.
|
|
12
15
|
* - State-file deletion: rm'ing the state file resets every gate in the
|
|
13
|
-
* session. Defensible because the session
|
|
14
|
-
*
|
|
16
|
+
* session. Defensible because the session is the trust boundary — and with
|
|
17
|
+
* per-session scoping that boundary is now real, not just asserted.
|
|
18
|
+
* - Self-heal: loadState treats a file older than STATE_TTL_MS (or one with an
|
|
19
|
+
* unparseable created_at) as empty, so a stale gate never needs a manual rm.
|
|
15
20
|
* - Concurrency: two parallel hook invocations can race the read+write.
|
|
16
21
|
* Acceptable trade-off vs OS-specific atomic-rename complexity on Windows.
|
|
17
|
-
* - Cap: MAX_CLEARED_FILES caps the
|
|
18
|
-
*
|
|
22
|
+
* - Cap: MAX_CLEARED_FILES caps the distinct files ONE session can clear,
|
|
23
|
+
* bounding stuck-loop / rogue-agent damage without affecting other sessions.
|
|
19
24
|
*/
|
|
20
25
|
import { createHash } from "node:crypto";
|
|
21
26
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -23,14 +28,36 @@ import { execFileSync } from "node:child_process";
|
|
|
23
28
|
import { homedir } from "node:os";
|
|
24
29
|
import { join } from "node:path";
|
|
25
30
|
export const MAX_CLEARED_FILES = 50;
|
|
26
|
-
|
|
31
|
+
// A clearance file whose created_at is older than this self-heals to empty on
|
|
32
|
+
// the next load, so a stale gate never needs a manual rm. Long enough not to
|
|
33
|
+
// reset an active session mid-work; the real cross-session fix is the
|
|
34
|
+
// per-session dir in resolveSessionDir below.
|
|
35
|
+
export const STATE_TTL_MS = 12 * 60 * 60 * 1000; // 12h
|
|
36
|
+
// The root that holds every project's gateguard + instinct state. The MCP
|
|
37
|
+
// clearance route uses this to bound a caller-supplied state_path to this tree
|
|
38
|
+
// so a hostile argument can't turn the clear into an arbitrary-write primitive.
|
|
39
|
+
export function resolveInstinctsRoot() {
|
|
40
|
+
const home = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
41
|
+
return join(home, ".claude", "instincts");
|
|
42
|
+
}
|
|
43
|
+
export function resolveSessionDir(sessionId) {
|
|
27
44
|
const fromEnv = process.env.GATEGUARD_SESSION_DIR;
|
|
28
45
|
if (fromEnv)
|
|
29
46
|
return fromEnv;
|
|
30
|
-
const home = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
31
47
|
const projectRoot = resolveProjectRoot();
|
|
32
48
|
const projectHash = createHash("sha256").update(canonicalizeProjectRoot(projectRoot)).digest("hex").slice(0, 12);
|
|
33
|
-
|
|
49
|
+
const base = join(resolveInstinctsRoot(), projectHash);
|
|
50
|
+
const scoped = sanitizeSessionId(sessionId);
|
|
51
|
+
return scoped ? join(base, "sessions", scoped) : base;
|
|
52
|
+
}
|
|
53
|
+
// A session id flows into a directory name. Strip anything outside a safe
|
|
54
|
+
// alphabet so a hostile or malformed id can't traverse (`../`) or break the
|
|
55
|
+
// path, and cap the length. Returns "" for an absent/empty id, which the caller
|
|
56
|
+
// treats as "no session" — the legacy unscoped dir, preserving back-compat.
|
|
57
|
+
function sanitizeSessionId(sessionId) {
|
|
58
|
+
if (!sessionId)
|
|
59
|
+
return "";
|
|
60
|
+
return sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
34
61
|
}
|
|
35
62
|
function resolveProjectRoot() {
|
|
36
63
|
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
@@ -49,21 +76,43 @@ function resolveProjectRoot() {
|
|
|
49
76
|
}
|
|
50
77
|
return "global";
|
|
51
78
|
}
|
|
79
|
+
function freshState() {
|
|
80
|
+
return { created_at: new Date().toISOString(), cleared_files: {} };
|
|
81
|
+
}
|
|
82
|
+
// A clearance file self-heals to empty once it ages out of STATE_TTL_MS, so a
|
|
83
|
+
// stale gate never needs a manual rm. An unparseable created_at fails closed
|
|
84
|
+
// (treated as expired) rather than living forever as a never-resetting gate.
|
|
85
|
+
function isExpired(createdAt) {
|
|
86
|
+
const created = Date.parse(createdAt);
|
|
87
|
+
if (Number.isNaN(created))
|
|
88
|
+
return true;
|
|
89
|
+
return Date.now() - created > STATE_TTL_MS;
|
|
90
|
+
}
|
|
52
91
|
export function loadState(sessionDir) {
|
|
53
92
|
const path = join(sessionDir, "gateguard-session.json");
|
|
54
93
|
if (!existsSync(path)) {
|
|
55
|
-
return
|
|
94
|
+
return freshState();
|
|
56
95
|
}
|
|
57
96
|
try {
|
|
58
97
|
const raw = readFileSync(path, "utf8");
|
|
59
98
|
const parsed = JSON.parse(raw);
|
|
99
|
+
// Absent created_at is treated as expired (fail closed), like an unparseable
|
|
100
|
+
// one — never a never-resetting gate. On expiry we persist the new epoch
|
|
101
|
+
// immediately so the cap re-applies from zero on the very next load;
|
|
102
|
+
// otherwise every load before the next clear would independently reset and a
|
|
103
|
+
// TTL-crossing session could clear well past MAX_CLEARED_FILES.
|
|
104
|
+
if (!parsed.created_at || isExpired(parsed.created_at)) {
|
|
105
|
+
const reset = freshState();
|
|
106
|
+
saveState(sessionDir, reset);
|
|
107
|
+
return reset;
|
|
108
|
+
}
|
|
60
109
|
return {
|
|
61
|
-
created_at: parsed.created_at
|
|
110
|
+
created_at: parsed.created_at,
|
|
62
111
|
cleared_files: parsed.cleared_files ?? {},
|
|
63
112
|
};
|
|
64
113
|
}
|
|
65
114
|
catch {
|
|
66
|
-
return
|
|
115
|
+
return freshState();
|
|
67
116
|
}
|
|
68
117
|
}
|
|
69
118
|
export function saveState(sessionDir, state) {
|
|
File without changes
|
|
@@ -20,10 +20,14 @@ const STOPWORDS = new Set([
|
|
|
20
20
|
"task", "goal", "plan", "work", "working", "build", "building", "code",
|
|
21
21
|
]);
|
|
22
22
|
// 4 suits Latin/Cyrillic/Thai prose (Thai combining marks are now kept; see
|
|
23
|
-
// extractKeywordsFromProse regex)
|
|
24
|
-
//
|
|
25
|
-
// (see CLAUDE.md Deferred).
|
|
23
|
+
// extractKeywordsFromProse regex). Korean Hangul words are typically 2-3 chars
|
|
24
|
+
// but represent full concepts, so they get a script-aware floor of 2.
|
|
26
25
|
const KEYWORD_MIN_LENGTH = 4;
|
|
26
|
+
function keywordMinLengthFor(word) {
|
|
27
|
+
if (/\p{Script=Hangul}/u.test(word))
|
|
28
|
+
return 2;
|
|
29
|
+
return KEYWORD_MIN_LENGTH;
|
|
30
|
+
}
|
|
27
31
|
const KEYWORD_CAP = 20;
|
|
28
32
|
const DEFAULT_WINDOW = 30;
|
|
29
33
|
const DEFAULT_THRESHOLD = 0.3;
|
|
@@ -61,7 +65,7 @@ export function extractKeywordsFromProse(prose) {
|
|
|
61
65
|
const out = [];
|
|
62
66
|
for (const raw of prose.toLowerCase().split(/[^\p{L}\p{N}\p{M}]+/u)) {
|
|
63
67
|
const word = raw.trim();
|
|
64
|
-
if (word.length <
|
|
68
|
+
if (word.length < keywordMinLengthFor(word))
|
|
65
69
|
continue;
|
|
66
70
|
if (/^\p{N}+$/u.test(word))
|
|
67
71
|
continue;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// hook-pack-gate.mts — Pure decision core for the enforcing-hook pack (v1).
|
|
2
|
+
//
|
|
3
|
+
// No I/O. Given a raw Bash command string, the resolved current branch, the
|
|
4
|
+
// staged-file count, and the operator's mode, decide allow / warn / block.
|
|
5
|
+
// The hook (src/hooks/hook-pack.mts) wires stdin + git subprocess calls around
|
|
6
|
+
// these functions; keeping them pure lets the unit tests cover every branch
|
|
7
|
+
// without spawning anything. Lives in lib/ so tests can import it (the
|
|
8
|
+
// test-imports-only invariant forbids importing from hooks/).
|
|
9
|
+
//
|
|
10
|
+
// Fail-open by construction: callers pass safe defaults (null branch, 0 staged)
|
|
11
|
+
// on any failure, and an unrecognized mode resolves to warn.
|
|
12
|
+
export const COMMIT_FILE_LIMIT = 15;
|
|
13
|
+
const ALLOW = { action: "allow", reason: "" };
|
|
14
|
+
export function parseMode(raw) {
|
|
15
|
+
const v = (raw ?? "warn").trim().toLowerCase();
|
|
16
|
+
return v === "block" || v === "off" ? v : "warn";
|
|
17
|
+
}
|
|
18
|
+
// Match `git push` / `git commit` at the start of the command or immediately
|
|
19
|
+
// after a shell separator (newline, ;, |, &). This deliberately does NOT match
|
|
20
|
+
// a mention like `echo git push`, where git is an argument, not the command.
|
|
21
|
+
export function classifyCommand(command) {
|
|
22
|
+
if (/(?:^|[\n;|&])\s*git\s+push\b/.test(command))
|
|
23
|
+
return "push";
|
|
24
|
+
if (/(?:^|[\n;|&])\s*git\s+commit\b/.test(command))
|
|
25
|
+
return "commit";
|
|
26
|
+
return "other";
|
|
27
|
+
}
|
|
28
|
+
export function isProtectedBranch(branch) {
|
|
29
|
+
return branch === "main" || branch === "master" || /^release\//.test(branch);
|
|
30
|
+
}
|
|
31
|
+
// Extract the explicit destination branch of a `git push`, or null when none is
|
|
32
|
+
// given (the caller should then fall back to the current branch). Handles
|
|
33
|
+
// `git push <remote> <branch>`, `git push -u <remote> <branch>`, and refspecs
|
|
34
|
+
// like `HEAD:main` (destination after the colon).
|
|
35
|
+
function parsePushTarget(command) {
|
|
36
|
+
const tokens = command.split(/\s+/).filter(Boolean);
|
|
37
|
+
const pushIdx = tokens.findIndex((t, i) => t === "push" && tokens[i - 1] === "git");
|
|
38
|
+
if (pushIdx === -1)
|
|
39
|
+
return null;
|
|
40
|
+
const operands = tokens.slice(pushIdx + 1).filter((t) => !t.startsWith("-"));
|
|
41
|
+
// operands[0] is the remote; operands[1] is the refspec. Fewer than two means
|
|
42
|
+
// no explicit branch (e.g. `git push` or `git push origin`).
|
|
43
|
+
if (operands.length < 2)
|
|
44
|
+
return null;
|
|
45
|
+
const refspec = operands[1];
|
|
46
|
+
return refspec.includes(":") ? (refspec.split(":").pop() ?? null) : refspec;
|
|
47
|
+
}
|
|
48
|
+
export function evaluatePush(command, currentBranch) {
|
|
49
|
+
if (/(?:^|\s)--dry-run\b/.test(command))
|
|
50
|
+
return { gated: false, branch: null };
|
|
51
|
+
const branch = parsePushTarget(command) ?? currentBranch;
|
|
52
|
+
if (!branch)
|
|
53
|
+
return { gated: false, branch: null };
|
|
54
|
+
return { gated: isProtectedBranch(branch), branch };
|
|
55
|
+
}
|
|
56
|
+
export function evaluateCommitSize(command, stagedCount) {
|
|
57
|
+
if (/(?:^|\s)--amend\b/.test(command))
|
|
58
|
+
return { gated: false };
|
|
59
|
+
return { gated: stagedCount > COMMIT_FILE_LIMIT };
|
|
60
|
+
}
|
|
61
|
+
export function decide(mode, gated, reason) {
|
|
62
|
+
if (!gated || mode === "off")
|
|
63
|
+
return ALLOW;
|
|
64
|
+
return { action: mode === "block" ? "block" : "warn", reason };
|
|
65
|
+
}
|
|
File without changes
|
|
@@ -26,7 +26,7 @@ const KEYWORDS = [
|
|
|
26
26
|
"transcript-linter",
|
|
27
27
|
];
|
|
28
28
|
const CLAUDE_PLUGIN_CATEGORY = "productivity";
|
|
29
|
-
const SHARED_PLUGIN_DESCRIPTION = "
|
|
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 26 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
|
|
@@ -143,6 +143,10 @@ const BEGINNER_TOOL_ENTRIES = [
|
|
|
143
143
|
type: "string",
|
|
144
144
|
description: "A single file path to clear (alternative to file_paths)",
|
|
145
145
|
},
|
|
146
|
+
state_path: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "State-file path from the GateGuard block reason (the session-scoped gateguard-session.json). Pass it verbatim so the clearance lands in the session the hook is reading; it must resolve inside ~/.claude/instincts/. Omitting it falls back to the unscoped canonical dir — safe only in legacy (no session id) contexts; when the hook is session-scoped, omitting it writes to the wrong dir and the retry will still block.",
|
|
149
|
+
},
|
|
146
150
|
},
|
|
147
151
|
required: [],
|
|
148
152
|
},
|
|
@@ -373,10 +377,16 @@ const EXPERT_TOOL_ENTRIES = [
|
|
|
373
377
|
required: ["id"],
|
|
374
378
|
},
|
|
375
379
|
},
|
|
380
|
+
{
|
|
381
|
+
name: "ci_distill_from_workflow",
|
|
382
|
+
description: "Draft a reusable instinct from the most recent completed-and-verified native Workflow run in this project's observation feed. A Workflow script is an authored recipe, so a single run whose output passed verification is enough — unlike ci_distill_candidates, which needs a pattern repeated across sessions. Writes a DRAFT to drafts/ (a skeleton you edit); it changes no behavior until promoted with ci_distill_promote. Returns a message when no verified workflow run is found.",
|
|
383
|
+
manifestWhat: "Draft a reusable instinct from a verified Workflow run",
|
|
384
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
385
|
+
},
|
|
376
386
|
];
|
|
377
387
|
const MODE_METADATA = {
|
|
378
388
|
beginner: {
|
|
379
|
-
description: "Beginner mode: see what your agent learned, list its instincts, and request a session reflection. Bundles
|
|
389
|
+
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.",
|
|
380
390
|
hooks: ["PreToolUse", "PostToolUse", "UserPromptSubmit"],
|
|
381
391
|
hookDescription: "Silently captures every tool call as observations and routes prompts to the matching skill via the route table. Lightweight and non-blocking.",
|
|
382
392
|
},
|
|
@@ -457,6 +467,11 @@ export function getPluginHooksConfig() {
|
|
|
457
467
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/companion-preference.mjs\"",
|
|
458
468
|
timeout: 5,
|
|
459
469
|
};
|
|
470
|
+
const hookPackCommand = {
|
|
471
|
+
type: "command",
|
|
472
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-pack.mjs\"",
|
|
473
|
+
timeout: 5,
|
|
474
|
+
};
|
|
460
475
|
const observeCommand = {
|
|
461
476
|
type: "command",
|
|
462
477
|
command: "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/observe.sh\"",
|
|
@@ -477,6 +492,11 @@ export function getPluginHooksConfig() {
|
|
|
477
492
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/goal-drift-stop.mjs\"",
|
|
478
493
|
timeout: 5,
|
|
479
494
|
};
|
|
495
|
+
const workflowDistillCommand = {
|
|
496
|
+
type: "command",
|
|
497
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/workflow-distill.mjs\"",
|
|
498
|
+
timeout: 5,
|
|
499
|
+
};
|
|
480
500
|
const routePromptCommand = {
|
|
481
501
|
type: "command",
|
|
482
502
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/route-prompt.mjs\"",
|
|
@@ -488,7 +508,7 @@ export function getPluginHooksConfig() {
|
|
|
488
508
|
timeout: 5,
|
|
489
509
|
};
|
|
490
510
|
return {
|
|
491
|
-
description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
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.",
|
|
492
512
|
hooks: {
|
|
493
513
|
// gateguard runs FIRST on PreToolUse so its block decision short-circuits
|
|
494
514
|
// before companion-preference sees the call. companion-preference runs
|
|
@@ -501,12 +521,18 @@ export function getPluginHooksConfig() {
|
|
|
501
521
|
// through with no output.
|
|
502
522
|
PreToolUse: [
|
|
503
523
|
{ hooks: [gateguardCommand, companionPreferenceCommand] },
|
|
524
|
+
// hook-pack gates Bash `git push` to protected branches and oversized
|
|
525
|
+
// `git commit`s. The "Bash" matcher scopes it off the hot path for all
|
|
526
|
+
// non-Bash tools, so the two-subprocess note above still holds for
|
|
527
|
+
// Edit/Read/etc. Warn-default (CLAUDE_CI_HOOKPACK_GATE) — never blocks
|
|
528
|
+
// until the operator opts in.
|
|
529
|
+
{ matcher: "Bash", hooks: [hookPackCommand] },
|
|
504
530
|
],
|
|
505
531
|
PostToolUse: [{ hooks: [observeCommand] }],
|
|
506
532
|
UserPromptSubmit: [{ hooks: [routePromptCommand, recallBriefingCommand] }],
|
|
507
533
|
SessionStart: [{ hooks: [sessionCommand] }],
|
|
508
534
|
SessionEnd: [{ hooks: [sessionCommand] }],
|
|
509
|
-
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand] }],
|
|
535
|
+
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand] }],
|
|
510
536
|
},
|
|
511
537
|
};
|
|
512
538
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -220,3 +220,144 @@ export function formatCandidates(candidates, limit = 10) {
|
|
|
220
220
|
}
|
|
221
221
|
return lines.join("\n");
|
|
222
222
|
}
|
|
223
|
+
// ── Workflow-run → instinct bridge ───────────────────────────────────────────
|
|
224
|
+
// A native Workflow run (Opus 4.8 orchestration / ultracode) is recorded in the
|
|
225
|
+
// observation feed as a `tool: "Workflow"` row whose input_summary holds
|
|
226
|
+
// {"script":"..."} — truncated (~500 chars), but meta.name/description/phases sit
|
|
227
|
+
// at the head of the script and survive. The output_summary holds
|
|
228
|
+
// {"status","runId",...} with status "async_launched": the feed captures the
|
|
229
|
+
// LAUNCH, not the result. So a workflow's success is never read from the Workflow
|
|
230
|
+
// row itself — it is inferred from a following verify-exit-0 in the same feed.
|
|
231
|
+
//
|
|
232
|
+
// Unlike findCandidates (which needs a pattern recurring across >=2 sessions to
|
|
233
|
+
// reject coincidence), a Workflow script is an AUTHORED recipe: one verified run
|
|
234
|
+
// warrants a draft, so the session/occurrence thresholds do not apply here.
|
|
235
|
+
const WORKFLOW_TOOL = "Workflow";
|
|
236
|
+
// Pull meta.name / meta.description / meta.phases[].title out of a (possibly
|
|
237
|
+
// truncated) workflow script embedded as JSON in the observation input_summary.
|
|
238
|
+
// Fail-closed: returns null unless a name is recoverable — a recipe with no
|
|
239
|
+
// identity is never fabricated.
|
|
240
|
+
function parseWorkflowScript(inputSummary) {
|
|
241
|
+
if (!inputSummary)
|
|
242
|
+
return null;
|
|
243
|
+
let script = "";
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(inputSummary);
|
|
246
|
+
if (typeof parsed.script === "string")
|
|
247
|
+
script = parsed.script;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// input_summary may itself be truncated mid-JSON; recover the script field loosely.
|
|
251
|
+
const m = inputSummary.match(/"script"\s*:\s*"((?:[^"\\]|\\.)*)/);
|
|
252
|
+
if (m) {
|
|
253
|
+
try {
|
|
254
|
+
script = JSON.parse(`"${m[1]}"`);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
script = m[1].replace(/\\n/g, "\n").replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!script)
|
|
262
|
+
return null;
|
|
263
|
+
// Scope name/description to the meta head (text before `phases:`) so a phase
|
|
264
|
+
// object's own description: is never mistaken for meta.description; scope phase
|
|
265
|
+
// titles to the phases array literal so inline agent/step title: fields are not
|
|
266
|
+
// captured. The capture classes exclude quotes and newlines, so a hostile
|
|
267
|
+
// name/description cannot inject lines into the draft YAML — serializeDraft emits
|
|
268
|
+
// trigger as a quoted scalar.
|
|
269
|
+
const phasesAt = script.search(/\bphases\s*:/);
|
|
270
|
+
const metaHead = phasesAt >= 0 ? script.slice(0, phasesAt) : script;
|
|
271
|
+
const name = (metaHead.match(/\bname\s*:\s*['"]([^'"\r\n]+)['"]/) ?? [])[1] ?? "";
|
|
272
|
+
if (!name)
|
|
273
|
+
return null; // fail closed: no recipe identity
|
|
274
|
+
const description = (metaHead.match(/\bdescription\s*:\s*['"]([^'"\r\n]+)['"]/) ?? [])[1] ?? "";
|
|
275
|
+
const phasesBlock = (script.match(/\bphases\s*:\s*\[([^\]]*)\]/) ?? [])[1] ?? "";
|
|
276
|
+
const phases = [];
|
|
277
|
+
const phaseRe = /\btitle\s*:\s*['"]([^'"\r\n]+)['"]/g;
|
|
278
|
+
let pm;
|
|
279
|
+
while ((pm = phaseRe.exec(phasesBlock)) !== null)
|
|
280
|
+
phases.push(pm[1]);
|
|
281
|
+
return { name, description, phases };
|
|
282
|
+
}
|
|
283
|
+
// A single verify-exit-0 Bash row: a verify/test/build command whose output is not
|
|
284
|
+
// failing. Mirrors classifyTrajectorySuccess's verify branch for one observation.
|
|
285
|
+
function isVerifySuccessRow(observation) {
|
|
286
|
+
if ((observation.tool ?? "") !== "Bash")
|
|
287
|
+
return false;
|
|
288
|
+
const input = (observation.input_summary ?? "").toString();
|
|
289
|
+
const output = (observation.output_summary ?? "").toString();
|
|
290
|
+
return VERIFY_CMD.test(input) && !FAILURE_MARKER.test(output) && (output === "" || SUCCESS_MARKER.test(output));
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Detect the most recent completed-and-verified Workflow run in an observation
|
|
294
|
+
* list. Returns null (fail closed) unless: a `tool: "Workflow"` row carries a
|
|
295
|
+
* parseable script with a name, AND a verify-exit-0 row follows it in the feed.
|
|
296
|
+
* The trailing verify is the only success signal — the Workflow row records the
|
|
297
|
+
* launch, never the result.
|
|
298
|
+
*/
|
|
299
|
+
export function workflowRunFromObservations(observations) {
|
|
300
|
+
let wfIndex = -1;
|
|
301
|
+
let meta = null;
|
|
302
|
+
for (let i = observations.length - 1; i >= 0; i -= 1) {
|
|
303
|
+
if ((observations[i].tool ?? "") !== WORKFLOW_TOOL)
|
|
304
|
+
continue;
|
|
305
|
+
const parsed = parseWorkflowScript((observations[i].input_summary ?? "").toString());
|
|
306
|
+
if (parsed) {
|
|
307
|
+
wfIndex = i;
|
|
308
|
+
meta = parsed;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (wfIndex === -1 || !meta)
|
|
313
|
+
return null;
|
|
314
|
+
const wfSession = (observations[wfIndex].session ?? "").toString();
|
|
315
|
+
// The verify that proves the run must follow the Workflow row in the SAME session.
|
|
316
|
+
// A verify from an unrelated later task (different session) does not count — the
|
|
317
|
+
// run is asynchronous, so an interleaved verify could otherwise falsely prove it.
|
|
318
|
+
let verifyCommand = "";
|
|
319
|
+
for (let i = wfIndex + 1; i < observations.length; i += 1) {
|
|
320
|
+
const row = observations[i];
|
|
321
|
+
if ((row.session ?? "").toString() !== wfSession)
|
|
322
|
+
continue;
|
|
323
|
+
if (isVerifySuccessRow(row)) {
|
|
324
|
+
verifyCommand = (row.input_summary ?? "").toString();
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!verifyCommand)
|
|
329
|
+
return null; // fail closed: no evidence the run's output landed
|
|
330
|
+
return { name: meta.name, description: meta.description, phases: meta.phases, verifyCommand };
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Turn a verified Workflow run into a DRAFT instinct. The script's phase outline is
|
|
334
|
+
* a real skeleton (not a placeholder n-gram), but the human still edits the body
|
|
335
|
+
* before promoting. Reuses serializeDraft and the drafts/ ladder; the
|
|
336
|
+
* `draft-workflow-` id prefix marks the source and stays filesystem-safe.
|
|
337
|
+
*/
|
|
338
|
+
export function draftFromWorkflowRun(run) {
|
|
339
|
+
// Cap the slug so a hostile/huge meta.name cannot produce a path that trips
|
|
340
|
+
// ENAMETOOLONG on write; strip a trailing hyphen left by the cut.
|
|
341
|
+
const slug = slugifyNgram([run.name]).slice(0, 120).replace(/-+$/, "") || "workflow";
|
|
342
|
+
const phaseLine = run.phases.length > 0 ? run.phases.join(" → ") : "(phases not captured)";
|
|
343
|
+
const lines = [
|
|
344
|
+
`When this situation recurs, the workflow "${run.name}" handled it end to end and its output passed verification.`,
|
|
345
|
+
"",
|
|
346
|
+
];
|
|
347
|
+
if (run.description)
|
|
348
|
+
lines.push(`Intent: ${run.description}`);
|
|
349
|
+
lines.push(`Phases: ${phaseLine}`);
|
|
350
|
+
lines.push(`Verified by: ${run.verifyCommand}`);
|
|
351
|
+
lines.push("", "Replace this with the concrete steps, preconditions, and gotchas before promoting —", "the phase outline is the skeleton, not the full recipe.");
|
|
352
|
+
return {
|
|
353
|
+
id: `draft-workflow-${slug}`,
|
|
354
|
+
trigger: `Auto-detected from a verified workflow run: ${run.name}${run.description ? ` — ${run.description}` : ""}`,
|
|
355
|
+
body: lines.join("\n"),
|
|
356
|
+
confidence: DRAFT_CONFIDENCE,
|
|
357
|
+
domain: "workflow",
|
|
358
|
+
ngram: run.phases.length > 0 ? run.phases : [run.name],
|
|
359
|
+
occurrences: 1,
|
|
360
|
+
sessions: 1,
|
|
361
|
+
outcome: "workflow-verified",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
@@ -18,7 +18,7 @@ skill set on disk.
|
|
|
18
18
|
## Tier 1 — beginner-mode pairing
|
|
19
19
|
- `deploy-receipt` — Enforces Law 4 (Verify Before Reporting) of the 7 Laws of AI Agent Discipline at the deploy seam. A merge into a branch that auto-deploys is not "done" until the deploy provider reports the merged commit SHA running and a healthcheck endpoint returns 200. Companion to the vendored `finishing-a-development-branch` skill — does not replace it, runs after it for projects on Railway, Cloudflare Workers, Vercel, Netlify, Fly.io, or any other auto-deploy target.
|
|
20
20
|
- `gateguard` — Enforces Law 1 (Research Before Executing) of the 7 Laws of AI Agent Discipline. Fact-forcing gate that blocks Edit/Write/Bash (including MultiEdit) and demands concrete investigation (importers, data schemas, user instruction) before allowing the action. Measurably improves output quality by +2.25 points vs ungated agents.
|
|
21
|
-
- `
|
|
21
|
+
- `model-forward` — Enforces all 7 Laws as a standing stance — go with Claude Code and the model, not against it. Skills are scaffolding that merges into the model over time; the durable core is goal-driven execution (the higher the stated goal, the better) plus self-discipline guardrails.
|
|
22
22
|
- `recall` — Enforces Law 1 (Research Before Executing) of the 7 Laws of AI Agent Discipline. Makes past sessions first-class research material by searching the observation log with BM25 ranking, so 'have I hit this before?' is answerable before re-deriving a fix or repeating a mistake.
|
|
23
23
|
- `tdd-workflow` — Enforces Law 3 (One Thing at a Time) and Law 4 (Verify Before Reporting) of the 7 Laws of AI Agent Discipline. Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests.
|
|
24
24
|
- `verification-loop` — Enforces Law 4 (Verify Before Reporting) of the 7 Laws of AI Agent Discipline. A comprehensive verification system for agent coding sessions covering build, types, lint, tests, security, and diff with a PASS/FAIL report.
|
|
@@ -31,6 +31,7 @@ skill set on disk.
|
|
|
31
31
|
- `handoff` — Enforces Law 5 (Reflect After Every Session) of the 7 Laws of AI Agent Discipline. Compact the current conversation into a handoff document for another agent to pick up. Ported from mattpocock/skills under MIT.
|
|
32
32
|
- `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, and verifies a push actually landed instead of assuming it did.
|
|
33
33
|
- `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'.
|
|
34
|
+
- `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.
|
|
34
35
|
- `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.
|
|
35
36
|
- `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.
|
|
36
37
|
- `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.
|
|
@@ -136,7 +136,7 @@ This gate is what catches the squash-merge / ahead-of-origin trap recorded in th
|
|
|
136
136
|
|
|
137
137
|
### Today: runtime hook + skill (zero install beyond the plugin)
|
|
138
138
|
|
|
139
|
-
`hooks/gateguard.mjs` is bundled with this plugin and wired as the first PreToolUse hook in `plugins/continuous-improvement/hooks/hooks.json`. When you install the plugin, the runtime gate is live — no extra config, no opt-in. The hook reads tool input from stdin, classifies it through a data-driven routing table (Read/Grep/Glob → allow, Write/Edit/MultiEdit → mutating-file gate, Bash → destructive-pattern check), and emits `{decision, reason?}` on stdout. Per-session state lives at `~/.claude/instincts/<project-hash>/gateguard-session.json` (
|
|
139
|
+
`hooks/gateguard.mjs` is bundled with this plugin and wired as the first PreToolUse hook in `plugins/continuous-improvement/hooks/hooks.json`. When you install the plugin, the runtime gate is live — no extra config, no opt-in. The hook reads tool input from stdin, classifies it through a data-driven routing table (Read/Grep/Glob → allow, Write/Edit/MultiEdit → mutating-file gate, Bash → destructive-pattern check), and emits `{decision, reason?}` on stdout. Per-session state lives at `~/.claude/instincts/<project-hash>/sessions/<session-id>/gateguard-session.json` — scoped by the stdin `session_id` so the cap never bleeds across concurrent sessions (it falls back to the unscoped `<project-hash>/` dir when no session id is present, and `GATEGUARD_SESSION_DIR` overrides it for tests). The cap is `MAX_CLEARED_FILES = 50` per session, and the state self-heals after `STATE_TTL_MS` so a stale gate never needs a manual `rm`.
|
|
140
140
|
|
|
141
141
|
Smoke-test the runtime gate after install: ask Claude to write a throwaway file with no research first. The hook should return a `block` decision with a fact-list reason; Claude should pause rather than write.
|
|
142
142
|
|
|
@@ -150,10 +150,10 @@ The block reason prints the exact `gateguard-session.json` path and the clearanc
|
|
|
150
150
|
|
|
151
151
|
The inline `_gateguard_facts_presented: true` retry still works on harnesses that forward unknown tool params, but Claude Code's strict tool schema (`additionalProperties: false`) rejects it with `InputValidationError` — use one of the above on Claude Code.
|
|
152
152
|
|
|
153
|
-
###
|
|
153
|
+
### Limitations and guarantees
|
|
154
154
|
|
|
155
|
-
- **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 bounds damage from stuck loops or rogue agents.
|
|
156
|
-
- **State-file deletion.** `rm`-ing the session state resets every gate.
|
|
155
|
+
- **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.
|
|
156
|
+
- **State-file deletion / self-heal.** `rm`-ing the session state resets every gate. Because state is scoped per session (`sessions/<session-id>/`), the session is a real trust boundary, not one shared across concurrent runs. A stale state file also self-heals once its `created_at` ages past `STATE_TTL_MS`, so a manual `rm` is rarely needed.
|
|
157
157
|
- **Parallel-hook race.** Two simultaneous hook invocations can race the read+write of the state file. Acceptable trade-off vs Windows atomic-rename complexity.
|
|
158
158
|
|
|
159
159
|
**MultiEdit per-file gating.** The hook clears and checks every `edits[]` path individually, so a mixed-clearance batch blocks until *all* edited files are cleared or facts are presented. The block reason now names the whole batch, not just the first uncleared path.
|