@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,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: phase-plan
|
|
3
|
+
description: "Drive the \"plan\" pipeline step — produce a phase plan grounded in the user decisions from /phase-discuss."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /phase-plan
|
|
7
|
+
|
|
8
|
+
You are running the **plan** step. Research is done, user decisions are captured in `context.md`. Your job is to produce a phase plan that downstream stages will execute.
|
|
9
|
+
|
|
10
|
+
## Preconditions
|
|
11
|
+
|
|
12
|
+
1. Run `luca state read`. The `pipelineStep` must be `architect` (entering plan) or `plan` (already advanced).
|
|
13
|
+
2. If currently `architect`, run `luca state advance --to-step plan`.
|
|
14
|
+
3. Run `luca phase current` to get the active slug and directory. If no active phase, abort.
|
|
15
|
+
|
|
16
|
+
## Read inputs
|
|
17
|
+
|
|
18
|
+
Read these in order via the `Read` tool:
|
|
19
|
+
- `.luca/phases/<slug>/research.md` — research findings
|
|
20
|
+
- `.luca/phases/<slug>/context.md` — user decisions
|
|
21
|
+
|
|
22
|
+
If either is missing, abort with a clear error pointing at the missing step.
|
|
23
|
+
|
|
24
|
+
## Produce the plan
|
|
25
|
+
|
|
26
|
+
The legacy v12 `luca-planner` subagent was dropped per plan §5.6 — planning work is done by the architect mode-agent or, when invoked from the `/phase-plan` command flow, inline by the orchestrator. Synthesize the plan from:
|
|
27
|
+
- The phase slug
|
|
28
|
+
- The current `pipelineStep` (always `plan` here)
|
|
29
|
+
- The research findings + user decisions read above
|
|
30
|
+
- The repo's coding patterns (from research) and acceptance criteria
|
|
31
|
+
|
|
32
|
+
The plan should be a markdown document with: objective, atomic tasks (waves), verification criteria per task, and success criteria for the phase.
|
|
33
|
+
|
|
34
|
+
## Persist the plan
|
|
35
|
+
|
|
36
|
+
Write the plan with the `Write` tool to the canonical path. Use the `dir` field from `luca phase current`; the plan path is `<dir>/plan.md`:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Write tool → <dir>/plan.md
|
|
40
|
+
content: "<plan markdown>"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The stage-gate hook only permits this `Write` to `<dir>/plan.md` while `pipelineStep === "plan"` — any other path or step is blocked.
|
|
44
|
+
|
|
45
|
+
## Advance
|
|
46
|
+
|
|
47
|
+
Run `luca state advance --to-step plan-review` to hand off to plan-review.
|
|
48
|
+
|
|
49
|
+
## What you must NOT do
|
|
50
|
+
|
|
51
|
+
- Do NOT write code. Code writes are blocked in PLANNING.
|
|
52
|
+
- Do NOT skip the synthesis step — read research.md + context.md before drafting the plan. The plan must be grounded in those inputs.
|
|
53
|
+
- Do NOT write `plan.md` to any path other than `<dir>/plan.md`, or via `Edit` — the hook blocks every other `.luca/` write.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: repo-cleanup
|
|
3
|
+
description: Scan the repository for AI-session debris and optionally clean it up.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /repo-cleanup
|
|
7
|
+
|
|
8
|
+
Scan the repository for AI-session debris — orphaned scripts, misplaced source files, tool artifacts, dead exports, `.luca/` contract violations, repo-root markdown debris — and optionally apply the remediations.
|
|
9
|
+
|
|
10
|
+
The scan is performed by the **`luca-shadow-scanner`** subagent (strictly read-only). This command drives that scan and applies fixes through the **`luca repo cleanup-apply`** CLI (the destructive write half).
|
|
11
|
+
|
|
12
|
+
## Parse arguments
|
|
13
|
+
|
|
14
|
+
Parse `$ARGUMENTS` for flags:
|
|
15
|
+
|
|
16
|
+
- `--quick` — quick scan (categories 1 + 3 only)
|
|
17
|
+
- `--full` — full scan (all 7 categories, including dead exports)
|
|
18
|
+
- `--dry-run` — show findings without applying anything
|
|
19
|
+
- `--fix` — auto-apply every `auto_fixable` finding without prompting
|
|
20
|
+
- `--category=N` — restrict to a single detection category (1–7)
|
|
21
|
+
|
|
22
|
+
If no scan-mode flag is given, default to **`standard`** mode with interactive review.
|
|
23
|
+
|
|
24
|
+
Resolve `<repo_vault>` from `.luca/config.json` → `muninn.vault`, falling back to `"default"`.
|
|
25
|
+
|
|
26
|
+
## Step 1 — Scan
|
|
27
|
+
|
|
28
|
+
Spawn the **`luca-shadow-scanner`** subagent via the `Agent` tool. The task prompt must include:
|
|
29
|
+
|
|
30
|
+
- The scan mode (`quick` | `standard` | `full`) resolved from the flags.
|
|
31
|
+
- Any `--category=N` filter — tell the scanner to report only that category.
|
|
32
|
+
|
|
33
|
+
The subagent ends its response with a single JSON block conforming to `ShadowScanReportSchema` (`scan_mode`, `categories_scanned`, `findings[]`, `summary`, `scanned_at`).
|
|
34
|
+
|
|
35
|
+
## Step 2 — Parse the report
|
|
36
|
+
|
|
37
|
+
Take the **last JSON block** of the subagent's response as the `ShadowScanReport`. Each entry in `findings[]` has: `category`, `severity`, `file_path`, `description`, `recommendation`, `recommended_action` (`delete` | `move` | `gitignore`), `target_path?`, `auto_fixable`.
|
|
38
|
+
|
|
39
|
+
Display the findings banner: total count plus the per-severity breakdown from `summary`.
|
|
40
|
+
|
|
41
|
+
## Step 3 — Handle the findings
|
|
42
|
+
|
|
43
|
+
- **No findings** (`summary.total === 0`) → report a clean scan and stop.
|
|
44
|
+
|
|
45
|
+
- **`--dry-run`** → display all findings grouped by severity (critical first) and stop. Apply nothing.
|
|
46
|
+
|
|
47
|
+
- **`--fix`** → for every finding where `auto_fixable === true`, stage that single finding object in a JSON file and run:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
# /tmp/luca-cleanup-finding.json holds the single finding object
|
|
51
|
+
luca repo cleanup-apply --file /tmp/luca-cleanup-finding.json --confirm
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Findings with `auto_fixable === false` (e.g. repo-root markdown, SUMMARY moves) are listed for the user but not auto-applied.
|
|
55
|
+
|
|
56
|
+
- **Interactive mode** (default) → present each finding sorted by severity (critical first). For each one, offer three choices:
|
|
57
|
+
|
|
58
|
+
- **Fix** → stage the single finding object in a JSON file and run `luca repo cleanup-apply --file <path> --confirm`. For a `move`, the finding must carry `target_path`; if it does not, ask the user where it should go and add `target_path` to the file before running.
|
|
59
|
+
- **Keep** → record the user's decision so the file is not re-flagged next scan:
|
|
60
|
+
|
|
61
|
+
First call `mcp__muninn__muninn_remember` with `vault: "<repo_vault>"`, `concept: "shadow-debt:kept:<file_path>"`, and content noting the user approved keeping `<file_path>` with an ISO timestamp. Then promote it: `mcp__muninn__muninn_trust({ id: <returned id>, trust: "verified", vault: "<repo_vault>" })` — this is a user-confirmed decision. The `luca-shadow-scanner` recalls `shadow-debt:kept` entries and excludes them from future scans.
|
|
62
|
+
- **Skip** → take no action; the file will be flagged again on the next scan.
|
|
63
|
+
|
|
64
|
+
## Step 4 — Store the cleanup metric
|
|
65
|
+
|
|
66
|
+
After processing every finding, record what the cleanup did (`metric:*` routes to the repo vault per the vault-routing rule):
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
mcp__muninn__muninn_remember({
|
|
70
|
+
vault: "<repo_vault>",
|
|
71
|
+
concept: "metric:shadow-debt-cleanup-<ISO timestamp>",
|
|
72
|
+
content: JSON.stringify({
|
|
73
|
+
scan_mode, total, fixed, kept, skipped, cleaned_at
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `luca-shadow-scanner` already stores the raw scan counts under `metric:shadow-debt-scan-*`; this metric captures the action outcome (`fixed` / `kept` / `skipped`).
|
|
79
|
+
|
|
80
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: todo-add
|
|
3
|
+
description: Add a new item to the development backlog.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /todo-add
|
|
7
|
+
|
|
8
|
+
Add a new todo to the development backlog. Todos persist in **MuninnDB** (concept `todo:<id>`, repo vault) — there is no `.luca/todos/` directory.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. **Parse the title.** Treat `$ARGUMENTS` as the todo title — a short imperative description (e.g. "Add retry to the upload client"). If no arguments were provided, ask the user what they'd like to add.
|
|
13
|
+
|
|
14
|
+
2. **Create the todo.** Run the `luca todo add` CLI with the title:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
luca todo add --title "<title>" --source manual
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `--source manual` marks this as a hand-entered item (vs. `gh-issue-#N` or `phase-research`).
|
|
21
|
+
- If the user supplied acceptance criteria or extra context, pass it as `--body "<text>"`.
|
|
22
|
+
- The `id` is derived from the title automatically; only pass an explicit `--id` if the user asked for a specific slug.
|
|
23
|
+
|
|
24
|
+
3. **Execute the returned instruction.** `luca todo add` validates the input and prints a `mcp__muninn__muninn_remember` instruction blob (delegation pattern — the `luca` CLI cannot call MuninnDB directly). Execute that instruction **exactly as returned** to persist the todo.
|
|
25
|
+
|
|
26
|
+
4. **Confirm.** Report that the todo was created and show its `id` (the kebab-case slug derived from the title) and its status (`pending`).
|
|
27
|
+
|
|
28
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: todo-check
|
|
3
|
+
description: List all items in the development backlog.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /todo-check
|
|
7
|
+
|
|
8
|
+
List the development backlog. Todos live in **MuninnDB** (concept `todo:*`, repo vault).
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. **Parse the filter.** Check `$ARGUMENTS` for an optional status filter — one of `pending`, `backlog`, or `done`.
|
|
13
|
+
|
|
14
|
+
2. **List the todos.** Run the `luca todo list` CLI:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
luca todo list
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pass `--status <filter>` if a filter was given.
|
|
21
|
+
|
|
22
|
+
3. **Execute the returned instruction.** `luca todo list` prints a `mcp__muninn__muninn_recall` instruction blob (delegation pattern). Execute it **exactly as returned** to recall the todos.
|
|
23
|
+
|
|
24
|
+
4. **Parse each entry.** Each recalled memory's `content` is JSON conforming to `TodoSchema` (`id`, `title`, `body?`, `status`, `source?`, `updatedAt`). Parse every entry. If a status filter was requested, keep only todos whose `content.status` matches it.
|
|
25
|
+
|
|
26
|
+
5. **Display.** Render a numbered checklist grouped by status, in this order:
|
|
27
|
+
|
|
28
|
+
1. ⬜ **Pending** — `status: "pending"`
|
|
29
|
+
2. 📋 **Backlog** — `status: "backlog"`
|
|
30
|
+
3. ✅ **Done** — `status: "done"`
|
|
31
|
+
|
|
32
|
+
For each todo, show its `id`, `title`, `source` (if set), and `updatedAt`.
|
|
33
|
+
|
|
34
|
+
6. **Empty backlog.** If no todos came back, tell the user the backlog is empty and suggest `/todo-add` to start building it.
|
|
35
|
+
|
|
36
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* context-refresher handler — `PostToolUse` hook that surfaces a
|
|
4
|
+
* per-step `<luca-reminder>` to combat context rot.
|
|
5
|
+
*
|
|
6
|
+
* This is the Claude Code delivery vehicle for the pure
|
|
7
|
+
* `computeContextRefresher()` algorithm 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. We do NOT
|
|
12
|
+
* narrow on tool_name — the hook is registered with matcher `*`
|
|
13
|
+
* because every tool call is a context-growth tick.
|
|
14
|
+
* 2. Load (or initialize) the sidecar `.claude/cache/
|
|
15
|
+
* context-refresher-state.json`.
|
|
16
|
+
* 3. Increment the toolCallCount.
|
|
17
|
+
* 4. Read `.luca/state.json` for the current pipelineStep.
|
|
18
|
+
* 5. Call `computeContextRefresher()`.
|
|
19
|
+
* 6. If the verdict carries `nextState`, persist it back to the
|
|
20
|
+
* sidecar. If the verdict carries a `refresh-emitted` reason,
|
|
21
|
+
* emit the message via `additionalContext` in the PostToolUse
|
|
22
|
+
* hook output JSON shape.
|
|
23
|
+
* 7. Exit 0 always (informational hook — never blocks).
|
|
24
|
+
*
|
|
25
|
+
* The Claude Code hook contract (PostToolUse):
|
|
26
|
+
*
|
|
27
|
+
* stdin JSON shape (snake_case fields per the docs):
|
|
28
|
+
* {
|
|
29
|
+
* "session_id": "...",
|
|
30
|
+
* "hook_event_name": "PostToolUse",
|
|
31
|
+
* "tool_name": "<any>",
|
|
32
|
+
* "tool_input": { ... },
|
|
33
|
+
* "tool_response": { ... }
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* Exit codes:
|
|
37
|
+
* 0 → all signals come from stdout JSON
|
|
38
|
+
* 2 → block (not used here — this hook is informational)
|
|
39
|
+
* * → other non-zero → error (failure-open: we never use this)
|
|
40
|
+
*
|
|
41
|
+
* Stdout JSON for context injection:
|
|
42
|
+
* {
|
|
43
|
+
* "hookSpecificOutput": {
|
|
44
|
+
* "hookEventName": "PostToolUse",
|
|
45
|
+
* "additionalContext": "<luca-reminder>...</luca-reminder>"
|
|
46
|
+
* }
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* Sidecar location: `.claude/cache/context-refresher-state.json`. This
|
|
50
|
+
* lives under `.claude/` rather than `.luca/` because it is hook-
|
|
51
|
+
* managed bookkeeping (the cross-invocation counter), not pipeline
|
|
52
|
+
* state. The `.luca/` contract is a strict allowlist of pipeline-state
|
|
53
|
+
* artifacts; the sidecar would violate that contract. `.claude/cache/`
|
|
54
|
+
* is a fresh subdirectory (Claude Code's harness ignores unknown
|
|
55
|
+
* files under `.claude/`, so we own the namespace) — wipe it freely
|
|
56
|
+
* during Phase H cleanup if needed.
|
|
57
|
+
*
|
|
58
|
+
* Failure-open philosophy (per E-1 / E-2 / E-3 reasoning, intentionally
|
|
59
|
+
* reused): if ANYTHING unexpected happens (stdin parse error, state.json
|
|
60
|
+
* missing/malformed, sidecar read/write failure), exit 0 silently. A
|
|
61
|
+
* hook that mis-emits a reminder is mildly annoying; a hook that
|
|
62
|
+
* crashes the session is worse. Choose silent skip.
|
|
63
|
+
*/
|
|
64
|
+
import { existsSync } from 'node:fs'
|
|
65
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
66
|
+
import { dirname, join } from 'node:path'
|
|
67
|
+
|
|
68
|
+
import { appendLedger } from '@alecsibilia/luca-core/ledger'
|
|
69
|
+
import {
|
|
70
|
+
computeContextRefresher,
|
|
71
|
+
type ContextRefresherCarryState,
|
|
72
|
+
type ContextRefresherInput,
|
|
73
|
+
} from '@alecsibilia/luca-core/orchestration'
|
|
74
|
+
import { loadCurrentState } from '@alecsibilia/luca-core/state'
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Shape of the relevant slice of the PostToolUse stdin payload. We do
|
|
78
|
+
* not introspect the payload beyond reading it; the matcher (`*`) is
|
|
79
|
+
* the only gate on this hook. Defensive typing — the harness may add
|
|
80
|
+
* fields and the handler should not break on them.
|
|
81
|
+
*/
|
|
82
|
+
interface PostToolUsePayload {
|
|
83
|
+
tool_name?: string
|
|
84
|
+
toolName?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* On-disk sidecar shape. Identical fields to ContextRefresherCarryState
|
|
89
|
+
* with a `schemaVersion` for future migrations. The handler reads the
|
|
90
|
+
* sidecar with defensive defaults — a missing or malformed file
|
|
91
|
+
* collapses to "no prior state" (counter zero, no lastFiredStep).
|
|
92
|
+
*/
|
|
93
|
+
interface SidecarFile {
|
|
94
|
+
schemaVersion: 1
|
|
95
|
+
toolCallCount: number
|
|
96
|
+
lastFiredStep?: string
|
|
97
|
+
lastFiredAt?: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const SIDECAR_RELATIVE_PATH = '.claude/cache/context-refresher-state.json'
|
|
101
|
+
|
|
102
|
+
async function main(): Promise<number> {
|
|
103
|
+
const raw = await Bun.stdin.text()
|
|
104
|
+
if (!raw.trim()) {
|
|
105
|
+
// Empty stdin — nothing to do. Allow silently.
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// We don't need any field from the payload — the matcher (`*`)
|
|
111
|
+
// already filtered to "every tool call". We still parse to
|
|
112
|
+
// detect malformed payloads and fail open silently in that case.
|
|
113
|
+
JSON.parse(raw) as PostToolUsePayload
|
|
114
|
+
} catch {
|
|
115
|
+
// Malformed stdin is the harness's problem. Fail open silently.
|
|
116
|
+
return 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cwd = process.cwd()
|
|
120
|
+
|
|
121
|
+
// Load prior sidecar state (or default to a fresh counter).
|
|
122
|
+
const priorRaw = await readSidecar(cwd)
|
|
123
|
+
// Increment the counter to reflect THIS tool call BEFORE calling
|
|
124
|
+
// the algorithm — the algorithm's threshold check compares the
|
|
125
|
+
// post-increment value to toolCallsPerRefresh.
|
|
126
|
+
const priorState: ContextRefresherCarryState = {
|
|
127
|
+
toolCallCount: priorRaw.toolCallCount + 1,
|
|
128
|
+
...(priorRaw.lastFiredStep !== undefined
|
|
129
|
+
? { lastFiredStep: priorRaw.lastFiredStep }
|
|
130
|
+
: {}),
|
|
131
|
+
...(priorRaw.lastFiredAt !== undefined
|
|
132
|
+
? { lastFiredAt: priorRaw.lastFiredAt }
|
|
133
|
+
: {}),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const state = await loadCurrentState({ cwd })
|
|
137
|
+
const now = new Date().toISOString()
|
|
138
|
+
|
|
139
|
+
const input: ContextRefresherInput = {
|
|
140
|
+
currentStep: state.pipelineStep,
|
|
141
|
+
priorState,
|
|
142
|
+
now,
|
|
143
|
+
...(state.complexity !== undefined
|
|
144
|
+
? { complexity: state.complexity }
|
|
145
|
+
: {}),
|
|
146
|
+
...(state.oversight !== undefined
|
|
147
|
+
? { oversight: state.oversight }
|
|
148
|
+
: {}),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const verdict = computeContextRefresher(input)
|
|
152
|
+
|
|
153
|
+
// Persist nextState when present. We do this BEFORE writing to
|
|
154
|
+
// stdout so a downstream stdout error doesn't leave the counter
|
|
155
|
+
// un-incremented (which would let the refresher fire on every
|
|
156
|
+
// subsequent tool call until the counter caught up again).
|
|
157
|
+
if (verdict?.nextState !== undefined) {
|
|
158
|
+
await writeSidecar(cwd, verdict.nextState)
|
|
159
|
+
} else {
|
|
160
|
+
// No nextState in the verdict — either the algorithm returned
|
|
161
|
+
// null (idle step, no carry needed) or this is an
|
|
162
|
+
// unknown-current-step verdict. In either case persist a
|
|
163
|
+
// fresh counter from the increment so we don't lose ticks.
|
|
164
|
+
await writeSidecar(cwd, priorState)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Emit a ledger event for the hook firing. Only log on decisive
|
|
168
|
+
// outcomes (emit vs skipped) — every tool call fires this hook, so
|
|
169
|
+
// logging every tick would flood the ledger. Failure-open.
|
|
170
|
+
if (verdict !== null && verdict.reason === 'refresh-emitted') {
|
|
171
|
+
try {
|
|
172
|
+
const runId = typeof (state as { sessionId?: unknown }).sessionId === 'string'
|
|
173
|
+
? (state as { sessionId: string }).sessionId
|
|
174
|
+
: ''
|
|
175
|
+
appendLedger({
|
|
176
|
+
cwd,
|
|
177
|
+
runId,
|
|
178
|
+
event: 'hook.context-refresher.fired',
|
|
179
|
+
data: {
|
|
180
|
+
pipelineStep: state.pipelineStep,
|
|
181
|
+
decision: 'emitted',
|
|
182
|
+
toolCallCount: priorState.toolCallCount,
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
} catch {
|
|
186
|
+
// Failure-open.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (verdict === null) {
|
|
191
|
+
// Idle step or quiet skip. No additionalContext emitted.
|
|
192
|
+
return 0
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (verdict.reason !== 'refresh-emitted') {
|
|
196
|
+
// No reminder for this tick (cooldown active, counter below
|
|
197
|
+
// threshold, or unknown step — we don't surface internal
|
|
198
|
+
// warnings to the model).
|
|
199
|
+
return 0
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Emit the additionalContext payload. Claude Code consumes the
|
|
203
|
+
// first valid JSON object on stdout for PostToolUse hooks.
|
|
204
|
+
const output = {
|
|
205
|
+
hookSpecificOutput: {
|
|
206
|
+
hookEventName: 'PostToolUse',
|
|
207
|
+
additionalContext: verdict.message,
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
process.stdout.write(JSON.stringify(output) + '\n')
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Read the sidecar file, returning defaults on missing/malformed file.
|
|
216
|
+
* Never throws — every error path collapses to "no prior state".
|
|
217
|
+
*/
|
|
218
|
+
async function readSidecar(cwd: string): Promise<SidecarFile> {
|
|
219
|
+
const fallback: SidecarFile = { schemaVersion: 1, toolCallCount: 0 }
|
|
220
|
+
const p = join(cwd, SIDECAR_RELATIVE_PATH)
|
|
221
|
+
if (!existsSync(p)) return fallback
|
|
222
|
+
try {
|
|
223
|
+
const raw = await readFile(p, 'utf-8')
|
|
224
|
+
if (!raw.trim()) return fallback
|
|
225
|
+
const parsed = JSON.parse(raw) as Partial<SidecarFile>
|
|
226
|
+
if (typeof parsed.toolCallCount !== 'number') return fallback
|
|
227
|
+
return {
|
|
228
|
+
schemaVersion: 1,
|
|
229
|
+
toolCallCount: parsed.toolCallCount,
|
|
230
|
+
...(typeof parsed.lastFiredStep === 'string'
|
|
231
|
+
? { lastFiredStep: parsed.lastFiredStep }
|
|
232
|
+
: {}),
|
|
233
|
+
...(typeof parsed.lastFiredAt === 'string'
|
|
234
|
+
? { lastFiredAt: parsed.lastFiredAt }
|
|
235
|
+
: {}),
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
return fallback
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Write the sidecar file atomically (write-then-rename). On any error,
|
|
244
|
+
* swallow silently — we'd rather lose a counter tick than crash the
|
|
245
|
+
* session over a transient disk hiccup.
|
|
246
|
+
*/
|
|
247
|
+
async function writeSidecar(
|
|
248
|
+
cwd: string,
|
|
249
|
+
state: ContextRefresherCarryState,
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
const p = join(cwd, SIDECAR_RELATIVE_PATH)
|
|
252
|
+
const dir = dirname(p)
|
|
253
|
+
const tmp = `${p}.tmp`
|
|
254
|
+
const payload: SidecarFile = {
|
|
255
|
+
schemaVersion: 1,
|
|
256
|
+
toolCallCount: state.toolCallCount,
|
|
257
|
+
...(state.lastFiredStep !== undefined
|
|
258
|
+
? { lastFiredStep: state.lastFiredStep }
|
|
259
|
+
: {}),
|
|
260
|
+
...(state.lastFiredAt !== undefined
|
|
261
|
+
? { lastFiredAt: state.lastFiredAt }
|
|
262
|
+
: {}),
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
await mkdir(dir, { recursive: true })
|
|
266
|
+
await writeFile(tmp, JSON.stringify(payload) + '\n', 'utf-8')
|
|
267
|
+
// Bun-supported rename via fs/promises (atomic on POSIX).
|
|
268
|
+
const { rename } = await import('node:fs/promises')
|
|
269
|
+
await rename(tmp, p)
|
|
270
|
+
} catch {
|
|
271
|
+
// Sidecar persistence failure is non-fatal — next invocation
|
|
272
|
+
// will recompute from a fresh counter. Silently absorb.
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
main().then(
|
|
277
|
+
(code) => process.exit(code),
|
|
278
|
+
(err) => {
|
|
279
|
+
// Defensive: any unexpected throw means we fail open silently.
|
|
280
|
+
// No stderr — informational hook; users shouldn't see internal
|
|
281
|
+
// errors.
|
|
282
|
+
void err
|
|
283
|
+
process.exit(0)
|
|
284
|
+
},
|
|
285
|
+
)
|