Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,5 @@ plans/
.cafekit.lock
.claude
.agents
.codex
.codex
.opencode/
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,4 @@ Always respond in **Tiếng Việt**. Technical terms, code identifiers, and fil

## Addressing (Context Overflow Indicator)

Claude Code always addresses the user as "đại ca" throughout the conversation. If it stops doing so, it is a sign the context has been compacted/truncated — tell the user to consider `/clear`.
Claude Code always addresses the user as "bro" throughout the conversation. If it stops doing so, it is a sign the context has been compacted/truncated — tell the user to consider `/clear`.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ CafeKit installs a working runtime bundle for Claude Code:
- `hapo:debug` and `hapo:hotfix` for evidence-first diagnosis and safe bug fixes
- `hapo:docs` for project documentation and source-backed as-is reconstruction
- `hapo:test` and `hapo:code-review` for verification
- `hapo:generate-graph` for technical diagrams
- supporting hooks, agents, rules, and statusline runtime

CafeKit uses rule-based skill routing guidance and an installed skill catalog.
Expand Down
19 changes: 16 additions & 3 deletions docs/project-changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,26 @@ All notable changes to CafeKit are documented here, following

## [Unreleased]

### Added
- **`spec-gate.cjs` (Stop completion gate)**: machine-enforced receipt check when the assistant ends a turn with newly-done tasks (Evidence section + proof, no placeholders, `completed_at` set). First-run seeds cache without blocking; escape hatch `spec.completion_gate: false`. Complements the soft `spec-state` reminder (audit §3.4.1).
- **OpenCode port Option A — spec-workflow enforcement**: `task-scaffold-guard.ts` (block hand-written task files via `write`/`apply_patch`) and `spec-state.ts` (tollgate inject via `chat.message`); installer self-test expects both plugins.
- **First hook behavioral test harness** (`src/claude/hooks/__tests__/`): runs each hook as a real subprocess (stdin payload → exit code / output) via `node --test`, wired into `run-skill-self-tests.mjs`. Covers the two hook-hardening fixes with regression tests proven to fail against the unpatched code.

### Changed
- **`spec-state.cjs` reminder slimmed**: state-change block is compact English only (no red ALL-CAPS / bilingual wall); one-line unchanged path kept. Hard completion enforcement is now the Stop gate.

