simt-dev 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +209 -7
  2. package/README.md +32 -5
  3. package/docs/decisions/ADR-001-push-requires-approval.md +5 -5
  4. package/docs/decisions/ADR-002-refactor-blast-radius.md +1 -1
  5. package/docs/decisions/ADR-003-test-breadth.md +4 -4
  6. package/docs/decisions/ADR-004-hook-owns-format.md +5 -5
  7. package/docs/decisions/ADR-005-file-length.md +5 -5
  8. package/docs/decisions/ADR-006-coverage-bar.md +3 -3
  9. package/docs/decisions/ADR-007-section-skeleton.md +5 -5
  10. package/docs/decisions/ADR-008-verdict-vocabulary.md +1 -1
  11. package/docs/decisions/ADR-009-skill-layout.md +1 -1
  12. package/docs/decisions/ADR-010-runner-invocation.md +1 -1
  13. package/docs/decisions/ADR-012-drop-windows-support.md +2 -2
  14. package/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +2 -2
  15. package/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +13 -13
  16. package/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +2 -2
  17. package/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +9 -9
  18. package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +34 -7
  19. package/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +2 -2
  20. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +5 -5
  21. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +2 -2
  22. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
  23. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +2 -2
  24. package/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +3 -3
  25. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +4 -4
  26. package/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +2 -2
  27. package/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +3 -3
  28. package/docs/decisions/ADR-046-one-rule-one-copy.md +5 -5
  29. package/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +4 -4
  30. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +5 -5
  31. package/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
  32. package/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
  33. package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
  34. package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
  35. package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
  36. package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
  37. package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
  38. package/docs/decisions/README.md +20 -1
  39. package/package.json +1 -2
  40. package/rules/stacks/dart.md +38 -0
  41. package/rules/stacks/deno.md +36 -0
  42. package/rules/stacks/dotnet.md +35 -0
  43. package/rules/stacks/elixir.md +39 -0
  44. package/rules/stacks/go.md +34 -0
  45. package/rules/stacks/ruby.md +36 -0
  46. package/rules/stacks/rust.md +34 -0
  47. package/rules/stacks/swift.md +37 -0
  48. package/src/argv.mjs +35 -6
  49. package/src/ci/dependabot.mjs +1 -1
  50. package/src/ci/workflow.mjs +33 -11
  51. package/src/commands/graph.mjs +13 -4
  52. package/src/commands/help.mjs +22 -4
  53. package/src/commands/hooks.mjs +183 -67
  54. package/src/commands/index.mjs +53 -45
  55. package/src/commands/init.mjs +156 -14
  56. package/src/commands/rules.mjs +54 -3
  57. package/src/commands/skills.mjs +143 -15
  58. package/src/commands/skillsinstall.mjs +4 -4
  59. package/src/commands/sync.mjs +153 -11
  60. package/src/compare/discover.mjs +1 -1
  61. package/src/compare/hooksstate.mjs +96 -9
  62. package/src/compare/row.mjs +36 -7
  63. package/src/decisions/index.mjs +51 -12
  64. package/src/graph/load.mjs +126 -15
  65. package/src/hooks/capability.mjs +49 -5
  66. package/src/hooks/dispatcher.mjs +51 -6
  67. package/src/hooks/gates.mjs +1 -3
  68. package/src/hooks/resolve.mjs +24 -25
  69. package/src/hooks/shim.mjs +29 -5
  70. package/src/hooks/where.mjs +1 -1
  71. package/src/identity.mjs +207 -0
  72. package/src/mcp/tools.mjs +2 -2
  73. package/src/output/index.mjs +8 -7
  74. package/src/package-version.mjs +74 -0
  75. package/src/projection/baselines.mjs +1 -1
  76. package/src/projection/conflicts.mjs +57 -6
  77. package/src/projection/manifest.mjs +248 -26
  78. package/src/projection/markers.mjs +3 -3
  79. package/src/projection/mdc.mjs +6 -15
  80. package/src/projection/project.mjs +1 -1
  81. package/src/projection/rules.mjs +21 -8
  82. package/src/projection/surfaces.mjs +2 -2
  83. package/src/projection/targets.mjs +53 -7
  84. package/src/resolve/eval.mjs +64 -0
  85. package/src/resolve/fingerprint.mjs +4 -3
  86. package/src/resolve/near.mjs +26 -7
  87. package/src/resolve/resolve.mjs +1 -1
  88. package/src/resolve/source.mjs +2 -2
  89. package/src/tsconfig.mjs +1 -1
  90. package/src/workspace.mjs +0 -9
  91. package/graph/skills.graph.json +0 -1622
