autonomous-sdlc-harness 0.2.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/docs.js +2 -1
  4. package/dist/commands/docs.js.map +1 -1
  5. package/dist/commands/doctor.js +56 -6
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/init.js +107 -20
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/config/check.js +11 -4
  10. package/dist/config/check.js.map +1 -1
  11. package/dist/config/model.js +25 -0
  12. package/dist/config/model.js.map +1 -1
  13. package/dist/core/defaultBranchPush.js +26 -0
  14. package/dist/core/defaultBranchPush.js.map +1 -0
  15. package/dist/core/git.js +61 -0
  16. package/dist/core/git.js.map +1 -1
  17. package/dist/core/paths.js +22 -2
  18. package/dist/core/paths.js.map +1 -1
  19. package/dist/core/prompt.js +6 -2
  20. package/dist/core/prompt.js.map +1 -1
  21. package/dist/core/writer.js +2 -0
  22. package/dist/core/writer.js.map +1 -1
  23. package/dist/doctor/checks.js +482 -73
  24. package/dist/doctor/checks.js.map +1 -1
  25. package/dist/generators/githubWorkflows.js +66 -0
  26. package/dist/generators/githubWorkflows.js.map +1 -0
  27. package/dist/generators/notifications.js +105 -19
  28. package/dist/generators/notifications.js.map +1 -1
  29. package/dist/generators/outerLoopScripts.js +17 -3
  30. package/dist/generators/outerLoopScripts.js.map +1 -1
  31. package/dist/generators/permissionProfile.js +141 -21
  32. package/dist/generators/permissionProfile.js.map +1 -1
  33. package/dist/generators/projectSettings.js +2 -1
  34. package/dist/generators/projectSettings.js.map +1 -1
  35. package/dist/generators/repoRoot.js +23 -4
  36. package/dist/generators/repoRoot.js.map +1 -1
  37. package/dist/generators/stateDir.js +9 -3
  38. package/dist/generators/stateDir.js.map +1 -1
  39. package/dist/machine/paths.js +4 -1
  40. package/dist/machine/paths.js.map +1 -1
  41. package/dist/remote/githubActions.js +86 -0
  42. package/dist/remote/githubActions.js.map +1 -0
  43. package/dist/retrieval/queryLog.js +6 -4
  44. package/dist/retrieval/queryLog.js.map +1 -1
  45. package/dist/retrieval/runtime.js +46 -33
  46. package/dist/retrieval/runtime.js.map +1 -1
  47. package/dist/retrieval/search.js +21 -10
  48. package/dist/retrieval/search.js.map +1 -1
  49. package/dist/retrieval/server.js +1 -1
  50. package/dist/retrieval/setup.js +2 -1
  51. package/dist/retrieval/setup.js.map +1 -1
  52. package/package.json +2 -2
  53. package/templates/README.md +3 -2
  54. package/templates/claude/context/conventions.md +1 -1
  55. package/templates/claude/context/layer.md +1 -1
  56. package/templates/claude/push-notify.env.example +7 -2
  57. package/templates/claude/settings.autonomous.json +1 -1
  58. package/templates/github/workflows/harness-resume.yml +124 -0
  59. package/templates/github/workflows/harness-run.yml +446 -0
  60. package/templates/repo/gitignore +11 -1
  61. package/templates/scripts/README.md +1 -1
  62. package/templates/scripts/autonomous-watcher.sh +1296 -169
  63. package/templates/scripts/flow-walker.sh +629 -0
  64. package/templates/scripts/flows/task_plan_writing.graph.json +192 -0
  65. package/templates/scripts/lib/flow-walker-gates.sh +165 -0
  66. package/templates/scripts/lib/harness-run-lib.sh +595 -18
  67. package/templates/scripts/remote-run.sh +1784 -0
  68. package/templates/scripts/restart-watcher.sh +21 -1
  69. package/templates/scripts/run-test-suite.sh +182 -0
  70. package/templates/scripts/scratch-run.sh +2 -1
  71. package/templates/state-dir/README-root.md +1 -1
  72. package/templates/state-dir/autonomous_logs/README.md +2 -2
  73. package/templates/state-dir/improvement_observations/README.md +2 -0
  74. package/templates/state-dir/scratch/README.md +1 -1
  75. package/templates/state-dir/test_fix_plan_reviews/README.md +9 -0
  76. package/templates/state-dir/test_fix_plans/README.md +9 -0
  77. package/templates/state-dir/test_fix_point_reviews/README.md +9 -0
  78. package/templates/state-dir/test_run_logs/README.md +11 -0
@@ -12,10 +12,10 @@
12
12
  * declares exactly {@link PUSH_URL_KEY} and {@link PUSH_CMD_KEY}. The credential-bearing value is
13
13
  * therefore the push **URL**, and its home is the machine-local `push.env` that `docs/watcher.md`
14
14
  * §6 already names as the first file the notifier reads. "Guided ntfy setup" ships accordingly as a
15
- * vendor-neutral endpoint prompt whose guided line uses `https://ntfy.sh/<your-topic>` as the
16
- * worked example rather than as the only thing that works.
15
+ * vendor-neutral destination: an adopter may give an ntfy topic name, which is written as its
16
+ * `https://ntfy.sh/<topic>` address, or the full URL of any endpoint that accepts a POST.
17
17
  *
18
- * ## Three non-obvious choices, and where each comes from
18
+ * ## Four non-obvious choices, and where each comes from
19
19
  *
20
20
  * 1. **An opt-in with no endpoint writes nothing — and that is the whole point of the file.**
21
21
  * `docs/watcher.md` §6's precedence is that the machine-local file wins and the repository's
@@ -24,17 +24,32 @@
24
24
  * file that already has values, turning an opt-in into a silent opt-out. When the answer to
