@agiflowai/scaffold-mcp 1.2.0 → 1.3.0

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 CHANGED
@@ -303,6 +303,20 @@ Hooks let scaffold-mcp proactively suggest templates when your AI agent creates
303
303
 
304
304
  When Claude tries to write a new file, the hook shows available scaffolding methods that match, so Claude can use templates instead of writing from scratch.
305
305
 
306
+ Hooks are also available for **Gemini CLI** and **OpenAI Codex CLI** (`--type codex.preToolUse`, configured in `.codex/config.toml`); see the hooks docs for setup.
307
+
308
+ **Relaxing enforcement:** to let some files (docs, content, generated code) be written directly, add exclude globs — workspace-wide via `scaffold-mcp.hook.excludeGlobs` in `.toolkit/settings.yaml`, or per-template via a top-level `exclude` in `scaffold.yaml`:
309
+
310
+ ```yaml
311
+ # .toolkit/settings.yaml
312
+ scaffold-mcp:
313
+ hook:
314
+ excludeGlobs:
315
+ - '**/*.md'
316
+ - '**/*.mdx'
317
+ - '**/src/content/**'
318
+ ```
319
+
306
320
  See [Hooks Documentation](./docs/hooks.md) for details.
307
321
 
308
322
  ---
@@ -174,6 +174,7 @@ const ScaffoldConfigEntrySchema = z.object({
174
174
  patterns: z.array(z.string()).optional()
175
175
  });
176
176
  const ScaffoldYamlSchema = z.object({
177
+ exclude: z.array(z.string()).optional(),
177
178
  boilerplate: z.union([ScaffoldConfigEntrySchema, z.array(ScaffoldConfigEntrySchema)]).optional(),
178
179
  features: z.union([ScaffoldConfigEntrySchema, z.array(ScaffoldConfigEntrySchema)]).optional()
179
180
  }).catchall(z.union([ScaffoldConfigEntrySchema, z.array(ScaffoldConfigEntrySchema)]));
