@haposoft/cafekit 0.16.0-rc.6 → 0.16.0-rc.7

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.
Files changed (86) hide show
  1. package/README.md +21 -58
  2. package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
  3. package/bin/__tests__/codex-hooks.test.js +137 -0
  4. package/bin/__tests__/codex-native.test.js +130 -74
  5. package/bin/__tests__/develop-contract.test.js +155 -76
  6. package/bin/__tests__/installer-safety.test.js +19 -154
  7. package/bin/__tests__/package-inventory.test.js +10 -20
  8. package/bin/__tests__/release-preflight.semantic-firewall.test.js +2 -2
  9. package/bin/__tests__/validator-grounding.test.js +11 -11
  10. package/bin/install.js +2 -7
  11. package/bin/lib/context.js +0 -45
  12. package/bin/lib/copy-utils.js +1 -1
  13. package/bin/lib/i18n.js +0 -3
  14. package/bin/lib/manifest.js +1 -1
  15. package/bin/phases/claude-runtime.js +1 -3
  16. package/bin/phases/codex-runtime.js +4 -2
  17. package/bin/phases/copy-payload.js +3 -15
  18. package/bin/phases/post-install.js +5 -17
  19. package/bin/phases/root-config.js +2 -3
  20. package/bin/phases/select-platform.js +2 -5
  21. package/bin/phases/summary.js +0 -1
  22. package/package.json +2 -5
  23. package/src/claude/CLAUDE.md +19 -6
  24. package/src/claude/agents/project-manager.md +41 -9
  25. package/src/claude/agents/spec-maker.md +76 -155
  26. package/src/claude/gitignore +1 -1
  27. package/src/claude/hooks/__tests__/spec-gate.test.js +162 -0
  28. package/src/claude/hooks/lib/context.cjs +1 -1
  29. package/src/claude/hooks/lib/detect.cjs +1 -1
  30. package/src/claude/hooks/spec-gate.cjs +44 -9
  31. package/src/claude/hooks/spec-state.cjs +22 -11
  32. package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
  33. package/src/claude/migration-manifest.json +17 -3
  34. package/src/claude/rules/manage-docs.md +43 -24
  35. package/src/claude/rules/state-sync.md +57 -62
  36. package/src/claude/rules/workflow.md +30 -13
  37. package/src/claude/scripts/change-firewall.cjs +2 -2
  38. package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
  39. package/src/claude/scripts/spec-receipt.cjs +141 -1
  40. package/src/claude/scripts/spec-resolver.cjs +288 -0
  41. package/src/claude/skills/develop/SKILL.md +89 -125
  42. package/src/claude/skills/develop/references/parallel-waves.md +75 -162
  43. package/src/claude/skills/develop/references/quality-gate.md +61 -121
  44. package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
  45. package/src/claude/skills/question/SKILL.md +1 -1
  46. package/src/claude/skills/specs/SKILL.md +142 -204
  47. package/src/claude/skills/specs/references/review.md +109 -243
  48. package/src/claude/skills/specs/references/templates.md +173 -0
  49. package/src/claude/skills/sync/SKILL.md +66 -90
  50. package/src/claude/skills/sync/references/sync-protocols.md +62 -83
  51. package/src/codex/AGENTS.md +20 -7
  52. package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
  53. package/src/codex/hooks/lib/spec-utils.cjs +25 -0
  54. package/src/codex/hooks/spec-gate.cjs +46 -16
  55. package/src/codex/hooks/spec-state.cjs +22 -12
  56. package/src/codex/rules/state-sync.md +57 -61
  57. package/src/common/AGENTS.md +8 -1
  58. package/bin/__tests__/opencode-spec-gate.test.js +0 -799
  59. package/bin/lib/opencode-install.js +0 -899
  60. package/bin/phases/opencode-runtime.js +0 -38
  61. package/src/claude/skills/specs/references/archive-workflow.md +0 -74
  62. package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
  63. package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
  64. package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
  65. package/src/claude/skills/specs/references/research-strategy.md +0 -107
  66. package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
  67. package/src/claude/skills/specs/references/translation-mirror.md +0 -83
  68. package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
  69. package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
  70. package/src/claude/skills/specs/rules/design-principles.md +0 -138
  71. package/src/claude/skills/specs/rules/design-review.md +0 -55
  72. package/src/claude/skills/specs/rules/ears-format.md +0 -42
  73. package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
  74. package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
  75. package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
  76. package/src/opencode/AGENTS.md +0 -41
  77. package/src/opencode/plugins/docs-sync.ts +0 -175
  78. package/src/opencode/plugins/inspect-block.ts +0 -164
  79. package/src/opencode/plugins/privacy-block.ts +0 -182
  80. package/src/opencode/plugins/rules.ts +0 -155
  81. package/src/opencode/plugins/session.ts +0 -179
  82. package/src/opencode/plugins/spec-gate.ts +0 -457
  83. package/src/opencode/plugins/spec-state.ts +0 -311
  84. package/src/opencode/plugins/state.ts +0 -253
  85. package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
  86. package/src/opencode/plugins/usage.ts +0 -178
