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
@@ -6,14 +6,14 @@ Affects: `simt-dev hooks install`, `simt-dev hooks status`, `simt-dev compare`,
6
6
 
7
7
  ## Context
8
8
 
9
- Phase 4 began by piloting `simt-dev init` on `simt-space`. The dry run was
9
+ Phase 4 began by piloting `simt-dev init` on `repo-15`. The dry run was
10
10
  unremarkable. The defect appeared when I read what `hooks install` would actually
11
11
  do to that repository, which is something I had not done in any of the earlier
12
12
  phases.
13
13
 
14
- `simt-space` has a hand-written `.git/hooks/pre-commit` — 4088 bytes, with prose
14
+ `repo-15` has a hand-written `.git/hooks/pre-commit` — 4088 bytes, with prose
15
15
  explaining why it deliberately does not run tests. `core.hooksPath` is unset.
16
- `simt-dev hooks status --root ../simt-space --json` reported:
16
+ `simt-dev hooks status --root ../repo-15 --json` reported:
17
17
 
18
18
  ```json
19
19
  { "hooksPath": null, "installed": [{ "event": "pre-commit", "present": false }],
@@ -64,7 +64,7 @@ question, which is why fixing the shared one changed nothing visible.
64
64
  1. **Ask git where hooks are, never infer it from whether config exists.**
65
65
  `resolveHooksDir(root)` implements the two rules directly: `core.hooksPath`
66
66
  when set (a relative value resolves against the top of the working tree, which
67
- is what makes `simt-app`'s `hooks` work), otherwise `<git-common-dir>/hooks`.
67
+ is what makes `repo-05`'s `hooks` work), otherwise `<git-common-dir>/hooks`.
68
68
 
69
69
  `git rev-parse --git-path hooks` is the obvious shortcut and is unusable. It
70
70
  returns `.git/hooks` in a normal repository, `hooks` in a linked worktree, and
@@ -103,7 +103,7 @@ question, which is why fixing the shared one changed nothing visible.
103
103
  7. **A foreign hook for an event this tool does not gate is chained, not stopped.**
104
104
  `core.hooksPath` does not scope this tool to the events it cares about and leave
105
105
  the rest to git; git reads *every* hook from the named directory and from nowhere
106
- else. So the moment routing lands, `simt-app`'s `post-checkout`, `post-commit`
106
+ else. So the moment routing lands, `repo-05`'s `post-checkout`, `post-commit`
107
107
  and `post-merge` — events this tool gates nothing about — stop running. Nothing
108
108
  fails, nothing is logged, and the hook is still on disk looking intact: the
109
109
  failure is indistinguishable from the hook having passed. Ruling 3's refusal
@@ -128,9 +128,9 @@ question, which is why fixing the shared one changed nothing visible.
128
128
  anything the author sees.
129
129
 
130
130
  "Does this tool gate it" is a fact about the manifest, not about the event vocabulary,
131
- and reading it the second way cost `simt-app` its `pre-push`. That hook is git-lfs.
131
+ and reading it the second way cost `repo-05` its `pre-push`. That hook is git-lfs.
132
132
  `pre-push` is an event this tool gates *in general*, so a vocabulary check displaced it
133
- — and because `simt-app` declares no `pre-push` gate there is no shim for that event
133
+ — and because `repo-05` declares no `pre-push` gate there is no shim for that event
134
134
  either, so git would have found no `pre-push` hook at all. LFS objects would have
135
135
  stopped being uploaded, with nothing to notice it: the same silent failure this ruling
136
136
  exists to prevent, reached by fixing the wrong half of it. The test asks the manifest.
@@ -154,8 +154,8 @@ a list that rots, so one test now asserts the two agree and another asserts ever
154
154
  row carries `notes` and `foreignHooks` even when empty.
155
155
 
156
156
  Measured across the estate after the fix, six repositories run hooks simt-dev did
157
- not write — `simt-space` (4088 B), `simt-app` (four hooks), `simt-social` (two),
158
- `simt-social-api` (two), `simt-landing` (one), and the `.wt-wear-redesign`
157
+ not write — `repo-15` (4088 B), `repo-05` (four hooks), `repo-14` (two),
158
+ `repo-11` (two), `repo-10` (one), and the `.wt-wear-redesign`
159
159
  worktree (five, including the `git-lfs` `pre-push`). Every one of them would have
160
160
  been silently taken over.
161
161
 
@@ -8,21 +8,21 @@ Affects: `simt-dev init`, `simt-dev check`, `simt-dev hooks status`, `simt-dev h
8
8
  ## Context