@@ -761,6 +762,7 @@ var ScaffoldingMethodsService = class {
761
762
  if (!await this.fileSystem.pathExists(scaffoldYamlPath)) throw new Error(`scaffold.yaml not found at ${scaffoldYamlPath}`);
762
763
  const scaffoldContent = await this.fileSystem.readFile(scaffoldYamlPath, "utf8");
763
764
  const architectConfig = yaml.load(scaffoldContent);
765
+ const excludeGlobs = Array.isArray(architectConfig.exclude) ? architectConfig.exclude : [];
764
766
  const methods = [];
765
767
  if (architectConfig.features && Array.isArray(architectConfig.features)) architectConfig.features.forEach((feature) => {
766
768
  const featureName = feature.name || `scaffold-${templateName}`;
@@ -779,16 +781,18 @@ var ScaffoldingMethodsService = class {
779
781
  });
780
782
  return {
781
783
  templatePath,
782
- methods
784
+ methods,
785
+ excludeGlobs
783
786
  };
784
787
  }
785
788
  async listScaffoldingMethodsByTemplate(templateName, cursor) {
786
- const { templatePath, methods } = await this.collectAllMethodsByTemplate(templateName);
789
+ const { templatePath, methods, excludeGlobs } = await this.collectAllMethodsByTemplate(templateName);
787
790
  const paginatedResult = PaginationHelper.paginate(methods, cursor);
788
791
  return {
789
792
  sourceTemplate: templateName,
790
793
  templatePath,
791
794
  methods: paginatedResult.items,
795
+ excludeGlobs,
792
796
  nextCursor: paginatedResult.nextCursor,
793
797
  _meta: paginatedResult._meta
794
798
  };
@@ -198,6 +198,7 @@ const ScaffoldConfigEntrySchema = zod.z.object({
198
198
  patterns: zod.z.array(zod.z.string()).optional()
199
199
  });
200
200
  const ScaffoldYamlSchema = zod.z.object({
201
+ exclude: zod.z.array(zod.z.string()).optional(),
201
202
  boilerplate: zod.z.union([ScaffoldConfigEntrySchema, zod.z.array(ScaffoldConfigEntrySchema)]).optional(),
202
203
  features: zod.z.union([ScaffoldConfigEntrySchema, zod.z.array(ScaffoldConfigEntrySchema)]).optional()
203
204
  }).catchall(zod.z.union([ScaffoldConfigEntrySchema, zod.z.array(ScaffoldConfigEntrySchema)]));
@@ -785,6 +786,7 @@ var ScaffoldingMethodsService = class {
785
786
  if (!await this.fileSystem.pathExists(scaffoldYamlPath)) throw new Error(`scaffold.yaml not found at ${scaffoldYamlPath}`);
786
787
  const scaffoldContent = await this.fileSystem.readFile(scaffoldYamlPath, "utf8");
787
788
  const architectConfig = js_yaml.default.load(scaffoldContent);
789
+ const excludeGlobs = Array.isArray(architectConfig.exclude) ? architectConfig.exclude : [];
788
790
  const methods = [];
789
791
  if (architectConfig.features && Array.isArray(architectConfig.features)) architectConfig.features.forEach((feature) => {
790
792
  const featureName = feature.name || `scaffold-${templateName}`;
@@ -803,16 +805,18 @@ var ScaffoldingMethodsService = class {
803
805
  });
804
806
  return {
805
807
  templatePath,
806
- methods
808
+ methods,
809
+ excludeGlobs
807
810
  };
808
811
  }
809
812
  async listScaffoldingMethodsByTemplate(templateName, cursor) {
810
- const { templatePath, methods } = await this.collectAllMethodsByTemplate(templateName);
813
+ const { templatePath, methods, excludeGlobs } = await this.collectAllMethodsByTemplate(templateName);
811
814
  const paginatedResult = PaginationHelper.paginate(methods, cursor);
812
815
  return {
813
816
  sourceTemplate: templateName,
814
817
  templatePath,
815
818
  methods: paginatedResult.items,
819
+ excludeGlobs,
816
820
  nextCursor: paginatedResult.nextCursor,
817
821
  _meta: paginatedResult._meta
818
822
  };
@@ -1,6 +1,6 @@
1
- import { t as ListScaffoldingMethodsTool } from "./ListScaffoldingMethodsTool-DxdrPlcb.mjs";
2
- import "./tools-Dn5KgBUI.mjs";
3
- import { n as resolveNewFileWriteTarget, t as formatScaffoldMethodsHookMessage } from "./shared-BVYIN3Is.mjs";
1
+ import { t as ListScaffoldingMethodsTool } from "./ListScaffoldingMethodsTool-dHaM4QXT.mjs";
2
+ import "./tools-BR1vQd_Y.mjs";
3
+ import { n as getGlobalExcludeGlobs, o as resolveNewFileWriteTarget, r as matchesExcludeGlob, t as formatScaffoldMethodsHookMessage } from "./shared-BJjclypj.mjs";
4
4
  import { ProjectFinderService, TemplatesManagerService } from "@agiflowai/aicode-utils";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
@@ -152,6 +152,7 @@ var PhantomCodeCheckHook = class {
152
152
  function isScaffoldMethodsResponse(value) {
153
153
  if (typeof value !== "object" || value === null) return false;
154
154
  if ("methods" in value && !Array.isArray(value.methods)) return false;
155
+ if ("excludeGlobs" in value && !Array.isArray(value.excludeGlobs)) return false;
155
156
  if ("nextCursor" in value && typeof value.nextCursor !== "string") return false;
156
157
  return true;
157
158
  }
@@ -189,6 +190,10 @@ var UseScaffoldMethodHook = class {
189
190
  decision: DECISION_SKIP,
190
191
  message: "Not a new file write operation"
191
192
  };
193
+ if (matchesExcludeGlob(absoluteFilePath, await getGlobalExcludeGlobs(context.cwd))) return {
194
+ decision: DECISION_ALLOW,
195
+ message: "File matches configured excludeGlobs - writing directly."
196
+ };
192
197
  const executionLog = new ExecutionLogService(context.session_id);
193
198
  if (await executionLog.hasExecuted({
194
199
  filePath,
@@ -225,6 +230,10 @@ var UseScaffoldMethodHook = class {
225
230
  message: "⚠️ Unexpected response shape from scaffolding methods tool"
226
231
  };
227
232
  const data = parsed;
233
+ if (matchesExcludeGlob(absoluteFilePath, data.excludeGlobs)) return {
234
+ decision: DECISION_ALLOW,
235
+ message: "File matches template exclude globs - writing directly."
236
+ };
228
237
  if (!data.methods || data.methods.length === 0) {
229
238
  await executionLog.logExecution({
230
239
  filePath,
@@ -1,6 +1,6 @@
1
- const require_ListScaffoldingMethodsTool = require("./ListScaffoldingMethodsTool-CYZtpU-W.cjs");
2
- require("./tools-COb9iBtt.cjs");
3
- const require_shared = require("./shared-QxPXh-L-.cjs");
1
+ const require_ListScaffoldingMethodsTool = require("./ListScaffoldingMethodsTool-rX9Hxn2d.cjs");
2
+ require("./tools-CV6GWs6f.cjs");
3
+ const require_shared = require("./shared-B0H-6AUG.cjs");
4
4
  let _agiflowai_aicode_utils = require("@agiflowai/aicode-utils");
5
5
  let node_path = require("node:path");
6
6
  node_path = require_ListScaffoldingMethodsTool.__toESM(node_path, 1);
@@ -155,6 +155,7 @@ var PhantomCodeCheckHook = class {
155
155
  function isScaffoldMethodsResponse(value) {
156
156
  if (typeof value !== "object" || value === null) return false;
157
157
  if ("methods" in value && !Array.isArray(value.methods)) return false;
158
+ if ("excludeGlobs" in value && !Array.isArray(value.excludeGlobs)) return false;
158
159
  if ("nextCursor" in value && typeof value.nextCursor !== "string") return false;
159
160
  return true;
160
161
  }
@@ -192,6 +193,10 @@ var UseScaffoldMethodHook = class {
192
193
  decision: _agiflowai_hooks_adapter.DECISION_SKIP,
193
194
  message: "Not a new file write operation"
194
195
  };
196
+ if (require_shared.matchesExcludeGlob(absoluteFilePath, await require_shared.getGlobalExcludeGlobs(context.cwd))) return {
197
+ decision: _agiflowai_hooks_adapter.DECISION_ALLOW,
198
+ message: "File matches configured excludeGlobs - writing directly."
199
+ };
195
200
  const executionLog = new _agiflowai_hooks_adapter.ExecutionLogService(context.session_id);
196
201
  if (await executionLog.hasExecuted({
197
202
  filePath,
@@ -228,6 +233,10 @@ var UseScaffoldMethodHook = class {
228
233
  message: "⚠️ Unexpected response shape from scaffolding methods tool"
229
234
  };
230
235
  const data = parsed;
236
+ if (require_shared.matchesExcludeGlob(absoluteFilePath, data.excludeGlobs)) return {
237
+ decision: _agiflowai_hooks_adapter.DECISION_ALLOW,
238
+ message: "File matches template exclude globs - writing directly."
239
+ };
231
240
  if (!data.methods || data.methods.length === 0) {
232
241
  await executionLog.logExecution({
233
242
  filePath,
package/dist/cli.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- const require_ListScaffoldingMethodsTool = require("./ListScaffoldingMethodsTool-CYZtpU-W.cjs");
3
- const require_src = require("./src-BG_X82dw.cjs");
4
- const require_tools = require("./tools-COb9iBtt.cjs");
2
+ const require_ListScaffoldingMethodsTool = require("./ListScaffoldingMethodsTool-rX9Hxn2d.cjs");
3
+ const require_src = require("./src-CRT1Er93.cjs");
4
+ const require_tools = require("./tools-CV6GWs6f.cjs");
5
5
  let _agiflowai_aicode_utils = require("@agiflowai/aicode-utils");
6
6
  let node_path = require("node:path");
7
7
  node_path = require_ListScaffoldingMethodsTool.__toESM(node_path, 1);
@@ -712,7 +712,7 @@ const hookCommand = new commander.Command("hook").description("Execute scaffold
712
712
  const resolvedAdapterConfig = Object.keys(adapterConfig).length > 0 ? adapterConfig : void 0;
713
713
  if (!isHookMethod(hookMethod)) process.exit(0);
714
714
  if (agent === _agiflowai_coding_agent_bridge.CLAUDE_CODE) {
715
- const hookModule = await Promise.resolve().then(() => require("./claudeCode-g1eRidPR.cjs"));
715
+ const hookModule = await Promise.resolve().then(() => require("./claudeCode-DZnJyFQt.cjs"));
716
716
  const claudeCallbacks = [];
717
717
  if (hookModule.UseScaffoldMethodHook) {
718
718
  const hookInstance = new hookModule.UseScaffoldMethodHook();
@@ -727,7 +727,7 @@ const hookCommand = new commander.Command("hook").description("Execute scaffold
727
727
  if (claudeCallbacks.length === 0) process.exit(0);
728
728
  await new _agiflowai_hooks_adapter.ClaudeCodeAdapter().executeMultiple(claudeCallbacks, resolvedAdapterConfig);
729
729
  } else if (agent === _agiflowai_coding_agent_bridge.GEMINI_CLI) {
730
- const hookModule = await Promise.resolve().then(() => require("./geminiCli--s1Qy7ZP.cjs"));
730
+ const hookModule = await Promise.resolve().then(() => require("./geminiCli-wTHUG-A6.cjs"));
731
731
  const geminiCallbacks = [];
732
732
  if (hookModule.UseScaffoldMethodHook) {
733
733
  const hookInstance = new hookModule.UseScaffoldMethodHook();
@@ -736,7 +736,17 @@ const hookCommand = new commander.Command("hook").description("Execute scaffold
736
736
  }
737
737
  if (geminiCallbacks.length === 0) process.exit(0);
738
738
  await new _agiflowai_hooks_adapter.GeminiCliAdapter().executeMultiple(geminiCallbacks, resolvedAdapterConfig);
739
- } else throw new Error(`Unsupported agent: ${agent}. Supported: ${_agiflowai_coding_agent_bridge.CLAUDE_CODE}, ${_agiflowai_coding_agent_bridge.GEMINI_CLI}`);
739
+ } else if (agent === _agiflowai_coding_agent_bridge.CODEX) {
740
+ const hookModule = await Promise.resolve().then(() => require("./codex-CfOJFx29.cjs"));
741
+ const codexCallbacks = [];
742
+ if (hookModule.UseScaffoldMethodHook) {
743
+ const hookInstance = new hookModule.UseScaffoldMethodHook();
744
+ const hookFn = hookInstance[hookMethod];
745
+ if (hookFn) codexCallbacks.push(hookFn.bind(hookInstance));
746
+ }
747
+ if (codexCallbacks.length === 0) process.exit(0);
748
+ await new _agiflowai_hooks_adapter.CodexAdapter().executeMultiple(codexCallbacks, resolvedAdapterConfig);
749
+ } else throw new Error(`Unsupported agent: ${agent}. Supported: ${_agiflowai_coding_agent_bridge.CLAUDE_CODE}, ${_agiflowai_coding_agent_bridge.GEMINI_CLI}, ${_agiflowai_coding_agent_bridge.CODEX}`);
740
750
  } catch (error) {
741
751
  _agiflowai_aicode_utils.print.error(`Hook error: ${error instanceof Error ? error.message : String(error)}`);
742
752
  process.exit(1);
package/dist/cli.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { a as createServer, i as StdioTransportHandler, n as HttpTransportHandler, o as version, r as SseTransportHandler, t as TransportMode } from "./src-DnVktdhD.mjs";
3
- import { n as ScaffoldingMethodsService, s as FileSystemService } from "./ListScaffoldingMethodsTool-DxdrPlcb.mjs";
4
- import { c as GenerateBoilerplateFileTool, o as GenerateFeatureScaffoldTool, r as writePendingScaffoldLog, s as GenerateBoilerplateTool, t as WriteToFileTool, u as BoilerplateService } from "./tools-Dn5KgBUI.mjs";
2
+ import { a as createServer, i as StdioTransportHandler, n as HttpTransportHandler, o as version, r as SseTransportHandler, t as TransportMode } from "./src-CUQR93cQ.mjs";
3
+ import { n as ScaffoldingMethodsService, s as FileSystemService } from "./ListScaffoldingMethodsTool-dHaM4QXT.mjs";
4
+ import { c as GenerateBoilerplateFileTool, o as GenerateFeatureScaffoldTool, r as writePendingScaffoldLog, s as GenerateBoilerplateTool, t as WriteToFileTool, u as BoilerplateService } from "./tools-BR1vQd_Y.mjs";
5
5
  import { ProjectConfigResolver, TemplatesManagerService, generateStableId, icons, messages, print, readFile, sections } from "@agiflowai/aicode-utils";
6
6
  import path from "node:path";
7
7
  import { Command } from "commander";
8
- import { CLAUDE_CODE, GEMINI_CLI, SUPPORTED_LLM_TOOLS, isValidLlmTool } from "@agiflowai/coding-agent-bridge";
9
- import { ClaudeCodeAdapter, GeminiCliAdapter, parseHookType } from "@agiflowai/hooks-adapter";
8
+ import { CLAUDE_CODE, CODEX, GEMINI_CLI, SUPPORTED_LLM_TOOLS, isValidLlmTool } from "@agiflowai/coding-agent-bridge";
9
+ import { ClaudeCodeAdapter, CodexAdapter, GeminiCliAdapter, parseHookType } from "@agiflowai/hooks-adapter";
10
10
  //#region src/commands/utils.ts
11
11
  function parseJsonOption(value, flagName) {
12
12
  if (!value) throw new Error(`${flagName} is required`);
@@ -711,7 +711,7 @@ const hookCommand = new Command("hook").description("Execute scaffold hooks for
711
711
  const resolvedAdapterConfig = Object.keys(adapterConfig).length > 0 ? adapterConfig : void 0;
712
712
  if (!isHookMethod(hookMethod)) process.exit(0);
713
713
  if (agent === CLAUDE_CODE) {
714
- const hookModule = await import("./claudeCode-DbKOy8W8.mjs");
714
+ const hookModule = await import("./claudeCode-CyHppxA_.mjs");
715
715
  const claudeCallbacks = [];
716
716
  if (hookModule.UseScaffoldMethodHook) {
717
717
  const hookInstance = new hookModule.UseScaffoldMethodHook();
@@ -726,7 +726,7 @@ const hookCommand = new Command("hook").description("Execute scaffold hooks for
726
726
  if (claudeCallbacks.length === 0) process.exit(0);
727
727
  await new ClaudeCodeAdapter().executeMultiple(claudeCallbacks, resolvedAdapterConfig);
728
728
  } else if (agent === GEMINI_CLI) {
729
- const hookModule = await import("./geminiCli-CcrZEqsz.mjs");
729
+ const hookModule = await import("./geminiCli-CgajucCd.mjs");
730
730
  const geminiCallbacks = [];
731
731
  if (hookModule.UseScaffoldMethodHook) {
732
732
  const hookInstance = new hookModule.UseScaffoldMethodHook();
@@ -735,7 +735,17 @@ const hookCommand = new Command("hook").description("Execute scaffold hooks for
735
735
  }
736
736
  if (geminiCallbacks.length === 0) process.exit(0);
737
737
  await new GeminiCliAdapter().executeMultiple(geminiCallbacks, resolvedAdapterConfig);
738
- } else throw new Error(`Unsupported agent: ${agent}. Supported: ${CLAUDE_CODE}, ${GEMINI_CLI}`);
738
+ } else if (agent === CODEX) {
739
+ const hookModule = await import("./codex-7ry557jE.mjs");
740
+ const codexCallbacks = [];
741
+ if (hookModule.UseScaffoldMethodHook) {
742
+ const hookInstance = new hookModule.UseScaffoldMethodHook();
743
+ const hookFn = hookInstance[hookMethod];
744
+ if (hookFn) codexCallbacks.push(hookFn.bind(hookInstance));
745
+ }
746
+ if (codexCallbacks.length === 0) process.exit(0);
747
+ await new CodexAdapter().executeMultiple(codexCallbacks, resolvedAdapterConfig);
748
+ } else throw new Error(`Unsupported agent: ${agent}. Supported: ${CLAUDE_CODE}, ${GEMINI_CLI}, ${CODEX}`);
739
749
  } catch (error) {
740
750
  print.error(`Hook error: ${error instanceof Error ? error.message : String(error)}`);
741
751
  process.exit(1);
@@ -0,0 +1,254 @@
1
+ import { t as ListScaffoldingMethodsTool } from "./ListScaffoldingMethodsTool-dHaM4QXT.mjs";
2
+ import { a as resolveApplyPatchNewFileTargets, i as resolveApplyPatchEditedPaths, n as getGlobalExcludeGlobs, r as matchesExcludeGlob, t as formatScaffoldMethodsHookMessage } from "./shared-BJjclypj.mjs";
3
+ import { ProjectFinderService, TemplatesManagerService } from "@agiflowai/aicode-utils";
4
+ import path from "node:path";
5
+ import { DECISION_ALLOW, DECISION_DENY, DECISION_SKIP, ExecutionLogService } from "@agiflowai/hooks-adapter";
6
+ //#region src/hooks/codex/useScaffoldMethod.ts
7
+ /**
8
+ * Type guard for ScaffoldMethodsResponse
9
+ */
10
+ function isScaffoldMethodsResponse(value) {
11
+ if (typeof value !== "object" || value === null) return false;
12
+ if ("methods" in value && !Array.isArray(value.methods)) return false;
13
+ if ("excludeGlobs" in value && !Array.isArray(value.excludeGlobs)) return false;
14
+ if ("nextCursor" in value && typeof value.nextCursor !== "string") return false;
15
+ return true;
16
+ }
17
+ /**
18
+ * UseScaffoldMethod Hook class for Codex CLI
19
+ *
20
+ * Provides lifecycle hooks for tool execution:
21
+ * - preToolUse: Shows available scaffolding methods before an apply_patch creates new files
22
+ * - postToolUse: Tracks scaffold completion progress after file edits
23
+ */
24
+ var UseScaffoldMethodHook = class {
25
+ /**
26
+ * PreToolUse hook for Codex CLI
27
+ * Proactively shows available scaffolding methods and guides the agent to use them
28
+ * when an apply_patch is about to create a new file.
29
+ *
30
+ * @param context - Codex CLI hook input
31
+ * @returns Hook response with scaffolding methods guidance
32
+ */
33
+ async preToolUse(context) {
34
+ try {
35
+ if (!("tool_name" in context)) return {
36
+ decision: DECISION_SKIP,
37
+ message: "Not a tool use event"
38
+ };
39
+ const command = typeof context.tool_input?.command === "string" ? context.tool_input.command : void 0;
40
+ const newFileTargets = resolveApplyPatchNewFileTargets(context.cwd, context.tool_name, command);
41
+ if (newFileTargets.length === 0) return {
42
+ decision: DECISION_SKIP,
43
+ message: "Not a new-file apply_patch operation"
44
+ };
45
+ const globalExcludeGlobs = await getGlobalExcludeGlobs(context.cwd);
46
+ const target = newFileTargets.find((p) => !matchesExcludeGlob(p, globalExcludeGlobs));
47
+ if (!target) return {
48
+ decision: DECISION_ALLOW,
49
+ message: "New file(s) match configured excludeGlobs - writing directly."
50
+ };
51
+ const executionLog = new ExecutionLogService(context.session_id);
52
+ if (await executionLog.hasExecuted({
53
+ filePath: target,
54
+ decision: DECISION_DENY
55
+ })) return {
56
+ decision: DECISION_SKIP,
57
+ message: "Scaffolding methods already provided for this file"
58
+ };
59
+ const templatesPath = await TemplatesManagerService.findTemplatesPath();
60
+ if (!templatesPath) return {
61
+ decision: DECISION_SKIP,
62
+ message: "Templates folder not found - skipping scaffold method check"
63
+ };
64
+ const tool = new ListScaffoldingMethodsTool(templatesPath, false);
65
+ const projectPath = (await new ProjectFinderService(await TemplatesManagerService.getWorkspaceRoot(context.cwd)).findProjectForFile(target))?.root || context.cwd;
66
+ const result = await tool.execute({ projectPath });
67
+ const firstContent = result.content[0];
68
+ if (firstContent?.type !== "text") return {
69
+ decision: DECISION_SKIP,
70
+ message: "⚠️ Unexpected response type from scaffolding methods tool"
71
+ };
72
+ if (result.isError) return {
73
+ decision: DECISION_SKIP,
74
+ message: `⚠️ Could not load scaffolding methods: ${firstContent.text}`
75
+ };
76
+ const resultText = firstContent.text;
77
+ if (typeof resultText !== "string") return {
78
+ decision: DECISION_SKIP,
79
+ message: "⚠️ Invalid response format from scaffolding methods tool"
80
+ };
81
+ const parsed = JSON.parse(resultText);
82
+ if (!isScaffoldMethodsResponse(parsed)) return {
83
+ decision: DECISION_SKIP,
84
+ message: "⚠️ Unexpected response shape from scaffolding methods tool"
85
+ };
86
+ const data = parsed;
87
+ if (matchesExcludeGlob(target, data.excludeGlobs)) return {
88
+ decision: DECISION_ALLOW,
89
+ message: "File matches template exclude globs - writing directly."
90
+ };
91
+ if (!data.methods || data.methods.length === 0) {
92
+ await executionLog.logExecution({
93
+ filePath: target,
94
+ operation: "list-scaffold-methods",
95
+ decision: DECISION_ALLOW
96
+ });
97
+ return {
98
+ decision: DECISION_ALLOW,
99
+ message: "No scaffolding methods are available for this project template. You should write new files directly."
100
+ };
101
+ }
102
+ const message = formatScaffoldMethodsHookMessage(data.methods);
103
+ await executionLog.logExecution({
104
+ filePath: target,
105
+ operation: "list-scaffold-methods",
106
+ decision: DECISION_DENY
107
+ });
108
+ return {
109
+ decision: DECISION_DENY,
110
+ message
111
+ };
112
+ } catch (error) {
113
+ return {
114
+ decision: DECISION_SKIP,
115
+ message: `⚠️ Hook error: ${error instanceof Error ? error.message : String(error)}`
116
+ };
117
+ }
118
+ }
119
+ /**
120
+ * PostToolUse hook for Codex CLI
121
+ * Tracks file edits after scaffold generation and reminds the agent to complete
122
+ * the implementation of all scaffold-generated files.
123
+ *
124
+ * @param context - Codex CLI hook input
125
+ * @returns Hook response with scaffold completion tracking
126
+ */
127
+ async postToolUse(context) {
128
+ try {
129
+ if (!("tool_name" in context)) return {
130
+ decision: DECISION_SKIP,
131
+ message: "Not a tool use event"
132
+ };
133
+ const executionLog = new ExecutionLogService(context.session_id);
134
+ if (context.tool_name === "use-scaffold-method" || context.tool_input?.toolName === "use-scaffold-method") return {
135
+ decision: DECISION_ALLOW,
136
+ message: "Scaffold execution logged for progress tracking"
137
+ };
138
+ const command = typeof context.tool_input?.command === "string" ? context.tool_input.command : void 0;
139
+ const editedPaths = resolveApplyPatchEditedPaths(context.tool_name, command);
140
+ if (editedPaths.length === 0) return {
141
+ decision: DECISION_SKIP,
142
+ message: "Not a file edit/write operation"
143
+ };
144
+ const lastScaffoldExecution = await getLastScaffoldExecution(executionLog);
145
+ if (!lastScaffoldExecution) return {
146
+ decision: DECISION_SKIP,
147
+ message: "No scaffold execution found"
148
+ };
149
+ const { scaffoldId, generatedFiles, featureName } = lastScaffoldExecution;
150
+ const fulfilledKey = `scaffold-fulfilled-${scaffoldId}`;
151
+ if (await executionLog.hasExecuted({
152
+ filePath: fulfilledKey,
153
+ decision: DECISION_ALLOW
154
+ })) return {
155
+ decision: DECISION_SKIP,
156
+ message: "Scaffold already fulfilled"
157
+ };
158
+ let touchedScaffoldFile = false;
159
+ for (const editedPath of editedPaths) {
160
+ const absoluteEditedPath = path.isAbsolute(editedPath) ? editedPath : path.join(context.cwd, editedPath);
161
+ const generatedMatch = generatedFiles.find((f) => f === editedPath || f === absoluteEditedPath);
162
+ if (!generatedMatch) continue;
163
+ touchedScaffoldFile = true;
164
+ const editKey = `scaffold-edit-${scaffoldId}-${generatedMatch}`;
165
+ if (!await executionLog.hasExecuted({
166
+ filePath: editKey,
167
+ decision: DECISION_ALLOW
168
+ })) await executionLog.logExecution({
169
+ filePath: editKey,
170
+ operation: "scaffold-file-edit",
171
+ decision: DECISION_ALLOW
172
+ });
173
+ }
174
+ const editedFiles = await getEditedScaffoldFiles(executionLog, scaffoldId);
175
+ const totalFiles = generatedFiles.length;
176
+ const remainingFiles = generatedFiles.filter((f) => !editedFiles.includes(f));
177
+ if (remainingFiles.length === 0) {
178
+ await executionLog.logExecution({
179
+ filePath: fulfilledKey,
180
+ operation: "scaffold-fulfilled",
181
+ decision: DECISION_ALLOW
182
+ });
183
+ return {
184
+ decision: DECISION_ALLOW,
185
+ message: `✅ All scaffold files${featureName ? ` for "${featureName}"` : ""} have been implemented! (${totalFiles}/${totalFiles} files completed)`
186
+ };
187
+ }
188
+ if (touchedScaffoldFile) {
189
+ const remainingFilesList = remainingFiles.map((f) => ` - ${f}`).join("\n");
190
+ return {
191
+ decision: DECISION_ALLOW,
192
+ message: `
193
+ ⚠️ **Scaffold Implementation Progress${featureName ? ` for "${featureName}"` : ""}: ${editedFiles.length}/${totalFiles} files completed**
194
+
195
+ **Remaining files to implement:**
196
+ ${remainingFilesList}
197
+
198
+ Don't forget to complete the implementation for all scaffolded files!
199
+ `.trim()
200
+ };
201
+ }
202
+ return {
203
+ decision: DECISION_SKIP,
204
+ message: "Edited file not part of last scaffold execution"
205
+ };
206
+ } catch (error) {
207
+ return {
208
+ decision: DECISION_SKIP,
209
+ message: `⚠️ Hook error: ${error instanceof Error ? error.message : String(error)}`
210
+ };
211
+ }
212
+ }
213
+ };
214
+ /**
215
+ * Helper function to get the last scaffold execution for a session.
216
+ * Returns null if no scaffold execution found or on error.
217
+ */
218
+ async function getLastScaffoldExecution(executionLog) {
219
+ try {
220
+ const entries = await executionLog.loadLog();
221
+ for (let i = entries.length - 1; i >= 0; i--) {
222
+ const entry = entries[i];
223
+ if (entry.operation === "scaffold" && entry.scaffoldId && entry.generatedFiles && entry.generatedFiles.length > 0) return {
224
+ scaffoldId: entry.scaffoldId,
225
+ generatedFiles: entry.generatedFiles,
226
+ featureName: entry.featureName
227
+ };
228
+ }
229
+ return null;
230
+ } catch (error) {
231
+ console.error("Error getting last scaffold execution:", error);
232
+ return null;
233
+ }
234
+ }
235
+ /**
236
+ * Helper function to get the list of edited scaffold files.
237
+ * Returns an empty array if no files found or on error.
238
+ */
239
+ async function getEditedScaffoldFiles(executionLog, scaffoldId) {
240
+ try {
241
+ const entries = await executionLog.loadLog();
242
+ const editedFiles = [];
243
+ for (const entry of entries) if (entry.operation === "scaffold-file-edit" && entry.filePath.startsWith(`scaffold-edit-${scaffoldId}-`)) {
244
+ const filePath = entry.filePath.replace(`scaffold-edit-${scaffoldId}-`, "");
245
+ editedFiles.push(filePath);
246
+ }
247
+ return editedFiles;
248
+ } catch (error) {
249
+ console.error(`Error getting edited scaffold files for ${scaffoldId}:`, error);
250
+ return [];
251
+ }
252
+ }
253
+ //#endregion
254
+ export { UseScaffoldMethodHook };