add-coder 0.3.31 → 0.3.32

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 (74) hide show
  1. package/package.json +1 -1
  2. package/templates/.add-coder-src-hash.json +73 -1
  3. package/templates/adapters/claude/hooks/doc-format-guard.mjs +850 -0
  4. package/templates/adapters/claude/hooks/notification.mjs +256 -0
  5. package/templates/adapters/claude/hooks/permission-denied.mjs +167 -0
  6. package/templates/adapters/claude/hooks/permission-gate.mjs +150 -0
  7. package/templates/adapters/claude/hooks/post-tool-failure.mjs +3 -0
  8. package/templates/adapters/claude/hooks/post-tool-use.mjs +413 -0
  9. package/templates/adapters/claude/hooks/pre-compact.mjs +226 -0
  10. package/templates/adapters/claude/hooks/pre-tool-use.mjs +914 -0
  11. package/templates/adapters/claude/hooks/prompt-submit.mjs +847 -0
  12. package/templates/adapters/claude/hooks/review-checklist.mjs +3 -0
  13. package/templates/adapters/claude/hooks/session-end.mjs +287 -0
  14. package/templates/adapters/claude/hooks/session-start.mjs +566 -0
  15. package/templates/adapters/claude/hooks/stop-check.mjs +505 -0
  16. package/templates/adapters/claude/hooks/stop-failure.mjs +222 -0
  17. package/templates/adapters/claude/hooks/subagent-guard.mjs +224 -0
  18. package/templates/adapters/claude/hooks/subagent-stop.mjs +277 -0
  19. package/templates/adapters/codex/hooks/doc-format-guard.mjs +849 -0
  20. package/templates/adapters/codex/hooks/notification.mjs +254 -0
  21. package/templates/adapters/codex/hooks/permission-gate.mjs +169 -0
  22. package/templates/adapters/codex/hooks/post-tool-failure.mjs +176 -0
  23. package/templates/adapters/codex/hooks/post-tool-use.mjs +575 -0
  24. package/templates/adapters/codex/hooks/pre-compact.mjs +328 -0
  25. package/templates/adapters/codex/hooks/pre-tool-use.mjs +963 -0
  26. package/templates/adapters/codex/hooks/prompt-submit.mjs +623 -0
  27. package/templates/adapters/codex/hooks/review-checklist.mjs +3 -0
  28. package/templates/adapters/codex/hooks/session-end.mjs +389 -0
  29. package/templates/adapters/codex/hooks/session-start.mjs +583 -0
  30. package/templates/adapters/codex/hooks/stop-check.mjs +539 -0
  31. package/templates/adapters/codex/hooks/subagent-guard.mjs +218 -0
  32. package/templates/adapters/codex/hooks/subagent-stop.mjs +381 -0
  33. package/templates/adapters/qoder/hooks/doc-format-guard.mjs +858 -0
  34. package/templates/adapters/qoder/hooks/notification.mjs +269 -0
  35. package/templates/adapters/qoder/hooks/permission-gate.mjs +284 -0
  36. package/templates/adapters/qoder/hooks/post-tool-failure.mjs +294 -0
  37. package/templates/adapters/qoder/hooks/post-tool-use.mjs +535 -0
  38. package/templates/adapters/qoder/hooks/pre-compact.mjs +234 -0
  39. package/templates/adapters/qoder/hooks/pre-tool-use.mjs +948 -0
  40. package/templates/adapters/qoder/hooks/prompt-submit.mjs +789 -0
  41. package/templates/adapters/qoder/hooks/review-checklist.mjs +409 -0
  42. package/templates/adapters/qoder/hooks/session-end.mjs +301 -0
  43. package/templates/adapters/qoder/hooks/session-start.mjs +619 -0
  44. package/templates/adapters/qoder/hooks/stop-check.mjs +567 -0
  45. package/templates/adapters/qoder/hooks/subagent-guard.mjs +219 -0
  46. package/templates/adapters/qoder/hooks/subagent-stop.mjs +298 -0
  47. package/templates/adapters/trae/hooks/doc-format-guard.mjs +849 -0
  48. package/templates/adapters/trae/hooks/notification.mjs +251 -0
  49. package/templates/adapters/trae/hooks/permission-gate.mjs +169 -0
  50. package/templates/adapters/trae/hooks/post-tool-failure.mjs +176 -0
  51. package/templates/adapters/trae/hooks/post-tool-use.mjs +415 -0
  52. package/templates/adapters/trae/hooks/pre-compact.mjs +224 -0
  53. package/templates/adapters/trae/hooks/pre-tool-use.mjs +873 -0
  54. package/templates/adapters/trae/hooks/prompt-submit.mjs +614 -0
  55. package/templates/adapters/trae/hooks/review-checklist.mjs +3 -0
  56. package/templates/adapters/trae/hooks/session-end.mjs +285 -0
  57. package/templates/adapters/trae/hooks/session-start.mjs +555 -0
  58. package/templates/adapters/trae/hooks/stop-check.mjs +502 -0
  59. package/templates/adapters/trae/hooks/subagent-guard.mjs +218 -0
  60. package/templates/adapters/trae/hooks/subagent-stop.mjs +274 -0
  61. package/templates/adapters/vscode/hooks/doc-format-guard.mjs +847 -0
  62. package/templates/adapters/vscode/hooks/notification.mjs +251 -0
  63. package/templates/adapters/vscode/hooks/permission-gate.mjs +169 -0
  64. package/templates/adapters/vscode/hooks/post-tool-failure.mjs +192 -0
  65. package/templates/adapters/vscode/hooks/post-tool-use.mjs +415 -0
  66. package/templates/adapters/vscode/hooks/pre-compact.mjs +224 -0
  67. package/templates/adapters/vscode/hooks/pre-tool-use.mjs +906 -0
  68. package/templates/adapters/vscode/hooks/prompt-submit.mjs +838 -0
  69. package/templates/adapters/vscode/hooks/review-checklist.mjs +3 -0
  70. package/templates/adapters/vscode/hooks/session-end.mjs +285 -0
  71. package/templates/adapters/vscode/hooks/session-start.mjs +560 -0
  72. package/templates/adapters/vscode/hooks/stop-check.mjs +502 -0
  73. package/templates/adapters/vscode/hooks/subagent-guard.mjs +218 -0
  74. package/templates/adapters/vscode/hooks/subagent-stop.mjs +274 -0
