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,221 @@
1
+ /**
2
+ * Generator: the run-artifact tree `init` materialises at the configured `stateDir`.
3
+ *
4
+ * **The rule this module exists to enforce: {@link STATE_DIR_ENTRIES} is the single declaration of
5
+ * which directories the tree has and which phase each one belongs to.** One row is one line, so a
6
+ * directory is added or gated by editing a row rather than by threading a condition through the
7
+ * writer, and `doctor` asks {@link selectedStateDirs} what should be there instead of re-deriving
8
+ * the list from its own copy. A second copy of this table anywhere is a copy that will drift.
9
+ *
10
+ * ## Three non-obvious choices, and where each comes from
11
+ *
12
+ * 1. **A phase that is off gets no directories at all.** An adopter who never runs the parity phase
13
+ * has no reference implementation to compare against, so a `business_parity_*` directory in
14
+ * their tree would be an empty slot they cannot fill and cannot tell from one the flow forgot to
15
+ * write. Turning the phase on in `harness.config.json` and re-running `init` creates them, which
16
+ * is why the omission is safe as well as tidy.
17
+ * 2. **The two ledgers are `create-if-absent` and are never truncated or rewritten.** They
18
+ * accumulate over a project's whole life and are the only copy of that history — the write
19
+ * engine's table says the same (`core/writer.ts`) — so a re-run leaves a ledger with content in
20
+ * it byte-identical. This is the artifact where the re-run contract is not a nicety: an
21
+ * overwrite here destroys a record nothing else holds.
22
+ * 3. **Templates are copied verbatim; there is no token substitution in this generator.** The
23
+ * wrapper-script generator renders `{{token}}`s because a wrapper's body is a *detected* value;
24
+ * nothing in this tree depends on one, and a README that named its own configured path would go
25
+ * stale the moment the adopter moved the tree. A path a reader wants is read from the config.
26
+ *
27
+ * ## Scope this release
28
+ *
29
+ * This module is the **writer**, and what it writes out is a contract per directory: each README
30
+ * states what one file in its directory is and exactly how it is named, who writes it and who reads
31
+ * it by role, when it appears and when it is superseded, and the one mistake a reader would
32
+ * otherwise make. The tree's root README frames the set, and the two ledgers carry a worked example
33
+ * entry each. Every row here has a file at its final path and name, so revising a contract is an
34
+ * edit in place rather than a second layout.
35
+ *
36
+ * Four of this tree's directories are runtime surfaces whose *contents* are machine-local — the two
37
+ * the run daemon writes, the park-and-ask clarification channel, and the scratch directory an agent
38
+ * writes a probe file into — but which of them an ignore rule
39
+ * may cover is settled in the generator that writes the repository's ignore file
40
+ * (`generators/repoRoot.ts`), not here: a directory with a row in {@link STATE_DIR_ENTRIES} has a
41
+ * committed README and is therefore ignored **by its contents** with a negation for that file, because
42
+ * a whole-directory rule takes the README with it and no negation can bring it back. That generator
43
+ * reads this table to check the row still exists, so a directory removed on either side fails loudly
44
+ * rather than leaving a rule pointing at nothing.
45
+ */
46
+ import { join } from 'node:path';
47
+ import { DEFAULTS, STATE_DIR_DOT_PATTERN } from '../config/model.js';
48
+ import { HarnessError } from '../core/errors.js';
49
+ import { readTemplate } from '../core/paths.js';
50
+ import { normalizeRepoDir } from '../core/repoPaths.js';
51
+ /**
52
+ * Every directory of the run-artifact tree, in the order `init` writes it: the always-on set
53
+ * first, then the phase-gated ones grouped by phase.
54
+ *
55
+ * The set comes from the artifact families the flow actually writes — one directory per family,
56
+ * with the per-item findings directories kept separate from the reviews they re-check, because the
57
+ * two have different writers and different readers.
58
+ */
59
+ export const STATE_DIR_ENTRIES = Object.freeze([
60
+ // Always written.
61
+ Object.freeze({ dir: 'task_prompts' }),
62
+ Object.freeze({ dir: 'story_plans' }),
63
+ Object.freeze({ dir: 'task_plans' }),
64
+ Object.freeze({ dir: 'code_reviews' }),
65
+ Object.freeze({ dir: 'task_plan_reviews' }),
66
+ Object.freeze({ dir: 'task_plan_point_reviews' }),
67
+ Object.freeze({ dir: 'review_plan_reviews' }),
68
+ Object.freeze({ dir: 'review_plan_point_reviews' }),
69
+ Object.freeze({ dir: 'skeptic_reviews' }),
70
+ Object.freeze({ dir: 'skeptic_review_plan_reviews' }),
71
+ Object.freeze({ dir: 'skeptic_review_point_reviews' }),
72
+ Object.freeze({ dir: 'architecture_reviews' }),
73
+ Object.freeze({ dir: 'architecture_branch_reviews' }),
74
+ Object.freeze({ dir: 'architecture_branch_review_point_reviews' }),
75
+ Object.freeze({ dir: 'architecture_user_review_reviews' }),
76
+ Object.freeze({ dir: 'user_reviews' }),
77
+ Object.freeze({ dir: 'user_review_fix_plan_point_reviews' }),
78
+ Object.freeze({ dir: 'flow_progress' }),
79
+ Object.freeze({ dir: 'branch_statistics' }),
80
+ Object.freeze({ dir: 'autonomous_inbox' }),
81
+ Object.freeze({ dir: 'autonomous_logs' }),
82
+ Object.freeze({ dir: 'clarifications' }),
83
+ // The fourth of the runtime surfaces whose contents are machine-local, and the reason it sits with
84
+ // the three above rather than under a phase: an implementer or a reviewer in *any* phase may need
85
+ // to run a probe, so gating it would take the capability away from the phases that ask for it most.
86
+ Object.freeze({ dir: 'scratch' }),
87
+ Object.freeze({ dir: 'improvement_observations' }),
88
+ Object.freeze({ dir: 'clarification_digests' }),
89
+ Object.freeze({ dir: 'dispatch_additions' }),
90
+ // The interactive-test phase.
91
+ Object.freeze({ dir: 'qa_reviews', phase: 'qa' }),
92
+ Object.freeze({ dir: 'qa_review_point_reviews', phase: 'qa' }),
93
+ Object.freeze({ dir: 'ui_test_plans', phase: 'qa' }),
94
+ Object.freeze({ dir: 'ui_test_plan_reviews', phase: 'qa' }),
95
+ // The documentation phase.
96
+ Object.freeze({ dir: 'docs_catalog', phase: 'docs' }),
97
+ // The reference-implementation-parity phase.
98
+ Object.freeze({ dir: 'business_parity_reviews', phase: 'parity' }),
99
+ Object.freeze({ dir: 'business_parity_branch_reviews', phase: 'parity' }),
100
+ Object.freeze({ dir: 'business_parity_branch_review_point_reviews', phase: 'parity' }),
101
+ Object.freeze({ dir: 'business_parity_user_review_reviews', phase: 'parity' }),
102
+ ]);
103
+ /**
104
+ * The long-lived ledgers at the root of the tree, which are files rather than directories and are
105
+ * written whatever the phases are.
106
+ */
107
+ export const STATE_DIR_LEDGERS = Object.freeze(['lessons.md', 'improvement_suggestions.md']);
108
+ /** The templates' subdirectory under `cli/templates/`, addressed as {@link readTemplate} wants it. */
109
+ const TEMPLATE_DIR = 'state-dir';
110
+ /** The per-directory contract file, in the template tree and in the adopter's tree alike. */
111
+ const README_FILENAME = 'README.md';
112
+ /**
113
+ * The template for the tree's own top-level README.
114
+ *
115
+ * Named apart from the per-directory ones because `templates/state-dir/README.md` already describes
116
+ * the *template* directory to a reader of this repository, and is not written to an adopter.
117
+ */
118
+ const ROOT_README_TEMPLATE = 'README-root.md';
119
+ /**
120
+ * Re-assert the two things `stateDir` must be before a single directory is enqueued under it.
121
+ *
122
+ * The schema checks both and so does `config/check.ts`, and this checks them **again** deliberately:
123
+ * a config can be hand-edited between the run that validated it and this one, and by the time a
124
+ * dot-named tree has been created the failure is invisible — an unattended run may not be permitted
125
+ * to write beneath a dot-path at all, and it reports success with no artifacts to show for it. That is a condition the adopter fixes by editing one key, so it carries the default
126
+ * exit code rather than the internal one.
127
+ *
128
+ * The check runs against the **raw** configured value rather than the normalised one: normalising
129
+ * strips a leading `./`, which is itself a dot segment the rule refuses.
130
+ */
131
+ function assertWritableStateDir(raw) {
132
+ if (STATE_DIR_DOT_PATTERN.test(raw)) {
133
+ throw new HarnessError(`stateDir is ${JSON.stringify(raw)}, which has a path segment starting with '.': the run-artifact tree must not be dot-named or reach through a dot segment, because it has to be writable by an unattended run and a dot-path is where a host reserves directories an unattended run may not write to — measured for .claude/**, where such a run completes with exit 0 having written nothing. Set stateDir in harness.config.json to a name without a leading dot; do not "fix" it back to a dot-name`);
134
+ }
135
+ const normalized = normalizeRepoDir(raw.trim());
136
+ if (normalized === '.' || normalized === '') {
137
+ throw new HarnessError(`stateDir is ${JSON.stringify(raw)}, which names the repository root rather than a directory in it: the run-artifact tree is a directory of its own, and writing it at the root would scatter the flow's artifact directories through the repository. Set stateDir in harness.config.json to a directory name`);
138
+ }
139
+ return normalized;
140
+ }
141
+ /** Whether a phase is on. Absent phases are off, which is the schema's default for all three. */
142
+ function phaseEnabled(phases, phase) {
143
+ return phases?.[phase] === true;
144
+ }
145
+ /**
146
+ * The directories that belong in the tree for a given set of phases, in table order.
147
+ *
148
+ * Exported so `doctor` reports a missing directory against the same list `init` writes, rather than
149
+ * against a second copy of it that can disagree.
150
+ */
151
+ export function selectedStateDirs(phases) {
152
+ return STATE_DIR_ENTRIES.filter((entry) => entry.phase === undefined || phaseEnabled(phases, entry.phase));
153
+ }
154
+ /**
155
+ * Enqueue the run-artifact tree: `ensure-dir` for the root and for every selected directory, and
156
+ * `create-if-absent` for the tree's README, each directory's README and both ledgers.
157
+ *
158
+ * `ensure-dir` is the whole of the directory contract — it is idempotent by construction, so a
159
+ * second `init` reports the tree as ensured and changes nothing, and it never removes a directory
160
+ * an adopter added. `create-if-absent` is the contract for every file here: the READMEs because the
161
+ * adopter may have rewritten a contract sentence to match how their team uses the directory, and
162
+ * the ledgers because they accumulate and an overwrite would destroy history.
163
+ *
164
+ * The two differ in what `--force` may do to them. A README is template content, so `--force`
165
+ * replaces one after the write engine has taken a `.bak`, and the adopter's sentence is a `.bak`
166
+ * away. **The ledgers are outside `--force` entirely** (`forceOverride: 'never'`): nothing can
167
+ * re-derive an accumulating ledger, and the engine's `.bak` is single-generation — it is
168
+ * overwritten rather than chained — so "recoverable" would only ever have meant "recoverable until
169
+ * the next forced run". `--force` is an operation this CLI's own remedy messages routinely send an
170
+ * adopter to, so a ledger has to survive it however many times it is run.
171
+ *
172
+ * Nothing here touches the filesystem: the generator plans, and `init` applies the plan once.
173
+ */
174
+ export function writeStateDir({ repoRoot, config, plan }) {
175
+ const stateDir = assertWritableStateDir(config.stateDir ?? DEFAULTS.stateDir);
176
+ const root = join(repoRoot, stateDir);
177
+ const directories = [];
178
+ const skipped = [];
179
+ const notes = [];
180
+ plan.add({ path: root, policy: 'ensure-dir', label: 'run-artifact tree' });
181
+ plan.add({
182
+ path: join(root, README_FILENAME),
183
+ policy: 'create-if-absent',
184
+ content: readTemplate(`${TEMPLATE_DIR}/${ROOT_README_TEMPLATE}`),
185
+ label: 'run-artifact tree README',
186
+ });
187
+ for (const ledger of STATE_DIR_LEDGERS) {
188
+ plan.add({
189
+ path: join(root, ledger),
190
+ policy: 'create-if-absent',
191
+ content: readTemplate(`${TEMPLATE_DIR}/${ledger}`),
192
+ label: `ledger ${ledger}`,
193
+ // Outside `--force` entirely (`core/writer.ts`'s `WriteRequest.forceOverride`), and for the
194
+ // same reason `harness.config.json` is: an accumulating ledger has no derivable content to
195
+ // regenerate, so an overwrite is pure loss — and the engine's `.bak` is single-generation, so
196
+ // a second forced run takes the history with it. `--force` is an operation this CLI's own
197
+ // remedy messages send an adopter to, so it has to be safe to run twice.
198
+ forceOverride: 'never',
199
+ });
200
+ }
201
+ for (const entry of STATE_DIR_ENTRIES) {
202
+ const relative = `${stateDir}/${entry.dir}`;
203
+ if (entry.phase !== undefined && !phaseEnabled(config.phases, entry.phase)) {
204
+ skipped.push(relative);
205
+ continue;
206
+ }
207
+ plan.add({ path: join(root, entry.dir), policy: 'ensure-dir', label: `artifact directory ${entry.dir}` });
208
+ plan.add({
209
+ path: join(root, entry.dir, README_FILENAME),
210
+ policy: 'create-if-absent',
211
+ content: readTemplate(`${TEMPLATE_DIR}/${entry.dir}/${README_FILENAME}`),
212
+ label: `artifact directory README ${entry.dir}`,
213
+ });
214
+ directories.push(relative);
215
+ }
216
+ if (skipped.length > 0) {
217
+ notes.push(`${skipped.length} artifact ${skipped.length === 1 ? 'directory belongs' : 'directories belong'} to a phase that is off, so ${skipped.length === 1 ? 'it was' : 'they were'} not created: turn the phase on in harness.config.json and re-run init to add ${skipped.length === 1 ? 'it' : 'them'}`);
218
+ }
219
+ return { stateDir, root, directories, skipped, notes };
220
+ }
221
+ //# sourceMappingURL=stateDir.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stateDir.js","sourceRoot":"","sources":["../../src/generators/stateDir.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAA0C,MAAM,oBAAoB,CAAC;AAC7G,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAcxD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAiC,MAAM,CAAC,MAAM,CAAC;IAC3E,kBAAkB;IAClB,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC;IACtC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,aAAa,EAAE,CAAC;IACrC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC;IACpC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC;IACtC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,CAAC;IAC3C,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,yBAAyB,EAAE,CAAC;IACjD,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,qBAAqB,EAAE,CAAC;IAC7C,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,2BAA2B,EAAE,CAAC;IACnD,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,iBAAiB,EAAE,CAAC;IACzC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,6BAA6B,EAAE,CAAC;IACrD,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,8BAA8B,EAAE,CAAC;IACtD,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,sBAAsB,EAAE,CAAC;IAC9C,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,6BAA6B,EAAE,CAAC;IACrD,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,0CAA0C,EAAE,CAAC;IAClE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,kCAAkC,EAAE,CAAC;IAC1D,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC;IACtC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,oCAAoC,EAAE,CAAC;IAC5D,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,eAAe,EAAE,CAAC;IACvC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,CAAC;IAC3C,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,kBAAkB,EAAE,CAAC;IAC1C,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,iBAAiB,EAAE,CAAC;IACzC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,CAAC;IACxC,mGAAmG;IACnG,kGAAkG;IAClG,oGAAoG;IACpG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;IACjC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,0BAA0B,EAAE,CAAC;IAClD,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,uBAAuB,EAAE,CAAC;IAC/C,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,oBAAoB,EAAE,CAAC;IAC5C,8BAA8B;IAC9B,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACjD,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,yBAAyB,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC9D,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACpD,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,sBAAsB,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC3D,2BAA2B;IAC3B,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACrD,6CAA6C;IAC7C,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,yBAAyB,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,gCAAgC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;IACzE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,6CAA6C,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;IACtF,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,qCAAqC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;CACtE,CAAC,CAAC;AAEZ;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAsB,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,EAAE,4BAA4B,CAAC,CAAC,CAAC;AAEhH,sGAAsG;AACtG,MAAM,YAAY,GAAG,WAAW,CAAC;AAEjC,6FAA6F;AAC7F,MAAM,eAAe,GAAG,WAAW,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,oBAAoB,GAAG,gBAAgB,CAAC;AA0B9C;;;;;;;;;;;GAWG;AACH,SAAS,sBAAsB,CAAC,GAAW;IACzC,IAAI,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,YAAY,CACpB,eAAe,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,ucAAuc,CAC1e,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAChD,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,YAAY,CACpB,eAAe,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,4QAA4Q,CAC/S,CAAC;IACJ,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,iGAAiG;AACjG,SAAS,YAAY,CAAC,MAAiC,EAAE,KAAoB;IAC3E,OAAO,MAAM,EAAE,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAiC;IACjE,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7G,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAmB;IACvE,MAAM,QAAQ,GAAG,sBAAsB,CAAC,MAAM,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC9E,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACtC,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;IAC3E,IAAI,CAAC,GAAG,CAAC;QACP,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC;QACjC,MAAM,EAAE,kBAAkB;QAC1B,OAAO,EAAE,YAAY,CAAC,GAAG,YAAY,IAAI,oBAAoB,EAAE,CAAC;QAChE,KAAK,EAAE,0BAA0B;KAClC,CAAC,CAAC;IAEH,KAAK,MAAM,MAAM,IAAI,iBAAiB,EAAE,CAAC;QACvC,IAAI,CAAC,GAAG,CAAC;YACP,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;YACxB,MAAM,EAAE,kBAAkB;YAC1B,OAAO,EAAE,YAAY,CAAC,GAAG,YAAY,IAAI,MAAM,EAAE,CAAC;YAClD,KAAK,EAAE,UAAU,MAAM,EAAE;YACzB,4FAA4F;YAC5F,2FAA2F;YAC3F,8FAA8F;YAC9F,0FAA0F;YAC1F,yEAAyE;YACzE,aAAa,EAAE,OAAO;SACvB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,iBAAiB,EAAE,CAAC;QACtC,MAAM,QAAQ,GAAG,GAAG,QAAQ,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;QAC5C,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3E,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACvB,SAAS;QACX,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,sBAAsB,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC1G,IAAI,CAAC,GAAG,CAAC;YACP,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,EAAE,eAAe,CAAC;YAC5C,MAAM,EAAE,kBAAkB;YAC1B,OAAO,EAAE,YAAY,CAAC,GAAG,YAAY,IAAI,KAAK,CAAC,GAAG,IAAI,eAAe,EAAE,CAAC;YACxE,KAAK,EAAE,6BAA6B,KAAK,CAAC,GAAG,EAAE;SAChD,CAAC,CAAC;QACH,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CACR,GAAG,OAAO,CAAC,MAAM,aAAa,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,oBAAoB,+BAA+B,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,iFAAiF,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CACnS,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AACzD,CAAC"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The machine-local directories this CLI addresses, resolved once: its own two, and the agent
3
+ * runner's configuration home.
4
+ *
5
+ * **The rule this module exists to enforce: the environment variables that move any of them are read
6
+ * here and nowhere else in the CLI.**
7
+ * `grep -rn 'XDG_STATE_HOME\|XDG_CONFIG_HOME\|CLAUDE_CONFIG_DIR' cli/src` answers with this file
8
+ * alone, which is the property that makes the resolution correctable in one place — a second reader
9
+ * would be a second answer, and the two would disagree exactly on the machines whose environment is
10
+ * not the default one.
11
+ *
12
+ * ## Three things this module is careful about
13
+ *
14
+ * 1. **It addresses the usage lane's directory; it never re-defines it.** {@link machineStateDir}
15
+ * is the same directory `docs/watcher.md` §5 publishes as the home of `usage-state.json` and
16
+ * `run-lane.lock/`, and that section is the format of record for both artifacts. Nothing here
17
+ * describes their shape, their merge rule or their lock protocol: a TypeScript consumer reads
18
+ * them there, and this module only says where the directory is.
19
+ * 2. **The resolution matches the shell half byte for byte**, because the two halves address one
20
+ * directory on one machine: `hr_lane_dir()` in the generated `lib/harness-run-lib.sh` reads
21
+ * `XDG_STATE_HOME`, falls back to `$HOME/.local/state` when it is **unset or empty**, strips one
22
+ * trailing slash, and appends {@link MACHINE_DIR_NAME}. So does {@link resolveMachineDir}, with
23
+ * `homeRoot()` — `core/paths.ts`'s single definition of the account home — standing in for
24
+ * `$HOME`. A variable holding a relative path yields a relative directory here exactly as it
25
+ * does there. Only a target on a `WritePlan` is refused for that, and of these two directories
26
+ * the one that goes on a plan is {@link machineConfigDir} — `generators/notifications.ts`
27
+ * enqueues it under `allowOutsideRepo`. `machine/registry.ts` writes {@link machineStateDir}
28
+ * directly, so a relative base there lands under the process's working directory.
29
+ * 3. **A read never creates any of these directories.** Every function here is pure string
30
+ * resolution — nothing touches the filesystem — so a `doctor` check or any other reader can ask
31
+ * where a file would be without bringing its directory into existence. Creation belongs to a
32
+ * writer, and a writer creates it at {@link MACHINE_DIR_MODE}: these directories hold an
33
+ * account's push credential and its cross-repository run state, neither of which is another
34
+ * account's to read. {@link claudeHome} is the one directory here **no** part of this CLI
35
+ * creates or writes into at all — it is the agent runner's, and this CLI only reads from it.
36
+ */
37
+ import { homeRoot } from '../core/paths.js';
38
+ /**
39
+ * The one directory name both machine-local trees carry, under whichever base applies.
40
+ *
41
+ * It is the package name, and it is shared with the shell half's `hr_lane_dir`. Changing it here
42
+ * without changing it there would leave two components each believing they hold the machine.
43
+ */
44
+ export const MACHINE_DIR_NAME = 'autonomous-sdlc-harness';
45
+ /**
46
+ * The mode a writer creates either directory at: the owner's, and nobody else's.
47
+ *
48
+ * The same `0700` the shell half applies with an explicit `chmod` after its `mkdir -p`, and for
49
+ * the same reason — the mode argument of a directory creation is masked by the process umask and
50
+ * does nothing at all for a directory that already exists, so it is applied as its own step.
51
+ */
52
+ export const MACHINE_DIR_MODE = 0o700;
53
+ /**
54
+ * `<base>/autonomous-sdlc-harness` for one XDG base variable and its fallback, in the shell half's
55
+ * exact order: the variable when it holds something, the fallback under {@link homeRoot} otherwise,
56
+ * then one trailing slash stripped.
57
+ *
58
+ * The result is joined with a literal separator rather than through `path.join`, which would
59
+ * normalise a base of `/` into a relative result — the one input where the two spellings differ,
60
+ * and the shell half's answer is the absolute one.
61
+ */
62
+ function resolveMachineDir(variable, fallback) {
63
+ const configured = process.env[variable];
64
+ const base = configured === undefined || configured === '' ? `${homeRoot()}/${fallback}` : configured;
65
+ const stripped = base.endsWith('/') ? base.slice(0, -1) : base;
66
+ return `${stripped}/${MACHINE_DIR_NAME}`;
67
+ }
68
+ /**
69
+ * The machine-local **state** directory — `${XDG_STATE_HOME:-$HOME/.local/state}/autonomous-sdlc-harness`.
70
+ *
71
+ * What lives in it is machine-scoped run state that no single repository owns: the usage lane's two
72
+ * artifacts, published by `docs/watcher.md` §5, and the registry of initialized repositories, which
73
+ * that section is explicit is a different artifact defined elsewhere.
74
+ */
75
+ export function machineStateDir() {
76
+ return resolveMachineDir('XDG_STATE_HOME', '.local/state');
77
+ }
78
+ /**
79
+ * The machine-local **configuration** directory — `${XDG_CONFIG_HOME:-$HOME/.config}/autonomous-sdlc-harness`.
80
+ *
81
+ * What lives in it is an operator's own settings rather than a repository's: `docs/watcher.md` §6's
82
+ * `push.env` is the one file this release puts there, and it comes first in that section's
83
+ * precedence because a push credential belongs to a person and a machine rather than to a checkout.
84
+ */
85
+ export function machineConfigDir() {
86
+ return resolveMachineDir('XDG_CONFIG_HOME', '.config');
87
+ }
88
+ /**
89
+ * The environment variable that relocates the agent runner's configuration home, and the directory
90
+ * name it falls back to under {@link homeRoot}.
91
+ */
92
+ const CLAUDE_HOME_VARIABLE = 'CLAUDE_CONFIG_DIR';
93
+ const CLAUDE_HOME_FALLBACK = '.claude';
94
+ /**
95
+ * The **agent runner's** configuration home — `${CLAUDE_CONFIG_DIR:-$HOME/.claude}`.
96
+ *
97
+ * Not this CLI's directory and never written to: what it holds that the CLI reads is the record of
98
+ * which plugins are installed and where (`machine/plugins.ts`), which is the one machine-local fact
99
+ * `init` cannot generate an answer for. It is resolved here rather than beside that reader for the
100
+ * module rule above — one home for the variable, so a scratch home an operator or a test points the
101
+ * runner at moves every reader with it instead of half of them.
102
+ *
103
+ * Same three properties as the two directories above: the variable when it holds something, the
104
+ * fallback otherwise, one trailing slash stripped, and no filesystem touch.
105
+ */
106
+ export function claudeHome() {
107
+ const configured = process.env[CLAUDE_HOME_VARIABLE];
108
+ const base = configured === undefined || configured === '' ? `${homeRoot()}/${CLAUDE_HOME_FALLBACK}` : configured;
109
+ return base.endsWith('/') ? base.slice(0, -1) : base;
110
+ }
111
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/machine/paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,yBAAyB,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAEtC;;;;;;;;GAQG;AACH,SAAS,iBAAiB,CAAC,QAAgB,EAAE,QAAgB;IAC3D,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,QAAQ,EAAE,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;IACtG,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/D,OAAO,GAAG,QAAQ,IAAI,gBAAgB,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,iBAAiB,CAAC,gBAAgB,EAAE,cAAc,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,iBAAiB,CAAC,iBAAiB,EAAE,SAAS,CAAC,CAAC;AACzD,CAAC;AAED;;;GAGG;AACH,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AACjD,MAAM,oBAAoB,GAAG,SAAS,CAAC;AAEvC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACrD,MAAM,IAAI,GACR,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,QAAQ,EAAE,IAAI,oBAAoB,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;IACvG,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvD,CAAC"}
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Where this harness's plugin lives on **this** machine — both roots of it — read from the records
3
+ * the agent runner keeps.
4
+ *
5
+ * **The rule this module exists to enforce: the plugin's roots are resolved here and nowhere else in
6
+ * the CLI, and each is re-derived on every call.** The install root carries the plugin's version in
7
+ * its path, so a value written down once — into a generated permission entry, into a document —
8
+ * stops being true at the next plugin upgrade while still looking right. A caller that wants a root
9
+ * asks for it now; nothing here caches, and nothing here may be persisted.
10
+ *
11
+ * **Why there are two.** A helper named in an **instruction file** arrives as bytes and the agent
12
+ * resolves the root itself; a helper named in an **agent definition body** has `${CLAUDE_PLUGIN_ROOT}`
13
+ * substituted by the runtime. On a `directory`-sourced marketplace those two were measured to differ
14
+ * (2026-08-26): the runtime substituted `<installLocation>/plugin` — the live source tree — while
15
+ * `installPath` named a version-pinned cache snapshot. So {@link pluginInstallRoot} answers "where
16
+ * the runner installed it" and {@link pluginRuntimeRoot} answers "what the runtime substitutes",
17
+ * and on a git-sourced marketplace the two collapse to one directory.
18
+ *
19
+ * The files are the **agent runner's**, not this CLI's, both under {@link claudeHome}'s `plugins/`:
20
+ * `installed_plugins.json` for the install root, and `known_marketplaces.json` — plus the
21
+ * `.claude-plugin/marketplace.json` its `installLocation` points at — for the runtime root. Two
22
+ * consequences follow and both are deliberate.
23
+ *
24
+ * 1. **They are read-only, always.** No function here writes, creates a directory, or repairs
25
+ * anything. A format this CLI does not recognise is the runner's to change and not ours to
26
+ * correct.
27
+ * 2. **Their shape is navigated, not validated.** `version` is read past rather than pinned: a bump
28
+ * that adds a field would otherwise turn every working machine into a finding. Only the fields a
29
+ * root is navigated for are looked at, and they are checked where they are used: `installPath`
30
+ * on a plugin row, and `source.source` plus `installLocation` on a marketplace entry together
31
+ * with the plugin `source` in the manifest that entry locates. That is the opposite of
32
+ * `machine/registry.ts`'s `schema` handling, and the reason is ownership: that file is this
33
+ * CLI's own and a version it does not know means a newer CLI wrote it, while these belong to
34
+ * another program that may change them at any release.
35
+ *
36
+ * **Every failure is `undefined`, never a throw** — file absent, unreadable, unparseable, key
37
+ * absent, no usable row. A machine where the plugin is not installed is an answer a caller has
38
+ * something correct to do with, which is the discipline `core/git.ts`'s `hasCommits` states for the
39
+ * same reason: the one consumer is a `doctor` check, and "not installed" is a state it reports. It
40
+ * is also why {@link pluginRuntimeRoot} falls back to the install root rather than throwing on a
41
+ * record it cannot navigate.
42
+ */
43
+ import { readdirSync, statSync } from 'node:fs';
44
+ import { join } from 'node:path';
45
+ import { isJsonObject, readJsonFile } from '../core/json.js';
46
+ import { MARKETPLACE_NAME, PLUGIN_KEY, PLUGIN_NAME } from '../generators/projectSettings.js';
47
+ import { claudeHome } from './paths.js';
48
+ /** The runner's plugin directory under {@link claudeHome}, and the two records inside it. */
49
+ const PLUGINS_DIRNAME = 'plugins';
50
+ const INSTALLED_PLUGINS_FILENAME = 'installed_plugins.json';
51
+ const KNOWN_MARKETPLACES_FILENAME = 'known_marketplaces.json';
52
+ /** The object keying every installed plugin's rows, and the fields one row is selected on. */
53
+ const PLUGINS_KEY = 'plugins';
54
+ const INSTALL_PATH_FIELD = 'installPath';
55
+ const SCOPE_FIELD = 'scope';
56
+ const PROJECT_PATH_FIELD = 'projectPath';
57
+ /** The marketplace entry's own fields, and the source type whose install location is a live tree. */
58
+ const SOURCE_FIELD = 'source';
59
+ const DIRECTORY_SOURCE = 'directory';
60
+ const INSTALL_LOCATION_FIELD = 'installLocation';
61
+ /** The marketplace manifest inside an install location, and the field one plugin entry is found by. */
62
+ const MARKETPLACE_MANIFEST_DIRNAME = '.claude-plugin';
63
+ const MARKETPLACE_MANIFEST_FILENAME = 'marketplace.json';
64
+ const NAME_FIELD = 'name';
65
+ /**
66
+ * The directory of helper scripts inside an install root — the segment {@link pluginScriptsDir}
67
+ * joins, and the one a reader outside this module recognises a helper path by. Exported so that
68
+ * reader imports it instead of keeping a second copy: a copy of a literal is a copy that can drift,
69
+ * and reading the name resolves no plugin and touches no filesystem.
70
+ */
71
+ export const SCRIPTS_DIRNAME = 'scripts';
72
+ /** The suffix a helper script is named with. */
73
+ const SCRIPT_SUFFIX = '.sh';
74
+ /** The record's absolute path. A pure string: asking where it is never creates anything. */
75
+ export function installedPluginsPath() {
76
+ return join(claudeHome(), PLUGINS_DIRNAME, INSTALLED_PLUGINS_FILENAME);
77
+ }
78
+ /** The marketplace register's absolute path, resolved the same pure way. */
79
+ export function knownMarketplacesPath() {
80
+ return join(claudeHome(), PLUGINS_DIRNAME, KNOWN_MARKETPLACES_FILENAME);
81
+ }
82
+ /** One row's install root, or `undefined` when the row is not one or does not carry a usable path. */
83
+ function installPathOf(row) {
84
+ if (!isJsonObject(row))
85
+ return undefined;
86
+ const value = row[INSTALL_PATH_FIELD];
87
+ return typeof value === 'string' && value !== '' ? value : undefined;
88
+ }
89
+ /** Whether a row was installed at the given scope. */
90
+ function atScope(row, scope) {
91
+ return isJsonObject(row) && row[SCOPE_FIELD] === scope;
92
+ }
93
+ /**
94
+ * The install root of this harness's plugin, or `undefined` when this machine has none.
95
+ *
96
+ * **The selection rule, in order**, because one plugin carries a row per scope it was enabled at and
97
+ * they can name different versions:
98
+ *
99
+ * 1. a `project`-scope row whose `projectPath` is `repoRoot` — the row describing *this* repository,
100
+ * which is the one a run started here resolves the plugin through;
101
+ * 2. failing that, a `user`-scope row — enabled once for the account, so it applies here too;
102
+ * 3. failing that, the first row carrying a usable `installPath`, so a scope this CLI has not been
103
+ * taught yet still yields an answer rather than a false "not installed".
104
+ *
105
+ * `repoRoot` is optional so a caller with no repository can still ask; it then starts at step 2.
106
+ */
107
+ export function pluginInstallRoot(repoRoot) {
108
+ let parsed;
109
+ try {
110
+ parsed = readJsonFile(installedPluginsPath());
111
+ }
112
+ catch {
113
+ return undefined;
114
+ }
115
+ if (!isJsonObject(parsed))
116
+ return undefined;
117
+ const plugins = parsed[PLUGINS_KEY];
118
+ if (!isJsonObject(plugins))
119
+ return undefined;
120
+ const rows = plugins[PLUGIN_KEY];
121
+ if (!Array.isArray(rows))
122
+ return undefined;
123
+ const usable = rows.filter((row) => installPathOf(row) !== undefined);
124
+ const here = repoRoot === undefined
125
+ ? undefined
126
+ : usable.find((row) => atScope(row, 'project') && isJsonObject(row) && row[PROJECT_PATH_FIELD] === repoRoot);
127
+ return installPathOf(here ?? usable.find((row) => atScope(row, 'user')) ?? usable[0]);
128
+ }
129
+ /**
130
+ * The root the **runtime** substitutes for `${CLAUDE_PLUGIN_ROOT}`, or the install root when this
131
+ * machine gives no reason to think they differ.
132
+ *
133
+ * **The one case where they differ is a `directory`-sourced marketplace.** The runner reads such a
134
+ * marketplace's manifest in place — `known_marketplaces.json` gives the entry an `installLocation`
135
+ * equal to the source tree — while still installing the plugin as a version-pinned snapshot under
136
+ * `installPath`. Measured 2026-08-26 on `claude` 2.1.246: a sub-agent's `${CLAUDE_PLUGIN_ROOT}`
137
+ * resolved to `<installLocation>/<the manifest's plugin source>`, not to `installPath`. So the
138
+ * derivation is the runner's own two records, in order: the marketplace entry for the location, and
139
+ * the manifest at that location for this plugin's relative `source`.
140
+ *
141
+ * **Anything else collapses to {@link pluginInstallRoot}** — a git source, an entry this CLI cannot
142
+ * navigate, a manifest that does not name this plugin, a `source` that is not a relative path
143
+ * string. A git-sourced adopter therefore gets one root, which is correct: there the two are the
144
+ * same directory. `repoRoot` is passed through to the fallback and is otherwise unused.
145
+ */
146
+ export function pluginRuntimeRoot(repoRoot) {
147
+ const fallback = () => pluginInstallRoot(repoRoot);
148
+ let parsed;
149
+ try {
150
+ parsed = readJsonFile(knownMarketplacesPath());
151
+ }
152
+ catch {
153
+ return fallback();
154
+ }
155
+ if (!isJsonObject(parsed))
156
+ return fallback();
157
+ const entry = parsed[MARKETPLACE_NAME];
158
+ if (!isJsonObject(entry))
159
+ return fallback();
160
+ const source = entry[SOURCE_FIELD];
161
+ if (!isJsonObject(source) || source[SOURCE_FIELD] !== DIRECTORY_SOURCE)
162
+ return fallback();
163
+ const location = entry[INSTALL_LOCATION_FIELD];
164
+ if (typeof location !== 'string' || location === '')
165
+ return fallback();
166
+ let manifest;
167
+ try {
168
+ manifest = readJsonFile(join(location, MARKETPLACE_MANIFEST_DIRNAME, MARKETPLACE_MANIFEST_FILENAME));
169
+ }
170
+ catch {
171
+ return fallback();
172
+ }
173
+ if (!isJsonObject(manifest))
174
+ return fallback();
175
+ const plugins = manifest[PLUGINS_KEY];
176
+ if (!Array.isArray(plugins))
177
+ return fallback();
178
+ const row = plugins.find((candidate) => isJsonObject(candidate) && candidate[NAME_FIELD] === PLUGIN_NAME);
179
+ const relative = isJsonObject(row) ? row[SOURCE_FIELD] : undefined;
180
+ if (typeof relative !== 'string' || relative === '')
181
+ return fallback();
182
+ return join(location, relative);
183
+ }
184
+ /**
185
+ * The helper scripts shipped inside an install root, by filename, in codepoint order.
186
+ *
187
+ * **Read from the directory rather than listed here**, and that is the point: the names are declared
188
+ * once, in the plugin's own `scripts/README.md`, and a copy in the CLI would be a second declaration
189
+ * that drifts the first time a helper is added or renamed. Sorted so one machine reports them the
190
+ * same way twice, whatever order the filesystem enumerates in.
191
+ *
192
+ * An unreadable or absent directory is an empty list, for the module header's reason: it is the
193
+ * runner's tree, and a caller grades what it found rather than being stopped by what it did not.
194
+ */
195
+ export function pluginHelperScripts(installRoot) {
196
+ const dir = pluginScriptsDir(installRoot);
197
+ let names;
198
+ try {
199
+ names = readdirSync(dir);
200
+ }
201
+ catch {
202
+ return [];
203
+ }
204
+ return names
205
+ .filter((name) => name.endsWith(SCRIPT_SUFFIX))
206
+ .filter((name) => {
207
+ try {
208
+ return statSync(join(dir, name)).isFile();
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ })
214
+ .sort();
215
+ }
216
+ /** `<installRoot>/scripts` — the directory {@link pluginHelperScripts} enumerates. */
217
+ export function pluginScriptsDir(installRoot) {
218
+ return join(installRoot, SCRIPTS_DIRNAME);
219
+ }
220
+ /** `<installRoot>/scripts/<name>` — the path a helper is invoked by, joined here and nowhere else. */
221
+ export function pluginHelperPath(installRoot, name) {
222
+ return join(pluginScriptsDir(installRoot), name);
223
+ }
224
+ //# sourceMappingURL=plugins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../../src/machine/plugins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,YAAY,EAAE,YAAY,EAAkB,MAAM,iBAAiB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC7F,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,6FAA6F;AAC7F,MAAM,eAAe,GAAG,SAAS,CAAC;AAClC,MAAM,0BAA0B,GAAG,wBAAwB,CAAC;AAC5D,MAAM,2BAA2B,GAAG,yBAAyB,CAAC;AAE9D,8FAA8F;AAC9F,MAAM,WAAW,GAAG,SAAS,CAAC;AAC9B,MAAM,kBAAkB,GAAG,aAAa,CAAC;AACzC,MAAM,WAAW,GAAG,OAAO,CAAC;AAC5B,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAEzC,qGAAqG;AACrG,MAAM,YAAY,GAAG,QAAQ,CAAC;AAC9B,MAAM,gBAAgB,GAAG,WAAW,CAAC;AACrC,MAAM,sBAAsB,GAAG,iBAAiB,CAAC;AAEjD,uGAAuG;AACvG,MAAM,4BAA4B,GAAG,gBAAgB,CAAC;AACtD,MAAM,6BAA6B,GAAG,kBAAkB,CAAC;AACzD,MAAM,UAAU,GAAG,MAAM,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,SAAS,CAAC;AAEzC,gDAAgD;AAChD,MAAM,aAAa,GAAG,KAAK,CAAC;AAE5B,4FAA4F;AAC5F,MAAM,UAAU,oBAAoB;IAClC,OAAO,IAAI,CAAC,UAAU,EAAE,EAAE,eAAe,EAAE,0BAA0B,CAAC,CAAC;AACzE,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB;IACnC,OAAO,IAAI,CAAC,UAAU,EAAE,EAAE,eAAe,EAAE,2BAA2B,CAAC,CAAC;AAC1E,CAAC;AAED,sGAAsG;AACtG,SAAS,aAAa,CAAC,GAA0B;IAC/C,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,KAAK,GAAG,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvE,CAAC;AAED,sDAAsD;AACtD,SAAS,OAAO,CAAC,GAAc,EAAE,KAAa;IAC5C,OAAO,YAAY,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,KAAK,KAAK,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAiB;IACjD,IAAI,MAA6B,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAE5C,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IACpC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IAE7C,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACjC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAE3C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,CAAC;IACtE,MAAM,IAAI,GACR,QAAQ,KAAK,SAAS;QACpB,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,kBAAkB,CAAC,KAAK,QAAQ,CAAC,CAAC;IACjH,OAAO,aAAa,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACxF,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAiB;IACjD,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAEnD,IAAI,MAA6B,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,qBAAqB,EAAE,CAAC,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,EAAE,CAAC;IACpB,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;QAAE,OAAO,QAAQ,EAAE,CAAC;IAE7C,MAAM,KAAK,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACvC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,EAAE,CAAC;IAE5C,MAAM,MAAM,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC;IACnC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC,KAAK,gBAAgB;QAAE,OAAO,QAAQ,EAAE,CAAC;IAE1F,MAAM,QAAQ,GAAG,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC/C,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,EAAE;QAAE,OAAO,QAAQ,EAAE,CAAC;IAEvE,IAAI,QAA+B,CAAC;IACpC,IAAI,CAAC;QACH,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,4BAA4B,EAAE,6BAA6B,CAAC,CAAC,CAAC;IACvG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,EAAE,CAAC;IACpB,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,EAAE,CAAC;IAE/C,MAAM,OAAO,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;IACtC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,QAAQ,EAAE,CAAC;IAE/C,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC,KAAK,WAAW,CAAC,CAAC;IAC1G,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACnE,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,EAAE;QAAE,OAAO,QAAQ,EAAE,CAAC;IAEvE,OAAO,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAmB;IACrD,MAAM,GAAG,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAC1C,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;SAC9C,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QACf,IAAI,CAAC;YACH,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC;SACD,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,gBAAgB,CAAC,WAAmB;IAClD,OAAO,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;AAC5C,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,IAAY;IAChE,OAAO,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,CAAC;AACnD,CAAC"}