9
9
 
10
10
  ADR-017 made `simt-dev init` declare only gates a repository can actually run, and it shipped
11
- green because the first adopter — `simt-social-media` — is a Node repository whose scripts
11
+ green because the first adopter — `repo-13` — is a Node repository whose scripts
12
12
  and binaries are exactly what the filter understood. It was verified by running that
13
13
  repository's own tests.
14
14
 
15
- `simt-app`, the third pilot, is a Gradle repository. It has **no `package.json` at all**. The
15
+ `repo-05`, the third pilot, is a Gradle repository. It has **no `package.json` at all**. The
16
16
  dry run dropped all five proposed gates, and the remedy for each one was an instruction the
17
17
  reader cannot follow. The output as it stood, before this record was written:
18
18
 
19
19
  ```
20
- $ simt-dev init --root ../simt-app
20
+ $ simt-dev init --root ../repo-05
21
21
  dropped gate test: no "test" script in package.json
22
22
  remedy: add a "test" script to package.json, then re-run: simt-dev init --yes
23
23
  ```
24
24
 
25
- Ruling 4 changes both lines. `simt-app` will never have a `package.json`. "Install prettier as
25
+ Ruling 4 changes both lines. `repo-05` will never have a `package.json`. "Install prettier as
26
26
  a dev dependency" is equally impossible. The filter was right about the facts and wrong about
27
27
  what followed from them, and the reader's only honest reaction to that remedy is to stop
28
28
  trusting the next one.
@@ -72,13 +72,40 @@ One manifest. Three answers. The one I would have shipped if I had only run `che
72
72
  every file (`ENOTDIR`) and classified every Gradle repository in the estate as having no
73
73
  runner at all — a wrong answer from an evidence reader, which is worse than no answer
74
74
  because the caller cannot tell them apart.
75
+ 8. **A declared command is a shell command line, not an argv.** *(S17, 2026-10-12)*
76
+
77
+ It was split on whitespace and handed to `execFile` as an argv. That made a declaration
78
+ a repository would plausibly write run the wrong thing rather than fail: a flag whose
79
+ value contains a space became two arguments, and the quotes in a compound line were
80
+ passed to the program as literal characters. There was no way to express a quoted
81
+ argument at all, and nothing said so.
82
+
83
+ So `gates.commands[name]` is one line, run through `sh -c`, and everything the
84
+ repository wrote in it survives — quotes, pipes, `&&`, a path with a space. This is the
85
+ same line the repository would type into its own shell, which is the only definition of
86
+ "what this repository means by that" that does not require a parser here.
87
+
88
+ **The trust boundary is the repository, and it is not a sandbox.** The line comes from
89
+ the repository's own manifest, which it can read and change, and it runs on its author's
90
+ own machine when that author commits. `sh -c` adds no privilege: the same person who
91
+ wrote the line could run it. What `sh -c` must not do is apply to anything this tool
92
+ resolved itself — a formatter path out of `node_modules/.bin` stays an argv, because
93
+ handing it to a shell would change who decided its arguments for no gain. The two
94
+ sources are separate branches in the dispatcher rather than a flag, so the distinction
95
+ cannot be forgotten by a caller that does not know it exists.
96
+
97
+ **Consequence for a repository that relied on the split.** A declaration with no quotes
98
+ behaves identically, because whitespace-splitting a shell word that needed no quoting
99
+ is the same thing the shell does. Every use of `gates.commands` in this estate declares
100
+ a bare path and is unaffected; the change only makes declarations *possible* that were
101
+ previously impossible rather than making any of them wrong.
75
102
 
76
103
  ## Consequences
77
104
 
78
- - `simt-app` declares `i18n-completeness: scripts/check_string_translations.sh` on
105
+ - `repo-05` declares `i18n-completeness: scripts/check_string_translations.sh` on
79
106
  pre-commit — the gate its own hand-written pre-commit hook already runs — and
80
107
  `test: ./gradlew check` on pre-push.
81
- - The `simt-social-media` and `simt-space` manifests are unaffected: they declare no commands.
108
+ - The `repo-13` and `repo-15` manifests are unaffected: they declare no commands.
82
109
  - A Node repository that wants to override a gate now can, without abandoning the gate name it
83
110
  shares with every other repository. `gates.commands.format` is a supported thing to write.
84
111
  - `readCapability` reads one more file per invocation, and it reads it in a `try` that cannot
@@ -104,7 +131,7 @@ One manifest. Three answers. The one I would have shipped if I had only run `che
104
131
 