@@ -64,6 +64,10 @@ record's status line — never by editing the ruling in place.
64
64
  | [054](ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md) | The digest covers every byte that leaves the package, and a cause is read off the body | accepted · amends ADR-032 |
65
65
  | [055](ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md) | A status line travels, so it may not name a location; prose is the record's own argument | accepted |
66
66
  | [056](ADR-056-three-names-one-of-which-is-cheap-to-move.md) | The command, the package and the project are three names, and only one of them is cheap to move | accepted · amends ADR-023 |
67
+ | [057](ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md) | An identity is per-developer, and a repository does not own it | accepted |
68
+ | [058](ADR-058-one-tool-three-channels-and-the-order-is-forced.md) | One tool, three channels, and the order is forced | accepted |
69
+ | [059](ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md) | The version a repository runs is the one it pinned, and the gate refuses otherwise | accepted |
70
+ | [060](ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md) | A repository with nothing to run is adopted and says so; a gate is verified where it runs | accepted |
67
71
 
68
72
  ADR-001 to ADR-010 resolve the contradictions found across the 16 repos in the 2026-10-05
69
73
  audit; each record names the repos whose text it replaces. ADR-011 and ADR-012 are the
@@ -400,6 +404,21 @@ docstring that used to claim the whole output "never contains a path" is withdra
400
404
  asserted a property the function never had, which is the same defect as a citation that
401
405
  cannot be resolved — a sentence that reads like evidence and carries none.
402
406
 
407
+ ADR-059 is the first record here whose subject is the version of the tool itself, and it
408
+ exists because a check that runs from the wrong binary is a pass that never happened. ADR-010
409
+ allowed a tool that is not a project dependency to be invoked directly, *provided its version
410
+ is pinned* — and for a repository without npm that "pinned" half had no mechanism. The version
411
+ on `PATH` was whatever the last global install left there, and the gate that ran on commit was
412
+ the one in it: a green tick from a check running a different gate than CI is worse than a red
413
+ build, because it is the failure that looks like evidence. The record rules that the version a
414
+ repository adopted is recorded in the manifest, that the gate refuses when the running binary
415
+ differs, and that the install and the remedy are per kind — so the check CI runs is the one the
416
+ machine remedy names. Its most load-bearing consequence is the one that keeps it from becoming a
417
+ trap: a repository adopted before the field existed has nothing to compare against, so it is
418
+ skipped rather than failed, and reported as drift instead — because a gate that turns red on the
419
+ day a check ships, for a mismatch nobody can resolve until they re-adopt, is a gate no one will
420
+ trust again.
421
+
403
422
  ## The rule these records enforce
404
423
 
405
424
  Every shared rule has exactly **one** canonical home. Every other location is either a
@@ -418,7 +437,7 @@ ADR-018's guard could not see the default hooks directory because it looked only
418
437
  key, and `hooks status` carried its own copy of the read logic the shared module already had.
419
438
 
420
439
  A fourth joins them. ADR-019 is the same shape one layer over: `simt init` dropped all
421
- five gates on `simt-app` and told the reader to add a script to a `package.json` the
440
+ five gates on `repo-05` and told the reader to add a script to a `package.json` the
422
441
  repository will never have. The facts were right, the remedy was not followable, and once
423
442
  the repository declared what it does have, the dispatcher, `check` and `hooks status`
