@phuc1403/musketeer 0.4.0 → 0.6.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.
Files changed (92) hide show
  1. package/INSTALLATION.md +47 -32
  2. package/README.md +49 -49
  3. package/bin/musketeer.js +168 -79
  4. package/manifest.json +303 -292
  5. package/package.json +46 -46
  6. package/src/dotnet-scaffold-copier.js +72 -72
  7. package/src/provisioner/detect.js +113 -93
  8. package/src/self-update.js +77 -73
  9. package/template/.claude/hooks/block-unsafe-adr-title.cjs +72 -70
  10. package/template/.claude/hooks/init-adr-dir.cjs +153 -110
  11. package/template/.claude/hooks/inject-adr-env.cjs +83 -82
  12. package/template/.claude/hooks/lib/adr/command-scan.cjs +107 -0
  13. package/template/.claude/hooks/lib/characteristics/checker.cjs +357 -0
  14. package/template/.claude/hooks/sync-adr-toc.cjs +111 -111
  15. package/template/.claude/hooks/validate-characteristics-hook.cjs +66 -0
  16. package/template/.claude/skills/architecture-characteristic-writer/SKILL.md +197 -99
  17. package/template/.claude/skills/architecture-characteristic-writer/assets/worksheet-template.md +18 -29
  18. package/template/.claude/skills/architecture-characteristic-writer/references/characteristics-catalog.md +22 -88
  19. package/template/.claude/skills/architecture-characteristic-writer/scripts/ranking-table.cjs +171 -0
  20. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.AssemblyInfo.cs +1 -1
  21. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.AssemblyInfoInputs.cache +1 -1
  22. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.AssemblyInfo.cs +1 -1
  23. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.AssemblyInfoInputs.cache +1 -1
  24. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.AssemblyInfo.cs +1 -1
  25. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.AssemblyInfoInputs.cache +1 -1
  26. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.AssemblyInfo.cs +1 -1
  27. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.AssemblyInfoInputs.cache +1 -1
  28. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.AssemblyInfo.cs +1 -1
  29. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.AssemblyInfoInputs.cache +1 -1
  30. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.AssemblyInfo.cs +1 -1
  31. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.AssemblyInfoInputs.cache +1 -1
  32. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.AssemblyInfo.cs +1 -1
  33. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.AssemblyInfoInputs.cache +1 -1
  34. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.AssemblyInfo.cs +1 -1
  35. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.AssemblyInfoInputs.cache +1 -1
  36. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.assets.cache +0 -0
  37. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.dgspec.json +0 -1536
  38. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.g.props +0 -16
  39. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.g.targets +0 -2
  40. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/project.assets.json +0 -559
  41. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/project.nuget.cache +0 -8
  42. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.assets.cache +0 -0
  43. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.dgspec.json +0 -690
  44. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.g.props +0 -16
  45. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.g.targets +0 -2
  46. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/project.assets.json +0 -376
  47. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/project.nuget.cache +0 -8
  48. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.assets.cache +0 -0
  49. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.dgspec.json +0 -347
  50. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.g.props +0 -16
  51. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.g.targets +0 -2
  52. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/project.assets.json +0 -353
  53. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/project.nuget.cache +0 -8
  54. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.assets.cache +0 -0
  55. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.csproj.AssemblyReference.cache +0 -0
  56. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.dgspec.json +0 -1047
  57. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.g.props +0 -16
  58. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.g.targets +0 -7
  59. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/project.assets.json +0 -858
  60. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/project.nuget.cache +0 -17
  61. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Api.Tests +0 -0
  62. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.assets.cache +0 -0
  63. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.csproj.AssemblyReference.cache +0 -0
  64. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.dgspec.json +0 -1908
  65. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.g.props +0 -27
  66. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.g.targets +0 -16
  67. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/project.assets.json +0 -3283
  68. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/project.nuget.cache +0 -60
  69. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Application.Tests +0 -0
  70. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.assets.cache +0 -0
  71. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.csproj.AssemblyReference.cache +0 -0
  72. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.dgspec.json +0 -1055
  73. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.g.props +0 -26
  74. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.g.targets +0 -11
  75. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/project.assets.json +0 -1662
  76. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/project.nuget.cache +0 -30
  77. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Domain.Tests +0 -0
  78. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.assets.cache +0 -0
  79. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.csproj.AssemblyReference.cache +0 -0
  80. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.dgspec.json +0 -712
  81. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.g.props +0 -26
  82. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.g.targets +0 -11
  83. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/project.assets.json +0 -1644
  84. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/project.nuget.cache +0 -30
  85. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Infrastructure.Tests +0 -0
  86. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.assets.cache +0 -0
  87. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.csproj.AssemblyReference.cache +0 -0
  88. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.dgspec.json +0 -1412
  89. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.g.props +0 -26
  90. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.g.targets +0 -13
  91. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/project.assets.json +0 -2130
  92. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/project.nuget.cache +0 -38