105
132
  ## Evidence
106
133
 
107
- `simt-app` is the first adopter with a non-npm runner and the first to declare a command for a
134
+ `repo-05` is the first adopter with a non-npm runner and the first to declare a command for a
108
135
  gate that is not of kind `command`. Every earlier adopter was a Node repository, which is why
109
136
  ADR-017's own verification passed. The estate's three runners — npm, Gradle and Python — were
110
137
  all in the repository set the whole time; only the pilot order had not reached them.
@@ -14,7 +14,7 @@ the machine of whoever remembers to run it, and a hook is a developer's machine.
14
14
  review is somebody else's, and every merge is a machine nobody gated.
15
15
 
16
16
  The plan called this "a reusable workflow", and named
17
- `QuazarMedia/simt-dev/.github/workflows/sync-check.yml` as a file "referenced by every
17
+ `the organisation/simt-dev/.github/workflows/sync-check.yml` as a file "referenced by every
18
18
  consumer". Nothing in this repository had a `.github/` at all. There were three ways to
19
19
  close the gap, and they differ in who the resulting file belongs to:
20
20
 
@@ -93,7 +93,7 @@ rules once told readers to edit a source tree they did not have.
93
93
 
94
94
  ## Rejected
95
95
 
96
- - **`uses: QuazarMedia/simt-dev/.github/workflows/sync-check.yml@main`.** Rejected under
96
+ - **`uses: the organisation/simt-dev/.github/workflows/sync-check.yml@main`.** Rejected under
97
97
  ruling 1: it is the plan's own suggestion, and it points every reader at bytes they do
98
98
  not have. Worth recording that the plan wrote it, because the next person to read §6
99
99
  will find it there.
@@ -19,8 +19,8 @@ empty", and deferred it to a record of its own.
19
19
 
20
20
  The estate confirms the first half and not the conclusion. Thirteen of sixteen repositories
21
21
  have no `.gitattributes` at all. The three that do disagree about almost everything:
22
- `simt-app` declares only LFS filters for `.tif` and `.zip`; `simt-garmin` declares line
23
- endings plus ConnectIQ binary markers; `simt-social-api` declares exactly one whitespace
22
+ `repo-05` declares only LFS filters for `.tif` and `.zip`; `repo-07` declares line
23
+ endings plus ConnectIQ binary markers; `repo-11` declares exactly one whitespace
24
24
  suppression for hash-locked vendored content. There is no divergence here of the kind a
25
25
  baseline fixes by being consistent. There is a file missing.
26
26
 
@@ -48,7 +48,7 @@ to be the line ruling 7 sent this file to make.
48
48
  because nothing in the pattern says those are binary. `text=auto` is a detection: git
49
49
  sniffs each path and applies normalisation only to files it concludes are text. This is
50
50
  the most common way a repository corrupts its own binaries, and it is invisible until