### Removed
- `generate-graph` and `impact-analysis` skills (unused) + orphaned `scripts/browser-tool.cjs`; routing/manifest/OpenCode wrappers/self-tests cleaned; upgrades remove them from existing installs (PR #66).

### Fixed
- specs `--validate` dispatch off-by-one (Steps 1-7, not 1-8); ui-ux-designer agent dev-path → installed `.claude/skills/` venv path; Gemini model IDs unified on `gemma-4-31b-it` family (runtime.json is the single source); installer `--lang` unknown codes fall back to English; installer settings merge dedupes per command and survives malformed settings.json; `validate-docs.cjs` exits 1 on broken links; spec validator verifies every tagged contract block per task (PR #66).
- Document-skill attribution restored to Anthropic (`pdf`/`pptx`/`docx`/`xlsx` frontmatter author); `frontend-design` license line corrected to MIT, removing a dangling `LICENSE.txt` reference (PR #67).
- OpenCode `session.ts` compaction banner + `AGENTS.md` map Claude `AskUserQuestion`/`TodoWrite`/`Task` to OpenCode built-ins (`question`/`todowrite`/agent-subtask) instead of instructing unavailable tools.

### Fixed — Hook hardening
- **`session.cjs` env escaping**: `writeEnv` now escapes `\ $ \` "` (was `"` only), so an attacker-influenced value flowing into `CLAUDE_ENV_FILE` (e.g. a git branch named `` evil$HOME-x`pwd` ``) can no longer expand or execute when the env file is sourced. Matches the canonical `escapeShellValue` semantics in `lib/config.cjs`.
- **`privacy-block.cjs` symlink bypass**: the sensitive-file gate now resolves symlink targets via `fs.realpathSync` and checks both the requested name and its real target, closing the bypass where a harmless-looking symlink (e.g. `notes.txt` → `.env`) slipped past the basename check. Exemptions (`.env.example`) still win on either name; fail-open when the path cannot be resolved.

### Added
- **First hook behavioral test harness** (`src/claude/hooks/__tests__/`): runs each hook as a real subprocess (stdin payload → exit code / output) via `node --test`, wired into `run-skill-self-tests.mjs`. Covers the two fixes above with regression tests proven to fail against the unpatched code.

## [0.13.2] - 2026-06-21

### Added — Enforce scaffold on task creation
Expand Down
26 changes: 26 additions & 0 deletions packages/spec/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **`spec-gate.cjs` (Stop completion gate)**: blocks turn end when *newly-done* tasks lack a verification receipt in their task markdown (`Status: done`, Evidence section with real proof — no `{{...}}` placeholders — plus non-empty `task_registry[].completed_at`). Loop-safe via `stop_hook_active`; first run (no cache) seeds history without blocking so legacy specs adopt cleanly. Escape hatch: `"spec": { "completion_gate": false }` in `.claude/runtime.json` (missing key keeps the gate ON). Registered first on the existing `Stop` hooks list (before `state.cjs`). Follow-up idea: a dedicated `TaskCompleted` event is not used here — its output contract is unverified on the target Claude Code version.
- **OpenCode `task-scaffold-guard.ts` plugin**: ports Claude `task-scaffold-guard.cjs` — hard-blocks `write` and `apply_patch` creating `specs/<feature>/tasks/task-*.md`, forcing generation via `spec-scaffold.cjs` then Edit-fill. Also gates `edit` on a **non-existent** task file: OpenCode's `edit` can create files (unlike Claude's Edit — smoke-verified on opencode 1.17.15), so edit-creation is blocked while stub-filling stays allowed. Safety valves: runtime escape hatch (functional but **not advertised in the block message** — a smoke test proved the model reads the advertised override and disables the guard itself), fail-open if the scaffold script is missing, actionable block message. Auto-discovered by the OpenCode installer.
- **OpenCode `spec-state.ts` plugin**: ports Claude `spec-state.cjs` tollgate via `chat.message` — injects state-sync reminder (fingerprint gate: one-line when unchanged, full URGENT block when phase/done-total changes). Injected parts are schema-complete (`id`/`sessionID`/`messageID`) — a bare `{type,text}` part crashes the whole user turn ("invalid user part before save"). Disable with `"spec": { "tollgate": false }` in `.opencode/runtime.json`. End-to-end verified on opencode 1.17.15 (tollgate text reaches the model).

### Removed
- **`generate-graph` and `impact-analysis` skills** (unused) plus the orphaned `scripts/browser-tool.cjs`. All routing rules, OpenCode command wrappers, manifest entries, and self-test expectations cleaned; "blast radius / side effects" intent now routes to `hapo:inspect`. Existing installs are cleaned automatically on upgrade via new `obsolete.runtimeFiles` entries.

### Fixed
- **Claude `task-scaffold-guard.cjs`**: block message no longer advertises the `runtime.json` override (same self-disarm vector proven in the OpenCode smoke test — the model flips the flag instead of scaffolding). The escape hatch remains functional for humans.
- **OpenCode compaction banner / AGENTS.md**: no longer instruct the unavailable Claude `AskUserQuestion` tool; map to OpenCode built-in `question` (and `TodoWrite` → `todowrite`, `Task` → agent/subtask flow).
- **specs SKILL**: `--validate` dispatch said "MUST NOT execute Steps 1-8" while jumping to Step 8 — corrected to 1-7.
- **ui-ux-designer agent**: `search.py` invocations used the dev-monorepo path; now use the installed `.claude/skills/` path via the skills venv.
- **Model IDs unified on the `gemma-4-31b-it` family** across ai-multimodal, frontend-design, and inspect (SKILL bodies, references, `.env.example`, script defaults). `runtime.json → gemini.model` is the single source of truth; previously three sources disagreed.
- **Installer i18n**: unsupported `--lang` codes now fall back to English (previously Japanese).
- **Installer settings merge**: managed hooks merge per command keyed by matcher entry — a command added to an existing matcher is appended on upgrade (the old dedupe judged the whole entry duplicate by its first command). Malformed user `settings.json` no longer aborts the install; the merge is skipped with a warning.
- **`validate-docs.cjs`** exits 1 on broken relative links (previously always exit 0).
- **`validate-spec-output.cjs`** verifies every `<!-- contract:NAME -->` tagged block in a task, not just the first fenced block. Multi-contract tasks must tag each copy (untagged → error); tagged-but-undeclared blocks warn; single-contract legacy format unchanged. Covered by a new self-test fixture (138 tests total).
- **Document skills attribution**: `pdf`/`pptx`/`docx`/`xlsx` frontmatter `metadata.author` now credits `Anthropic, PBC — adapted by Haposoft` (the bundled `LICENSE.txt` files are Anthropic's; the field previously claimed haposoft).
- **OpenCode session plugin**: the compact-recovery banner instructed the Claude-only `AskUserQuestion` tool, which `AGENTS.md` declares unavailable in OpenCode — it now tells the agent to ask the user directly in chat.
- **frontend-design skill**: frontmatter pointed at a non-existent `LICENSE.txt`; corrected to `license: MIT` (no Anthropic-derived content found in the skill folder).

### Changed
- **`spec-state.cjs` tollgate reminder slimmed**: state-change path is now a compact English block (≤7 lines: feature, phase, task counts, next unblocked task, sync/validate rule, Stop-gate note). Removed the red ALL-CAPS / bilingual MANDATORY wall (`URGENT`, `BẮT BUỘC`, `CẤM`). One-line unchanged-fingerprint path is unchanged. Completion enforcement moved to `spec-gate.cjs` on Stop.
- Installer obsolete mechanism removes directories recursively and prunes ownership-manifest entries by prefix (`tracker.prunePrefix`), enabling skill-level cleanup on upgrade.

## [0.13.2] - 2026-06-21

### Added — Enforce scaffold on task creation
Expand Down
3 changes: 1 addition & 2 deletions packages/spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,8 @@ CafeKit ships many skills, but the main release surface is:
- `/hapo:docs [--init|--update|--summarize|--reconstruct]`: create project docs or reconstruct as-is system documentation from source code
- `/hapo:test [scope|--full]`: run verification and return a structured verdict
- `/hapo:code-review [scope|--pending]`: adversarial review focused on correctness, regressions, and security
- `/hapo:generate-graph <diagram request>`: generate technical SVG/PNG diagrams

Common companion skills bundled in this package include `inspect`, `impact-analysis`, `research`, `ai-multimodal`, `frontend-development`, `backend-development`, and `react-best-practices`.
Common companion skills bundled in this package include `inspect`, `research`, `ai-multimodal`, `frontend-development`, `backend-development`, and `react-best-practices`.

CafeKit uses rule-based skill routing guidance instead of an automatic prompt-scoring hook. See `.claude/rules/skill-workflow-routing.md`, `.claude/rules/skill-domain-routing.md`, or run:

Expand Down
34 changes: 23 additions & 11 deletions packages/spec/docs/opencode-hook-port-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ Per-hook decision matrix with exact behavior translation, dependencies on shared
| `state.cjs` | SessionStart, PostToolUse, Stop, SubagentStop | `event` + `tool.execute.after` | **PORT (partial)** — drop SubagentStop branch; Stop ≈ session.idle | T3.1 |
| `docs-sync.cjs` | SessionStart | `event` (session.created) | **PORT (full)** | T3.2 |
| `session.cjs` | SessionStart(startup,resume,clear,compact) | `event` (session.created + session.compacted) | **PORT (partial)** — banner only, no `CLAUDE_ENV_FILE` writes (OpenCode has no equivalent) | T3.3 |
| `spec-state.cjs` | UserPromptSubmit | none | **DROP** — fold into AGENTS.md + specs skill |
| `spec-state.cjs` | UserPromptSubmit | `chat.message` (mutate `output.parts`) | **PORTED** — inject tollgate text part before model turn |
| `task-scaffold-guard.cjs` | PreToolUse(Write) | `tool.execute.before` (`write` + `apply_patch`) | **PORTED** — block task-file creation; three safety valves |
| `rules.cjs` | UserPromptSubmit | none | **DROP** — fold into AGENTS.md + skill-workflow-routing.md |
| `agent.cjs` | SubagentStart | none | **DROP** — orchestrator already injects context in subagent prompts |
| `agent.cjs` | SubagentStart | none | **DROPPED** — OpenCode `session.created`+`parentID` subagent surface is unreliable; orchestrator already mandates self-contained subagent prompts (low ROI) |
| `usage.cjs` | UserPromptSubmit + PostToolUse | `tool.execute.after` (only) | **PORT (partial)** — drop prompt half; keep file-edit counter |

## Detailed Per-Hook Translation
Expand Down Expand Up @@ -88,23 +89,34 @@ Per-hook decision matrix with exact behavior translation, dependencies on shared
- Compact warning: trigger via `session.compacted` event branch.
- `source` field: OpenCode events do not carry source discriminator → all session.created becomes generic "Session started.".

### 6. spec-state.cjs → **DROP**
### 6. spec-state.cjs → `.opencode/plugins/spec-state.ts` (**PORTED**)

**Reason:** Relies on UserPromptSubmit to inject reminders before assistant processes user message. OpenCode has no preempt-style user-prompt hook. The closest `chat.message` fires after submission with no way to prepend system context.
**Rationale (2026-07 re-investigation):** OpenCode `@opencode-ai/plugin` `chat.message` fires **before** the model processes the user message. Mutating `output.parts` (push a text part) injects tollgate context into the turn — the OpenCode equivalent of Claude `UserPromptSubmit` stdout injection.

**Workaround:** Add explicit reminder block to:
- `AGENTS.md` under "Spec workflow guardrails" section
- `hapo:specs` skill front matter
**OpenCode mapping:**
- Hook: `chat.message(input, output)` → push a **schema-complete** part onto `output.parts`: `{ id, sessionID: output.message.sessionID, messageID: output.message.id, type: "text", text, synthetic: true }`. A bare `{ type, text }` part fails OpenCode's durable-part schema and **crashes the entire user turn** ("invalid user part before save" — verified on 1.17.15).
- Scan `<project>/<runtime.paths.specs || "specs">/*/spec.json` for first `status === "in_progress"` (also accept legacy `"in-progress"`)
- State-change gate: fingerprint `phase|done/total` in `plugins/.logs/tollgate-last.txt`; unchanged → one-line reminder; changed → full URGENT block (Vietnamese text preserved) + refresh fingerprint
- Escape hatch: `.opencode/runtime.json` `{ "spec": { "tollgate": false } }` (default ON)
- Fail-open crash wrapper → `plugins/.logs/hook-log.jsonl`; never throw

### 6b. task-scaffold-guard.cjs → `.opencode/plugins/task-scaffold-guard.ts` (**PORTED**)

Document in CHANGELOG/install README that OpenCode users get the spec-drift reminder via AGENTS.md prose only.
**OpenCode mapping:**
- Hook: `tool.execute.before`
- Gate tools: `write` (`output.args.filePath`), `apply_patch` (scan `output.args.patchText` for `*** Add File: <path>` lines — absent from the 1.17.15 default toolset, kept as a defensive branch), and `edit` on a **non-existent** task file — OpenCode's `edit` CAN create files (unlike Claude's Edit, which requires an existing file), verified on 1.17.15; edit on an existing stub stays allowed (legitimate stub filling). The existence check resolves the project-relative `specs/...` suffix against the plugin `directory` because tool paths may be sandbox-virtual (`/home/user/...`).
- Task-file regex: `/(^|\/)specs\/[^/]+\/tasks\/task-[^/]+\.md$/`
- Three safety valves preserved: runtime `spec.scaffold_guard === false` escape (fail-closed on missing runtime); fail-open if `.opencode/scripts/spec-scaffold.cjs` absent; actionable block message with exact scaffold command
- **Escape hatch is NOT advertised in the block message.** Smoke-tested: when the message included the `runtime.json` override line, the model wrote `{"spec":{"scaffold_guard":false}}` itself and disabled the guard (self-disarm). The hatch stays functional for humans and is documented here only. The Claude `.cjs` hook was hardened identically.
- Crash-wrapper fail-open: re-throw only intentional `TASK SCAFFOLD REQUIRED` errors

### 7. rules.cjs → **DROP**

**Reason:** Same UserPromptSubmit dependency. OpenCode users rely on `AGENTS.md` + `.opencode/rules/skill-workflow-routing.md` + `.opencode/rules/skill-domain-routing.md` — already installed.
**Reason:** Same UserPromptSubmit dependency historically. OpenCode users rely on `AGENTS.md` + `.opencode/rules/skill-workflow-routing.md` + `.opencode/rules/skill-domain-routing.md` — already installed. (Not re-ported; AGENTS fold-in remains sufficient.)

### 8. agent.cjs → **DROP**
### 8. agent.cjs → **DROPPED** (explicit, low ROI)

**Reason:** OpenCode has no `SubagentStart` event. CafeKit orchestrator rule (`orchestrator.md`) already mandates self-contained subagent prompts including work-context paths. No regression.
**Reason:** OpenCode's subagent event surface (`session.created` + `parentID`) is unreliable for seeding subagent context. CafeKit orchestrator rule (`orchestrator.md`) already mandates self-contained subagent prompts including work-context paths. Porting would add fragile coupling for little gain.

### 9. usage.cjs → `.opencode/plugins/usage.ts` (partial)

Expand Down
2 changes: 1 addition & 1 deletion packages/spec/docs/opencode-plugin-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export const MyPlugin: Plugin = async () => ({
|---|---|---|
| `PreToolUse` | `tool.execute.before` (throw to block, mutate args) | Tool names differ — see normalization below |
| `PostToolUse` | `tool.execute.after` | Has full `args` + result |
| `UserPromptSubmit` | **none** | Closest: `chat.message` mutate, but fires after submission; no preemptive gate |
| `UserPromptSubmit` | `chat.message` (mutate `output.parts`) | Fires before model processing (verified on 1.17.15); pushed parts MUST be schema-complete — `id`, `sessionID`, `messageID`, `type`, `text` — or the WHOLE turn fails with "invalid user part before save" (used by `spec-state.ts`) |
| `SessionStart` (startup/resume/clear) | `event` with `type === "session.created"` | No `source` discriminator; treat as single mode |
| `Stop` | `event` with `type === "session.idle"` | Fires on idle, not exactly Stop semantics |
| `SubagentStart` | **none** | No subagent lifecycle exposed |
Expand Down
6 changes: 5 additions & 1 deletion packages/spec/scripts/run-skill-self-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ function runCommand({ label, command, args, parseCount }) {
return new Promise((resolveRun) => {
const child = spawn(command, args, {
cwd: packageRoot,
env: process.env,
// Force colorless child output: the count parsers anchor on line starts,
// and ANSI prefixes (e.g. FORCE_COLOR envs) break them -> false NO_TESTS.
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
stdio: ["ignore", "pipe", "pipe"],
});

Expand Down Expand Up @@ -1062,6 +1064,8 @@ async function runOpenCodeInstallerFixtureTests() {
"state.ts",
"usage.ts",
"rules.ts",
"task-scaffold-guard.ts",
"spec-state.ts",
];
for (const plugin of expectedPlugins) {
if (!(await fileExists(join(root, ".opencode", "plugins", plugin)))) {
Expand Down
Loading