@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.
- package/INSTALLATION.md +47 -32
- package/README.md +49 -49
- package/bin/musketeer.js +168 -79
- package/manifest.json +303 -292
- package/package.json +46 -46
- package/src/dotnet-scaffold-copier.js +72 -72
- package/src/provisioner/detect.js +113 -93
- package/src/self-update.js +77 -73
- package/template/.claude/hooks/block-unsafe-adr-title.cjs +72 -70
- package/template/.claude/hooks/init-adr-dir.cjs +153 -110
- package/template/.claude/hooks/inject-adr-env.cjs +83 -82
- package/template/.claude/hooks/lib/adr/command-scan.cjs +107 -0
- package/template/.claude/hooks/lib/characteristics/checker.cjs +357 -0
- package/template/.claude/hooks/sync-adr-toc.cjs +111 -111
- package/template/.claude/hooks/validate-characteristics-hook.cjs +66 -0
- package/template/.claude/skills/architecture-characteristic-writer/SKILL.md +197 -99
- package/template/.claude/skills/architecture-characteristic-writer/assets/worksheet-template.md +18 -29
- package/template/.claude/skills/architecture-characteristic-writer/references/characteristics-catalog.md +22 -88
- package/template/.claude/skills/architecture-characteristic-writer/scripts/ranking-table.cjs +171 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.assets.cache +0 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.dgspec.json +0 -1536
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.g.props +0 -16
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.g.targets +0 -2
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/project.assets.json +0 -559
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/project.nuget.cache +0 -8
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.assets.cache +0 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.dgspec.json +0 -690
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.g.props +0 -16
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.g.targets +0 -2
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/project.assets.json +0 -376
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/project.nuget.cache +0 -8
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.assets.cache +0 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.dgspec.json +0 -347
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.g.props +0 -16
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.g.targets +0 -2
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/project.assets.json +0 -353
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/project.nuget.cache +0 -8
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.assets.cache +0 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.csproj.AssemblyReference.cache +0 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.dgspec.json +0 -1047
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.g.props +0 -16
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.g.targets +0 -7
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/project.assets.json +0 -858
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/project.nuget.cache +0 -17
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Api.Tests +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.assets.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.csproj.AssemblyReference.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.dgspec.json +0 -1908
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.g.props +0 -27
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.g.targets +0 -16
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/project.assets.json +0 -3283
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/project.nuget.cache +0 -60
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Application.Tests +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.assets.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.csproj.AssemblyReference.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.dgspec.json +0 -1055
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.g.props +0 -26
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.g.targets +0 -11
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/project.assets.json +0 -1662
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/project.nuget.cache +0 -30
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Domain.Tests +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.assets.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.csproj.AssemblyReference.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.dgspec.json +0 -712
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.g.props +0 -26
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.g.targets +0 -11
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/project.assets.json +0 -1644
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/project.nuget.cache +0 -30
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Infrastructure.Tests +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.assets.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.csproj.AssemblyReference.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.dgspec.json +0 -1412
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.g.props +0 -26
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.g.targets +0 -13
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/project.assets.json +0 -2130
- 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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
process.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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);
|