muse-crew 0.17.1 → 0.17.3
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 +11 -0
- package/docs/decisions/composition-machinery.md +180 -0
- package/docs/decisions/publish-path.md +6 -0
- package/docs/decisions/workflow-core.md +5 -4
- package/lib/AGENTS.md +2 -1
- package/lib/bugfix/phases/build.js +168 -0
- package/lib/bugfix/phases/capture.js +170 -0
- package/lib/bugfix/phases/integrate.js +165 -0
- package/lib/bugfix/phases/map.js +129 -0
- package/lib/bugfix/phases/publish.js +592 -0
- package/lib/bugfix/phases/qa.js +356 -0
- package/lib/bugfix/phases/reproduce.js +254 -0
- package/lib/bugfix/phases/review.js +292 -0
- package/lib/bugfix/phases/triage.js +69 -0
- package/lib/chore/CONTRACT.md +181 -0
- package/lib/chore/DISPOSITION.md +98 -0
- package/lib/chore/extract.js +204 -0
- package/lib/chore/phase-lib.js +885 -0
- package/lib/chore/phases/build.js +110 -0
- package/lib/chore/phases/capture.js +82 -0
- package/lib/chore/phases/integrate.js +109 -0
- package/lib/chore/phases/map.js +81 -0
- package/lib/chore/phases/publish.js +543 -0
- package/lib/chore/phases/review.js +255 -0
- package/lib/chore/phases/triage.js +57 -0
- package/lib/chore/prompts/evidence-gatherer.js +41 -0
- package/lib/chore/prompts/evidence-gatherer.schema.json +1 -0
- package/lib/chore/prompts/tool-check.js +15 -0
- package/lib/chore/prompts/trailers.js +56 -0
- package/lib/chore/prompts/verdict-reask.js +28 -0
- package/lib/chore/prompts/verdict-reask.schema.json +1 -0
- package/lib/chore/prompts/work-agent.js +52 -0
- package/lib/chore/prompts/work-agent.schema.json +1 -0
- package/lib/chore/spawn-keys.js +44 -0
- package/lib/chore/spawn-vocab.js +87 -0
- package/lib/chore-run.js +538 -0
- package/lib/chore-tick.js +289 -0
- package/lib/compare-dispatch-shadow.js +35 -10
- package/lib/crew-api.js +273 -0
- package/lib/crew-dispatch-worker.js +27 -7
- package/lib/crew-release.sh +7 -2
- package/lib/extract.js +252 -0
- package/lib/prompts/tool-check.js +18 -0
- package/lib/prompts/trailers.js +59 -0
- package/lib/prompts/verdict-reask.js +31 -0
- package/lib/prompts/verdict-reask.schema.json +1 -0
- package/lib/prompts/work-agent.js +56 -0
- package/lib/prompts/work-agent.schema.json +1 -0
- package/lib/reap-spawns.js +407 -0
- package/lib/schema.sql +12 -1
- package/lib/spawn-keys.js +47 -0
- package/lib/spawn-step.js +572 -0
- package/lib/standard/phases/build.js +120 -0
- package/lib/standard/phases/capture.js +163 -0
- package/lib/standard/phases/integrate.js +172 -0
- package/lib/standard/phases/map.js +119 -0
- package/lib/standard/phases/publish.js +565 -0
- package/lib/standard/phases/qa.js +399 -0
- package/lib/standard/phases/review.js +281 -0
- package/lib/standard/phases/triage.js +64 -0
- package/lib/test-detached-integrate.sh +47 -0
- package/lib/workflow-driver.js +605 -0
- package/lib/workflow-lib.js +1012 -0
- package/lib/workflow-spec.js +187 -0
- package/lib/worktree-lifecycle.sh +55 -3
- package/package.json +1 -1
- package/seed/cron-body-template.md +61 -9
- package/workflows/bugfix.js +17 -17
- package/workflows/chore.js +16 -16
- package/workflows/docs.js +14 -11
- package/workflows/standard.js +16 -16
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Chore phase contracts (sandbox exit, Piece 2)
|
|
2
|
+
|
|
3
|
+
Each phase module under `lib/chore/phases/` is a pure-ish function of its
|
|
4
|
+
declared inputs. This document is the contract: inputs, outputs, state
|
|
5
|
+
read/written, the typed handoff to the next phase, and the explicit
|
|
6
|
+
transitions. No shared mutable scratch exists between phases except through
|
|
7
|
+
these declared channels.
|
|
8
|
+
|
|
9
|
+
The driver (`lib/chore-run.js`, Piece 3) invokes `runPhase(ctx)` and acts on the
|
|
10
|
+
returned result object. `ctx = { env, state }`:
|
|
11
|
+
|
|
12
|
+
- `env` — derived once per run by `deriveRunEnv()` in `phase-lib.js`: task
|
|
13
|
+
identity, repo/crew paths, pinned lifecycle script paths, publish target,
|
|
14
|
+
surface classification. Read-only.
|
|
15
|
+
- `state` — per-run driver state: `activeSessionId` (the claimed session id;
|
|
16
|
+
null until the first claim), `reworkCount` (integer, 0 on first pass),
|
|
17
|
+
`isFirstClaimVisit` (true only on the run's first phase visit),
|
|
18
|
+
`nextPhaseRouted` (the dispatcher's routed next phase, if any),
|
|
19
|
+
`experiential` (seeded by Triage's ADVANCE result), `memo` (phase scratch,
|
|
20
|
+
namespaced per phase). The driver owns it; phases read it and only mutate
|
|
21
|
+
`memo` and the driver's own `experiential`/`activeSessionId` transitions.
|
|
22
|
+
|
|
23
|
+
Result types (all phases):
|
|
24
|
+
|
|
25
|
+
| type | meaning |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `ADVANCE` | phase done; driver proceeds to `next` (a phase name) or ends the run (`next: null`) |
|
|
28
|
+
| `REWIND` | go back to an earlier phase (`next`), carrying `reason` |
|
|
29
|
+
| `PARK` / `PARK_FAILED` | task parked with `reason`; driver stops |
|
|
30
|
+
| `FAILED` | phase failed operationally; dispatcher retries under its consecutive-failure cap |
|
|
31
|
+
| `STAND_DOWN` | lost the first-claim race; driver stops quietly |
|
|
32
|
+
| `NEED_SPAWN` | the creative boundary needs a spawn; driver posts it and re-invokes later |
|
|
33
|
+
| `STANDBY` | a requested spawn is still running; driver waits |
|
|
34
|
+
|
|
35
|
+
Every phase begins with `ensureClaimed(env, state, PHASE)`: on the first
|
|
36
|
+
visit of the run it claims the task (state→in_progress, atomic claimtask);
|
|
37
|
+
a lost race returns `STAND_DOWN`. Every phase claims its own session
|
|
38
|
+
(source parity — one session per phase): the driver nulls
|
|
39
|
+
`state.activeSessionId` on each ADVANCE/REWIND, so the next phase goes
|
|
40
|
+
through `claimStep` for a fresh per-phase session, never a second
|
|
41
|
+
first-claim. When the driver resumes mid-run, the derived running session
|
|
42
|
+
is reused for its own phase.
|
|
43
|
+
|
|
44
|
+
Creative phases (Triage, Map, Build, Review, Integrate, Publish) cross the
|
|
45
|
+
work boundary through `runWorkBoundary`, which never spawns itself: it reads
|
|
46
|
+
terminal spawn-ledger rows via `getSpawnRow`, or returns `NEED_SPAWN` with a
|
|
47
|
+
`workRequest` the driver posts. Capture is fully mechanical (no spawn).
|
|
48
|
+
|
|
49
|
+
`recordPhase` writes the session row + event row in one transaction
|
|
50
|
+
(`record-phase` composite). Session `status` vocabulary: `completed`,
|
|
51
|
+
`failed`, `rejected`, `in_progress`. Event `type` mirrors the session
|
|
52
|
+
status.
|
|
53
|
+
|
|
54
|
+
## Triage (Sage) — `triage.js`
|
|
55
|
+
|
|
56
|
+
- **Inputs:** task title/description, repo path, surface description, UX
|
|
57
|
+
doctrine path, event preamble.
|
|
58
|
+
- **State read:** claim context.
|
|
59
|
+
- **Work:** one work-agent (Sage) validates the task; report must end with
|
|
60
|
+
the machine-read `experiential: yes|no` line.
|
|
61
|
+
- **State written:** completed session + event (notes = summarized report).
|
|
62
|
+
Not a verdict step: usable output = pass.
|
|
63
|
+
- **Handoff:** `experiential` (`"yes"`/`"no"`/null) — read by Capture and
|
|
64
|
+
Map from the durable Triage session notes (re-derived, never passed in
|
|
65
|
+
memory).
|
|
66
|
+
- **Transitions:** `ADVANCE → Capture` always (a rejected Triage still
|
|
67
|
+
advances; the rejection is recorded).
|
|
68
|
+
|
|
69
|
+
## Capture (Hazel) — `capture.js`
|
|
70
|
+
|
|
71
|
+
- **Inputs:** experiential status (from Triage session notes), surface
|
|
72
|
+
classification, baseline notes (`baselineStatus`), visual-protocol
|
|
73
|
+
availability flag.
|
|
74
|
+
- **State read:** prior `baseline:` notes (exact-prefix scan).
|
|
75
|
+
- **Work:** fully mechanical. Skips when not experiential or not an
|
|
76
|
+
artifact surface. Otherwise: baseline already recorded → carry-forward
|
|
77
|
+
note; 2 requests exhausted → `baseline: none`; else request baseline
|
|
78
|
+
(attempt N) and park for the parent protocol.
|
|
79
|
+
- **State written:** `baseline:` notes (`requested`, `none`, or the
|
|
80
|
+
carry-forward form), completed session + event.
|
|
81
|
+
- **Handoff:** the `baseline:` notes themselves (Map's gate reads them).
|
|
82
|
+
- **Transitions:** `ADVANCE → Map` on every path except the park path
|
|
83
|
+
(`PARK` with the baseline-request reason).
|
|
84
|
+
|
|
85
|
+
## Map (Mara) — `map.js`
|
|
86
|
+
|
|
87
|
+
- **Inputs:** experiential/baseline gate, task context, prior events,
|
|
88
|
+
mapper inputs.
|
|
89
|
+
- **State read:** Triage session notes (experiential), `baseline:` notes.
|
|
90
|
+
- **Work:** baseline gate first — an experiential artifact task with no
|
|
91
|
+
baseline evidence bounces to Capture (session `failed`, never a task
|
|
92
|
+
failure). Then one work-agent (Mara) writes the builder spec; the
|
|
93
|
+
baseline-gate paragraph is included when the gate applied.
|
|
94
|
+
- **State written:** completed/failed session + event (notes = the mapper
|
|
95
|
+
specification — the durable cross-run handoff Build reads).
|
|
96
|
+
- **Handoff:** the Map session notes (the spec); Build re-derives them via
|
|
97
|
+
`latestSessionNotes`.
|
|
98
|
+
- **Transitions:** `REWIND → Capture` (Map gate bounce); `ADVANCE → Build`.
|
|
99
|
+
|
|
100
|
+
## Build (Wren) — `build.js`
|
|
101
|
+
|
|
102
|
+
- **Inputs:** mapper spec + rejection notes (from durable session notes),
|
|
103
|
+
worktree hints, task branch, publish-type npm paragraph, surface UX
|
|
104
|
+
paragraphs.
|
|
105
|
+
- **State read:** Map session notes (spec), prior rejection notes, claim
|
|
106
|
+
context.
|
|
107
|
+
- **Work:** one work-agent (Wren) prepares the worktree (STEP 1), starts
|
|
108
|
+
the heartbeat loop, implements the spec (STEP 2). Verdict step:
|
|
109
|
+
`VERDICT: PASS|FAIL` extracted deterministically; missing verdict →
|
|
110
|
+
bounded verdict-reask.
|
|
111
|
+
- **State written:** completed/failed session + event (notes = summarized
|
|
112
|
+
report, incl. the machine-read `release:`/`version_bump:` and
|
|
113
|
+
`repo_diff:` marker lines).
|
|
114
|
+
- **Handoff:** the Build session notes (report + markers); Review reads the
|
|
115
|
+
report, Publish re-derives the release decision.
|
|
116
|
+
- **Transitions:** `ADVANCE → Review` on pass; `FAILED` (dispatcher retry)
|
|
117
|
+
on terminal work-agent failure.
|
|
118
|
+
|
|
119
|
+
## Review (Cass) — `review.js`
|
|
120
|
+
|
|
121
|
+
- **Inputs:** mapper spec, branch classification (direct `inspect` parse),
|
|
122
|
+
npm `version` touch check (bounded instrument retries), Build report,
|
|
123
|
+
release decision, worktree hint.
|
|
124
|
+
- **State read:** Build session notes, git branch state (mechanical).
|
|
125
|
+
- **Work:** direct branch classification first (no Cass on empty/touched
|
|
126
|
+
branches — synthetic mechanical FAIL). Cold Cass review otherwise:
|
|
127
|
+
structured verdict extracted deterministically; rejection carries a
|
|
128
|
+
bounded rework budget and rewinds to Build with rejection notes.
|
|
129
|
+
- **State written:** completed (`VERDICT: PASS`), `rejected` (structured
|
|
130
|
+
verdict payload in the `verdicts` table, first-write-wins), or `failed`
|
|
131
|
+
session + event.
|
|
132
|
+
- **Handoff:** rejection notes (Build re-reads them); the accepted report.
|
|
133
|
+
- **Transitions:** `ADVANCE → Integrate` on pass; `REWIND → Build`
|
|
134
|
+
(rework, bounded); `FAILED` on mechanical/instrument failure.
|
|
135
|
+
|
|
136
|
+
## Integrate (Wren) — `integrate.js`
|
|
137
|
+
|
|
138
|
+
- **Inputs:** task branch, merge-lock holder identity, lifecycle
|
|
139
|
+
`integrate` command, mechanical `verify-merge` check.
|
|
140
|
+
- **State read:** lock status, prior merge records, claim context.
|
|
141
|
+
- **Work:** one work-agent (Wren) runs the lifecycle `integrate`
|
|
142
|
+
instruction (conflict-resolution protocol, detached-HEAD rules); the
|
|
143
|
+
workflow then runs mechanical `verify-merge <taskId>` requiring a
|
|
144
|
+
`VERIFIED:` line. `MERGED_EMPTY` (no commits ahead) is a clean skip —
|
|
145
|
+
no lock is taken.
|
|
146
|
+
- **State written:** completed/failed session + event; merge record via the
|
|
147
|
+
lifecycle script.
|
|
148
|
+
- **Handoff:** the merge on the integration target; Publish reads the lock
|
|
149
|
+
state (`UNLOCKED` ⇒ empty-diff skip).
|
|
150
|
+
- **Transitions:** `ADVANCE → Publish` on verified merge (incl.
|
|
151
|
+
`MERGED_EMPTY`); `FAILED` on verification failure.
|
|
152
|
+
|
|
153
|
+
## Publish (Wren) — `publish.js`
|
|
154
|
+
|
|
155
|
+
- **Inputs:** publish target (`npm`/`artifact`/`vercel`/empty), pinned
|
|
156
|
+
lifecycle scripts, merge-lock state, registry version, artifact
|
|
157
|
+
provenance, publish diff.
|
|
158
|
+
- **State read:** Build session notes (release decision), provenance,
|
|
159
|
+
lock status, registry.
|
|
160
|
+
- **Ordering (source-preserved):** pin guard + one re-pin → target
|
|
161
|
+
validation → npm precomputation (registry base + deterministic bump) →
|
|
162
|
+
claim → instructions branch.
|
|
163
|
+
- **Work — npm:** the work agent runs exactly one command (the pinned
|
|
164
|
+
publish script); the workflow verifies the registry version and
|
|
165
|
+
refreshes provenance mechanically. `release: no` and empty target skip
|
|
166
|
+
deterministically before the claim.
|
|
167
|
+
- **Work — artifact:** fully mechanical before the work agent — verified
|
|
168
|
+
re-entry guard, lock-status/refresh, artifact target preflight,
|
|
169
|
+
provenance base, checksummed diff with binary/rename/line-budget guards,
|
|
170
|
+
intent-ledger anchor → parks at publish intent for the tick worker. The
|
|
171
|
+
work agent only reports on the mechanical outcome (verified / skipped /
|
|
172
|
+
normal framing).
|
|
173
|
+
- **Work — vercel:** not implemented → `VERDICT: FAIL`, no invented
|
|
174
|
+
behavior.
|
|
175
|
+
- **State written:** completed/failed session + event; publish ledger
|
|
176
|
+
entries (`recordPublishLedger`). P4 preserved: early artifact ledger
|
|
177
|
+
entries carry `commit: null` (the `var mergeCommitForPublish`
|
|
178
|
+
declaration hoists below them — do not move it).
|
|
179
|
+
- **Handoff:** terminal — the run ends (`ADVANCE` with `next: null`).
|
|
180
|
+
- **Transitions:** `ADVANCE → null` (done); `PARK` on any fail-closed
|
|
181
|
+
condition; `FAILED` on work-agent failure.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Chore 44-site disposition inventory (sandbox exit, Piece 2)
|
|
2
|
+
|
|
3
|
+
Independent review (2026-09-26) found 44 `agent()` call sites in
|
|
4
|
+
`workflows/chore.js`: **42 couriers** (deterministic — become `execFile` /
|
|
5
|
+
Node in the worker layer), **1 work-agent** (creative), **1 verdict-reask**
|
|
6
|
+
(creative). Chore fits completely; Standard QA and Bugfix Reproduce/QA
|
|
7
|
+
remain deferred Piece 4.
|
|
8
|
+
|
|
9
|
+
Disposition vocabulary: `execFile` · `spawn-bridge` · `deferred-piece-4` ·
|
|
10
|
+
`deleted`. Unknown → fail closed.
|
|
11
|
+
|
|
12
|
+
## Creative (spawn-bridge)
|
|
13
|
+
|
|
14
|
+
| # | site | disposition |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| 1 | L2004 — the phase work-agent (`workPromptBase`) | `spawn-bridge` → `work-agent` kind via `runWorkBoundary`/`workRequest` |
|
|
17
|
+
| 2 | L247 — verdict re-ask (`buildVerdictReaskPrompt`) | `spawn-bridge` → `verdict-reask` kind via `runWorkBoundary` |
|
|
18
|
+
|
|
19
|
+
## Deterministic couriers → execFile
|
|
20
|
+
|
|
21
|
+
All 42 become direct `execFile`/Node calls in the worker layer (argv only,
|
|
22
|
+
no shell). Grouped by phase:
|
|
23
|
+
|
|
24
|
+
**Run scaffolding / telemetry**
|
|
25
|
+
| site | command |
|
|
26
|
+
|---|---|
|
|
27
|
+
| L72 | `record-run-start` (crew-api) |
|
|
28
|
+
| L95 | `record-run-events-batch` (crew-api) |
|
|
29
|
+
| L114 | `record-run-end` (crew-api) |
|
|
30
|
+
| L282 | `pinLifecycle` — mkdir/cp/chmod/ls of pinned scripts |
|
|
31
|
+
| L1022 | pin verification — `ls -1 RUN_LIB` |
|
|
32
|
+
| L2397 | cleanup pins — `rm -rf RUN_LIB` |
|
|
33
|
+
|
|
34
|
+
**Claim protocol**
|
|
35
|
+
| site | command |
|
|
36
|
+
|---|---|
|
|
37
|
+
| L1114 | first claim — `update-task` (state→in_progress + workflow) |
|
|
38
|
+
| L1148 | session claim — `claim-task` |
|
|
39
|
+
|
|
40
|
+
**Phase tracking**
|
|
41
|
+
| site | command |
|
|
42
|
+
|---|---|
|
|
43
|
+
| L1181 | `record-phase` |
|
|
44
|
+
| L1201 | `log-event` (baseline carry-forward) |
|
|
45
|
+
| L1219 | `log-event` (baseline: none) |
|
|
46
|
+
| L1233 | `log-event` (baseline: requested) |
|
|
47
|
+
| L1242 | `log-event` (baseline: none, protocol unavailable) |
|
|
48
|
+
| L1270 | `record-phase` (Map gate bounce) |
|
|
49
|
+
| L2053 | `record-phase` (work failure) |
|
|
50
|
+
| L2092 | `record-phase` (verdict failure) |
|
|
51
|
+
| L2134 | `record-phase` (worktree confinement failure) |
|
|
52
|
+
| L2323 | `record-phase` (phase closeout) |
|
|
53
|
+
| L2388 | `update-task` (done) + `log-event` (completed) |
|
|
54
|
+
| L918 | `park-task` |
|
|
55
|
+
| L998 | `record-phase` (abort stale run) |
|
|
56
|
+
|
|
57
|
+
**Preflight / probes**
|
|
58
|
+
| site | command |
|
|
59
|
+
|---|---|
|
|
60
|
+
| L715 | experiential check — `get-state` → Triage notes |
|
|
61
|
+
| L751 | baseline notes — `get-events` |
|
|
62
|
+
| L826 | baseline seed — `get-events` |
|
|
63
|
+
| L983 | project check — `get-state` |
|
|
64
|
+
| L902 | `terminal-cleanup` (lifecycle) |
|
|
65
|
+
|
|
66
|
+
**Review instruments** (bounded, schema-shaped)
|
|
67
|
+
| site | command |
|
|
68
|
+
|---|---|
|
|
69
|
+
| L1383 | `classify-branch` — lifecycle `inspect` → JSON |
|
|
70
|
+
| L1454 | `version-check` — lifecycle `version-check` → JSON |
|
|
71
|
+
|
|
72
|
+
**Publish** (all deterministic pre/post work)
|
|
73
|
+
| site | command |
|
|
74
|
+
|---|---|
|
|
75
|
+
| L1045 | `post-deploy` (npm skip path) |
|
|
76
|
+
| L1070 | `post-deploy` (empty-target skip path) |
|
|
77
|
+
| L1085 | `npm view muse-crew version` (registry base) |
|
|
78
|
+
| L1606 | verified re-entry guard — `get-provenance` + `git rev-parse HEAD` |
|
|
79
|
+
| L1634 | `lock-status` (lifecycle) |
|
|
80
|
+
| L1702 | artifact preflight — `test -d` |
|
|
81
|
+
| L1739 | `get-provenance` (publish base) |
|
|
82
|
+
| L1755 | `compute-publish-diff.js` (checksummed diff) |
|
|
83
|
+
| L1872 | `post-deploy` (artifact path) |
|
|
84
|
+
| L1895 | `post-deploy` (cleanup after failure) |
|
|
85
|
+
| L2177 | `verify-merge` (lifecycle) |
|
|
86
|
+
| L2219 | `npm view muse-crew version` (registry verification) |
|
|
87
|
+
| L2232 | provenance refresh — `get-provenance` + `set-provenance` |
|
|
88
|
+
| L631 | publish-attempt ledger append (mkdir + printf) |
|
|
89
|
+
|
|
90
|
+
## Deferred (Piece 4)
|
|
91
|
+
|
|
92
|
+
Standard QA and Bugfix Reproduce/QA — not in Chore. No Chore site is
|
|
93
|
+
deferred.
|
|
94
|
+
|
|
95
|
+
## Deleted
|
|
96
|
+
|
|
97
|
+
None in Piece 2. The publish-verification content-verdict machine was
|
|
98
|
+
already retired (0.14.6); no Chore `agent()` site references it.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// lib/chore/extract.js — deterministic report extractors for the worker-layer
|
|
2
|
+
// chore port (sandbox exit, Piece 2, 2026-09-26).
|
|
3
|
+
//
|
|
4
|
+
// Import-safe: pure functions, no I/O, no clock, no randomness, no
|
|
5
|
+
// top-level await (require(esm)-safe). Moved logic-verbatim from
|
|
6
|
+
// workflows/chore.js; the one function that closed over module state
|
|
7
|
+
// (releaseDecisionText) now takes that state as an explicit parameter.
|
|
8
|
+
// The four dead-in-chore helpers (parseMergeRecord, decidePublishLockPath,
|
|
9
|
+
// decideIntegrateRetry, hydrateReleaseDecision) are NOT carried — they are
|
|
10
|
+
// dead in chore and standard. captureTargets is NOT carried — write-only
|
|
11
|
+
// in every reviewed workflow.
|
|
12
|
+
|
|
13
|
+
export function extractVerdict(workerText) {
|
|
14
|
+
// The verdict is the LAST VERDICT: PASS/FAIL in the report (contract: end
|
|
15
|
+
// your report with the verdict). This ignores literal VERDICT strings echoed
|
|
16
|
+
// from the worker's instructions (which contain quoted examples). Fail
|
|
17
|
+
// closed if: no verdict found, the last verdict is not in the trailing 100
|
|
18
|
+
// chars (verdict must be at the end), or conflicting verdicts appear in the
|
|
19
|
+
// trailing 200 chars. Word boundary prevents "PASSING" matching as PASS.
|
|
20
|
+
var text = workerText || "";
|
|
21
|
+
var regex = /VERDICT:\s*(PASS|FAIL)\b/gi;
|
|
22
|
+
var matches = [];
|
|
23
|
+
var m;
|
|
24
|
+
while ((m = regex.exec(text)) !== null) {
|
|
25
|
+
matches.push({ value: /FAIL/i.test(m[0]) ? "FAIL" : "PASS", index: m.index });
|
|
26
|
+
}
|
|
27
|
+
if (matches.length === 0) return { ok: false, count: 0 };
|
|
28
|
+
var last = matches[matches.length - 1];
|
|
29
|
+
if (last.index < text.length - 100) return { ok: false, count: matches.length };
|
|
30
|
+
var trailing = matches.filter(function (x) { return x.index >= text.length - 200; });
|
|
31
|
+
var uniq = trailing.map(function (x) { return x.value; }).filter(function (v, i, a) { return a.indexOf(v) === i; });
|
|
32
|
+
if (uniq.length !== 1) return { ok: false, count: matches.length };
|
|
33
|
+
return { ok: true, passed: last.value === "PASS" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function extractMarkerLines(workerText) {
|
|
37
|
+
var lines = (workerText || "").split("\n");
|
|
38
|
+
var markers = [];
|
|
39
|
+
for (var i = 0; i < lines.length; i++) {
|
|
40
|
+
var line = lines[i].trim();
|
|
41
|
+
if (/^(repo_diff:|release:|version_bump:|VERDICT:|TARGET_VERSION=|published:|experiential:|layer:|capture_targets:|terminal_targets:|worktree:)/i.test(line)) {
|
|
42
|
+
markers.push(line);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return markers.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseToolSignals(text) {
|
|
49
|
+
var t = String(text || "");
|
|
50
|
+
var out = { artifactTools: "unknown", shellTransport: "unknown" };
|
|
51
|
+
var am = t.match(/^artifact_tools:\s*(ok|missing)\s*$/m);
|
|
52
|
+
if (am) out.artifactTools = am[1];
|
|
53
|
+
var sm = t.match(/^shell_transport:\s*(ok|unavailable)\s*$/m);
|
|
54
|
+
if (sm) out.shellTransport = sm[1];
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseClassifyFerry(bsResult) {
|
|
59
|
+
var bsStdout = (bsResult && typeof bsResult.stdout === "string") ? bsResult.stdout : null;
|
|
60
|
+
var bsStderr = (bsResult && typeof bsResult.stderr === "string") ? bsResult.stderr : null;
|
|
61
|
+
if (bsStdout === null || bsStderr === null) {
|
|
62
|
+
return { state: null, diagField: null, diagText: "",
|
|
63
|
+
failReason: "classifier return fields missing or non-string (stdout and stderr must both be strings)" };
|
|
64
|
+
}
|
|
65
|
+
// State channel: stdout ONLY. A BRANCH_STATE match on stderr is never
|
|
66
|
+
// trusted — workers merge streams constantly, and trusting a channel
|
|
67
|
+
// swap as truth is the wrong shape. Search, never anchor (blocker 11):
|
|
68
|
+
// the ferry labels output ("stdout: BRANCH_STATE: has-work"). Collect
|
|
69
|
+
// ALL matches and dedupe normalized values: exactly one distinct value
|
|
70
|
+
// is truth; zero or two-or-more is UNKNOWN — alternation order never
|
|
71
|
+
// adjudicates.
|
|
72
|
+
var bsRe = /\bBRANCH_STATE:\s*(has-work|already-merged:[0-9a-f]{40}|empty-no-work)(?![\w-])/gi;
|
|
73
|
+
var bsSeen = {};
|
|
74
|
+
var bsM;
|
|
75
|
+
while ((bsM = bsRe.exec(bsStdout)) !== null) {
|
|
76
|
+
bsSeen[bsM[1].toLowerCase()] = true;
|
|
77
|
+
}
|
|
78
|
+
var bsStates = Object.keys(bsSeen);
|
|
79
|
+
if (bsStates.length !== 1) {
|
|
80
|
+
return { state: null, diagField: null, diagText: "",
|
|
81
|
+
failReason: bsStates.length === 0
|
|
82
|
+
? "no BRANCH_STATE match in the stdout field"
|
|
83
|
+
: "ambiguous BRANCH_STATE values in stdout (" + bsStates.length + " distinct)" };
|
|
84
|
+
}
|
|
85
|
+
var bsState = bsStates[0];
|
|
86
|
+
// DIAG pin: search both fields, stderr first. Keep the DIAG: prefix — a
|
|
87
|
+
// prose mention without it must not satisfy the pin.
|
|
88
|
+
var bsDiagRe = /DIAG:\s*classify-branch\s+records=\d+\s+resolvable=\d+/;
|
|
89
|
+
var bsDiagField = bsDiagRe.test(bsStderr) ? "stderr" : (bsDiagRe.test(bsStdout) ? "stdout" : null);
|
|
90
|
+
var bsDiagText = "";
|
|
91
|
+
if (bsDiagField !== null) {
|
|
92
|
+
bsDiagText = bsDiagRe.exec(bsDiagField === "stderr" ? bsStderr : bsStdout)[0];
|
|
93
|
+
}
|
|
94
|
+
if (bsState.indexOf("already-merged:") === 0 && bsDiagField === null) {
|
|
95
|
+
// already-merged's sha flows into Cass's `git diff <sha>^1 <sha>` — a
|
|
96
|
+
// hallucinated-but-well-formed sha is a false-PASS vector. The script
|
|
97
|
+
// emits exactly one DIAG on every path that emits a BRANCH_STATE, so a
|
|
98
|
+
// missing pin means stderr wasn't faithfully ferried: UNKNOWN.
|
|
99
|
+
// Residual risk, named (blocker-42 fib 4): a worker could fabricate
|
|
100
|
+
// BOTH fields wholesale — the pin is a tripwire, not proof of
|
|
101
|
+
// execution. The workflow has no exec channel for a deterministic
|
|
102
|
+
// cross-check; if the platform ever offers one, verify the sha
|
|
103
|
+
// against the task's durable merge records directly.
|
|
104
|
+
return { state: null, diagField: null, diagText: "",
|
|
105
|
+
failReason: "already-merged without the DIAG pin — stderr not faithfully ferried" };
|
|
106
|
+
}
|
|
107
|
+
return { state: bsState, diagField: bsDiagField, diagText: bsDiagText, failReason: "" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function parseVersionFerry(vtResult) {
|
|
111
|
+
var vtStdout = (vtResult && typeof vtResult.stdout === "string") ? vtResult.stdout : null;
|
|
112
|
+
if (vtStdout === null) {
|
|
113
|
+
return { versionState: null,
|
|
114
|
+
failReason: "version-check return field missing or non-string (stdout must be a string)" };
|
|
115
|
+
}
|
|
116
|
+
// Search, never anchor (blocker 11): the ferry labels output
|
|
117
|
+
// ("stdout: VERSION_CLEAN"). Collect ALL matches and dedupe normalized
|
|
118
|
+
// values: exactly one distinct value is truth; zero or two-or-more is
|
|
119
|
+
// UNKNOWN — alternation order never adjudicates.
|
|
120
|
+
var vtRe = /\bVERSION_(TOUCHED|CLEAN)(?![\w-])/gi;
|
|
121
|
+
var vtSeen = {};
|
|
122
|
+
var vtM;
|
|
123
|
+
while ((vtM = vtRe.exec(vtStdout)) !== null) {
|
|
124
|
+
vtSeen[vtM[1].toUpperCase()] = true;
|
|
125
|
+
}
|
|
126
|
+
var vtStates = Object.keys(vtSeen);
|
|
127
|
+
if (vtStates.length !== 1) {
|
|
128
|
+
return { versionState: null,
|
|
129
|
+
failReason: vtStates.length === 0
|
|
130
|
+
? "no VERSION_(TOUCHED|CLEAN) match in the stdout field"
|
|
131
|
+
: "ambiguous VERSION values in stdout (" + vtStates.length + " distinct)" };
|
|
132
|
+
}
|
|
133
|
+
return { versionState: vtStates[0] === "CLEAN" ? "clean" : "touched", failReason: "" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function extractReleaseDecision(workerText) {
|
|
137
|
+
var t = workerText || "";
|
|
138
|
+
var r = /release:\s*(yes|no)\b/i.exec(t);
|
|
139
|
+
if (!r) return null;
|
|
140
|
+
var b = /version_bump:\s*(patch|minor|major)\b/i.exec(t);
|
|
141
|
+
var rel = r[1].toLowerCase();
|
|
142
|
+
if (rel === "yes" && !b) return null;
|
|
143
|
+
return { release: rel, version_bump: b ? b[1].toLowerCase() : null };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function extractWorktree(workerText) {
|
|
147
|
+
var lines = (workerText || "").split("\n");
|
|
148
|
+
var found = null;
|
|
149
|
+
for (var i = 0; i < lines.length; i++) {
|
|
150
|
+
var line = lines[i].trim();
|
|
151
|
+
var m = /^worktree:\s*(\S.*)$/i.exec(line);
|
|
152
|
+
if (m) found = m[1].trim();
|
|
153
|
+
}
|
|
154
|
+
if (!found) return { ok: false };
|
|
155
|
+
// Normalize trailing slashes: ".../<task_id>/" and ".../<task_id>"
|
|
156
|
+
// name the same directory. Compare locations, not spellings — a
|
|
157
|
+
// builder that emits the trailing slash still worked in the right
|
|
158
|
+
// place. (Canary 2026-09-11: an exact comparison rejected a correct
|
|
159
|
+
// declaration over one trailing slash.)
|
|
160
|
+
return { ok: true, path: found.replace(/\/+$/, "") };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function extractExperiential(workerText) {
|
|
164
|
+
// Reads Sage's experiential: yes|no marker line. Missing or malformed
|
|
165
|
+
// fails closed to null — the flag is opt-in; null is treated as
|
|
166
|
+
// non-experiential (never silently parks a task on a garbled line).
|
|
167
|
+
var t = workerText || "";
|
|
168
|
+
var r = /experiential:\s*(yes|no)\b/i.exec(t);
|
|
169
|
+
if (!r) return null;
|
|
170
|
+
return r[1].toLowerCase() === "yes";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function bumpVersion(base, scope) {
|
|
174
|
+
var p = String(base).trim().split(".").map(function (x) { return parseInt(x, 10) || 0; });
|
|
175
|
+
if (scope === "major") return (p[0] + 1) + ".0.0";
|
|
176
|
+
if (scope === "minor") return p[0] + "." + (p[1] + 1) + ".0";
|
|
177
|
+
return p[0] + "." + p[1] + "." + (p[2] + 1); // patch (default)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function releaseDecisionText(releaseDecision) {
|
|
181
|
+
// In workflows/chore.js this closed over the module-level
|
|
182
|
+
// `releaseDecision`; here it is an explicit parameter (import-safe).
|
|
183
|
+
// Logic otherwise verbatim.
|
|
184
|
+
if (!releaseDecision) return "no machine-readable release decision from the Build report";
|
|
185
|
+
return "release: " + releaseDecision.release + (releaseDecision.version_bump ? ", version_bump: " + releaseDecision.version_bump : " (no version_bump line)");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// extractRepoDiffNone — the Build report's plain `repo_diff: none` (no repo
|
|
189
|
+
// change) claim. Inline in workflows/chore.js
|
|
190
|
+
// (`/repo_diff:\s*none/im.test(workerText || "")`); named here per the
|
|
191
|
+
// phase-module contract. Pure, no I/O.
|
|
192
|
+
export function extractRepoDiffNone(workerText) {
|
|
193
|
+
return /repo_diff:\s*none/im.test(workerText || "");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// extractPublishSkipped — the PUBLISH_SKIPPED=<reason> marker the npm publish
|
|
197
|
+
// script emits and the Publish agent pastes verbatim into its report. Inline
|
|
198
|
+
// in workflows/chore.js
|
|
199
|
+
// (`/^PUBLISH_SKIPPED=(no-lock-held|no-npm-publish)$/m.exec(workerText || "")`);
|
|
200
|
+
// named here per the phase-module contract. Returns the reason string or null.
|
|
201
|
+
export function extractPublishSkipped(workerText) {
|
|
202
|
+
var m = /^PUBLISH_SKIPPED=(no-lock-held|no-npm-publish)$/m.exec(workerText || "");
|
|
203
|
+
return m ? m[1] : null;
|
|
204
|
+
}
|