autonomous-sdlc-harness 0.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.
Files changed (171) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +24 -0
  4. package/dist/cli.js +194 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/config.js +561 -0
  7. package/dist/commands/config.js.map +1 -0
  8. package/dist/commands/daemon.js +791 -0
  9. package/dist/commands/daemon.js.map +1 -0
  10. package/dist/commands/doctor.js +336 -0
  11. package/dist/commands/doctor.js.map +1 -0
  12. package/dist/commands/init.js +2023 -0
  13. package/dist/commands/init.js.map +1 -0
  14. package/dist/commands/registry.js +42 -0
  15. package/dist/commands/registry.js.map +1 -0
  16. package/dist/config/check.js +505 -0
  17. package/dist/config/check.js.map +1 -0
  18. package/dist/config/io.js +177 -0
  19. package/dist/config/io.js.map +1 -0
  20. package/dist/config/model.js +406 -0
  21. package/dist/config/model.js.map +1 -0
  22. package/dist/core/errors.js +71 -0
  23. package/dist/core/errors.js.map +1 -0
  24. package/dist/core/git.js +537 -0
  25. package/dist/core/git.js.map +1 -0
  26. package/dist/core/json.js +125 -0
  27. package/dist/core/json.js.map +1 -0
  28. package/dist/core/layerCoverage.js +141 -0
  29. package/dist/core/layerCoverage.js.map +1 -0
  30. package/dist/core/layerGapRemedy.js +62 -0
  31. package/dist/core/layerGapRemedy.js.map +1 -0
  32. package/dist/core/nameList.js +23 -0
  33. package/dist/core/nameList.js.map +1 -0
  34. package/dist/core/paths.js +153 -0
  35. package/dist/core/paths.js.map +1 -0
  36. package/dist/core/prompt.js +206 -0
  37. package/dist/core/prompt.js.map +1 -0
  38. package/dist/core/repoPaths.js +55 -0
  39. package/dist/core/repoPaths.js.map +1 -0
  40. package/dist/core/report.js +150 -0
  41. package/dist/core/report.js.map +1 -0
  42. package/dist/core/templating.js +88 -0
  43. package/dist/core/templating.js.map +1 -0
  44. package/dist/core/writer.js +479 -0
  45. package/dist/core/writer.js.map +1 -0
  46. package/dist/daemon/backend.js +180 -0
  47. package/dist/daemon/backend.js.map +1 -0
  48. package/dist/daemon/units.js +380 -0
  49. package/dist/daemon/units.js.map +1 -0
  50. package/dist/detect/nestedApplication.js +79 -0
  51. package/dist/detect/nestedApplication.js.map +1 -0
  52. package/dist/detect/presets.js +2033 -0
  53. package/dist/detect/presets.js.map +1 -0
  54. package/dist/detect/signals.js +1368 -0
  55. package/dist/detect/signals.js.map +1 -0
  56. package/dist/doctor/checks.js +3530 -0
  57. package/dist/doctor/checks.js.map +1 -0
  58. package/dist/generators/claudeContext.js +588 -0
  59. package/dist/generators/claudeContext.js.map +1 -0
  60. package/dist/generators/githooks.js +446 -0
  61. package/dist/generators/githooks.js.map +1 -0
  62. package/dist/generators/harnessConfig.js +632 -0
  63. package/dist/generators/harnessConfig.js.map +1 -0
  64. package/dist/generators/notifications.js +191 -0
  65. package/dist/generators/notifications.js.map +1 -0
  66. package/dist/generators/outerLoopScripts.js +165 -0
  67. package/dist/generators/outerLoopScripts.js.map +1 -0
  68. package/dist/generators/permissionProfile.js +1172 -0
  69. package/dist/generators/permissionProfile.js.map +1 -0
  70. package/dist/generators/projectSettings.js +322 -0
  71. package/dist/generators/projectSettings.js.map +1 -0
  72. package/dist/generators/repoRoot.js +417 -0
  73. package/dist/generators/repoRoot.js.map +1 -0
  74. package/dist/generators/scripts.js +557 -0
  75. package/dist/generators/scripts.js.map +1 -0
  76. package/dist/generators/stateDir.js +221 -0
  77. package/dist/generators/stateDir.js.map +1 -0
  78. package/dist/machine/paths.js +111 -0
  79. package/dist/machine/paths.js.map +1 -0
  80. package/dist/machine/plugins.js +224 -0
  81. package/dist/machine/plugins.js.map +1 -0
  82. package/dist/machine/registry.js +330 -0
  83. package/dist/machine/registry.js.map +1 -0
  84. package/package.json +23 -0
  85. package/scripts/README.md +13 -0
  86. package/scripts/daemon/launchd.plist.template +59 -0
  87. package/scripts/daemon/systemd.service.template +58 -0
  88. package/templates/README.md +15 -0
  89. package/templates/claude/CLAUDE.md +54 -0
  90. package/templates/claude/README.md +5 -0
  91. package/templates/claude/context/api.md +29 -0
  92. package/templates/claude/context/conventions.md +23 -0
  93. package/templates/claude/context/data-layer.md +28 -0
  94. package/templates/claude/context/data-storage.md +29 -0
  95. package/templates/claude/context/docs-catalog.md +29 -0
  96. package/templates/claude/context/domain.md +28 -0
  97. package/templates/claude/context/layer.md +20 -0
  98. package/templates/claude/context/module.md +30 -0
  99. package/templates/claude/context/package.md +29 -0
  100. package/templates/claude/context/presentation.md +32 -0
  101. package/templates/claude/context/state-slices.md +28 -0
  102. package/templates/claude/context/tests.md +28 -0
  103. package/templates/claude/harness-task-offer.md +58 -0
  104. package/templates/claude/push-notify.env.example +21 -0
  105. package/templates/claude/qa-accounts.env.example +38 -0
  106. package/templates/claude/qa_test_scenarios.md +110 -0
  107. package/templates/claude/settings.autonomous.json +93 -0
  108. package/templates/claude/settings.autonomous.qa.json +36 -0
  109. package/templates/githooks/README.md +3 -0
  110. package/templates/githooks/pre-push +72 -0
  111. package/templates/repo/README.md +3 -0
  112. package/templates/repo/gitattributes +16 -0
  113. package/templates/repo/gitignore +61 -0
  114. package/templates/repo/gitignore.qa +25 -0
  115. package/templates/repo/mcp.json +17 -0
  116. package/templates/scripts/README.md +5 -0
  117. package/templates/scripts/autonomous-format-stream.sh +95 -0
  118. package/templates/scripts/autonomous-notify.sh +337 -0
  119. package/templates/scripts/autonomous-watcher.sh +3087 -0
  120. package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
  121. package/templates/scripts/commit-on-branch.sh +288 -0
  122. package/templates/scripts/create-worktree.sh +360 -0
  123. package/templates/scripts/deploy.sh +47 -0
  124. package/templates/scripts/lib/harness-run-lib.sh +1481 -0
  125. package/templates/scripts/push-branch.sh +140 -0
  126. package/templates/scripts/refresh-branch.sh +244 -0
  127. package/templates/scripts/restart-watcher.sh +401 -0
  128. package/templates/scripts/scratch-run.sh +302 -0
  129. package/templates/scripts/setup-worktree.sh +262 -0
  130. package/templates/scripts/start-dev-server.sh +99 -0
  131. package/templates/scripts/test.sh +50 -0
  132. package/templates/scripts/typecheck.sh +50 -0
  133. package/templates/state-dir/README-root.md +13 -0
  134. package/templates/state-dir/README.md +9 -0
  135. package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
  136. package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
  137. package/templates/state-dir/architecture_reviews/README.md +9 -0
  138. package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
  139. package/templates/state-dir/autonomous_inbox/README.md +9 -0
  140. package/templates/state-dir/autonomous_logs/README.md +9 -0
  141. package/templates/state-dir/branch_statistics/README.md +9 -0
  142. package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
  143. package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
  144. package/templates/state-dir/business_parity_reviews/README.md +9 -0
  145. package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
  146. package/templates/state-dir/clarification_digests/README.md +9 -0
  147. package/templates/state-dir/clarifications/README.md +9 -0
  148. package/templates/state-dir/code_reviews/README.md +9 -0
  149. package/templates/state-dir/dispatch_additions/README.md +19 -0
  150. package/templates/state-dir/docs_catalog/README.md +9 -0
  151. package/templates/state-dir/flow_progress/README.md +9 -0
  152. package/templates/state-dir/improvement_observations/README.md +19 -0
  153. package/templates/state-dir/improvement_suggestions.md +29 -0
  154. package/templates/state-dir/lessons.md +23 -0
  155. package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
  156. package/templates/state-dir/qa_reviews/README.md +9 -0
  157. package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
  158. package/templates/state-dir/review_plan_reviews/README.md +9 -0
  159. package/templates/state-dir/scratch/README.md +11 -0
  160. package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
  161. package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
  162. package/templates/state-dir/skeptic_reviews/README.md +9 -0
  163. package/templates/state-dir/story_plans/README.md +9 -0
  164. package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
  165. package/templates/state-dir/task_plan_reviews/README.md +9 -0
  166. package/templates/state-dir/task_plans/README.md +9 -0
  167. package/templates/state-dir/task_prompts/README.md +9 -0
  168. package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
  169. package/templates/state-dir/ui_test_plans/README.md +9 -0
  170. package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
  171. package/templates/state-dir/user_reviews/README.md +9 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * The machine-local registry of initialized repositories: its format of record, and the only code