51
- someone diffs an image. `simt-garmin` had independently arrived at `text=auto eol=lf` on
51
+ someone diffs an image. `repo-07` had independently arrived at `text=auto eol=lf` on
52
52
  its first line, which is the strongest available evidence that the estate would pick the
53
53
  cautious form if asked — this ruling does not ask, it supplies.
54
54
 
@@ -96,8 +96,8 @@ what any file is". A future baseline for a per-stack attribute would have to arr
96
96
  different row with its own record, which is what §3.2's keep-it-local rule asks for anyway.
97
97
 
98
98
  The repositories with an existing `.gitattributes` are the interesting case and they are not
99
- in conflict: `simt-app`'s LFS filters, `simt-garmin`'s binary markers and
100
- `simt-social-api`'s whitespace suppression all stay beneath the region and all keep winning,
99
+ in conflict: `repo-05`'s LFS filters, `repo-07`'s binary markers and
100
+ `repo-11`'s whitespace suppression all stay beneath the region and all keep winning,
101
101
  because git takes the last match. No repository loses a line it wrote.
102
102
 
103
103
  This repository adopts neither baseline for itself, and the reason is the same one that keeps
@@ -10,7 +10,7 @@ a copied byte range. Settles the `tsconfig.base.json` item of §3.1 row 5 and ro
10
10
  ## Context
11
11
 
12
12
  Row 5 promised eight baselines and named two problems among them that are not about a file
13
- being missing: simt-goto's seven absent strictness flags, and `moduleResolution` spelled
13
+ being missing: repo-08's seven absent strictness flags, and `moduleResolution` spelled
14
14
  inconsistently across the workspace. ADR-025 ruled `tsconfig.base.json` out of the region
15
15
  mechanism because JSON has no comments, and its consequences promised that "the next fix is
16
16
  three more JSON files and therefore a different mechanism". ADR-026 supplied that mechanism
@@ -95,7 +95,7 @@ one an external document uses.
95
95
 
96
96
  A TypeScript repository now gets twelve flags it did not have, and one of them — the floor
97
97
  arriving through `extends` — is a behaviour change that can surface real type errors in
98
- `simt-goto`, which is the point of the record and also the reason adoption is one commit
98
+ `repo-08`, which is the point of the record and also the reason adoption is one commit
99
99
  with a `tsc` run rather than a silent upgrade. A base file is a floor, and a repository
100
100
  that had not been standing on one is the case where raising it finds something.
101
101
 
@@ -104,7 +104,7 @@ nobody has been asked, and it is the record to read next for either.
104
104
 
105
105
  - **A shipped `.nvmrc` or an `engines` value in a shared baseline.** Rejected as
106
106
  unrulable: the value is per-repository and the tool can only guess it. Shipping `22` would
107
- break `simt-social`, which pins 24.
107
+ break `repo-14`, which pins 24.
108
108
  - **Reporting cross-repository node divergence as a finding.** Rejected by the Context. The
109
109
  plan's premise was wrong and the correction is recorded here so it is not re-derived from
110
110
  the plan text.
@@ -56,7 +56,7 @@ correcting at all.
56
56
  detectable by reading the file. Corrected in the row.
57
57
 
58
58
  5. **Two of the four configurations are plain JavaScript with no TypeScript in them,**
59
- `simt-api` and `simt-blog`. A "shared 6-rule ESLint block" cannot describe all four,
59
+ `repo-04` and `repo-06`. A "shared 6-rule ESLint block" cannot describe all four,
60
60
  because two of them have no rules to share — they differ in `ignores` and per-file
61
61
  `globals`, which are per-repository facts about paths.
62
62
 
@@ -67,7 +67,7 @@ correcting at all.
67
67
  may make on its own.
68
68
 
69
69
  7. **The fix for the two conflicts is one dependency each,** and it is not a shared block.
70
- Installing `eslint-config-prettier` in `simt-social` and `simt-social-api` and spreading
70
+ Installing `eslint-config-prettier` in `repo-14` and `repo-11` and spreading
71
71
  it last in the config array resolves both. That is a command per repository, run by the