@@ -1,161 +0,0 @@
1
- /**
2
- * Copyright (c) 2026 Haposoft. MIT License.
3
- *
4
- * OpenCode Plugin — task-scaffold-guard.ts
5
- *
6
- * Port of .claude/hooks/task-scaffold-guard.cjs to OpenCode plugin protocol.
7
- * Also covers the OpenCode-only `apply_patch` file-creation vector
8
- * (`*** Add File: <path>` in `output.args.patchText`).
9
- *
10
- * Behavior:
11
- * - Block `write` and `apply_patch` that create `specs/<feature>/tasks/task-*.md`.
12
- * - Task files must be generated via `spec-scaffold.cjs`, then Edit-filled.
13
- *
14
- * Safety valves:
15
- * 1. Escape hatch: `.opencode/runtime.json` `{ "spec": { "scaffold_guard": false } }`
16
- * (fail-closed: missing/broken runtime keeps the guard ON).
17
- * 2. Fail-open if `.opencode/scripts/spec-scaffold.cjs` is absent.
18
- * 3. Actionable block message with the exact scaffold command.
19
- */
20
-
21
- import type { Plugin } from "@opencode-ai/plugin";
22
- import { existsSync, readFileSync, mkdirSync, appendFileSync } from "node:fs";
23
- import { join, dirname } from "node:path";
24
- import { fileURLToPath } from "node:url";
25
-
26
- const TASK_FILE_RE = /(^|\/)specs\/[^/]+\/tasks\/task-[^/]+\.md$/;
27
- const TASK_FILE_REL_RE = /(?:^|\/)(specs\/[^/]+\/tasks\/task-[^/]+\.md)$/;
28
- const FEATURE_RE = /(^|\/)specs\/([^/]+)\/tasks\//;
29
- const ADD_FILE_RE = /^\*\*\* Add File:\s*(.+?)\s*$/gm;
30
-
31
- const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url));
32
-
33
- function readRuntime(cwd: string): Record<string, unknown> {
34
- try {
35
- const file = join(cwd, ".opencode", "runtime.json");
36
- return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
37
- } catch {
38
- return {};
39
- }
40
- }
41
-
42
- function isTaskFile(filePath: string): boolean {
43
- const norm = filePath.replace(/\\/g, "/");
44
- return TASK_FILE_RE.test(norm);
45
- }
46
-
47
- function extractFeature(filePath: string): string {
48
- const norm = filePath.replace(/\\/g, "/");
49
- const m = norm.match(FEATURE_RE);
50
- return m ? m[2] : "<feature>";
51
- }
52
-
53
- function pathsFromApplyPatch(patchText: string): string[] {
54
- const paths: string[] = [];
55
- let match: RegExpExecArray | null;
56
- const re = new RegExp(ADD_FILE_RE.source, ADD_FILE_RE.flags);
57
- while ((match = re.exec(patchText)) !== null) {
58
- const p = match[1]?.trim();
59
- if (p) paths.push(p);
60
- }
61
- return paths;
62
- }
63
-
64
- function collectTargetPaths(tool: string, args: Record<string, unknown>): string[] {
65
- if (tool === "write") {
66
- const filePath = typeof args.filePath === "string" ? args.filePath.trim() : "";
67
- return filePath ? [filePath] : [];
68
- }
69
- if (tool === "apply_patch") {
70
- const patchText = typeof args.patchText === "string" ? args.patchText : "";
71
- return patchText ? pathsFromApplyPatch(patchText) : [];
72
- }
73
- return [];
74
- }
75
-
76
- /**
77
- * Resolve a (possibly sandbox-virtual) tool path to the real on-disk task file.
78
- * OpenCode may present paths like /home/user/specs/... to the model; the
79
- * project-relative `specs/...` suffix joined onto the plugin `directory` is
80
- * the reliable location for existence checks.
81
- */
82
- function resolveTaskFile(directory: string, filePath: string): string {
83
- const norm = filePath.replace(/\\/g, "/");
84
- const m = norm.match(TASK_FILE_REL_RE);
85
- return m ? join(directory, m[1]) : filePath;
86
- }
87
-
88
- function formatBlockMessage(filePath: string): string {
89
- const feature = extractFeature(filePath);
90
- // NOTE: deliberately does NOT mention the runtime.json escape hatch.
91
- // Smoke-tested on opencode 1.17.15: when the block message advertised the
92
- // override, the model simply wrote runtime.json and disabled the guard.
93
- // The hatch stays functional for humans; it is documented in the docs only.
94
- return [
95
- "TASK SCAFFOLD REQUIRED: task files must be generated, not hand-written.",
96
- `Blocked Write: ${filePath}`,
97
- "",
98
- "Generate the stub(s), then Edit-fill the {{...}} placeholders:",
99
- ` node .opencode/scripts/spec-scaffold.cjs ${feature} --tasks "R0-01-slug,R1-01-slug,..." --tasks-only`,
100
- "Then use Edit (not Write) on each tasks/task-*.md stub.",
101
- ].join("\n");
102
- }
103
-
104
- function logCrash(error: unknown): void {
105
- try {
106
- const logDir = join(PLUGIN_DIR, ".logs");
107
- if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
108
- appendFileSync(
109
- join(logDir, "hook-log.jsonl"),
110
- JSON.stringify({
111
- ts: new Date().toISOString(),
112
- hook: "task-scaffold-guard",
113
- status: "crash",
114
- error: error instanceof Error ? error.message : String(error),
115
- }) + "\n",
116
- );
117
- } catch {
118
- // fail-open
119
- }
120
- }
121
-
122
- export const TaskScaffoldGuard: Plugin = async ({ directory }) => ({
123
- "tool.execute.before": async (input, output) => {
124
- try {
125
- if (input.tool !== "write" && input.tool !== "apply_patch" && input.tool !== "edit") return;
126
-
127
- const runtime = readRuntime(directory);
128
- const spec = runtime.spec as Record<string, unknown> | undefined;
129
- if (spec?.scaffold_guard === false) return;
130
-
131
- const scaffold = join(directory, ".opencode", "scripts", "spec-scaffold.cjs");
132
- if (!existsSync(scaffold)) return;
133
-
134
- const args = (output.args ?? {}) as Record<string, unknown>;
135
-
136
- // OpenCode's `edit` CAN create new files (unlike Claude's Edit, which
137
- // requires an existing file — the original guard's design premise).
138
- // Verified on opencode 1.17.15. Gate: edit on a NON-existent task file
139
- // is creation (block); edit on an existing stub is legitimate filling.
140
- if (input.tool === "edit") {
141
- const filePath = typeof args.filePath === "string" ? args.filePath.trim() : "";
142
- if (filePath && isTaskFile(filePath) && !existsSync(resolveTaskFile(directory, filePath))) {
143
- throw new Error(formatBlockMessage(filePath));
144
- }
145
- return;
146
- }
147
-
148
- const paths = collectTargetPaths(input.tool, args);
149
- for (const filePath of paths) {
150
- if (!isTaskFile(filePath)) continue;
151
- throw new Error(formatBlockMessage(filePath));
152
- }
153
- } catch (error) {
154
- // Re-throw intentional scaffold blocks; swallow programming errors.
155
- if (error instanceof Error && error.message.includes("TASK SCAFFOLD REQUIRED")) {
156
- throw error;
157
- }
158
- logCrash(error);
159
- }
160
- },
161
- });
@@ -1,178 +0,0 @@
1
- /**
2
- * Copyright (c) 2026 Haposoft. MIT License.
3
- *
4
- * OpenCode Plugin — usage.ts
5
- *
6
- * Port of .claude/hooks/usage.cjs adapted to OpenCode's plugin surface.
7
- *
8
- * Claude's version polls the Anthropic OAuth API for plan usage so the
9
- * statusline can render quota. OpenCode has no statusline injection point
10
- * and no OAuth plumbing, so this port instead tracks per-session token
11
- * usage by inspecting `message.updated` payloads and writing the rolling
12
- * totals to `.opencode/session-state/usage.json` for downstream tooling.
13
- *
14
- * Disable via .opencode/runtime.json: { "usage": { "enabled": false } }.
15
- */
16
-
17
- import type { Plugin } from "@opencode-ai/plugin";
18
- import {
19
- existsSync,
20
- readFileSync,
21
- writeFileSync,
22
- mkdirSync,
23
- } from "node:fs";
24
- import { join, dirname } from "node:path";
25
- import { fileURLToPath } from "node:url";
26
-
27
- const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url));
28
-
29
- interface UsageTotals {
30
- inputTokens: number;
31
- outputTokens: number;
32
- cacheReadTokens: number;
33
- cacheWriteTokens: number;
34
- messages: number;
35
- lastUpdated: string;
36
- }
37
-
38
- interface UsageFile {
39
- sessionId: string;
40
- startedAt: string;
41
- totals: UsageTotals;
42
- }
43
-
44
- function resolveProjectDir(directory?: string): string {
45
- if (directory && existsSync(directory)) return directory;
46
- // Plugin installed under <project>/.opencode/plugins/, so go up twice.
47
- return dirname(dirname(PLUGIN_DIR));
48
- }
49
-
50
- function readRuntime(cwd: string): Record<string, any> {
51
- try {
52
- const file = join(cwd, ".opencode", "runtime.json");
53
- if (existsSync(file)) {
54
- return JSON.parse(readFileSync(file, "utf8"));
55
- }
56
- } catch {
57
- /* fail-open */
58
- }
59
- return {};
60
- }
61
-
62
- function isEnabled(cwd: string): boolean {
63
- const runtime = readRuntime(cwd);
64
- return runtime?.usage?.enabled !== false;
65
- }
66
-
67
- function usagePath(cwd: string): string {
68
- return join(cwd, ".opencode", "session-state", "usage.json");
69
- }
70
-
71
- function readUsage(file: string): UsageFile | null {
72
- try {
73
- if (!existsSync(file)) return null;
74
- return JSON.parse(readFileSync(file, "utf8"));
75
- } catch {
76
- return null;
77
- }
78
- }
79
-
80
- function writeUsage(file: string, payload: UsageFile): void {
81
- try {
82
- mkdirSync(dirname(file), { recursive: true });
83
- writeFileSync(file, JSON.stringify(payload, null, 2));
84
- } catch {
85
- /* fail-open */
86
- }
87
- }
88
-
89
- // Defensive extractor: OpenCode message payloads may expose usage either
90
- // directly on the message or nested under metadata/usage. Accept both.
91
- function extractUsage(message: any): {
92
- input: number;
93
- output: number;
94
- cacheRead: number;
95
- cacheWrite: number;
96
- } | null {
97
- const candidates = [
98
- message?.usage,
99
- message?.metadata?.usage,
100
- message?.tokens,
101
- message?.metadata?.tokens,
102
- ].filter(Boolean);
103
-
104
- for (const c of candidates) {
105
- const input =
106
- Number(c.input_tokens ?? c.inputTokens ?? c.prompt_tokens ?? 0) || 0;
107
- const output =
108
- Number(c.output_tokens ?? c.outputTokens ?? c.completion_tokens ?? 0) ||
109
- 0;
110
- const cacheRead =
111
- Number(
112
- c.cache_read_input_tokens ??
113
- c.cacheReadInputTokens ??
114
- c.cache_read_tokens ??
115
- 0,
116
- ) || 0;
117
- const cacheWrite =
118
- Number(
119
- c.cache_creation_input_tokens ??
120
- c.cacheCreationInputTokens ??
121
- c.cache_write_tokens ??
122
- 0,
123
- ) || 0;
124
- if (input || output || cacheRead || cacheWrite) {
125
- return { input, output, cacheRead, cacheWrite };
126
- }
127
- }
128
- return null;
129
- }
130
-
131
- export const UsagePlugin: Plugin = async ({ directory }) => {
132
- const cwd = resolveProjectDir(directory);
133
-
134
- return {
135
- event: async ({ event }) => {
136
- if (event.type !== "message.updated") return;
137
- if (!isEnabled(cwd)) return;
138
-
139
- const message = (event as any).properties?.info ?? (event as any).properties?.message;
140
- if (!message) return;
141
-
142
- const tokens = extractUsage(message);
143
- if (!tokens) return;
144
-
145
- const sessionId = message.sessionID ?? message.sessionId ?? "default";
146
- const file = usagePath(cwd);
147
- const now = new Date().toISOString();
148
- const existing = readUsage(file);
149
-
150
- const sameSession = existing && existing.sessionId === sessionId;
151
- const totals: UsageTotals = sameSession
152
- ? existing!.totals
153
- : {
154
- inputTokens: 0,
155
- outputTokens: 0,
156
- cacheReadTokens: 0,
157
- cacheWriteTokens: 0,
158
- messages: 0,
159
- lastUpdated: now,
160
- };
161
-
162
- totals.inputTokens += tokens.input;
163
- totals.outputTokens += tokens.output;
164
- totals.cacheReadTokens += tokens.cacheRead;
165
- totals.cacheWriteTokens += tokens.cacheWrite;
166
- totals.messages += 1;
167
- totals.lastUpdated = now;
168
-
169
- writeUsage(file, {
170
- sessionId,
171
- startedAt: sameSession ? existing!.startedAt : now,
172
- totals,
173
- });
174
- },
175
- };
176
- };
177
-
178
- export default UsagePlugin;