25
25
  * "where should notifications be posted" is nothing, this generator enqueues nothing and returns
26
26
  * the guided setup as a note instead.
27
- * 2. **The value is never logged back.** The run's action log names the path the write engine
28
- * wrote, the note names the two keys, and neither says what the URL is — the same discipline
27
+ * 2. **The value is never logged back — a topic name no more than a URL.** The run's action log
28
+ * names the path the write engine wrote, the note names the two keys and which form was
29
+ * recognised, and none of them says what the URL or the topic is — the same discipline
29
30
  * `autonomous-notify.sh`'s header states for the delivery side ("NO VALUE IS EVER PRINTED, by
30
31
  * this script or in a failure line — only paths and key names"). A push URL is a bearer
31
- * credential for every endpoint worth pointing this at, and a terminal scrollback is not where
32
- * it belongs.
32
+ * credential for every endpoint worth pointing this at, and an ntfy topic is one too: the public
33
+ * server has no account behind a topic, so anyone who knows its name can read and post to it. An
34
+ * answer that is neither form is not repeated either, because it is most likely a mistyped one.
35
+ * A terminal scrollback is not where any of them belongs.
33
36
  * 3. **The precedence has exactly one definition on the TypeScript side**, {@link pushEnvCandidates},
34
37
  * so a `doctor` check reporting which file is in effect reads that order rather than re-joining
35
38
  * either path. Two derivations of a first-wins order is two answers to the one question anybody
36
39
  * asks about these files, and the answers would differ exactly where it matters — on a machine
37
40
  * that has both.
41
+ * 4. **One grammar for the destination, and a file that survives being sourced.**
42
+ * {@link resolvePushDestination} is the only definition of what may be given, and
43
+ * {@link PUSH_DESTINATION_FORMS} the only sentence describing it, for the reason choice 3 gives
44
+ * for the precedence; {@link UNRECOGNISED_DESTINATION_NOTE}, the line `init`'s re-ask prints,
45
+ * is produced here too. The file is a shell fragment `autonomous-notify.sh` sources under
46
+ * `set -a`, so {@link pushEnvContent} single-quotes a URL outside a conservative character set:
47
+ * an unquoted `&` or `;` in a query string would break the file or run a command. The grammar
48
+ * refuses `'`, so single quotes always suffice.
49
+ *
50
+ * **Finding: `autonomous-notify.sh` needs no change for the topic form.** The stored value is
51
+ * always a full URL, which it POSTs verbatim; a topic is resolved to its address here, before the
52
+ * file is written.
38
53
  *
39
54
  * Nothing here touches the filesystem: the generator plans, and `init` applies the plan once — so
40
55
  * `--dry-run` writes no machine-local file for the same structural reason it writes nothing inside
@@ -52,13 +67,68 @@ export const PUSH_CMD_KEY = 'HARNESS_PUSH_CMD';
52
67
  /** Mode of the written file: it holds a credential, so it is the owner's to read and nobody else's. */
53
68
  const PUSH_ENV_MODE = 0o600;
