autonomous-sdlc-harness 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.js +0 -0
- package/dist/commands/docs.js +2 -1
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/doctor.js +27 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +102 -20
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +11 -4
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +25 -0
- package/dist/config/model.js.map +1 -1
- package/dist/core/git.js +29 -0
- package/dist/core/git.js.map +1 -1
- package/dist/core/paths.js +22 -2
- package/dist/core/paths.js.map +1 -1
- package/dist/core/prompt.js +6 -2
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/writer.js +2 -0
- package/dist/core/writer.js.map +1 -1
- package/dist/doctor/checks.js +343 -28
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +66 -0
- package/dist/generators/githubWorkflows.js.map +1 -0
- package/dist/generators/notifications.js +105 -19
- package/dist/generators/notifications.js.map +1 -1
- package/dist/generators/outerLoopScripts.js +17 -3
- package/dist/generators/outerLoopScripts.js.map +1 -1
- package/dist/generators/permissionProfile.js +101 -12
- package/dist/generators/permissionProfile.js.map +1 -1
- package/dist/generators/repoRoot.js +18 -2
- package/dist/generators/repoRoot.js.map +1 -1
- package/dist/generators/stateDir.js +9 -3
- package/dist/generators/stateDir.js.map +1 -1
- package/dist/machine/paths.js +4 -1
- package/dist/machine/paths.js.map +1 -1
- package/dist/remote/githubActions.js +86 -0
- package/dist/remote/githubActions.js.map +1 -0
- package/dist/retrieval/queryLog.js +6 -4
- package/dist/retrieval/queryLog.js.map +1 -1
- package/dist/retrieval/runtime.js +12 -22
- package/dist/retrieval/runtime.js.map +1 -1
- package/dist/retrieval/search.js +21 -10
- package/dist/retrieval/search.js.map +1 -1
- package/dist/retrieval/server.js +1 -1
- package/dist/retrieval/setup.js +2 -1
- package/dist/retrieval/setup.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +3 -2
- package/templates/claude/context/conventions.md +1 -1
- package/templates/claude/context/layer.md +1 -1
- package/templates/claude/push-notify.env.example +7 -2
- package/templates/claude/settings.autonomous.json +1 -1
- package/templates/github/workflows/harness-resume.yml +124 -0
- package/templates/github/workflows/harness-run.yml +446 -0
- package/templates/repo/gitignore +5 -0
- package/templates/scripts/README.md +1 -1
- package/templates/scripts/autonomous-watcher.sh +1141 -143
- package/templates/scripts/flow-walker.sh +629 -0
- package/templates/scripts/flows/task_plan_writing.graph.json +192 -0
- package/templates/scripts/lib/flow-walker-gates.sh +165 -0
- package/templates/scripts/lib/harness-run-lib.sh +433 -18
- package/templates/scripts/remote-run.sh +1785 -0
- package/templates/scripts/restart-watcher.sh +21 -1
- package/templates/scripts/run-test-suite.sh +182 -0
- package/templates/scripts/scratch-run.sh +2 -1
- package/templates/state-dir/README-root.md +1 -1
- package/templates/state-dir/autonomous_logs/README.md +1 -1
- package/templates/state-dir/improvement_observations/README.md +2 -0
- package/templates/state-dir/scratch/README.md +1 -1
- package/templates/state-dir/test_fix_plan_reviews/README.md +9 -0
- package/templates/state-dir/test_fix_plans/README.md +9 -0
- package/templates/state-dir/test_fix_point_reviews/README.md +9 -0
- 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
|
|
16
|
-
*
|
|
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
|
-
* ##
|
|
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
|
|
28
|
-
* wrote, the note names the two keys
|
|
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
|
|
32
|
-
* it
|
|
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
|
-
*
|
|
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
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
|
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}=${
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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":"AAAA
|
|
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
|
|
3
|
-
*
|
|
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":"AAAA
|
|
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
|
|
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,14 @@
|
|
|
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
|
|
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 still never tests whether the profile exists: whether the render
|
|
111
|
+
* lands is the write engine's `create-if-absent` answer, which `init` hands back to
|
|
112
|
+
* {@link pluginRootEntriesNote}. On a forced run the generated lines are appended before the
|
|
113
|
+
* carry-forward, which skips any line already in `allow`, so each line has one producer — the rule
|
|
114
|
+
* the browser-fragment section states.
|
|
107
115
|
* - **It adds no `hooks` key.** The plugin ships its own guard hooks, which fire from the plugin,
|
|
108
116
|
* resolve their own root and append to the adopter's hooks rather than replacing them; writing
|
|
109
117
|
* them into user settings as well would run each guard twice. The template says so in a comment
|
|
@@ -126,9 +134,11 @@
|
|
|
126
134
|
* `plugin-permissions` check re-derives the root on every run and prints each missing line — and
|
|
127
135
|
* {@link QA_TEMPLATE_PATH}'s `_README` carries the entry *form* an adopter adds by hand, which the
|
|
128
136
|
* `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
|
-
*
|
|
131
|
-
*
|
|
137
|
+
* there — is what makes survive every later `init`. Whether `init` should also *write* those
|
|
138
|
+
* entries was an open owner decision, now taken for `--plugin-root-entries` only, default unchanged:
|
|
139
|
+
* inside a remote job the plugin is installed immediately before `init` runs and the profile dies
|
|
140
|
+
* with the job, so neither reason above holds there, while both still hold for the default.
|
|
141
|
+
* Moving the helpers somewhere a rule can name unconditionally — the adopter's own
|
|
132
142
|
* `scriptsDir`, which the three wrapper forms already reach — would remove the manual step but
|
|
133
143
|
* change the invocation form everywhere the shipped instruction corpus calls them, so it belongs
|
|
134
144
|
* to the item that ships them (`plugin/scripts/README.md`) and not to this module.
|
|
@@ -183,7 +193,7 @@ import { isJsonObject, readJsonFile } from '../core/json.js';
|
|
|
183
193
|
import { defaultProjectName, readTemplate, workRoot as resolveWorkRoot, worktreeGlob } from '../core/paths.js';
|
|
184
194
|
import { normalizeRepoDir } from '../core/repoPaths.js';
|
|
185
195
|
import { containsToken, renderTemplate } from '../core/templating.js';
|
|
186
|
-
import { installedPluginsPath, pluginInstallRoot, pluginRuntimeRoot, SCRIPTS_DIRNAME } from '../machine/plugins.js';
|
|
196
|
+
import { installedPluginsPath, pluginHelperPath, pluginHelperScripts, pluginInstallRoot, pluginRuntimeRoot, SCRIPTS_DIRNAME, } from '../machine/plugins.js';
|
|
187
197
|
import { SEARCH_TOOL_PERMISSION } from '../retrieval/server.js';
|
|
188
198
|
import { OUTER_LOOP_SCRIPTS, outerLoopRelativePath, } from './outerLoopScripts.js';
|
|
189
199
|
import { scriptInvocation, selectWrapper, WRAPPER_SCRIPTS, wrapperInvocation, } from './scripts.js';
|
|
@@ -565,16 +575,16 @@ function entriesOf(permissions, list) {
|
|
|
565
575
|
*
|
|
566
576
|
* It is also one of the two forms a **plugin-root** entry is written in: `doctor`'s
|
|
567
577
|
* `plugin-permissions` check prints those lines for an operator to paste, and
|
|
568
|
-
* {@link pluginRootEntryTarget} reads one back.
|
|
569
|
-
* root — the module header's
|
|
578
|
+
* {@link pluginRootEntryTarget} reads one back. This generator emits one for a plugin root only
|
|
579
|
+
* under `init --plugin-root-entries`, through {@link pluginRootEntries} — the module header's decision.
|
|
570
580
|
*/
|
|
571
581
|
export function readRule(absolutePath) {
|
|
572
582
|
return `Read(/${absolutePath}/**)`;
|
|
573
583
|
}
|
|
574
584
|
/**
|
|
575
585
|
* 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
|
|
577
|
-
*
|
|
586
|
+
* the other plugin-root form, for a helper under the plugin's own scripts directory. Same consumers,
|
|
587
|
+
* and the same decision, as {@link readRule}.
|
|
578
588
|
*/
|
|
579
589
|
export function bashScriptRule(absolutePath) {
|
|
580
590
|
return `Bash(${INVOCATION_PREFIX}${absolutePath}:*)`;
|
|
@@ -942,6 +952,63 @@ function resolvedPluginRoots(repoRoot) {
|
|
|
942
952
|
const roots = [pluginRuntimeRoot(repoRoot), pluginInstallRoot(repoRoot)];
|
|
943
953
|
return [...new Set(roots.filter((root) => root !== undefined).map(normalizedRoot))];
|
|
944
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* The helper-script names graded at every root: the sorted, de-duplicated union of
|
|
957
|
+
* {@link pluginHelperScripts} over `roots` while `phases.qa` is on, and empty while it is off — the
|
|
958
|
+
* helpers are that phase's alone. Called by `doctor`'s `plugin-permissions` check and by
|
|
959
|
+
* {@link generatedPluginRootEntries}, so the two cannot grade different names.
|
|
960
|
+
*/
|
|
961
|
+
export function pluginRootHelpers(roots, qaOn) {
|
|
962
|
+
return qaOn ? [...new Set(roots.flatMap((root) => pluginHelperScripts(root)))].sort() : [];
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* The entries one root requires: {@link readRule} unless it is the install root — reads there were
|
|
966
|
+
* measured to succeed ungranted (`doctor/checks.ts`, `PLUGIN_PERMISSIONS_CHECK`) — then
|
|
967
|
+
* {@link bashScriptRule} for each of `helpers`, in that order. The one per-root builder, called by
|
|
968
|
+
* that check and by {@link generatedPluginRootEntries}.
|
|
969
|
+
*/
|
|
970
|
+
export function pluginRootEntries(root, options) {
|
|
971
|
+
return [
|
|
972
|
+
...(options.isInstallRoot ? [] : [{ kind: 'read', rule: readRule(root) }]),
|
|
973
|
+
...options.helpers.map((name) => ({ kind: 'helper', rule: bashScriptRule(pluginHelperPath(root, name)) })),
|
|
974
|
+
];
|
|
975
|
+
}
|
|
976
|
+
/** The switch as a message names it — and as `init`'s option row spells it. */
|
|
977
|
+
export const PLUGIN_ROOT_ENTRIES_FLAG = '--plugin-root-entries';
|
|
978
|
+
/**
|
|
979
|
+
* The entries `doctor`'s `plugin-permissions` check requires at every root {@link resolvedPluginRoots}
|
|
980
|
+
* returns, built by the same two functions it calls, or `undefined` when no root resolves. An entry
|
|
981
|
+
* a root path would make unmatchable is warned about and left out rather than handed to
|
|
982
|
+
* {@link assertRunnable}, which would bill a machine-local path to this CLI.
|
|
983
|
+
*/
|
|
984
|
+
function generatedPluginRootEntries(repoRoot, config, warn) {
|
|
985
|
+
const roots = resolvedPluginRoots(repoRoot);
|
|
986
|
+
if (roots.length === 0)
|
|
987
|
+
return undefined;
|
|
988
|
+
const installRoot = pluginInstallRoot(repoRoot);
|
|
989
|
+
const install = installRoot === undefined ? undefined : normalizedRoot(installRoot);
|
|
990
|
+
const helpers = pluginRootHelpers(roots, config.phases?.qa === true);
|
|
991
|
+
return roots
|
|
992
|
+
.flatMap((root) => pluginRootEntries(root, { isInstallRoot: root === install, helpers }))
|
|
993
|
+
.map(({ rule }) => rule)
|
|
994
|
+
.filter((rule) => {
|
|
995
|
+
const forbidden = FORBIDDEN_IN_ENTRY.find(([needle]) => rule.includes(needle));
|
|
996
|
+
if (forbidden === undefined)
|
|
997
|
+
return true;
|
|
998
|
+
warn(`${PLUGIN_ROOT_ENTRIES_FLAG} left out ${JSON.stringify(rule)}: the plugin root it names contains ${forbidden[1]}, which the permission guard matches literally, so the entry would match nothing at run time`);
|
|
999
|
+
return false;
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* The note `init` reports for `--plugin-root-entries` once the plan is applied: the kept-profile line
|
|
1004
|
+
* when `effect` is `'kept'` and entries were appended; undefined otherwise. `effect` is the write
|
|
1005
|
+
* engine's answer for {@link PROFILE_PATH}, identical under `--dry-run`, so the line is too.
|
|
1006
|
+
*/
|
|
1007
|
+
export function pluginRootEntriesNote(outcome, effect) {
|
|
1008
|
+
if (outcome !== 'appended' || effect !== 'kept')
|
|
1009
|
+
return undefined;
|
|
1010
|
+
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`;
|
|
1011
|
+
}
|
|
945
1012
|
/**
|
|
946
1013
|
* The `permissions.allow` entries of the profile currently on disk, or **an empty list for every way
|
|
947
1014
|
* that can fail** — absent, unreadable, not JSON, not an object, no `permissions`, no `allow`.
|
|
@@ -1099,7 +1166,7 @@ function appendCarriedPluginRootEntries(profile, { repoRoot, dryRun, warn, note
|
|
|
1099
1166
|
* reading the template: `<repo_root>`, `<work_root>` and `<worktree_glob>` are resolved per run
|
|
1100
1167
|
* (`docs/config.md` §4), which is the whole reason the profile is generated rather than copied.
|
|
1101
1168
|
*/
|
|
1102
|
-
export function renderProfile({ repoRoot, config, workRoot, written, writtenOuterLoop, referenceToolchainPath, force = false, dryRun = false, warn, note, }) {
|
|
1169
|
+
export function renderProfile({ repoRoot, config, workRoot, written, writtenOuterLoop, referenceToolchainPath, force = false, dryRun = false, pluginRootEntries: withPluginRootEntries = false, pluginRootOutcome, warn, note, }) {
|
|
1103
1170
|
const scriptsDir = normalizeRepoDir(config.scriptsDir ?? DEFAULTS.scriptsDir);
|
|
1104
1171
|
const stateDir = normalizeRepoDir(config.stateDir ?? DEFAULTS.stateDir);
|
|
1105
1172
|
const projectName = config.projectName ?? defaultProjectName(repoRoot);
|
|
@@ -1155,6 +1222,22 @@ export function renderProfile({ repoRoot, config, workRoot, written, writtenOute
|
|
|
1155
1222
|
...(referenceToolchainPath === undefined ? {} : { referenceToolchainPath }),
|
|
1156
1223
|
warn: warn ?? (() => { }),
|
|
1157
1224
|
});
|
|
1225
|
+
// In the same window, and before the carry-forward, whose `allow` check then skips every line
|
|
1226
|
+
// generated here: one producer per line (module header).
|
|
1227
|
+
let outcome = 'off';
|
|
1228
|
+
if (withPluginRootEntries) {
|
|
1229
|
+
const generated = generatedPluginRootEntries(repoRoot, config, warn ?? (() => { }));
|
|
1230
|
+
if (generated === undefined) {
|
|
1231
|
+
outcome = 'no-root';
|
|
1232
|
+
(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`);
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
outcome = 'appended';
|
|
1236
|
+
appendAllow(profile, generated);
|
|
1237
|
+
appendReadme(profile, `THE PLUGIN-ROOT ENTRIES WERE WRITTEN BY init ${PLUGIN_ROOT_ENTRIES_FLAG}: they 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.`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
pluginRootOutcome?.(outcome);
|
|
1158
1241
|
// In the same window and for the same reason: a carried plugin helper whose basename happens to
|
|
1159
1242
|
// match a wrapper's would be counted as a fourth form of that wrapper above. Gated on `force`,
|
|
1160
1243
|
// which is the only run that replaces the file these entries are read out of — an unforced render
|
|
@@ -1184,9 +1267,10 @@ export function renderProfile({ repoRoot, config, workRoot, written, writtenOute
|
|
|
1184
1267
|
*
|
|
1185
1268
|
* Nothing here touches the filesystem: the generator plans, and `init` applies the plan once.
|
|
1186
1269
|
*/
|
|
1187
|
-
export function writePermissionProfile({ repoRoot, config, plan, workRoot, written, writtenOuterLoop, referenceToolchainPath, force, dryRun, }) {
|
|
1270
|
+
export function writePermissionProfile({ repoRoot, config, plan, workRoot, written, writtenOuterLoop, referenceToolchainPath, force, dryRun, pluginRootEntries, }) {
|
|
1188
1271
|
const warnings = [];
|
|
1189
1272
|
const notes = [];
|
|
1273
|
+
let outcome = 'off';
|
|
1190
1274
|
const profile = renderProfile({
|
|
1191
1275
|
repoRoot,
|
|
1192
1276
|
config,
|
|
@@ -1196,6 +1280,10 @@ export function writePermissionProfile({ repoRoot, config, plan, workRoot, writt
|
|
|
1196
1280
|
...(referenceToolchainPath === undefined ? {} : { referenceToolchainPath }),
|
|
1197
1281
|
...(force === undefined ? {} : { force }),
|
|
1198
1282
|
...(dryRun === undefined ? {} : { dryRun }),
|
|
1283
|
+
...(pluginRootEntries === undefined ? {} : { pluginRootEntries }),
|
|
1284
|
+
pluginRootOutcome: (value) => {
|
|
1285
|
+
outcome = value;
|
|
1286
|
+
},
|
|
1199
1287
|
warn: (message) => warnings.push(message),
|
|
1200
1288
|
note: (message) => notes.push(message),
|
|
1201
1289
|
});
|
|
@@ -1209,6 +1297,7 @@ export function writePermissionProfile({ repoRoot, config, plan, workRoot, writt
|
|
|
1209
1297
|
`${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.`,
|
|
1210
1298
|
...notes,
|
|
1211
1299
|
],
|
|
1300
|
+
pluginRootEntries: outcome,
|
|
1212
1301
|
};
|
|
1213
1302
|
}
|
|
1214
1303
|
//# sourceMappingURL=permissionProfile.js.map
|