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

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 (87) 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 +382 -0
  4. package/bin/__tests__/codex-native.test.js +579 -80
  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 +494 -26
  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 +82 -155
  26. package/src/claude/gitignore +1 -1
  27. package/src/claude/hooks/__tests__/spec-gate.test.js +238 -0
  28. package/src/claude/hooks/__tests__/state.test.js +249 -3
  29. package/src/claude/hooks/lib/context.cjs +1 -1
  30. package/src/claude/hooks/lib/detect.cjs +1 -1
  31. package/src/claude/hooks/spec-gate.cjs +44 -9
  32. package/src/claude/hooks/spec-state.cjs +51 -13
  33. package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
  34. package/src/claude/migration-manifest.json +17 -3
  35. package/src/claude/rules/manage-docs.md +43 -24
  36. package/src/claude/rules/state-sync.md +57 -62
  37. package/src/claude/rules/workflow.md +30 -13
  38. package/src/claude/scripts/change-firewall.cjs +2 -2
  39. package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
  40. package/src/claude/scripts/spec-receipt.cjs +196 -1
  41. package/src/claude/scripts/spec-resolver.cjs +428 -0
  42. package/src/claude/skills/develop/SKILL.md +89 -125
  43. package/src/claude/skills/develop/references/parallel-waves.md +75 -162
  44. package/src/claude/skills/develop/references/quality-gate.md +61 -121
  45. package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
  46. package/src/claude/skills/question/SKILL.md +1 -1
  47. package/src/claude/skills/specs/SKILL.md +152 -204
  48. package/src/claude/skills/specs/references/review.md +124 -243
  49. package/src/claude/skills/specs/references/templates.md +202 -0
  50. package/src/claude/skills/sync/SKILL.md +66 -90
  51. package/src/claude/skills/sync/references/sync-protocols.md +62 -83
  52. package/src/codex/AGENTS.md +20 -7
  53. package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
  54. package/src/codex/hooks/lib/spec-utils.cjs +25 -0
  55. package/src/codex/hooks/spec-gate.cjs +46 -16
  56. package/src/codex/hooks/spec-state.cjs +54 -16
  57. package/src/codex/rules/state-sync.md +57 -61
  58. package/src/common/AGENTS.md +8 -1
  59. package/bin/__tests__/opencode-spec-gate.test.js +0 -799
  60. package/bin/lib/opencode-install.js +0 -899
  61. package/bin/phases/opencode-runtime.js +0 -38
  62. package/src/claude/skills/specs/references/archive-workflow.md +0 -74
  63. package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
  64. package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
  65. package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
  66. package/src/claude/skills/specs/references/research-strategy.md +0 -107
  67. package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
  68. package/src/claude/skills/specs/references/translation-mirror.md +0 -83
  69. package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
  70. package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
  71. package/src/claude/skills/specs/rules/design-principles.md +0 -138
  72. package/src/claude/skills/specs/rules/design-review.md +0 -55
  73. package/src/claude/skills/specs/rules/ears-format.md +0 -42
  74. package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
  75. package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
  76. package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
  77. package/src/opencode/AGENTS.md +0 -41
  78. package/src/opencode/plugins/docs-sync.ts +0 -175
  79. package/src/opencode/plugins/inspect-block.ts +0 -164
  80. package/src/opencode/plugins/privacy-block.ts +0 -182
  81. package/src/opencode/plugins/rules.ts +0 -155
  82. package/src/opencode/plugins/session.ts +0 -179
  83. package/src/opencode/plugins/spec-gate.ts +0 -457
  84. package/src/opencode/plugins/spec-state.ts +0 -311
  85. package/src/opencode/plugins/state.ts +0 -253
  86. package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
  87. 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;