424
443
  gave three different answers about one manifest. Every one of these shipped green because
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simt-dev",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Shared agent rules, skills and git hooks for a repository: one command adopts a repo, and every gate it declares is probed before it is written.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Said Elimam",
@@ -19,7 +19,6 @@
19
19
  "files": [
20
20
  "bin",
21
21
  "src",
22
- "graph",
23
22
  "rules",
24
23
  "docs/decisions",
25
24
  "CHANGELOG.md"
@@ -0,0 +1,38 @@
1
+ ---
2
+ simt-id: dart
3
+ simt-title: Dart
4
+ simt-applies: [has-pubspec]
5
+ ---
6
+
7
+ # Stack profile: Dart
8
+
9
+ Applies to: `has-pubspec` — a `pubspec.yaml`. A Flutter project is a Dart project
10
+ with the `flutter` SDK dependency; the Flutter-specific rules live with the tools
11
+ that build it, not here.
12
+
13
+ ## Build and gates
14
+
15
+ - `dart analyze`, `dart test` are the entrypoints; `dart format --output=none
16
+ --set-exit-if-changed .` is the formatting gate. A Flutter project runs the same
17
+ commands through `flutter`, and the two must not be mixed in one change.
18
+ - Dependencies are declared in `pubspec.yaml`; `pub get` resolves them into
19
+ `pubspec.lock`, which is committed for an application. A package that imports a
20
+ library the manifest does not list resolves only by accident.
21
+
22
+ ## Style
23
+
24
+ - `package:lints` (or the repo's stricter set) is the lint source; a rule the repo
25
+ disabled is a decision, and re-enabling it is a change, not a fix.
26
+ - No `dynamic` where a real type exists. A `dynamic` that is guessed rather than
27
+ checked is a hole the analyzer was told to stop looking through.
28
+ - `late` and `?` are decisions: a field that is `late` because the author did not
29
+ know when it is set is a defect, not a convenience.
30
+ - Prefer a small `sealed` class hierarchy over booleans; a pair of `bool` fields is
31
+ four states, one of which is always wrong.
32
+
33
+ ## Testing
34
+
35
+ - `dart test` is the gate; a library with no tests is allowed to have none, but a
36
+ change that touches one must not break its neighbours.
37
+ - Assert on values through the public API; a test that reaches into a private field
38
+ is coupled to an implementation it should not own.
@@ -0,0 +1,36 @@
1
+ ---
2
+ simt-id: deno
3
+ simt-title: Deno
4
+ simt-applies: [has-deno-config]
5
+ ---
6
+
7
+ # Stack profile: Deno
8
+
9
+ Applies to: `has-deno-config` — a `deno.json` or `deno.jsonc`.
10
+
11
+ ## Build and gates
12
+
13
+ - `deno test`, `deno check .` are the entrypoints; `deno fmt --check` and
14
+ `deno lint` are the formatting and lint gates. A repo that vendors dependencies
15
+ runs them with `--vendor`; one that does not must not add the flag.
16
+ - The lockfile (`deno.lock`) is committed where it exists; a build that ignores it
17
+ is not reproducible. A lockfile the repo chose to delete is a decision, and
18
+ re-adding it is a change, not a fix.
19
+ - Import from `npm:` specifiers where the repo does; do not introduce a second
20
+ registry convention into a repo that has one.
21
+
22
+ ## Style
23
+
24
+ - Types are checked with `deno check .`; a file that compiles under `deno run` but
25
+ not under `deno check` is a defect the gate exists to catch.
26
+ - No `any`. An `unknown` that is narrowed at the boundary is a type; an `any` is a
27
+ hole the checker was told to stop looking through.
28
+ - Prefer a small `enum`-shaped union over booleans; a pair of `boolean` fields is
29
+ four states, one of which is always wrong.
30
+
31
+ ## Testing
32
+
33
+ - `Deno.test` is the runner; a test that needs a fixture reads it from a path
34
+ relative to the test file, never from a machine-specific location.
35
+ - Assert on values through the public API; a test that reaches into module-private
36
+ state is coupled to an implementation it should not own.
@@ -0,0 +1,35 @@
1
+ ---
2
+ simt-id: dotnet
3
+ simt-title: .NET
4
+ simt-applies: [has-nuget]
5
+ ---
6
+
7
+ # Stack profile: .NET
8
+
9
+ Applies to: `has-nuget` — a `.csproj` or solution.
10
+
11
+ ## Build and gates
12
+
13
+ - `dotnet build`, `dotnet test` are the entrypoints; `dotnet format
14
+ --verify-no-changes` is the formatting gate. A solution builds with `dotnet build
15
+ <solution>`, never a bare project path that skips the solution's configuration.
16
+ - Build with `-warnaserror` where the repo sets it; a warning that is not an error
17
+ is a defect the gate is being asked to ignore.
18
+ - Restore is deterministic: commit `global.json` and any NuGet.config the repo has,
19
+ so a build does not depend on a machine's cache.
20
+
21
+ ## Style
22
+
23
+ - Prefer `required` members and `init` over a public setter that exists to satisfy
24
+ the constructor. A mutable property nobody should write is a hole in the type.
25
+ - `async`/`await` all the way: no `.Result`, no `.Wait()`. A synchronous call into
26
+ an async boundary is a deadlock waiting for the thread pool.
27
+ - Nullability is on: `#nullable enable` in new code, and a `?` that is guessed
28
+ rather than checked is a defect.
29
+
30
+ ## Testing
31
+
32
+ - `dotnet test` runs xUnit, NUnit or MSTest — whichever the project references; do
33
+ not add a second framework to a repo that has one.
34
+ - Assert on behaviour through the public surface; a test that reaches into a private
35
+ field is coupled to an implementation it should not own.
@@ -0,0 +1,39 @@
1
+ ---
2
+ simt-id: elixir
3
+ simt-title: Elixir
4
+ simt-applies: [has-mix]
5
+ ---
6
+
7
+ # Stack profile: Elixir
8
+
9
+ Applies to: `has-mix` — a `mix.exs`.
10
+
11
+ ## Build and gates
12
+
13
+ - `mix test` is the entrypoint; `mix format --check-formatted` is the formatting
14
+ gate. A repo that uses `mix credo` runs it with `mix credo --strict` where the
15
+ `.credo.exs` exists; a check the repo configured is a decision, and skipping it
16
+ is a change, not a fix.
17
+ - Dependencies are declared in `mix.exs`; `mix deps.get` resolves them into
18
+ `mix.lock`, which is committed. A module that imports a library the manifest does
19
+ not list compiles only by accident.
20
+ - A release is built with `mix release` where the repo defines one; do not run a
21
+ `mix run` against a release's expectations.
22
+
23
+ ## Style
24
+
25
+ - Pattern-match on the way in, not guard-and-raise on the way out. A function that
26
+ takes a map and digs three levels deep is a boundary somebody refused to name;
27
+ match the shape at the edge and pass values in.
28
+ - Errors are returned, not raised, for expected failures; `raise` is for bugs, not
29
+ for a missing record. A caller that cannot handle the failure is a defect in the
30
+ caller's contract, not a reason to crash.
31
+ - Prefer a small `struct` with named fields over a map of strings; a `%{}` that is
32
+ passed through five functions is a type somebody refused to give a name.
33
+
34
+ ## Testing
35
+
36
+ - `mix test` is the gate; ExUnit is the runner, and a change that touches a module
37
+ must not break its neighbours.
38
+ - Assert on behaviour through the public API; a test that reaches into a private
39
+ field is coupled to an implementation it should not own.
@@ -0,0 +1,34 @@
1
+ ---
2
+ simt-id: go
3
+ simt-title: Go
4
+ simt-applies: [has-go-mod]
5
+ ---
6
+
7
+ # Stack profile: Go
8
+
9
+ Applies to: `has-go-mod` — a module.
10
+
11
+ ## Build and gates
12
+
13
+ - `go build ./...`, `go test ./...` are the entrypoints; `go vet ./...` runs with
14
+ them. Never rely on a file the build does not include.
15
+ - Format with `gofmt` (or `go fmt ./...`). A diff that is only formatting is a
16
+ defect: the gate fails closed on it.
17
+ - Depend on the module, not on a `GOPATH` layout. A package import that resolves
18
+ only because of a local checkout is a broken dependency.
19
+
20
+ ## Style
21
+
22
+ - Errors are values, not exceptions. Wrap with `fmt.Errorf("...: %w", err)` so the
23
+ caller can `errors.Is`/`errors.As`; a bare string loses the chain.
24
+ - `context.Context` is the first parameter of anything that can be cancelled; never
25
+ store one in a struct.
26
+ - Table-driven tests: one `for range` over named cases beats ten copy-pasted
27
+ subtests.
28
+
29
+ ## Testing
30
+
31
+ - `go test ./...` is the gate; a package with no test file is allowed to have none,
32
+ but a change that touches one must not break its neighbours.
33
+ - Benchmark with `-bench` only where the number is a decision; an unowned benchmark
34
+ is decoration.
@@ -0,0 +1,36 @@
1
+ ---
2
+ simt-id: ruby
3
+ simt-title: Ruby
4
+ simt-applies: [has-gemfile]
5
+ ---
6
+
7
+ # Stack profile: Ruby
8
+
9
+ Applies to: `has-gemfile` — a Bundled application or gem.
10
+
11
+ ## Build and gates
12
+
13
+ - `bundle install` then `bundle exec rspec` are the entrypoints; run every command
14
+ through `bundle exec`, never a bare binary, so the Gemfile.lock is the only
15
+ source of truth for versions.
16
+ - `bundle exec rubocop` is the lint gate where the repo has a `.rubocop.yml`; a
17
+ cop the repo disabled is a decision, and re-enabling it is a change, not a fix.
18
+ - `Gemfile.lock` is committed for an application; for a library it is not, and the
19
+ distinction is a decision the repo made, not one to reverse silently.
20
+
21
+ ## Style
22
+
23
+ - Frozen string literals (`# frozen_string_literal: true`) at the top of new files
24
+ where the repo already uses them; a mixed repo is a transition, not a licence to
25
+ pick either.
26
+ - Prefer `case`/`when` or a hash over a chain of `if`/`elsif` on one variable; a
27
+ five-branch if-chain is a lookup table somebody refused to name.
28
+ - `raise` with a message that names the failure and the value that caused it; a
29
+ bare `raise` is a defect the next reader cannot debug.
30
+
31
+ ## Testing
32
+
33
+ - RSpec is the runner where the repo has one; do not add a second framework to a
34
+ repo that has one.
35
+ - Assert on behaviour through the public interface; a spec that stubs everything
36
+ and asserts on the stub is testing the stub, not the code.
@@ -0,0 +1,34 @@
1
+ ---
2
+ simt-id: rust
3
+ simt-title: Rust
4
+ simt-applies: [has-cargo-toml]
5
+ ---
6
+
7
+ # Stack profile: Rust
8
+
9
+ Applies to: `has-cargo-toml` — a Cargo workspace or crate.
10
+
11
+ ## Build and gates
12
+
13
+ - `cargo build`, `cargo test` are the entrypoints; a workspace builds with
14
+ `cargo build --workspace`. Never invoke a toolchain the manifest does not pin.
15
+ - Format with `cargo fmt`; lint with `cargo clippy -- -D warnings`. A warning is a
16
+ defect, not a note: fix it or the gate fails.
17
+ - Pin the toolchain in `rust-toolchain.toml` where one is committed; a rustup
18
+ default on the developer's machine is not a floor anything enforces.
19
+
20
+ ## Style
21
+
22
+ - No `unwrap()` or `expect()` outside tests. A panic path in library code is an
23
+ unhandled error; return a `Result` and let the caller decide.
24
+ - `#[must_use]` on fallible constructors; a `Result` that is ignored is a bug the
25
+ compiler should flag.
26
+ - Prefer a small `enum` of states over booleans; a pair of `bool` fields is four
27
+ states, one of which is always wrong.
28
+
29
+ ## Testing
30
+
31
+ - Unit tests live in `#[cfg(test)]` next to the code; integration tests in `tests/`
32
+ exercise the public API only.
33
+ - Assert on values, not on panic messages. A test that asserts a panic string has
34
+ coupled itself to an implementation detail it should not own.
@@ -0,0 +1,37 @@
1
+ ---
2
+ simt-id: swift
3
+ simt-title: Swift
4
+ simt-applies: [has-swift-package]
5
+ ---
6
+
7
+ # Stack profile: Swift
8
+
9
+ Applies to: `has-swift-package` — a Swift package.
10
+
11
+ ## Build and gates
12
+
13
+ - `swift build`, `swift test` are the entrypoints; `swift format lint -r .` is the
14
+ formatting gate. A platform build (Xcode) is a different question from a package
15
+ build — do not conflate the two in one command.
16
+ - Pin the toolchain where the repo does (a `Package.swift` tools version or a
17
+ pinned Xcode); a compiler on the developer's machine is not a floor anything
18
+ enforces.
19
+ - Dependencies are declared in `Package.swift`; a package that imports a module the
20
+ manifest does not list builds only by accident.
21
+
22
+ ## Style
23
+
24
+ - Concurrency is checked, not assumed: `Sendable` conformance is a decision. A
25
+ `@unchecked Sendable` that papers over a real race is a defect the compiler was
26
+ told to stop reporting.
27
+ - `throws` for expected failures, not `fatalError`. A crash path in library code is
28
+ an unhandled error; return a `Result` or throw.
29
+ - Prefer a small `enum` of states over optionals of optionals; a triple `??` is a
30
+ state machine somebody refused to name.
31
+
32
+ ## Testing
33
+
34
+ - `swift test` is the gate; a target with no tests is allowed to have none, but a
35
+ change that touches one must not break its neighbours.
36
+ - Assert on values through the public API; a test that reaches into an internal
37
+ field is coupled to an implementation it should not own.
package/src/argv.mjs CHANGED
@@ -12,23 +12,34 @@
12
12
 
13
13
  import { EXIT, fail } from './output/index.mjs';
14
14
 
15
- const BOOL_FLAGS = new Set([
15
+ // `--quiet`, `--semantic` and `--mermaid` were here and were read by nothing (review
16
+ // 0001 filed it; it survived every change to this file since). Each was a plan's
17
+ // intention rather than a feature: `--semantic` was to be an explicitly non-canonical
18
+ // escape hatch, `--mermaid` belonged to a `skills graph` command that was never built,
19
+ // and `--quiet` was to shorten `help`. The plans keep those records; the parser does not
20
+ // need to keep a promise no code makes.
21
+ //
22
+ // An accepted flag that changes nothing is worse than an absent one. `--nonsense` exits
23
+ // 2 and tells the reader the flag does not exist; `--quiet` exits 0 and tells them
24
+ // nothing happened, which is indistinguishable from the output being quiet on purpose.
25
+ // Removing them makes `test/help.test.mjs`'s "every flag is described" assertion able to
26
+ // mean something, which it could not while three accepted flags had no description.
27
+
28
+ export const BOOL_FLAGS = new Set([
16
29
  'json',
17
30
  'all',
18
31
  'dry-run',
19
32
  'yes',
20
33
  'check',
21
- 'quiet',
22
34
  'plan',
23
- 'semantic',
24
- 'mermaid',
25
35
  'help',
26
36
  'version',
27
37
  'accept-conflicts',
38
+ 'backup',
28
39
  'list',
29
40
  ]);
30
41
 
31
- const VALUE_FLAGS = new Set([
42
+ export const VALUE_FLAGS = new Set([
32
43
  'budget',
33
44
  'max-bytes',
34
45
  'paths',
@@ -43,6 +54,21 @@ const VALUE_FLAGS = new Set([
43
54
  'surfaces',
44
55
  ]);
45
56
 
57
+ /**
58
+ * Flags that accumulate rather than overwrite.
59
+ *
60
+ * `--skill` was documented as "(repeatable)" and was not: `flags.skill = value` meant
61
+ * `--skill a --skill b` resolved to `b`, with `a` silently dropped. No error, no
62
+ * warning, and a brief that quietly lacked a skill the caller named — which is the one
63
+ * failure mode a tool whose job is to be explainable cannot have (ADR-009, "the answer
64
+ * is a function of what was asked").
65
+ *
66
+ * Comma separation still works, because the consumer splits on commas either way. This
67
+ * is a superset of the old behaviour, not a replacement: `--skill a,b` parsed before it
68
+ * parses now.
69
+ */
70
+ export const REPEATABLE_FLAGS = new Set(['skill']);
71
+
46
72
  /**
47
73
  * @param {string[]} argv raw arguments, excluding node and the script path
48
74
  * @returns {{ok: true, value: object} | {ok: false, code: number}}
@@ -96,7 +122,10 @@ export function parse(argv) {
96
122
  }
97
123
  flags[camel(raw)] = n;
98
124
  } else {
99
- flags[camel(raw)] = value;
125
+ const key = camel(raw);
126
+ flags[key] = REPEATABLE_FLAGS.has(raw)
127
+ ? [...(Array.isArray(flags[key]) ? flags[key] : []), value]
128
+ : value;
100
129
  }
101
130
  continue;
102
131
  }
@@ -142,7 +142,7 @@ function header(own) {
142
142
  # only writer here, so an entry is added deliberately rather than as a side effect.
143
143
  `;
144
144
  }
145
- return `# Written by simt-dev when this repo was adopted. Yours from here: simt-dev
145
+ return `# Written by simt-dev when this repo was adopted. Yours from here: ${CLI}
146
146
  # creates this file once and never rewrites it, so your own cadence, ignore rules
147
147
  # and groups stay yours.
148
148
  #
@@ -50,6 +50,7 @@ import { PACKAGE_ROOT } from '../workspace.mjs';
50
50
  import { PACKAGE_NAME, WORKFLOW_FILE_NAME } from '../name.mjs';
51
51
  import { renderLocator } from '../locate.mjs';
52
52
  import { CLI } from '../name.mjs';
53
+ import { packageVersion } from '../package-version.mjs';
53
54
 
54
55
  /** Directory whose presence means this repo runs GitHub Actions. */
55
56
  export const WORKFLOWS_DIR = '.github/workflows';
@@ -169,12 +170,16 @@ function skeleton(name, steps, { matrix = null } = {}) {
169
170
  const job = matrix?.node ? '\${{ matrix.os }} node \${{ matrix.node }}' : name;
170
171
  return `name: ${name}
171
172
 
173
+ # Pull requests only, and that is a decision rather than a default.
174
+ #
175
+ # A gate that runs on every push re-runs itself for every commit that pushes a
176
+ # branch forward, including the ones nobody asked it about, and its verdict has to
177
+ # be cleared before the next one lands. \`pull_request\` asks one question — may this
178
+ # merge — and answers it once. \`main\` here is the branch protection list, not a
179
+ # trigger: the gate that has to pass is the one the pull request runs, and the one
180
+ # on \`main\` is whatever CI the repository already has there.
172
181
  on:
173
182
  pull_request:
174
- push:
175
- branches:
176
- - main
177
- - master
178
183
 
179
184
  # Read-only is enough to check a repo, and a gate job does not need to write to it.
180
185
  permissions:
@@ -235,7 +240,7 @@ function indent(text, spaces) {
235
240
  * @param {{own?: boolean}} [options]
236
241
  * @returns {string} YAML, LF-terminated
237
242
  */
238
- export function renderWorkflow({ own = false } = {}) {
243
+ export function renderWorkflow({ own = false, npm = true, version } = {}) {
239
244
  if (own) {
240
245
  const floor = nodeFloor();
241
246
  return `${header(true)}${skeleton(
@@ -267,17 +272,34 @@ export function renderWorkflow({ own = false } = {}) {
267
272
  )}\n`;
268
273
  }
269
274
 
270
- const locator = renderLocator('check --root "$ROOT"', {
271
- remedy: `REMEDY run: npm i -D ${PACKAGE_NAME} (the gate runs from the pinned dependency)`,
272
- });
275
+ const v = version ?? packageVersion();
276
+ // The install step is the whole of S35's consumer CI: a repository with npm pins
277
+ // simt-dev as a devDependency and `npm ci` restores it; one without (Gradle,
278
+ // Connect IQ) has no node_modules to pin in, so CI installs the same pinned global
279
+ // binary the machine remedy names. The two must agree, or the gate passes in one
280
+ // place and cannot start in the other.
281
+ const install = npm
282
+ ? [
283
+ ' - name: install dependencies',
284
+ ' # `npm ci` needs a lockfile; a repo without one is still gated, not skipped.',
285
+ ' run: npm ci || npm install',
286
+ ]
287
+ : [
288
+ ' - name: install the pinned gate binary',
289
+ ' # No node_modules to pin in, so a pinned global install: the same bytes',
290
+ ' # the machine remedy names, so the gate cannot pass locally and fail here.',
291
+ ` run: npm i -g ${PACKAGE_NAME}@${v}`,
292
+ ];
293
+ const remedy = npm
294
+ ? `REMEDY run: npm i -D ${PACKAGE_NAME} (the gate runs from the pinned dependency)`
295
+ : `REMEDY run: npm i -g ${PACKAGE_NAME}@${v} (the gate runs from a pinned global install)`;
296
+ const locator = renderLocator('check --root "$ROOT"', { remedy });
273
297
  return `${header(false)}${skeleton(
274
298
  `${CLI} check`,
275
299
  [
276
300
  uses('checkout'),
277
301
  setupNode(nodeFloor()),
278
- ' - name: install dependencies',
279
- ' # \`npm ci\` needs a lockfile; a repo without one is still gated, not skipped.',
280
- ' run: npm ci || npm install',
302
+ ...install,
281
303
  ` - name: ${CLI} check`,
282
304
  ' run: |',
283
305
  indent(`set -eu\nROOT="\${GITHUB_WORKSPACE}"\n${locator}`, 10),
@@ -7,11 +7,17 @@
7
7
  // the offending node, the file it disagrees with, and both numbers.
8
8
 
9
9
  import { EXIT, emit, fail, isJson } from '../output/index.mjs';
10
- import { loadRegistry, REGISTRY_PATH } from '../graph/load.mjs';
10
+ import { loadRegistry } from '../graph/load.mjs';
11
+ import { CLI } from '../name.mjs';
11
12
 
12
13
  export async function lintCommand(positional, flags) {
13
- const path = positional[0] ?? REGISTRY_PATH;
14
- const { ok, registry, errors, warnings, verified, workspace } = await loadRegistry(path, flags);
14
+ // The resolved path comes back from the loader, because when no positional is given it
15
+ // falls back to the member that holds the registry in this workspace (ADR-061) - and the
16
+ // reader should see where it actually looked, not a constant that no longer exists.
17
+ const { ok, registry, errors, warnings, verified, workspace, path } = await loadRegistry(
18
+ positional[0] ?? null,
19
+ flags,
20
+ );
15
21
 
16
22
  if (isJson(flags)) {
17
23
  emit(
@@ -40,7 +46,10 @@ export async function lintCommand(positional, flags) {
40
46
  }
41
47
 
42
48
  if (!registry) {
43
- return fail(errors[0] ?? `${path}: unreadable`, EXIT.REGISTRY);
49
+ // The default remedy for this exit code is `run: simt-dev graph lint`, which from
50
+ // inside `graph lint` is circular: the reader just ran it and got this. Name the
51
+ // argument the command takes, because an explicit path is the way out (ADR-061).
52
+ return fail(errors[0] ?? `${path}: unreadable`, EXIT.REGISTRY, `run: ${CLI} graph lint <path-to-registry>`);
44
53
  }
45
54
 
46
55
  const lines = [
@@ -4,7 +4,8 @@ import { CLI } from '../name.mjs';
4
4
 
5
5
  const COMMANDS = `${CLI} init [path] adopt or scaffold a repo into the ecosystem
6
6
  (dry-run unless --yes; conflicts block without
7
- --accept-conflicts; never commits or pushes)
7
+ --accept-conflicts; in place unless --backup;
8
+ never commits or pushes)
8
9
  ${CLI} doctor classify a repo and check config health
9
10
  ${CLI} check the gate: projection drift, manifest validity, policy
10
11
  ${CLI} sync [--yes] regenerate the projection (idempotent)
@@ -19,6 +20,10 @@ ${CLI} skills brief --task tier-1 pre-assembled brief <- the common case
19
20
  ${CLI} skills show <name> tier-2 body (respects --max-bytes)
20
21
  ${CLI} skills why <name> the signal chain that selected it
21
22
  ${CLI} skills find <query> search across capability and keyword
23
+ ${CLI} skills eval [corpus] measure the resolver against a gold corpus
24
+ (reports precision and recall; exits non-zero on
25
+ regression; without an argument, the corpus in this
26
+ checkout)
22
27
  ${CLI} skills install <name> pin a skill into the repo as a local override
23
28
  (dry-run unless --yes; a multi-variant skill needs
24
29
  --variant or --from rather than a guess)
@@ -26,9 +31,15 @@ ${CLI} skills install <name> pin a skill into the repo as a local override
26
31
  ${CLI} rules list the rule catalogue (--root marks what applies to a repo)
27
32
  ${CLI} rules show <id> one rule body
28
33
  ${CLI} rules why <id|ADR-NNN> the decision that settled it, and whether it is still in force
34
+ (--all prints every ruling, not the first two)
29
35
  ${CLI} hooks install [--yes] install the shared dispatcher; report what it would run
30
36
  (dry-run unless --yes; refuses to take over routing git
31
37
  already uses, and --yes is that consent)
38
+ ${CLI} hooks status what each git event routes to, and whether it is wired
39
+ ${CLI} hooks run <event> run one event's gates now, as git would
40
+ (pre-commit, pre-push, post-merge, ...)
41
+ ${CLI} graph lint check the skill registry against the skills on disk
42
+ (byte counts, dangling edges, unresolvable variants)
32
43
  ${CLI} ci simt-dev's own .github files: create or refresh them
33
44
  (dry-run unless --yes; refreshes the templates' own copies
34
45
  and no repository else's)
@@ -55,9 +66,16 @@ const FLAGS = `--json structured output
55
66
  file, then the sibling heuristic if it verifies)
56
67
  --dry-run report what would change, change nothing
57
68
  --yes assume yes; required in automation, never prompts
58
- --all ignore the budget and resolve everything
69
+ --check report drift and write nothing (sync, ci)
70
+ --all ignore a cap: every ruling of a record, or the skills budget
59
71
  --variant <id> which body of a multi-variant skill to use
60
- --from <repo/path> install from a declared source by name`;
72
+ --from <repo/path> install from a declared source by name
73
+ --list print the MCP tool surface without reading stdin (mcp)
74
+ --accept-conflicts init: adopt rules that collide with hand-written ones
75
+ --backup init: tag the pre-adoption commit and branch onto
76
+ simt-dev/adopt before writing (the undo path)
77
+ --help this text
78
+ --version the version, and the SHA it was built from`;
61
79
 
62
80
  const PRINCIPLES = `design rules:
63
81
  deterministic same repo + same task = same answer, forever
@@ -78,7 +96,7 @@ export async function help(argv) {
78
96
  [
79
97
  'simt-dev - shared agent rules, skills and gates for a repo ecosystem',
80
98
  '',
81
- 'usage: simt-dev <command> [args] [flags]',
99
+ `usage: ${CLI} <command> [args] [flags]`,
82
100
  '',
83
101
  'commands:',
84
102
  COMMANDS,