muse-crew 0.7.10 → 0.7.12

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/API.md CHANGED
@@ -50,10 +50,11 @@ Atomically claim a task for a workflow step. Used by the dispatcher and by workf
50
50
  | `identity` | string (1–80) | yes | The crew identity performing the step (e.g. `sage`, `wren`) |
51
51
  | `step` | string (1–120) | no | The workflow phase name |
52
52
  | `notes` | string (≤ 3000) | no | Defaults to `""` |
53
+ | `expected_next_phase` | string (1–120) | no | The `next_phase` routing the dispatcher launched this run for. When the claim wins, the matching `next_phase` is cleared in the **same transaction** as the winning session insert — there is no claim→consume window for a platform death to replay the routing through. Only an exact match clears; a stale or superseded routing survives. Returned as `next_phase_consumed: true/false` on the winning response (absent when the field was not passed). A losing claim never touches `next_phase`. |
53
54
 
54
55
  Returns one of:
55
56
 
56
- - `{ "ok": true, "claimed": true, "session_id": "<uuid>", "session": {...} }`
57
+ - `{ "ok": true, "claimed": true, "session_id": "<uuid>", "session": {...}, "next_phase_consumed": true/false }`
57
58
  - `{ "ok": true, "claimed": false, "reason": "already_claimed", "existing_session_id": "<uuid>" }`
58
59
 
59
60
  `claimed` is the single field callers branch on. `reason` is present only when `claimed` is false.
@@ -66,9 +67,22 @@ Send a stuck or failed task to a specific workflow phase for recovery.
66
67
  |-------|------|----------|-------|
67
68
  | `task_id` | uuid | yes | |
68
69
  | `action` | `"send_to"` | yes | Only `send_to` is supported |
69
- | `target_phase` | string (1–120) | yes | The phase to send the task to |
70
+ | `target_phase` | string (1–120) | yes | The phase to send the task to — must be a phase of the task's workflow (validated against the workflow's phase registry; unknown phases fail closed) |
70
71
  | `updated_description` | string (≤ 5000) | no | Optional revised description |
71
72
 
73
+ Recovery intent, not a verdict: accepts a latest session of `failed`, `timed_out`, or `stalled` — or no session at all (pre-claim platform death). Rejects a `running` latest session (work in flight) and terminal task states. Sets `tasks.next_phase` to the target and moves `parked`/`todo` tasks to `in_progress`. Inserts **no** session: recovery never consumes retry budget. The dispatcher routes the task to `next_phase` one-shot (bypassing the retry cap); the launched workflow's successful self-claim clears it atomically (`claim-task` with `expected_next_phase`). Returns the task and `valid_phases`.
74
+
75
+ ### `consume-next-phase`
76
+
77
+ One-shot consumption of a routed `next_phase`. Retained as an idempotent public helper for out-of-band recovery tooling — workflows no longer call this; `claim-task` with `expected_next_phase` clears the routed `next_phase` in the same transaction as the winning session insert, so the old claim→consume death window is closed.
78
+
79
+ | Field | Type | Required | Notes |
80
+ |-------|------|----------|-------|
81
+ | `task_id` | uuid | yes | |
82
+ | `expected` | string (1–120) | yes | The `next_phase` value the dispatcher routed on |
83
+
84
+ Clears `tasks.next_phase` only when the stored value still matches `expected` (atomic conditional update). Returns `{ "consumed": true/false }`. A newer `recovertask` written in the race window survives a stale consumer.
85
+
72
86
  ### `parktask`
73
87
 
74
88
  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.
@@ -80,6 +94,16 @@ Atomically park a task for human attention: sets `state` to `parked`, writes the
80
94
 
81
95
  Returns `{ "ok": true, "task": {...}, "settled_sessions": <n> }` with the updated task record and the count of sessions settled (`0` when none were running).
82
96
 
