@thedecipherist/claude-packs 1.0.0 → 1.1.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/CHANGELOG.md +74 -0
- package/README.md +275 -18
- package/bin/claude-packs.mjs +18 -10
- package/configurations/base/config.json +1 -1
- package/configurations/cpp/config.json +2 -1
- package/configurations/full-stack/config.json +2 -1
- package/configurations/go/config.json +3 -1
- package/configurations/node/config.json +3 -1
- package/configurations/python/config.json +3 -1
- package/configurations/rust/config.json +106 -0
- package/configurations/synth-creator/config.json +1 -1
- package/global/hooks/lib/shell-writes.mjs +2 -293
- package/lib/build-queue.mjs +22 -0
- package/lib/claude-md.mjs +4 -2
- package/lib/config-cli.mjs +1 -1
- package/lib/coverage.mjs +10 -2
- package/lib/doctor.mjs +14 -3
- package/lib/fix-plan.mjs +2 -1
- package/lib/found-folders.mjs +25 -0
- package/lib/global.mjs +3 -3
- package/lib/hook-proof.mjs +24 -7
- package/lib/kit-command.mjs +14 -0
- package/lib/layout.mjs +27 -3
- package/lib/logs.mjs +45 -5
- package/lib/new.mjs +1 -1
- package/lib/packs.mjs +4 -2
- package/lib/rule-tests.mjs +42 -0
- package/lib/scan-detect.mjs +41 -5
- package/lib/scan.mjs +5 -2
- package/lib/suggest.mjs +16 -3
- package/lib/sync.mjs +5 -2
- package/lib/workorder.mjs +26 -0
- package/package.json +1 -1
- package/packs/audio-core/hooks/param-id-lock.mjs +59 -16
- package/packs/audio-core/pack.json +5 -0
- package/packs/audio-core/rules/a-control-is-proved-through-its-input.md +26 -0
- package/packs/audio-core/rules/cpu-worst-case-per-block.md +3 -1
- package/packs/audio-core/rules/house-control-behaviours.md +1 -1
- package/packs/audio-core/rules/scoped-no-denormals.md +1 -1
- package/packs/audio-core/rules/straight-on-and-aligned.md +1 -1
- package/packs/audio-core/rules/ui-matches-reference-look.md +2 -0
- package/packs/audio-core/skills/analyse-reference-look/SKILL.md +25 -11
- package/packs/audio-core/skills/prove-control/SKILL.md +2 -0
- package/packs/base/checks/one-walk-per-template.json +1 -1
- package/packs/base/hooks/code-shape.mjs +10 -6
- package/packs/base/hooks/dispatch-once.mjs +58 -21
- package/packs/base/hooks/folder-law.mjs +3 -0
- package/packs/base/hooks/git-guard.mjs +22 -109
- package/packs/base/hooks/lesson-checks.mjs +9 -1
- package/packs/base/hooks/lib/code-shapes.mjs +11 -6
- package/packs/base/hooks/lib/crash-policy.mjs +47 -0
- package/packs/base/hooks/lib/dup-index.mjs +6 -1
- package/packs/base/hooks/lib/dup.mjs +2 -2
- package/packs/base/hooks/lib/event-log.mjs +4 -3
- package/packs/base/hooks/lib/git-adds.mjs +2 -1
- package/packs/base/hooks/lib/git-auth.mjs +44 -0
- package/packs/base/hooks/lib/git-guard-selftest.mjs +93 -0
- package/packs/base/hooks/lib/languages.mjs +5 -2
- package/packs/base/hooks/lib/layout.mjs +6 -2
- package/packs/base/hooks/lib/making-check-selftest.mjs +3 -0
- package/packs/base/hooks/lib/near-copies.mjs +1 -1
- package/packs/base/hooks/lib/program-read.mjs +6 -3
- package/packs/base/hooks/lib/rust-scan.mjs +139 -0
- package/packs/base/hooks/lib/shape-lists.mjs +31 -0
- package/packs/base/hooks/lib/shape-prose.mjs +78 -0
- package/packs/base/hooks/lib/shape.mjs +18 -39
- package/packs/base/hooks/lib/turn.mjs +6 -1
- package/packs/base/hooks/lib/unread-writes.mjs +24 -0
- package/packs/base/hooks/making-check.mjs +12 -7
- package/packs/base/hooks/run-hooks.mjs +15 -7
- package/packs/base/hooks/shape-law.mjs +11 -4
- package/packs/base/hooks/suite-once.mjs +22 -6
- package/packs/base/rules/a-control-is-seen-to-fire.md +1 -1
- package/packs/base/rules/behaviour-lives-with-its-kind.md +1 -1
- package/packs/base/rules/bug-fix-starts-red.md +1 -1
- package/packs/base/rules/field-map-over-if-runs.md +1 -1
- package/packs/base/rules/instances-by-index-identities-by-id.md +2 -2
- package/packs/base/rules/lookup-over-if-chains.md +1 -1
- package/packs/base/rules/plan-and-logs.md +1 -1
- package/packs/base/rules/refusals-are-judged.md +1 -0
- package/packs/base/rules/test-the-template-once.md +2 -1
- package/packs/base/rules/unattended-runs.md +15 -0
- package/packs/base/skills/new-project/SKILL.md +23 -0
- package/packs/base/skills/write-a-workorder/SKILL.md +41 -0
- package/packs/cpp/checks/cpp-find-with-the-library.json +2 -2
- package/packs/go/pack.json +5 -0
- package/packs/hook-proof/rules/a-refusal-is-exit-2.md +2 -2
- package/packs/instrument-shell/rules/render-the-extremes.md +2 -2
- package/packs/midi/checks/position-not-integrated.json +5 -1
- package/packs/postgres/checks/pg-bind-values.json +33 -0
- package/packs/postgres/checks/pg-concurrently-in-txn.json +18 -0
- package/packs/postgres/checks/pg-fk-not-valid.json +18 -0
- package/packs/postgres/checks/pg-index-concurrently.json +18 -0
- package/packs/postgres/checks/pg-no-float-money.json +19 -0
- package/packs/postgres/checks/pg-no-money.json +18 -0
- package/packs/postgres/checks/pg-no-required-column.json +18 -0
- package/packs/postgres/checks/pg-no-volatile-default.json +18 -0
- package/packs/postgres/checks/pg-not-exists.json +18 -0
- package/packs/postgres/checks/pg-timestamptz.json +18 -0
- package/packs/postgres/hooks/db-wipe-guard.mjs +95 -0
- package/packs/postgres/pack.json +48 -0
- package/packs/postgres/rules/postgres-migrations-never-stop-traffic.md +28 -0
- package/packs/postgres/rules/postgres-one-pool-per-process.md +21 -0
- package/packs/postgres/rules/postgres-queries-are-null-safe-and-bounded.md +26 -0
- package/packs/postgres/rules/postgres-types-say-what-they-hold.md +22 -0
- package/packs/postgres/rules/postgres-values-are-bound.md +130 -0
- package/packs/postgres/skills/postgres-safe-migration/SKILL.md +45 -0
- package/packs/reverse-engineering/rules/addresses-name-their-space.md +1 -0
- package/packs/reverse-engineering/rules/reproduction-or-original.md +1 -1
- package/packs/reverse-engineering/skills/gather-sources/SKILL.md +1 -0
- package/packs/rust/checks/rust-allow-needs-reason.json +16 -0
- package/packs/rust/checks/rust-blocking-in-async.json +32 -0
- package/packs/rust/checks/rust-if-chain-is-a-match.json +32 -0
- package/packs/rust/checks/rust-static-mut.json +32 -0
- package/packs/rust/checks/rust-string-as-error.json +34 -0
- package/packs/rust/checks/rust-stub-left-in.json +32 -0
- package/packs/rust/checks/rust-unsafe-needs-safety.json +31 -0
- package/packs/rust/checks/rust-unwrap-in-library.json +34 -0
- package/packs/rust/checks/rust-wildcard-dependency.json +16 -0
- package/packs/rust/clippy.toml +5 -0
- package/packs/rust/pack.json +27 -0
- package/packs/rust/rules/rust-async-never-blocks.md +20 -0
- package/packs/rust/rules/rust-borrow-before-you-clone.md +19 -0
- package/packs/rust/rules/rust-errors-are-returned.md +22 -0
- package/packs/rust/rules/rust-lints-are-the-gate.md +23 -0
- package/packs/rust/rules/rust-repeats-become-templates.md +28 -0
- package/packs/rust/rules/rust-unsafe-is-proved.md +21 -0
- package/packs/rust/skills/rust-extract-template/SKILL.md +50 -0
- package/packs/rust/skills/rust-port/SKILL.md +22 -0
- package/packs/rust/skills/rust-review/SKILL.md +24 -0
- package/packs/rust/tools/rust-lints.toml +19 -0
- package/packs/sound-design/skills/bounce-a-plugin/SKILL.md +40 -0
- package/packs/sound-design/tools/vst3host/build.sh +73 -0
- package/packs/sound-design/tools/vst3host/host_events.h +101 -0
- package/packs/sound-design/tools/vst3host/options.h +172 -0
- package/packs/sound-design/tools/vst3host/render.cpp +245 -0
- package/packs/sound-design/tools/vst3host/session.h +241 -0
- package/packs/synth-creator/hooks/build-guard.mjs +7 -7
- package/packs/synth-creator/hooks/protect-vendor.mjs +2 -1
- package/packs/synth-creator/hooks/stage-guard.mjs +1 -1
- package/packs/synth-creator/rules/timing-from-synthcore.md +1 -1
- package/packs/synth-creator/skills/build-through-the-queue/SKILL.md +4 -2
- package/packs/synth-creator/tools/buildq.mjs +74 -0
- package/prompts/workorder/PROMPT.md +53 -0
- package/prompts/workorder/run.sh +36 -0
- package/prompts/workorder/workorder.md +59 -0
- package/global/hooks/lib/program-read.mjs +0 -196
- package/global/hooks/lib/program-writes.mjs +0 -177
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"ci",
|
|
24
24
|
"dynamic-data",
|
|
25
25
|
"mongo",
|
|
26
|
+
"postgres",
|
|
26
27
|
"ai",
|
|
27
28
|
"nginx",
|
|
28
29
|
"content-site"
|
|
@@ -139,7 +140,7 @@
|
|
|
139
140
|
"Ask the user before inventing a new folder. A new folder goes into `.claude/layout.json` first.",
|
|
140
141
|
"When a hook, check or guard blocks work that is correct, stop and report its exact message. A blocking hook is a red check: fix the hook first, with a selftest, then continue.",
|
|
141
142
|
"When a command is refused or fails, the step fails loudly. Never report a refused write or a failed command as done.",
|
|
142
|
-
"claude-packs (
|
|
143
|
+
"claude-packs (`@thedecipherist/claude-packs`) is the source of truth for every rule, check, hook and skill this project's configuration installs. A fix to one of them is made in the kit, with its test, and comes here with `npx --no-install @thedecipherist/claude-packs update`. A project edits its installed copy only to try a fix before it goes into the kit."
|
|
143
144
|
],
|
|
144
145
|
"never": [
|
|
145
146
|
"Create source files outside these folders, or put a kind of thing anywhere but its place. The folder-law and placement-law hooks refuse both.",
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
],
|
|
10
10
|
"choose": [
|
|
11
11
|
"go",
|
|
12
|
+
"rust",
|
|
12
13
|
"shell",
|
|
13
14
|
"docker",
|
|
15
|
+
"postgres",
|
|
14
16
|
"http-api",
|
|
15
17
|
"multi-tenant",
|
|
16
18
|
"websocket",
|
|
@@ -83,7 +85,7 @@
|
|
|
83
85
|
"Ask the user before inventing a new folder. A new folder goes into `.claude/layout.json` first.",
|
|
84
86
|
"When a hook, check or guard blocks work that is correct, stop and report its exact message. A blocking hook is a red check: fix the hook first, with a selftest, then continue.",
|
|
85
87
|
"When a command is refused or fails, the step fails loudly. Never report a refused write or a failed command as done.",
|
|
86
|
-
"claude-packs (
|
|
88
|
+
"claude-packs (`@thedecipherist/claude-packs`) is the source of truth for every rule, check, hook and skill this project's configuration installs. A fix to one of them is made in the kit, with its test, and comes here with `npx --no-install @thedecipherist/claude-packs update`. A project edits its installed copy only to try a fix before it goes into the kit."
|
|
87
89
|
],
|
|
88
90
|
"never": [
|
|
89
91
|
"Create source files outside these folders, or put a kind of thing anywhere but its place. The folder-law and placement-law hooks refuse both.",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"choose": [
|
|
11
11
|
"js-ts",
|
|
12
12
|
"mongo",
|
|
13
|
+
"postgres",
|
|
13
14
|
"fastify",
|
|
14
15
|
"ai",
|
|
15
16
|
"docker",
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
"websocket",
|
|
30
31
|
"host-agent",
|
|
31
32
|
"go",
|
|
33
|
+
"rust",
|
|
32
34
|
"ci",
|
|
33
35
|
"dynamic-data",
|
|
34
36
|
"zod",
|
|
@@ -88,7 +90,7 @@
|
|
|
88
90
|
"Ask the user before inventing a new folder. A new folder goes into `.claude/layout.json` first.",
|
|
89
91
|
"When a hook, check or guard blocks work that is correct, stop and report its exact message. A blocking hook is a red check: fix the hook first, with a selftest, then continue.",
|
|
90
92
|
"When a command is refused or fails, the step fails loudly. Never report a refused write or a failed command as done.",
|
|
91
|
-
"claude-packs (
|
|
93
|
+
"claude-packs (`@thedecipherist/claude-packs`) is the source of truth for every rule, check, hook and skill this project's configuration installs. A fix to one of them is made in the kit, with its test, and comes here with `npx --no-install @thedecipherist/claude-packs update`. A project edits its installed copy only to try a fix before it goes into the kit."
|
|
92
94
|
],
|
|
93
95
|
"never": [
|
|
94
96
|
"Create source files outside these folders, or put a kind of thing anywhere but its place. The folder-law and placement-law hooks refuse both.",
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
"docker",
|
|
14
14
|
"ai",
|
|
15
15
|
"mongo",
|
|
16
|
+
"postgres",
|
|
16
17
|
"js-ts",
|
|
17
18
|
"cpp",
|
|
18
19
|
"http-api",
|
|
19
20
|
"multi-tenant",
|
|
20
21
|
"ci",
|
|
21
22
|
"go",
|
|
23
|
+
"rust",
|
|
22
24
|
"dynamic-data",
|
|
23
25
|
"web",
|
|
24
26
|
"ui-craft",
|
|
@@ -84,7 +86,7 @@
|
|
|
84
86
|
"Ask the user before inventing a new folder. A new folder goes into `.claude/layout.json` first.",
|
|
85
87
|
"When a hook, check or guard blocks work that is correct, stop and report its exact message. A blocking hook is a red check: fix the hook first, with a selftest, then continue.",
|
|
86
88
|
"When a command is refused or fails, the step fails loudly. Never report a refused write or a failed command as done.",
|
|
87
|
-
"claude-packs (
|
|
89
|
+
"claude-packs (`@thedecipherist/claude-packs`) is the source of truth for every rule, check, hook and skill this project's configuration installs. A fix to one of them is made in the kit, with its test, and comes here with `npx --no-install @thedecipherist/claude-packs update`. A project edits its installed copy only to try a fix before it goes into the kit."
|
|
88
90
|
],
|
|
89
91
|
"never": [
|
|
90
92
|
"Create source files outside these folders, or put a kind of thing anywhere but its place. The folder-law and placement-law hooks refuse both.",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rust",
|
|
3
|
+
"description": "Rust projects: universal engineering and the rust pack.",
|
|
4
|
+
"packs": {
|
|
5
|
+
"always": [
|
|
6
|
+
"base",
|
|
7
|
+
"writing",
|
|
8
|
+
"hook-proof"
|
|
9
|
+
],
|
|
10
|
+
"choose": [
|
|
11
|
+
"rust",
|
|
12
|
+
"go",
|
|
13
|
+
"shell",
|
|
14
|
+
"docker",
|
|
15
|
+
"postgres",
|
|
16
|
+
"http-api",
|
|
17
|
+
"multi-tenant",
|
|
18
|
+
"websocket",
|
|
19
|
+
"host-agent",
|
|
20
|
+
"ci",
|
|
21
|
+
"js-ts",
|
|
22
|
+
"python",
|
|
23
|
+
"dynamic-data",
|
|
24
|
+
"web",
|
|
25
|
+
"ui-craft",
|
|
26
|
+
"content-site"
|
|
27
|
+
],
|
|
28
|
+
"default": [
|
|
29
|
+
"rust"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"layout": {
|
|
33
|
+
"folders": {
|
|
34
|
+
"docs": {
|
|
35
|
+
"path": "docs",
|
|
36
|
+
"purpose": "spec, laws, licences, work orders, decisions"
|
|
37
|
+
},
|
|
38
|
+
"src": {
|
|
39
|
+
"path": "src",
|
|
40
|
+
"purpose": "the source of a single crate"
|
|
41
|
+
},
|
|
42
|
+
"tests": {
|
|
43
|
+
"path": "tests",
|
|
44
|
+
"purpose": "integration tests (unit tests live beside their code, in a #[cfg(test)] module)"
|
|
45
|
+
},
|
|
46
|
+
"tools": {
|
|
47
|
+
"path": "tools",
|
|
48
|
+
"purpose": "scripts and research tooling (not shipped)"
|
|
49
|
+
},
|
|
50
|
+
"crates": {
|
|
51
|
+
"path": "crates",
|
|
52
|
+
"purpose": "the crates of a workspace, each with its own src/ and tests/",
|
|
53
|
+
"planned": true
|
|
54
|
+
},
|
|
55
|
+
"benches": {
|
|
56
|
+
"path": "benches",
|
|
57
|
+
"purpose": "benchmarks",
|
|
58
|
+
"planned": true
|
|
59
|
+
},
|
|
60
|
+
"examples": {
|
|
61
|
+
"path": "examples",
|
|
62
|
+
"purpose": "runnable examples",
|
|
63
|
+
"planned": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"kinds": {
|
|
67
|
+
"tests": {
|
|
68
|
+
"what": "integration tests",
|
|
69
|
+
"in": "tests",
|
|
70
|
+
"concepts": [
|
|
71
|
+
"tests"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"docs": {
|
|
75
|
+
"what": "the spec, laws.md, decisions, research, work orders",
|
|
76
|
+
"in": "docs",
|
|
77
|
+
"concepts": [
|
|
78
|
+
"docs-folder"
|
|
79
|
+
],
|
|
80
|
+
"files": [
|
|
81
|
+
"**/laws.md",
|
|
82
|
+
"**/*-spec.md"
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
"tools": {
|
|
86
|
+
"what": "scripts and research tools that never ship",
|
|
87
|
+
"in": "tools"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"preamble": {
|
|
92
|
+
"always": [
|
|
93
|
+
"Ask the user before inventing a new folder. A new folder goes into `.claude/layout.json` first.",
|
|
94
|
+
"When a hook, check or guard blocks work that is correct, stop and report its exact message. A blocking hook is a red check: fix the hook first, with a selftest, then continue.",
|
|
95
|
+
"When a command is refused or fails, the step fails loudly. Never report a refused write or a failed command as done.",
|
|
96
|
+
"claude-packs (`@thedecipherist/claude-packs`) is the source of truth for every rule, check, hook and skill this project's configuration installs. A fix to one of them is made in the kit, with its test, and comes here with `npx --no-install @thedecipherist/claude-packs update`. A project edits its installed copy only to try a fix before it goes into the kit."
|
|
97
|
+
],
|
|
98
|
+
"never": [
|
|
99
|
+
"Create source files outside these folders, or put a kind of thing anywhere but its place. The folder-law and placement-law hooks refuse both.",
|
|
100
|
+
"Edit files in `.claude/rules`, `.claude/checks` or `.claude/hooks` to get past a block. Fix the code, or ask the user to change the law.",
|
|
101
|
+
"Route around a block: no override variables, no writing through the shell after a refusal, no parking files in a scratchpad, no putting new code into an unrelated existing file.",
|
|
102
|
+
"Point at a document by line number. Cite a fixed anchor (`{#id}`) or a symbol name; lines move."
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"sourceFiles": "(?<!/build)\\.rs$"
|
|
106
|
+
}
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"Ask the user before inventing a new folder. A new folder goes into `.claude/layout.json` first.",
|
|
136
136
|
"When a hook, check or guard blocks work that is correct, stop and report its exact message. A blocking hook is a red check: fix the hook first, with a selftest, then continue.",
|
|
137
137
|
"When a command is refused or fails, the step fails loudly. Never report a refused write or a failed command as done.",
|
|
138
|
-
"claude-packs (
|
|
138
|
+
"claude-packs (`@thedecipherist/claude-packs`) is the source of truth for every rule, check, hook and skill used to build synths. A fix to one of them, or a lesson that should hold in every synth, is made in the kit, with its test, and comes here with `npx --no-install @thedecipherist/claude-packs update`, so every synth gets it. A project edits its installed copy only to try a fix before it goes into the kit."
|
|
139
139
|
],
|
|
140
140
|
"never": [
|
|
141
141
|
"Create source files outside these folders, or put a kind of thing anywhere but its place. The folder-law and placement-law hooks refuse both.",
|
|
@@ -1,293 +1,2 @@
|
|
|
1
|
-
// shell
|
|
2
|
-
|
|
3
|
-
// Write tool lacks (WB-69: a guard covered Write and Edit while `cat > file <<EOF` walked past it).
|
|
4
|
-
//
|
|
5
|
-
// shellWrites(command, cwd) -> [{ path, op, content, from?, sed? }]
|
|
6
|
-
// op: write (>, >|, &>, tee), append (>>, &>>, tee -a), copy (cp, install, rsync), move (mv; its source
|
|
7
|
-
// is a delete), link (ln), edit (sed -i, perl -i, ruby -i), truncate, touch, delete (rm, unlink, shred, mv's source)
|
|
8
|
-
// and an inline program's own writes (python -c / python - <<EOF, node -e, perl -e, ruby -e: program-writes.mjs,
|
|
9
|
-
// KWB-8); a script file run by an interpreter (python3 tools/x.py) is the project's own tool, not a write.
|
|
10
|
-
// content: the text written (an echo or printf, a heredoc or here-string, a cat of known files, what a pipe
|
|
11
|
-
// carries into tee, the source of a cp or mv), or null when it cannot be known.
|
|
12
|
-
// afterText(write) -> { before, after } after is null when it cannot be known; sed -i is run for real on a
|
|
13
|
-
// copy of the text, in GNU sed's --sandbox (no e, r or w commands), so its result is exact.
|
|
14
|
-
// `node shell-writes.mjs --json` reads { command, cwd } on stdin and prints the writes with before and after
|
|
15
|
-
// (the python hooks use this, so there is one reader of the shell). `--selftest`: each route and program form.
|
|
16
|
-
// Text inside quotes, and a heredoc's body, is never read as a command.
|
|
17
|
-
import { spawnSync } from 'node:child_process';
|
|
18
|
-
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
19
|
-
import { basename, join, resolve } from 'node:path';
|
|
20
|
-
import { fileURLToPath } from 'node:url';
|
|
21
|
-
import { programOf, programWrites } from './program-writes.mjs';
|
|
22
|
-
|
|
23
|
-
const HOME = process.env.HOME ?? '';
|
|
24
|
-
// A path that starts with a variable is where that variable points, never a folder of this name in the project (KWB-44). One not set is somewhere unknown.
|
|
25
|
-
const expand = (p) => p.replace(/^(~|\$HOME|\$\{HOME\})(?=\/|$)/, HOME).replace(/^\$\{?([A-Za-z_]\w*)\}?(?=\/|$)/, (_, name) => process.env[name] ?? `/unresolved-${name}`);
|
|
26
|
-
const NOT_FILES = new Set(['/dev/null', '/dev/stdout', '/dev/stderr', '/dev/tty']);
|
|
27
|
-
|
|
28
|
-
export function readText(path) {
|
|
29
|
-
try {
|
|
30
|
-
const st = statSync(path);
|
|
31
|
-
if (!st.isFile() || st.size > 8 * 1024 * 1024) return null;
|
|
32
|
-
return readFileSync(path, 'utf8');
|
|
33
|
-
} catch {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
|
|
38
|
-
|
|
39
|
-
// Heredoc bodies are cut out of the command and kept, in order, for the command that reads them.
|
|
40
|
-
function cutHeredocs(command) {
|
|
41
|
-
const lines = command.split('\n');
|
|
42
|
-
const out = [];
|
|
43
|
-
const bodies = [];
|
|
44
|
-
const MARK = /(?<!<)<<(?!<)(-?)\s*(['"]?)([A-Za-z_][\w-]*)\2/g;
|
|
45
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
46
|
-
const marks = [...lines[i].matchAll(MARK)];
|
|
47
|
-
let n = bodies.length;
|
|
48
|
-
out.push(lines[i].replace(MARK, () => ` <<HEREDOC${n++} `));
|
|
49
|
-
for (const m of marks) {
|
|
50
|
-
const body = [];
|
|
51
|
-
const end = (l) => (m[1] ? l.replace(/^\t+/, '') : l) === m[3];
|
|
52
|
-
while (i + 1 < lines.length && !end(lines[i + 1])) body.push(m[1] ? lines[++i].replace(/^\t+/, '') : lines[++i]);
|
|
53
|
-
i += 1;
|
|
54
|
-
bodies.push(body.length ? `${body.join('\n')}\n` : '');
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return { text: out.join('\n'), bodies };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Words and operators, quotes removed from words; a heredoc is its marker word.
|
|
61
|
-
function lex(text) {
|
|
62
|
-
const tokens = [];
|
|
63
|
-
let cur = null;
|
|
64
|
-
let i = 0;
|
|
65
|
-
const push = () => { if (cur !== null) tokens.push({ w: cur }); cur = null; };
|
|
66
|
-
while (i < text.length) {
|
|
67
|
-
const c = text[i];
|
|
68
|
-
if (c === "'") {
|
|
69
|
-
const j = text.indexOf("'", i + 1);
|
|
70
|
-
cur = (cur ?? '') + text.slice(i + 1, j < 0 ? text.length : j);
|
|
71
|
-
i = j < 0 ? text.length : j + 1;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (c === '"') {
|
|
75
|
-
let j = i + 1;
|
|
76
|
-
let s = '';
|
|
77
|
-
while (j < text.length && text[j] !== '"') {
|
|
78
|
-
if (text[j] === '\\' && j + 1 < text.length && '"\\$`'.includes(text[j + 1])) { s += text[j + 1]; j += 2; } else s += text[j++];
|
|
79
|
-
}
|
|
80
|
-
cur = (cur ?? '') + s;
|
|
81
|
-
i = j + 1;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (c === '\\' && i + 1 < text.length) {
|
|
85
|
-
if (text[i + 1] !== '\n') cur = (cur ?? '') + text[i + 1];
|
|
86
|
-
i += 2;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (c === '\n') { push(); tokens.push({ op: ';' }); i += 1; continue; }
|
|
90
|
-
if (/\s/.test(c)) { push(); i += 1; continue; }
|
|
91
|
-
if (c === '#' && cur === null) { while (i < text.length && text[i] !== '\n') i += 1; continue; }
|
|
92
|
-
const rest = text.slice(i);
|
|
93
|
-
const redir = rest.match(/^(&>>|&>|>>|>\||>&\d*-?|>|<<<|<<HEREDOC(\d+)|<)/);
|
|
94
|
-
if (redir && (cur === null || /^\d+$/.test(cur))) {
|
|
95
|
-
const fd = cur ?? '';
|
|
96
|
-
cur = null;
|
|
97
|
-
tokens.push({ op: redir[2] !== undefined ? '<<' : redir[1].startsWith('>&') ? '>&' : redir[1], fd, doc: redir[2] !== undefined ? Number(redir[2]) : undefined });
|
|
98
|
-
i += redir[0].length;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const ctl = rest.match(/^(&&|\|\||\|&|\||;|&|\(|\)|`|\$\()/);
|
|
102
|
-
if (ctl) { push(); tokens.push({ op: ctl[1] === '|&' ? '|' : ctl[1] }); i += ctl[1].length; continue; }
|
|
103
|
-
cur = (cur ?? '') + c;
|
|
104
|
-
i += 1;
|
|
105
|
-
}
|
|
106
|
-
push();
|
|
107
|
-
return tokens;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const PREFIX = new Set(['sudo', 'env', 'command', 'builtin', 'nohup', 'time', 'exec', 'nice']);
|
|
111
|
-
|
|
112
|
-
function printf(args) {
|
|
113
|
-
if (!args.length) return '';
|
|
114
|
-
const esc = (s) => s.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\\\/g, '\\');
|
|
115
|
-
const [fmt, ...vals] = args;
|
|
116
|
-
const slots = (fmt.match(/%[sdbq]/g) ?? []).length;
|
|
117
|
-
if (!slots) return esc(fmt);
|
|
118
|
-
let out = '';
|
|
119
|
-
let k = 0;
|
|
120
|
-
do {
|
|
121
|
-
out += esc(fmt).replace(/%[sdbq]/g, () => vals[k++] ?? '');
|
|
122
|
-
} while (k < vals.length);
|
|
123
|
-
return out;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// One simple command: its words, redirections and stdin, and what it writes.
|
|
127
|
-
function judge(cmd, cwd, stdin, writes, bodies) {
|
|
128
|
-
let words = [...cmd.words];
|
|
129
|
-
while (words.length && (PREFIX.has(words[0]) || /^[A-Za-z_]\w*=/.test(words[0]) || (words[0] === 'timeout' && words.length > 1))) words = words[0] === 'timeout' ? words.slice(2) : words.slice(1);
|
|
130
|
-
const at = (p) => resolve(cwd, expand(p));
|
|
131
|
-
let input = stdin;
|
|
132
|
-
for (const r of cmd.redirs) {
|
|
133
|
-
if (r.op === '<<') input = bodies[r.doc] ?? null;
|
|
134
|
-
else if (r.op === '<<<') input = `${r.target}\n`;
|
|
135
|
-
else if (r.op === '<') input = readText(at(r.target));
|
|
136
|
-
}
|
|
137
|
-
const exe = words.length ? basename(words[0]) : '';
|
|
138
|
-
const args = words.slice(1);
|
|
139
|
-
const flags = args.filter((a) => a.startsWith('-'));
|
|
140
|
-
const plain = args.filter((a) => !a.startsWith('-'));
|
|
141
|
-
let stdout = null;
|
|
142
|
-
if (exe === 'echo') {
|
|
143
|
-
const n = args[0] === '-n' || args[0] === '-ne' || args[0] === '-en';
|
|
144
|
-
const e = /^-[ne]*e/.test(args[0] ?? '');
|
|
145
|
-
const text = args.filter((a, k) => !(k === 0 && /^-[neE]+$/.test(a))).join(' ');
|
|
146
|
-
stdout = (e ? text.replace(/\\n/g, '\n').replace(/\\t/g, '\t') : text) + (n ? '' : '\n');
|
|
147
|
-
} else if (exe === 'printf') stdout = printf(args);
|
|
148
|
-
else if (exe === 'cat') stdout = plain.length ? plain.map((f) => readText(at(f))).reduce((a, b) => (a === null || b === null ? null : a + b), '') : input;
|
|
149
|
-
else if (exe === 'tee') stdout = input;
|
|
150
|
-
else if (exe === '' && cmd.redirs.some((r) => r.op === '<<' || r.op === '<<<')) stdout = input;
|
|
151
|
-
else if (exe === ':' || exe === 'true') stdout = '';
|
|
152
|
-
|
|
153
|
-
const add = (path, op, content, extra = {}) => { if (!NOT_FILES.has(path)) writes.push({ path, op, content, ...extra }); };
|
|
154
|
-
for (const r of cmd.redirs) {
|
|
155
|
-
if (!['>', '>>', '>|', '&>', '&>>'].includes(r.op) || !r.target) continue;
|
|
156
|
-
const toStdout = r.fd === '' || r.fd === '1' || r.op.startsWith('&');
|
|
157
|
-
add(at(r.target), r.op.includes('>>') ? 'append' : 'write', toStdout ? stdout : null);
|
|
158
|
-
}
|
|
159
|
-
if (exe === 'tee') for (const f of plain) add(at(f), flags.some((x) => /^-\w*a/.test(x) || x === '--append') ? 'append' : 'write', input);
|
|
160
|
-
if (['cp', 'mv', 'install', 'rsync', 'ln'].includes(exe)) {
|
|
161
|
-
const t = args.findIndex((a) => a === '-t' || a === '--target-directory');
|
|
162
|
-
const dirArg = t >= 0 ? args[t + 1] : null;
|
|
163
|
-
const files = plain.filter((a) => a !== dirArg && !(['install'].includes(exe) && /^\d{3,4}$/.test(a)));
|
|
164
|
-
const srcs = dirArg ? files : files.slice(0, -1);
|
|
165
|
-
const dest = dirArg ?? files.at(-1);
|
|
166
|
-
if (dest && srcs.length) {
|
|
167
|
-
for (const s of srcs) {
|
|
168
|
-
const into = dirArg || srcs.length > 1 || dest.endsWith('/') || isDir(at(dest));
|
|
169
|
-
const target = into ? join(at(dest), basename(s)) : at(dest);
|
|
170
|
-
const op = exe === 'mv' ? 'move' : exe === 'ln' ? 'link' : 'copy';
|
|
171
|
-
add(target, op, exe === 'ln' ? null : readText(at(s)), { from: at(s) });
|
|
172
|
-
if (exe === 'mv') add(at(s), 'delete', null);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (exe === 'sed' && args.some((a) => a === '-i' || a.startsWith('-i') || a.startsWith('--in-place'))) {
|
|
177
|
-
const scripts = [];
|
|
178
|
-
const keep = [];
|
|
179
|
-
const files = [];
|
|
180
|
-
for (let k = 0; k < args.length; k += 1) {
|
|
181
|
-
const a = args[k];
|
|
182
|
-
if (a === '-i' && args[k + 1] === '') { k += 1; continue; } // BSD: -i ''
|
|
183
|
-
if (a === '-e' || a === '--expression') { scripts.push(args[++k]); continue; }
|
|
184
|
-
if (a.startsWith('--expression=')) { scripts.push(a.slice(13)); continue; }
|
|
185
|
-
if (a === '-f' || a === '--file') { scripts.push(null); k += 1; continue; }
|
|
186
|
-
if (/^-[^-]/.test(a)) { for (const f of a.slice(1).replace(/i.*$/, '')) if ('Ernsz'.includes(f)) keep.push(`-${f}`); continue; }
|
|
187
|
-
if (a.startsWith('--')) { if (['--regexp-extended', '--quiet', '--silent', '--separate', '--null-data'].includes(a)) keep.push(a); continue; }
|
|
188
|
-
if (!scripts.length) scripts.push(a); else files.push(a);
|
|
189
|
-
}
|
|
190
|
-
for (const f of files) add(at(f), 'edit', null, { sed: scripts.includes(null) ? null : { flags: keep, scripts } });
|
|
191
|
-
}
|
|
192
|
-
if ((exe === 'perl' || exe === 'ruby') && args.some((a) => /^-\w*i/.test(a))) {
|
|
193
|
-
const files = [];
|
|
194
|
-
for (let k = 0; k < args.length; k += 1) {
|
|
195
|
-
if (/^-\w*[eE]$/.test(args[k])) { k += 1; continue; } // -e, -pe, -ne: the next word is the program
|
|
196
|
-
if (!args[k].startsWith('-')) files.push(args[k]);
|
|
197
|
-
}
|
|
198
|
-
for (const f of files) add(at(f), 'edit', null);
|
|
199
|
-
}
|
|
200
|
-
const program = programOf(exe, args, input);
|
|
201
|
-
if (program) for (const w of programWrites(program, at, readText)) add(w.path, w.op, w.content, w.from ? { from: w.from } : {});
|
|
202
|
-
if (exe === 'dd') for (const a of args) if (a.startsWith('of=')) add(at(a.slice(3)), 'write', null);
|
|
203
|
-
if (exe === 'truncate') {
|
|
204
|
-
const k = args.findIndex((a) => a === '-s' || a.startsWith('--size'));
|
|
205
|
-
const size = k >= 0 ? (args[k].includes('=') ? args[k].split('=')[1] : args[k + 1]) : null;
|
|
206
|
-
for (const f of plain.filter((a) => a !== size)) add(at(f), 'truncate', size === '0' ? '' : null);
|
|
207
|
-
}
|
|
208
|
-
if (exe === 'touch') for (const f of plain) add(at(f), 'touch', null);
|
|
209
|
-
if (['rm', 'unlink', 'shred'].includes(exe)) for (const f of plain) add(at(f), 'delete', null);
|
|
210
|
-
return { stdout, cd: exe === 'cd' && plain[0] ? at(plain[0]) : null };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function shellWrites(command = '', cwd = process.cwd()) {
|
|
214
|
-
const { text, bodies } = cutHeredocs(command);
|
|
215
|
-
const writes = [];
|
|
216
|
-
let here = cwd;
|
|
217
|
-
let cmd = { words: [], redirs: [] };
|
|
218
|
-
let pipeIn = null;
|
|
219
|
-
const tokens = lex(text);
|
|
220
|
-
// a variable set earlier in the command ("S=/tmp/x; cat > $S/a.md") names that folder, not "<cwd>/$S" (KWB-18); one never set
|
|
221
|
-
// stays as written
|
|
222
|
-
const vars = {};
|
|
223
|
-
const sub = (w) => w.replace(/\$(?:\{(\w+)\}|(\w+))/g, (m, a, b) => vars[a ?? b] ?? m);
|
|
224
|
-
const flush = () => {
|
|
225
|
-
if (cmd.words.length && !cmd.redirs.length && cmd.words.every((w) => /^[A-Za-z_]\w*=/.test(w))) {
|
|
226
|
-
for (const w of cmd.words) { const i = w.indexOf('='); vars[w.slice(0, i)] = sub(w.slice(i + 1)); }
|
|
227
|
-
cmd = { words: [], redirs: [] };
|
|
228
|
-
pipeIn = null;
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
cmd = { words: cmd.words.map(sub), redirs: cmd.redirs.map((r) => (r.target === undefined ? r : { ...r, target: sub(r.target) })) };
|
|
232
|
-
if (cmd.words.length || cmd.redirs.length) {
|
|
233
|
-
const r = judge(cmd, here, pipeIn, writes, bodies);
|
|
234
|
-
if (r.cd) here = r.cd;
|
|
235
|
-
pipeIn = r.stdout;
|
|
236
|
-
} else pipeIn = null;
|
|
237
|
-
cmd = { words: [], redirs: [] };
|
|
238
|
-
};
|
|
239
|
-
for (let k = 0; k < tokens.length; k += 1) {
|
|
240
|
-
const t = tokens[k];
|
|
241
|
-
if (t.w !== undefined) { cmd.words.push(t.w); continue; }
|
|
242
|
-
if (['>', '>>', '>|', '&>', '&>>', '<', '<<<'].includes(t.op)) {
|
|
243
|
-
const next = tokens[k + 1];
|
|
244
|
-
if (next?.w !== undefined) { cmd.redirs.push({ op: t.op, fd: t.fd, target: next.w }); k += 1; }
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
if (t.op === '<<') { cmd.redirs.push({ op: '<<', doc: t.doc }); continue; }
|
|
248
|
-
if (t.op === '>&') continue;
|
|
249
|
-
if (t.op === '|') { flush(); continue; } // the next command reads this one's output
|
|
250
|
-
flush();
|
|
251
|
-
pipeIn = null;
|
|
252
|
-
}
|
|
253
|
-
flush();
|
|
254
|
-
return writes;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
let gnuSed;
|
|
258
|
-
function findGnuSed() {
|
|
259
|
-
if (gnuSed !== undefined) return gnuSed;
|
|
260
|
-
for (const s of ['/opt/homebrew/opt/gnu-sed/libexec/gnubin/sed', '/usr/local/opt/gnu-sed/libexec/gnubin/sed', 'gsed', 'sed']) {
|
|
261
|
-
const r = spawnSync(s, ['--version'], { encoding: 'utf8', timeout: 2000 });
|
|
262
|
-
if (r.status === 0 && /GNU sed/.test(r.stdout)) return (gnuSed = s);
|
|
263
|
-
}
|
|
264
|
-
return (gnuSed = null);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function afterText(w, before = readText(w.path) ?? '') {
|
|
268
|
-
switch (w.op) {
|
|
269
|
-
case 'write': case 'copy': case 'move': case 'truncate': return { before, after: w.content };
|
|
270
|
-
case 'append': return { before, after: w.content === null ? null : before + w.content };
|
|
271
|
-
case 'touch': return { before, after: before };
|
|
272
|
-
case 'edit': {
|
|
273
|
-
const sed = w.sed && findGnuSed();
|
|
274
|
-
if (!sed) return { before, after: null };
|
|
275
|
-
const r = spawnSync(sed, ['--sandbox', ...w.sed.flags, ...w.sed.scripts.flatMap((s) => ['-e', s])], { input: before, encoding: 'utf8', timeout: 3000 });
|
|
276
|
-
return { before, after: r.status === 0 ? r.stdout : null };
|
|
277
|
-
}
|
|
278
|
-
default: return { before, after: null };
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const direct = process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
283
|
-
if (direct && process.argv.includes('--selftest')) import('./shell-writes-selftest.mjs').then((m) => { process.exitCode = m.selftest(); });
|
|
284
|
-
if (process.argv[1] && process.argv.includes('--json') && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
285
|
-
let raw = '';
|
|
286
|
-
process.stdin.on('data', (c) => { raw += c; });
|
|
287
|
-
process.stdin.on('end', () => {
|
|
288
|
-
let q = {};
|
|
289
|
-
try { q = JSON.parse(raw || '{}'); } catch { /* nothing to read */ }
|
|
290
|
-
const out = shellWrites(q.command ?? '', q.cwd ?? process.cwd()).map((w) => ({ path: w.path, op: w.op, ...afterText(w) }));
|
|
291
|
-
process.stdout.write(JSON.stringify(out));
|
|
292
|
-
});
|
|
293
|
-
}
|
|
1
|
+
// The one shell reader is base's. This line lets the global hooks run from the kit's own folder; `global add` installs the reader itself (lib/global.mjs).
|
|
2
|
+
export * from '../../../packs/base/hooks/lib/shell-writes.mjs';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// KF-1: build-guard named a queue no pack shipped, and took any file called build.sh as a queue. doctor checks both ways it fails open: the tool is gone, or the project's
|
|
2
|
+
// wrapper queues nothing. A wrapper that holds the law itself says so: layout.json "guards": { "build": { "queue": "none: <reason>" } }.
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const TOOL = '.claude/tools/buildq.mjs';
|
|
7
|
+
|
|
8
|
+
const wrappersOf = (commands) => [...new Set(Object.values(commands ?? {}).flatMap((c) => String(c).split(/\s+/)).filter((w) => /(^|\/)build\.sh$/.test(w)))];
|
|
9
|
+
|
|
10
|
+
export function buildQueueProblems(projectDir, layout) {
|
|
11
|
+
if (!existsSync(join(projectDir, '.claude/hooks/build-guard.mjs'))) return [];
|
|
12
|
+
const problems = [];
|
|
13
|
+
if (!existsSync(join(projectDir, TOOL))) problems.push(`build-guard sends every build to ${TOOL}, which is missing: run update`);
|
|
14
|
+
if (/^none:\s*\S/.test(layout.guards?.build?.queue ?? '')) return problems;
|
|
15
|
+
for (const rel of wrappersOf(layout.commands)) {
|
|
16
|
+
const file = join(projectDir, rel);
|
|
17
|
+
if (existsSync(file) && !readFileSync(file, 'utf8').includes('buildq')) {
|
|
18
|
+
problems.push(`${rel} does not call the queue, so build-guard passes it and no job waits. Call ${TOOL} in it, or set layout.json "guards": { "build": { "queue": "none: <reason>" } }`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return problems;
|
|
22
|
+
}
|
package/lib/claude-md.mjs
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// Everything outside the markers is the user's and is never touched.
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { noPackageLine } from './kit-command.mjs';
|
|
5
6
|
|
|
6
7
|
const START = '<!-- claude-packs:start -->';
|
|
7
8
|
const END = '<!-- claude-packs:end -->';
|
|
8
9
|
|
|
9
10
|
// The house rules after the folders are the configuration's preamble (configurations/<name>/config.json, 0.10.0).
|
|
10
|
-
function section(layout, packNames, preamble) {
|
|
11
|
+
function section(layout, packNames, preamble, notes = []) {
|
|
11
12
|
// Folders no kind points at are listed by purpose; the rest appear once, in the kinds list.
|
|
12
13
|
const used = new Set(Object.values(layout.kinds ?? {}).map((k) => k.in));
|
|
13
14
|
const folders = Object.entries(layout.folders)
|
|
@@ -31,6 +32,7 @@ function section(layout, packNames, preamble) {
|
|
|
31
32
|
'</constraints>',
|
|
32
33
|
'',
|
|
33
34
|
...commandLines(layout),
|
|
35
|
+
...notes,
|
|
34
36
|
`Installed packs: ${packNames.join(', ')}.`,
|
|
35
37
|
END,
|
|
36
38
|
].join('\n');
|
|
@@ -48,7 +50,7 @@ function commandLines(layout) {
|
|
|
48
50
|
|
|
49
51
|
export function writeClaudeMd(projectDir, layout, packNames, preamble) {
|
|
50
52
|
const file = join(projectDir, 'CLAUDE.md');
|
|
51
|
-
const block = section(layout, packNames, preamble);
|
|
53
|
+
const block = section(layout, packNames, preamble, noPackageLine(projectDir));
|
|
52
54
|
let text = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
53
55
|
const s = text.indexOf(START);
|
|
54
56
|
const e = text.indexOf(END);
|
package/lib/config-cli.mjs
CHANGED
|
@@ -32,5 +32,5 @@ export async function switchConfig(dir, name, installed, { force = false, dryRun
|
|
|
32
32
|
if (dryRun) { log('dry run: nothing changed'); return 0; }
|
|
33
33
|
if (lines.at(-1) === ' no change' && layout.configuration === to.name) return 0;
|
|
34
34
|
await sync(dir, chosen, { force, mkdirs: true, config: name, log });
|
|
35
|
-
return doctor(dir, log);
|
|
35
|
+
return doctor(dir, log, { installing: true });
|
|
36
36
|
}
|
package/lib/coverage.mjs
CHANGED
|
@@ -79,8 +79,16 @@ export async function coverage(projectDir, source = SOURCE) {
|
|
|
79
79
|
const all = projectFiles(projectDir);
|
|
80
80
|
const rules = rulePaths(claudeDir);
|
|
81
81
|
const reachable = [...all, ...claudeFiles(projectDir)];
|
|
82
|
-
const
|
|
82
|
+
const matchesNothing = (r) => r.globs.length && !reachable.some((f) => matchesAny(f, r.globs));
|
|
83
|
+
// A rule whose folder holds nothing yet waits for its first file; only one whose folder holds other files disagrees with the structure (KF-9: a new unit's
|
|
84
|
+
// empty src/dsp made `doctor --strict` impossible). A glob with no folder of its own (**/*.py) says nothing about the structure either.
|
|
85
|
+
const folderOf = (glob) => glob.split(/[*?{[]/)[0].replace(/\/?[^/]*$/, '');
|
|
86
|
+
const holdsFiles = (folder) => Boolean(folder) && reachable.some((f) => f.startsWith(`${folder}/`));
|
|
87
|
+
const waiting = (r) => !r.globs.some((g) => holdsFiles(folderOf(g)));
|
|
88
|
+
const deadRules = rules.filter((r) => matchesNothing(r) && !waiting(r)).map((r) => r.rule);
|
|
89
|
+
const waitingRules = rules.filter((r) => matchesNothing(r) && waiting(r)).map((r) => r.rule);
|
|
90
|
+
const liveRules = rules.filter((r) => all.some((f) => source.test(f) && matchesAny(f, r.globs))).map((r) => r.rule); // it governs source the project wrote
|
|
83
91
|
const protectedGlobs = readLayout(projectDir).protected ?? [];
|
|
84
92
|
const uncovered = all.filter((f) => source.test(f) && !matchesAny(f, protectedGlobs) && !rules.some((r) => matchesAny(f, r.globs)));
|
|
85
|
-
return { deadRules, uncovered, sourceCount: all.filter((f) => source.test(f)).length };
|
|
93
|
+
return { deadRules, waitingRules, liveRules, uncovered, files: all, sourceCount: all.filter((f) => source.test(f)).length };
|
|
86
94
|
}
|
package/lib/doctor.mjs
CHANGED
|
@@ -13,7 +13,9 @@ import { onceOnlyDoctor, useJscpd } from './once-only-report.mjs';
|
|
|
13
13
|
import { proved, remember, selftestKey, sharedHash } from './selftest-cache.mjs';
|
|
14
14
|
import { timingProblems } from './hook-timing.mjs';
|
|
15
15
|
import { configProblems, sourceFilesOf } from './configs.mjs';
|
|
16
|
-
import { hookProofProblems } from './hook-proof.mjs';
|
|
16
|
+
import { hookProofProblems, paramLockProblems } from './hook-proof.mjs';
|
|
17
|
+
import { ruleTestProblems } from './rule-tests.mjs';
|
|
18
|
+
import { buildQueueProblems } from './build-queue.mjs';
|
|
17
19
|
import { manifestProblems } from './pack-manifest.mjs';
|
|
18
20
|
import { readPack } from './packs.mjs';
|
|
19
21
|
import { hoverGateProblems, pageGateProblems } from './hover-gate.mjs';
|
|
@@ -21,7 +23,9 @@ import { hoverGateProblems, pageGateProblems } from './hover-gate.mjs';
|
|
|
21
23
|
const SELFTEST_JOBS = Number(process.env.CLAUDE_PACKS_SELFTEST_JOBS ?? 4);
|
|
22
24
|
const SELFTEST_S = Number(process.env.CLAUDE_PACKS_SELFTEST_S ?? 180);
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
// installing: doctor run at the end of an install. What the project has yet to do (a rule's test, a parameter lock) is a note there: an install on
|
|
27
|
+
// a project with code has not failed. The `doctor` command is red for it.
|
|
28
|
+
export async function doctor(projectDir, log = console.log, { strict = false, fresh = false, installing = false } = {}) {
|
|
25
29
|
const claudeDir = join(projectDir, '.claude');
|
|
26
30
|
const lock = readLock(claudeDir);
|
|
27
31
|
const problems = [];
|
|
@@ -37,7 +41,7 @@ export async function doctor(projectDir, log = console.log, { strict = false, fr
|
|
|
37
41
|
if (stale) problems.push(`${stale} rule(s) have paths out of date with layout.json (run update)`);
|
|
38
42
|
|
|
39
43
|
const layout = JSON.parse(readFileSync(join(claudeDir, 'layout.json'), 'utf8'));
|
|
40
|
-
for (const f of Object.values(layout.folders)) if (!existsSync(join(projectDir, f.path))) problems.push(`layout folder missing: ${f.path}/`);
|
|
44
|
+
for (const f of Object.values(layout.folders)) if (!f.planned && !existsSync(join(projectDir, f.path))) problems.push(`layout folder missing: ${f.path}/`);
|
|
41
45
|
|
|
42
46
|
const settings = existsSync(join(claudeDir, 'settings.json')) ? readFileSync(join(claudeDir, 'settings.json'), 'utf8') : '';
|
|
43
47
|
if (!settings.includes('.claude/hooks/lesson-checks.mjs')) problems.push('hooks not registered in .claude/settings.json');
|
|
@@ -93,6 +97,9 @@ export async function doctor(projectDir, log = console.log, { strict = false, fr
|
|
|
93
97
|
|
|
94
98
|
for (const name of lock.packs) problems.push(...manifestProblems(readPack(name), name));
|
|
95
99
|
problems.push(...await hookProofProblems(projectDir, settings)); // a guard that fails open fails silently (0.18.0)
|
|
100
|
+
const toDo = (found) => (installing ? found.forEach((f) => log(` note: ${f}`)) : problems.push(...found));
|
|
101
|
+
toDo(paramLockProblems(projectDir)); // a lock holding no ids protects nothing, and says nothing (KF-15)
|
|
102
|
+
toDo(buildQueueProblems(projectDir, layout)); // a queue that is not there queues nothing (KF-1)
|
|
96
103
|
problems.push(...await pageGateProblems(projectDir, layout)); // UI code is proved rendered (0.17.0)
|
|
97
104
|
problems.push(...await hoverGateProblems(projectDir, layout)); // UI code needs a declared hover gate (0.10.1, Stellix B-261)
|
|
98
105
|
problems.push(...tokenProblems(projectDir, layout.siblings ?? []));
|
|
@@ -101,9 +108,13 @@ export async function doctor(projectDir, log = console.log, { strict = false, fr
|
|
|
101
108
|
problems.push(...timingProblems(projectDir, log)); // each hook's median and worst from .claude/hooks.log; a median over 500 ms is red (KWB-24)
|
|
102
109
|
|
|
103
110
|
const cov = await coverage(projectDir, sourceFilesOf(projectDir, lock.packs)); // the configuration says what is source (KWB-32)
|
|
111
|
+
const ruleTests = ruleTestProblems(projectDir, layout, cov); // a rule's own test can be missing for a whole build (KF-31)
|
|
112
|
+
if (layout.ruleTests) toDo(ruleTests); // a new law never turns an installed project red: the key in layout.json is the choice
|
|
113
|
+
else ruleTests.forEach((f) => log(` note: ${f}\n To be held to this, add "ruleTests": {} to .claude/layout.json.`));
|
|
104
114
|
if (cov.sourceCount > 0) {
|
|
105
115
|
const say = (msg) => (strict ? problems.push(msg) : log(` note: ${msg}`));
|
|
106
116
|
if (cov.deadRules.length) say(`${cov.deadRules.length} rule(s) match no file yet, so they never fire: ${cov.deadRules.slice(0, 8).join(', ')}${cov.deadRules.length > 8 ? ', …' : ''}`);
|
|
117
|
+
if (cov.waitingRules.length) log(` note: ${cov.waitingRules.length} rule(s) wait for their first file: their folders hold nothing yet`);
|
|
107
118
|
if (cov.uncovered.length) say(`${cov.uncovered.length} source file(s) no rule covers (outside the structure?): ${cov.uncovered.slice(0, 8).join(', ')}${cov.uncovered.length > 8 ? ', …' : ''}`);
|
|
108
119
|
}
|
|
109
120
|
|