72
72
  repositories, and simt-dev has no business writing it.
73
73
 
@@ -16,9 +16,9 @@ landed. Not intermittently: 22 of 543 tests failed on runs `37476671363` through
16
16
  Three separate causes, none of them recent:
17
17
 
18
18
  1. **`test/install.test.mjs:17` read the estate off the author's disk.**
19
- `const WORKSPACE = resolve(HERE, '..', '..')` is `/Users/said.elimam/code/simt`, and the
20
- install tests copied skill bodies out of it — `simt-social-api/.agents/skills/d1-safe-change`,
21
- and `singleRepoClone()` out of `simt-social`. On a runner the checkout is
19
+ `const WORKSPACE = resolve(HERE, '..', '..')` is `/home/developer/code/simt`, and the
20
+ install tests copied skill bodies out of it — `repo-11/.agents/skills/d1-safe-change`,
21
+ and `singleRepoClone()` out of `repo-14`. On a runner the checkout is
22
22
  `/home/runner/work/simt-dev/simt-dev`, so `../..` is `/home/runner/work/simt-dev`,
23
23
  which contains this repository and nothing else. The bodies were not there,
24
24
  `skills install` exited 3, and nineteen tests asserted on a refusal they never asked for.
@@ -25,7 +25,7 @@ Phase 6 asked to "collapse the 7 drifted Social skill twins to canonical + stub"
25
25
  answers the row directly. All three resolvers select `role === 'canonical'` and nothing else —
26
26
  `src/commands/skills.mjs` twice, `src/commands/skillsinstall.mjs:291`, `src/mcp/tools.mjs:192` — so
27
27
  a copy declared `duplicate` or `variant` is never installed. The one pair that genuinely had to be
28
- collapsed, `add-country-galaxy`, is already a stub in `simt-social` pointing at the API repo.
28
+ collapsed, `add-country-galaxy`, is already a stub in `repo-14` pointing at the API repo.
29
29
 
30
30
  Phase 7 asked for a sweep of hardcoded absolute paths. The sweep found 17 tracked files. Six were
31
31
  fixed. The two Android `local.properties` files were not, and were nearly fixed by reflex.
@@ -47,7 +47,7 @@ fixed. The two Android `local.properties` files were not, and were nearly fixed
47
47
  because resolution never read those bytes. Phase 6's twin collapse is therefore closed as
48
48
  already answered, at `a9983c8`.
49
49
 
50
- 3. **`local.properties` stays tracked.** Both `simt-app` and `wakt-app` commit it, and both
50
+ 3. **`local.properties` stays tracked.** Both `repo-05` and `repo-03` commit it, and both
51
51
  `.gitignore` files have the conventional `# local.properties` line commented out at lines 22 and
52
52
  24 — a deliberate override of Android's own convention, not an oversight. Untracking them would
53
53
  be a house-style change made while closing a plan, and a build machine that checks out without
@@ -74,8 +74,8 @@ unmade decision is not a gap to fill by invention.
74
74
 
75
75
  **Collapsing `graphify` and `parallel-feature-team`.** Both are declared `duplicate` with matching
76
76
  byte counts, so they look like the obvious candidates. `graphify` is `vendored: true` — an
77
- 84 534-byte, ten-file, version-pinned tool present in both Social repos on purpose. Deleting
78
- simt-social's copy would leave nothing to detect the loss, because resolution only ever read the
77
+ 84 534-byte, ten-file, version-pinned tool present in both one repository on purpose. Deleting
78
+ repo-14's copy would leave nothing to detect the loss, because resolution only ever read the
79
79
  canonical copy.
80
80
 
81
81
  **Untracking `local.properties`.** An Android SDK path is machine state, and two repos chose to