97
+ ### `resolvepublishunknown`
98
+
99
+ Recovery contract for publish attempts parked with an `unknown` outcome (2026-09-14): the rebuild trigger's child failed structured closeout and the in-flight-only build-state poll could not see the completed build, so the workflow parked fail-closed — but the edit may still have gone through. When a platform audit build completed inside the publish window (between the Integrate-completion event and the unknown-outcome park), this action routes the task to the parent's independent content verification WITHOUT re-issuing the edit and WITHOUT stamping provenance. The original unknown ledger entry and park event are preserved; the resolution is appended to the publish ledger (`outcome: "unknown-resolved"`) and to the event log, and a `publish: verification-requested <commit>` note is written so `scan-verification-pending` claims the task on the next tick. The parent's read-back ([publish verification](docs/publish-verification.md)) remains the real verification and can still fail terminally. Cases that stay parked with `{ "resolved": false, ... }`: task not parked, latest ledger outcome not `unknown`, no usable commit on the unknown entry, no audit build inside the publish window, unobservable publish window (missing Integrate-completion or park event), or an already-resolved attempt (idempotent).
100
+
101
+ | Field | Type | Required | Notes |
102
+ |-------|------|----------|-------|
103
+ | `task_id` | uuid | yes | Must be parked with a latest publish-ledger outcome of `unknown` |
104
+
105
+ Returns `{ "resolved": true, "task_id", "commit", "evidence_dir", "audit_report_ok" }` on success, or `{ "resolved": false, "reason" }` when the recovery preconditions are not met.
106
+
83
107
  ---
84
108
 
85
109
  ## Sessions
@@ -296,7 +320,7 @@ The platform records workflow run status in `runtime.workflow_runs` — separate
296
320
  Two layers, in order of preference:
297
321
 
298
322
  1. **Prevention — the launcher stays alive.** Async workflow `agent()` authorization is tied to the launcher's lifetime: if the cron tick ends while a workflow is still running, the workflow's next `agent()` call fails. So the poll tick runs with a 90-minute execution timeout (`timeout_secs: 5400` in `seed/crons.json`) and its Step 5 monitor stays alive until every launched run reaches a terminal state. A launcher that outlives its runs never triggers the failure in the first place.
299
- 2. **Recovery — per-tick correlation through the durable mapping.** If the launcher dies early anyway (platform kill, cell recycle), the next tick's Step 0 finds the dead run, `record-platform-failure` correlates it via the `platform_run_tasks` mapping, and `retry-platform-failure` requeues the task — at most ~3 minutes later, with the ghost session settled so the dispatcher treats it as a retry candidate immediately.
323
+ 2. **Recovery — per-tick correlation through the durable mapping.** If the launcher dies early anyway (platform kill, cell recycle), the next tick's Step 0 finds the dead run, `record-platform-failure` correlates it via the `platform_run_tasks` mapping, and `retry-platform-failure` requeues the task — at most ~15 minutes later, with the ghost session settled so the dispatcher treats it as a retry candidate immediately.
300
324
 
301
325
  ### `record-platform-failure`
302
326
 
@@ -477,21 +501,19 @@ Phases whose outcome drives control flow (Build, Review, QA, Reproduce, Integrat
477
501
  Artifact Publish passes only when the parent has verified content independently:
478
502
 
479
503
  1. The workflow carries the merged diff to the artifact builder, polls the
480
- build to completion, triggers an independent `artifact_inspect` read-back
481
- of the changed regions, and parks with
504
+ build to completion, and parks with
482
505
  `publish: verification-requested <commit>` — it never stamps provenance
483
506
  and never treats the builder's applied-changes report as verification.
484
507
  The applied report is derived from the carried diff and demonstrated an
485
508
  unreliable false-negative mode (2026-09-12: `applied:[]` for a diff the
486
- builder had applied); it is logged as observation only. The read-back
487
- request carries the observed builder build identifier (`build.agent_id`)
488
- so the read-back can be correlated to the exact builder run that built
489
- this attempt.
490
- 2. The parent compares the read-back against the merged diff mechanically
491
- (every added line present, every removed line absent), correlates the
492
- builder build identifier, and stamps provenance via `set-provenance` only
493
- on a match. A mismatch, a build-id mismatch, or an unobtainable read-back
494
- never stamps; the task stays parked.
509
+ builder had applied); it is logged as observation only.
510
+ 2. The parent's verification protocol (docs/publish-verification.md) owns
511
+ the independent content confirmation. The independent read-back step is
512
+ currently unavailable: `artifact_inspect` was removed by the platform
513
+ (2026-09-14) and no agent-callable replacement exists (`artifact.inspect`
514
+ is malfunction diagnosis, not a read-back tool). Until a read-back path
515
+ exists, the parent cannot confirm content independently and the task
516
+ stays parked at `publish: verification-requested`.
495
517
 
496
518
  Verification fails closed. The loop never retries a verification and never
497
519
  issues a blind re-publish on an unknown outcome.
package/docs/guide.md CHANGED
@@ -17,7 +17,7 @@ This is the full setup and operations reference. If you're new, start with the [
17
17
  - A **release** — the first immutable snapshot of the crew's runtime code.
18
18
  - An **`.orchestration/` directory** in the crew home with identities, personas, workflow docs, and feedback conventions.
