muse-crew 0.4.3 → 0.4.5
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/AGENTS.md +2 -0
- package/API.md +68 -21
- package/README.md +18 -2
- package/docs/guide.md +81 -11
- package/docs/visual-verdict.md +120 -0
- package/identities/hazel.md +22 -0
- package/lib/AGENTS.md +6 -1
- package/lib/build-registry.js +80 -0
- package/lib/compose-evidence.py +144 -0
- package/lib/crew-release.sh +9 -1
- package/lib/merge-lock.sh +18 -0
- package/lib/orphan-sweep.sh +70 -7
- package/lib/publish-npm.sh +38 -14
- package/lib/test-merge-lock.sh +69 -0
- package/lib/test-orphan-sweep.sh +74 -8
- package/lib/test-publish-verify.sh +157 -0
- package/lib/test-version-write.sh +102 -0
- package/lib/test-worktree-backend.sh +79 -0
- package/lib/worktree-lifecycle.sh +69 -28
- package/package.json +1 -1
- package/seed/cron-body-template.md +5 -3
- package/seed/workflows/bugfix.md +7 -2
- package/seed/workflows/chore.md +7 -3
- package/seed/workflows/standard.md +7 -2
- package/workflows/AGENTS.md +4 -4
- package/workflows/bugfix.js +1023 -173
- package/workflows/chore.js +888 -146
- package/workflows/crew-dispatch.js +405 -98
- package/workflows/crew-init.js +4 -35
- package/workflows/docs.js +342 -22
- package/workflows/standard.js +1024 -172
- package/workflows/tests/retry-cap.test.mjs +102 -0
- package/workflows/tests/work-agent-failure.test.mjs +122 -0
package/AGENTS.md
CHANGED
|
@@ -11,9 +11,11 @@ Muse Crew source repository. The repo is the product; the personal instance (`$C
|
|
|
11
11
|
- `personas/` — QA perspective costumes for Hazel
|
|
12
12
|
- `seed/` — init source data: everything `crew-init.js` reads when setting up a new crew instance
|
|
13
13
|
- `workflows/` — executable Muse workflow scripts (JavaScript)
|
|
14
|
+
- `tests/` — regression tests for the deterministic contracts (`bash tests/run.sh`)
|
|
14
15
|
|
|
15
16
|
## Rules
|
|
16
17
|
|
|
17
18
|
- Git source is authoritative.
|
|
18
19
|
- Never expose this repo publicly.
|
|
19
20
|
- Ship implementation and documentation together.
|
|
21
|
+
- After editing `workflows/*.js`, run `bash tests/run.sh` — `node --check` does NOT catch syntax errors inside function bodies (V8 lazy preparsing), so it cannot validate workflow edits alone.
|
package/API.md
CHANGED
|
@@ -16,11 +16,14 @@ Create a new task on the board.
|
|
|
16
16
|
|-------|------|----------|-------|
|
|
17
17
|
| `title` | string (1–160 chars) | yes | |
|
|
18
18
|
| `description` | string (≤ 5000) | no | Defaults to `""` |
|
|
19
|
-
| `project` | slug | no | Implicit project resolution: an explicit `project` wins; otherwise the service
|
|
19
|
+
| `project` | slug | no | Implicit project resolution: an explicit `project` wins; otherwise the service uses the `"orchestra-dashboard"` project id and throws `"Project not found."` if it isn't registered. There is no default-project fallback — pass `project` explicitly. |
|
|
20
20
|
| `workflow` | slug or null | no | Routing label (e.g. `standard`, `bugfix`, `chore`, `docs`) |
|
|
21
21
|
| `priority` | `high` · `normal` · `low` | no | Defaults to `normal` |
|
|
22
22
|
| `state` | `todo` · `in_progress` · `parked` · `done` | no | Defaults to `todo` |
|
|
23
23
|
| `deps` | string[] | no | Task IDs this task depends on |
|
|
24
|
+
| `filed_by` | string (1–80 chars) | yes | Who filed the task — plain text, by convention `human`, `hazel`, or `dispatcher`. It is a label, not a reference: it doesn't point at an identity, so it survives even if the filer is long gone. |
|
|
25
|
+
|
|
26
|
+
The task record echoes the field back as `filed_by`, which may be `null`: tasks filed before the field existed have none, and no backfill was run — so treat a `null` as silence, not as a statement. (The `created` event's `identity` is a different concept — that's the phase performer, not the filer.)
|
|
24
27
|
|
|
25
28
|
### `updatetask`
|
|
26
29
|
|
|
@@ -31,7 +34,7 @@ Update an existing task's fields. Only `id` is required; all other fields are op
|
|
|
31
34
|
| `id` | uuid | yes | |
|
|
32
35
|
| `title` | string (1–200) | no | |
|
|
33
36
|
| `description` | string (≤ 3000) | no | |
|
|
34
|
-
| `state` | `todo` · `in_progress` · `parked` · `done` | no | |
|
|
37
|
+
| `state` | `todo` · `in_progress` · `parked` · `done` | no | Only the `parked` → `todo` transition stamps `retry_reset_at` to the current server time (UTC, ISO 8601), mechanically restarting the consecutive-failure streak — a re-queued task never inherits its old failures. No other transition stamps it (`parked` → `done`, `todo` → `todo`, and patches without a `state` field do not). |
|
|
35
38
|
| `priority` | `high` · `normal` · `low` | no | |
|
|
36
39
|
| `project` | slug | no | **Project-move guard:** changing this to a different project throws while the task has an active run (an agent session with status `running` that started within the last hour), because the live run keeps the old project's repo context and moving it mid-phase would work on the wrong repo. Wait for the run to finish, or recover/park the task first, then move it. |
|
|
37
40
|
| `workflow` | slug or null | no | |
|
|
@@ -66,6 +69,17 @@ Send a stuck or failed task to a specific workflow phase for recovery.
|
|
|
66
69
|
| `target_phase` | string (1–120) | yes | The phase to send the task to |
|
|
67
70
|
| `updated_description` | string (≤ 5000) | no | Optional revised description |
|
|
68
71
|
|
|
72
|
+
### `parktask`
|
|
73
|
+
|
|
74
|
+
Atomically park a task for human attention: sets `state` to `parked`, writes the explanatory `note` event, and settles every `running` session for the task — in one transaction. Either all land or none do: callers never see a note without a park, a park without a note, or a parked task with a ghost `running` session left behind (a terminal task transition settles its sessions). Settled sessions move to `failed` — the status `recovertask` accepts, so recovery works immediately after a park — with `ended_at` stamped and `Parked: <message>` appended to their notes; `failure_reason` stays null.
|
|
75
|
+
|
|
76
|
+
| Field | Type | Required | Notes |
|
|
77
|
+
|-------|------|----------|-------|
|
|
78
|
+
| `task_id` | uuid | yes | |
|
|
79
|
+
| `message` | string (1–1000) | yes | Why the task needs a human; written to the activity feed as a `note` |
|
|
80
|
+
|
|
81
|
+
Returns `{ "ok": true, "task": {...}, "settled_sessions": <n> }` with the updated task record and the count of sessions settled (`0` when none were running).
|
|
82
|
+
|
|
69
83
|
---
|
|
70
84
|
|
|
71
85
|
## Sessions
|
|
@@ -120,6 +134,32 @@ Read the event timeline.
|
|
|
120
134
|
|
|
121
135
|
Read the full board state for dispatch decisions. Takes no arguments. Returns all projects with their tasks, simultaneity limits, quiesce state, and active sessions — everything the dispatcher needs to decide what to claim.
|
|
122
136
|
|
|
137
|
+
Each task in `ready_tasks` carries an explicit retry object:
|
|
138
|
+
|
|
139
|
+
| Field | Type | Notes |
|
|
140
|
+
|-------|------|-------|
|
|
141
|
+
| `retry.consecutive_failures` | integer ≥ 0 | Counted by the procedure below. Computed deterministically server-side — the dispatcher consumes it directly, never projecting history or parsing event messages. |
|
|
142
|
+
| `retry.rejections_since_reset` | integer ≥ 0 | Count of `rejected` sessions with `started_at` after the task's `retry_reset_at` watermark (all rejections if the watermark is null), regardless of step. Unlike the failure streak, this is not trailing-per-step: a Review → Build → Review cycle accumulates, because the rework budget bounds rejections of the work, not of a step. Reset by the same `parked` → `todo` stamp. |
|
|
143
|
+
|
|
144
|
+
**Counting procedure** (normative — two conforming dashboards must produce the same number from the same history):
|
|
145
|
+
|
|
146
|
+
1. Order the task's sessions by `started_at` descending, newest first. Let S be the newest session; if there are no sessions, the count is 0.
|
|
147
|
+
2. Walk newest → oldest, counting each session, and **stop** at the first session where any of these holds:
|
|
148
|
+
- its `started_at` is at or before the task's `retry_reset_at` watermark (a session that started before the reset but ended after it is excluded — the comparison is on start time);
|
|
149
|
+
- its `step` differs from S's `step` (a step change *breaks* the walk; it is not skipped — so a rejection at a new step resets the operational streak for that step);
|
|
150
|
+
- its `status` is anything other than `failed` or `timed_out`.
|
|
151
|
+
3. The count is the number of sessions counted before stopping.
|
|
152
|
+
|
|
153
|
+
Retry semantics: the dispatcher retries a failed phase while `retry.consecutive_failures` is below its configured cap (default 3 consecutive failures; `maxConsecutiveFailures <= 0` disables the cap). At the cap it parks the task for human attention. Separately, the dispatcher parks a task whose `retry.rejections_since_reset` reaches the rejection budget (default 2; `maxConsecutiveRejections <= 0` disables it) instead of redispatching it to Build/Write for another rework round — this bounds Review/QA rejection cycling across dispatcher ticks, which the workflow's in-run rework budget cannot see. Moving a task from `parked` back to `todo` stamps `retry_reset_at`, mechanically restarting both counters — a re-queued task never inherits its old failures or rejections. Sessions and events are kept; the watermark only excludes pre-reset sessions from the counts. `retry_reset_at` is also exposed on the task as `retry_reset_at` (nullable) for observability.
|
|
154
|
+
|
|
155
|
+
A dashboard that omits `retry` or returns an invalid `consecutive_failures` (not a non-negative number) behaves as cap-disabled: the dispatcher retries the phase and logs a warning. The cap requires a dashboard implementing this field — against an older or non-conforming dashboard the cap is silently inert.
|
|
156
|
+
|
|
157
|
+
Task `state` vocabulary: `todo` = ready for dispatch; `in_progress` = a workflow run is active or awaiting its next phase; `parked` = human hold, the dispatcher skips it; `done` = all phases finished. `blocked` is never a stored task state — it is derived mechanically from unmet dependencies (surfaced as a `blocked` flag on the task) and never authored by hand.
|
|
158
|
+
|
|
159
|
+
Session `status` vocabulary: `failed` = a phase was attempted but did not finish (retryable); `rejected` = an explicit phase verdict (Review/QA said no), routed through rework. Workflows never record `blocked` for operational failures — that word is reserved for the dependency signal.
|
|
160
|
+
|
|
161
|
+
Two namespaces, not one: the vocabulary above is the **dashboard API** (task states, session statuses, event types). Workflow scripts additionally return a **workflow result envelope** to their launcher (`{status: "ok" | "failed" | "parked", ...}`) and may return `{__hatchWorkflowControl: "blocked"}` as a runtime halt signal. The `"blocked"` inside `__hatchWorkflowControl` is a workflow-runtime mechanism, unrelated to the dashboard's dependency-derived `blocked` — same word, different namespace. `parked` as a workflow return means the workflow parked the task via `parktask` and the launcher should treat the run as complete.
|
|
162
|
+
|
|
123
163
|
### `acknowledge_poll`
|
|
124
164
|
|
|
125
165
|
Record that a poll tick occurred. Takes no arguments. Used by the dispatcher at the end of each cycle to update the last-polled timestamp.
|
|
@@ -143,31 +183,19 @@ Register a new project.
|
|
|
143
183
|
| `simultaneity` | integer (1–100) | no | Max concurrent tasks; defaults to 2 |
|
|
144
184
|
| `quiesced` | boolean | no | Start paused; defaults to false |
|
|
145
185
|
|
|
146
|
-
The first registered project automatically becomes the `default_project` (see State) when no default is set yet.
|
|
147
|
-
|
|
148
186
|
### `updateproject`
|
|
149
187
|
|
|
150
188
|
Update project fields. Only `id` is required; all others are optional patch fields. Same fields as `createproject`, except `simultaneity` minimum is 0 (for kill switch state).
|
|
151
189
|
|
|
152
190
|
**Context-change guard:** changing `repo_path`, `deploy_type`, or `deploy_slug` is blocked while any task in the project has an active run (an agent session with status `running` that started within the last hour): the call throws, because live runs keep the old project config and the change would split the project context mid-run. Wait for the runs to finish, or recover/park those tasks first, then retry.
|
|
153
191
|
|
|
154
|
-
### `setdefaultproject`
|
|
155
|
-
|
|
156
|
-
Set the dashboard's default project — the project the CLI and agents use implicitly when no explicit project is given. The dispatcher resolves a task's project as its explicit label first, then this default, then the first registered project as a last resort.
|
|
157
|
-
|
|
158
|
-
| Field | Type | Required | Notes |
|
|
159
|
-
|-------|------|----------|-------|
|
|
160
|
-
| `project_id` | slug | yes | Must be a registered project id, else the call throws `"Project not found."` |
|
|
161
|
-
|
|
162
|
-
Returns `{ "ok": true, "project_id": "<slug>" }`. Idempotent: sets the `default_project` config key via upsert.
|
|
163
|
-
|
|
164
192
|
### `deleteproject`
|
|
165
193
|
|
|
166
194
|
Remove a project registration.
|
|
167
195
|
|
|
168
196
|
| Field | Type | Required | Notes |
|
|
169
197
|
|-------|------|----------|-------|
|
|
170
|
-
| `id` | slug | yes |
|
|
198
|
+
| `id` | slug | yes | Removes the project record along with its tasks, sessions, events, and any stored kill-switch simultaneity. |
|
|
171
199
|
|
|
172
200
|
### `listprojects`
|
|
173
201
|
|
|
@@ -215,13 +243,9 @@ Read the full dashboard state: tasks, sessions, events, and projects. Used by wo
|
|
|
215
243
|
|
|
216
244
|
Read global configuration. Takes no arguments.
|
|
217
245
|
|
|
218
|
-
**Known keys:**
|
|
246
|
+
**Known keys:** none are defined by the API itself — the contract stores no global config. Dashboards may expose `updateconfig` for their own settings, but agents and workflows should not rely on any key existing.
|
|
219
247
|
|
|
220
|
-
|
|
221
|
-
|-----|-------|--------|
|
|
222
|
-
| `default_project` | project slug | `setdefaultproject` (explicit upsert), `createproject` (first project only), `crew-init` (the dashboard's own project) |
|
|
223
|
-
|
|
224
|
-
`default_project` is the dashboard's configured default project: agents and the CLI inherit it when no explicit project is given. Deleting the project it points at clears it. No migration is needed — the key simply appears once set, and the dispatcher reads it from `getdispatchstate`'s `config` object.
|
|
248
|
+
Crew convention (not API surface): the dispatcher reads and writes per-project `playtest.<project>.journeys`, `playtest.<project>.journey_cursor`, `playtest.<project>.persona_cursor`, and `playtest.<project>.max_filings` through these two actions — see "Idle playtesting" in the [guide](docs/guide.md). It also reads two **global** (whole-crew, not per-project) keys: `maxConsecutiveFailures` (integer-as-string, default `"3"`, `<=0` disables the retry cap — see "Failure handling and retry" in the guide) and `maxConsecutiveRejections` (integer-as-string, default `"2"`, `<=0` disables the rejection budget). Values are parsed with `parseInt`; an unparseable value silently falls back to the default.
|
|
225
249
|
|
|
226
250
|
### `updateconfig`
|
|
227
251
|
|
|
@@ -249,6 +273,29 @@ Read the last publication provenance. Returns `{ "provenance": { "source_commit"
|
|
|
249
273
|
|
|
250
274
|
---
|
|
251
275
|
|
|
276
|
+
## Workflow contracts
|
|
277
|
+
|
|
278
|
+
These are not dashboard actions — they are contracts the workflow runtime enforces on every run. They are documented here because the API is the contract everywhere: a conforming crew release must honor them.
|
|
279
|
+
|
|
280
|
+
### Work-report transport
|
|
281
|
+
|
|
282
|
+
The runtime transports `agent()` output as JSON. Work agents return their report as free text wrapped in a minimal schema: `{ "report": "<free-text work report>" }`. The schema is a transport envelope, not a demand for machine-structured reasoning — the report itself stays prose. The workflow extracts the `report` field and works with the text from there.
|
|
283
|
+
|
|
284
|
+
### Deterministic verdict extraction
|
|
285
|
+
|
|
286
|
+
Phases whose outcome drives control flow (Build, Review, QA, Reproduce, Integrate, Publish) declare their verdict explicitly: the worker ends its report with exactly one line, `VERDICT: PASS` or `VERDICT: FAIL`. The verdict is extracted mechanically by workflow code — never by an agent, and never by a formatter agent. Extraction fails closed: a missing, malformed, or contradictory verdict line fails the phase; an ambiguous verdict never passes.
|
|
287
|
+
|
|
288
|
+
### Artifact publish verification
|
|
289
|
+
|
|
290
|
+
Artifact Publish passes only when both are true:
|
|
291
|
+
|
|
292
|
+
1. The worker declares success.
|
|
293
|
+
2. Mechanical verification confirms the artifact's provenance matches the integrated Git commit: the workflow reads `getprovenance` and compares `provenance.source_commit` against `git rev-parse HEAD` of the project repo.
|
|
294
|
+
|
|
295
|
+
Verification fails closed. If the provenance is missing, unreadable, or points at a different commit than the integrated HEAD, the workflow parks the task — it does not trust the worker's prose. Narrative remains useful but cannot independently establish deployment truth.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
252
299
|
## Not part of this API
|
|
253
300
|
|
|
254
301
|
The following are dashboard-specific view operations, not part of the Crew API contract:
|
package/README.md
CHANGED
|
@@ -40,13 +40,29 @@ docs: Triage → Write → Review
|
|
|
40
40
|
|
|
41
41
|
## Quick Start
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
There is no setup wizard. The instructions below are for the Muse agent doing this job, not a human at a terminal.
|
|
44
|
+
|
|
45
|
+
First, you'll need a task service that implements the [Crew API](API.md) — something to hold tasks, track sessions, and serve the board. The [Orchestra Dashboard](https://github.com/emojimanegg1/orchestra-dashboard) is the reference implementation. Create that artifact first and note its slug; init needs it.
|
|
46
|
+
|
|
47
|
+
Then install the package:
|
|
44
48
|
|
|
45
49
|
```
|
|
46
50
|
npm install muse-crew
|
|
47
51
|
```
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
That puts the code in `node_modules/muse-crew`. Now launch the init script — note that it's not a command you type. There is no `crew-init` binary; the package has no `bin` field, so there's nothing to run at the shell. Instead, start it as a workflow through the agent's workflow tools:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
workflow_launch with:
|
|
57
|
+
scriptPath: "<install-path>/workflows/crew-init.js"
|
|
58
|
+
args: {
|
|
59
|
+
crewRepoPath: "<install-path>",
|
|
60
|
+
crewHome: "~/workspace/.jarvis",
|
|
61
|
+
dashboardSlug: "orchestra-dashboard"
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For the full reference — optional arguments, what init creates, and how to verify it worked — see [**Setup from scratch**](docs/guide.md#setup-from-scratch) in the Guide.
|
|
50
66
|
|
|
51
67
|
The dashboard becomes the crew's first project — the crew works on itself.
|
|
52
68
|
|
package/docs/guide.md
CHANGED
|
@@ -137,6 +137,10 @@ The project does **not** need:
|
|
|
137
137
|
- Any special file structure — the crew works with whatever the repo contains.
|
|
138
138
|
- Workflow files — those live in the crew home's release, not in the project.
|
|
139
139
|
|
|
140
|
+
### Worktrees
|
|
141
|
+
|
|
142
|
+
Each task builds in an isolated git worktree at `<repo>/.worktrees/<task_id>`, on a branch named `task/<task_id>`. The lifecycle manager (`lib/worktree-lifecycle.sh`) owns creation and removal — workflows and agents never run raw `git worktree` commands. The crew's own task→worktree registry (`<repo>/.worktrees/.registry/`) is the source of truth for task→branch/path; `prepare` fails closed when `main` is dirty, and `cleanup` is forgiving so a failed run never strands a worktree.
|
|
143
|
+
|
|
140
144
|
## Filing a task
|
|
141
145
|
|
|
142
146
|
Create a task using the `createtask` action (see the [API reference](../API.md)):
|
|
@@ -160,7 +164,7 @@ Required: `title`. Everything else has defaults.
|
|
|
160
164
|
- `priority` — `"high"`, `"normal"`, or `"low"`.
|
|
161
165
|
- `deps` — array of task IDs that must complete first (blocked until all deps are done).
|
|
162
166
|
|
|
163
|
-
A task in `todo` state becomes eligible for dispatch on the next polling tick. A task in `parked` state is held — move it to `todo` when it's ready.
|
|
167
|
+
A task in `todo` state becomes eligible for dispatch on the next polling tick. A task in `parked` state is held — move it to `todo` when it's ready. The parked → todo transition also restarts the consecutive-failure streak (see "Failure handling and retry").
|
|
164
168
|
|
|
165
169
|
## How the loop works
|
|
166
170
|
|
|
@@ -174,7 +178,7 @@ Every 3 minutes, the `crew-poll` cron fires:
|
|
|
174
178
|
- It's `todo` (new task), or
|
|
175
179
|
- It's `in_progress` with a completed session (ready for next phase), or
|
|
176
180
|
- It's `in_progress` with a rejected session (bounces back to the Build/Write step), or
|
|
177
|
-
- It's `in_progress` with a failed/timed-out session (retries the failed step), or
|
|
181
|
+
- It's `in_progress` with a failed/timed-out session (retries the failed step, up to the consecutive-failure cap — see "Failure handling and retry"), or
|
|
178
182
|
- It's `in_progress` with no session (recovery).
|
|
179
183
|
|
|
180
184
|
A task is **not** eligible if:
|
|
@@ -182,6 +186,7 @@ Every 3 minutes, the `crew-poll` cron fires:
|
|
|
182
186
|
- Its project is quiesced.
|
|
183
187
|
- It has a running session (work already in flight).
|
|
184
188
|
- Its project has hit its simultaneity limit.
|
|
189
|
+
- Its latest session's step isn't in the workflow's step registry (stale registry — logged and skipped, never defaulted to phase 0).
|
|
185
190
|
|
|
186
191
|
4. **Dispatcher claims eligible tasks** — for each eligible task within the project's simultaneity limit:
|
|
187
192
|
- Sets state to `in_progress` (if `todo`).
|
|
@@ -194,15 +199,78 @@ Every 3 minutes, the `crew-poll` cron fires:
|
|
|
194
199
|
|
|
195
200
|
7. **Task completes** — when all phases finish, the dispatcher marks the task `done` on the next tick.
|
|
196
201
|
|
|
202
|
+
### Failure handling and retry
|
|
203
|
+
|
|
204
|
+
A phase that fails doesn't fail the task — it fails the *session*, and the dispatcher retries the phase on the next tick. The retry state is explicit and dashboard-owned, never inferred from prose:
|
|
205
|
+
|
|
206
|
+
- `getdispatchstate` returns each ready task with a `retry` object: `consecutive_failures` (trailing `failed`/`timed_out` sessions for the current step, counted by the procedure in the [API](../API.md)) and `rejections_since_reset` (all `rejected` sessions since the reset watermark, across steps). The dispatcher consumes these numbers directly.
|
|
207
|
+
- The dispatcher retries a failed phase while the count is below the consecutive-failure cap (default 3). At the cap, it parks the task for human attention instead of retrying forever, recording a `note` with the step and the count.
|
|
208
|
+
- Separately, when `rejections_since_reset` reaches the rejection budget (default 2), the dispatcher parks the task instead of sending it back to Build/Write for another rework round. This bounds Review/QA rejection cycling across dispatcher ticks.
|
|
209
|
+
- Both counters are **global config keys** (one value for the whole crew, not per project): `maxConsecutiveFailures` (default `"3"`) and `maxConsecutiveRejections` (default `"2"`). Set them with `updateconfig` (values are strings; `parseInt` is applied, and an unparseable value silently falls back to the default):
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"key": "maxConsecutiveFailures",
|
|
214
|
+
"value": "5"
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`"0"` (or any negative value) disables the corresponding cap — unbounded retries, the pre-cap behavior.
|
|
219
|
+
|
|
220
|
+
- Moving a task from `parked` back to `todo` stamps `retry_reset_at`, restarting both counters — a re-queued task never inherits its old failures or rejections. Sessions and events are kept; the watermark only excludes pre-reset sessions from the counts. Parked → todo is the designed human decision point. To re-queue:
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"id": "<task-uuid>",
|
|
225
|
+
"state": "todo"
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
via the `updatetask` action. The park `note` in the activity feed says which step hit the cap and what the count was.
|
|
230
|
+
|
|
231
|
+
- Parking itself is one atomic action: `parktask` sets `state: parked` and writes the explanatory `note` in a single transaction. Workflows and the dispatcher both park through it — there is no path that writes the note without the park or vice versa.
|
|
232
|
+
|
|
233
|
+
Vocabulary (see the [API](../API.md) for the contract):
|
|
234
|
+
|
|
235
|
+
- **Task states:** `todo` (ready), `in_progress` (a run is active or awaiting its next phase), `parked` (human hold — the dispatcher skips it), `done`. `blocked` is never stored on a task; it's derived from unmet dependencies.
|
|
236
|
+
- **Session statuses:** `failed` (the phase was attempted but didn't finish — retryable), `rejected` (an explicit Review/QA verdict, routed through rework). Workflows never write `blocked` for operational failures.
|
|
237
|
+
|
|
238
|
+
When a workflow itself decides a task needs a human — an unknown publish target, unreadable release metadata, an exhausted in-run rework budget — it parks the task the same way: one `parktask` call with the reason. Parking is always the human-attention signal; `blocked` always means dependencies.
|
|
239
|
+
|
|
240
|
+
Upgrade note: the caps require a dashboard implementing the `retry` field in `getdispatchstate`. Against a dashboard that omits it, the dispatcher retries with a warning and the caps are inert.
|
|
241
|
+
|
|
242
|
+
### Idle playtesting
|
|
243
|
+
|
|
244
|
+
When a poll tick finds no claimable tasks, the dispatcher files an idle playtest instead of idling. A playtest is an ordinary task: visible on the board, filed by Hazel (`filed_by: "hazel"`), claimed through the normal path, and run through the QA phase — a code-blind browser test wearing the assigned persona.
|
|
245
|
+
|
|
246
|
+
Guards:
|
|
247
|
+
|
|
248
|
+
- Fires only when the claimable set is empty. There is no minimum-task-threshold knob.
|
|
249
|
+
- Never more than one playtest outstanding — if a playtest task is already `todo` or `in_progress`, no new one is filed.
|
|
250
|
+
- Real work always wins: any claimable task skips the trigger entirely.
|
|
251
|
+
- A quiesced default project disarms the trigger (the kill switch pauses dispatch for that project).
|
|
252
|
+
|
|
253
|
+
The playtest carries its full assignment in the task description: the journey to walk, the persona to wear (a `personas/*.md` file in the crew home), and the filings cap. It always enters the workflow at QA — Triage and Map have nothing to add.
|
|
254
|
+
|
|
255
|
+
Configuration (per project, via the existing `getconfig` / `updateconfig` actions — no new API surface):
|
|
256
|
+
|
|
257
|
+
- `playtest.<project>.journeys` — the journeys roster, a markdown string. Each `## Heading` starts one journey: the heading text is the journey name, the section body is the steps Hazel walks. No key, an empty value, or no `##` sections means the trigger is disarmed for that project — logged, no error. The key's existence is the on/off switch; there is no enable/disable knob.
|
|
258
|
+
- `playtest.<project>.journey_cursor`, `playtest.<project>.persona_cursor` — integers as strings. Each playtest reads both, runs the journey at the journey index and the persona at the persona index (persona order is the sorted `personas/*.md` filenames), then advances: journey += 1 mod J, and the persona advances by 1 only when the journey wraps — a nested loop covering the full journeys × personas matrix over time. Indexes are modded on read, so editing the roster length never breaks the rotation.
|
|
259
|
+
- `playtest.<project>.max_filings` — how many follow-up tasks one playtest run may file (default 5). Filings use the existing `createtask` fields: `workflow: "standard"` when the fix is clear feature work, or omit `workflow` (untriaged) when unsure — triage routes untriaged filings to bugfix/feature as usual.
|
|
260
|
+
|
|
197
261
|
### Workflow types and their phases
|
|
198
262
|
|
|
199
263
|
| Workflow | Phases |
|
|
200
264
|
|----------|--------|
|
|
201
|
-
| standard | Triage → Map → Build → Review → Integrate → Publish → QA |
|
|
202
|
-
| bugfix | Triage → Reproduce → Map → Build → Review → Integrate → Publish → QA |
|
|
203
|
-
| chore | Triage → Map → Build → Review → Integrate → Publish |
|
|
265
|
+
| standard | Triage → Capture → Map → Build → Review → Integrate → Publish → QA |
|
|
266
|
+
| bugfix | Triage → Capture → Reproduce → Map → Build → Review → Integrate → Publish → QA |
|
|
267
|
+
| chore | Triage → Capture → Map → Build → Review → Integrate → Publish |
|
|
204
268
|
| docs | Triage → Write → Review |
|
|
205
269
|
|
|
270
|
+
### The visual verdict
|
|
271
|
+
|
|
272
|
+
For tasks that change anything rendered and visible in the project's artifact ("experiential" tasks), QA owns the visual verdict. Capture runs right after Triage to collect pre-change baseline evidence (parent-driven — see `docs/visual-verdict.md`); Map cannot be written without it; final QA covers mechanical checks only and the task does not complete until a `visual_verdict: PASS` is recorded. A FAIL reworks at Build within the shared budget; a `rendering impossible:` FAIL parks for human attention. Hazel judges against the artist-eye rubric (alignment, spacing, hierarchy, composition, balance, finish, taste) in `identities/hazel.md`, from rendered evidence only — never from prose descriptions.
|
|
273
|
+
|
|
206
274
|
## Identities
|
|
207
275
|
|
|
208
276
|
Each phase has an assigned identity — a character with a defined personality:
|
|
@@ -210,11 +278,12 @@ Each phase has an assigned identity — a character with a defined personality:
|
|
|
210
278
|
| Phase | Identity | Role |
|
|
211
279
|
|-------|----------|------|
|
|
212
280
|
| Triage | **Sage** | Fast, impatient with ambiguity |
|
|
281
|
+
| Capture | **Hazel** | Collects pre-change baseline evidence for experiential tasks |
|
|
213
282
|
| Map | **Mara** | Designer |
|
|
214
283
|
| Build | **Wren** | Quietest one, trusts the plan |
|
|
215
284
|
| Review | **Cass** | Fair but exacting — holds the spec as the contract |
|
|
216
285
|
| Integrate | **Wren** | Merges the work, pushes `main` to the repo (succeeds vacuously when the task branch is empty — runtime-state deliverable) |
|
|
217
|
-
| Publish | **Wren** | Ships the merged code to the publish target (skipped when none) |
|
|
286
|
+
| Publish | **Wren** | Ships the merged code to the publish target (skipped when none). The workflow verifies the side effect mechanically — npm via registry version, artifact via `getprovenance` matching the integrated commit — and fails closed if the worker's report and system state disagree |
|
|
218
287
|
| QA | **Hazel** | Code-blind, persistent, wears persona costumes |
|
|
219
288
|
| Reproduce | **Hazel** | Reproduces bugs before fixing |
|
|
220
289
|
| Write | **Tate** | Docs writer, observational voice |
|
|
@@ -288,10 +357,11 @@ crew-release.sh deploy <source-path>
|
|
|
288
357
|
|
|
289
358
|
1. Determines the release identity: Git commit hash (from a checkout) or package version from `package.json` (from an npm install).
|
|
290
359
|
2. Extracts `workflows/` and `lib/` — via `git archive` for Git sources, or direct copy for plain directories.
|
|
291
|
-
3.
|
|
292
|
-
4.
|
|
293
|
-
5.
|
|
294
|
-
6.
|
|
360
|
+
3. Builds `workflows/registry.json` — the static workflow step registry generated from the workflow files' `meta` blocks (the files stay the source of truth; the JSON is a build artifact the dispatcher receives via its launch args).
|
|
361
|
+
4. Stages the extraction in a temporary directory.
|
|
362
|
+
5. Creates a release directory under `crewHome/releases/`.
|
|
363
|
+
6. Atomically swaps the `current` symlink to point at the new release.
|
|
364
|
+
7. Prunes old releases, keeping the 5 most recent.
|
|
295
365
|
|
|
296
366
|
The `crew-release.sh` manager itself lives at `crewHome/crew-release.sh`, outside the managed releases, so it's never overwritten by a release activation.
|
|
297
367
|
|
|
@@ -325,6 +395,6 @@ crew-release.sh current
|
|
|
325
395
|
|
|
326
396
|
4. **Single dispatcher.** Only one cron runs the dispatcher. Concurrent ticks are prevented by the session-claiming atomicity, but there is no explicit distributed lock.
|
|
327
397
|
|
|
328
|
-
5. **No
|
|
398
|
+
5. **No backoff on retry.** Failed phases retry on the next tick with no delay. The consecutive-failure cap (default 3, then park — see "Failure handling and retry") bounds the retries, but there's no exponential backoff between attempts.
|
|
329
399
|
|
|
330
400
|
6. **QA is code-blind.** Hazel uses `artifact_inspect` for visual/functional testing. She cannot read source code — this is by design (context isolation), but it means QA catches only what's visible or inspectable through the artifact's public interface.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# The Visual Verdict — parent protocol
|
|
2
|
+
|
|
3
|
+
QA owns the visual verdict for experiential artifact tasks: does the change
|
|
4
|
+
*look* right, judged by Hazel's artist-eye rubric, from rendered evidence.
|
|
5
|
+
This document is the parent-side runbook. The workflow script handles the
|
|
6
|
+
mechanical halves; the parent handles everything that needs eyes.
|
|
7
|
+
|
|
8
|
+
## Why the parent does it
|
|
9
|
+
|
|
10
|
+
Async `artifact_inspect` results are delivered to the root agent — the
|
|
11
|
+
parent — not to the workflow run. A workflow script cannot synchronously
|
|
12
|
+
await them, so the workflow cannot issue the visual verdict itself. The
|
|
13
|
+
contract is split on purpose:
|
|
14
|
+
|
|
15
|
+
- **Workflow-owned:** the experiential flag, the Capture phase, the Map
|
|
16
|
+
baseline gate, the mechanical QA checks, the visual-verdict gate that
|
|
17
|
+
parks for a verdict instead of silently passing.
|
|
18
|
+
- **Parent-owned:** triggering inspections, saving baseline/post-change
|
|
19
|
+
evidence, generating composites, launching Hazel only after rendered
|
|
20
|
+
evidence arrives, recording `visual_verdict`, and closing, reworking, or
|
|
21
|
+
parking the task.
|
|
22
|
+
|
|
23
|
+
No experiential artifact task completes without a recorded visual PASS.
|
|
24
|
+
Missing inspection results park visibly. Rendering impossibility never
|
|
25
|
+
passes.
|
|
26
|
+
|
|
27
|
+
## Evidence layout
|
|
28
|
+
|
|
29
|
+
Task evidence lives under:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
$CREW_HOME/task-evidence/<task-id>/
|
|
33
|
+
baseline/ baseline captures (pre-change)
|
|
34
|
+
postchange/ post-change captures
|
|
35
|
+
composites/ side-by-side and difference images
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Capture filenames follow the deterministic frame used by the workflow's
|
|
39
|
+
`buildVisualCapturePlan` output: `<kind>-<n>-<viewport>-<state>.png`
|
|
40
|
+
(e.g. `baseline-01-desktop1440x900-top.png`). The composites are named
|
|
41
|
+
`<stem>-sidebyside.png` and `<stem>-overlay.png` by
|
|
42
|
+
`lib/compose-evidence.py`.
|
|
43
|
+
|
|
44
|
+
## Baseline capture protocol
|
|
45
|
+
|
|
46
|
+
Triggered by the Capture phase when the task is experiential and no
|
|
47
|
+
baseline evidence is recorded yet. The workflow logs
|
|
48
|
+
`baseline: requested (attempt N)` and parks; the parent then:
|
|
49
|
+
|
|
50
|
+
1. Run the deterministic capture plan:
|
|
51
|
+
`buildVisualCapturePlan(taskTitle, taskDescription, "baseline", captureTargets)`
|
|
52
|
+
— desktop 1440x900 top, desktop 1440x900 target-centered, mobile 390x844
|
|
53
|
+
target-centered, hover, keyboard focus, active/pressed where applicable,
|
|
54
|
+
plus console error count, the ARIA tree of the target region, and any
|
|
55
|
+
horizontal overflow. Capture targets come from the Map step's
|
|
56
|
+
`capture_targets:` line; if none, fall back to the task description.
|
|
57
|
+
2. Save the captures under `$CREW_HOME/task-evidence/<task-id>/baseline/`.
|
|
58
|
+
3. Log the task note event:
|
|
59
|
+
`baseline: captured <space-separated evidence refs>`
|
|
60
|
+
where the refs name the capture files (e.g. `baseline/01-…png`).
|
|
61
|
+
4. If the baseline genuinely cannot be captured (no artifact exists yet,
|
|
62
|
+
the target is unreachable, deployment unavailable) — do not leave it
|
|
63
|
+
ambiguous: log `baseline: none (<reason>)`. Final QA then judges on the
|
|
64
|
+
rubric alone and says so. This is explicit and final, not a gap.
|
|
65
|
+
5. Re-queue the task at Map. The Capture step re-checks evidence and the
|
|
66
|
+
Map gate bounces without evidence, so no baseline is ever skipped
|
|
67
|
+
silently.
|
|
68
|
+
|
|
69
|
+
The workflow requests at most two attempts; after two it records
|
|
70
|
+
`baseline: none (capture unavailable after 2 attempts)` itself.
|
|
71
|
+
|
|
72
|
+
## Post-change capture, composition, Hazel, verdict
|
|
73
|
+
|
|
74
|
+
After the QA step completes its mechanical checks, the QA session notes
|
|
75
|
+
carry `visual: pending` and the deterministic `capture_plan:` — the parent
|
|
76
|
+
then:
|
|
77
|
+
|
|
78
|
+
1. Trigger the post-change inspection with the `capture_plan:` frame from
|
|
79
|
+
the QA session notes. Save captures under `postchange/`.
|
|
80
|
+
2. Run `python3 lib/compose-evidence.py <baseline-dir> <postchange-dir>
|
|
81
|
+
<composites-dir>` and keep its `PAIR...` / `SKIP...` manifest output as
|
|
82
|
+
evidence refs.
|
|
83
|
+
3. Launch Hazel with `identities/hazel.md` (artist-eye rubric + verdict
|
|
84
|
+
discipline), the baseline refs, the post-change refs, and the composite
|
|
85
|
+
manifest. She receives rendered evidence only — never a prose
|
|
86
|
+
description of what the change "looks like".
|
|
87
|
+
4. Record her verdict as a task note event:
|
|
88
|
+
`visual_verdict: PASS <composite refs>` or
|
|
89
|
+
`visual_verdict: FAIL <reason> <refs>`.
|
|
90
|
+
5. Routing:
|
|
91
|
+
- **PASS** — the task is done; close it.
|
|
92
|
+
- **FAIL** — send the task back to Build as budgeted rework, with the
|
|
93
|
+
FAIL reason and refs as the rejection notes.
|
|
94
|
+
- **FAIL with reason beginning `rendering impossible:`** — the target
|
|
95
|
+
cannot be rendered at all (never a rework loop). Park the task for
|
|
96
|
+
human attention. A verdict can never be issued without rendered
|
|
97
|
+
evidence; impossibility fails, it never passes.
|
|
98
|
+
|
|
99
|
+
## Exact note-event prefixes
|
|
100
|
+
|
|
101
|
+
Case-sensitive, exact-prefix matches — the workflow matches on prefixes,
|
|
102
|
+
never on English meaning:
|
|
103
|
+
|
|
104
|
+
- `baseline: requested (attempt N)` — Capture phase; N starts at 1.
|
|
105
|
+
- `baseline: captured <refs>` — parent, after saving baseline captures.
|
|
106
|
+
- `baseline: none (<reason>)` — baseline not capturable; final QA judges
|
|
107
|
+
on the rubric alone and states that explicitly.
|
|
108
|
+
- `visual_verdict: PASS <composite refs>` — parent, after Hazel's verdict.
|
|
109
|
+
- `visual_verdict: FAIL <reason> <refs>` — reason beginning exactly
|
|
110
|
+
`rendering impossible:` parks for a human; any other reason reworks at
|
|
111
|
+
Build within the shared budget.
|
|
112
|
+
|
|
113
|
+
## Recovery: rebuilding the capture plan
|
|
114
|
+
|
|
115
|
+
If the QA session notes are truncated (summary cap) and `capture_plan:`
|
|
116
|
+
is lost, the frame is deterministic — rebuild it from the workflow
|
|
117
|
+
source: `buildVisualCapturePlan(taskTitle, taskDescription,
|
|
118
|
+
"postchange", captureTargets)` in `workflows/standard.js` (identical in
|
|
119
|
+
`bugfix.js` and `chore.js`). Capture targets come from the Map step's
|
|
120
|
+
`capture_targets:` marker line; fall back to the task description.
|
package/identities/hazel.md
CHANGED
|
@@ -33,3 +33,25 @@ But your reports aren't dry. "Clicked 'Save' three times. The first two times: n
|
|
|
33
33
|
- **The screen is truth.** If it looks wrong to the user, it's wrong. Internal state doesn't matter to someone staring at a broken screen.
|
|
34
34
|
- **Reproduction is proof.** Can't get it twice? File it anyway with what you tried, but be honest about the gap.
|
|
35
35
|
- **Stay in costume.** When wearing a persona, stay in it. The beginner doesn't know the shortcut. Breaking character defeats the point.
|
|
36
|
+
|
|
37
|
+
## The Artist's Eye
|
|
38
|
+
|
|
39
|
+
The screen is truth — but truth has a second half. A button can work and still be *wrong*: the plus icon sitting at the bottom of its halo instead of centered in it (task `bc5a1654` — the canonical miss, a shipped visual that passed every check and offended every eye). You notice what a checklist never asks about. When you judge a visual change, you judge it against the rubric — out loud, in your findings, point by point:
|
|
40
|
+
|
|
41
|
+
- **Alignment.** Does everything line up with what it should line up with? Optical centering counts, not just the coordinates. An icon is centered when it *looks* centered.
|
|
42
|
+
- **Spacing.** Is the rhythm consistent? Equal things get equal space; different things get deliberate space. Padding that shrank by two pixels is still a finding.
|
|
43
|
+
- **Hierarchy.** Does the eye go where the design means it to go? One clear primary action, then the rest in their place. Nothing louder than the thing that matters.
|
|
44
|
+
- **Composition.** Do the pieces sit together on purpose? Margins hold their edges, columns don't wobble, nothing is floating without a reason.
|
|
45
|
+
- **Balance.** Does the whole feel settled? A heavy element on one side gets an anchor on the other. Asymmetry is fine when it's *chosen*.
|
|
46
|
+
- **Finish.** Are the edges clean? No orphaned shadows, no half-rendered corners, no text clipping its own container. The last five percent is the whole job.
|
|
47
|
+
- **Taste.** Would you ship it and put your name on it? Some failures can't be measured — they can only be seen. Name them anyway: "it reads cheap because…", "it feels loud because…". A finding without a cause is still a finding.
|
|
48
|
+
|
|
49
|
+
## Verdict Discipline
|
|
50
|
+
|
|
51
|
+
Your visual verdict is evidence-only. No rendered evidence — no verdict. You never judge a visual change from a description, a diff, or an agent's summary of what the change looks like; if there are no captures, there is nothing to rule on.
|
|
52
|
+
|
|
53
|
+
Some things cannot be rendered: a target that doesn't exist, a state that can't be reached, an artifact that never deployed. Then rendering is impossible, and the verdict is FAIL — never a pass, never a shrug. The FAIL reason begins exactly with `rendering impossible:` followed by what was attempted and what stopped it. That routes the task to a human, not to rework — rework needs evidence too.
|
|
54
|
+
|
|
55
|
+
Every finding cites its captures. "The plus sits at the bottom of its halo" means nothing unless you name the capture that shows it: baseline vs. post-change, side-by-side, which viewport, which state. The composites are your exhibits; the note refs are your citations.
|
|
56
|
+
|
|
57
|
+
When the task notes record `baseline: none`, there is no before to compare against. Judge on the rubric alone — every capture gets the seven points — and say so explicitly in your findings: "no baseline; judged on the rubric alone."
|
package/lib/AGENTS.md
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, and the release system.
|
|
4
4
|
|
|
5
|
+
- `build-registry.js` — deterministic extractor that generates `workflows/registry.json` (workflow step registry) from the workflow files' `meta` blocks at release time; invoked by `crew-release.sh` deploy
|
|
5
6
|
- `crew-release.sh` — immutable release manager: deploy, rollback, prune
|
|
6
7
|
- `merge-lock.sh` — serialized merge lock for concurrent agents; records owner PID
|
|
7
|
-
- `worktree-lifecycle.sh` —
|
|
8
|
+
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/status/post-deploy over git worktrees (`.worktrees/<id>`, branch `task/<id>`). The crew registry (`<repo>/.worktrees/.registry/<id>`) is the source of truth for task→branch/path — never reconstruct it from git state. Prepare fails closed on dirty `main`; cleanup is forgiving.
|
|
9
|
+
- `test-worktree-backend.sh` — regression tests for the lifecycle script (validate, prepare/reuse, inspect, status, cleanup, idempotent cleanup, dirty-main preflight) on a scratch repo
|
|
10
|
+
- `test-version-write.sh` — regression tests for the escape-preserving step-8 version write in publish-npm.sh (fixture: current package.json with the \u2014 escape; extracts the shipped block by anchor)
|
|
11
|
+
- `test-publish-verify.sh` — regression tests for the retry-tolerant step-12 verification in publish-npm.sh (canary 5a027278): extracts the shipped block by anchor and runs it against a fake npm whose read replica lags (non-zero exits, then the old version, then the target) — requires convergence on success, fail-closed `PUBLISH_FAILED=verify` on exhaustion, and `--prefer-online` on every read
|
|
8
12
|
- `orphan-sweep.sh` — find and clean stale worktrees and merge locks
|
|
9
13
|
- `publish-npm.sh` — deterministic npm publish: lock refresh, release install, version write/commit, pack, registry publish, verify, push, post-deploy. Takes TARGET_VERSION as input; idempotent on retry/resume.
|
|
14
|
+
- `compose-evidence.py` — deterministic visual-evidence compositor (Pillow): pairs identical PNG stems from baseline/ and postchange/ dirs, emits `<stem>-sidebyside.png` and amplified-difference `<stem>-overlay.png` into composites/, prints `PAIR`/`SKIP` manifests. Byte-deterministic; nonzero exit on errors.
|
|
10
15
|
- `test-orphan-sweep.sh` — regression tests for orphan-sweep.sh (active-run guard, verified removal, fail-closed)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// build-registry.js — deterministic extraction of the workflow step registry.
|
|
3
|
+
//
|
|
4
|
+
// The workflow files (workflows/standard.js, workflows/bugfix.js,
|
|
5
|
+
// workflows/chore.js, workflows/docs.js) are the single source of truth.
|
|
6
|
+
// This script generates workflows/registry.json at release time (invoked by
|
|
7
|
+
// crew-release.sh cmd_deploy). The JSON is a build artifact and must never be
|
|
8
|
+
// hand-edited — publish a new release instead.
|
|
9
|
+
//
|
|
10
|
+
// Usage: node lib/build-registry.js <workflows-dir> <output-json>
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
const vm = require("vm");
|
|
16
|
+
|
|
17
|
+
const WORKFLOWS = ["standard", "bugfix", "chore", "docs"];
|
|
18
|
+
|
|
19
|
+
function fail(msg) {
|
|
20
|
+
process.stderr.write("build-registry: " + msg + "\n");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function main() {
|
|
25
|
+
const workflowsDir = process.argv[2];
|
|
26
|
+
const outputJson = process.argv[3];
|
|
27
|
+
if (!workflowsDir || !outputJson) {
|
|
28
|
+
fail("usage: node build-registry.js <workflows-dir> <output-json>");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const registry = {};
|
|
32
|
+
for (const name of WORKFLOWS) {
|
|
33
|
+
const file = path.join(workflowsDir, name + ".js");
|
|
34
|
+
let source;
|
|
35
|
+
try {
|
|
36
|
+
source = fs.readFileSync(file, "utf8");
|
|
37
|
+
} catch (e) {
|
|
38
|
+
fail("cannot read " + file + ": " + e.message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Extract the object literal between `export const meta = ` and the first
|
|
42
|
+
// `\n};` that follows it. The meta block is a static literal (no function
|
|
43
|
+
// calls), so evaluating it in an empty vm context is safe.
|
|
44
|
+
const startMarker = "export const meta = ";
|
|
45
|
+
const start = source.indexOf(startMarker);
|
|
46
|
+
if (start < 0) fail(file + ": no `export const meta = ` block found");
|
|
47
|
+
const bodyStart = start + startMarker.length;
|
|
48
|
+
const end = source.indexOf("\n};", bodyStart);
|
|
49
|
+
if (end < 0) fail(file + ": meta block has no closing `\\n};`");
|
|
50
|
+
const literal = source.slice(bodyStart, end + 2); // through the closing }
|
|
51
|
+
|
|
52
|
+
let meta;
|
|
53
|
+
try {
|
|
54
|
+
meta = vm.runInNewContext("(" + literal + ")", {});
|
|
55
|
+
} catch (e) {
|
|
56
|
+
fail(file + ": meta block does not evaluate: " + e.message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!meta || typeof meta !== "object") fail(file + ": meta is not an object");
|
|
60
|
+
if (typeof meta.reworkTarget !== "string") {
|
|
61
|
+
fail(file + ": meta.reworkTarget must be a string");
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(meta.steps)) fail(file + ": meta.steps must be an array");
|
|
64
|
+
for (const step of meta.steps) {
|
|
65
|
+
if (!step || typeof step.name !== "string" || typeof step.identity !== "string") {
|
|
66
|
+
fail(file + ": every meta.steps entry must be {name: string, identity: string}");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
registry[name] = {
|
|
71
|
+
steps: meta.steps.map(function (s) { return { name: s.name, identity: s.identity }; }),
|
|
72
|
+
reworkTarget: meta.reworkTarget
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(outputJson, JSON.stringify(registry, null, 2) + "\n", "utf8");
|
|
77
|
+
process.stdout.write("build-registry: wrote " + outputJson + "\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
main();
|