@@ -0,0 +1,849 @@
1
+ // templates/adapters/codex/hooks/doc-format-guard.ts
2
+ import { readFileSync as readFileSync3 } from "node:fs";
3
+
4
+ // templates/core/governance/doc-format-guard.ts
5
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync2, readdirSync } from "node:fs";
6
+ import { join as join3, basename as basename2, dirname as dirname2 } from "node:path";
7
+ import { spawnSync as spawnSync2 } from "node:child_process";
8
+
9
+ // templates/core/governance/common.ts
10
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
11
+ import { basename, dirname, join } from "node:path";
12
+ import { spawnSync } from "node:child_process";
13
+ import { createHash } from "node:crypto";
14
+
15
+ // templates/core/governance/rules.ts
16
+ var doc = {
17
+ "token_rules": [
18
+ {
19
+ "template": "add-route-template-heavyweight.md",
20
+ "tokens": [
21
+ "add-route",
22
+ "heavy"
23
+ ]
24
+ },
25
+ {
26
+ "template": "add-route-template.md",
27
+ "tokens": [
28
+ "add-route"
29
+ ]
30
+ },
31
+ {
32
+ "template": "hitl-template.md",
33
+ "tokens": [
34
+ "hitl"
35
+ ]
36
+ },
37
+ {
38
+ "template": "",
39
+ "tokens": [
40
+ "handoff"
41
+ ]
42
+ },
43
+ {
44
+ "template": "checklist-template.md",
45
+ "tokens": [
46
+ "checklist"
47
+ ]
48
+ },
49
+ {
50
+ "template": "fix-verification-template.md",
51
+ "tokens": [
52
+ "fix-verif"
53
+ ]
54
+ },
55
+ {
56
+ "template": "runtime-report-template.md",
57
+ "tokens": [
58
+ "report",
59
+ "runtime"
60
+ ]
61
+ },
62
+ {
63
+ "template": "report-template.md",
64
+ "tokens": [
65
+ "report"
66
+ ]
67
+ },
68
+ {
69
+ "template": "tasks-template.md",
70
+ "tokens": [
71
+ "tasks"
72
+ ]
73
+ },
74
+ {
75
+ "template": "spec-template.md",
76
+ "tokens": [
77
+ "spec"
78
+ ]
79
+ },
80
+ {
81
+ "template": "standard-plan-template.md",
82
+ "tokens": [
83
+ "plan"
84
+ ]
85
+ }
86
+ ],
87
+ "content_rules": [
88
+ {
89
+ "marker": "## PLAN \u5143\u4FE1\u606F",
90
+ "template": "standard-plan-template.md"
91
+ },
92
+ {
93
+ "marker": "## \u4E00\u3001Plan \u6982\u8FF0",
94
+ "template": "simple-plan-template.md"
95
+ },
96
+ {
97
+ "marker": "## \u56DB\u3001Handoff",
98
+ "template": "simple-plan-template.md"
99
+ },
100
+ {
101
+ "marker": "## Review \u5143\u4FE1\u606F",
102
+ "template": "review-template.md",
103
+ "sub_markers": [
104
+ {
105
+ "marker": "\u8FD0\u884C\u65F6\u9A8C\u8BC1",
106
+ "template": "review-runtime-template.md"
107
+ },
108
+ {
109
+ "marker": "\u8DE8\u4ED3\u5E93\u683C\u5F0F\u5951\u7EA6",
110
+ "template": "review-implementation-template.md"
111
+ }
112
+ ]
113
+ },
114
+ {
115
+ "marker": "## Why",
116
+ "template": "spec-template.md"
117
+ },
118
+ {
119
+ "marker": "## Preconditions",
120
+ "template": "tasks-template.md"
121
+ },
122
+ {
123
+ "marker": "\u5BA1\u8BA1\u94FE\uFF08\u8BC1\u636E\u2192devlog\u2192checklist\uFF09",
124
+ "template": "checklist-template.md"
125
+ }
126
+ ],
127
+ "adapter_content_rules": [
128
+ {
129
+ "adapter": "claude",
130
+ "marker": "## \u56DB\u3001Handoff",
131
+ "template": "simple-standard-plan-template.md"
132
+ },
133
+ {
134
+ "adapter": "claude",
135
+ "marker": "## PLAN \u5143\u4FE1\u606F",
136
+ "template": "standard-plan-template.md"
137
+ },
138
+ {
139
+ "adapter": "claude",
140
+ "marker": "## \u4E00\u3001Plan \u6982\u8FF0",
141
+ "template": "simple-plan-template.md"
142
+ },
143
+ {
144
+ "adapter": "claude",
145
+ "marker": "## Review \u5143\u4FE1\u606F",
146
+ "template": "review-template.md",
147
+ "sub_markers": [
148
+ {
149
+ "marker": "\u8FD0\u884C\u65F6\u9A8C\u8BC1",
150
+ "template": "review-runtime-template.md"
151
+ },
152
+ {
153
+ "marker": "\u8DE8\u4ED3\u5E93\u683C\u5F0F\u5951\u7EA6",
154
+ "template": "review-implementation-template.md"
155
+ }
156
+ ]
157
+ },
158
+ {
159
+ "adapter": "claude",
160
+ "marker": "## Why",
161
+ "template": "spec-template.md"
162
+ },
163
+ {
164
+ "adapter": "claude",
165
+ "marker": "## Preconditions",
166
+ "template": "tasks-template.md"
167
+ },
168
+ {
169
+ "adapter": "claude",
170
+ "marker": "\u5BA1\u8BA1\u94FE\uFF08\u8BC1\u636E\u2192devlog\u2192checklist\uFF09",
171
+ "template": "checklist-template.md"
172
+ }
173
+ ],
174
+ "handoff": {
175
+ "marker_multi": "## \u5168\u5C40\u5143\u4FE1\u606F",
176
+ "marker_single": "## 1. \u4EA4\u63A5\u524D\u72B6\u6001"
177
+ },
178
+ "fallback_rules": [
179
+ {
180
+ "template": "add-route-template-heavyweight.md",
181
+ "tokens": [
182
+ "add-route",
183
+ "heavy"
184
+ ]
185
+ },
186
+ {
187
+ "template": "add-route-template.md",
188
+ "tokens": [
189
+ "add-route"
190
+ ]
191
+ },
192
+ {
193
+ "template": "standard-plan-template.md",
194
+ "tokens": [
195
+ "plan"
196
+ ]
197
+ },
198
+ {
199
+ "template": "tasks-template.md",
200
+ "tokens": [
201
+ "tasks"
202
+ ]
203
+ },
204
+ {
205
+ "template": "spec-template.md",
206
+ "tokens": [
207
+ "spec"
208
+ ]
209
+ },
210
+ {
211
+ "template": "checklist-template.md",
212
+ "tokens": [
213
+ "checklist"
214
+ ]
215
+ },
216
+ {
217
+ "template": "runtime-report-template.md",
218
+ "tokens": [
219
+ "report",
220
+ "runtime"
221
+ ]
222
+ },
223
+ {
224
+ "template": "report-template.md",
225
+ "tokens": [
226
+ "report"
227
+ ]
228
+ },
229
+ {
230
+ "template": "fix-verification-template.md",
231
+ "tokens": [
232
+ "fix-verif"
233
+ ]
234
+ },
235
+ {
236
+ "template": "hitl-template.md",
237
+ "tokens": [
238
+ "hitl"
239
+ ]
240
+ }
241
+ ],
242
+ "incremental": {
243
+ "regex": "~~.+~~|\u2192|\\[\\d{4}-\\d{2}-\\d{2}\\s+\u4FEE\u8BA2"
244
+ },
245
+ "anti_cheat": {
246
+ "max_file_count": 3,
247
+ "fuzzy_file_regex": "\u7B49\\s*\\d*\\s*\u4E2A\u6587\u4EF6|\u7B49\\s*\u82E5\u5E72",
248
+ "fuzzy_decision_regex": "\u7B49\\s*\u82E5\u5E72\\s*(\u51B3\u7B56|\u65B9\u6848|\u8BBE\u8BA1)",
249
+ "forbidden_heading": "## \u4E09\u3001\u67B6\u6784\u8BBE\u8BA1"
250
+ }
251
+ };
252
+ var event = {
253
+ "file": {
254
+ "path": "{magicDir}/reports/hook-events.jsonl",
255
+ "rotate_bytes": 262144,
256
+ "total_bytes": 524288,
257
+ "note": "MCP Server \u5B95\u673A\u4E0D\u4E22\u4E8B\u4EF6\uFF0C\u91CD\u542F\u540E\u4ECE\u6587\u4EF6\u6062\u590D\u6D88\u8D39"
258
+ },
259
+ "schema": {
260
+ "fields": [
261
+ "ts",
262
+ "hook",
263
+ "decision",
264
+ "cmd",
265
+ "reason",
266
+ "planKeyword",
267
+ "planStatus"
268
+ ],
269
+ "ts_format": "date -u +%Y-%m-%dT%H:%M:%SZ",
270
+ "extra_fields": [
271
+ "anchor_hit",
272
+ "struct_score",
273
+ "override"
274
+ ]
275
+ },
276
+ "daily": {
277
+ "warn_threshold": 10
278
+ }
279
+ };
280
+ var protocol = {
281
+ "exit_codes": {
282
+ "pass": 0,
283
+ "block": 2,
284
+ "note": "\u5192\u70DF\u6821\u9A8C: \u4EA7\u7269\u9000\u51FA\u7801 \u2208 {0,2}\uFF08\u5176\u4F59\u4E3A\u975E\u9884\u671F\uFF0C\u9700\u4FEE\u590D\uFF09"
285
+ },
286
+ "output": {
287
+ "stdout_json_only": true,
288
+ "stderr_human_text": true,
289
+ "field_separator": "::",
290
+ "magic_dir_resolution": "\u6CE8\u5165\u4F18\u5148 \u2192 \u7269\u7406\u4F4D\u7F6E\u63A8\u5BFC \u2192 failClosed\uFF08\u7981\u6B62\u731C\u6D4B adapter \u540D\uFF09"
291
+ },
292
+ "adapters": {
293
+ "claude": {
294
+ "stdout_form": "plain-text",
295
+ "project_dir_env": "CLAUDE_PROJECT_DIR",
296
+ "magic_dir": ".claude",
297
+ "handlerTypes": [
298
+ "command",
299
+ "mcp_tool"
300
+ ]
301
+ },
302
+ "qoder": {
303
+ "stdout_form": "json",
304
+ "project_dir_env": "QODER_PROJECT_DIR",
305
+ "magic_dir": ".qoder",
306
+ "handlerTypes": [
307
+ "command",
308
+ "http"
309
+ ]
310
+ },
311
+ "codex": {
312
+ "stdout_form": "systemMessage",
313
+ "project_dir_env": "git-toplevel",
314
+ "magic_dir": ".codex",
315
+ "handlerTypes": [
316
+ "command"
317
+ ]
318
+ },
319
+ "vscode": {
320
+ "stdout_form": "plain-text",
321
+ "project_dir_env": "PWD",
322
+ "magic_dir": ".vscode",
323
+ "handlerTypes": [
324
+ "command"
325
+ ]
326
+ },
327
+ "trae": {
328
+ "stdout_form": "plain-text",
329
+ "project_dir_env": "PWD",
330
+ "magic_dir": ".trae",
331
+ "handlerTypes": [
332
+ "command"
333
+ ]
334
+ }
335
+ },
336
+ "event_outputs": {
337
+ "qoder": {
338
+ "SessionStart": "additionalContext",
339
+ "UserPromptSubmit": "additionalContext",
340
+ "PreToolUse": "permissionDecision",
341
+ "Stop": "additionalContext",
342
+ "PostToolUse": "feedback",
343
+ "SubagentStart": "additionalContext",
344
+ "SubagentStop": "additionalContext",
345
+ "PostToolUseFailure": "text",
346
+ "PermissionRequest": "text",
347
+ "SessionEnd": "text",
348
+ "PreCompact": "text",
349
+ "Notification": "text"
350
+ },
351
+ "claude": {
352
+ "SessionStart": "additionalContext",
353
+ "UserPromptSubmit": "additionalContext",
354
+ "PreToolUse": "permissionDecision",
355
+ "Stop": "text",
356
+ "PostToolUse": "feedback",
357
+ "SubagentStart": "text",
358
+ "SubagentStop": "text",
359
+ "PostToolUseFailure": "text",
360
+ "PermissionRequest": "text",
361
+ "SessionEnd": "text",
362
+ "PreCompact": "text",
363
+ "Notification": "text"
364
+ },
365
+ "codex": {
366
+ "Stop": "systemMessage",
367
+ "PostToolUse": "text"
368
+ },
369
+ "vscode": {
370
+ "PostToolUse": "text"
371
+ },
372
+ "trae": {
373
+ "PostToolUse": "text"
374
+ }
375
+ },
376
+ "core": {
377
+ "stdout_form": "json",
378
+ "magic_dir": ".add",
379
+ "note": "core \u5165\u53E3\u534F\u8BAE = qoder \u540C\u6784\u53C2\u8003\u5B9E\u73B0\uFF1Badapter \u4EC5\u4FDD\u7559\u672C\u8868\u58F0\u660E\u7684\u79C1\u6709\u5DEE\u5F02"
380
+ },
381
+ "adapter_defaults": {
382
+ "magic_dir_fallback": ".qoder",
383
+ "probe_magic_dirs": [
384
+ ".claude",
385
+ ".qoder",
386
+ ".vscode",
387
+ ".add",
388
+ ".trae",
389
+ ".codex"
390
+ ]
391
+ }
392
+ };
393
+
394
+ // templates/core/governance/common.ts
395
+ var EXIT_PASS = protocol.exit_codes.pass;
396
+ var EXIT_BLOCK = protocol.exit_codes.block;
397
+ var STATE_SEP = protocol.output.field_separator;
398
+ function queryPlanStatus() {
399
+ const magicDir = process.env.MAGIC_DIR;
400
+ if (!magicDir) {
401
+ return {
402
+ stdout: '{"availability":"STATUS_UNAVAILABLE","source":"database","reason":"magicDir \u672A\u6CE8\u5165\u4E14\u65E0\u6CD5\u4ECE\u7269\u7406\u4F4D\u7F6E\u63A8\u5BFC"}',
403
+ exitCode: 3
404
+ };
405
+ }
406
+ const bridge = join(
407
+ process.env.PROJECT_DIR || process.cwd(),
408
+ magicDir,
409
+ "scripts",
410
+ "plan-status-bridge.ts"
411
+ );
412
+ if (!existsSync(bridge)) {
413
+ return {
414
+ stdout: '{"availability":"STATUS_UNAVAILABLE","source":"database","reason":"plan-status bridge missing"}',
415
+ exitCode: 3
416
+ };
417
+ }
418
+ const r = spawnSync("node", ["--import", "tsx", bridge], {
419
+ encoding: "utf-8",
420
+ timeout: 1e4
421
+ });
422
+ return { stdout: r.stdout ?? "", exitCode: r.status ?? -1 };
423
+ }
424
+ function detectActiveAdd() {
425
+ const r = queryPlanStatus();
426
+ if (r.exitCode !== 0) {
427
+ let reason = "database status unavailable";
428
+ try {
429
+ const parsed = JSON.parse(r.stdout);
430
+ if (parsed.reason) reason = parsed.reason;
431
+ } catch {
432
+ }
433
+ return `__STATUS_UNAVAILABLE__::${reason}::database::none::none`;
434
+ }
435
+ let snapshot;
436
+ try {
437
+ snapshot = JSON.parse(r.stdout);
438
+ } catch {
439
+ return null;
440
+ }
441
+ if (!(snapshot.availability === "READY" && snapshot.isActive === true)) return null;
442
+ const done = snapshot.progress?.doneTasks ?? 0;
443
+ const total = snapshot.progress?.totalTasks ?? 0;
444
+ const approval = snapshot.approvalStatus ?? "none";
445
+ return `${snapshot.planName}::${done}/${total}::${approval}::none::none`;
446
+ }
447
+ function localIsoSeconds() {
448
+ const d = /* @__PURE__ */ new Date();
449
+ const pad = (n) => String(n).padStart(2, "0");
450
+ const offsetMin = -d.getTimezoneOffset();
451
+ const sign = offsetMin >= 0 ? "+" : "-";
452
+ const offAbs = Math.abs(offsetMin);
453
+ const off = `${sign}${pad(Math.floor(offAbs / 60))}:${pad(offAbs % 60)}`;
454
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${off}`;
455
+ }
456
+ function projectHash() {
457
+ try {
458
+ return createHash("md5").update(`${process.env.PROJECT_DIR || process.cwd()}
459
+ `).digest("hex").slice(0, 8);
460
+ } catch {
461
+ return "default";
462
+ }
463
+ }
464
+ var DEV_FLAG = `/tmp/add_dev_${projectHash()}`;
465
+
466
+ // templates/core/governance/notify.ts
467
+ import { existsSync as existsSync2, mkdirSync, renameSync, statSync, appendFileSync } from "node:fs";
468
+ import { join as join2 } from "node:path";
469
+ function writeHookEvent(hook, decision, cmd, reason, plan = "unknown", status = "none", extra = "", magicDirOverride) {
470
+ const defaults = protocol.adapter_defaults;
471
+ const fallback = defaults?.magic_dir_fallback ?? ".qoder";
472
+ const dir = join2(magicDirOverride ?? (process.env.MAGIC_DIR || fallback), "reports");
473
+ try {
474
+ mkdirSync(dir, { recursive: true });
475
+ } catch {
476
+ }
477
+ const file = join2(dir, "hook-events.jsonl");
478
+ if (existsSync2(file)) {
479
+ let sz = 0;
480
+ try {
481
+ sz = statSync(file).size;
482
+ } catch {
483
+ sz = 0;
484
+ }
485
+ if (sz > event.file.rotate_bytes) {
486
+ try {
487
+ renameSync(file, `${file}.old`);
488
+ } catch {
489
+ }
490
+ }
491
+ }
492
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
493
+ const extraPart = extra ? `,${extra}` : "";
494
+ const line = `{"ts":"${ts}","hook":"${hook}","decision":"${decision}","cmd":"${cmd}","reason":"${reason}","planKeyword":"${plan}","planStatus":"${status}"${extraPart}}
495
+ `;
496
+ appendFileSync(file, line);
497
+ }
498
+
499
+ // templates/core/governance/doc-format-guard.ts
500
+ function extractContent(input) {
501
+ const ti = input.tool_input;
502
+ if (!ti) return "";
503
+ if (typeof ti.file_content === "string") return ti.file_content;
504
+ if (typeof ti.content === "string") return ti.content;
505
+ if (typeof ti.new_string === "string") return ti.new_string;
506
+ const reps = ti.replacements;
507
+ if (reps && reps.length > 0 && typeof reps[0].new_text === "string") return reps[0].new_text;
508
+ return "";
509
+ }
510
+ function templateNameByToken(filePath) {
511
+ const base = basename2(filePath).replace(/-v\d.*$/, "");
512
+ for (const rule of doc.token_rules ?? []) {
513
+ if (rule.tokens.every((t) => base.includes(t))) return rule.template;
514
+ }
515
+ return "";
516
+ }
517
+ function templateNameByContent(content, filePath, adapterName) {
518
+ const rules = adapterName === "core" ? doc.content_rules ?? [] : (doc.adapter_content_rules ?? []).filter((r) => r.adapter === adapterName);
519
+ for (const rule of rules) {
520
+ if (content.includes(rule.marker)) {
521
+ for (const sub of rule.sub_markers ?? []) {
522
+ if (content.includes(sub.marker)) return sub.template;
523
+ }
524
+ return rule.template;
525
+ }
526
+ }
527
+ if (filePath.includes("handoff")) {
528
+ const handoffRules = doc.handoff;
529
+ if (content.includes(handoffRules.marker_multi)) return "handoff-multi-round-template.md";
530
+ if (content.includes(handoffRules.marker_single)) return "handoff-single-round-template.md";
531
+ return null;
532
+ }
533
+ for (const rule of doc.fallback_rules ?? []) {
534
+ if (rule.tokens.every((t) => filePath.includes(t))) return rule.template;
535
+ }
536
+ return null;
537
+ }
538
+ function isIncrementalEdit(content) {
539
+ const regex = doc.incremental.regex;
540
+ return new RegExp(regex).test(content);
541
+ }
542
+ var DocFormatGuard = class {
543
+ projectDir;
544
+ magicDir;
545
+ planKeyword;
546
+ planStatus;
547
+ /** adapter 名(缺省 core;adapter 子类传名加载独立内容探测链) */
548
+ adapterName;
549
+ issues = [];
550
+ constructor(projectDir, magicDir, adapterName = "core") {
551
+ this.projectDir = projectDir;
552
+ this.magicDir = magicDir;
553
+ this.adapterName = adapterName;
554
+ const active = detectActiveAdd();
555
+ if (active !== null) {
556
+ this.planKeyword = active.split("::")[0] ?? "";
557
+ this.planStatus = "active";
558
+ } else {
559
+ this.planKeyword = "no-active-plan";
560
+ this.planStatus = "none";
561
+ }
562
+ }
563
+ /** 识别模板:token → 内容 → null(无法识别);handoff 内容无法识别时由调用方阻断
564
+ * protected: adapter 子类可 override 探测链(如 claude 版 simple-standard-plan 分支) */
565
+ identifyTemplate(filePath, content) {
566
+ let name = templateNameByToken(filePath);
567
+ if (name === "") {
568
+ const byContent = templateNameByContent(content, filePath, this.adapterName);
569
+ if (byContent === null && filePath.includes("handoff")) {
570
+ return { handoffUnrecognized: true };
571
+ }
572
+ if (byContent === null) {
573
+ if (isIncrementalEdit(content)) {
574
+ return null;
575
+ }
576
+ return null;
577
+ }
578
+ name = byContent;
579
+ }
580
+ return { name };
581
+ }
582
+ /** 读取 schema.json(含默认空结构) */
583
+ loadSchema(templateName) {
584
+ const schemaFile = join3(this.projectDir, this.magicDir, "templates", templateName.replace(/\.md$/, ".schema.json"));
585
+ if (!existsSync3(schemaFile)) return null;
586
+ try {
587
+ return JSON.parse(readFileSync2(schemaFile, "utf-8"));
588
+ } catch {
589
+ return null;
590
+ }
591
+ }
592
+ /** 章节/锚定/占位符/禁词校验,返回 struct 统计 */
593
+ runSchemaChecks(schema, templateName, content, isSearchReplace) {
594
+ const templatesDir = join3(this.projectDir, this.magicDir, "templates");
595
+ let applied = 0;
596
+ let missed = 0;
597
+ let anchorHit = true;
598
+ const headings = schema.sections.filter((s) => s.heading).map((s) => s.heading);
599
+ const requiredHeadings = schema.sections.filter((s) => s.required === true && s.heading).map((s) => s.heading);
600
+ const subs = schema.sections.flatMap((s) => s.subsections ?? []).filter((s) => s.heading).map((s) => s.heading);
601
+ const placeholders = schema.placeholders ?? [];
602
+ const terms = schema.forbidden_terms ?? [];
603
+ if (!isSearchReplace) {
604
+ for (const section of schema.sections) {
605
+ if (!section.anchor) continue;
606
+ applied++;
607
+ const templateContent = existsSync3(join3(templatesDir, templateName)) ? readFileSync2(join3(templatesDir, templateName), "utf-8") : "";
608
+ const refLine = templateContent.split("\n").find((l) => l.includes(section.anchor));
609
+ if (!refLine) {
610
+ process.stderr.write(`[doc-format-guard] anchor_miss: schema ${section.id} \u58F0\u660E\u7684 anchor '${section.anchor}' \u5728 ${templateName} \u4E2D\u672A\u5B9A\u4F4D\uFF0C\u8DF3\u8FC7\u8BE5\u89C4\u5219\uFF08\u5192\u70DF\u5DE1\u68C0\u5151\u5E95\uFF09
611
+ `);
612
+ applied--;
613
+ continue;
614
+ }
615
+ const tokens = [...new Set(refLine.replace(/[#*`|(){]/g, " ").split(/\s+/).filter((t) => t !== "" && !t.includes("{")))];
616
+ if (tokens.length === 0) {
617
+ applied--;
618
+ continue;
619
+ }
620
+ let scope = content;
621
+ if (section.within) {
622
+ if (content.includes(section.within)) {
623
+ const startIdx = content.indexOf(section.within);
624
+ const endIdx = content.indexOf("\n## ", startIdx + 1);
625
+ scope = content.slice(startIdx, endIdx === -1 ? void 0 : endIdx);
626
+ } else {
627
+ process.stderr.write(`[doc-format-guard] within_miss: schema ${section.id} \u7684 within '${section.within}' \u5728\u6587\u6863\u4E2D\u672A\u5B9A\u4F4D\uFF0C\u8DF3\u8FC7\u8BE5\u89C4\u5219
628
+ `);
629
+ applied--;
630
+ continue;
631
+ }
632
+ }
633
+ const missTokens = tokens.filter((tok) => !scope.includes(tok));
634
+ if (missTokens.length > 0) {
635
+ this.issues.push(` \u7F3A\u951A\u70B9(${section.id}): ${missTokens.join(" ")}`);
636
+ missed++;
637
+ anchorHit = false;
638
+ }
639
+ }
640
+ for (const heading of requiredHeadings) {
641
+ applied++;
642
+ if (!content.includes(heading)) {
643
+ this.issues.push(` \u7F3A\u7AE0\u8282: ${heading}`);
644
+ missed++;
645
+ }
646
+ }
647
+ for (const sub of subs) {
648
+ applied++;
649
+ if (!content.includes(sub)) {
650
+ this.issues.push(` \u7F3A\u5B50\u7AE0\u8282: ${sub}`);
651
+ missed++;
652
+ }
653
+ }
654
+ }
655
+ for (const ph of placeholders) {
656
+ if (content.includes(ph)) {
657
+ this.issues.push(` \u672A\u66FF\u6362\u5360\u4F4D\u7B26: ${ph}`);
658
+ missed++;
659
+ }
660
+ }
661
+ let structText = (content.match(/^#{2,}\s.*$/gm) ?? []).join("\n");
662
+ const col = typeof schema.groupColumn === "number" ? schema.groupColumn : Number(schema.groupColumn);
663
+ if (!Number.isNaN(col) && col > 0) {
664
+ const colLines = content.split("\n").map((l) => {
665
+ const cells = l.split("|");
666
+ return cells.length > col ? (cells[col + 1] ?? "").trim() : "";
667
+ }).filter(Boolean);
668
+ structText += "\n" + colLines.join("\n");
669
+ }
670
+ for (const term of terms) {
671
+ applied++;
672
+ if (structText.includes(term)) {
673
+ this.issues.push(` \u7ED3\u6784\u4F4D\u7981\u8BCD: ${term}`);
674
+ missed++;
675
+ }
676
+ }
677
+ return { applied, missed, anchorHit };
678
+ }
679
+ /** 算法化规则校验(真源: [doc.anti_cheat] + HITL 表非空 + handoff 冲突)
680
+ * protected: adapter 子类可 override(如 claude 版无算法规则段) */
681
+ runAlgoChecks(templateName, filePath, content) {
682
+ const antiCheat = doc.anti_cheat;
683
+ if (templateName.includes("simple-plan")) {
684
+ const fileCount = (content.match(/^\|\s*`[^`]+`/gm) || []).length;
685
+ if (fileCount > antiCheat.max_file_count) {
686
+ this.issues.push(` \u274C \u7CBE\u7B80\u7248\u53CD\u4F5C\u5F0A: \u6D89\u53CA ${fileCount} \u4E2A\u6587\u4EF6\uFF08\u8D85\u8FC7 ${antiCheat.max_file_count} \u4E2A\u9650\u5236\uFF09\uFF0C\u5E94\u6539\u7528 standard-plan-template.md`);
687
+ }
688
+ if (new RegExp(antiCheat.fuzzy_file_regex).test(content)) {
689
+ this.issues.push(" \u274C \u7CBE\u7B80\u7248\u53CD\u4F5C\u5F0A: HITL \u8868\u6587\u4EF6\u6E05\u5355\u4F7F\u7528\u6A21\u7CCA\u63CF\u8FF0\uFF08'\u7B49 N \u4E2A\u6587\u4EF6'\uFF09\uFF0C\u5FC5\u987B\u5217\u51FA\u5B9E\u9645\u5B8C\u6574\u8DEF\u5F84");
690
+ }
691
+ if (new RegExp(antiCheat.fuzzy_decision_regex).test(content)) {
692
+ this.issues.push(" \u274C \u7CBE\u7B80\u7248\u53CD\u4F5C\u5F0A: HITL \u8868\u65B9\u6848/\u8BBE\u8BA1\u51B3\u7B56\u4F7F\u7528\u6A21\u7CCA\u63CF\u8FF0\uFF08'\u7B49\u82E5\u5E72\u51B3\u7B56'\uFF09\uFF0C\u5FC5\u987B\u9010\u6761\u5217\u51FA");
693
+ }
694
+ if (content.includes(antiCheat.forbidden_heading)) {
695
+ this.issues.push(" \u274C \u7CBE\u7B80\u7248\u53CD\u4F5C\u5F0A: \u5305\u542B '## \u4E09\u3001\u67B6\u6784\u8BBE\u8BA1' \u7AE0\u8282\uFF0C\u7CBE\u7B80\u7248\u4E0D\u5E94\u6709\u67B6\u6784\u8BBE\u8BA1\u2014\u2014\u5E94\u6539\u7528 standard-plan-template.md");
696
+ }
697
+ }
698
+ if (/plan|review/.test(templateName)) {
699
+ if (content.includes("## HITL")) {
700
+ const hitlSection = content.slice(content.indexOf("## HITL"));
701
+ const hitlRows = (hitlSection.match(/^\|\s*[^|{]*\s*\|/gm) || []).length;
702
+ const hitlData = hitlRows - 2;
703
+ if (hitlData < 1) {
704
+ this.issues.push(" \u26A0\uFE0F HITL \u8868\u4E3A\u7A7A\u2014\u2014\u5FC5\u987B\u586B\u5199\u81F3\u5C11 1 \u884C\u5B9E\u9645\u5185\u5BB9\u540E\u518D\u63D0\u4EA4\u5BA1\u6838");
705
+ }
706
+ }
707
+ }
708
+ if (templateName.includes("simple-plan")) {
709
+ const planBase = basename2(filePath).replace(/-plan-v.*/, "");
710
+ const planDir = dirname2(filePath);
711
+ const pattern = `${planBase}-handoff`;
712
+ const found = (() => {
713
+ try {
714
+ return readdirSync(planDir).some((f) => f.startsWith(pattern) && f.endsWith(".md"));
715
+ } catch {
716
+ return false;
717
+ }
718
+ })();
719
+ if (found) {
720
+ this.issues.push(` \u274C \u7CBE\u7B80\u7248 Handoff \u51B2\u7A81: \u68C0\u6D4B\u5230\u72EC\u7ACB handoff \u6587\u4EF6\uFF08${pattern}*.md\uFF09\u3002\u7CBE\u7B80\u7248 Plan \u7684 Handoff \u5DF2\u878D\u5408\u5728 \xA7\u56DB\uFF0C\u4E0D\u5E94\u751F\u6210\u72EC\u7ACB\u6587\u4EF6\u3002\u8BF7\u5220\u9664\u72EC\u7ACB handoff \u6587\u4EF6\u6216\u6539\u7528 standard-plan-template.md`);
721
+ }
722
+ }
723
+ }
724
+ /** 主入口:返回 exit code(0 放行 / 2 阻断) */
725
+ run(inputRaw) {
726
+ const input = (() => {
727
+ try {
728
+ return JSON.parse(inputRaw);
729
+ } catch {
730
+ return {};
731
+ }
732
+ })();
733
+ const ti = input.tool_input;
734
+ if (inputRaw.trim() !== "" && !ti) {
735
+ process.stderr.write("jq: error (at <stdin>:1): null (null) has no keys\n");
736
+ }
737
+ try {
738
+ const debugDir = join3(this.projectDir, this.magicDir, "debug-dump");
739
+ mkdirSync2(debugDir, { recursive: true });
740
+ const log = [
741
+ `=== ${localIsoSeconds()} ===`,
742
+ `file_path: ${typeof ti?.file_path === "string" ? ti.file_path : "EMPTY"}`,
743
+ `has_file_content: ${ti && typeof ti.file_content === "string"}`,
744
+ `has_replacements: ${ti && Array.isArray(ti.replacements)}`
745
+ ];
746
+ if (ti && typeof ti.file_content === "string") {
747
+ log.push(`[file_content[500]]: ${ti.file_content.slice(0, 500)}`);
748
+ }
749
+ if (ti && Array.isArray(ti.replacements) && ti.replacements[0]?.new_text) {
750
+ log.push(`[replacement_new_text[500]]: ${ti.replacements[0].new_text.slice(0, 500)}`);
751
+ }
752
+ log.push(`top_keys: ${Object.keys(input).join(", ")}`);
753
+ log.push(`tool_input_keys: ${ti ? Object.keys(ti).join(", ") : "NO_TOOL_INPUT"}`);
754
+ log.push("=== DONE ===");
755
+ appendFileSync2(join3(debugDir, "stdin.log"), log.join("\n") + "\n");
756
+ } catch {
757
+ }
758
+ const filePath = typeof ti?.file_path === "string" ? ti.file_path : "";
759
+ if (filePath === "") return 0;
760
+ if (!new RegExp(`${this.magicDir}/(plans|specs)/`).test(filePath)) return 0;
761
+ const CONTENT = extractContent(input);
762
+ if (CONTENT === "") {
763
+ process.stderr.write("\u26D4 \u62D2\u7EDD\uFF1AWrite \u5DE5\u5177\u672A\u4F20 file_content\uFF0C\u65E0\u6CD5\u6821\u9A8C\u624B\u5199\u6587\u6863\u683C\u5F0F\n");
764
+ process.stdout.write('{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Write \u5DE5\u5177\u672A\u4F20 file_content\uFF0C\u65E0\u6CD5\u6821\u9A8C\u624B\u5199\u6587\u6863\u3002\u8BF7\u7528 SearchReplace \u6539\u5199\u5DF2\u6709\u6587\u4EF6\uFF0C\u6216\u7528 Write \u5DE5\u5177\u91CD\u8BD5\u3002"}}\n');
765
+ writeHookEvent("doc-format-guard", "deny", "Write", "Write \u5DE5\u5177\u672A\u4F20 file_content", this.planKeyword, this.planStatus);
766
+ return 2;
767
+ }
768
+ const identified = this.identifyTemplate(filePath, CONTENT);
769
+ let TEMPLATE_NAME = "";
770
+ if (identified && "handoffUnrecognized" in identified) {
771
+ process.stderr.write("\u26D4 handoff \u6587\u4EF6\u5185\u5BB9\u65E0\u6CD5\u8BC6\u522B\u6A21\u677F\u7C7B\u578B\uFF08\u7F3A '## \u5168\u5C40\u5143\u4FE1\u606F' \u6216 '## 1. \u4EA4\u63A5\u524D\u72B6\u6001'\uFF09\uFF0C\u62D2\u7EDD\u5199\u5165\n");
772
+ process.stdout.write('{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"handoff \u5185\u5BB9\u4E0D\u7B26\u5408 single/multi \u6A21\u677F\u89C4\u8303"}}\n');
773
+ writeHookEvent("doc-format-guard", "deny", filePath, "handoff \u6A21\u677F\u7C7B\u578B\u65E0\u6CD5\u8BC6\u522B", this.planKeyword, this.planStatus);
774
+ return 2;
775
+ }
776
+ if (identified && "name" in identified) {
777
+ TEMPLATE_NAME = identified.name;
778
+ } else {
779
+ if (isIncrementalEdit(CONTENT)) {
780
+ process.stderr.write("[doc-format-guard] \u68C0\u6D4B\u5230\u589E\u91CF\u4FEE\u8BA2\u683C\u5F0F\uFF0C\u8DF3\u8FC7\u5B8C\u6574\u7AE0\u8282\u6821\u9A8C\n");
781
+ return 0;
782
+ }
783
+ process.stderr.write(`\u26D4 \u62D2\u7EDD\uFF1A\u65E0\u6CD5\u8BC6\u522B\u6587\u6863\u7C7B\u578B (file_path: ${filePath})\uFF0C\u7F3A\u5C11\u6A21\u677F\u5339\u914D\u89C4\u5219
784
+ `);
785
+ process.stdout.write('{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"\u65E0\u6CD5\u8BC6\u522B ADD \u6587\u6863\u7C7B\u578B\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u66F4\u65B0 doc-format-guard"}}\n');
786
+ writeHookEvent("doc-format-guard", "deny", filePath, "\u65E0\u6CD5\u8BC6\u522B\u6587\u6863\u7C7B\u578B", this.planKeyword, this.planStatus);
787
+ return 2;
788
+ }
789
+ const schema = this.loadSchema(TEMPLATE_NAME);
790
+ if (!schema) {
791
+ process.stderr.write(`\u26D4 \u963B\u65AD\uFF1A\u6A21\u677F ${TEMPLATE_NAME} \u7F3A\u5C11\u5BF9\u5E94\u7684 .schema.json \u6821\u9A8C\u89C4\u5219
792
+ `);
793
+ process.stdout.write('{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"\u7F3A\u5C11 .schema.json \u6821\u9A8C\u89C4\u5219\u6587\u4EF6\uFF0C\u7981\u6B62\u65E0\u89C4\u5219\u653E\u884C"}}\n');
794
+ writeHookEvent("doc-format-guard", "deny", filePath, "\u7F3A\u5C11 .schema.json \u6821\u9A8C\u89C4\u5219", this.planKeyword, this.planStatus);
795
+ return 2;
796
+ }
797
+ const isSearchReplace = ti && Array.isArray(ti.replacements) && ti.replacements.length > 0 ? true : ti && typeof ti.new_string === "string";
798
+ const { applied, missed, anchorHit } = this.runSchemaChecks(schema, TEMPLATE_NAME, CONTENT, Boolean(isSearchReplace));
799
+ this.runAlgoChecks(TEMPLATE_NAME, filePath, CONTENT);
800
+ const termTotal = (schema.forbidden_terms ?? []).length;
801
+ const finalApplied = isSearchReplace ? termTotal : applied;
802
+ const finalMissed = this.issues.length;
803
+ let structScore = 100;
804
+ if (finalApplied > 0 && finalMissed > 0) {
805
+ structScore = Math.max(0, Math.floor((finalApplied - finalMissed) * 100 / finalApplied));
806
+ }
807
+ const BACKFLOW_EXTRA = `"anchor_hit":${anchorHit},"struct_score":${structScore}`;
808
+ if (this.issues.length > 0) {
809
+ process.stderr.write(`\u26D4 ${TEMPLATE_NAME} \u6821\u9A8C\u4E0D\u901A\u8FC7:
810
+ ${this.issues.join("\n")}
811
+ `);
812
+ const brief = this.issues.join(" ").replace(/"/g, "").replace(/\s+/g, " ").slice(0, 180);
813
+ process.stdout.write(`{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"\u6587\u6863\u683C\u5F0F\u6821\u9A8C\u4E0D\u901A\u8FC7: ${brief}"}}
814
+ `);
815
+ writeHookEvent("doc-format-guard", "deny", filePath, "\u6587\u6863\u683C\u5F0F\u6821\u9A8C\u4E0D\u901A\u8FC7", this.planKeyword, this.planStatus, BACKFLOW_EXTRA);
816
+ return 2;
817
+ }
818
+ if (new RegExp(`${this.magicDir}/plans/`).test(filePath)) {
819
+ const genIndex = join3(this.projectDir, "scripts", "gen-plan-index.sh");
820
+ if (existsSync3(genIndex)) {
821
+ try {
822
+ spawnSync2("bash", [genIndex], { stdio: "ignore" });
823
+ } catch {
824
+ }
825
+ }
826
+ }
827
+ writeHookEvent("doc-format-guard", "allow", filePath, "\u6821\u9A8C\u901A\u8FC7", this.planKeyword, this.planStatus, BACKFLOW_EXTRA);
828
+ return 0;
829
+ }
830
+ };
831
+
832
+ // templates/adapters/codex/hooks/doc-format-guard.ts
833
+ var CodexDocFormatGuard = class extends DocFormatGuard {
834
+ /** 协议差异封装: MAGIC_DIR 兜底 = ".codex"(codex 端) */
835
+ constructor(projectDir, magicDir) {
836
+ super(projectDir, magicDir);
837
+ }
838
+ // 当前无 override(探测链/算法段同 core);未来 codex 协议演进在此扩展,不改 core
839
+ };
840
+ var PROJECT_DIR = process.env.PROJECT_DIR || process.cwd();
841
+ var MAGIC_DIR = process.env.MAGIC_DIR || ".codex";
842
+ var guard = new CodexDocFormatGuard(PROJECT_DIR, MAGIC_DIR);
843
+ var raw = "";
844
+ try {
845
+ raw = readFileSync3(0, "utf-8");
846
+ } catch {
847
+ raw = "";
848
+ }
849
+ process.exitCode = guard.run(raw);