@@ -21,7 +21,7 @@ Two facts about this repository make that gap harder to notice than it looks. `t
21
21
  reach outside the checkout (ADR-031), and this package has no dependencies (ADR-012). Together
22
22
  those mean the suite has no installed copy of itself to test, and the natural workaround —
23
23
  borrowing a formatter from a neighbouring checkout — is the one ADR-031 rules out. Prettier 3.8.1
24
- is sitting in `../simt-blog/node_modules/.bin` and is not available here.
24
+ is sitting in `../repo-06/node_modules/.bin` and is not available here.
25
25
 
26
26
  ## Ruling
27
27
 
@@ -87,6 +87,6 @@ node, git and a POSIX shell; npm is in the same class. A conditional skip conver
87
87
  into an unverified one, which is the failure this file was written to stop.
88
88
 
89
89
  **Copying Prettier out of a sibling checkout.** It exists, it works, and ADR-031 rules it out for
90
- the same reason it rules out reading skill bodies from `../simt-social`: a suite that passes on
90
+ the same reason it rules out reading skill bodies from `../repo-14`: a suite that passes on
91
91
  the author's machine and fails on a runner with only this checkout is evidence about the author's
92
92
  directory.
@@ -16,13 +16,13 @@ Measured over the 16 repositories at `~/code/simt`:
16
16
  ```
17
17
  staleRuleFiles: 17
18
18
  repos naming stale rule files in problems: 0
19
- simt-app problems:
19
+ repo-05 problems:
20
20
  "the generated region was hand-edited; restore it with: simt-dev sync --yes"
21
21
  "core.hooksPath is \"hooks\", not the shared dispatcher; run: simt-dev hooks install --yes"
22
22
  ```
23
23
 
24
- Five adopted repositories have stale rule files — `simt-app`, `simt-blog` and `simt-social` at 0
25
- of 3 current, `simt-social-api` and `simt-social-media` at 0 of 4 — and not one of them said so in
24
+ Five adopted repositories have stale rule files — `repo-05`, `repo-06` and `repo-14` at 0
25
+ of 3 current, `repo-11` and `repo-13` at 0 of 4 — and not one of them said so in
26
26
  its problems list. The summary already said `staleRuleFiles: 17`, because `summarise` has always
27
27
  counted them. So the dashboard was reporting a count of a defect it refused to call a defect.
28
28
 
@@ -14,11 +14,11 @@ Review 0001 §S12, marked P1 and "decision". Measured across the estate before a
14
14
  the five repositories selecting `opencode,cursor`:
15
15
 
16
16
  ```
