@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.
- package/README.md +21 -58
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
- package/bin/__tests__/codex-hooks.test.js +382 -0
- package/bin/__tests__/codex-native.test.js +579 -80
- package/bin/__tests__/develop-contract.test.js +155 -76
- package/bin/__tests__/installer-safety.test.js +19 -154
- package/bin/__tests__/package-inventory.test.js +494 -26
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +2 -2
- package/bin/__tests__/validator-grounding.test.js +11 -11
- package/bin/install.js +2 -7
- package/bin/lib/context.js +0 -45
- package/bin/lib/copy-utils.js +1 -1
- package/bin/lib/i18n.js +0 -3
- package/bin/lib/manifest.js +1 -1
- package/bin/phases/claude-runtime.js +1 -3
- package/bin/phases/codex-runtime.js +4 -2
- package/bin/phases/copy-payload.js +3 -15
- package/bin/phases/post-install.js +5 -17
- package/bin/phases/root-config.js +2 -3
- package/bin/phases/select-platform.js +2 -5
- package/bin/phases/summary.js +0 -1
- package/package.json +2 -5
- package/src/claude/CLAUDE.md +19 -6
- package/src/claude/agents/project-manager.md +41 -9
- package/src/claude/agents/spec-maker.md +82 -155
- package/src/claude/gitignore +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +238 -0
- package/src/claude/hooks/__tests__/state.test.js +249 -3
- package/src/claude/hooks/lib/context.cjs +1 -1
- package/src/claude/hooks/lib/detect.cjs +1 -1
- package/src/claude/hooks/spec-gate.cjs +44 -9
- package/src/claude/hooks/spec-state.cjs +51 -13
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/migration-manifest.json +17 -3
- package/src/claude/rules/manage-docs.md +43 -24
- package/src/claude/rules/state-sync.md +57 -62
- package/src/claude/rules/workflow.md +30 -13
- package/src/claude/scripts/change-firewall.cjs +2 -2
- package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
- package/src/claude/scripts/spec-receipt.cjs +196 -1
- package/src/claude/scripts/spec-resolver.cjs +428 -0
- package/src/claude/skills/develop/SKILL.md +89 -125
- package/src/claude/skills/develop/references/parallel-waves.md +75 -162
- package/src/claude/skills/develop/references/quality-gate.md +61 -121
- package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
- package/src/claude/skills/question/SKILL.md +1 -1
- package/src/claude/skills/specs/SKILL.md +152 -204
- package/src/claude/skills/specs/references/review.md +124 -243
- package/src/claude/skills/specs/references/templates.md +202 -0
- package/src/claude/skills/sync/SKILL.md +66 -90
- package/src/claude/skills/sync/references/sync-protocols.md +62 -83
- package/src/codex/AGENTS.md +20 -7
- package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
- package/src/codex/hooks/lib/spec-utils.cjs +25 -0
- package/src/codex/hooks/spec-gate.cjs +46 -16
- package/src/codex/hooks/spec-state.cjs +54 -16
- package/src/codex/rules/state-sync.md +57 -61
- package/src/common/AGENTS.md +8 -1
- package/bin/__tests__/opencode-spec-gate.test.js +0 -799
- package/bin/lib/opencode-install.js +0 -899
- package/bin/phases/opencode-runtime.js +0 -38
- package/src/claude/skills/specs/references/archive-workflow.md +0 -74
- package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
- package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
- package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
- package/src/claude/skills/specs/references/research-strategy.md +0 -107
- package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
- package/src/claude/skills/specs/references/translation-mirror.md +0 -83
- package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
- package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
- package/src/claude/skills/specs/rules/design-principles.md +0 -138
- package/src/claude/skills/specs/rules/design-review.md +0 -55
- package/src/claude/skills/specs/rules/ears-format.md +0 -42
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
- package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
- package/src/opencode/AGENTS.md +0 -41
- package/src/opencode/plugins/docs-sync.ts +0 -175
- package/src/opencode/plugins/inspect-block.ts +0 -164
- package/src/opencode/plugins/privacy-block.ts +0 -182
- package/src/opencode/plugins/rules.ts +0 -155
- package/src/opencode/plugins/session.ts +0 -179
- package/src/opencode/plugins/spec-gate.ts +0 -457
- package/src/opencode/plugins/spec-state.ts +0 -311
- package/src/opencode/plugins/state.ts +0 -253
- package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
- 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;
|