19
19
  - A **project registration** — the task service registered as its own first project.
20
- - **Cron jobs** — the scheduler state declared in `seed/crons.json`: a polling loop (every 3 minutes via Muse's scheduling, even when nobody's in the conversation). Owner is `space:<slug>`, so deleting the task service also removes the crons.
20
+ - **Cron jobs** — the scheduler state declared in `seed/crons.json`: a polling loop (every 15 minutes via Muse's scheduling, even when nobody's in the conversation). Owner is `space:<slug>`, so deleting the task service also removes the crons.
21
21
 
22
22
  The agent running in the main chat receives the dispatcher's claims and launches each task workflow. Workflows can't launch workflows, so this handoff is structural.
23
23
 
@@ -197,7 +197,7 @@ A task in `todo` state becomes eligible for dispatch on the next polling tick. A
197
197
 
198
198
  ## How the loop works
199
199
 
200
- Every 3 minutes, the `crew-poll` cron fires:
200
+ Every 15 minutes, the `crew-poll` cron fires:
201
201
 
202
202
  1. **Cron runs the dispatcher** — `crew-dispatch.js` with the task service slug and `crewHome` as arguments.
203
203
 
@@ -302,7 +302,7 @@ For tasks that change anything rendered and visible in the project's artifact ("
302
302
 
303
303
  ### Publish content verification
304
304
 
305
- The artifact builder's `applied` report is derived from the diff the workflow carries to it, so comparing the report to the diff is circular — canary run 8 (2026-09-11) stamped provenance on a hollow build and every phase went green. The workflow therefore never stamps provenance itself: after the build lands it triggers an independent `artifact_inspect` read-back of the changed regions and parks with `publish: verification-requested <commit> (build <agent_id|agent_id unobserved>)`. The parent compares the read-back against the merged diff and stamps provenance only on a match (parent-driven — see `docs/publish-verification.md`); the park message records the observed builder build identifier as `(build <agent_id|agent_id unobserved>)`, and the parent correlates the read-back's live build agent_id against it — a mismatch logs `publish: build-mismatch <commit> …`, stays parked, and is never stamped (parent-driven — see `docs/publish-verification.md` step 4b). QA's provenance check then enforces the stamp mechanically, so an unverified publish fails loudly in QA instead of passing silently.
305
+ The artifact builder's `applied` report is derived from the diff the workflow carries to it, so comparing the report to the diff is circular — canary run 8 (2026-09-11) stamped provenance on a hollow build and every phase went green. The workflow therefore never stamps provenance itself: after the build lands it parks with `publish: verification-requested <commit> (build <agent_id|agent_id unobserved>)`. "Landed" requires positive evidence (canary 2026-09-15, task `1d692d91`): the build poll must have positively observed our build — a running build with the receipt `agent_id`, or a completed-build record matching it. Absence of a running build is not evidence our build ran; an unobserved "done" is an unknown outcome, parked fail-closed with an append-only `unknown` ledger entry — never parked as verification-requested. The parent protocol owns the independent content confirmation (parent-driven — see `docs/publish-verification.md`); the park message records the observed builder build identifier as `(build <agent_id|agent_id unobserved>)`, and the parent correlates the read-back's live build agent_id against it — a mismatch logs `publish: build-mismatch <commit> …`, stays parked, and is never stamped (parent-driven — see `docs/publish-verification.md` step 4b). The independent read-back step is currently unavailable: `artifact_inspect` was removed by the platform (2026-09-14) and no agent-callable replacement exists (`artifact.inspect` is malfunction diagnosis, not a read-back tool), so the parent cannot confirm content independently and tasks stay parked at verification-requested until a read-back path exists. QA's provenance check then enforces the stamp mechanically, so an unverified publish fails loudly in QA instead of passing silently.
306
306
 
307
307
  ## Identities
308
308
 
@@ -424,10 +424,10 @@ crew-release.sh current
424
424
 
425
425
  2. **Stuck sessions.** If a workflow dies mid-run, its session stays `running` indefinitely and the cron skips it every tick. Manual cleanup is currently required.
426
426
 
427
- 3. **No streaming.** The polling loop checks every 3 minutes. There is no webhook or event-driven dispatch.
427
+ 3. **No streaming.** The polling loop checks every 15 minutes. There is no webhook or event-driven dispatch.
428
428
 
429
429
  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.
430
430
 
431
431
  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.
432
432
 