17
- simt-app folded=3 .mdc=3 0/3 current
18
- simt-blog folded=3 .mdc=3 0/3 current
19
- simt-social folded=3 .mdc=3 0/3 current
20
- simt-social-api folded=4 .mdc=4 0/4 current
21
- simt-social-media folded=4 .mdc=4 0/4 current
17
+ repo-05 folded=3 .mdc=3 0/3 current
18
+ repo-06 folded=3 .mdc=3 0/3 current
19
+ repo-14 folded=3 .mdc=3 0/3 current
20
+ repo-11 folded=4 .mdc=4 0/4 current
21
+ repo-13 folded=4 .mdc=4 0/4 current
22
22
  ```
23
23
 
24
24
  Seventeen generated rule files. Every one of them byte-identical to the section already folded into
@@ -50,7 +50,7 @@ other `react-native`, and neither receives both.
50
50
  **Two.** The Android and uv facts name a path at an unknown depth, which the two existing kinds
51
51
  (`file`, `dir`) cannot express. A third kind, `glob`, walks at most `GLOB_DEPTH` directory levels
52
52
  below the root. The bound is a convention, not a preference: the Android Gradle Plugin puts a
53
- module's manifest at `<module>/src/main/AndroidManifest.xml`, and simt-app's sits at
53
+ module's manifest at `<module>/src/main/AndroidManifest.xml`, and repo-05's sits at
54
54
  `app/src/main/AndroidManifest.xml`. A shallower bound makes `has-android-manifest` the shape S07
55
55
  found — a fact no repository could have. A deeper one is a scan rather than a probe, and
56
56
  ADR-013's promise is that adding an ecosystem is data, so a fact must not cost a search.
@@ -78,11 +78,11 @@ repository loses coverage:
78
78
 
79
79
  | Repo | Before | After |
80
80
  | --- | --- | --- |
81
- | `app-ui`, `simt-app`, `wakt-app` | `core, jvm-android` | `core, android, jvm` |
82
- | `bunnyland-server` | `container, core, python` | `container, core, python, uv` |
81
+ | `repo-01`, `repo-05`, `repo-03` | `core, jvm-android` | `core, android, jvm` |
82
+ | `repo-02` | `container, core, python` | `container, core, python, uv` |
83
83
 
84
84
  The three Android repositories gain the profile whose body they actually needed and keep the JVM
85
- build rules; `simt-app` gaining `android` was the row's acceptance criterion. `bunnyland-server`
85
+ build rules; `repo-05` gaining `android` was the row's acceptance criterion. `repo-02`
86
86
  carries a committed `uv.lock`, so its gain is the defect repaired, not a new selection.
87
87
 
88
88
  The rename is breaking in one narrow way: the id `jvm-android` no longer exists. A consumer that
@@ -63,11 +63,11 @@ it.
63
63
 
64
64
  | Repository | Prettier config | States `singleQuote` | Adopting `quote_type = single` |
65
65
  | --- | --- | --- | --- |
66
- | `simt-api` | `.prettierrc` | `true` | no change |
67
- | `simt-blog` | `.prettierrc.json` | `true` | no change |
68
- | `simt-space` | `.prettierrc.json` | `true` | no change |
69
- | `simt-social-api` | `.prettierrc.json` | absent | **every source file reformatted** |
70
- | `simt-social` | `.prettierrc.json` | absent | **every source file reformatted** |
66
+ | `repo-04` | `.prettierrc` | `true` | no change |
67
+ | `repo-06` | `.prettierrc.json` | `true` | no change |
68
+ | `repo-15` | `.prettierrc.json` | `true` | no change |
69
+ | `repo-11` | `.prettierrc.json` | absent | **every source file reformatted** |
70
+ | `repo-14` | `.prettierrc.json` | absent | **every source file reformatted** |
71
71
 
72
72
  Two repositories would have every source file rewritten on the day they adopted the
73
73
  tool, in a commit whose message says it adopted a rules projection. The measurement is
@@ -0,0 +1,115 @@
1
+ # An identity is per-developer, and a repository does not own it
2
+
3
+ Status: **accepted** · 2026-10-11
4
+
5
+ Affects: `simt init`, `src/identity.mjs`, `test/identity.test.mjs`
6
+
7
+ ## Context
8
+
9
+ Two repositories in the estate carried a `git.config` file in their tracked files.
10
+ It holds a name and an email address:
11
+
12
+ ```ini
13
+ [user]
14
+ name = <one developer's name>
15
+ email = <that developer's email address>
16
+ ```
17
+
18
+ Both had a local `include.path` pointing at it, and both worked. Both were also
19
+ wrong in ways that had nothing to do with each other.
20
+
21
+ **It was a tracked file.** A repository has more than one person in it. Committed,
22
+ one developer's name and email is published to every other developer and to
23
+ everybody who can read the repository. This repository's own release review had
24
+ already said so, in the table of findings, as: *"`git.config` tracked at the root
25
+ with a personal name and email — Identity file in a public tree, read by nothing
26
+ — Remove it from the tree."* The finding was recorded and the file stayed.
27
+
28
+ **It pointed somewhere else.** Both `include.path` values named
29
+ `../../simt-dev/git.config` — another repository's identity file, resolved through
30
+ the git directory rather than the working tree. Including the git config's own
31
+ file from a relative path three directories up means one repository's commits
32
+ depend on a sibling checkout existing at an expected path, which fails the moment
33
+ anybody clones without it. It went unnoticed because every developer here has a
34
+ global identity configured, so the include resolved to the same person the
35
+ include would have skipped to, and the file it never read was the reason nothing
36
+ broke.
37
+
38
+ The interesting part is that these are opposite mistakes with the same root.
39
+ `git.config` is not the repository's to own, and treating it as the repository's
40
+ produced both: a file that should have been ignored got committed, and an
41
+ `include` that should have named the repository's own file named a stranger's.
42
+
43
+ ## Ruling
44
+
45
+ 1. **`simt init` writes `git.config` when the repository has none.** It is planned
46
+ like everything else `init` writes, so it appears in the same preview that lists
47
+ the rest, and its source is named on the line — writing somebody's name is a
48
+ statement about a person and the plan says where the name came from.
49
+
50
+ 2. **An existing `git.config` is left exactly as it is.** Not updated, not merged,
51
+ not reformatted. It may have been written by a person, by an older version of
52
+ this tool, or by hand years ago, and none of those are this tool's to
53
+ second-guess.
54
+
55
+ 3. **The identity is derived, in this order, and the first answer wins:**
56
+ the repository's **last commit author**, then the identity **git would use on
57
+ this machine anyway**, then nothing.
58
+
59
+ Last commit first because it is a fact rather than a default: it is who has
60
+ actually been working here. It is read from `git log`, not from
61
+ `git config user.email`, because at that moment the configured value is what the
62
+ file is about to be derived from, and the step would be circular.
63
+
64
+ The second is `--get` rather than `--global`, so it resolves through git's own
65
+ precedence and returns what a commit would have used had this file never
66
+ existed. That is the fallback's whole point: it must not be a second opinion
67
+ about what identity means.
68
+
69
+ 4. **When nothing is available, no file is written.** A placeholder is an identity
70
+ git would use on the next commit, and an adopting command is the last thing that
71
+ should invent one. The refusal is printed with its reason rather than swallowed.
72
+
73
+ 5. **Only the keys that were found are written.** A partial identity is written as a
74
+ partial identity, for the same reason.
75
+
76
+ 6. **`init` adds `git.config` to `.gitignore`** when it creates one, and points
77
+ `include.path` at `../git.config` — the repository's own file.
78
+
79
+ `--replace-all`, not `--add`: a value naming *another* repository's identity file
80
+ is not an addition to keep, it is a link to break.
81
+
82
+ A repository that already *tracks* `git.config` is not untracked by `init`.
83
+ Removing a tracked file is not something an adopting command should do silently,
84
+ and doing it would be a change to that repository's history rather than to its
85
+ working tree.
86
+
87
+ ## Consequences
88
+
89
+ `src/identity.mjs` is new and is the single answer to "whose identity is this, and
90
+ may we write it". `init` plans it, writes it, gitignores it and repoints the
91
+ include; the four refusals are asserted in `test/identity.test.mjs` rather than
92
+ described, because each of them is the correct answer rather than a failure to try
93
+ harder.
94
+
95
+ One test scrubs `HOME` and `GIT_CONFIG_GLOBAL` before asserting the "nothing
96
+ anywhere" case. That is not tidiness: a developer with a global `user.email` — which
97
+ is most of them — makes the identity resolvable, so without scrubbing the test
98
+ would pass for the wrong reason on that machine and nowhere else. ADR-031 is about
99
+ a result depending on the author's environment; this test would have been the next
100
+ instance of it.
101
+
102
+ ## Rejected
103
+
104
+ - **Write `git.config` into the tracked tree, as a shared record of who works
105
+ here.** This is what the two repositories did, and it is a publication of a
106
+ person's email address with a commit attached.
107
+ - **Take the identity from the OS (the login name, the home directory).** It
108
+ produces `<login-name>@example.com` and similar: an identity nobody configured,
109
+ used for authorship. Ruling 4's refusal covers it.
110
+ - **Overwrite an existing `git.config` when it differs from the derived one.**
111
+ Difference is not error. A file that has been there for years and disagrees with
112
+ the last commit author is a decision somebody made.
113
+ - **Fix the `include` only when it is wrong.** The include is only written when it
114
+ is missing or does not name this repository's own file. Rewriting it when it is
115
+ already correct would be a no-op that reads as a change.