archstrict 0.0.0 → 0.2.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/.agents/hooks/hooks.json +29 -0
- package/.agents/hooks/post-tool-use.mjs +107 -0
- package/.agents/hooks/pre-tool-use.mjs +182 -0
- package/.agents/mcp/server.mjs +71 -0
- package/.agents/plugin.json +19 -0
- package/AGENTS.md +81 -0
- package/CHANGELOG.md +77 -0
- package/README.ja.md +142 -0
- package/README.md +143 -2
- package/dist/augmentation-cache.js +65 -0
- package/dist/check-options.js +40 -0
- package/dist/classify.js +148 -0
- package/dist/cli.js +243 -0
- package/dist/config-pointer.js +251 -0
- package/dist/config.js +194 -0
- package/dist/edge-cache.js +530 -0
- package/dist/gitignore.js +271 -0
- package/dist/mcp-server.js +111 -0
- package/dist/module-candidates.js +125 -0
- package/dist/module-graph.js +2179 -0
- package/dist/project-path.js +59 -0
- package/dist/report-error.js +13 -0
- package/dist/rules/config-meaning.js +143 -0
- package/dist/rules/constraints.js +419 -0
- package/dist/rules/cycles.js +285 -0
- package/dist/rules/deprecated.js +67 -0
- package/dist/rules/empty-rule.js +101 -0
- package/dist/rules/moves.js +79 -0
- package/dist/rules/must-be-empty.js +52 -0
- package/dist/rules/public-surface.js +100 -0
- package/dist/rules/uncovered.js +75 -0
- package/dist/todo-migration.js +112 -0
- package/dist/todo-store.js +434 -0
- package/dist/type-closure.js +959 -0
- package/dist/type-leak.js +590 -0
- package/dist/verbs/agents.js +116 -0
- package/dist/verbs/check.js +1011 -0
- package/dist/verbs/fix.js +170 -0
- package/dist/verbs/hotspots.js +261 -0
- package/dist/verbs/init.js +538 -0
- package/dist/verbs/map-shape.js +78 -0
- package/dist/verbs/recommend.js +863 -0
- package/dist/verbs/rules.js +188 -0
- package/dist/verbs/search.js +109 -0
- package/dist/verbs/simulate.js +220 -0
- package/dist/verbs/todo.js +180 -0
- package/dist/warm-graph.js +82 -0
- package/docs/boundary-patterns.md +374 -0
- package/docs/calibrated-rules-design.md +124 -0
- package/docs/init-singleton-modules.md +133 -0
- package/docs/maintenance.md +109 -0
- package/docs/releasing.md +58 -0
- package/docs/rules-edge-cache.md +50 -0
- package/docs/todo-single-file-migration.md +58 -0
- package/llms.txt +25 -0
- package/package.json +61 -4
- package/skills/archstrict/SKILL.md +54 -0
- package/skills/archstrict/references/agents-verb.md +39 -0
- package/skills/archstrict/references/config.md +116 -0
- package/skills/archstrict/references/hook.md +57 -0
- package/skills/archstrict/references/path-rules.md +57 -0
- package/skills/archstrict/references/patterns.md +915 -0
- package/skills/archstrict/references/prove-rules.md +58 -0
- package/skills/archstrict/references/rearchitect.md +66 -0
- package/skills/archstrict/references/recommend.md +98 -0
- package/skills/archstrict/references/rules.md +149 -0
- package/skills/archstrict/references/simulate.md +109 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "archstrict - runs simulate before a file edit and check right after, returning any violation into the agent's own context",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "command -v node >/dev/null 2>&1 && exec node \"${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.mjs\" || exit 0",
|
|
11
|
+
"timeout": 30
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"PostToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "command -v node >/dev/null 2>&1 && exec node \"${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use.mjs\" || exit 0",
|
|
23
|
+
"timeout": 30
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Responsibility: the PostToolUse hook. Runs the edited project's own
|
|
3
|
+
// installed `archstrict check <file>` right after Edit/Write/MultiEdit and
|
|
4
|
+
// feeds any violation back into the agent's context via
|
|
5
|
+
// hookSpecificOutput.additionalContext - the same moment a human editor's
|
|
6
|
+
// red squiggly would appear, per code.claude.com/docs/en/hooks.
|
|
7
|
+
// Boundary: no rule logic here - this only shells out to the project's own
|
|
8
|
+
// `archstrict` binary and reshapes its JSON. It never runs this
|
|
9
|
+
// repository's own dist/cli.js: the hook ships to OTHER projects, each
|
|
10
|
+
// with its own installed archstrict and its own archstrict.config.ts.
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
function readStdin() {
|
|
16
|
+
return JSON.parse(readFileSync(0, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Every path writes exactly one line of JSON to stdout and exits 0 - a
|
|
20
|
+
// hook that fails loudly would block the edit it's only meant to comment
|
|
21
|
+
// on. `additionalContext` omitted (not merely empty) when there is
|
|
22
|
+
// nothing to say: an empty string is still a context entry Claude reads.
|
|
23
|
+
function emit(additionalContext) {
|
|
24
|
+
const output =
|
|
25
|
+
additionalContext === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext } };
|
|
28
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function main() {
|
|
32
|
+
let input;
|
|
33
|
+
try {
|
|
34
|
+
input = readStdin();
|
|
35
|
+
} catch {
|
|
36
|
+
emit();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const editTools = new Set(["Edit", "Write", "MultiEdit"]);
|
|
41
|
+
if (!editTools.has(input.tool_name)) {
|
|
42
|
+
emit();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const filePath = input.tool_input?.file_path;
|
|
47
|
+
if (typeof filePath !== "string" || !/\.(ts|tsx|mts|cts)$/.test(filePath)) {
|
|
48
|
+
emit();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The edited project's own cwd, not this plugin's own install
|
|
53
|
+
// location: check <file> resolves archstrict.config.ts from cwd, and a
|
|
54
|
+
// project checked with this hook is never this repository itself.
|
|
55
|
+
const cwd = typeof input.cwd === "string" ? input.cwd : process.cwd();
|
|
56
|
+
const binPath = join(cwd, "node_modules", ".bin", "archstrict");
|
|
57
|
+
if (!existsSync(binPath)) {
|
|
58
|
+
// Not an error: most edits happen in files or projects with no
|
|
59
|
+
// archstrict installed at all. Said once, not raised as a failure -
|
|
60
|
+
// the hook's job is to add context when there is real feedback to
|
|
61
|
+
// add, not to insist a project adopt this tool.
|
|
62
|
+
emit();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let stdout;
|
|
67
|
+
try {
|
|
68
|
+
stdout = execFileSync(binPath, ["check", filePath, "--json"], { cwd, encoding: "utf8" });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// check exits 1 exactly when violations exist (module-graph.ts's own
|
|
71
|
+
// documented contract) - that is real data on stderr's sibling
|
|
72
|
+
// stdout, not a hook failure. Only a stdout-less failure (the binary
|
|
73
|
+
// itself couldn't run) is reported as broken.
|
|
74
|
+
const execError = /** @type {{ stdout?: unknown; message: string }} */ (error);
|
|
75
|
+
if (typeof execError.stdout !== "string" || execError.stdout.length === 0) {
|
|
76
|
+
emit(`archstrict: check did not run (${execError.message}).`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
stdout = execError.stdout;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = JSON.parse(stdout);
|
|
83
|
+
// A config or missing-file error (a required field absent, an
|
|
84
|
+
// unsupported edges shape) reports as { error, do } instead of a
|
|
85
|
+
// real CheckResult, exit 1, no stdout-less failure - so the branch
|
|
86
|
+
// above never catches it. Surfaced, not silently ignored: the project
|
|
87
|
+
// has archstrict installed but something about its own setup is broken,
|
|
88
|
+
// which the agent editing it needs to know, the same as a real
|
|
89
|
+
// violation would. `do` is the command to run; older binaries that
|
|
90
|
+
// omit it still surface the error text.
|
|
91
|
+
if (typeof result.error === "string") {
|
|
92
|
+
const doText = typeof result.do === "string" && result.do.length > 0 ? `\ndo: ${result.do}` : "";
|
|
93
|
+
emit(`archstrict: check did not run (${result.error}).${doText}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (result.violations.length === 0) {
|
|
97
|
+
emit();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lines = result.violations.map(
|
|
102
|
+
(v) => `[${v.rule}] ${v.path}:${v.line}:${v.column}\n ${v.evidence}\n because: ${v.because}\n do: ${v.do}`,
|
|
103
|
+
);
|
|
104
|
+
emit(`archstrict found ${result.violations.length} violation(s) in ${filePath}:\n${lines.join("\n")}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main();
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Responsibility: the PreToolUse hook. Before an Edit/Write/MultiEdit
|
|
3
|
+
// actually touches disk, this builds the file text the tool call would
|
|
4
|
+
// produce and runs the edited project's own installed
|
|
5
|
+
// `archstrict simulate --json` against that one change, so the agent
|
|
6
|
+
// hears about a new violation before the write happens instead of after
|
|
7
|
+
// (the PostToolUse hook's own moment). Per code.claude.com/docs/en/hooks,
|
|
8
|
+
// PreToolUse returns its decision inside hookSpecificOutput:
|
|
9
|
+
// permissionDecision ("allow"/"deny"/"ask"/"defer") plus
|
|
10
|
+
// permissionDecisionReason, or additionalContext alongside "allow".
|
|
11
|
+
// Boundary: no rule logic here - this only shells out to the project's own
|
|
12
|
+
// `archstrict` binary and reshapes its JSON, mirroring post-tool-use.mjs.
|
|
13
|
+
// It never runs this repository's own dist/cli.js: the hook ships to OTHER
|
|
14
|
+
// projects, each with its own installed archstrict.
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
// 10s covers simulate's own measured cost (about twice `check <file>`,
|
|
20
|
+
// which reads the edge cache) on a real project; configurable because a
|
|
21
|
+
// large monorepo's cold run can exceed it. A hook that stalls the tool
|
|
22
|
+
// call for longer than the agent's patience defeats its own purpose.
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
24
|
+
const MAX_SHOWN_VIOLATIONS = 5;
|
|
25
|
+
|
|
26
|
+
function readStdin() {
|
|
27
|
+
return JSON.parse(readFileSync(0, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function emit(decision) {
|
|
31
|
+
const output =
|
|
32
|
+
decision === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: { hookSpecificOutput: { hookEventName: "PreToolUse", ...decision } };
|
|
35
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Applies one old_string -> new_string replacement the same way Edit and
|
|
39
|
+
// MultiEdit do. Returns undefined (not a thrown error) when old_string
|
|
40
|
+
// isn't found: the caller stays silent rather than guessing at the tool's
|
|
41
|
+
// own error text, since the tool itself will report that failure.
|
|
42
|
+
function applyReplacement(text, oldString, newString, replaceAll) {
|
|
43
|
+
if (typeof oldString !== "string" || typeof newString !== "string") return undefined;
|
|
44
|
+
if (!text.includes(oldString)) return undefined;
|
|
45
|
+
return replaceAll ? text.split(oldString).join(newString) : text.replace(oldString, newString);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Builds the file text the tool call would produce, without writing
|
|
49
|
+
// anything. Returns undefined when the input can't be built (a missing
|
|
50
|
+
// old_string, a file that doesn't exist yet for Edit/MultiEdit, or a
|
|
51
|
+
// malformed tool_input) - every such case stays silent per design, since
|
|
52
|
+
// the tool call itself will surface its own failure.
|
|
53
|
+
function proposedContent(toolName, toolInput, filePath) {
|
|
54
|
+
if (toolName === "Write") {
|
|
55
|
+
return typeof toolInput.content === "string" ? toolInput.content : undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!existsSync(filePath)) return undefined;
|
|
58
|
+
const current = readFileSync(filePath, "utf8");
|
|
59
|
+
if (toolName === "Edit") {
|
|
60
|
+
return applyReplacement(current, toolInput.old_string, toolInput.new_string, toolInput.replace_all === true);
|
|
61
|
+
}
|
|
62
|
+
if (toolName === "MultiEdit") {
|
|
63
|
+
if (!Array.isArray(toolInput.edits)) return undefined;
|
|
64
|
+
let text = current;
|
|
65
|
+
for (const edit of toolInput.edits) {
|
|
66
|
+
const next = applyReplacement(text, edit?.old_string, edit?.new_string, edit?.replace_all === true);
|
|
67
|
+
if (next === undefined) return undefined;
|
|
68
|
+
text = next;
|
|
69
|
+
}
|
|
70
|
+
return text;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Matches check.ts's own formatConfigPointerLines (`config-pointer.ts`'s
|
|
76
|
+
// ConfigPointers shape) - duplicated here, not imported, because this hook
|
|
77
|
+
// ships to other projects and only shells out to their installed
|
|
78
|
+
// archstrict; it never depends on this repository's own src.
|
|
79
|
+
function formatConfigPointerLines(config) {
|
|
80
|
+
const pointers = Array.isArray(config) ? config : [config];
|
|
81
|
+
return pointers.map(p => ` config: ${p.path}:${p.line}:${p.column} ${p.pointer} (${p.role})`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatViolation(v) {
|
|
85
|
+
return [
|
|
86
|
+
`[${v.rule}] ${v.path}:${v.line}:${v.column}`,
|
|
87
|
+
` ${v.evidence}`,
|
|
88
|
+
` because: ${v.because}`,
|
|
89
|
+
...formatConfigPointerLines(v.config),
|
|
90
|
+
` do: ${v.do}`,
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatAdded(added, filePath) {
|
|
95
|
+
const shown = added.slice(0, MAX_SHOWN_VIOLATIONS).map(formatViolation);
|
|
96
|
+
const rest = added.length - shown.length;
|
|
97
|
+
const restLine = rest > 0 ? `\n...and ${rest} more.` : "";
|
|
98
|
+
return `archstrict: this edit would add ${added.length} violation(s) in ${filePath}:\n${shown.join("\n")}${restLine}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function main() {
|
|
102
|
+
let input;
|
|
103
|
+
try {
|
|
104
|
+
input = readStdin();
|
|
105
|
+
} catch {
|
|
106
|
+
emit();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const editTools = new Set(["Edit", "Write", "MultiEdit"]);
|
|
111
|
+
if (!editTools.has(input.tool_name)) {
|
|
112
|
+
emit();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const toolInput = input.tool_input;
|
|
117
|
+
const filePath = toolInput?.file_path;
|
|
118
|
+
if (typeof filePath !== "string" || !/\.(ts|tsx|mts|cts)$/.test(filePath)) {
|
|
119
|
+
emit();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const content = proposedContent(input.tool_name, toolInput, filePath);
|
|
124
|
+
if (content === undefined) {
|
|
125
|
+
emit();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The edited project's own cwd, matching the PostToolUse hook's own
|
|
130
|
+
// resolution: never this plugin's own install location.
|
|
131
|
+
const cwd = typeof input.cwd === "string" ? input.cwd : process.cwd();
|
|
132
|
+
const binPath = join(cwd, "node_modules", ".bin", "archstrict");
|
|
133
|
+
if (!existsSync(binPath)) {
|
|
134
|
+
emit();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const timeoutMs = Number(process.env.ARCHSTRICT_PRETOOLUSE_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS;
|
|
139
|
+
const stdinBody = JSON.stringify({ changes: [{ path: filePath, content }] });
|
|
140
|
+
|
|
141
|
+
let stdout;
|
|
142
|
+
try {
|
|
143
|
+
stdout = execFileSync(binPath, ["simulate", "--json"], { cwd, encoding: "utf8", input: stdinBody, timeout: timeoutMs });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
// simulate exits 1 both when `added` is nonempty (real data, read from
|
|
146
|
+
// stdout below) and on an input/config error ({ error, do } json) - a
|
|
147
|
+
// timeout or a crash leaves no stdout at all. Unlike the PostToolUse
|
|
148
|
+
// hook, a config error here stays silent rather than being reported:
|
|
149
|
+
// this hook runs before every edit, so a broken project config would
|
|
150
|
+
// otherwise interrupt every tool call instead of the one edit that
|
|
151
|
+
// actually caused it.
|
|
152
|
+
const execError = error;
|
|
153
|
+
if (typeof execError.stdout !== "string" || execError.stdout.length === 0) {
|
|
154
|
+
emit();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
stdout = execError.stdout;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let result;
|
|
161
|
+
try {
|
|
162
|
+
result = JSON.parse(stdout);
|
|
163
|
+
} catch {
|
|
164
|
+
emit();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (typeof result.error === "string" || !Array.isArray(result.added) || result.added.length === 0) {
|
|
168
|
+
emit();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const resolvedLine = result.resolved?.length > 0 ? `\narchstrict: this edit would also resolve ${result.resolved.length} violation(s).` : "";
|
|
173
|
+
const text = `${formatAdded(result.added, filePath)}${resolvedLine}`;
|
|
174
|
+
|
|
175
|
+
if (process.env.ARCHSTRICT_PRETOOLUSE === "deny") {
|
|
176
|
+
emit({ permissionDecision: "deny", permissionDecisionReason: text });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
emit({ permissionDecision: "allow", additionalContext: text });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
main();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Responsibility: connect the consumer's installed server or an empty MCP fallback.
|
|
2
|
+
// Boundary: Node built-ins only; the installed package owns architecture queries.
|
|
3
|
+
// Plugin distribution supplies files without its own npm installation, so npm packages
|
|
4
|
+
// such as the SDK may not resolve here. Use built-ins until delegation reaches
|
|
5
|
+
// the consuming project's installed archstrict package, where its SDK dependency resolves.
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
// Match LATEST_PROTOCOL_VERSION in the pinned SDK 1.30.0. This dependency-free
|
|
11
|
+
// wrapper cannot import the SDK to compute the value dynamically.
|
|
12
|
+
const PROTOCOL_VERSION = "2025-11-25";
|
|
13
|
+
|
|
14
|
+
// Observations across many live Claude Code MCP processes consistently found both cwd
|
|
15
|
+
// and CLAUDE_PROJECT_DIR set to the session's project root. Prefer the explicit
|
|
16
|
+
// environment signal over a potentially inherited cwd; retain cwd as the fallback.
|
|
17
|
+
const root = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
18
|
+
const require = createRequire(join(root, "package.json"));
|
|
19
|
+
let mcpServerPath;
|
|
20
|
+
// createRequire anchors Node's synchronous resolution to the consumer. Its clean failure
|
|
21
|
+
// detects absence while respecting node_modules layouts, symlinks, and workspaces.
|
|
22
|
+
// An existsSync check on a guessed path would not follow those resolution rules.
|
|
23
|
+
try { mcpServerPath = require.resolve("archstrict/dist/mcp-server.js"); }
|
|
24
|
+
catch { mcpServerPath = undefined; }
|
|
25
|
+
|
|
26
|
+
let imported;
|
|
27
|
+
if (mcpServerPath !== undefined) {
|
|
28
|
+
// Import in this process to avoid a second Node process for each session.
|
|
29
|
+
// A child server would need a full MCP client proxy here to inspect its tools
|
|
30
|
+
// and provide a clean empty fallback. Import failures can reach that fallback directly.
|
|
31
|
+
try { imported = await import(pathToFileURL(mcpServerPath).href); }
|
|
32
|
+
catch { imported = undefined; }
|
|
33
|
+
}
|
|
34
|
+
// Older installations can resolve and import successfully yet export only createArchstrictMcpServer.
|
|
35
|
+
// Those successes do not establish this newer entry point. Check its shape before
|
|
36
|
+
// an unconditional call to a missing function would throw a TypeError.
|
|
37
|
+
if (typeof imported?.startArchstrictMcpServer === "function") {
|
|
38
|
+
await imported.startArchstrictMcpServer(root);
|
|
39
|
+
// Missing, broken, and incompatible installations all lack usable architecture queries.
|
|
40
|
+
// One stub gives clients the same connected, empty result for all three states;
|
|
41
|
+
// separate fallbacks would falsely suggest different degrees of usable connection.
|
|
42
|
+
} else {
|
|
43
|
+
let buffer = "";
|
|
44
|
+
process.stdin.setEncoding("utf8");
|
|
45
|
+
process.stdin.on("data", chunk => {
|
|
46
|
+
buffer += chunk;
|
|
47
|
+
let end;
|
|
48
|
+
while ((end = buffer.indexOf("\n")) !== -1) {
|
|
49
|
+
const line = buffer.slice(0, end);
|
|
50
|
+
buffer = buffer.slice(end + 1);
|
|
51
|
+
let message;
|
|
52
|
+
try { message = JSON.parse(line); } catch { continue; }
|
|
53
|
+
// JSON-RPC and MCP notifications have no id and must receive no response.
|
|
54
|
+
// A reply would be an unsolicited message that violates the client's protocol expectations.
|
|
55
|
+
if (message === null || typeof message !== "object" || !Object.hasOwn(message, "id")) continue;
|
|
56
|
+
const { id, method } = message;
|
|
57
|
+
let result;
|
|
58
|
+
switch (method) {
|
|
59
|
+
case "initialize":
|
|
60
|
+
result = { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { name: "archstrict", version: "0.0.0" } };
|
|
61
|
+
break;
|
|
62
|
+
case "tools/list": result = { tools: [] }; break;
|
|
63
|
+
case "ping": result = {}; break;
|
|
64
|
+
default:
|
|
65
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" } }) + "\n");
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "archstrict",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "TypeScript module boundary checking, in the sense of ArchUnit and archspec. A PostToolUse hook runs check right after a file edit and returns violations into the agent's own context, the same moment a human editor's red squiggly would.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "meganemura"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/meganemura/archstrict",
|
|
9
|
+
"repository": "https://github.com/meganemura/archstrict",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"archstrict": {
|
|
13
|
+
"command": "node",
|
|
14
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/mcp/server.mjs"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"hooks": ["./hooks/hooks.json"],
|
|
18
|
+
"keywords": ["claude-code", "claude-code-plugin", "typescript", "architecture", "module-boundaries", "linting"]
|
|
19
|
+
}
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Context for agents that work in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
archstrict checks TypeScript module boundaries.
|
|
8
|
+
It is not tsc, not ESLint, and not a type checker; it is architecture linting, in the sense of ArchUnit (Java) and archspec (Ruby).
|
|
9
|
+
It is designed for a reader that starts from an empty context: a coding agent first, a human second.
|
|
10
|
+
|
|
11
|
+
The shape, in one paragraph.
|
|
12
|
+
A module is one directory, declared explicitly in config (`declaredModules`) rather than discovered by convention - a barrel `index.ts` is not evidence of an enforced boundary. A glob that names a single file is a module too: `surface` resolves against that file's parent.
|
|
13
|
+
A module shows the rest of the codebase one file, named by the config's own `surface` field (default one entry per analyzed source extension: `index.ts`, `index.tsx`, `index.mts`, `index.cts`); anything a module does not export from that file is private, and an import that reaches past it into the module's internals is a violation.
|
|
14
|
+
A module with no public-surface file present is entirely private: every external import into it is a violation.
|
|
15
|
+
A module's surface file can also leak an internal type it never exported by name - a property, a return type, or a generic constraint that structurally reaches past the surface - which is its own violation, distinct from an import bypassing the surface from outside.
|
|
16
|
+
A file can also carry tags (`classify`/`classifyByDirectoryName`, glob or ambient directory-name -> tag) independent of module membership, and a constraint engine (`edges`: `allowDeny`, `order`, `point`) checks edges between tags - the same domain/layer/plane shapes tools like dependency-cruiser and Nx's `depConstraints` express, generalized over tags instead of a fixed vocabulary.
|
|
17
|
+
Shape (scope, exclude, classify, declaredModules, edges, the surface file name) lives in one root file, `archstrict.config.ts`, written as a plain TypeScript value; it is never scattered per module.
|
|
18
|
+
A known cycle can be named as an exception (`ignoredCycles`), and a directory a project decided must hold no code at all can be declared with `mustBeEmpty` - see [skills/archstrict/references/rules.md](skills/archstrict/references/rules.md) for both.
|
|
19
|
+
Known violations freeze into one project-root todo file, `archstrict.todo.json`, grouped by module name, that can only shrink; a module marked `strict` in config can never accumulate a todo entry, existing or new — any entry there is itself a violation.
|
|
20
|
+
A violation report always carries a rule id, `path:line:col`, the evidence, the `because` reason, and a `do:` command — enough for an agent to fix its own mistake without asking.
|
|
21
|
+
|
|
22
|
+
## Layout
|
|
23
|
+
|
|
24
|
+
- `src/` is the library and CLI. The root `archstrict.config.ts` checks that tree: `core` is the analysis engine, `rules` and `verbs` are where a new rule or verb lands, and `cli`, `mcp`, and `check-options` sit above them. `archstrict.todo.json` is the ratchet. `skills/archstrict/` is the product skill shipped to consumers, not a second config.
|
|
25
|
+
- `test/` is the Vitest suite; `features/` is the nukadoko (Gherkin) dogfood scenario.
|
|
26
|
+
- `.agents/` is the canonical Claude Code plugin: the manifest, the PreToolUse and PostToolUse hooks, and the MCP server. `.claude-plugin/plugin.json` symlinks to `.agents/plugin.json`, and repo-root `hooks/` and `mcp/` symlink to `.agents/hooks` and `.agents/mcp`. `${CLAUDE_PLUGIN_ROOT}` is the directory that contains `.claude-plugin/`, so those plugin-root paths still resolve. `.claude-plugin/` stays a real directory holding only the manifest symlink: Claude loads `hooks/` from the plugin root, and a directory symlink onto `.agents/` would place `hooks/` and `mcp/` inside `.claude-plugin/`.
|
|
27
|
+
- `skills/archstrict/` (and root `llms.txt`) is the agent-facing skill (`SKILL.md` plus `references/`).
|
|
28
|
+
- `scripts/` holds standalone scripts (not part of the shipped library): the typescript 7 probe, two narrow config converters (Prisma's architecture.config.json, VS Code's code-layering table) to archstrict.config.ts's own shape, and the two runners that re-run the Prisma/VS Code oracle comparisons through the real CLI against a real clone (see Commands below). `.github/workflows/` is CI.
|
|
29
|
+
- `.claude-team/` holds the task spec, the report, and their working artifacts. It is gitignored; do not reference it from committed content.
|
|
30
|
+
|
|
31
|
+
## Visibility
|
|
32
|
+
|
|
33
|
+
This repository is intended for public release.
|
|
34
|
+
Write all committed text in English: code, comments, docs, commit messages.
|
|
35
|
+
Do not reference private tools, private repositories, or internal working documents (including `.claude-team/`) in committed content.
|
|
36
|
+
If you want to cite an internal document, write its substance in place instead.
|
|
37
|
+
|
|
38
|
+
## Rules
|
|
39
|
+
|
|
40
|
+
- Do not add dependencies without the owner's approval. Pin exact versions, at least 7 days past release. Prefer language-official packages, then vendor packages, and avoid single-maintainer packages.
|
|
41
|
+
- Write tests with Hegel (`@hegeldev/hegel`, property-based) wherever a property exists: round trips, invariants, monotonicity. Example-based tests cover exact CLI output and JSON shape.
|
|
42
|
+
- Comments say why: the constraint, or the alternative that was refused. Each module starts with its responsibility and its boundary.
|
|
43
|
+
- Violation text never uses the words "strict" or "typed"; a name must not mislead about what fixes a violation.
|
|
44
|
+
- A release, a `npm publish`, or a change of the repository's visibility is the owner's to run.
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
For working on archstrict itself:
|
|
49
|
+
|
|
50
|
+
- `npm run build` — compile `src/` to `dist/`. The CLI's own tests spawn the built `dist/cli.js`, so run this before `npm test` if `dist/` is missing or stale.
|
|
51
|
+
- `node dist/cli.js check` — checks this repository against the root `archstrict.config.ts`. `node dist/cli.js todo` prunes `archstrict.todo.json` after the first run and does not add debt again.
|
|
52
|
+
- `npm run typecheck` — `tsc --noEmit` over the whole project.
|
|
53
|
+
- `npm test` — the Vitest suite.
|
|
54
|
+
- `npm run dogfood:nukadoko` — a nukadoko (Gherkin) scenario that runs the built CLI's full init/check/todo/edit/check round trip against nukadoko's own published `src/` (a real, unrelated codebase with no public-surface convention), copied into a disposable scratch directory. Never modifies the real nukadoko package or a checkout of it.
|
|
55
|
+
- `npm run ci:ts7-probe` — measures, against whatever typescript 7 happens to be installed, which of the operations rule 6 needs actually work on typescript 7's `./unstable/*` surface. Never fails: an unsupported operation is the measurement, not an error. archstrict itself always analyzes with its own pinned `typescript` dependency (6.0.3), independent of this. CI (`.github/workflows/ci.yml`) installs `typescript@7.0.2` (`--no-save`, never touching `package.json`) just for this job.
|
|
56
|
+
- `node scripts/run-prisma-oracle.mjs <path-to-a-real-prisma/prisma-clone>` / `node scripts/run-vscode-oracle.mjs <path-to-a-real-microsoft/vscode-clone>` — re-run the Prisma/VS Code oracle comparisons through the real, built CLI (`dist/cli.js check --json`) against a scratch copy of the clone, converting its own real config (`architecture.config.json` / the `code-layering` ESLint rule's table) with the existing `scripts/convert-*` converters. Requires a local clone (`ghq get --shallow` or equivalent); the Prisma one also needs `pnpm install` run in it once, for its own real `node_modules`. Neither ever writes into the clone itself. Each prints a positive-control run (a rule deliberately forbidding something real and common) alongside the baseline, so a `0` in the baseline reads as a genuine pass, not the constraint engine silently seeing no edges at all.
|
|
57
|
+
|
|
58
|
+
The CLI itself:
|
|
59
|
+
|
|
60
|
+
- `archstrict init [dir] [--json]` — on a fresh project, declare one module per top-level directory holding TypeScript source (`.ts`, `.tsx`, `.mts`, `.cts`; default container `src/`, or the project root when it's absent or holds none) and one single-file module per loose top-level source file, so the first `check` covers every analyzed file by construction. That map is an inventory: group files that change together (`glob` may be an array of paths in one directory), split a directory that holds almost every file, then add `edges`. `archstrict recommend` names a mega-module and a file-per-module inventory in `mapNotes`. Write `archstrict.types.ts` (the module-name union type). Re-run any time; it never touches an existing `archstrict.config.ts`, only regenerates `archstrict.types.ts` from its own `declaredModules` names. `--json` prints one object (`configPath`, `typesPath`, `configWritten`, `opened`, `moduleNames`, `hiddenDirs`, `noiseDirs`, `testFileExcludes`, `uncovered`, `notes`, `do`), or `{ "error": "<message>", "do": "<command>" }` on failure, the same convention `check` and `todo` follow.
|
|
61
|
+
- `archstrict check [file] [--json] [--rule <id>] [--module <name>] [--frozen]` — analyze the whole project and report violations. With a file argument, analysis still covers the whole project (resolving an edge needs it), but the report is scoped to that file's own violations. Text output prints every violation up to 20; past that it prints grouped counts (by rule, then by the module owning the todo) with one full example per group and a `do:` that reruns just that group, cut at a line-count cap with a count of the groups left out. `--rule`/`--module` are repeatable and filter both the text and `--json` output after analysis; the exit code reflects the filtered set once either is given. An unknown rule id or module name is an error listing the valid ones. `--frozen` includes todo-matched violations too, each marked `frozen: true` and still combinable with `--rule`/`--module`; the exit code ignores a frozen violation, so a project already covered by `archstrict todo` still exits 0. When most public-surface-bypass violations target modules with no surface file at all, the summary says so and offers freezing, naming a surface, or adding surface files as next steps.
|
|
62
|
+
- `archstrict todo [--json]` — on a project's first run, freeze every current freezable violation into one project-root `archstrict.todo.json`, grouped by module name; on every later run, only prune entries that no longer match a current violation. Never adds after the first run. That file's own existence is the "first run happened" signal, replacing an earlier marker file; a project with no debt after its first run still gets the file, with an empty module map, so the ratchet stays visible. The first run refuses instead (exit 1, no file written) while any `uncovered-module` violation exists, since such a file can never be frozen and a later declared-and-covered version of it would then find freezing already closed forever; a later, prune-only run is unaffected. `--json` prints `{ firstRun, added, pruned }`, or `{ "error": "<message>" }` on a config error, the same convention `check` follows.
|
|
63
|
+
|
|
64
|
+
- `archstrict hotspots [--since <git ref or date>] [--json]` — report commits, changed lines, fan-in, fan-out, frozen debt, active violations, and a commits-times-fan-in score per module. It also reports co-change counts and both directional shares for module pairs. Text output is limited to ten modules and ten pairs; JSON contains all results.
|
|
65
|
+
|
|
66
|
+
- `archstrict simulate [--json] [--whole-project]` — preview proposed source or config changes without writing to disk. The default report is scoped to changed files; `--whole-project` reports the complete project delta. Read the JSON change set from stdin; see [simulation](skills/archstrict/references/simulate.md).
|
|
67
|
+
|
|
68
|
+
## Claude Code plugin
|
|
69
|
+
|
|
70
|
+
This repository is itself a Claude Code plugin (`.claude-plugin/plugin.json`, a symlink to `.agents/plugin.json`). It ships two hooks around every Edit/Write/MultiEdit, both shelling out to the edited project's own `node_modules/.bin/archstrict`, never to this repository's own build. Its `PreToolUse` hook (`.agents/hooks/pre-tool-use.mjs`, reached as `hooks/pre-tool-use.mjs`) runs before the write happens: it builds the file text the tool call would produce and previews it through that project's `archstrict simulate --json`, returning any added violation into the agent's own context so it can change course before the write lands, or denying the tool call outright when `ARCHSTRICT_PRETOOLUSE=deny` is set. Its `PostToolUse` hook (`.agents/hooks/post-tool-use.mjs`, reached as `hooks/post-tool-use.mjs`) runs the edited project's own installed `archstrict check <file>` right after the write and returns any violation into the agent's own context - the same moment a human editor's red squiggly would appear. Both say nothing when the edited project has no `archstrict` installed at all or the edited file has no violation; see [the hook reference](skills/archstrict/references/hook.md) for the full set of silent cases. The MCP server is `.agents/mcp/server.mjs`, reached as `${CLAUDE_PLUGIN_ROOT}/mcp/server.mjs`. `npm pack` ships `.agents/` and drops the symlinks, so an installed package's hooks are `node_modules/archstrict/.agents/hooks/pre-tool-use.mjs` and `post-tool-use.mjs`.
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
<!-- ARCHSTRICT_START -->
|
|
74
|
+
## archstrict
|
|
75
|
+
|
|
76
|
+
In projects with an `archstrict.config.ts` (module-boundary/architecture linting), run `archstrict rules <path>` BEFORE creating a file or adding an import - it reports the module, tags, and constraints that would govern that path, even before it exists. Run `archstrict check` after editing to confirm.
|
|
77
|
+
|
|
78
|
+
The full rule reference (every rule's evidence/because/do shape, the config schema, the pre-edit query) is at `node_modules/archstrict/skills/archstrict/SKILL.md` when installed via npm - read it before configuring `archstrict.config.ts`, or when a violation's `do:` text alone isn't enough.
|
|
79
|
+
|
|
80
|
+
If there is no `archstrict.config.ts`, skip archstrict entirely - it may not be installed here.
|
|
81
|
+
<!-- ARCHSTRICT_END -->
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
The format follows Keep a Changelog, and the versions follow SemVer. Before 1.0, a minor version
|
|
4
|
+
may change commands, flags, config, or output shape. The version entry will describe each change.
|
|
5
|
+
|
|
6
|
+
## 0.2.0 (2026-10-01)
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- This repository includes `.claude-plugin/marketplace.json`. Claude Code users can install the plugin with `/plugin marketplace add meganemura/archstrict` and `/plugin install archstrict@archstrict`.
|
|
11
|
+
- `declaredModules[].glob` accepts an array of paths that share one directory, so a flat directory can name a multi-file seam without a directory move. Surface and friends resolve against that directory. The type-leak boundary is each listed file. Paths in two directories, and an empty array, are config errors.
|
|
12
|
+
- `archstrict init` prints that the generated map is an inventory. A container of only files is told to group seams with a glob array. A directory that holds at least four fifths of the files (and at least eight) is named so it can be split before `archstrict todo`.
|
|
13
|
+
- `archstrict recommend` adds `mapNotes` (`mega-module`, `file-per-module`). A surface proposal for the mega-module says to split it before freezing its bypasses.
|
|
14
|
+
- `archstrict check` and `archstrict todo` name the case where one module holds most analyzed files and most public-surface bypasses, including bypasses a todo file already suppresses. The next command is to split that module before freezing more. `check <file>` leaves the note out.
|
|
15
|
+
- While the config has no `edges` rule, a whole-project `check` prints one `summary:` line: the
|
|
16
|
+
config so far freezes today's import graph, not a target architecture. Its `do:` lines name
|
|
17
|
+
`archstrict recommend`, `archstrict hotspots`, and the rearchitect reference. `--json` carries
|
|
18
|
+
the same text as `nextSteps`. `check <file>` leaves it out.
|
|
19
|
+
- A cycle between two modules now names the imports on each side and two moves in its `do:`:
|
|
20
|
+
extract the shared part into a leaf module both import, or pass the dependency in from the side
|
|
21
|
+
that owns it, and run `archstrict simulate` on the planned change first. A lopsided pair still
|
|
22
|
+
names its minority imports first. A cycle of three or more modules keeps the earlier text.
|
|
23
|
+
- A `type-leak` `do:` now tells two cases apart. A type this module owns needs only a name on this
|
|
24
|
+
surface. A type owned by another module with no surface needs a surface on that module, or the
|
|
25
|
+
exposing export must leave this surface.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Every verb that walks the project (`init`, `check`, `todo`, `simulate`, `hotspots`, `recommend`)
|
|
30
|
+
now skips gitignored paths. archstrict reads the root and nested `.gitignore` files, the ones
|
|
31
|
+
above the project root, and the repository's `info/exclude` with git's own pattern rules, and
|
|
32
|
+
never runs git. A local scratch directory such as `tmp/` no longer floods `check` with
|
|
33
|
+
`uncovered-module` violations. A gitignored file stays resolvable as an import target. A config
|
|
34
|
+
that already declares a module inside a gitignored directory keeps analyzing that module; remove
|
|
35
|
+
the declaration to drop it.
|
|
36
|
+
|
|
37
|
+
### Documentation
|
|
38
|
+
|
|
39
|
+
- The skill, recommend reference, and re-architecture notes tell an adopter to treat `init` as an inventory, to split a mega-module before freezing it, to group a flat directory with a `glob` array, to install the skill once per user, and where a CLI and a graph helper sit.
|
|
40
|
+
- The README (and its Japanese twin) has one Install section per host. Claude Code users add the
|
|
41
|
+
repository marketplace, then install `archstrict@archstrict` for the skill, edit hooks, and MCP
|
|
42
|
+
server. Other agents install the skill with `gh skill install`, add the `AGENTS.md` section with
|
|
43
|
+
`archstrict agents`, and run `archstrict check` in CI, since the edit hooks are Claude Code only.
|
|
44
|
+
|
|
45
|
+
## 0.1.0 (2026-09-29)
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- `archstrict init [dir] [--json]` declares one module per top-level directory holding TypeScript
|
|
50
|
+
source and one per loose top-level source file, so the first `check` covers every analyzed file
|
|
51
|
+
by construction, and writes `archstrict.types.ts`.
|
|
52
|
+
- `archstrict check [file] [--json] [--rule <id>] [--module <name>] [--frozen]` reports module
|
|
53
|
+
boundary violations: a public-surface bypass, an import cycle, an uncovered module, an empty rule
|
|
54
|
+
set, a deprecated module edge whose count grew, a type leaked out of a surface, and a tag-boundary,
|
|
55
|
+
tag-order, or point-rule violation from the constraint engine. `--rule` and `--module` filter both
|
|
56
|
+
the text and JSON output; `--frozen` also reports todo-matched violations without affecting the
|
|
57
|
+
exit code.
|
|
58
|
+
- `archstrict todo [--json]` freezes current violations into one project-root
|
|
59
|
+
`archstrict.todo.json`, grouped by module name, on the first run, then only prunes stale entries
|
|
60
|
+
afterward. A module marked `strict` in config can never accumulate a todo entry.
|
|
61
|
+
- `archstrict rules <path> [--json]`, `archstrict search <query> [--json]`,
|
|
62
|
+
`archstrict recommend [dir] [--json]`, `archstrict fix [file] [--dry-run] [--json]`, and
|
|
63
|
+
`archstrict hotspots [--since <ref>] [--json]` describe a path's constraints, search declared
|
|
64
|
+
public surfaces, propose a boundary config from real edges, fix a surface bypass by adding a
|
|
65
|
+
re-export, and rank modules by Git change frequency, co-change, fan-in, fan-out, and frozen debt.
|
|
66
|
+
- `archstrict simulate [--json] [--whole-project]` previews proposed source or config changes
|
|
67
|
+
against the full rule pipeline without writing to disk, reading the change set from stdin.
|
|
68
|
+
- `archstrict.config.ts` declares a project's modules, their public-surface file name, tags
|
|
69
|
+
(`classify`/`classifyByDirectoryName`), edges between tags (`allowDeny`, `order`, `point`), known
|
|
70
|
+
cycle exceptions (`ignoredCycles`), and directories that must hold no code (`mustBeEmpty`).
|
|
71
|
+
- A violation always carries a rule id, `path:line:col`, the evidence, a `because` reason, and a
|
|
72
|
+
`do:` command, in both text and JSON output.
|
|
73
|
+
- A Claude Code plugin (`.agents/`): a `PreToolUse` hook previews an Edit, Write, or MultiEdit
|
|
74
|
+
through `simulate` before the write lands, a `PostToolUse` hook confirms the result through
|
|
75
|
+
`check` right after, and an MCP server exposes the same verbs as tools.
|
|
76
|
+
- An agent skill (`skills/archstrict/SKILL.md` and its references) and `llms.txt` give a coding
|
|
77
|
+
agent the workflow and the rule reference without reading the source.
|