3
+ * that reads or writes it.
4
+ *
5
+ * **The rule this module exists to enforce: `repos.json` has one definition, and it is this file.**
6
+ * Two consumers ask about this artifact — `daemon list`, which enumerates it, and `doctor`, which
7
+ * reports on it — and a second derivation of the path, the JSON shape or the staleness grading would
8
+ * be a second answer to "what is armed on this machine". The name is joined here and nowhere else
9
+ * (`grep -rn REGISTRY_FILENAME cli/src`); that is the property worth keeping. The one mirror outside
10
+ * that scope is `MACHINE_REGISTRY_FILENAME` in `cli/templates/scripts/autonomous-watcher.sh`, which
11
+ * the footprint report reads — `grep -rn 'repos.json' cli/` reaches both.
12
+ *
13
+ * `docs/watcher.md` §7 is the human-facing half of this definition and says the same things in the
14
+ * same order. The two are one contract: change either and change the other in the same commit.
15
+ *
16
+ * ## The three questions this artifact has to answer, and its answers
17
+ *
18
+ * 1. **Where it lives.** {@link registryPath} — `repos.json` in {@link machineStateDir}, i.e. beside
19
+ * the usage lane's two artifacts, in a directory created `0700` and outside every repository. It
20
+ * is a **separate file** and is never merged into `usage-state.json`: that one is the lane, its
21
+ * format is `docs/watcher.md` §5's, and §5 is explicit that this registry "is a different
22
+ * artifact". Sharing a file would put a per-run publisher and a per-install index behind one
23
+ * merge rule that suits neither.
24
+ * 2. **What happens when it is stale** — a recorded root that is gone, is no longer a repository, or
25
+ * whose unit file has been removed. It is **reported and never removed by a read**
26
+ * ({@link inspect} grades; nothing it does touches the filesystem). A read command must not
27
+ * mutate machine state, and a checkout on an unmounted volume or temporarily moved aside must not
28
+ * be silently dropped from the index while its daemon is still installed. Pruning is the explicit
29
+ * `daemon list --prune`, which is the one caller of {@link removeRepositories}.
30
+ * 3. **Who writes it.** `daemon install`, and nothing else. `init` does not — a wired repository with
31
+ * no daemon polls nothing, so registering it would list repositories that never run. `doctor` does
32
+ * not — it repairs nothing and writes nothing, which is what makes it safe in CI. `daemon stop`
33
+ * does not remove an entry — the unit file survives a stop, and this index mirrors *installed*
34
+ * units rather than running ones.
35
+ *
36
+ * ## Two properties nobody should re-derive wrongly
37
+ *
38
+ * - **It is not consulted before starting a run.** That is the usage lane's job and `docs/watcher.md`
39
+ * §5 says so. Nothing here may become a precondition of anything: an absent, truncated or garbage
40
+ * registry costs a listing, never a run, which is why {@link readRegistry} fails open in every one
41
+ * of those cases rather than reporting a fault.
42
+ * - **`daemon install` run from a git worktree registers that worktree, and that is correct.** The
43
+ * unit's identity is the checkout it was installed from (`daemon/units.ts`, choice 1) and its text
44
+ * carries that checkout as both `ExecStart` and `WorkingDirectory` (choice 4), so the entry has to
45
+ * describe the same checkout the unit does. Keying on {@link repoSlug} — the same function that
46
+ * names the unit — is what makes the two unable to disagree.
47
+ *
48
+ * ## Why the writes here are not enqueued on a `WritePlan`
49
+ *
50
+ * `core/writer.ts` implements the `init` re-run contract for artifacts **inside a repository** that
51
+ * an adopter then owns and edits: its policies are create-if-absent, merge and back-up-then-replace,
52
+ * and its plan is built by generators and applied once. This file is none of that. It is a
53
+ * machine-scoped index the CLI owns end to end, it is rewritten in place on every registration
54
+ * (read-modify-write, not merge), and no adopter edits it. The daemon unit is written through a plan
55
+ * with `allowOutsideRepo` because it is a create-if-absent artifact an operator may go on to edit;
56
+ * this one is not.
57
+ *
58
+ * The consequence a caller must honor: **{@link upsertRepository} and {@link removeRepositories}
59
+ * write when they are called.** They are not on the plan, so `--dry-run` does not skip them for you —
60
+ * a command that supports `--dry-run` must not call either under it.
61
+ */
62
+ import { chmodSync, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
63
+ import { isAbsolute, join } from 'node:path';
64
+ import { internal } from '../core/errors.js';
65
+ import { probeRepoRoot } from '../core/git.js';
66
+ import { formatJson, isJsonObject, readJsonFile } from '../core/json.js';
67
+ import { repoSlug } from '../daemon/units.js';
68
+ import { MACHINE_DIR_MODE, machineStateDir } from './paths.js';
69
+ /** The file's name under {@link machineStateDir}. Joined here and nowhere else in the CLI. */
70
+ const REGISTRY_FILENAME = 'repos.json';
71
+ /**
72
+ * The format version written into every record, and the only one {@link readRegistry} recognises.
73
+ *
74
+ * A reader that meets a value it does not know treats the whole file as unreadable — the same rule
75
+ * `usage-state.json` carries in `docs/watcher.md` §5 — which is also this format's forward-compatible
76
+ * escape hatch: a release that adds a field or a backend value bumps this integer, and an older CLI
77
+ * then declines to interpret the file rather than half-understanding it.
78
+ */
79
+ const REGISTRY_SCHEMA = 1;
80
+ /** Mode of the file: it names an account's checkouts, so it is the owner's to read and nobody else's. */
81
+ const REGISTRY_FILE_MODE = 0o600;
82
+ /** The registry file's absolute path. A pure string: asking where it is never creates anything. */
83
+ export function registryPath() {
84
+ return join(machineStateDir(), REGISTRY_FILENAME);
85
+ }
86
+ /** Codepoint order, not the locale's — the same ASCII-only discipline {@link repoSlug} is built on. */
87
+ function compareSlugs(left, right) {
88
+ return left < right ? -1 : left > right ? 1 : 0;
89
+ }
90
+ /** A non-empty string, or `undefined` for every other JSON value. */
91
+ function stringField(value) {
92
+ return typeof value === 'string' && value !== '' ? value : undefined;
93
+ }
94
+ /**
95
+ * One record's parsed form, or `undefined` when it is not one.
96
+ *
97
+ * **A malformed record is dropped, not fatal.** The file is an index of independent rows, and one
98
+ * row a hand-edit mangled should not hide the rest — the fail-open rule of the module header applied
99
+ * at record granularity. The dropped row does not come back: the next {@link upsertRepository}
100
+ * rewrites the file from what was readable. That is acceptable for the same reason the whole-file
101
+ * case is (see {@link readRegistry}), and it is why the strictness here is safe rather than brittle:
102
+ * a future release that adds a field or a backend value arrives with a new
103
+ * {@link REGISTRY_SCHEMA}, which this reader declines wholesale instead of parsing row by row.
104
+ */
105
+ function parseEntry(value) {
106
+ if (!isJsonObject(value))
107
+ return undefined;
108
+ const root = stringField(value['root']);
109
+ const projectName = stringField(value['projectName']);
110
+ const label = stringField(value['label']);
111
+ const unitPath = stringField(value['unitPath']);
112
+ const backend = value['backend'];
113
+ if (root === undefined || projectName === undefined || label === undefined || unitPath === undefined) {
114
+ return undefined;
115
+ }
116
+ if (backend !== 'launchd' && backend !== 'systemd')
117
+ return undefined;
118
+ // A timestamp that is not a whole non-negative number is reduced to 0 rather than discarding the
119
+ // row: it is a breadcrumb, and losing the entry would cost more than losing its age.
120
+ const stamp = value['registered_at'];
121
+ const registered_at = typeof stamp === 'number' && Number.isInteger(stamp) && stamp >= 0 ? stamp : 0;
122
+ return { root, projectName, label, backend, unitPath, registered_at };
123
+ }
124
+ /**
125
+ * The registry as it is on disk, or an empty one.
126
+ *
127
+ * **It fails open in every failure mode there is**: absent, unreadable (a permission bit, a bad
128
+ * mount), unparseable, not a JSON object, `repos` not an object, or a `schema` this release does not
129
+ * recognise all read as "no repositories are registered" rather than throwing. A machine-scoped file
130
+ * must not be able to stop a repository-scoped command, and this one is not consulted before
131
+ * starting a run, so there is nothing a fault here could correctly block.
132
+ *
133
+ * That is the opposite of `readJsonFile`'s own contract, which throws on unparseable JSON so a later
134
+ * create-if-absent write cannot clobber a file the adopter merely mistyped. The trade is different
135
+ * here and worth stating: an unreadable registry **is** rewritten by the next
136
+ * {@link upsertRepository}, and that is acceptable because this file is *derived* state — every
137
+ * entry in it can be recreated by re-running `daemon install` in the repository it names, and losing
138
+ * one costs a line in a listing rather than a run or an adopter's edits.
139
+ *
140
+ * The returned registry is a fresh object each call; a caller may keep it and grade it later without
141
+ * aliasing anyone else's copy.
142
+ */
143
+ export function readRegistry() {
144
+ let parsed;
145
+ try {
146
+ parsed = readJsonFile(registryPath());
147
+ }
148
+ catch {
149
+ return { schema: REGISTRY_SCHEMA, repos: {} };
150
+ }
151
+ const repos = {};
152
+ if (!isJsonObject(parsed) || parsed['schema'] !== REGISTRY_SCHEMA)
153
+ return { schema: REGISTRY_SCHEMA, repos };
154
+ const stored = parsed['repos'];
155
+ if (!isJsonObject(stored))
156
+ return { schema: REGISTRY_SCHEMA, repos };
157
+ for (const slug of Object.keys(stored)) {
158
+ const entry = parseEntry(stored[slug]);
159
+ if (entry !== undefined)
160
+ repos[slug] = entry;
161
+ }
162
+ return { schema: REGISTRY_SCHEMA, repos };
163
+ }
164
+ /** The serialized form: the current schema, and the entries in slug order with a fixed field order. */
165
+ function toJson(registry) {
166
+ const repos = {};
167
+ for (const slug of Object.keys(registry.repos).sort(compareSlugs)) {
168
+ const entry = registry.repos[slug];
169
+ if (entry === undefined)
170
+ continue;
171
+ repos[slug] = {
172
+ root: entry.root,
173
+ projectName: entry.projectName,
174
+ label: entry.label,
175
+ backend: entry.backend,
176
+ unitPath: entry.unitPath,
177
+ registered_at: entry.registered_at,
178
+ };
179
+ }
180
+ return { schema: REGISTRY_SCHEMA, repos };
181
+ }
182
+ /**
183
+ * Write the whole registry, **atomically**: a temp file in the same directory, then a rename.
184
+ *
185
+ * Same directory, so the rename is within one filesystem and therefore atomic — a reader mid-write
186
+ * sees the previous record or the new one and never a half-written line. It is the same protocol
187
+ * `hr_lane_publish` uses for `usage-state.json` in the generated `lib/harness-run-lib.sh`, and for
188
+ * the same reason.
189
+ *
190
+ * The temp name carries this process's pid: two writers with one pid cannot exist at the same time
191
+ * on one machine, so it is unique among concurrent writers, and a crashed run leaves at most one
192
+ * stale temp per pid rather than an accumulating pile. The mode is applied by an explicit `chmod`
193
+ * after the write as well as by `writeFileSync`'s own option, because that option is masked by the
194
+ * process umask and is ignored outright for a file that is already there — which a leftover temp
195
+ * from a crash would be. The directory is created and `chmod`ed on the same two-step reasoning
196
+ * (`core/writer.ts`'s `ensure-dir` commit, and the shell half's `mkdir -p` then `chmod 700`).
197
+ *
198
+ * A failure leaves the previous file untouched and removes the temp. The removal is `rmSync` on one
199
+ * file **without** `recursive`, so it cannot empty a directory even if it were aimed at one.
200
+ */
201
+ function writeRegistry(registry) {
202
+ const dir = machineStateDir();
203
+ mkdirSync(dir, { recursive: true });
204
+ chmodSync(dir, MACHINE_DIR_MODE);
205
+ const temp = join(dir, `.${REGISTRY_FILENAME}.${process.pid}.tmp`);
206
+ try {
207
+ writeFileSync(temp, formatJson(toJson(registry)), { encoding: 'utf8', mode: REGISTRY_FILE_MODE });
208
+ chmodSync(temp, REGISTRY_FILE_MODE);
209
+ renameSync(temp, registryPath());
210
+ }
211
+ catch (error) {
212
+ try {
213
+ rmSync(temp, { force: true });
214
+ }
215
+ catch {
216
+ // The original failure is the one worth reporting; a temp file left behind is not.
217
+ }
218
+ throw error;
219
+ }
220
+ }
221
+ /** Refuse a path that is not absolute, before it becomes a record two commands later disagree on. */
222
+ function assertAbsolute(field, value) {
223
+ if (!isAbsolute(value)) {
224
+ throw internal(`the repository registry was given a relative ${field} (${JSON.stringify(value)}), which would read differently depending on where a later command was run from`);
225
+ }
226
+ }
227
+ /**
228
+ * Register a repository, or replace its entry in place.
229
+ *
230
+ * The key is {@link repoSlug} of `entry.root`, derived **here** rather than taken from the caller,
231
+ * so a re-install over an existing repository can only ever land on that repository's own row and
232
+ * two checkouts can never collide. Every other entry in the file is preserved.
233
+ *
234
+ * It writes when it is called: see the module header's last paragraph, and do not call it under
235
+ * `--dry-run`.
236
+ */
237
+ export function upsertRepository(entry) {
238
+ assertAbsolute('root', entry.root);
239
+ assertAbsolute('unitPath', entry.unitPath);
240
+ const current = readRegistry();
241
+ const repos = { ...current.repos, [repoSlug(entry.root)]: entry };
242
+ writeRegistry({ schema: REGISTRY_SCHEMA, repos });
243
+ }
244
+ /**
245
+ * Remove the named entries and report how many were there — the explicit prune, and the only removal
246
+ * in this module.
247
+ *
248
+ * A slug that is not registered is not an error: the caller of `daemon list --prune` passes what it
249
+ * graded stale, and a concurrent `daemon install` may legitimately have changed the file in between.
250
+ * When nothing matched, **nothing is written at all** — so a prune over an unreadable file leaves
251
+ * that file exactly as it is rather than replacing it with an empty one.
252
+ *
253
+ * It writes when it is called: see the module header's last paragraph.
254
+ */
255
+ export function removeRepositories(slugs) {
256
+ const repos = { ...readRegistry().repos };
257
+ let removed = 0;
258
+ for (const slug of slugs) {
259
+ if (!Object.hasOwn(repos, slug))
260
+ continue;
261
+ delete repos[slug];
262
+ removed += 1;
263
+ }
264
+ if (removed === 0)
265
+ return 0;
266
+ writeRegistry({ schema: REGISTRY_SCHEMA, repos });
267
+ return removed;
268
+ }
269
+ /** True when something is at `path` and it is a directory. Any error reads as "not there". */
270
+ function isDirectory(path) {
271
+ try {
272
+ return statSync(path).isDirectory();
273
+ }
274
+ catch {
275
+ return false;
276
+ }
277
+ }
278
+ /**
279
+ * Grade one entry. Reads the filesystem and probes git; changes neither.
280
+ *
281
+ * **The order of the two first checks is load-bearing, not cosmetic.** `probeRepoRoot` runs `git` with
282
+ * the candidate directory as its working directory, and spawning a process in a directory that is not
283
+ * there fails with `ENOENT` — which that probe reads as "git is not on PATH". So the root's existence
284
+ * is settled first, and the repository question is only ever asked of a directory that exists.
285
+ *
286
+ * A recorded root that exists and *is inside* a repository whose top level is some other directory is
287
+ * `not-a-repository`: the entry claims a repository root, and this path is no longer one. Comparing a
288
+ * `--show-toplevel` answer against a recorded value is comparing like with like, because the recorded
289
+ * value came from the same probe when the daemon was installed.
290
+ *
291
+ * When git is not on PATH, or refused to answer at all (`detected dubious ownership`), the repository
292
+ * question has no answer, so it is skipped and the entry is graded on the two axes that remain. Only
293
+ * git's own "not a git repository" grades the entry stale. Reporting every registered repository as
294
+ * broken because the machine running `daemon list` has no git, or will not read a root it does not
295
+ * own, would be a fault of the reader dressed up as a fault of the registry.
296
+ */
297
+ function gradeEntry(entry) {
298
+ if (!isDirectory(entry.root))
299
+ return 'root-missing';
300
+ const probe = probeRepoRoot(entry.root);
301
+ if (probe.kind === 'not-a-repository')
302
+ return 'not-a-repository';
303
+ if (probe.kind === 'repository' && probe.root !== entry.root)
304
+ return 'not-a-repository';
305
+ if (!existsSync(entry.unitPath))
306
+ return 'unit-missing';
307
+ return 'ok';
308
+ }
309
+ /**
310
+ * Grade every entry — **the one place staleness is decided**, so `daemon list` and `doctor` cannot
311
+ * report the same machine differently.
312
+ *
313
+ * Sorted by slug rather than left in the file's own order, so a listing an operator reads twice reads
314
+ * the same way twice even if the file was hand-edited into some other order.
315
+ *
316
+ * It writes nothing and removes nothing; that is the module header's answer 2, and it is why this
317
+ * function is safe to call from `doctor`, whose published contract is that a run of it writes
318
+ * nothing at all.
319
+ */
320
+ export function inspect(registry) {
321
+ const graded = [];
322
+ for (const slug of Object.keys(registry.repos).sort(compareSlugs)) {
323
+ const entry = registry.repos[slug];
324
+ if (entry === undefined)
325
+ continue;
326
+ graded.push({ slug, entry, state: gradeEntry(entry) });
327
+ }
328
+ return graded;
329
+ }
330
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/machine/registry.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "autonomous-sdlc-harness",
3
+ "version": "0.1.0",
4
+ "description": "The outer loop of the autonomous SDLC harness: init, doctor, config and daemon management for the harness Claude Code plugin.",
5
+ "license": "Apache-2.0",
6
+ "author": "firu-daniel",
7
+ "homepage": "https://github.com/firu-daniel/autonomous-sdlc-harness",
8
+ "repository": { "type": "git", "url": "https://github.com/firu-daniel/autonomous-sdlc-harness.git" },
9
+ "type": "module",
10
+ "bin": { "autonomous-sdlc-harness": "./dist/cli.js" },
11
+ "files": ["dist", "scripts", "templates", "README.md", "LICENSE", "NOTICE"],
12
+ "engines": { "node": ">=20.11.0" },
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json",
15
+ "pretest": "npm run build",
16
+ "test": "node --test",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^5.6.0",
21
+ "@types/node": "^20.14.0"
22
+ }
23
+ }
@@ -0,0 +1,13 @@
1
+ # scripts/
2
+
3
+ The **daemon unit templates** the `daemon` subcommand renders — `daemon/launchd.plist.template` and `daemon/systemd.service.template` — and nothing else. They live here rather than under `templates/` because they are the one generated artifact that never lands in the adopting repository: the CLI reads them out of its own installed copy and writes the rendered unit into the account's home directory, whereas every subdirectory of `templates/` is an adopter-side home `init` writes *into*. The package's `files` field carries this directory into the tarball and the compiler's `include` (`src/**/*.ts`) leaves it alone. `plugin/scripts/` is a different thing again — the helpers an instruction or agent body calls by path — and that README states the split from the plugin side.
4
+
5
+ **Where the outer-loop scripts went, and why.** An earlier version of this README had them executing from the installed package and named the conflict that made the location undecidable: this directory claimed them, while the shipped instruction corpus names three of them by their `<scripts_dir>/` destination, two of them as `bash <scripts_dir>/<name>.sh` invocations. Both could not be right, and the generated permission profile has to name one location or the other — an allow entry naming the location the corpus does not call fails exactly as silently as no entry at all, because in an unattended run a command matching neither `allow` nor `deny` parks with no diagnostic. **The resolution taken is the `scriptsDir` option**, the one this README recommended: the run watcher, its restart wrapper, the notifier and its stream formatter, and the commit / push / branch-refresh / worktree / cleanup wrappers ship as generator templates under `cli/templates/scripts/` (with the shared library they source at `cli/templates/scripts/lib/`), and `init` copies them verbatim into the adopter's configured `scriptsDir`. `cli/src/generators/outerLoopScripts.ts` is the single declaration of that set.
6
+
7
+ Three things decided it, against the alternative of keeping them in the installed package and adding one absolute literal profile entry per script, built at run time from the package root:
8
+
9
+ - **The corpus resolves unchanged.** Every `<scripts_dir>/`-prefixed invocation in the instruction corpus is already in the shape this resolution produces, so the port added no corpus edit at all; the package-relative alternative required changing the prefix at each of those sites. Reproduce the site list from the tree root with `grep -rn 'commit-on-branch\.sh\|push-branch\.sh\|autonomous-watcher\.sh' plugin/`.
10
+ - **Coverage is a row, not a mechanism.** An agent-invocable row in the shipped table emits the three literal forms the profile generator already builds and tests — the repo-relative invocation, its repo-root-absolute twin and its sibling-worktree twin — gated so a script that was never written is never allow-listed. The alternative had to name each script **file** in an absolute entry, because a directory-prefix entry does not match even with a literal path (`docs/development.md` §3).
11
+ - **The daemon's program stays inside the repository it works on.** `ExecStart` resolves as `<repoRoot>/<scriptsDir>/autonomous-watcher.sh`, inside the very checkout `WorkingDirectory` names, rather than at an `npx` cache path that can be evicted under a long-running service (`docs/cli.md` §9).
12
+
13
+ The cost is recorded rather than hidden: a copied script is upgraded by re-running `init`, not by updating the package, and the `create-if-absent` re-run contract deliberately keeps an adopter's edited copy instead of replacing it (`docs/cli.md` §3). That is the price of the corpus, the profile and the unit file all naming one location.
@@ -0,0 +1,59 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <!--
4
+ User LaunchAgent for the autonomous run watcher of {{projectName}}.
5
+
6
+ Rendered by `autonomous-sdlc-harness daemon install`, which writes it to
7
+ ~/Library/LaunchAgents/{{label}}.plist and loads it with `launchctl bootstrap gui/<uid>`.
8
+ It is a per-user agent and never a system daemon: it runs as the account that installed it, and
9
+ so has that account's keychain and agent-runner credentials, which is exactly what an unattended
10
+ run needs and what a system daemon would not have. Its PATH is not that account's — launchd
11
+ starts an agent from launchd's own environment and never from a login shell — so the unit
12
+ renders one in below.
13
+
14
+ Every value below is substituted at install time. This file carries no account name, no home
15
+ directory and no checkout path of its own, so the same template serves every adopter.
16
+
17
+ No comment in this file may contain a double hyphen, substituted values included: XML 1.0 §2.5
18
+ forbids one, and `cli/test/daemon.test.mjs` fails if one appears in a rendered unit.
19
+ -->
20
+ <plist version="1.0">
21
+ <dict>
22
+ <key>Label</key>
23
+ <string>{{label}}</string>
24
+
25
+ <!-- The interpreter is named rather than left to the script's own shebang: an agent is
26
+ started with no shell, and naming it keeps the unit working for a watcher whose
27
+ executable bit did not survive a checkout. -->
28
+ <key>ProgramArguments</key>
29
+ <array>
30
+ <string>/bin/bash</string>
31
+ <string>{{watcherPath}}</string>
32
+ </array>
33
+
34
+ <key>WorkingDirectory</key>
35
+ <string>{{repoRoot}}</string>
36
+
37
+ <!-- The PATH the agent runs on. Without this key that is launchd's own
38
+ /usr/bin:/bin:/usr/sbin:/sbin, and a toolchain installed anywhere else — Homebrew, nvm,
39
+ ~/.local/bin — is not found: the watcher's package manager and its agent CLI both resolve
40
+ through PATH. `daemon install` renders the PATH of the shell that ran it; if that shell had
41
+ none, no key is rendered at all, because a PATH of "" is worse than launchd's four
42
+ directories. Re-run `daemon install` with the force flag when the toolchain moves. -->
43
+ {{environment}}
44
+ <key>RunAtLoad</key>
45
+ <true/>
46
+
47
+ <!-- Restarted whenever it exits: a watcher that died between dispatches looks exactly like
48
+ one with nothing to do, and the run queue drains silently. -->
49
+ <key>KeepAlive</key>
50
+ <true/>
51
+
52
+ <!-- The watcher's own output, beside the per-run logs in the run-artifact tree. launchd
53
+ creates these files but not the directory above them, which `init` has already made. -->
54
+ <key>StandardOutPath</key>
55
+ <string>{{stateDirAbs}}/autonomous_logs/watcher.out.log</string>
56
+ <key>StandardErrorPath</key>
57
+ <string>{{stateDirAbs}}/autonomous_logs/watcher.err.log</string>
58
+ </dict>
59
+ </plist>
@@ -0,0 +1,58 @@
1
+ # User service unit for the autonomous run watcher of {{projectName}}.
2
+ #
3
+ # A documented template rather than a system unit. `autonomous-sdlc-harness daemon install`
4
+ # renders it to ~/.config/systemd/user/{{label}}, and it is driven with `systemctl --user`
5
+ # throughout — `--user daemon-reload`, then `--user enable --now {{label}}`. As a user unit it
6
+ # runs as the account that installed it, and so has that account's secret store and agent-runner
7
+ # credentials; installed system-wide it would have none of them. Its PATH is not that account's —
8
+ # a user unit inherits the service manager's environment and never a login shell's — so the unit
9
+ # renders one in below.
10
+ #
11
+ # The unit's name carries a slug of the repository it was installed from, so a second repository
12
+ # on this machine installs its own watcher rather than overwriting this one. It is a concrete
13
+ # file and not a `harness-watcher@.service` template unit: systemd resolves an instance name to a
14
+ # literal file of that name before falling back to a template, and a template would have to reach
15
+ # the checkout path through %i — the value this file renders in below instead.
16
+ #
17
+ # On a headless host, `loginctl enable-linger <account>` is what keeps a user unit running
18
+ # while nobody is logged in. That is left to the operator deliberately: it changes how the
19
+ # account behaves outside this harness.
20
+ #
21
+ # Every value below is substituted at install time — no account name, no home directory and no
22
+ # checkout path is written into this file. The renderer escapes each value for the directive it
23
+ # lands in: `%` is doubled everywhere, and the ExecStart path is quoted here and backslash-
24
+ # escaped there, so a checkout holding a space is one command and not two. The two log lines
25
+ # need systemd 240 or newer for `append:`; on anything older, drop them and read the watcher's
26
+ # output from the journal.
27
+ #
28
+ # Prose written here to be shared with, or copied into, the launchd template must carry no double
29
+ # hyphen: a `#` comment forbids nothing, and an XML comment there may not contain one.
30
+
31
+ [Unit]
32
+ Description=Autonomous SDLC harness run watcher for {{projectName}}
33
+
34
+ [Service]
35
+ Type=simple
36
+ ExecStart=/bin/bash "{{watcherPath}}"
37
+ WorkingDirectory={{repoRoot}}
38
+
39
+ # The PATH the service runs on. Without this directive it is whatever the user manager was started
40
+ # with, and the toolchain the outer loop runs — the agent CLI, the configured package manager —
41
+ # need not resolve there. `daemon install` renders the PATH of the shell that ran it; if that shell
42
+ # had none, no directive is rendered at all, because an empty PATH is worse than the manager's own.
43
+ # The value is quoted because a PATH entry may hold a space, so a `"` or a `\` in one is backslash-
44
+ # escaped by the renderer, and its `%` is doubled like every other value here. Re-run
45
+ # `daemon install --force` when the toolchain moves.
46
+ {{environment}}
47
+ # Restarted whenever it exits: a watcher that died between dispatches looks exactly like one
48
+ # with nothing to do, and the run queue drains silently.
49
+ Restart=always
50
+ RestartSec=5
51
+
52
+ # The watcher's own output, beside the per-run logs in the run-artifact tree, which `init` has
53
+ # already created.
54
+ StandardOutput=append:{{stateDirAbs}}/autonomous_logs/watcher.out.log
55
+ StandardError=append:{{stateDirAbs}}/autonomous_logs/watcher.err.log
56
+
57
+ [Install]
58
+ WantedBy=default.target
@@ -0,0 +1,15 @@
1
+ # templates/
2
+
3
+ The generator templates `init` copies into an adopted repository. Nothing here is compiled — the package's `tsconfig.json` includes only `src/**/*.ts` — and nothing here is loaded at runtime by the plugin; these files are source material `init` writes out at adoption time. Most are rendered with configured values substituted in, but the outer-loop family under `scripts/` carries no `{{token}}` at all and is copied byte for byte, because each of those scripts reads `harness.config.json` **at run time** rather than carrying a value frozen in when `init` ran — `scripts/README.md` states that rule and `cli/src/generators/outerLoopScripts.ts` implements it. Roadmap items 6, 7, 13 and 14 fill the tree and all four have shipped, and each subdirectory's README names its own content owner and its writer; the package's `files` field carries the tree into the published tarball. One subdirectory per adopter-side home:
4
+
5
+ | Template subdirectory | Adopter-side home `init` writes to |
6
+ |---|---|
7
+ | `claude/` | `.claude/` in the adopter's repo |
8
+ | `repo/` | the adopter's repo root |
9
+ | `githooks/` | the configured `githooksDir` |
10
+ | `scripts/` | the configured `scriptsDir` |
11
+ | `state-dir/` | the configured `stateDir` |
12
+
13
+ **Naming rule for the whole tree: a template whose adopter-side name begins with a dot is stored here without the dot.** `repo/gitignore` is written as `.gitignore`, `repo/mcp.json` as `.mcp.json`, and the `claude/` subtree lands at `.claude/`; `init` adds the dot when it writes. The rule is load-bearing twice over — a real `.gitignore` inside this repository would be honoured by git and could silently exclude files that are meant to ship, and npm rewrites a packaged `.gitignore` to `.npmignore` on publish, so a dot-named template does not survive the wire.
14
+
15
+ A template whose adopter-side home is none of the five above is new information for the item that adds it: give it a sixth subdirectory rather than forcing it into one of these.
@@ -0,0 +1,54 @@
1
+ # {{projectName}}
2
+
3
+ {{setupBanner}}
4
+
5
+ _`/harness-analyze` writes this paragraph from the repository itself — or replace this line by hand: what this project is, who uses it, and anything an agent must know before it touches a file._
6
+
7
+ ---
8
+
9
+ ## File naming conventions
10
+
11
+ | Type | Pattern | Example |
12
+ |---|---|---|
13
+ | _(kind of file)_ | _(the name pattern it follows)_ | _(one real file in this repository that follows it)_ |
14
+
15
+ _`/harness-analyze` fills this table from the repository's real file names — or add the rows by hand, one per kind of file this project has a naming rule for. Until it is filled, an implementer follows whatever the files around it already do, which is slower and less consistent than one row here._
16
+
17
+ ---
18
+
19
+ ## Context files (read on demand)
20
+
21
+ This file is auto-loaded into every agent, which is why it deliberately holds almost nothing. Everything else is loaded on purpose: **read the relevant file before starting work in that area — do not load them all upfront.** An always-loaded file carrying every rule this project has would spend every agent's context budget on every turn, and the table below exists so that it does not.
22
+
23
+ | When working on… | Read |
24
+ |---|---|
25
+ {{routingRows}}
26
+
27
+ > **Checkout root — derive it, never assume it.** Take the root of the checkout **you are running in** from a **bare** `git rev-parse --show-toplevel` (a read-only command an unattended run can allow-list as a literal), then build every literal path from that result — this run's artifacts under `<root>/{{stateDir}}/`, the lessons ledger at `<root>/{{stateDir}}/lessons.md`. This binds **every** repo-relative path you are given and every file you write, not only the ledger. Do not hardcode an absolute path, do not wrap the substitution inside another shell command, and do not stash it in a shell variable — separate tool calls do not share shell state, and a command carrying a substitution is not reliably auto-allowed in an unattended run. In a second working copy the original checkout is a *different branch*: reading it returns that branch's file, and writing to it puts this run's artifact on the wrong branch. One exception, and it is not one you resolve: an **argument** path handed to a wrapper script is **relative to that wrapper's own base and never prefixed with a checkout root** — which base that is, is the wrapper's to state (its `--repo`, or the directory it `cd`s to); take it from the wrapper's own documented usage rather than assuming the repo top. The wrapper's own invocation path is rooted like everything else. The links in the table above are document pointers; resolve the live path this way at read and write time.
28
+
29
+ The rows above were generated from the `layers` list in `harness.config.json`, so this table and the layers the orchestrator dispatches on started out in agreement. Keep them that way: add a layer there, then add its row here by hand — that is the route that costs nothing. `autonomous-sdlc-harness init --force` will also re-render the rows from the `layers` list as it then stands (`--force` overwrites generated files but never `harness.config.json`, which `autonomous-sdlc-harness init` reads on every run, so the layer just added is the one the rows come from), but it regenerates **this whole file** from the template — every section `/harness-analyze` filled goes with it, recoverable only from the single `CLAUDE.md.bak` the run writes. That `.bak` is single-generation, and the next `--force` does not necessarily spend it: a forced run that finds this file byte-identical to the one it would write keeps the file and leaves the `.bak` alone, so the sections a previous pass rescued into it stay there. What overwrites a `.bak` holding filled sections is a forced run over a file that has been filled again since. The orchestrator reads none of these files; the committer reads exactly one section of one of them — the commit-message policy in the shared cross-layer conventions document — and nothing else. One read-on-demand document is deliberately not in that table because no layer owns it: `.claude/harness-task-offer.md`, which `## Where a change request runs` below points at directly and nothing else reads.
30
+
31
+ ---
32
+
33
+ ## Agent authoring rules
34
+
35
+ **Every agent you add under `.claude/agents/` MUST declare a `tools:` allowlist, and that allowlist MUST omit the browser-automation tool namespaces unless the agent is the one that drives a browser for the interactive test phase.** That agent's own allowlist grants them explicitly, and it is the only one that may.
36
+
37
+ The allowlist is the whole mechanism, deliberately: a global deny is evaluated before any allow and cannot be overridden, and a subagent's `tools:` allowlist compiles into *narrowing* deny rules in the same pool rather than into overriding allow ones — so a namespace-wide deny would revoke the test agent's own grant as well. An agent added with no `tools:` field inherits the full default tool set and silently re-opens browser access, so a review rejects it.
38
+
39
+ ---
40
+
41
+ ## Where a change request runs
42
+
43
+ Four tests, all of which must hold, **in this order** — the first three cost nothing, so reach the fourth only when they have all passed. If any fails: say nothing, offer nothing, and carry out the instruction you were given.
44
+
45
+ 1. **Tool.** `AskUserQuestion` is in your own toolset. If it is not, you are an unattended run and nothing below applies to you.
46
+ 2. **Provenance.** The message was **typed by the user in this conversation**. Not an instruction you are executing from a harness command or a flow-instruction document it dispatched — every `/branch-*` and `/harness-*`, supervised, semi-autonomous and unattended alike, including reading a task prompt or a review file as your own work. Not one handed to you as a **dispatched sub-agent**, whichever agent type you are and whatever your toolset holds. Not a continuation of work the user has already routed, in this conversation or in the one that dispatched you.
47
+ 3. **Trigger.** The message **asks for a change to this project's code** — a feature, a fix, a refactor, a chore. **Size is never a factor.** The line is *asks for a change* versus *asks about the code*: *"explain this function"* fires **nothing**; *"this button isn't centred"* fires; *"check this file `/some/path/notes.txt` to implement adding comments to a content item"* fires too, because where a spec lives does not change what is being asked. A message invoking or continuing a harness command does not fire. Any shape you cannot place: **stay silent**.
48
+ 4. **Opt-out.** `.claude/harness-no-offer` does not exist at the **main worktree's** root — the checkout you are running in may be a worktree of it, and the marker is written once for all of them. Test it with the shell rather than by reading anything: `test -e "$(git worktree list | head -1 | awk '{print $1}')/.claude/harness-no-offer"`, whose first line is always the main worktree and which is the same path in an ordinary single-checkout repository; the checkout-root rule's literal-command constraint is an unattended-run allow-listing concern and clause 1 has already excluded those. Presence-only: never read, parse or act on anything inside it — a read of a path outside this session's own root can raise a permission prompt mid-fence, while the test's non-zero exit is a clean answer. The file is normally absent, and absent means only *not opted out*; a check you cannot make — the command declined, the repository not a git one, no output — is silence too, never a remark to the user about a file they never created.
49
+
50
+ All four hold: read `.claude/harness-task-offer.md`, at the root of the checkout you are running in, and follow it — it owns the question, the four options and what each answer does — and ask nothing before reading it. If that file cannot be read, make no offer and carry out the request in this session.
51
+
52
+ ---
53
+
54
+ _Written by `autonomous-sdlc-harness init`, and yours from there on: edit it freely, a re-run keeps your copy._
@@ -0,0 +1,5 @@
1
+ # claude/
2
+
3
+ Copied into the adopter's `.claude/` directory by `autonomous-sdlc-harness init`: the project-context and conventions stubs that `/harness-analyze` fills in from the adopted repository's real code, under the marker contract `docs/analyze.md` records, the settings and permission profile the unattended modes run under (roadmap item 14), the `*.env.example` files an adopter copies and completes with its own credentials and push configuration, `harness-task-offer.md`, the change-request offer rules the always-loaded file's fence reads on demand, and — when the interactive test phase is on — the test-scenario-rules skeleton that phase's agents read. The settings profile is the reason a CLI exists at all — a plugin cannot write a repository's `settings.json`, and getting that profile right, with every wrapper script allow-listed in the exact literal form the guard matches, is the highest-friction part of adoption.
4
+
5
+ **This directory is never named `.claude` inside this repository, and must not be renamed to it.** These are templates for **someone else's** configuration, not this repository's own: the dot is added at the adopter's end, by `autonomous-sdlc-harness init`, and nowhere else, so the stored path is the undotted one. The reason once given here was that a dotted path would register these templates as live agents and commands in any session opened at this root; that was measured false on Claude Code 2.1.234 on 2026-08-19, against a root-level control in the same repository — a `.claude/` in a subdirectory registers nothing into a session rooted above it — so do not restore it. The one literal `.claude/` in this tree, under `examples/notes-app/`, is an adopted repository's own generated output rather than a template, which is why it is not a counter-example to this rule.
@@ -0,0 +1,29 @@
1
+ # Request-handling layer
2
+
3
+ > **Read this when:** the change is about an endpoint this project serves — its route, its request and response shapes, its status codes, its authentication or its validation. **Skip when:** the change is a rule that would hold however it was invoked; that belongs to the layer owning the rules.
4
+
5
+ **Purpose.** What every endpoint here does before it does anything else, and how thin a handler is required to stay.
6
+
7
+ **What belongs here**
8
+
9
+ - How a route is declared and registered, which file it lives in and how that file is named.
10
+ - The validation step every handler runs before it touches anything else, and where the request shape it validates against is declared.
11
+ - Authentication and authorisation: where each check is made, and what a caller who fails one gets back.
12
+ - The response contract — the success shape, the error shape, and which status code carries which outcome — so two endpoints do not report the same failure two ways.
13
+ - What counts as a breaking change to a published endpoint, and how a compatible one is introduced.
14
+
15
+ **Rule that holds whatever the framework is:** a handler validates, delegates and formats — nothing else. A rule written inside a handler is unreachable from every other entry point this project has, so the scheduled job, the administrative tool and the next endpoint each grow their own copy of it, and the copies disagree before anyone notices there are several.
16
+
17
+ **One generic example**
18
+
19
+ ```
20
+ POST /orders
21
+ 1. validate the request against the declared shape — refuse before anything else runs
22
+ 2. authorise this caller for this action
23
+ 3. delegate to one unit of work in the layer that owns the rules
24
+ 4. map its result onto the response shape and the status code
25
+ ```
26
+
27
+ _Run `{{analyzeInvocation}}` to fill this in from this repository's own code, or replace everything above with this project's own request-handling rules; either way this file is yours from here on and a re-run of `autonomous-sdlc-harness init` keeps your copy — and if you write it by hand, the marker on the last line goes with it._
28
+
29
+ <!-- harness:unfilled -->