@@ -1,70 +1,72 @@
1
- #!/usr/bin/env node
2
- // PreToolUse guard: block an `adr new` title that adr-tools' own script cannot
3
- // handle safely.
4
- //
5
- // adr-new builds the ADR file with `sed -e "s|TITLE|$title|"`, substituting the
6
- // title in unescaped and using `|` as the delimiter. Two failure modes follow,
7
- // verified directly against /usr/bin/adr-new:
8
- // - `|` in the title breaks sed's own syntax: it exits 1, but only after
9
- // creating a zero-byte ADR file that already burned that number.
10
- // - `&` in a sed replacement means "the whole match" — it does NOT error.
11
- // "Use Kafka & Queues" silently becomes the title "Use Kafka TITLE Queues",
12
- // written straight into the file and the generated index.
13
- // A title starting with `-` is a third case: adr-new parses its own flags with
14
- // getopts, so without a `--` separator the title is read as an unknown option
15
- // and the ADR is silently never created (clean failure, still worth catching
16
- // early with a clearer reason than adr-tools' own "no title given").
17
- //
18
- // Blocking, not warning: `|` wastes an ADR number and `&` corrupts data with no
19
- // error at all, so there is nothing later in the pipeline that will catch it.
20
-
21
- let raw = "";
22
- process.stdin.on("data", (chunk) => (raw += chunk));
23
- process.stdin.on("end", () => {
24
- let input;
25
- try {
26
- input = JSON.parse(raw || "{}");
27
- } catch {
28
- process.exit(0); // unparseable payload — fail open, don't block legit work
29
- }
30
-
31
- const command = (input && input.tool_input && input.tool_input.command) || "";
32
- if (!/\badr\s+new\b/.test(command)) process.exit(0);
33
-
34
- // The skill always places `--` immediately before a quoted title. Extract
35
- // that argument; anything else about the command's shape is not this hook's
36
- // concern.
37
- const afterDashDash = command.match(/--\s+(["'])((?:(?!\1).)*)\1/);
38
- const title = afterDashDash ? afterDashDash[2] : null;
39
-
40
- let reason = null;
41
- if (!afterDashDash) {
42
- reason =
43
- "no `--` before the title (or no quoted title found after it). Without `--`, " +
44
- "adr-new's getopts can read a title starting with `-` as a flag and silently " +
45
- "create nothing. Always: adr new [-s STEM]... -- \"Title\".";
46
- } else if (title.includes("|")) {
47
- reason =
48
- "the title contains `|`. adr-new substitutes it into `sed -e \"s|TITLE|$title|\"`, " +
49
- "using `|` as sed's own delimiter — this breaks sed's syntax and exits non-zero, " +
50
- "but only after creating a zero-byte ADR file that already consumed that number.";
51
- } else if (title.includes("&")) {
52
- reason =
53
- "the title contains `&`. In a sed replacement `&` means \"the whole match\", so " +
54
- "adr-new writes it as literal text into the title (e.g. \"Use Kafka & Queues\" " +
55
- "becomes \"Use Kafka TITLE Queues\") and exits 0 — nothing else will catch this.";
56
- }
57
-
58
- if (reason) {
59
- process.stdout.write(
60
- JSON.stringify({
61
- hookSpecificOutput: {
62
- hookEventName: "PreToolUse",
63
- permissionDecision: "deny",
64
- permissionDecisionReason: `Refusing this \`adr new\` call: ${reason} Rename the title and retry.`,
65
- },
66
- })
67
- );
68
- }
69
- process.exit(0);
70
- });
1
+ #!/usr/bin/env node
2
+ // PreToolUse guard: block an `adr new` title that adr-tools' own script cannot
3
+ // handle safely.
4
+ //
5
+ // adr-new builds the ADR file with `sed -e "s|TITLE|$title|"`, substituting the
6
+ // title in unescaped and using `|` as the delimiter. Two failure modes follow,
7
+ // verified directly against /usr/bin/adr-new:
8
+ // - `|` in the title breaks sed's own syntax: it exits 1, but only after
9
+ // creating a zero-byte ADR file that already burned that number.
10
+ // - `&` in a sed replacement means "the whole match" — it does NOT error.
11
+ // "Use Kafka & Queues" silently becomes the title "Use Kafka TITLE Queues",
12
+ // written straight into the file and the generated index.
13
+ // A title starting with `-` is a third case: adr-new parses its own flags with
14
+ // getopts, so without a `--` separator the title is read as an unknown option
15
+ // and the ADR is silently never created (clean failure, still worth catching
16
+ // early with a clearer reason than adr-tools' own "no title given").
17
+ //
18
+ // Blocking, not warning: `|` wastes an ADR number and `&` corrupts data with no
19
+ // error at all, so there is nothing later in the pipeline that will catch it.
20
+
21
+ const { invokesAdr } = require("./lib/adr/command-scan.cjs");
22
+
23
+ let raw = "";
24
+ process.stdin.on("data", (chunk) => (raw += chunk));
25
+ process.stdin.on("end", () => {
26
+ let input;
27
+ try {
28
+ input = JSON.parse(raw || "{}");
29
+ } catch {
30
+ process.exit(0); // unparseable payload — fail open, don't block legit work
31
+ }
32
+
33
+ const command = (input && input.tool_input && input.tool_input.command) || "";
34
+ if (!invokesAdr(command, "new")) process.exit(0);
35
+
36
+ // The skill always places `--` immediately before a quoted title. Extract
37
+ // that argument; anything else about the command's shape is not this hook's
38
+ // concern.
39
+ const afterDashDash = command.match(/--\s+(["'])((?:(?!\1).)*)\1/);
40
+ const title = afterDashDash ? afterDashDash[2] : null;
41
+
42
+ let reason = null;
43
+ if (!afterDashDash) {
44
+ reason =
45
+ "no `--` before the title (or no quoted title found after it). Without `--`, " +
46
+ "adr-new's getopts can read a title starting with `-` as a flag and silently " +
47
+ "create nothing. Always: adr new [-s STEM]... -- \"Title\".";
48
+ } else if (title.includes("|")) {
49
+ reason =
50
+ "the title contains `|`. adr-new substitutes it into `sed -e \"s|TITLE|$title|\"`, " +
51
+ "using `|` as sed's own delimiter — this breaks sed's syntax and exits non-zero, " +
52
+ "but only after creating a zero-byte ADR file that already consumed that number.";
53
+ } else if (title.includes("&")) {
54
+ reason =
55
+ "the title contains `&`. In a sed replacement `&` means \"the whole match\", so " +
56
+ "adr-new writes it as literal text into the title (e.g. \"Use Kafka & Queues\" " +
57
+ "becomes \"Use Kafka TITLE Queues\") and exits 0 — nothing else will catch this.";
58
+ }
59
+
60
+ if (reason) {
61
+ process.stdout.write(
62
+ JSON.stringify({
63
+ hookSpecificOutput: {
64
+ hookEventName: "PreToolUse",
65
+ permissionDecision: "deny",
66
+ permissionDecisionReason: `Refusing this \`adr new\` call: ${reason} Rename the title and retry.`,
67
+ },
68
+ })
69
+ );
70
+ }
71
+ process.exit(0);
72
+ });
@@ -1,110 +1,153 @@
1
- #!/usr/bin/env node
2
- // PreToolUse guard: set up `.adr-dir` before the first `adr new` in a project,
3
- // so the agent never has to pick between `adr init` and a hand-written file.
4
- //
5
- // Two setup paths, and picking the wrong one is destructive:
6
- // - fresh (no `.adr-dir`, no numbered ADRs yet): `adr init docs/adr` is safe —
7
- // it creates docs/adr/, writes `.adr-dir`, and adds a baseline ADR.
8
- // - migration (numbered ADRs already exist, but no `.adr-dir`): `adr init`
9
- // would ALSO add that baseline ADR, burning the next real number. The fix
10
- // is a plain `.adr-dir` file with no tool call: `printf 'docs/adr\n' > .adr-dir`.
11
- // Already initialized: nothing to do, both paths are a no-op.
12
- //
13
- // Runs the setup itself (as a side effect) before allowing `adr new` through,
14
- // and denies a *direct* `adr init` call whenever running it would be wrong —
15
- // already initialized, or migration state — since either would burn a number.
16
- // Anything it does is echoed to stdout so it stays visible in the transcript.
17
-
18
- const fs = require('fs');
19
- const path = require('path');
20
- const { spawnSync } = require('child_process');
21
-
22
- const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
23
- const ADR_DIR = 'docs/adr';
24
- const ADR_DIR_FILE = path.join(root, '.adr-dir');
25
- const ADR_FILE = /^\d+-.*\.md$/;
26
-
27
- function allow(message) {
28
- if (message) process.stdout.write(message + '\n');
29
- process.exit(0);
30
- }
31
-
32
- function deny(reason) {
33
- process.stdout.write(
34
- JSON.stringify({
35
- hookSpecificOutput: {
36
- hookEventName: 'PreToolUse',
37
- permissionDecision: 'deny',
38
- permissionDecisionReason: reason,
39
- },
40
- })
41
- );
42
- process.exit(0);
43
- }
44
-
45
- function isInitialized() {
46
- try {
47
- return fs.readFileSync(ADR_DIR_FILE, 'utf8').trim() !== '';
48
- } catch {
49
- return false;
50
- }
51
- }
52
-
53
- function hasNumberedAdrs() {
54
- try {
55
- return fs.readdirSync(path.join(root, ADR_DIR)).some((f) => ADR_FILE.test(f));
56
- } catch {
57
- return false; // directory doesn't exist yet -> nothing to migrate
58
- }
59
- }
60
-
61
- function runAdrInit() {
62
- const shell = process.env.CLAUDE_CODE_GIT_BASH_PATH || 'bash';
63
- return spawnSync(shell, ['-c', `adr init ${ADR_DIR}`], { cwd: root, encoding: 'utf8' });
64
- }
65
-
66
- function main() {
67
- const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
68
- const command = payload?.tool_input?.command || '';
69
- const isInit = /\badr\s+init\b/.test(command);
70
- const isNew = /\badr\s+new\b/.test(command);
71
- if (!isInit && !isNew) return allow();
72
-
73
- if (isInitialized()) {
74
- if (isInit) {
75
- return deny(
76
- '.adr-dir already exists — do not run `adr init` again, it would add a duplicate ' +
77
- 'baseline ADR and burn the next number. Run `adr new` directly.'
78
- );
79
- }
80
- return allow(); // adr new, already set up: nothing to do
81
- }
82
-
83
- if (hasNumberedAdrs()) {
84
- // Migration: numbered ADRs exist but `.adr-dir` does not. `adr init` would
85
- // still add a baseline ADR here, so it is never the right command.
86
- if (isInit) {
87
- return deny(
88
- 'Numbered ADRs already exist without `.adr-dir` — this is a migration, not a fresh ' +
89
- 'project. `adr init` would add a duplicate baseline ADR and burn the next number. ' +
90
- 'Run `adr new` directly; `.adr-dir` is set up automatically.'
91
- );
92
- }
93
- fs.writeFileSync(ADR_DIR_FILE, `${ADR_DIR}\n`);
94
- return allow(`Migration detected: wrote .adr-dir (${ADR_DIR}) by hand, no baseline ADR added.`);
95
- }
96
-
97
- // Fresh project.
98
- if (isInit) return allow(); // the agent's own `adr init` call is correct here — let it run
99
- const res = runAdrInit();
100
- if (res.error || res.status !== 0) {
101
- return allow(`Could not run \`adr init ${ADR_DIR}\` (${res.error?.message || res.stderr}); letting the original command run and fail with its own error.`);
102
- }
103
- return allow(`Fresh project: ran \`adr init ${ADR_DIR}\` (creates the baseline ADR).`);
104
- }
105
-
106
- try {
107
- main();
108
- } catch {
109
- allow(); // fail open
110
- }
1
+ #!/usr/bin/env node
2
+ // PreToolUse guard: set up `.adr-dir` before the first `adr new` in a project,
3
+ // so the agent never has to pick between `adr init` and a hand-written file.
4
+ //
5
+ // Two setup paths, and picking the wrong one is destructive:
6
+ // - fresh (no `.adr-dir`, no numbered ADRs yet): `adr init docs/adr` is safe —
7
+ // it creates docs/adr/, writes `.adr-dir`, and adds a baseline ADR.
8
+ // - migration (numbered ADRs already exist, but no `.adr-dir`): `adr init`
9
+ // would ALSO add that baseline ADR, burning the next real number. The fix
10
+ // is a plain `.adr-dir` file with no tool call: `printf 'docs/adr\n' > .adr-dir`.
11
+ // Already initialized: nothing to do, both paths are a no-op.
12
+ //
13
+ // Runs the setup itself (as a side effect) before allowing `adr new` through,
14
+ // and denies a *direct* `adr init` call whenever running it would be wrong —
15
+ // already initialized, or migration state — since either would burn a number.
16
+ // Anything it does is echoed to stdout so it stays visible in the transcript.
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { spawnSync } = require('child_process');
21
+ const { invokesAdr } = require('./lib/adr/command-scan.cjs');
22
+
23
+ const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
24
+ const ADR_DIR = 'docs/adr';
25
+ const ADR_DIR_FILE = path.join(root, '.adr-dir');
26
+ const ADR_FILE = /^\d+-.*\.md$/;
27
+
28
+ function allow(message) {
29
+ if (message) process.stdout.write(message + '\n');
30
+ process.exit(0);
31
+ }
32
+
33
+ function deny(reason) {
34
+ process.stdout.write(
35
+ JSON.stringify({
36
+ hookSpecificOutput: {
37
+ hookEventName: 'PreToolUse',
38
+ permissionDecision: 'deny',
39
+ permissionDecisionReason: reason,
40
+ },
41
+ })
42
+ );
43
+ process.exit(0);
44
+ }
45
+
46
+ function readAdrDir() {
47
+ try {
48
+ return fs.readFileSync(ADR_DIR_FILE, 'utf8').trim() || null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function isInitialized() {
55
+ try {
56
+ return fs.readFileSync(ADR_DIR_FILE, 'utf8').trim() !== '';
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ function hasNumberedAdrs() {
63
+ try {
64
+ return fs.readdirSync(path.join(root, ADR_DIR)).some((f) => ADR_FILE.test(f));
65
+ } catch {
66
+ return false; // directory doesn't exist yet -> nothing to migrate
67
+ }
68
+ }
69
+
70
+ function runAdrInit() {
71
+ const shell = process.env.CLAUDE_CODE_GIT_BASH_PATH || 'bash';
72
+ return spawnSync(shell, ['-c', `adr init ${ADR_DIR}`], { cwd: root, encoding: 'utf8' });
73
+ }
74
+
75
+ // Where adr-tools itself thinks the ADRs go. `_adr_dir` is the resolver every
76
+ // adr subcommand uses, so asking it directly is the only honest answer.
77
+ function resolvedAdrDir() {
78
+ const shell = process.env.CLAUDE_CODE_GIT_BASH_PATH || 'bash';
79
+ const res = spawnSync(shell, ['-c', '_adr_dir'], { cwd: root, encoding: 'utf8' });
80
+ if (res.error || res.status !== 0) return null;
81
+ const out = (res.stdout || '').trim();
82
+ return out || null;
83
+ }
84
+
85
+ // adr-tools resolves the directory with an unquoted `[` test, so a path holding
86
+ // a space splits into too many arguments, the search loop is skipped, and the
87
+ // script falls through to its `doc/adr` default. It prints the wrong answer on
88
+ // stdout and exits 0, so the ADR lands in a directory nothing else looks at and
89
+ // nothing reports a problem. Refuse the call instead of losing the record.
90
+ function denyOnDirMismatch() {
91
+ const declared = readAdrDir();
92
+ const resolved = resolvedAdrDir();
93
+ if (!declared || !resolved || declared === resolved) return;
94
+
95
+ deny(
96
+ `adr-tools resolves the ADR directory to "${resolved}", but .adr-dir says "${declared}". ` +
97
+ `The new ADR would be written to "${resolved}", where the index hook never looks. ` +
98
+ (/\s/.test(root)
99
+ ? `This project's path contains a space (${root}), which is the known cause: ` +
100
+ "adr-tools' `_adr_dir` leaves its path test unquoted. "
101
+ : '') +
102
+ 'Fix the quoting in adr-tools\' `_adr_dir`, or move the project to a path with no spaces.'
103
+ );
104
+ }
105
+
106
+ function main() {
107
+ const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
108
+ const command = payload?.tool_input?.command || '';
109
+ const isInit = invokesAdr(command, 'init');
110
+ const isNew = invokesAdr(command, 'new');
111
+ if (!isInit && !isNew) return allow();
112
+
113
+ if (isInitialized()) {
114
+ if (isInit) {
115
+ return deny(
116
+ '.adr-dir already exists — do not run `adr init` again, it would add a duplicate ' +
117
+ 'baseline ADR and burn the next number. Run `adr new` directly.'
118
+ );
119
+ }
120
+ denyOnDirMismatch(); // exits when adr-tools would write somewhere else
121
+ return allow(); // adr new, already set up: nothing to do
122
+ }
123
+
124
+ if (hasNumberedAdrs()) {
125
+ // Migration: numbered ADRs exist but `.adr-dir` does not. `adr init` would
126
+ // still add a baseline ADR here, so it is never the right command.
127
+ if (isInit) {
128
+ return deny(
129
+ 'Numbered ADRs already exist without `.adr-dir` — this is a migration, not a fresh ' +
130
+ 'project. `adr init` would add a duplicate baseline ADR and burn the next number. ' +
131
+ 'Run `adr new` directly; `.adr-dir` is set up automatically.'
132
+ );
133
+ }
134
+ fs.writeFileSync(ADR_DIR_FILE, `${ADR_DIR}\n`);
135
+ denyOnDirMismatch();
136
+ return allow(`Migration detected: wrote .adr-dir (${ADR_DIR}) by hand, no baseline ADR added.`);
137
+ }
138
+
139
+ // Fresh project.
140
+ if (isInit) return allow(); // the agent's own `adr init` call is correct here — let it run
141
+ const res = runAdrInit();
142
+ if (res.error || res.status !== 0) {
143
+ return allow(`Could not run \`adr init ${ADR_DIR}\` (${res.error?.message || res.stderr}); letting the original command run and fail with its own error.`);
144
+ }
145
+ denyOnDirMismatch();
146
+ return allow(`Fresh project: ran \`adr init ${ADR_DIR}\` (creates the baseline ADR).`);
147
+ }
148
+
149
+ try {
150
+ main();
151
+ } catch {
152
+ allow(); // fail open
153
+ }
@@ -1,82 +1,83 @@
1
- #!/usr/bin/env node
2
- // PreToolUse guard: inject the env vars every `adr new` call needs, so the
3
- // agent can write a bare `adr new -- "Title"` and get a correct command.
4
- //
5
- // VISUAL=true EDITOR=true — without them an ambient EDITOR opens an
6
- // interactive editor and hangs the session (adr-new always shells out to
7
- // $VISUAL/$EDITOR to "edit" the new file).
8
- // ADR_TEMPLATE=...adr-template.md — points adr-new at this skill's template
9
- // instead of adr-tools' own bundled default.
10
- //
11
- // Uses PreToolUse's `updatedInput` (not just allow/deny) to rewrite the
12
- // command before it runs. Only fills in vars that are missing — an assignment
13
- // the agent already wrote (any value, even a deliberately different template)
14
- // is left alone. Silent no-op if all three are already present.
15
- //
16
- // Safety: a title that happens to contain the literal text "adr new" could
17
- // make the matching regex fire inside a quoted string. Guarded by counting
18
- // quote characters before each match — an odd count means "inside an open
19
- // string", and that occurrence is left untouched rather than risk corrupting
20
- // the title.
21
-
22
- const fs = require('fs');
23
-
24
- const DEFAULTS = [
25
- ['ADR_TEMPLATE', '.claude/skills/adr-writer/references/adr-template.md'],
26
- ['VISUAL', 'true'],
27
- ['EDITOR', 'true'],
28
- ];
29
-
30
- // boundary/start, leading whitespace, any existing KEY=value prefix, `adr new`.
31
- const ADR_NEW_RE = /(^|[;&|\n])([ \t]*)((?:[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S*)[ \t]+)*)(adr[ \t]+new\b)/g;
32
-
33
- function existingVars(prefix) {
34
- const names = new Set();
35
- const re = /([A-Za-z_][A-Za-z0-9_]*)=/g;
36
- let m;
37
- while ((m = re.exec(prefix))) names.add(m[1]);
38
- return names;
39
- }
40
-
41
- function isInsideQuotes(command, index) {
42
- const before = command.slice(0, index);
43
- const dq = (before.match(/"/g) || []).length;
44
- const sq = (before.match(/'/g) || []).length;
45
- return dq % 2 === 1 || sq % 2 === 1;
46
- }
47
-
48
- function main() {
49
- const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
50
- const command = payload?.tool_input?.command || '';
51
- if (!/\badr\s+new\b/.test(command)) return;
52
-
53
- let changed = false;
54
- const updated = command.replace(ADR_NEW_RE, (whole, boundary, ws, existingPrefix, adrNew, offset) => {
55
- if (isInsideQuotes(command, offset)) return whole;
56
- const present = existingVars(existingPrefix);
57
- const missing = DEFAULTS.filter(([name]) => !present.has(name));
58
- if (missing.length === 0) return whole;
59
- changed = true;
60
- const injected = missing.map(([name, value]) => `${name}=${value}`).join(' ');
61
- return `${boundary}${ws}${injected} ${existingPrefix}${adrNew}`;
62
- });
63
- if (!changed) return;
64
-
65
- process.stdout.write(
66
- JSON.stringify({
67
- systemMessage: 'Added missing VISUAL/EDITOR/ADR_TEMPLATE env vars before `adr new`.',
68
- hookSpecificOutput: {
69
- hookEventName: 'PreToolUse',
70
- permissionDecision: 'allow',
71
- updatedInput: { command: updated },
72
- },
73
- })
74
- );
75
- }
76
-
77
- try {
78
- main();
79
- } catch {
80
- /* fail open: leave the command untouched */
81
- }
82
- process.exit(0);
1
+ #!/usr/bin/env node
2
+ // PreToolUse guard: inject the env vars every `adr new` call needs, so the
3
+ // agent can write a bare `adr new -- "Title"` and get a correct command.
4
+ //
5
+ // VISUAL=true EDITOR=true — without them an ambient EDITOR opens an
6
+ // interactive editor and hangs the session (adr-new always shells out to
7
+ // $VISUAL/$EDITOR to "edit" the new file).
8
+ // ADR_TEMPLATE=...adr-template.md — points adr-new at this skill's template
9
+ // instead of adr-tools' own bundled default.
10
+ //
11
+ // Uses PreToolUse's `updatedInput` (not just allow/deny) to rewrite the
12
+ // command before it runs. Only fills in vars that are missing — an assignment
13
+ // the agent already wrote (any value, even a deliberately different template)
14
+ // is left alone. Silent no-op if all three are already present.
15
+ //
16
+ // Safety: a title that happens to contain the literal text "adr new" could
17
+ // make the matching regex fire inside a quoted string. Guarded by counting
18
+ // quote characters before each match — an odd count means "inside an open
19
+ // string", and that occurrence is left untouched rather than risk corrupting
20
+ // the title.
21
+
22
+ const fs = require('fs');
23
+ const {
24
+ invocationRe,
25
+ isInsideQuotes,
26
+ isInsideHeredoc,
27
+ invokesAdr,
28
+ } = require('./lib/adr/command-scan.cjs');
29
+
30
+ const DEFAULTS = [
31
+ ['ADR_TEMPLATE', '.claude/skills/adr-writer/references/adr-template.md'],
32
+ ['VISUAL', 'true'],
33
+ ['EDITOR', 'true'],
34
+ ];
35
+
36
+ function existingVars(prefix) {
37
+ const names = new Set();
38
+ const re = /([A-Za-z_][A-Za-z0-9_]*)=/g;
39
+ let m;
40
+ while ((m = re.exec(prefix))) names.add(m[1]);
41
+ return names;
42
+ }
43
+
44
+ function main() {
45
+ const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
46
+ const command = payload?.tool_input?.command || '';
47
+ if (!invokesAdr(command, 'new')) return;
48
+
49
+ let changed = false;
50
+ const updated = command.replace(invocationRe('new'), (whole, boundary, ws, existingPrefix, adrNew, offset) => {
51
+ // Both guards, for the same reason the deny hooks use them: a heredoc body
52
+ // is documentation being written, and injecting env vars into it would
53
+ // rewrite the file's contents rather than the command.
54
+ // `offset` is the boundary character; step past it to the word `adr`.
55
+ const at = offset + boundary.length + ws.length + existingPrefix.length;
56
+ if (isInsideQuotes(command, at) || isInsideHeredoc(command, at)) return whole;
57
+ const present = existingVars(existingPrefix);
58
+ const missing = DEFAULTS.filter(([name]) => !present.has(name));
59
+ if (missing.length === 0) return whole;
60
+ changed = true;
61
+ const injected = missing.map(([name, value]) => `${name}=${value}`).join(' ');
62
+ return `${boundary}${ws}${injected} ${existingPrefix}${adrNew}`;
63
+ });
64
+ if (!changed) return;
65
+
66
+ process.stdout.write(
67
+ JSON.stringify({
68
+ systemMessage: 'Added missing VISUAL/EDITOR/ADR_TEMPLATE env vars before `adr new`.',
69
+ hookSpecificOutput: {
70
+ hookEventName: 'PreToolUse',
71
+ permissionDecision: 'allow',
72
+ updatedInput: { command: updated },
73
+ },
74
+ })
75
+ );
76
+ }
77
+
78
+ try {
79
+ main();
80
+ } catch {
81
+ /* fail open: leave the command untouched */
82
+ }
83
+ process.exit(0);