@haposoft/cafekit 0.13.4 → 0.14.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.
Files changed (114) hide show
  1. package/README.md +1 -2
  2. package/bin/lib/i18n.js +2 -2
  3. package/bin/lib/manifest.js +15 -0
  4. package/bin/lib/opencode-install.js +0 -14
  5. package/bin/phases/claude-runtime.js +4 -1
  6. package/bin/phases/claude-settings.js +38 -7
  7. package/package.json +1 -1
  8. package/src/claude/agents/ui-ux-designer.md +4 -4
  9. package/src/claude/hooks/__tests__/privacy-block.test.js +79 -0
  10. package/src/claude/hooks/__tests__/session-writeenv.test.js +68 -0
  11. package/src/claude/hooks/__tests__/spec-gate.test.js +270 -0
  12. package/src/claude/hooks/privacy-block.cjs +22 -2
  13. package/src/claude/hooks/session.cjs +5 -1
  14. package/src/claude/hooks/spec-gate.cjs +173 -0
  15. package/src/claude/hooks/spec-state.cjs +10 -17
  16. package/src/claude/hooks/task-scaffold-guard.cjs +4 -2
  17. package/src/claude/migration-manifest.json +5 -4
  18. package/src/claude/rules/skill-domain-routing.md +2 -3
  19. package/src/claude/rules/skill-workflow-routing.md +2 -3
  20. package/src/claude/rules/state-sync.md +4 -0
  21. package/src/claude/runtime.json +4 -0
  22. package/src/claude/scripts/validate-docs.cjs +4 -7
  23. package/src/claude/scripts/validate-spec-output.cjs +29 -9
  24. package/src/claude/settings/settings.json +4 -0
  25. package/src/claude/skills/ai-multimodal/.env.example +11 -13
  26. package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
  27. package/src/claude/skills/ai-multimodal/references/audio-processing.md +25 -27
  28. package/src/claude/skills/ai-multimodal/references/video-analysis.md +23 -27
  29. package/src/claude/skills/ai-multimodal/references/vision-understanding.md +26 -26
  30. package/src/claude/skills/ai-multimodal/scripts/check_setup.py +1 -1
  31. package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +7 -9
  32. package/src/claude/skills/brainstorm/SKILL.md +1 -2
  33. package/src/claude/skills/docx/SKILL.md +1 -1
  34. package/src/claude/skills/frontend-design/SKILL.md +1 -1
  35. package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +4 -4
  36. package/src/claude/skills/frontend-design/references/analysis-techniques.md +3 -3
  37. package/src/claude/skills/frontend-design/references/asset-generation.md +2 -2
  38. package/src/claude/skills/frontend-design/references/design-extraction-overview.md +4 -4
  39. package/src/claude/skills/frontend-design/references/extraction-best-practices.md +3 -3
  40. package/src/claude/skills/frontend-design/references/technical-accessibility.md +1 -1
  41. package/src/claude/skills/frontend-design/references/technical-optimization.md +1 -1
  42. package/src/claude/skills/frontend-design/references/technical-overview.md +2 -2
  43. package/src/claude/skills/frontend-design/references/technical-workflows.md +6 -6
  44. package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +3 -3
  45. package/src/claude/skills/inspect/SKILL.md +3 -3
  46. package/src/claude/skills/inspect/references/external-gemini-inspection.md +9 -9
  47. package/src/claude/skills/pdf/SKILL.md +1 -1
  48. package/src/claude/skills/pptx/SKILL.md +1 -1
  49. package/src/claude/skills/specs/SKILL.md +1 -1
  50. package/src/claude/skills/xlsx/SKILL.md +1 -1
  51. package/src/opencode/AGENTS.md +1 -2
  52. package/src/opencode/plugins/session.ts +2 -2
  53. package/src/opencode/plugins/spec-state.ts +244 -0
  54. package/src/opencode/plugins/task-scaffold-guard.ts +161 -0
  55. package/src/claude/scripts/browser-tool.cjs +0 -138
  56. package/src/claude/skills/generate-graph/LICENSE +0 -21
  57. package/src/claude/skills/generate-graph/README.md +0 -523
  58. package/src/claude/skills/generate-graph/SKILL.md +0 -429
  59. package/src/claude/skills/generate-graph/agentloop-core.svg +0 -101
  60. package/src/claude/skills/generate-graph/agents/openai.yaml +0 -4
  61. package/src/claude/skills/generate-graph/assets/samples/sample-style1-flat.png +0 -0
  62. package/src/claude/skills/generate-graph/assets/samples/sample-style2-dark.png +0 -0
  63. package/src/claude/skills/generate-graph/assets/samples/sample-style3-blueprint.png +0 -0
  64. package/src/claude/skills/generate-graph/assets/samples/sample-style4-notion.png +0 -0
  65. package/src/claude/skills/generate-graph/assets/samples/sample-style5-glass.png +0 -0
  66. package/src/claude/skills/generate-graph/assets/samples/sample-style6-claude.png +0 -0
  67. package/src/claude/skills/generate-graph/assets/samples/sample-style7-openai.png +0 -0
  68. package/src/claude/skills/generate-graph/fixtures/agent-memory-types-style4.json +0 -181
  69. package/src/claude/skills/generate-graph/fixtures/api-flow-style7.json +0 -40
  70. package/src/claude/skills/generate-graph/fixtures/mem0-style1.json +0 -297
  71. package/src/claude/skills/generate-graph/fixtures/microservices-style3.json +0 -64
  72. package/src/claude/skills/generate-graph/fixtures/multi-agent-style5.json +0 -45
  73. package/src/claude/skills/generate-graph/fixtures/system-architecture-style6.json +0 -48
  74. package/src/claude/skills/generate-graph/fixtures/tool-call-style2.json +0 -182
  75. package/src/claude/skills/generate-graph/package.json +0 -42
  76. package/src/claude/skills/generate-graph/references/icons.md +0 -281
  77. package/src/claude/skills/generate-graph/references/style-1-flat-icon.md +0 -108
  78. package/src/claude/skills/generate-graph/references/style-2-dark-terminal.md +0 -107
  79. package/src/claude/skills/generate-graph/references/style-3-blueprint.md +0 -113
  80. package/src/claude/skills/generate-graph/references/style-4-notion-clean.md +0 -94
  81. package/src/claude/skills/generate-graph/references/style-5-glassmorphism.md +0 -125
  82. package/src/claude/skills/generate-graph/references/style-6-claude-official.md +0 -209
  83. package/src/claude/skills/generate-graph/references/style-7-openai.md +0 -215
  84. package/src/claude/skills/generate-graph/references/style-diagram-matrix.md +0 -135
  85. package/src/claude/skills/generate-graph/references/svg-layout-best-practices.md +0 -100
  86. package/src/claude/skills/generate-graph/scripts/generate-diagram.sh +0 -157
  87. package/src/claude/skills/generate-graph/scripts/generate-from-template.py +0 -1556
  88. package/src/claude/skills/generate-graph/scripts/test-all-styles.sh +0 -135
  89. package/src/claude/skills/generate-graph/scripts/validate-svg.sh +0 -292
  90. package/src/claude/skills/generate-graph/templates/agent-architecture.svg +0 -28
  91. package/src/claude/skills/generate-graph/templates/architecture.svg +0 -23
  92. package/src/claude/skills/generate-graph/templates/comparison-matrix.svg +0 -14
  93. package/src/claude/skills/generate-graph/templates/data-flow.svg +0 -28
  94. package/src/claude/skills/generate-graph/templates/er-diagram.svg +0 -21
  95. package/src/claude/skills/generate-graph/templates/flowchart.svg +0 -21
  96. package/src/claude/skills/generate-graph/templates/sequence.svg +0 -20
  97. package/src/claude/skills/generate-graph/templates/state-machine.svg +0 -20
  98. package/src/claude/skills/generate-graph/templates/timeline.svg +0 -19
  99. package/src/claude/skills/generate-graph/templates/use-case.svg +0 -21
  100. package/src/claude/skills/impact-analysis/SKILL.md +0 -276
  101. package/src/claude/skills/impact-analysis/references/change-detection.md +0 -270
  102. package/src/claude/skills/impact-analysis/references/dependency-scouting.md +0 -338
  103. package/src/claude/skills/impact-analysis/references/edge-case-identification.md +0 -439
  104. package/src/claude/skills/impact-analysis/references/industry-techniques.md +0 -695
  105. package/src/claude/skills/impact-analysis/references/practical-techniques-guide.md +0 -753
  106. package/src/claude/skills/impact-analysis/references/project-detection.md +0 -704
  107. package/src/claude/skills/impact-analysis/references/react-native-customization.md +0 -508
  108. package/src/claude/skills/impact-analysis/references/report-template.md +0 -604
  109. package/src/claude/skills/impact-analysis/references/test-scenario-generation.md +0 -459
  110. package/src/claude/skills/impact-analysis/scripts/README.md +0 -476
  111. package/src/claude/skills/impact-analysis/scripts/ast-analyze.js +0 -403
  112. package/src/claude/skills/impact-analysis/scripts/calculate-risk.js +0 -475
  113. package/src/claude/skills/impact-analysis/scripts/find-dependencies.sh +0 -202
  114. package/src/claude/skills/impact-analysis/scripts/run-analysis.sh +0 -312
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Copyright (c) 2026 Haposoft. MIT License.
3
+ *
4
+ * OpenCode Plugin — spec-state.ts
5
+ *
6
+ * Port of .claude/hooks/spec-state.cjs to OpenCode plugin protocol.
7
+ *
8
+ * Behavior:
9
+ * - On chat.message (before the model processes the user turn), scan
10
+ * project specs dirs for the first in_progress / in-progress spec.json.
11
+ * - Inject a tollgate reminder into the turn by pushing a text part onto
12
+ * output.parts (OpenCode equivalent of Claude UserPromptSubmit stdout inject).
13
+ * - State-change gate: fingerprint phase|done/total in plugins/.logs/tollgate-last.txt;
14
+ * unchanged -> one-line reminder; changed -> full URGENT block + refresh fingerprint.
15
+ *
16
+ * Disable via `.opencode/runtime.json` { "spec": { "tollgate": false } }.
17
+ * Fail-open: never throw — a tollgate crash must not break the turn.
18
+ */
19
+
20
+ import type { Plugin } from "@opencode-ai/plugin";
21
+ import {
22
+ existsSync,
23
+ readFileSync,
24
+ writeFileSync,
25
+ mkdirSync,
26
+ appendFileSync,
27
+ readdirSync,
28
+ } from "node:fs";
29
+ import { join, dirname } from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+
32
+ const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url));
33
+ const CACHE_FILE = join(PLUGIN_DIR, ".logs", "tollgate-last.txt");
34
+
35
+ type SpecJson = {
36
+ status?: string;
37
+ current_phase?: string;
38
+ phase?: string;
39
+ task_registry?: Record<
40
+ string,
41
+ {
42
+ status?: string;
43
+ dependencies?: string[];
44
+ }
45
+ >;
46
+ };
47
+
48
+ function readRuntime(cwd: string): Record<string, unknown> {
49
+ try {
50
+ const file = join(cwd, ".opencode", "runtime.json");
51
+ return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
52
+ } catch {
53
+ return {};
54
+ }
55
+ }
56
+
57
+ function logCrash(error: unknown): void {
58
+ try {
59
+ const logDir = join(PLUGIN_DIR, ".logs");
60
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
61
+ appendFileSync(
62
+ join(logDir, "hook-log.jsonl"),
63
+ JSON.stringify({
64
+ ts: new Date().toISOString(),
65
+ hook: "spec-state",
66
+ status: "crash",
67
+ error: error instanceof Error ? error.message : String(error),
68
+ }) + "\n",
69
+ );
70
+ } catch {
71
+ // fail-open
72
+ }
73
+ }
74
+
75
+ function findActiveSpec(
76
+ specsPath: string,
77
+ ): { featureName: string; activeSpec: SpecJson } | null {
78
+ if (!existsSync(specsPath)) return null;
79
+
80
+ let entries;
81
+ try {
82
+ entries = readdirSync(specsPath, { withFileTypes: true });
83
+ } catch {
84
+ return null;
85
+ }
86
+
87
+ for (const entry of entries) {
88
+ if (!entry.isDirectory()) continue;
89
+ const specFile = join(specsPath, entry.name, "spec.json");
90
+ if (!existsSync(specFile)) continue;
91
+ try {
92
+ const specData = JSON.parse(readFileSync(specFile, "utf8")) as SpecJson;
93
+ if (specData.status === "in_progress" || specData.status === "in-progress") {
94
+ return { featureName: entry.name, activeSpec: specData };
95
+ }
96
+ } catch {
97
+ // skip bad JSON
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+
103
+ function buildTollgateText(
104
+ featureName: string,
105
+ activeSpec: SpecJson,
106
+ ): { stateKey: string; fullBlock: string; reminder: string } {
107
+ const phase = activeSpec.current_phase || activeSpec.phase || "unknown";
108
+ const taskRegistry = activeSpec.task_registry || {};
109
+ const taskEntries = Object.entries(taskRegistry);
110
+ const taskCounts = taskEntries.reduce<Record<string, number>>((acc, [, task]) => {
111
+ const status = task?.status || "pending";
112
+ acc[status] = (acc[status] || 0) + 1;
113
+ return acc;
114
+ }, {});
115
+ const taskStatusByPath = new Map(
116
+ taskEntries.map(([taskPath, task]) => [taskPath, task?.status || "pending"]),
117
+ );
118
+ const nextUnblocked = taskEntries.find(([, task]) => {
119
+ const status = task?.status || "pending";
120
+ const deps = Array.isArray(task?.dependencies) ? task.dependencies : [];
121
+ return status === "pending" && deps.every((dep) => taskStatusByPath.get(dep) === "done");
122
+ });
123
+
124
+ const done = taskCounts.done || 0;
125
+ const total = taskEntries.length;
126
+ const stateKey = `${phase}|${done}/${total}`;
127
+
128
+ const reminder =
129
+ `\n> 🔵 Spec \`${featureName}\` @ \`${phase}\` (${done}/${total} tasks done). ` +
130
+ "Tollgate active — sync `spec.json` when state changes.\n";
131
+
132
+ const lines: string[] = [
133
+ "",
134
+ "### 🔴 URGENT SYSTEM TOLLGATE (STATE SYNC) 🔴",
135
+ `- **Active Feature:** \`${featureName}\``,
136
+ `- **Current Phase:** \`${phase}\``,
137
+ ];
138
+ if (total > 0) {
139
+ lines.push(
140
+ `- **Task Registry:** \`${total} total | ${done} done | ${taskCounts.in_progress || 0} in_progress | ${taskCounts.blocked || 0} blocked | ${taskCounts.pending || 0} pending\``,
141
+ );
142
+ if (nextUnblocked) {
143
+ lines.push(`- **Next Unblocked Task:** \`${nextUnblocked[0]}\``);
144
+ }
145
+ }
146
+ lines.push("");
147
+ lines.push(
148
+ "> BẮT BUỘC (MANDATORY): Nếu bạn vừa hoàn thành một bước, bạn KHÔNG ĐƯỢC báo cáo \"Đã xong\" ngay.",
149
+ );
150
+ lines.push(
151
+ "> Bạn PHẢI sử dụng công cụ Edit để cập nhật trạng thái vật lý sau khi đã có bằng chứng verify thật (build/test/runtime/artifact), không phải chỉ vì code đã viết xong.",
152
+ );
153
+ lines.push(
154
+ "> 1. Sửa file `spec.json` (status, phase/current_phase, timestamps, `task_files`, `task_registry`, validation state nếu có thay đổi).",
155
+ );
156
+ lines.push(
157
+ "> 2. Chỉ khi verify xong mới sửa file `tasks/task-*.md` (status + tick '[x]' các sub-task và completion criteria liên quan).",
158
+ );
159
+ lines.push(
160
+ `> 3. Trước khi set \`ready_for_implementation = true\`, PHẢI chạy \`node .opencode/scripts/validate-spec-output.cjs specs/${featureName}\` và sửa mọi lỗi.`,
161
+ );
162
+ lines.push(
163
+ "> 4. NẾU VỪA HOÀN THÀNH 1 TASK CÓ SỬA SOURCE CODE, BẮT BUỘC cập nhật ngay tài liệu trong `docs/` (`system-architecture.md` hoặc Changelog) cho đồng bộ.",
164
+ );
165
+ lines.push(
166
+ "> CẤM VI PHẠM LUẬT TOLLGATE NÀY NHẰM ĐẢM BẢO TÍNH ĐỒNG BỘ CỦA HỆ THỐNG.",
167
+ );
168
+ lines.push("");
169
+
170
+ return { stateKey, fullBlock: lines.join("\n"), reminder };
171
+ }
172
+
173
+ type MessageRef = { id?: string; sessionID?: string };
174
+
175
+ function injectText(
176
+ output: { message?: MessageRef; parts?: unknown[] },
177
+ text: string,
178
+ ): void {
179
+ // OpenCode persists user parts and validates their schema: a part MUST carry
180
+ // id + sessionID + messageID or the whole turn fails with
181
+ // "invalid user part before save" (verified on opencode 1.17.15).
182
+ const message = output.message ?? {};
183
+ if (!Array.isArray(output.parts)) {
184
+ (output as { parts: unknown[] }).parts = [];
185
+ }
186
+ output.parts!.push({
187
+ id: `prt_ck${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`,
188
+ sessionID: message.sessionID ?? "",
189
+ messageID: message.id ?? "",
190
+ type: "text",
191
+ text,
192
+ synthetic: true,
193
+ });
194
+ }
195
+
196
+ export const SpecState: Plugin = async ({ directory }) => ({
197
+ "chat.message": async (_input, output) => {
198
+ try {
199
+ const runtime = readRuntime(directory);
200
+ const spec = runtime.spec as Record<string, unknown> | undefined;
201
+ if (spec?.tollgate === false) return;
202
+
203
+ const paths = runtime.paths as Record<string, unknown> | undefined;
204
+ const specsRel =
205
+ typeof paths?.specs === "string" && paths.specs.trim()
206
+ ? paths.specs.trim()
207
+ : "specs";
208
+ const specsPath = join(directory, specsRel);
209
+
210
+ const found = findActiveSpec(specsPath);
211
+ if (!found) return;
212
+
213
+ const { featureName, activeSpec } = found;
214
+ const { stateKey, fullBlock, reminder } = buildTollgateText(
215
+ featureName,
216
+ activeSpec,
217
+ );
218
+
219
+ let lastKey = "";
220
+ try {
221
+ lastKey = readFileSync(CACHE_FILE, "utf8").trim();
222
+ } catch {
223
+ // first run
224
+ }
225
+
226
+ if (lastKey === stateKey) {
227
+ injectText(output as { message?: MessageRef; parts?: unknown[] }, reminder);
228
+ return;
229
+ }
230
+
231
+ try {
232
+ mkdirSync(dirname(CACHE_FILE), { recursive: true });
233
+ writeFileSync(CACHE_FILE, stateKey);
234
+ } catch {
235
+ // fail-open: still inject full block
236
+ }
237
+
238
+ injectText(output as { message?: MessageRef; parts?: unknown[] }, fullBlock);
239
+ } catch (error) {
240
+ logCrash(error);
241
+ // never throw — tollgate must not break the turn
242
+ }
243
+ },
244
+ });
@@ -0,0 +1,161 @@
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,138 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Hapo Native Browser Multi-Tool (Puppeteer Bundler)
4
- * Gồm: screenshot, aria-snapshot, và console logs. Gọn gàng nén trong 1 script.
5
- *
6
- * Usage:
7
- * node browser-tool.cjs --action screenshot --url http://localhost:3000 --output ./shot.png
8
- * node browser-tool.cjs --action console --url http://localhost:3000
9
- * node browser-tool.cjs --action aria --url http://localhost:3000
10
- */
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
-
15
- let puppeteer;
16
- try {
17
- puppeteer = require('puppeteer-core');
18
- } catch (e) {
19
- try {
20
- puppeteer = require('puppeteer');
21
- } catch (err) {
22
- console.error('\n[LỖI] Hapo Browser Tool yêu cầu gói thư viện Puppeteer!');
23
- console.error('Do tôn trọng dung lượng nhẹ (Clean-room), Hapo không tự nhồi nhét Puppeteer vào dự án.');
24
- console.error('-> HÃY BẢO LLM (hoặc Dev) CHẠY LỆNH: `npm i -D puppeteer` TRONG PROJECT TRƯỚC KHI DÙNG CÔNG CỤ NÀY!\n');
25
- process.exit(1);
26
- }
27
- }
28
-
29
- // Hàm phân tích Argument đơn giản
30
- function parseArgs() {
31
- const args = process.argv.slice(2);
32
- const config = { action: 'screenshot', url: '', output: './hapo-debug.png' };
33
- for (let i = 0; i < args.length; i++) {
34
- if (args[i] === '--action') config.action = args[++i];
35
- if (args[i] === '--url') config.url = args[++i];
36
- if (args[i] === '--output') config.output = args[++i];
37
- }
38
- return config;
39
- }
40
-
41
- async function getBrowser() {
42
- // Ưu tiên nỗ lực dùng Chromium của máy nếu xài puppeteer-core (Edge / Chrome)
43
- let executablePath = process.env.CHROME_BIN || process.env.PUPPETEER_EXECUTABLE_PATH;
44
-
45
- if (!executablePath && puppeteer.executablePath) {
46
- try {
47
- executablePath = puppeteer.executablePath();
48
- } catch(e) {}
49
- }
50
-
51
- // Fallback map cơ bản cho Mac/Linux
52
- if (!executablePath) {
53
- if (process.platform === 'darwin') executablePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
54
- else if (process.platform === 'linux') executablePath = '/usr/bin/google-chrome';
55
- else if (process.platform === 'win32') executablePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
56
- }
57
-
58
- console.log(`[Hapo Browser] Khởi tạo trình duyệt tại: ${executablePath}`);
59
-
60
- return puppeteer.launch({
61
- executablePath: executablePath || undefined,
62
- headless: "new",
63
- args: ['--no-sandbox', '--disable-setuid-sandbox']
64
- });
65
- }
66
-
67
- async function takeScreenshot(page, output) {
68
- console.log(`[Hapo Browser] Đang chụp hình dồn vào file: ${output}`);
69
- await fs.promises.mkdir(path.dirname(path.resolve(output)), { recursive: true });
70
- await page.screenshot({ path: output, fullPage: true });
71
- console.log(`✅ [THÀNH CÔNG] Ảnh chụp full-page đã được nướng chín! LLM hãy gửi cho User lệnh mở ảnh nhé.`);
72
- }
73
-
74
- async function captureConsole(page) {
75
- console.log(`[Hapo Browser] Đang rình Console Log trong 5 giây...`);
76
- const logs = [];
77
- page.on('console', msg => {
78
- if (msg.type() === 'error' || msg.type() === 'warning') {
79
- logs.push(`[${msg.type().toUpperCase()}] ${msg.text()}`);
80
- }
81
- });
82
- // Gài thêm bẫy vớt lỗi Page Error (JS Crash)
83
- page.on('pageerror', err => logs.push(`[CRASH] ${err.message}`));
84
-
85
- // Cho thời gian sống 5 giấy để lắng nghe event
86
- await new Promise(r => setTimeout(r, 5000));
87
-
88
- console.log('\n=== KẾT QUẢ RÌNH RẬP DÀNH CHO LLM ===');
89
- if (logs.length === 0) console.log('Sạch bóng quân thù! Không có Warning hay Error nào gào thét trên Console cả.');
90
- else logs.forEach(l => console.log(l));
91
- console.log('=====================================\n');
92
- }
93
-
94
- async function captureAria(page) {
95
- console.log(`[Hapo Browser] Đang quét cấu trúc xẻ thịt HTML (ARIA DOM)...`);
96
- // Truy xuất cây Accessibility đơn giản
97
- const snapshot = await page.accessibility.snapshot();
98
- console.log('\n=== CẤU TRÚC DOM ẨN DƯỚI GÓC NHÌN ARIA ===');
99
- console.log(JSON.stringify(snapshot, null, 2));
100
- console.log('=============================================\n');
101
- }
102
-
103
- async function main() {
104
- const config = parseArgs();
105
- if (!config.url || !config.url.startsWith('http')) {
106
- console.error('[LỖI] Thiếu --url hợp lệ (Ví dụ: --url http://localhost:3000)');
107
- process.exit(1);
108
- }
109
-
110
- let browser;
111
- try {
112
- browser = await getBrowser();
113
- const page = await browser.newPage();
114
-
115
- // Set viewport như Macbook Pro
116
- await page.setViewport({ width: 1440, height: 900 });
117
-
118
- console.log(`[Hapo Browser] Định vị tọa độ: ${config.url}`);
119
- await page.goto(config.url, { waitUntil: 'networkidle2', timeout: 15000 });
120
-
121
- if (config.action === 'screenshot') {
122
- await takeScreenshot(page, config.output);
123
- } else if (config.action === 'console') {
124
- await captureConsole(page);
125
- } else if (config.action === 'aria') {
126
- await captureAria(page);
127
- } else {
128
- console.error(`[LỖI] Không tồn tại Action tên là: ${config.action}`);
129
- }
130
-
131
- } catch (error) {
132
- console.error('\n[LỖI SẬP NGUỒN]', error.message);
133
- } finally {
134
- if (browser) await browser.close();
135
- }
136
- }
137
-
138
- main();
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 fireworks-tech-graph contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.