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.
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +24 -0
- package/dist/cli.js +194 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.js +561 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/daemon.js +791 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/doctor.js +336 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.js +2023 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/registry.js +42 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/config/check.js +505 -0
- package/dist/config/check.js.map +1 -0
- package/dist/config/io.js +177 -0
- package/dist/config/io.js.map +1 -0
- package/dist/config/model.js +406 -0
- package/dist/config/model.js.map +1 -0
- package/dist/core/errors.js +71 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +537 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/json.js +125 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/layerCoverage.js +141 -0
- package/dist/core/layerCoverage.js.map +1 -0
- package/dist/core/layerGapRemedy.js +62 -0
- package/dist/core/layerGapRemedy.js.map +1 -0
- package/dist/core/nameList.js +23 -0
- package/dist/core/nameList.js.map +1 -0
- package/dist/core/paths.js +153 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/prompt.js +206 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/repoPaths.js +55 -0
- package/dist/core/repoPaths.js.map +1 -0
- package/dist/core/report.js +150 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/templating.js +88 -0
- package/dist/core/templating.js.map +1 -0
- package/dist/core/writer.js +479 -0
- package/dist/core/writer.js.map +1 -0
- package/dist/daemon/backend.js +180 -0
- package/dist/daemon/backend.js.map +1 -0
- package/dist/daemon/units.js +380 -0
- package/dist/daemon/units.js.map +1 -0
- package/dist/detect/nestedApplication.js +79 -0
- package/dist/detect/nestedApplication.js.map +1 -0
- package/dist/detect/presets.js +2033 -0
- package/dist/detect/presets.js.map +1 -0
- package/dist/detect/signals.js +1368 -0
- package/dist/detect/signals.js.map +1 -0
- package/dist/doctor/checks.js +3530 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/generators/claudeContext.js +588 -0
- package/dist/generators/claudeContext.js.map +1 -0
- package/dist/generators/githooks.js +446 -0
- package/dist/generators/githooks.js.map +1 -0
- package/dist/generators/harnessConfig.js +632 -0
- package/dist/generators/harnessConfig.js.map +1 -0
- package/dist/generators/notifications.js +191 -0
- package/dist/generators/notifications.js.map +1 -0
- package/dist/generators/outerLoopScripts.js +165 -0
- package/dist/generators/outerLoopScripts.js.map +1 -0
- package/dist/generators/permissionProfile.js +1172 -0
- package/dist/generators/permissionProfile.js.map +1 -0
- package/dist/generators/projectSettings.js +322 -0
- package/dist/generators/projectSettings.js.map +1 -0
- package/dist/generators/repoRoot.js +417 -0
- package/dist/generators/repoRoot.js.map +1 -0
- package/dist/generators/scripts.js +557 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/generators/stateDir.js +221 -0
- package/dist/generators/stateDir.js.map +1 -0
- package/dist/machine/paths.js +111 -0
- package/dist/machine/paths.js.map +1 -0
- package/dist/machine/plugins.js +224 -0
- package/dist/machine/plugins.js.map +1 -0
- package/dist/machine/registry.js +330 -0
- package/dist/machine/registry.js.map +1 -0
- package/package.json +23 -0
- package/scripts/README.md +13 -0
- package/scripts/daemon/launchd.plist.template +59 -0
- package/scripts/daemon/systemd.service.template +58 -0
- package/templates/README.md +15 -0
- package/templates/claude/CLAUDE.md +54 -0
- package/templates/claude/README.md +5 -0
- package/templates/claude/context/api.md +29 -0
- package/templates/claude/context/conventions.md +23 -0
- package/templates/claude/context/data-layer.md +28 -0
- package/templates/claude/context/data-storage.md +29 -0
- package/templates/claude/context/docs-catalog.md +29 -0
- package/templates/claude/context/domain.md +28 -0
- package/templates/claude/context/layer.md +20 -0
- package/templates/claude/context/module.md +30 -0
- package/templates/claude/context/package.md +29 -0
- package/templates/claude/context/presentation.md +32 -0
- package/templates/claude/context/state-slices.md +28 -0
- package/templates/claude/context/tests.md +28 -0
- package/templates/claude/harness-task-offer.md +58 -0
- package/templates/claude/push-notify.env.example +21 -0
- package/templates/claude/qa-accounts.env.example +38 -0
- package/templates/claude/qa_test_scenarios.md +110 -0
- package/templates/claude/settings.autonomous.json +93 -0
- package/templates/claude/settings.autonomous.qa.json +36 -0
- package/templates/githooks/README.md +3 -0
- package/templates/githooks/pre-push +72 -0
- package/templates/repo/README.md +3 -0
- package/templates/repo/gitattributes +16 -0
- package/templates/repo/gitignore +61 -0
- package/templates/repo/gitignore.qa +25 -0
- package/templates/repo/mcp.json +17 -0
- package/templates/scripts/README.md +5 -0
- package/templates/scripts/autonomous-format-stream.sh +95 -0
- package/templates/scripts/autonomous-notify.sh +337 -0
- package/templates/scripts/autonomous-watcher.sh +3087 -0
- package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
- package/templates/scripts/commit-on-branch.sh +288 -0
- package/templates/scripts/create-worktree.sh +360 -0
- package/templates/scripts/deploy.sh +47 -0
- package/templates/scripts/lib/harness-run-lib.sh +1481 -0
- package/templates/scripts/push-branch.sh +140 -0
- package/templates/scripts/refresh-branch.sh +244 -0
- package/templates/scripts/restart-watcher.sh +401 -0
- package/templates/scripts/scratch-run.sh +302 -0
- package/templates/scripts/setup-worktree.sh +262 -0
- package/templates/scripts/start-dev-server.sh +99 -0
- package/templates/scripts/test.sh +50 -0
- package/templates/scripts/typecheck.sh +50 -0
- package/templates/state-dir/README-root.md +13 -0
- package/templates/state-dir/README.md +9 -0
- package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
- package/templates/state-dir/architecture_reviews/README.md +9 -0
- package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
- package/templates/state-dir/autonomous_inbox/README.md +9 -0
- package/templates/state-dir/autonomous_logs/README.md +9 -0
- package/templates/state-dir/branch_statistics/README.md +9 -0
- package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
- package/templates/state-dir/clarification_digests/README.md +9 -0
- package/templates/state-dir/clarifications/README.md +9 -0
- package/templates/state-dir/code_reviews/README.md +9 -0
- package/templates/state-dir/dispatch_additions/README.md +19 -0
- package/templates/state-dir/docs_catalog/README.md +9 -0
- package/templates/state-dir/flow_progress/README.md +9 -0
- package/templates/state-dir/improvement_observations/README.md +19 -0
- package/templates/state-dir/improvement_suggestions.md +29 -0
- package/templates/state-dir/lessons.md +23 -0
- package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
- package/templates/state-dir/qa_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_reviews/README.md +9 -0
- package/templates/state-dir/scratch/README.md +11 -0
- package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_reviews/README.md +9 -0
- package/templates/state-dir/story_plans/README.md +9 -0
- package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/task_plan_reviews/README.md +9 -0
- package/templates/state-dir/task_plans/README.md +9 -0
- package/templates/state-dir/task_prompts/README.md +9 -0
- package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
- package/templates/state-dir/ui_test_plans/README.md +9 -0
- package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
- 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 -->
|