433
- 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.
433
+ 6. **QA is code-blind.** Hazel never reads source code — this is by design (context isolation), so QA covers the mechanical checks only: data-level effects via the Crew API, the docs gate, and the provenance check. There is currently no agent-callable visual-inspection tool (`artifact_inspect` was removed by the platform 2026-09-14; `artifact.inspect` is malfunction diagnosis, not a substitute), so visual/functional inspection of rendered output is unavailable until a capture path exists (see `docs/visual-verdict.md`).
@@ -0,0 +1,150 @@
1
+ # OODA Report
2
+
3
+ The experiential report produced by the Reproduce and QA phases' see-act loop
4
+ (2026-09-14; hardened 2026-09-15). When an agent drives the browser one step
5
+ at a time — observing each frame, deciding the next action — the complete
6
+ record of that session is the **OODA report**: the ordered
7
+ Observe/Orient/Decide/Act trace, the archived frames, and a machine-readable
8
+ terminal verdict.
9
+
10
+ ## Location
11
+
12
+ ```
13
+ $CREW_HOME/task-evidence/<taskId>/<phase>/
14
+ ooda-log.jsonl # ordered step records (every attempt preserved)
15
+ verdict.json # latest terminal verdict (what the workflow reads)
16
+ verdicts.jsonl # append-only ledger: every attempt's verdict, never overwritten
17
+ 001-shot-desktop.png
18
+ 002-click-desktop.png
19
+ ...
20
+ ```
21
+
22
+ `<phase>` is `repro` (bugfix Reproduce), `postchange` (standard QA, bugfix QA).
23
+
24
+ ## ooda-log.jsonl
25
+
26
+ One JSON object per line, appended after every browser action via
27
+ `lib/append-ooda-step.js`:
28
+
29
+ ```json
30
+ {"step":1,"attempt":"1","action":"aria","args":{},"exit":0,"screenshot":null,"observation":"Login button found in header.","ts":"..."}
31
+ {"step":2,"attempt":"1","action":"shot","args":{},"exit":0,"screenshot":"/abs/.../001-shot-desktop.png","observation":"Header renders, no console errors.","ts":"..."}
32
+ ```
33
+
34
+ Schema: `step` (positive int), `attempt` (the attempt/run identity —
35
+ `"1"`, `"2"` for successive QA loops, `"repro-1"` for a reproduce run),
36
+ `action`, `args` (object), `exit` (the see-act exit code), `screenshot`
37
+ (absolute path or null), `observation` (the agent's 1–2 sentences: what was
38
+ seen and what it implies — this is the payload, not the filename), `ts`
39
+ (optional, agent-supplied).
40
+
41
+ **Attempts never overwrite.** Step numbers are strictly monotonic *per
42
+ attempt*: an attempt's first step is 1 and each following step is exactly
43
+ previous+1. A retry starts a NEW `--attempt` in the same log — it never
44
+ renumbers or overwrites. A non-monotonic step, a gap, or a corrupt line fails
45
+ loudly (exit 2) and appends nothing.
46
+
47
+ **Actions** (2026-09-15): `aria|shot|click|scroll|type` (see-act driver) plus
48
+ the image tools — `crop|zoom|label|nup` (`lib/edit-image.py`: pixel-exact
49
+ crop, pixel-crisp zoom, caption bars, n-up grids) and `compose`
50
+ (`lib/render-html.js`: Chromium-rendered HTML compositions — the reef-qa
51
+ pattern, real typography and layout for before/after and annotated evidence).
52
+ Every frame-producing action (`shot|crop|zoom|label|nup|compose`) requires
53
+ `--screenshot`: the frame the agent looked at must be the frame it logs. A
54
+ captured-but-unlogged frame is invisible to everyone after the agent.
55
+
56
+ The writer validates and fails loudly (exit 2) on bad input. The agent never
57
+ hand-writes JSON — it supplies fields as flags.
58
+
59
+ ## verdict.json and verdicts.jsonl
60
+
61
+ Written at phase end via `lib/write-ooda-verdict.js --dir <phase-dir>
62
+ --attempt <id> --verdict <PASS|FAIL|NOT_POSSIBLE>`:
63
+
64
+ ```json
65
+ {
66
+ "verdict": "FAIL",
67
+ "attempt": "1",
68
+ "summary": "Login button missing on desktop header.",
69
+ "expected": "login button on header",
70
+ "actual": "no login control in the ARIA tree",
71
+ "missing_evidence": ["mobile viewport not checked"],
72
+ "reason": "Header renders but no login control present in the ARIA tree on desktop."
73
+ }
74
+ ```
75
+
76
+ `verdict` is `PASS`, `FAIL`, or `NOT_POSSIBLE`. `missing_evidence` lists what
77
+ was not checked — an honest gap is recorded, never hidden. Unknown/
78
+ inconclusive is neither PASS nor FAIL.
79
+
80
+ `reason` is REQUIRED and must be non-empty when the verdict is `FAIL` or
81
+ `NOT_POSSIBLE`: the writer rejects a reason-less negative verdict with exit
82
+ 2 and writes nothing. A FAIL verdict without a machine-readable reason is
83
+ not writable — the defect that parked the 2026-09-15 canary at Publish was a
84
+ bare `VERDICT: FAIL` with an all-positive report and no recorded reason.
85
+
86
+ Two records are written:
87
+
88
+ - `verdict.json` — the LATEST verdict, what the workflow closeout reads.
89
+ Overwritten on each call.
90
+ - `verdicts.jsonl` — the append-only ledger. One JSON line per verdict,
91
+ NEVER overwritten: `{seq, attempt, verdict, summary, expected, actual,
92
+ missing_evidence[], reason?, ts?}`. `seq` is assigned mechanically
93
+ (existing lines + 1). A QA retry that FAILs after an earlier PASS keeps
94
+ both — a later attempt can never silently erase an earlier verdict.
95
+
96
+ ## Screenshot archival
97
+
98
+ `SEE_ACT_ARCHIVE_DIR=<phase-dir>` (set on every see-act invocation — shell env
99
+ vars do not persist between the agent's commands) funnels every screenshot
100
+ into the phase dir automatically:
101
+
102
+ - `--out` becomes optional; the archived path is the durable record.
103
+ - Sequential names: `001-shot-desktop.png`, `002-click-mobile.png`, ...
104
+ (counter in `<dir>/.seq` — stable across the driver's one-process-per-invocation shape).
105
+ - The JSON response carries `screenshot` (what to READ) and `archived` (the durable copy).
106
+ - Explicit `--out` + archive: the file is captured to `--out` AND copied into the archive.
107
+ - An unusable archive dir is `NOT POSSIBLE` (exit 3) — lost evidence fails loudly, never silently.
108
+
109
+ No frame can be lost: Reproduce, QA, and exploratory hunting all funnel through
110
+ the same wrapper.
111
+
112
+ ## Workflow integration
113
+
114
+ - **Standard QA** (artifact): archive to `task-evidence/<task>/postchange/`,
115
+ bounded at 8 steps, logs every step, writes verdict.
116
+ - **Bugfix Reproduce**: archive to `task-evidence/<task>/repro/`, bounded at 8
117
+ steps, logs every step, writes verdict (the bug is confirmed or not).
118
+ - **Bugfix QA**: archive to `task-evidence/<task>/postchange/`, bounded at 8
119
+ steps, re-runs the reproduction steps plus surrounding views, logs every
120
+ step, writes verdict.
121
+
122
+ No nested agents — the depth-1 work agent runs the browser steps itself.
123
+
124
+ ## Reading a report
125
+
126
+ 1. Read `verdict.json` for the terminal claim.
127
+ 2. Read `ooda-log.jsonl` for the ordered trace — each observation next to its frame.
128
+ 3. Open the archived PNGs and compare against the written observations. Never trust prose alone.
129
+
130
+ The report is the evidence. A QA claim without its OODA report is an unverified claim.
131
+
132
+ ## Reading the verdict back (2026-09-15)
133
+
134
+ `lib/read-ooda-verdict.js --dir <phase-dir> --expect <PASS|FAIL>` is the
135
+ deterministic cross-checker the bugfix QA closeout runs after extracting the
136
+ prose `VERDICT:` line. It prints one JSON line to stdout and never touches
137
+ the clock or randomness:
138
+
139
+ - Exit 0, `{ok:true, verdict, reason, summary, expected, actual, attempt}` —
140
+ the record exists, parses, carries a `verdict` field, agrees with the
141
+ prose expectation, and a FAIL carries a non-empty reason.
142
+ - Exit 2, `{ok:false, code}` — `missing` (no verdict.json), `corrupt`
143
+ (unparseable or no verdict field), `contradiction` (record disagrees with
144
+ the prose line), `no_reason` (FAIL with no machine-readable reason).
145
+
146
+ The prose `VERDICT:` line routes, but `verdict.json` carries the reason the
147
+ workflow reads. On a cross-check failure the QA phase is recorded as failed
148
+ and retried at the same step — an unreasoned or contradictory verdict never
149
+ routes to rework. No LLM judges report-prose consistency; the machine only
150
+ enforces that the reason is present and the records agree.