54
69
  /**
55
- * The worked example in the guided setup — an endpoint that accepts a POST and needs no account.
70
+ * A value bash reads back unchanged when written unquoted in an assignment. `~` is deliberately
71
+ * outside it: bash tilde-expands it after a `:` in an assignment.
72
+ */
73
+ const SHELL_SAFE_VALUE = /^[A-Za-z0-9._:/?#@%+=,-]+$/;
74
+ /** The public ntfy server a bare topic name is posted to. */
75
+ export const NTFY_PUBLIC_ORIGIN = 'https://ntfy.sh';
76
+ /** The ntfy server's own topic-name rule; a name outside it is one the server would refuse. */
77
+ const NTFY_TOPIC_PATTERN = /^[-_A-Za-z0-9]{1,64}$/;
78
+ /** The one placeholder every flag row, note and advice line prints for the destination. */
79
+ export const PUSH_DESTINATION_PLACEHOLDER = '<url-or-ntfy-topic>';
80
+ /**
81
+ * What an ntfy topic becomes once written. Module-private: it reaches callers only inside
82
+ * {@link PUSH_DESTINATION_FORMS}, which is the surface every caller quotes.
83
+ */
84
+ const GUIDED_ENDPOINT_EXAMPLE = `${NTFY_PUBLIC_ORIGIN}/<your-topic>`;
85
+ /**
86
+ * The one description of what may be given as a push destination: a noun phrase, without a final
87
+ * stop, so a caller can write "Type {@link PUSH_DESTINATION_FORMS}." or "takes …; the value given is
88
+ * neither". Every surface asking for or refusing a destination interpolates it rather than
89
+ * re-wording it.
90
+ */
91
+ export const PUSH_DESTINATION_FORMS = `either an ntfy topic name — install the free ntfy app from the App Store or Play Store, create a topic there and type its name, with no server and no account needed (it is posted to as ${GUIDED_ENDPOINT_EXAMPLE}); the name works like a password, because anyone who knows it can read and send these notifications, so choose one nobody would guess — or the full http:// or https:// URL of any other endpoint that accepts a POST`;
92
+ const NOT_REPEATED_CLAUSE = 'it is not repeated here, because a push destination is a credential';
93
+ /**
94
+ * The line `init`'s re-ask prints after an unrecognised answer. It names no form, because the
95
+ * question it follows interpolates {@link PUSH_DESTINATION_FORMS}, and never quotes the answer.
96
+ */
97
+ export const UNRECOGNISED_DESTINATION_NOTE = `That answer is not one of the forms asked for, so it was not used; ${NOT_REPEATED_CLAUSE}.`;
98
+ const HTTP_SCHEME = /^https?:\/\//i;
99
+ // `'` is refused so the single-quoting in `pushEnvContent` can never be broken out of.
100
+ const UNWRITABLE_IN_URL = /[\s\u0000-\u001f\u007f']/;
101
+ const NTFY_HOSTED_TOPIC = /^ntfy\.sh\/([^/]*)\/?$/i;
102
+ /**
103
+ * Resolve a push destination as given — a full `http://` / `https://` URL, `ntfy.sh/<topic>`, or a
104
+ * bare ntfy topic name — to the URL the notifier posts to.
56
105
  *
57
- * Exported so `init`'s prompt and this generator's guided note quote **one** example: two would be
58
- * two answers to "what does a URL that works look like", and the prompt is where the question is
59
- * first asked while the note is where an unanswered one is explained.
106
+ * **The rule this function exists to enforce: it is the only grammar for this value, and every
107
+ * entry point calls it** — the `--push-url` flag, the terminal prompt, {@link writeNotifications},
108
+ * and any later one such as `ROADMAP.md`'s unshipped `notifications set-url`. A second copy of the
109
+ * grammar is two answers to "what may I type here".
110
+ *
111
+ * A URL is kept as typed (trimmed), not normalised through `URL.href`, so what is written is what
112
+ * the adopter gave.
60
113
  */
61
- export const GUIDED_ENDPOINT_EXAMPLE = 'https://ntfy.sh/<your-topic>';
114
+ export function resolvePushDestination(answer) {
115
+ const trimmed = answer.trim();
116
+ if (HTTP_SCHEME.test(trimmed) && !UNWRITABLE_IN_URL.test(trimmed)) {
117
+ let hostname = '';
118
+ try {
119
+ hostname = new URL(trimmed).hostname;
120
+ }
121
+ catch {
122
+ hostname = '';
123
+ }
124
+ if (hostname !== '')
125
+ return { kind: 'url', url: trimmed };
126
+ }
127
+ const topic = NTFY_HOSTED_TOPIC.exec(trimmed)?.[1] ?? trimmed;
128
+ if (NTFY_TOPIC_PATTERN.test(topic))
129
+ return { kind: 'ntfy-topic', url: `${NTFY_PUBLIC_ORIGIN}/${topic}` };
130
+ return { kind: 'unrecognised' };
131
+ }
62
132
  /** The machine-local push-notification settings file — the first file the notifier reads. */
63
133
  export function machinePushEnvPath() {
64
134
  return join(machineConfigDir(), PUSH_ENV_FILENAME);
@@ -111,8 +181,12 @@ function repositoryPushEnvPath(repoRoot, config) {
111
181
  * copy is. Nor does it restate what either key does: that is `autonomous-notify.sh`'s header, which
112
182
  * is the one implementation of the delivery arms, and a second account of it here would be the copy
113
183
  * that goes stale.
184
+ *
185
+ * The value is written unquoted only inside {@link SHELL_SAFE_VALUE}, and single-quoted otherwise
186
+ * (module header, choice 4).
114
187
  */
115
188
  function pushEnvContent(url) {
189
+ const value = SHELL_SAFE_VALUE.test(url) ? url : `'${url}'`;
116
190
  return [
117
191
  '# Push notifications for unattended runs — machine-local, written by',
118
192
  '# `npx autonomous-sdlc-harness init --notifications`.',
@@ -126,8 +200,12 @@ function pushEnvContent(url) {
126
200
  '# It holds a credential: it is created 0600 inside a 0700 directory, it is outside every',
127
201
  '# repository so it cannot be committed by accident, and no command in this CLI prints a value',
128
202
  '# from it back. Edit it freely — `init` creates it once and a later run leaves it as you left it.',
203
+ '#',
204
+ `# ${PUSH_URL_KEY} always holds a full URL: an ntfy topic is written as ${NTFY_PUBLIC_ORIGIN}/<topic>,`,
205
+ '# and the topic name in it is itself the credential — anyone who knows it can read and post to it.',
206
+ "# The file is sourced by a shell, so a URL containing `&`, `;` or a space must stay single-quoted.",
129
207
  '',
130
- `${PUSH_URL_KEY}=${url}`,
208
+ `${PUSH_URL_KEY}=${value}`,
131
209
  `${PUSH_CMD_KEY}=`,
132
210
  '',
133
211
  ].join('\n');
@@ -135,14 +213,16 @@ function pushEnvContent(url) {
135
213
  /**
136
214
  * Enqueue the machine-local settings file, or explain why nothing was written.
137
215
  *
138
- * Three outcomes, and the middle one is the one to read first:
216
+ * Four outcomes, and the second is the one to read first:
139
217
  *
140
218
  * - **opt-in not taken** — nothing is enqueued and a note records that delivery stays opt-in and
141
219
  * defaults to nothing pushed, which is what every release before this one did;
142
220
  * - **opt-in taken with no endpoint** — nothing is enqueued, and the note carries the guided setup:
143
221
  * the path, the two key names and a worked example. Writing an empty file here would shadow a
144
222
  * repository-side file that already has values (choice 1 in the module header);
145
- * - **opt-in taken with an endpoint** — the directory at `0700` and the file at `0600`, both
223
+ * - **opt-in taken with a destination {@link resolvePushDestination} does not recognise** — nothing
224
+ * is enqueued, for the same reason, and one warning says so without repeating the value;
225
+ * - **opt-in taken with a recognised destination** — its resolved URL is written; the directory at `0700` and the file at `0600`, both
146
226
  * `allowOutsideRepo`, the file `create-if-absent` so a re-run leaves an edited one alone.
147
227
  */
148
228
  export function writeNotifications({ repoRoot, config, plan, enabled, pushUrl, }) {
@@ -163,13 +243,19 @@ export function writeNotifications({ repoRoot, config, plan, enabled, pushUrl, }
163
243
  if (pushUrl !== undefined) {
164
244
  warnings.push('--push-url was given without --notifications, so it was not written anywhere: the endpoint is only ever written into the machine-local push-notification settings file, and that file is written only when the opt-in is taken — re-run with --notifications to take it and set the endpoint in the same run');
165
245
  }
166
- notes.push(`no push-notification settings were written and none were changed: delivery is opt-in and defaults to nothing pushed, so an unattended run's completed, parked and failed events reach a desktop banner where one is available and nothing else. Re-run with --notifications --push-url <url> to write ${target}, or ${repositoryRemedy} — the machine-local file is read first and the repository one only when it is absent (docs/watcher.md §6)`);
246
+ notes.push(`no push-notification settings were written and none were changed: delivery is opt-in and defaults to nothing pushed, so an unattended run's completed, parked and failed events reach a desktop banner where one is available and nothing else. Re-run with --notifications --push-url ${PUSH_DESTINATION_PLACEHOLDER}, giving ${PUSH_DESTINATION_FORMS}, to write ${target}, or ${repositoryRemedy} — the machine-local file is read first and the repository one only when it is absent (docs/watcher.md §6)`);
167
247
  return { written: false, notes, warnings };
168
248
  }
169
249
  if (pushUrl === undefined) {
170
- notes.push(`push notifications were asked for and no endpoint was given, so nothing was written — deliberately: the machine-local file is read before ${repositoryName} and while it exists that one is not read at all, so an empty ${target} would shadow values you may already have there and turn this opt-in into a silent opt-out. Re-run with --push-url <url>, naming any endpoint that accepts a POST (${GUIDED_ENDPOINT_EXAMPLE} is the worked example: choose a topic name nobody would guess, subscribe to it in an ntfy client, and that URL is the endpoint), or write ${target} yourself with ${PUSH_URL_KEY} set and ${PUSH_CMD_KEY} empty`);
250
+ notes.push(`push notifications were asked for and no destination was given, so nothing was written — deliberately: the machine-local file is read before ${repositoryName} and while it exists that one is not read at all, so an empty ${target} would shadow values you may already have there and turn this opt-in into a silent opt-out. Re-run with --push-url ${PUSH_DESTINATION_PLACEHOLDER}, giving ${PUSH_DESTINATION_FORMS}; or write ${target} yourself with ${PUSH_URL_KEY} set to a full URL and ${PUSH_CMD_KEY} empty`);
251
+ return { written: false, notes, warnings };
252
+ }
253
+ const destination = resolvePushDestination(pushUrl);
254
+ if (destination.kind === 'unrecognised') {
255
+ warnings.push(`the push destination given is not ${PUSH_DESTINATION_FORMS}, so nothing was written; ${NOT_REPEATED_CLAUSE}. Re-run with --notifications --push-url ${PUSH_DESTINATION_PLACEHOLDER}, or write ${target} yourself with ${PUSH_URL_KEY} set to a full URL and ${PUSH_CMD_KEY} empty`);
171
256
  return { written: false, notes, warnings };
172
257
  }
258
+ const recognised = destination.kind === 'ntfy-topic' ? `the ntfy topic you gave, as its ${NTFY_PUBLIC_ORIGIN} address` : 'the URL you gave';
173
259
  plan.add({
174
260
  path: machineConfigDir(),
175
261
  policy: 'ensure-dir',
@@ -180,12 +266,12 @@ export function writeNotifications({ repoRoot, config, plan, enabled, pushUrl, }
180
266
  plan.add({
181
267
  path: target,
182
268
  policy: 'create-if-absent',
183
- content: pushEnvContent(pushUrl),
269
+ content: pushEnvContent(destination.url),
184
270
  label: 'machine-local push-notification settings',
185
271
  mode: PUSH_ENV_MODE,
186
272
  allowOutsideRepo: true,
187
273
  });
188
- notes.push(`${target} is the machine-local push-notification settings file, written to hold ${PUSH_URL_KEY} and an empty ${PUSH_CMD_KEY} — the endpoint itself is deliberately not printed here or anywhere else. It is machine-local rather than committed, and it is read before ${repositoryName}, which is then not read at all. It is create-if-absent: a file already there is kept exactly as you edited it and still carries the endpoint it was written with, so --push-url on a re-run sets nothing — to point it elsewhere, edit ${PUSH_URL_KEY} in it by hand, or re-run with --force, which copies it to a .bak sibling and re-writes it from this run's --push-url`);
274
+ notes.push(`${target} is the machine-local push-notification settings file, written to hold ${PUSH_URL_KEY}, set to ${recognised}, and an empty ${PUSH_CMD_KEY} — the endpoint itself is deliberately not printed here or anywhere else. It is machine-local rather than committed, and it is read before ${repositoryName}, which is then not read at all. It is create-if-absent: a file already there is kept exactly as you edited it and still carries the endpoint it was written with, so --push-url on a re-run sets nothing — to point it elsewhere, edit ${PUSH_URL_KEY} in it by hand, or re-run with --force, which copies it to a .bak sibling and re-writes it from this run's --push-url`);
189
275
  return { written: true, notes, warnings };
190
276
  }
191
277
  //# sourceMappingURL=notifications.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../src/generators/notifications.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../src/generators/notifications.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Generator: the outer-loop scripts — the run watcher, the git wrappers, the worktree tooling and
3
- * the shared library they all source — written into the adopter's configured `scriptsDir`.
2
+ * Generator: the outer-loop scripts — the run watcher, the git wrappers, the worktree tooling, the
3
+ * flow walker with its gate library and flow graph, the test-suite runner, and the shared library
4
+ * they all source —
5
+ * written into the adopter's configured `scriptsDir`.
4
6
  *
5
7
  * **The rule this module exists to enforce: {@link OUTER_LOOP_SCRIPTS} is the single declaration of
6
8
  * which outer-loop files ship, where each one lands, what mode it carries and which of them an
@@ -68,7 +70,8 @@ export const NOTIFY_SCRIPT_NAME = 'autonomous-notify.sh';
68
70
  export const DOCS_SEARCH_SERVER_SCRIPT_NAME = 'docs-search-server.sh';
69
71
  /**
70
72
  * The whole outer-loop set, in the order `init` writes it: the shared library first, because every
71
- * other row sources it.
73
+ * other row sources it, and each further library or data file before the script that sources or
74
+ * reads it.
72
75
  *
73
76
  * **Declared once, here.** One row is one line, and adding a script is adding its template plus its
74
77
  * row — no condition threaded through the writer, no second list to keep in step.
@@ -85,6 +88,14 @@ export const OUTER_LOOP_SCRIPTS = Object.freeze([
85
88
  // regenerated, which is the whole point of shipping it as a script rather than as an interpreter
86
89
  // allow entry.
87
90
  Object.freeze({ file: 'scratch-run.sh', mode: 0o755, agentInvocable: true }),
91
+ // Sourced and read by `flow-walker.sh` from its own directory, never run.
92
+ Object.freeze({ file: 'flow-walker-gates.sh', subdir: 'lib', mode: 0o644, agentInvocable: false }),
93
+ Object.freeze({ file: 'task_plan_writing.graph.json', subdir: 'flows', mode: 0o644, agentInvocable: false }),
94
+ // Run by the orchestrating session; no `DENY_SCRIPT_BASENAMES` entry, because it must be reachable.
95
+ Object.freeze({ file: 'flow-walker.sh', mode: 0o755, agentInvocable: true }),
96
+ // Run by the orchestrating session in the Run gates phase; no `DENY_SCRIPT_BASENAMES` entry,
97
+ // because it must be reachable.
98
+ Object.freeze({ file: 'run-test-suite.sh', mode: 0o755, agentInvocable: true }),
88
99
  Object.freeze({ file: 'create-worktree.sh', mode: 0o755, agentInvocable: false }),
89
100
  Object.freeze({ file: 'setup-worktree.sh', mode: 0o755, agentInvocable: false }),
90
101
  // Started by the agent runner from `.mcp.json`, never by a dispatched agent's Bash call, so `false`
@@ -96,6 +107,9 @@ export const OUTER_LOOP_SCRIPTS = Object.freeze([
96
107
  Object.freeze({ file: NOTIFY_SCRIPT_NAME, mode: 0o755, agentInvocable: false }),
97
108
  Object.freeze({ file: WATCHER_SCRIPT_NAME, mode: 0o755, agentInvocable: false }),
98
109
  Object.freeze({ file: 'restart-watcher.sh', mode: 0o755, agentInvocable: false }),
110
+ // Run by the watcher, the remote job or a person. Carries a `DENY_SCRIPT_BASENAMES` entry: a run that
111
+ // could dispatch runs could start runs about itself.
112
+ Object.freeze({ file: 'remote-run.sh', mode: 0o755, agentInvocable: false }),
99
113
  ]);
100
114
  /** The templates' subdirectory under `cli/templates/`, addressed as {@link readTemplate} wants it. */
101
115
  const TEMPLATE_DIR = 'scripts';
@@ -1 +1 @@
1
- {"version":3,"file":"outerLoopScripts.js","sourceRoot":"","sources":["../../src/generators/outerLoopScripts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"outerLoopScripts.js","sourceRoot":"","sources":["../../src/generators/outerLoopScripts.ts"],"names":[],"mappings":"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"}
@@ -92,7 +92,8 @@
92
92
  * ## What this module deliberately does not do
93
93
  *
94
94
  * - **It touches no filesystem beyond reading its two templates — and, on a forced run, the profile
95
- * it is about to replace.** {@link writePermissionProfile} enqueues the rendered profile into the
95
+ * it is about to replace; under `init --plugin-root-entries`, the agent runner's plugin records.**
96
+ * {@link writePermissionProfile} enqueues the rendered profile into the
96
97
  * command's write plan under the `create-if-absent` contract — the profile is hand-tuned after
97
98
  * generation, and an overwrite silently drops the allow entries an adopter added to close a stall —
98
99
  * and the write itself belongs to the write engine. `--force` is the one flag that suspends that
@@ -103,7 +104,16 @@
103
104
  * outside this checkout, unverified: a run that graded nothing revokes nothing. That is a read and
104
105
  * not a question put to the adopter again: they
105
106
  * were told once, by `doctor`, exactly which lines to paste. Preserving an entry is not generating
106
- * one — the open owner decision below is untouched by it.
107
+ * one: by default nothing here generates one, and the once-open owner decision below is taken for
108
+ * the opt-in `init --plugin-root-entries` only, the default unchanged. Under that switch
109
+ * {@link generatedPluginRootEntries} reads the plugin records through {@link resolvedPluginRoots}
110
+ * on every run, forced or not, and yields both halves of the grant: the `allow` entries, and each
111
+ * root in `permissions.additionalDirectories`, without which a shell read under the root is refused
112
+ * as outside the allowed working directories. It still never tests whether the profile exists: whether the render
113
+ * lands is the write engine's `create-if-absent` answer, which `init` hands back to
114
+ * {@link pluginRootEntriesNote}. On a forced run the generated lines are appended before the
115
+ * carry-forward, which skips any line already in `allow`, so each line has one producer — the rule
116
+ * the browser-fragment section states.
107
117
  * - **It adds no `hooks` key.** The plugin ships its own guard hooks, which fire from the plugin,
108
118
  * resolve their own root and append to the adopter's hooks rather than replacing them; writing
109
119
  * them into user settings as well would run each guard twice. The template says so in a comment
@@ -126,9 +136,11 @@
126
136
  * `plugin-permissions` check re-derives the root on every run and prints each missing line — and
127
137
  * {@link QA_TEMPLATE_PATH}'s `_README` carries the entry *form* an adopter adds by hand, which the
128
138
  * `create-if-absent` contract above — and, on the run that suspends it, the carry-forward named
129
- * there — is what makes survive every later `init`. Whether `init`
130
- * should also *write* those entries is an open owner decision rather than a closed one; nothing
131
- * here takes it. Moving the helpers somewhere a rule can name unconditionally — the adopter's own
139
+ * there — is what makes survive every later `init`. Whether `init` should also *write* those
140
+ * entries was an open owner decision, now taken for `--plugin-root-entries` only, default unchanged:
141
+ * inside a remote job the plugin is installed immediately before `init` runs and the profile dies
142
+ * with the job, so neither reason above holds there, while both still hold for the default.
143
+ * Moving the helpers somewhere a rule can name unconditionally — the adopter's own
132
144
  * `scriptsDir`, which the three wrapper forms already reach — would remove the manual step but
133
145
  * change the invocation form everywhere the shipped instruction corpus calls them, so it belongs
134
146
  * to the item that ships them (`plugin/scripts/README.md`) and not to this module.
@@ -183,7 +195,7 @@ import { isJsonObject, readJsonFile } from '../core/json.js';
183
195
  import { defaultProjectName, readTemplate, workRoot as resolveWorkRoot, worktreeGlob } from '../core/paths.js';
184
196
  import { normalizeRepoDir } from '../core/repoPaths.js';
185
197
  import { containsToken, renderTemplate } from '../core/templating.js';
186
- import { installedPluginsPath, pluginInstallRoot, pluginRuntimeRoot, SCRIPTS_DIRNAME } from '../machine/plugins.js';
198
+ import { installedPluginsPath, pluginHelperPath, pluginHelperScripts, pluginInstallRoot, pluginRuntimeRoot, SCRIPTS_DIRNAME, } from '../machine/plugins.js';
187
199
  import { SEARCH_TOOL_PERMISSION } from '../retrieval/server.js';
188
200
  import { OUTER_LOOP_SCRIPTS, outerLoopRelativePath, } from './outerLoopScripts.js';
189
201
  import { scriptInvocation, selectWrapper, WRAPPER_SCRIPTS, wrapperInvocation, } from './scripts.js';
@@ -565,16 +577,16 @@ function entriesOf(permissions, list) {
565
577
  *
566
578
  * It is also one of the two forms a **plugin-root** entry is written in: `doctor`'s
567
579
  * `plugin-permissions` check prints those lines for an operator to paste, and
568
- * {@link pluginRootEntryTarget} reads one back. Nothing in this generator emits one for a plugin
569
- * root — the module header's standing decision.
580
+ * {@link pluginRootEntryTarget} reads one back. This generator emits one for a plugin root only
581
+ * under `init --plugin-root-entries`, through {@link pluginRootEntries} — the module header's decision.
570
582
  */
571
583
  export function readRule(absolutePath) {
572
584
  return `Read(/${absolutePath}/**)`;
573
585
  }
574
586
  /**
575
587
  * A wrapper-script rule, in the unquoted `Bash(bash <path>:*)` form every generated one uses — and
576
- * the other plugin-root form, for a helper under the plugin's own scripts directory. Same two
577
- * consumers, and the same standing decision, as {@link readRule}.
588
+ * the other plugin-root form, for a helper under the plugin's own scripts directory. Same consumers,
589
+ * and the same decision, as {@link readRule}.
578
590
  */
579
591
  export function bashScriptRule(absolutePath) {
580
592
  return `Bash(${INVOCATION_PREFIX}${absolutePath}:*)`;
@@ -881,16 +893,27 @@ function appendReadme(profile, line) {
881
893
  }
882
894
  readme.push(line);
883
895
  }
896
+ /** Append `values` to one `permissions` list after the template's own, adding none twice; returns how many were added. */
897
+ function appendToPermissionList(profile, list, values) {
898
+ const target = permissionsOf(profile)[list];
899
+ if (!Array.isArray(target))
900
+ throw internal(`the permission-profile template has no \`permissions.${list}\` list to add to`);
901
+ let added = 0;
902
+ for (const value of values) {
903
+ if (target.includes(value))
904
+ continue;
905
+ target.push(value);
906
+ added += 1;
907
+ }
908
+ return added;
909
+ }
884
910
  /** Append generated `allow` entries after the template's own, adding none of them twice. */
885
911
  function appendAllow(profile, entries) {
886
- const permissions = permissionsOf(profile);
887
- const allow = permissions['allow'];
888
- if (!Array.isArray(allow))
889
- throw internal('the permission-profile template has no `permissions.allow` list to add to');
890
- for (const entry of entries) {
891
- if (!allow.includes(entry))
892
- allow.push(entry);
893
- }
912
+ return appendToPermissionList(profile, 'allow', entries);
913
+ }
914
+ /** Append directories after the template's own `additionalDirectories`, adding none twice. */
915
+ function appendAdditionalDirectories(profile, directories) {
916
+ return appendToPermissionList(profile, 'additionalDirectories', directories);
894
917
  }
895
918
  /**
896
919
  * One `allow` entry per `parity.toolchainCommands` value, each an exact literal under the directory
@@ -942,6 +965,78 @@ function resolvedPluginRoots(repoRoot) {
942
965
  const roots = [pluginRuntimeRoot(repoRoot), pluginInstallRoot(repoRoot)];
943
966
  return [...new Set(roots.filter((root) => root !== undefined).map(normalizedRoot))];
944
967
  }
968
+ /**
969
+ * The helper-script names graded at every root: the sorted, de-duplicated union of
970
+ * {@link pluginHelperScripts} over `roots` while `phases.qa` is on, and empty while it is off — the
971
+ * helpers are that phase's alone. Called by `doctor`'s `plugin-permissions` check and by
972
+ * {@link generatedPluginRootEntries}, so the two cannot grade different names.
973
+ */
974
+ export function pluginRootHelpers(roots, qaOn) {
975
+ return qaOn ? [...new Set(roots.flatMap((root) => pluginHelperScripts(root)))].sort() : [];
976
+ }
977
+ /**
978
+ * The entries one root requires: {@link readRule} iff it is the runtime root
979
+ * ({@link pluginRuntimeRoot}), including where that is also the install root, then
980
+ * {@link bashScriptRule} for each of `helpers`, in that order. Only an install root distinct from the
981
+ * runtime root is exempt from the read: it is the one shape measured to read ungranted (2026-08-26,
982
+ * a `directory`-sourced marketplace), while a single root that was both was refused every `Read` on
983
+ * a GitHub-sourced one (2026-09-28) — both cited in `doctor/checks.ts`, `PLUGIN_PERMISSIONS_CHECK`.
984
+ * The one per-root builder, called by that check and by {@link generatedPluginRootEntries}.
985
+ */
986
+ export function pluginRootEntries(root, options) {
987
+ return [
988
+ ...(options.isRuntimeRoot ? [{ kind: 'read', rule: readRule(root) }] : []),
989
+ ...options.helpers.map((name) => ({ kind: 'helper', rule: bashScriptRule(pluginHelperPath(root, name)) })),
990
+ ];
991
+ }
992
+ /**
993
+ * The directories a plugin-root grant adds to `permissions.additionalDirectories`: every root given,
994
+ * normalized with {@link normalizedRoot}, de-duplicated, in input order. Called by the generator and
995
+ * by `doctor`, so the two cannot require different directories.
996
+ */
997
+ export function pluginRootDirectories(roots) {
998
+ return [...new Set(roots.map(normalizedRoot))];
999
+ }
1000
+ /** The switch as a message names it — and as `init`'s option row spells it. */
1001
+ export const PLUGIN_ROOT_ENTRIES_FLAG = '--plugin-root-entries';
1002
+ /**
1003
+ * The entries `doctor`'s `plugin-permissions` check requires at every root {@link resolvedPluginRoots}
1004
+ * returns, built by the same two functions it calls, plus those roots as
1005
+ * {@link pluginRootDirectories} gives them — or `undefined` when no root resolves. An entry or a
1006
+ * directory a root path would make unmatchable is warned about and left out rather than handed to
1007
+ * {@link assertRunnable}, which would bill a machine-local path to this CLI.
1008
+ */
1009
+ function generatedPluginRootEntries(repoRoot, config, warn) {
1010
+ const roots = resolvedPluginRoots(repoRoot);
1011
+ if (roots.length === 0)
1012
+ return undefined;
1013
+ const runtimeRoot = pluginRuntimeRoot(repoRoot);
1014
+ const runtime = runtimeRoot === undefined ? undefined : normalizedRoot(runtimeRoot);
1015
+ const helpers = pluginRootHelpers(roots, config.phases?.qa === true);
1016
+ const usable = (value, described) => {
1017
+ const forbidden = FORBIDDEN_IN_ENTRY.find(([needle]) => value.includes(needle));
1018
+ if (forbidden === undefined)
1019
+ return true;
1020
+ warn(`${PLUGIN_ROOT_ENTRIES_FLAG} left out ${described} ${JSON.stringify(value)}: the plugin root it names contains ${forbidden[1]}, which the permission guard matches literally, so it would match nothing at run time`);
1021
+ return false;
1022
+ };
1023
+ const entries = roots
1024
+ .flatMap((root) => pluginRootEntries(root, { isRuntimeRoot: root === runtime, helpers }))
1025
+ .map(({ rule }) => rule)
1026
+ .filter((rule) => usable(rule, 'the entry'));
1027
+ const directories = pluginRootDirectories(roots).filter((directory) => usable(directory, 'the directory'));
1028
+ return { entries, directories };
1029
+ }
1030
+ /**
1031
+ * The note `init` reports for `--plugin-root-entries` once the plan is applied: the kept-profile line
1032
+ * when `effect` is `'kept'` and entries were appended; undefined otherwise. `effect` is the write
1033
+ * engine's answer for {@link PROFILE_PATH}, identical under `--dry-run`, so the line is too.
1034
+ */
1035
+ export function pluginRootEntriesNote(outcome, effect) {
1036
+ if (outcome !== 'appended' || effect !== 'kept')
1037
+ return undefined;
1038
+ return `${PLUGIN_ROOT_ENTRIES_FLAG} had no effect: ${PROFILE_PATH} already exists and is kept under its create-if-absent contract, so the plugin-root entries this run rendered do not reach it. Run \`${DOCTOR_COMMAND}\` for the lines it lacks, or \`init --force ${PLUGIN_ROOT_ENTRIES_FLAG}\` to regenerate it after a .bak`;
1039
+ }
945
1040
  /**
946
1041
  * The `permissions.allow` entries of the profile currently on disk, or **an empty list for every way
947
1042
  * that can fail** — absent, unreadable, not JSON, not an object, no `permissions`, no `allow`.
@@ -1099,7 +1194,7 @@ function appendCarriedPluginRootEntries(profile, { repoRoot, dryRun, warn, note
1099
1194
  * reading the template: `<repo_root>`, `<work_root>` and `<worktree_glob>` are resolved per run
1100
1195
  * (`docs/config.md` §4), which is the whole reason the profile is generated rather than copied.
1101
1196
  */
1102
- export function renderProfile({ repoRoot, config, workRoot, written, writtenOuterLoop, referenceToolchainPath, force = false, dryRun = false, warn, note, }) {
1197
+ export function renderProfile({ repoRoot, config, workRoot, written, writtenOuterLoop, referenceToolchainPath, force = false, dryRun = false, pluginRootEntries: withPluginRootEntries = false, pluginRootOutcome, warn, note, }) {
1103
1198
  const scriptsDir = normalizeRepoDir(config.scriptsDir ?? DEFAULTS.scriptsDir);
1104
1199
  const stateDir = normalizeRepoDir(config.stateDir ?? DEFAULTS.stateDir);
1105
1200
  const projectName = config.projectName ?? defaultProjectName(repoRoot);
@@ -1155,6 +1250,25 @@ export function renderProfile({ repoRoot, config, workRoot, written, writtenOute
1155
1250
  ...(referenceToolchainPath === undefined ? {} : { referenceToolchainPath }),
1156
1251
  warn: warn ?? (() => { }),
1157
1252
  });
1253
+ // In the same window, and before the carry-forward, whose `allow` check then skips every line
1254
+ // generated here: one producer per line (module header).
1255
+ let outcome = 'off';
1256
+ if (withPluginRootEntries) {
1257
+ const generated = generatedPluginRootEntries(repoRoot, config, warn ?? (() => { }));
1258
+ if (generated === undefined) {
1259
+ outcome = 'no-root';
1260
+ (warn ?? (() => { }))(`${PLUGIN_ROOT_ENTRIES_FLAG} was given but ${installedPluginsPath()} records no plugin root on this machine, so no plugin-root entry was written: run \`claude plugin install\` for this plugin before init, then run init again`);
1261
+ }
1262
+ else {
1263
+ // Directories after the template's own, so its state-directory entry stays first.
1264
+ const added = appendAllow(profile, generated.entries) + appendAdditionalDirectories(profile, generated.directories);
1265
+ outcome = added > 0 ? 'appended' : 'none-written';
1266
+ if (added > 0) {
1267
+ appendReadme(profile, `THE PLUGIN-ROOT ENTRIES WERE WRITTEN BY init ${PLUGIN_ROOT_ENTRIES_FLAG}: the Read and Bash entries in permissions.allow, and the roots in permissions.additionalDirectories that let a shell command read under them, name this machine's plugin roots, which carry the plugin version, so they go stale on an upgrade. The switch is for a profile that lives no longer than the install it names - a remote job's.`);
1268
+ }
1269
+ }
1270
+ }
1271
+ pluginRootOutcome?.(outcome);
1158
1272
  // In the same window and for the same reason: a carried plugin helper whose basename happens to
1159
1273
  // match a wrapper's would be counted as a fourth form of that wrapper above. Gated on `force`,
1160
1274
  // which is the only run that replaces the file these entries are read out of — an unforced render
@@ -1184,9 +1298,10 @@ export function renderProfile({ repoRoot, config, workRoot, written, writtenOute
1184
1298
  *
1185
1299
  * Nothing here touches the filesystem: the generator plans, and `init` applies the plan once.
1186
1300
  */
1187
- export function writePermissionProfile({ repoRoot, config, plan, workRoot, written, writtenOuterLoop, referenceToolchainPath, force, dryRun, }) {
1301
+ export function writePermissionProfile({ repoRoot, config, plan, workRoot, written, writtenOuterLoop, referenceToolchainPath, force, dryRun, pluginRootEntries, }) {
1188
1302
  const warnings = [];
1189
1303
  const notes = [];
1304
+ let outcome = 'off';
1190
1305
  const profile = renderProfile({
1191
1306
  repoRoot,
1192
1307
  config,
@@ -1196,6 +1311,10 @@ export function writePermissionProfile({ repoRoot, config, plan, workRoot, writt
1196
1311
  ...(referenceToolchainPath === undefined ? {} : { referenceToolchainPath }),
1197
1312
  ...(force === undefined ? {} : { force }),
1198
1313
  ...(dryRun === undefined ? {} : { dryRun }),
1314
+ ...(pluginRootEntries === undefined ? {} : { pluginRootEntries }),
1315
+ pluginRootOutcome: (value) => {
1316
+ outcome = value;
1317
+ },
1199
1318
  warn: (message) => warnings.push(message),
1200
1319
  note: (message) => notes.push(message),
1201
1320
  });
@@ -1206,9 +1325,10 @@ export function writePermissionProfile({ repoRoot, config, plan, workRoot, writt
1206
1325
  repoPath: PROFILE_PATH,
1207
1326
  warnings,
1208
1327
  notes: [
1209
- `${PROFILE_PATH} is committed, and it is machine-specific: the absolute paths in it are this checkout's, so a copy of this repository somewhere else needs doctor to re-check them and init --force to regenerate them. Select it per run with the agent runner's settings flag; it is never installed as the interactive default.`,
1328
+ `${PROFILE_PATH} is gitignored - the managed .gitignore block carries its rule - and machine-specific: the absolute paths in it are this checkout's, so a copy of this repository somewhere else generates its own with init. A copy an earlier release committed stays tracked until it is untracked by hand; doctor reports that state and prints the route. Select it per run with the agent runner's settings flag; it is never installed as the interactive default.`,
1210
1329
  ...notes,
1211
1330
  ],
1331
+ pluginRootEntries: outcome,
1212
1332
  };
1213
1333
  }
1214
1334
  //# sourceMappingURL=permissionProfile.js.map