@alecsibilia/luca 13.0.0-alpha.1
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/LICENSE +201 -0
- package/README.md +47 -0
- package/bin/luca.js +3 -0
- package/dist/chunks/branch.mjs +47 -0
- package/dist/chunks/bun-runtime.mjs +46 -0
- package/dist/chunks/checks.mjs +53 -0
- package/dist/chunks/claim-verify.mjs +465 -0
- package/dist/chunks/classify.mjs +105 -0
- package/dist/chunks/confidence.mjs +199 -0
- package/dist/chunks/doctor.mjs +158 -0
- package/dist/chunks/hook.mjs +696 -0
- package/dist/chunks/init.mjs +715 -0
- package/dist/chunks/muninndb-health.mjs +66 -0
- package/dist/chunks/phase.mjs +38 -0
- package/dist/chunks/pr-review.mjs +122 -0
- package/dist/chunks/preferences.mjs +61 -0
- package/dist/chunks/repair.mjs +111 -0
- package/dist/chunks/repo.mjs +58 -0
- package/dist/chunks/retro.mjs +86 -0
- package/dist/chunks/roadmap.mjs +58 -0
- package/dist/chunks/rules.mjs +527 -0
- package/dist/chunks/stale-mcp-server.mjs +90 -0
- package/dist/chunks/state.mjs +57 -0
- package/dist/chunks/stray-local-install.mjs +200 -0
- package/dist/chunks/telemetry.mjs +165 -0
- package/dist/chunks/todo.mjs +151 -0
- package/dist/chunks/vault-init.mjs +300 -0
- package/dist/chunks/verification.mjs +95 -0
- package/dist/chunks/version.mjs +70 -0
- package/dist/chunks/workflow.mjs +47 -0
- package/dist/claude/.claude/agents/architect.md +410 -0
- package/dist/claude/.claude/agents/build.md +111 -0
- package/dist/claude/.claude/agents/discuss.md +93 -0
- package/dist/claude/.claude/agents/discussion.md +149 -0
- package/dist/claude/.claude/agents/execute.md +416 -0
- package/dist/claude/.claude/agents/executor.md +161 -0
- package/dist/claude/.claude/agents/fast.md +84 -0
- package/dist/claude/.claude/agents/finalize.md +484 -0
- package/dist/claude/.claude/agents/learner.md +160 -0
- package/dist/claude/.claude/agents/plan-reviewer.md +129 -0
- package/dist/claude/.claude/agents/plan.md +96 -0
- package/dist/claude/.claude/agents/research.md +327 -0
- package/dist/claude/.claude/agents/researcher.md +78 -0
- package/dist/claude/.claude/agents/review.md +283 -0
- package/dist/claude/.claude/agents/reviewer.md +163 -0
- package/dist/claude/.claude/agents/shadow-scanner.md +257 -0
- package/dist/claude/.claude/agents/triage.md +230 -0
- package/dist/claude/.claude/agents/verifier.md +131 -0
- package/dist/claude/.claude/commands/bug-diagnose.md +12 -0
- package/dist/claude/.claude/commands/gh-issue-triage.md +14 -0
- package/dist/claude/.claude/commands/gh-pr-address.md +235 -0
- package/dist/claude/.claude/commands/gh-prepare.md +12 -0
- package/dist/claude/.claude/commands/grill-me.md +12 -0
- package/dist/claude/.claude/commands/lu-review.md +51 -0
- package/dist/claude/.claude/commands/lu.md +75 -0
- package/dist/claude/.claude/commands/luca-init.md +14 -0
- package/dist/claude/.claude/commands/luca-telemetry-report.md +12 -0
- package/dist/claude/.claude/commands/memory-audit.md +12 -0
- package/dist/claude/.claude/commands/milestone-new.md +122 -0
- package/dist/claude/.claude/commands/phase-discuss.md +45 -0
- package/dist/claude/.claude/commands/phase-execute.md +39 -0
- package/dist/claude/.claude/commands/phase-plan.md +53 -0
- package/dist/claude/.claude/commands/repo-cleanup.md +80 -0
- package/dist/claude/.claude/commands/todo-add.md +28 -0
- package/dist/claude/.claude/commands/todo-check.md +36 -0
- package/dist/claude/.claude/hooks/context-refresher.ts +285 -0
- package/dist/claude/.claude/hooks/continuation-messages.ts +215 -0
- package/dist/claude/.claude/hooks/pipeline-guard.ts +182 -0
- package/dist/claude/.claude/settings.json +41 -0
- package/dist/claude/skills/arch-audit/SKILL.md +161 -0
- package/dist/claude/skills/autopilot/SKILL.md +1299 -0
- package/dist/claude/skills/bug-diagnose/SKILL.md +102 -0
- package/dist/claude/skills/choose/SKILL.md +124 -0
- package/dist/claude/skills/gh-issue-triage/SKILL.md +97 -0
- package/dist/claude/skills/gh-pr-address/SKILL.md +235 -0
- package/dist/claude/skills/gh-prepare/SKILL.md +209 -0
- package/dist/claude/skills/grill-me/SKILL.md +46 -0
- package/dist/claude/skills/lu/SKILL.md +112 -0
- package/dist/claude/skills/lu-review/SKILL.md +51 -0
- package/dist/claude/skills/luca-init/SKILL.md +91 -0
- package/dist/claude/skills/luca-telemetry-report/SKILL.md +145 -0
- package/dist/claude/skills/luca-write-surface/SKILL.md +213 -0
- package/dist/claude/skills/memory-audit/SKILL.md +217 -0
- package/dist/claude/skills/milestone-audit/SKILL.md +545 -0
- package/dist/claude/skills/milestone-complete/SKILL.md +168 -0
- package/dist/claude/skills/milestone-gaps/SKILL.md +60 -0
- package/dist/claude/skills/milestone-new/SKILL.md +125 -0
- package/dist/claude/skills/note/SKILL.md +162 -0
- package/dist/claude/skills/phase-add/SKILL.md +91 -0
- package/dist/claude/skills/phase-assumptions/SKILL.md +92 -0
- package/dist/claude/skills/phase-discuss/SKILL.md +165 -0
- package/dist/claude/skills/phase-execute/SKILL.md +1786 -0
- package/dist/claude/skills/phase-insert/SKILL.md +100 -0
- package/dist/claude/skills/phase-plan/SKILL.md +461 -0
- package/dist/claude/skills/phase-remove/SKILL.md +113 -0
- package/dist/claude/skills/phase-research/SKILL.md +80 -0
- package/dist/claude/skills/post-init-tour/SKILL.md +58 -0
- package/dist/claude/skills/progress/SKILL.md +271 -0
- package/dist/claude/skills/project-new/SKILL.md +609 -0
- package/dist/claude/skills/quick/SKILL.md +256 -0
- package/dist/claude/skills/rename-audit/SKILL.md +52 -0
- package/dist/claude/skills/repo-audit/SKILL.md +88 -0
- package/dist/claude/skills/repo-cleanup/SKILL.md +80 -0
- package/dist/claude/skills/seed-memory/SKILL.md +235 -0
- package/dist/claude/skills/session-pause/SKILL.md +126 -0
- package/dist/claude/skills/session-plan/SKILL.md +112 -0
- package/dist/claude/skills/session-resume/SKILL.md +75 -0
- package/dist/claude/skills/todo-add/SKILL.md +85 -0
- package/dist/claude/skills/todo-check/SKILL.md +77 -0
- package/dist/claude/skills/workflow-save/SKILL.md +277 -0
- package/dist/index.d.mts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.mjs +69 -0
- package/dist/shared/luca.B3Mimc0P.mjs +52 -0
- package/dist/shared/luca.B3saVjJm.mjs +163 -0
- package/dist/shared/luca.BYdjkfnz.mjs +217 -0
- package/dist/shared/luca.BmhNkYe2.mjs +56 -0
- package/dist/shared/luca.C4gMUoBd.mjs +358 -0
- package/dist/shared/luca.CQ3g1xrD.mjs +19 -0
- package/dist/shared/luca.CRmaAfXR.mjs +713 -0
- package/dist/shared/luca.CrXzXueR.mjs +57 -0
- package/dist/shared/luca.DTomPq7I.mjs +91 -0
- package/dist/shared/luca.DjDTeDCi.mjs +1904 -0
- package/dist/shared/luca.HZxBTBgD.mjs +201 -0
- package/dist/shared/luca.TSMg1t7I.mjs +10 -0
- package/dist/shared/luca.dM-MKlNE.mjs +25 -0
- package/dist/shared/luca.naWEcQ4B.mjs +7 -0
- package/package.json +76 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* continuation-messages handler — `PostToolUse` hook that injects a
|
|
4
|
+
* mode-entry kick-off prompt after the pipeline advances.
|
|
5
|
+
*
|
|
6
|
+
* This is the Claude Code delivery vehicle for the pure
|
|
7
|
+
* `computeContinuationMessage()` builder in
|
|
8
|
+
* `@alecsibilia/luca-core/orchestration`. The algorithm decides whether
|
|
9
|
+
* to emit and what to say; this handler is glue:
|
|
10
|
+
*
|
|
11
|
+
* 1. Read the Claude Code PostToolUse payload from stdin.
|
|
12
|
+
* 2. If it isn't a `Bash` invocation of `luca state advance <step>`,
|
|
13
|
+
* exit 0 silently (this hook only fires after pipeline advances).
|
|
14
|
+
* 3. Read `.luca/state.json` for the now-current `pipelineStep`.
|
|
15
|
+
* 4. Confirm the requested step matches the current step (i.e. the
|
|
16
|
+
* advance succeeded). If not, exit 0 silently — the CLI's own
|
|
17
|
+
* validation rejected the call, and an injected continuation for
|
|
18
|
+
* a step we didn't actually enter would mislead the agent.
|
|
19
|
+
* 5. Call `computeContinuationMessage()`.
|
|
20
|
+
* 6. Emit the message via the Claude Code PostToolUse hook output
|
|
21
|
+
* JSON shape:
|
|
22
|
+
*
|
|
23
|
+
* {
|
|
24
|
+
* "hookSpecificOutput": {
|
|
25
|
+
* "hookEventName": "PostToolUse",
|
|
26
|
+
* "additionalContext": "<system-reminder>...</system-reminder>"
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* Exit 0 — `additionalContext` is the hook's only effect on the
|
|
31
|
+
* session.
|
|
32
|
+
*
|
|
33
|
+
* The Claude Code hook contract (PostToolUse):
|
|
34
|
+
*
|
|
35
|
+
* stdin JSON shape (snake_case fields per the docs):
|
|
36
|
+
* {
|
|
37
|
+
* "session_id": "...",
|
|
38
|
+
* "hook_event_name": "PostToolUse",
|
|
39
|
+
* "tool_name": "Bash",
|
|
40
|
+
* "tool_input": { "command": "luca state advance plan", ... },
|
|
41
|
+
* "tool_response": { "stdout": "...", "stderr": "...", ... }
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* Exit codes:
|
|
45
|
+
* 0 → all signals come from stdout JSON
|
|
46
|
+
* 2 → block (not used here — this hook is informational)
|
|
47
|
+
* * → other non-zero → error (failure-open: we never use this
|
|
48
|
+
* because a hook bug should never disrupt the session)
|
|
49
|
+
*
|
|
50
|
+
* Stdout JSON for context injection:
|
|
51
|
+
* {
|
|
52
|
+
* "hookSpecificOutput": {
|
|
53
|
+
* "hookEventName": "PostToolUse",
|
|
54
|
+
* "additionalContext": "..."
|
|
55
|
+
* }
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* Failure-open philosophy (per E-1 / E-2 reasoning, intentionally
|
|
59
|
+
* reused): if ANYTHING unexpected happens (stdin parse error,
|
|
60
|
+
* state.json missing/malformed, command argv doesn't parse), exit 0
|
|
61
|
+
* silently. A hook that mis-injects a continuation will confuse the
|
|
62
|
+
* agent; a hook that crashes is worse. Choose silent skip.
|
|
63
|
+
*/
|
|
64
|
+
import { appendLedger } from '@alecsibilia/luca-core/ledger'
|
|
65
|
+
import {
|
|
66
|
+
computeContinuationMessage,
|
|
67
|
+
type ContinuationInput,
|
|
68
|
+
} from '@alecsibilia/luca-core/orchestration'
|
|
69
|
+
import {
|
|
70
|
+
loadCurrentState,
|
|
71
|
+
parseAdvanceCommand,
|
|
72
|
+
} from '@alecsibilia/luca-core/state'
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Shape of the relevant slice of the PostToolUse stdin payload. We
|
|
76
|
+
* only look at `tool_name` and `tool_input.command`; everything else
|
|
77
|
+
* is ignored. Defensive typing — the harness may add fields and the
|
|
78
|
+
* handler should not break on them.
|
|
79
|
+
*/
|
|
80
|
+
interface PostToolUsePayload {
|
|
81
|
+
tool_name?: string
|
|
82
|
+
toolName?: string
|
|
83
|
+
tool_input?: { command?: string }
|
|
84
|
+
toolInput?: { command?: string }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function main(): Promise<number> {
|
|
88
|
+
const raw = await Bun.stdin.text()
|
|
89
|
+
if (!raw.trim()) {
|
|
90
|
+
// Empty stdin — nothing to do. Allow silently.
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let payload: PostToolUsePayload
|
|
95
|
+
try {
|
|
96
|
+
payload = JSON.parse(raw) as PostToolUsePayload
|
|
97
|
+
} catch {
|
|
98
|
+
// Malformed stdin is the harness's problem. Fail open silently.
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const toolName = payload.tool_name ?? payload.toolName
|
|
103
|
+
if (toolName !== 'Bash') {
|
|
104
|
+
// The Bash matcher catches every Bash call; we only act on
|
|
105
|
+
// `luca state advance`. Other Bash → silent skip.
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const command = payload.tool_input?.command ?? payload.toolInput?.command
|
|
110
|
+
if (typeof command !== 'string') {
|
|
111
|
+
return 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const requestedStep = parseAdvanceCommand(command)
|
|
115
|
+
if (requestedStep === null) {
|
|
116
|
+
// Not a `luca state advance` invocation.
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const cwd = process.cwd()
|
|
121
|
+
const state = await loadCurrentState({ cwd })
|
|
122
|
+
|
|
123
|
+
// Confirm the advance actually happened by comparing the now-current
|
|
124
|
+
// step with the requested step. If they don't match the CLI rejected
|
|
125
|
+
// the transition (or the user wrote an exotic command form we didn't
|
|
126
|
+
// recognise); either way, skip silently rather than inject a
|
|
127
|
+
// misleading continuation.
|
|
128
|
+
if (state.pipelineStep !== requestedStep) {
|
|
129
|
+
return 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const input: ContinuationInput = {
|
|
133
|
+
currentStep: state.pipelineStep,
|
|
134
|
+
...(state.complexity !== undefined
|
|
135
|
+
? { complexity: state.complexity }
|
|
136
|
+
: {}),
|
|
137
|
+
...(state.oversight !== undefined
|
|
138
|
+
? { oversight: state.oversight }
|
|
139
|
+
: {}),
|
|
140
|
+
...(typeof state.currentPhase === 'number'
|
|
141
|
+
? { currentPhase: state.currentPhase }
|
|
142
|
+
: {}),
|
|
143
|
+
...(typeof state.totalPhases === 'number'
|
|
144
|
+
? { totalPhases: state.totalPhases }
|
|
145
|
+
: {}),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const verdict = computeContinuationMessage(input)
|
|
149
|
+
|
|
150
|
+
// Emit a ledger event for the hook firing. Records whether a
|
|
151
|
+
// continuation was emitted (and why not, when applicable) so the
|
|
152
|
+
// postmortem analyzer has signal on hook coverage. Failure-open.
|
|
153
|
+
try {
|
|
154
|
+
const runId = typeof (state as { sessionId?: unknown }).sessionId === 'string'
|
|
155
|
+
? (state as { sessionId: string }).sessionId
|
|
156
|
+
: ''
|
|
157
|
+
appendLedger({
|
|
158
|
+
cwd,
|
|
159
|
+
runId,
|
|
160
|
+
event: 'hook.continuation-messages.fired',
|
|
161
|
+
data: {
|
|
162
|
+
pipelineStep: state.pipelineStep,
|
|
163
|
+
decision: verdict === null
|
|
164
|
+
? 'skipped'
|
|
165
|
+
: verdict.reason === 'unknown-current-step'
|
|
166
|
+
? 'skipped'
|
|
167
|
+
: 'emitted',
|
|
168
|
+
reason: verdict === null ? 'no-continuation' : verdict.reason,
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
} catch {
|
|
172
|
+
// Failure-open.
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (verdict === null) {
|
|
176
|
+
// No continuation appropriate (e.g. advance into `idle`).
|
|
177
|
+
return 0
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (verdict.reason === 'unknown-current-step') {
|
|
181
|
+
// The algorithm flagged a data-integrity warning; we don't
|
|
182
|
+
// surface this to the model (it would be alarming and not
|
|
183
|
+
// actionable). Skip silently — the CLI's own validation will
|
|
184
|
+
// catch the underlying state.json corruption.
|
|
185
|
+
return 0
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Emit the additionalContext payload. Claude Code consumes the
|
|
189
|
+
// first valid JSON object on stdout for PostToolUse hooks.
|
|
190
|
+
const output = {
|
|
191
|
+
hookSpecificOutput: {
|
|
192
|
+
hookEventName: 'PostToolUse',
|
|
193
|
+
additionalContext: verdict.message,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
process.stdout.write(JSON.stringify(output) + '\n')
|
|
197
|
+
return 0
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// `parseAdvanceCommand` (and its `stripQuotes` helper) was promoted to
|
|
201
|
+
// `@alecsibilia/luca-core/state` (see `packages/luca-core/src/state/
|
|
202
|
+
// cli-parse.ts`) — both this hook and `pipeline-guard/handler.ts`
|
|
203
|
+
// used byte-identical copies, so the helper now lives in luca-core
|
|
204
|
+
// and is imported above (CF11).
|
|
205
|
+
|
|
206
|
+
main().then(
|
|
207
|
+
(code) => process.exit(code),
|
|
208
|
+
(err) => {
|
|
209
|
+
// Defensive: any unexpected throw means we fail open silently.
|
|
210
|
+
// No stderr — this is a PostToolUse informational hook; users
|
|
211
|
+
// shouldn't see internal errors.
|
|
212
|
+
void err
|
|
213
|
+
process.exit(0)
|
|
214
|
+
},
|
|
215
|
+
)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* pipeline-guard handler — `PreToolUse` hook that vets pipeline-step
|
|
4
|
+
* transitions before they execute.
|
|
5
|
+
*
|
|
6
|
+
* This is the Claude Code delivery vehicle for the pure
|
|
7
|
+
* `checkPipelineGuard()` algorithm in `@alecsibilia/luca-core/orchestration`.
|
|
8
|
+
* The algorithm decides legality; this handler is glue:
|
|
9
|
+
*
|
|
10
|
+
* 1. Read the Claude Code PreToolUse payload from stdin.
|
|
11
|
+
* 2. If it isn't a `Bash` invocation of `luca state advance <step>`,
|
|
12
|
+
* exit 0 (this hook only guards that one command).
|
|
13
|
+
* 3. Read `.luca/state.json` for the current `pipelineStep`.
|
|
14
|
+
* 4. Call `checkPipelineGuard()` with current + requested step.
|
|
15
|
+
* 5. Exit 0 on allow, 2 on block (Claude Code blocks the tool call
|
|
16
|
+
* and surfaces the stderr message to the model).
|
|
17
|
+
*
|
|
18
|
+
* The Claude Code hook contract (PreToolUse):
|
|
19
|
+
*
|
|
20
|
+
* stdin JSON shape (snake_case fields per the docs):
|
|
21
|
+
* {
|
|
22
|
+
* "session_id": "...",
|
|
23
|
+
* "hook_event_name": "PreToolUse",
|
|
24
|
+
* "tool_name": "Bash",
|
|
25
|
+
* "tool_input": { "command": "luca state advance plan", ... }
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* Exit codes:
|
|
29
|
+
* 0 → allow the tool call
|
|
30
|
+
* 2 → block + send stderr to the model
|
|
31
|
+
* * → other non-zero → error (Claude Code surfaces to the user;
|
|
32
|
+
* we never use this path because we'd rather fail open than
|
|
33
|
+
* break the user's workflow on a parse error)
|
|
34
|
+
*
|
|
35
|
+
* Why a bun-script: the algorithm needs typed access to
|
|
36
|
+
* `checkPipelineGuard` + the pipeline-transitions table. A shell
|
|
37
|
+
* wrapper would have to re-implement the legality check or shell out
|
|
38
|
+
* to the `luca` CLI; both create drift surfaces. Direct TS import
|
|
39
|
+
* keeps the source of truth in one place.
|
|
40
|
+
*
|
|
41
|
+
* Failure-open philosophy: if ANYTHING unexpected happens (stdin
|
|
42
|
+
* parse error, state.json missing/malformed, command argv doesn't
|
|
43
|
+
* parse), we exit 0 and let the call through. Hooks that crash will
|
|
44
|
+
* break the user's session; hooks that wrongly block will frustrate
|
|
45
|
+
* them. Both are bad — but blocking is worse because the user can't
|
|
46
|
+
* recover without disabling the hook. Choose to fail open and lean
|
|
47
|
+
* on the CLI's own validation (the `luca state advance` command does
|
|
48
|
+
* its own legal-transition check independently).
|
|
49
|
+
*/
|
|
50
|
+
import { join } from 'node:path'
|
|
51
|
+
|
|
52
|
+
import { appendLedger } from '@alecsibilia/luca-core/ledger'
|
|
53
|
+
import {
|
|
54
|
+
checkPipelineGuard,
|
|
55
|
+
type PipelineGuardInput,
|
|
56
|
+
} from '@alecsibilia/luca-core/orchestration'
|
|
57
|
+
import {
|
|
58
|
+
loadCurrentState,
|
|
59
|
+
parseAdvanceCommand,
|
|
60
|
+
} from '@alecsibilia/luca-core/state'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Shape of the relevant slice of the PreToolUse stdin payload. We
|
|
64
|
+
* only look at `tool_name` and `tool_input.command`; everything else
|
|
65
|
+
* is ignored. Defensive typing — the harness may add fields and the
|
|
66
|
+
* handler should not break on them.
|
|
67
|
+
*/
|
|
68
|
+
interface PreToolUsePayload {
|
|
69
|
+
tool_name?: string
|
|
70
|
+
toolName?: string
|
|
71
|
+
tool_input?: { command?: string }
|
|
72
|
+
toolInput?: { command?: string }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main(): Promise<number> {
|
|
76
|
+
const raw = await Bun.stdin.text()
|
|
77
|
+
if (!raw.trim()) {
|
|
78
|
+
// Empty stdin — nothing to guard. Allow.
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let payload: PreToolUsePayload
|
|
83
|
+
try {
|
|
84
|
+
payload = JSON.parse(raw) as PreToolUsePayload
|
|
85
|
+
} catch {
|
|
86
|
+
// Malformed stdin is the harness's problem, not ours. Fail open.
|
|
87
|
+
return 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const toolName = payload.tool_name ?? payload.toolName
|
|
91
|
+
if (toolName !== 'Bash') {
|
|
92
|
+
// The hook also matches non-Bash tools if registered broadly,
|
|
93
|
+
// but pipeline-guard only cares about `luca state advance`.
|
|
94
|
+
return 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const command = payload.tool_input?.command ?? payload.toolInput?.command
|
|
98
|
+
if (typeof command !== 'string') {
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const requestedStep = parseAdvanceCommand(command)
|
|
103
|
+
if (requestedStep === null) {
|
|
104
|
+
// Not a `luca state advance` invocation, or shape doesn't
|
|
105
|
+
// match. Nothing to guard.
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cwd = process.cwd()
|
|
110
|
+
const state = await loadCurrentState({ cwd })
|
|
111
|
+
|
|
112
|
+
const input: PipelineGuardInput = {
|
|
113
|
+
currentStep: state.pipelineStep,
|
|
114
|
+
requestedStep,
|
|
115
|
+
...(state.complexity !== undefined
|
|
116
|
+
? { complexity: state.complexity }
|
|
117
|
+
: {}),
|
|
118
|
+
...(state.oversight !== undefined
|
|
119
|
+
? { oversight: state.oversight }
|
|
120
|
+
: {}),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const verdict = checkPipelineGuard(input)
|
|
124
|
+
|
|
125
|
+
// Emit a ledger event for the hook firing. The postmortem analyzer
|
|
126
|
+
// scans the ledger for `hook.pipeline-guard.fired` events to detect
|
|
127
|
+
// pipeline-guard rejections and forced transitions over time.
|
|
128
|
+
// Failure-open: any ledger-write error MUST NOT block the hook.
|
|
129
|
+
try {
|
|
130
|
+
const runId = typeof (state as { sessionId?: unknown }).sessionId === 'string'
|
|
131
|
+
? (state as { sessionId: string }).sessionId
|
|
132
|
+
: ''
|
|
133
|
+
appendLedger({
|
|
134
|
+
cwd,
|
|
135
|
+
runId,
|
|
136
|
+
event: 'hook.pipeline-guard.fired',
|
|
137
|
+
data: {
|
|
138
|
+
pipelineStep: state.pipelineStep,
|
|
139
|
+
requestedStep,
|
|
140
|
+
decision: verdict.allowed ? 'allow' : 'block',
|
|
141
|
+
reason: verdict.allowed ? undefined : verdict.message,
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
} catch {
|
|
145
|
+
// Failure-open: never block the hook on a ledger-write error.
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (verdict.allowed) {
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Block: print to stderr (Claude Code surfaces to the model) and
|
|
153
|
+
// exit 2.
|
|
154
|
+
process.stderr.write(verdict.message + '\n')
|
|
155
|
+
return 2
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// `parseAdvanceCommand` (and its `stripQuotes` helper) was promoted to
|
|
159
|
+
// `@alecsibilia/luca-core/state` (see `packages/luca-core/src/state/
|
|
160
|
+
// cli-parse.ts`) — both the pipeline-guard hook and the
|
|
161
|
+
// continuation-messages hook used byte-identical copies, so the
|
|
162
|
+
// helper now lives in luca-core and is imported above (CF11).
|
|
163
|
+
|
|
164
|
+
// Touched to suppress unused-import in some toolchains; `join` may be
|
|
165
|
+
// folded out by the bundler if unused, but kept here in case future
|
|
166
|
+
// edits need explicit cwd handling.
|
|
167
|
+
void join
|
|
168
|
+
|
|
169
|
+
main().then(
|
|
170
|
+
(code) => process.exit(code),
|
|
171
|
+
(err) => {
|
|
172
|
+
// Defensive: any unexpected throw means we fail open. We
|
|
173
|
+
// explicitly do NOT exit 2 on internal errors — that would
|
|
174
|
+
// block the user's command on a hook bug.
|
|
175
|
+
process.stderr.write(
|
|
176
|
+
`pipeline-guard handler: internal error (failing open): ${
|
|
177
|
+
(err as Error).message
|
|
178
|
+
}\n`,
|
|
179
|
+
)
|
|
180
|
+
process.exit(0)
|
|
181
|
+
},
|
|
182
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "bun \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pipeline-guard.ts",
|
|
9
|
+
"timeout": 5,
|
|
10
|
+
"statusMessage": "PreToolUse guard for `luca state advance` — blocks illegal pipelineStep transitions via the canonical legal-transitions table."
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"matcher": "Bash"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"PostToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "bun \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/continuation-messages.ts",
|
|
22
|
+
"timeout": 5,
|
|
23
|
+
"statusMessage": "PostToolUse continuation prompt for `luca state advance` — injects a mode-entry kick-off message via additionalContext when the pipeline advances."
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"matcher": "Bash"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "bun \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/context-refresher.ts",
|
|
33
|
+
"timeout": 5,
|
|
34
|
+
"statusMessage": "PostToolUse context-refresher — surfaces a per-step <luca-reminder> via additionalContext after every Nth tool call (default 30) or on a step change since the last fire."
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"matcher": "*"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: arch-audit
|
|
3
|
+
description: "Find deepening opportunities — shallow modules, premature abstractions, misplaced seams. Uses the deletion test and promotion model to surface architectural friction. Use when user says \"audit architecture\", \"find refactoring opportunities\", \"what's shallow\", \"improve structure\", or invokes /arch-audit."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# arch-audit
|
|
7
|
+
|
|
8
|
+
Surface architectural friction and propose deepening opportunities — refactors that turn shallow modules into deep ones, improve testability, and increase locality of change.
|
|
9
|
+
|
|
10
|
+
## Vocabulary
|
|
11
|
+
|
|
12
|
+
Use these terms exactly in every finding. Consistent language prevents drift into vague terms like "component," "service," or "boundary."
|
|
13
|
+
|
|
14
|
+
- **Module** — anything with an interface and implementation (function, class, file, package). Scale-agnostic.
|
|
15
|
+
- **Interface** — what a caller must know: types, invariants, error modes, ordering. Not just the type signature.
|
|
16
|
+
- **Depth** — leverage at the interface. **Deep** = significant behavior behind a small interface. **Shallow** = interface nearly as complex as the implementation.
|
|
17
|
+
- **Seam** — where behavior can be altered without editing in place. A boundary that accepts different adapters.
|
|
18
|
+
- **Deletion test** — imagine deleting the module. Complexity vanishes → pass-through (shallow). Complexity reappears across callers → earning its keep (deep).
|
|
19
|
+
- **Promotion tier** — where code lives based on caller count. Tier 1 (single caller, private). Tier 2 (shared within feature). Tier 3 (shared across features).
|
|
20
|
+
|
|
21
|
+
## Step 1: Recall architectural context
|
|
22
|
+
|
|
23
|
+
Query MuninnDB for past decisions so you don't re-suggest rejected refactors:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
mcp__muninn__muninn_recall({
|
|
27
|
+
vault: "<repo_vault>",
|
|
28
|
+
context: ["architectural decisions", "rejected refactors", "module structure"],
|
|
29
|
+
tags: ["decision"],
|
|
30
|
+
limit: 15
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Resolve `<repo_vault>` from `.luca/config.json` → `muninn.vault`, falling back to `"default"`.
|
|
35
|
+
|
|
36
|
+
If a `docs/context.md` exists, read it for project constraints and domain terminology (check the repo root for a legacy `context.md` as a fallback).
|
|
37
|
+
|
|
38
|
+
**Guard**: if a past decision explicitly rejected a refactor you'd otherwise suggest, skip it. Only resurface if friction has materially worsened since the decision was recorded.
|
|
39
|
+
|
|
40
|
+
## Step 2: Explore for friction
|
|
41
|
+
|
|
42
|
+
Spawn one or more codebase-exploration subagents (the `Explore` agent, via the `Agent` tool) to walk the codebase. Provide this brief:
|
|
43
|
+
|
|
44
|
+
> Walk the codebase and report friction. Look for:
|
|
45
|
+
>
|
|
46
|
+
> 1. **Shallow modules** — interface nearly as complex as implementation. Apply the deletion test: would removing this module concentrate complexity (earning its keep) or just redistribute it (pass-through)?
|
|
47
|
+
> 2. **Concept bouncing** — understanding one concept requires reading across many small files
|
|
48
|
+
> 3. **Premature abstractions** — interfaces/abstract types with a single implementation
|
|
49
|
+
> 4. **Leaked coupling** — tightly-coupled modules that share internal details across their seam
|
|
50
|
+
> 5. **Misplaced code** — helpers/utilities at promotion tier 3 (shared across features) that have only 1-2 callers
|
|
51
|
+
> 6. **Untestable surfaces** — code that's hard to test through its current public interface
|
|
52
|
+
>
|
|
53
|
+
> For each finding, report: file path, what the friction is, deletion test result (if applicable), caller count.
|
|
54
|
+
|
|
55
|
+
If the codebase is large, scope the exploration to specific areas the user mentions, or split into multiple focused subagents (e.g., one per package in a monorepo).
|
|
56
|
+
|
|
57
|
+
Do NOT follow rigid heuristics. Explore organically. Note where you experience friction — where understanding breaks down, where you bounce between files, where interfaces feel heavier than what they hide.
|
|
58
|
+
|
|
59
|
+
## Step 3: Present deepening candidates
|
|
60
|
+
|
|
61
|
+
Synthesize subagent findings into a numbered list of **deepening opportunities**. For each candidate:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
### <N>. <Short description>
|
|
65
|
+
|
|
66
|
+
- **Files**: <paths involved>
|
|
67
|
+
- **Problem**: <what the friction is — use vocabulary precisely>
|
|
68
|
+
- **Deletion test**: <result — "vanishes" or "reappears across N callers">
|
|
69
|
+
- **Promotion check**: <current tier vs appropriate tier based on caller count>
|
|
70
|
+
- **Proposed direction**: <one sentence — what "deeper" looks like here>
|
|
71
|
+
- **Risk**: <what could go wrong if refactored>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Order by impact (most friction first). Limit to 5-7 candidates — more than that is noise.
|
|
75
|
+
|
|
76
|
+
**Do NOT propose interfaces or implementations yet.** Present candidates only.
|
|
77
|
+
|
|
78
|
+
Ask: "Which of these would you like to explore?"
|
|
79
|
+
|
|
80
|
+
## Step 4: Design alternatives (design-it-twice)
|
|
81
|
+
|
|
82
|
+
For the candidate the user selects:
|
|
83
|
+
|
|
84
|
+
1. **Frame the problem** — write a short explanation for the user:
|
|
85
|
+
- What constraints any new interface must satisfy
|
|
86
|
+
- What dependencies exist and their nature
|
|
87
|
+
- A rough illustrative sketch (not a proposal — just grounding)
|
|
88
|
+
|
|
89
|
+
2. **Spawn 2-3 `Explore` / `Plan` subagents in parallel** (via the `Agent` tool) — each must produce a *radically different* interface design for the deepened module. Brief each subagent with:
|
|
90
|
+
- File paths and current coupling
|
|
91
|
+
- Caller list and their expectations
|
|
92
|
+
- The vocabulary (Module, Interface, Depth, Seam)
|
|
93
|
+
- Constraint: "Your design must be fundamentally different from the others"
|
|
94
|
+
|
|
95
|
+
3. **Present designs sequentially** — let the user absorb each one:
|
|
96
|
+
- Interface signature (public surface)
|
|
97
|
+
- Usage example (how callers use it)
|
|
98
|
+
- What it hides (complexity kept internal)
|
|
99
|
+
- Trade-offs (what this design makes easy vs hard)
|
|
100
|
+
|
|
101
|
+
4. **Compare and recommend** — contrast by:
|
|
102
|
+
- Depth (leverage at the interface)
|
|
103
|
+
- Locality (where change concentrates)
|
|
104
|
+
- Seam placement (where behavior can be swapped)
|
|
105
|
+
- Testability (how easy to test through the interface)
|
|
106
|
+
|
|
107
|
+
Give your recommendation. If elements from different designs combine well, propose a hybrid.
|
|
108
|
+
|
|
109
|
+
5. **User decides** — they pick a design (or request iteration).
|
|
110
|
+
|
|
111
|
+
## Step 5: Record the outcome
|
|
112
|
+
|
|
113
|
+
After the user decides, store the result in MuninnDB.
|
|
114
|
+
|
|
115
|
+
**If the refactor is accepted:**
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
mcp__muninn__muninn_remember({
|
|
119
|
+
vault: "<repo_vault>",
|
|
120
|
+
concept: "decision:arch-audit-<descriptive-slug>",
|
|
121
|
+
content: "Accepted deepening: <what was refactored, which design was chosen, why>. Files: <paths>. Date: <ISO>.",
|
|
122
|
+
tags: ["decision", "architecture", "refactor"]
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Capture the returned id and promote this user-confirmed decision to the verified tier: `mcp__muninn__muninn_trust({ id: <returned-id>, trust: "verified", vault: "<repo_vault>" })`.
|
|
127
|
+
|
|
128
|
+
**If the refactor is rejected:**
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
mcp__muninn__muninn_remember({
|
|
132
|
+
vault: "<repo_vault>",
|
|
133
|
+
concept: "decision:arch-audit-rejected-<descriptive-slug>",
|
|
134
|
+
content: "Rejected deepening of <module>. Reason: <user's reason>. Friction level at time of rejection: <description>. Re-evaluate if: <conditions that would change the decision>.",
|
|
135
|
+
tags: ["decision", "architecture", "rejected"]
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Capture the returned id and promote it to the verified tier: `mcp__muninn__muninn_trust({ id: <returned-id>, trust: "verified", vault: "<repo_vault>" })`.
|
|
140
|
+
|
|
141
|
+
The rejected decision prevents re-suggestion on future runs (Step 1 guard).
|
|
142
|
+
|
|
143
|
+
Note: `decision:*` memories are project-scoped — they route to the **repo** vault.
|
|
144
|
+
|
|
145
|
+
## Step 6: Plan the refactor (optional)
|
|
146
|
+
|
|
147
|
+
If the user wants to proceed with implementation:
|
|
148
|
+
|
|
149
|
+
- Ask: "Want me to create a plan for this refactor, or do it directly?"
|
|
150
|
+
- If plan → suggest invoking `/lu` with the chosen design as context
|
|
151
|
+
- If direct → proceed with implementation in the current session
|
|
152
|
+
|
|
153
|
+
The chosen interface from Step 4 becomes the first test's target (interface-first task boundaries).
|
|
154
|
+
|
|
155
|
+
## Behavioral Notes
|
|
156
|
+
|
|
157
|
+
- **This is ad-hoc** — invoked when the user feels friction, not on a schedule
|
|
158
|
+
- **No file dependencies** — everything lives in MuninnDB or is inlined in this skill
|
|
159
|
+
- **Scope** — if the user doesn't specify an area, ask before exploring the entire codebase. For monorepos, start with one package.
|
|
160
|
+
- **Vocabulary discipline** — use Module/Interface/Depth/Seam/Deletion Test consistently. Don't drift into "component," "service," "API," "boundary," "layer."
|
|
161
|
+
- **Don't be prescriptive about tooling** — the skill identifies friction and proposes designs. It doesn't force a specific architecture style (DDD, hexagonal, etc.) unless the project already uses one.
|