muse-crew 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md CHANGED
@@ -124,6 +124,9 @@ Create or update an agent session record. Sessions track which identity is worki
124
124
  | `notes` | string (≤ 3000) | no | |
125
125
  | `failure_reason` | string (≤ 3000) or null | no | |
126
126
  | `caveats` | array of `{text, severity}` | no | |
127
+ | `already_merged_sha` | 7–40 lowercase hex or null | no | **Do not set this yourself.** The workflow sets it automatically when it verifies that a task's work is already on the main branch (e.g., landed by a prior run). Written by workflow code (not by the agent's text output) after the `rev-parse` + `merge-base --is-ancestor` check passes; a bad value is a usage error. Review loads it from the field instead of parsing the notes. Null semantics: omitting the field preserves the existing value; passing explicit `null` clears it. `record-phase` always preserves (it never clears). |
128
+
129
+ The `already_merged_sha` field exists because session `notes` are hard-capped at 3000 chars: a truthful already-merged declaration at the tail of a long Build report was silently truncated, and Review could not recover it. The sha is managed by the workflow — the workflow writes it after verification, and downstream phases read the field, never the agent's text.
127
130
 
128
131
  ### `heartbeat-session`
129
132
 
@@ -474,18 +477,49 @@ Set a global configuration value.
474
477
 
475
478
  ### `setprovenance`
476
479
 
477
- Stamp publication provenance. Called by the crew Publish phase after rebuilding the artifact, and after installing/activating a crew release (npm path): in the release case Publish refreshes `crew_release` to the live release identity while preserving the existing `source_commit`.
480
+ Stamp publication provenance. Provenance is per-project (2026-09-18): each
481
+ project row carries its own publish base (`provenance_source_commit`,
482
+ `provenance_crew_release`, `provenance_published_at`, `provenance_task_id`);
483
+ a crew home with N artifact projects carries N independent bases. There are
484
+ two stamp call paths, and they are different operations on the same row:
485
+
486
+ - **Full stamp** — called by the parent's publish verification (`lib/verify-publish.js`)
487
+ after independently verifying the rebuilt artifact: `source_commit` is the
488
+ verified build commit, `crew_release` the active release at publish time.
489
+ - **Refresh** — called by the release installer (`lib/advance-publish-base.js`)
490
+ after installing/activating a crew release: `crew_release` is refreshed to
491
+ the live release identity while preserving the existing `source_commit`.
492
+ A base-pointer move is not a publish.
493
+
494
+ Both paths flow through the single writer in `lib/crew-api.js`; an unknown
495
+ `project_id` fails closed (exit 3), and omitting `project_id` is a usage
496
+ error (exit 2) — there is no silent global fallback.
478
497
 
479
498
  | Field | Type | Required | Notes |
480
499
  |-------|------|----------|-------|
500
+ | `project_id` | string | yes | The project whose publish base is stamped |
481
501
  | `source_commit` | 40-char hex | yes | Repo commit the artifact was built from |
482
- | `crew_release` | 40-char hex | yes | Active crew release (`$CREW_HOME/current`) at publish time |
483
- | `published_at` | ISO 8601 datetime (UTC) | yes | |
502
+ | `crew_release` | 40-char hex, or `pkg-<semver>` | yes | Active crew release (`$CREW_HOME/current`) at stamp time |
503
+ | `published_at` | ISO 8601 datetime (UTC) | yes | Defaults to now when omitted |
484
504
  | `task_id` | string | no | Publishing task, for traceability |
485
505
 
506
+ A one-time migration attributes a pre-per-project legacy `config.provenance.*`
507
+ triple to a project row only when its `source_commit` is an ancestor of
508
+ exactly one project's repo HEAD (verified with git); zero or ambiguous
509
+ matches are recorded as `deferred:<reason>` and never attributed. A triple
510
+ that matches exactly one project but violates the writer's shape rules
511
+ (e.g. a hand-written abbreviated sha, which git resolves for the ancestor
512
+ check but which fails the 40-char storage rule) is recorded as
513
+ `deferred:stamp-failed:<reason>` — the migration never throws, so a bad
514
+ triple can never brick the home. A `deferred:*` watermark is re-evaluated
515
+ when a project is created (the only event that changes the match set), so
516
+ a triple that predates the home's first project is attributed once a
517
+ descendant project exists. The legacy keys are inert dead storage after a
518
+ successful migration — new code never reads them.
519
+
486
520
  ### `getprovenance`
487
521
 
488
- Read the last publication provenance. Returns `{ "provenance": { "source_commit", "crew_release", "published_at", "task_id" } }`, or `{ "provenance": null }` when nothing has been published yet. `task_id` is the publishing task, for traceability — present when `setprovenance` was called with one, absent otherwise.
522
+ Read a project's publication provenance. Returns `{ "provenance": { "source_commit", "crew_release", "published_at", "task_id" } }`, or `{ "provenance": null }` when nothing has been published yet. A project that was never stamped reads `null` — another project's stamp is never visible here. `task_id` is the publishing task, for traceability — present when `setprovenance` was called with one, absent otherwise. `project_id` is required (exit 2 when omitted); an unknown `project_id` fails closed (exit 3).
489
523
 
490
524
  ---
491
525
 
package/docs/guide.md CHANGED
@@ -34,7 +34,7 @@ When this guide says "call an action," it means calling an action defined in the
34
34
  - A Muse account with access to workflows, crons, and artifacts.
35
35
  - The `muse-crew` package installed via npm, or the repo cloned locally.
36
36
  - A directory chosen for the crew home (e.g., `~/workspace/.jarvis`).
37
- - A task service artifact already created. The [Orchestra Dashboard](https://github.com/emojimanegg1/orchestra-dashboard) is the reference implementation — clone its repo, create the artifact, and note its slug.
37
+ - A task service artifact already created. (An "artifact" is a Muse platform primitive — a hosted web app created through the Muse UI. The crew never creates artifacts; it only edits existing ones.) The [Orchestra Dashboard](https://github.com/emojimanegg1/orchestra-dashboard) is the reference implementation — clone its repo, create an artifact from it via the Muse UI, and note its slug. **Rule: every `deploy_slug` must point to an artifact that already exists.** Init checks that `~/workspace/ts-spaces/<slug>/` exists and will stop with an error if it doesn't — without this check, the first task would reach Publish and fail with a confusing error message. Publish edits an existing artifact; it never creates one.
38
38
 
39
39
  ### Running init
40
40
 
@@ -46,8 +46,12 @@ The workflow dropped the report entirely on 2026-09-16 (clean-room task
46
46
  `e2a8d9f8`): the trigger's JSON closeout contract traveled over the
47
47
  stochastic text channel and the runtime's JSON-candidate heuristic misfired
48
48
  on its prose ("workflow agent output was not JSON"), parking a task whose
49
- edit may have gone through. The trigger is now awaited-but-unconsumed — no schema,
50
- no consumed return value — and there is no builder report to observe:
49
+ edit may have gone through. The trigger is now awaited and scanned for a
50
+ single explicit refusal signal — `ARTIFACT_EDIT_REFUSED: <text>` as the
51
+ entire trimmed turn output — and nothing else is consumed from the return.
52
+ An exact refusal is conclusive negative evidence (parks `rejected`, skips
53
+ observation polling); any other output (including prose quoting the signal)
54
+ is inconclusive and follows the existing fail-closed observation path.
51
55
  `applied_report` is `missing-report` on ledger lines for issued triggers
52
56
  (pre-trigger parks and unattributed-unknown parks write null — no trigger
53
57
  was observed, so there is nothing to report). The
@@ -223,7 +227,9 @@ For a task parked with `publish: verification-requested <commit>`:
223
227
  slug (the project's publish target).
224
228
  2. **Expected change.** The publish delta is `git diff <base> <commit>`
225
229
  in `repo_path`, where `<base>` is the previously-stamped provenance
226
- `source_commit` (read it via `get-provenance`; use the empty-tree sha
230
+ `source_commit` for this project (read it via
231
+ `get-provenance --json '{"project_id":"<id>"}'` — provenance is
232
+ per-project; without `project_id` the call exits 2; use the empty-tree sha
227
233
  `4b825dc642cb6eb9a060e54bf8d69288fbee4904` when no provenance is
228
234
  stamped yet — a first publish). Never use `commit^1` as the base and
229
235
  never take the expected change from the builder's report: push-time
@@ -293,9 +299,15 @@ For a task parked with `publish: verification-requested <commit>`:
293
299
  (the crew-owned store). Do NOT use the artifact's `setprovenance`
294
300
  action — it writes a different, non-authoritative store that QA never
295
301
  reads, so the stamp would be invisible to every gate:
296
- `set-provenance --json '{"source_commit":"<commit>","crew_release":"<release>","task_id":"<task>"}'`
302
+ `set-provenance --json '{"project_id":"<id>","source_commit":"<commit>","crew_release":"<release>","task_id":"<task>"}'`
297
303
  (crew_release is the basename of the active release, e.g.
298
- `pkg-0.7.10`). Then read the stamp back with `get-provenance`
304
+ `pkg-0.7.10`). This is the full publish-verification stamp — it
305
+ records a verified publish. Do not confuse it with the
306
+ provenance *refresh* (re-pointing `crew_release` at the active
307
+ release after a crew upgrade without a new publish), which also goes
308
+ through `set-provenance` but keeps the existing `source_commit`.
309
+ Then read the stamp back with
310
+ `get-provenance --json '{"project_id":"<id>"}'`
299
311
  and confirm source_commit, crew_release, and task_id match exactly
300
312
  what was sent — a stamp that cannot be read back is not a stamp. Only
301
313
  then log the task note event
package/lib/AGENTS.md CHANGED
@@ -3,18 +3,18 @@
3
3
  Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, and the release system.
4
4
 
5
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
6
- - `crew-api.js` — the crew-owned task-service API (dependency inversion, 2026-09-11): a zero-dependency Node CLI implementing the API.md contract against `$CREW_HOME/crew-state.db` (schema in `schema.sql`). Workflows call it through their agents' shell; the dashboard delegates to it. All state-machine invariants live as CHECK constraints in the schema, never in client prose. Includes the `record-phase` composite (session + event in one transaction) and a one-time `migrate` import from a dashboard app.db.
6
+ - `crew-api.js` — the crew-owned task-service API (dependency inversion, 2026-09-11): a zero-dependency Node CLI implementing the API.md contract against `$CREW_HOME/crew-state.db` (schema in `schema.sql`). Workflows call it through their agents' shell; the dashboard delegates to it. All state-machine invariants live as CHECK constraints in the schema, never in client prose. Includes the `record-phase` composite (session + event in one transaction) and a one-time `migrate` import from a dashboard app.db. Active-release resolution is split in two (room #15): `resolveActiveReleaseName` (symlink-only — writers like the initial-provenance stamp record the active release without proving they are it) and `resolveActiveRelease` (symlink + self-path cross-check — verifiers like `scan-verification-pending` refuse to stamp claims when the running code isn't the active release's own). Provenance is per-project (2026-09-18, room #15 blocker 8): nullable `provenance_*` columns on the projects row, a single `stampProvenance()` writer, `set-provenance`/`get-provenance` require `project_id` (no silent global fallback), and a watermarked openDb backfill that attributes the legacy `config.provenance.*` triple to exactly-one ancestor match — never fabricated, otherwise deferred.
7
7
  - `schema.sql` — the crew-owned state schema: projects, tasks, poll_state, config, agent_sessions, events. Vocabularies enforced by CHECK constraints; `rejected` is a valid event type (the 2026-09-11 crash was a stored session whose event was rejected). Column names match the historical dashboard tables for a verbatim migration.
8
8
  - `crew-release.sh` — immutable release manager: deploy, rollback, prune
9
9
  - `merge-lock.sh` — serialized merge lock for concurrent agents: time-based holder lease (bug 2fc8f52f — an unexpired lease is held regardless of process liveness; only an expired lease may be broken). Requires both `CREW_REPO` and `CREW_HOME` (fail closed: BLOCKED, exit 2 when either is unset). Lock file is key=value: task_id, opaque holder identity (never a PID), acquired_at epoch, lease_seconds (default 600, override via MERGE_LOCK_LEASE_SECONDS). acquire/refresh/release/status/force-release; holder-only refresh and release; every op appends to $CREW_HOME/.merge-lock.log
10
- - `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/verify-merge/status/post-deploy/terminal-cleanup/refresh-lock/lock-status over git worktrees (`integrate` reconciles `origin/main` under the merge lock after the task merge — canary `a6d8b0c8`, 2026-09-11 — so the push is a fast-forward; fail-soft with no remote, fail closed on conflict) (`.worktrees/<id>`, branch `task/<id>`). Branch resolution order: the crew registry (`<repo>/.worktrees/.registry/<id>`), then the canonical `task/<full-id>` ref, then a `task/<id-prefix>` ref matched by strict prefix enumeration (task 4e1a1bba — a Build agent may create the branch with raw git from an abbreviated id, bypassing prepare; prefix names are tolerated, never created; ambiguous prefixes fail closed). `resolve-branch` prints the resolved branch for agent-side one-liners. Nothing reconstructs the branch name — every command resolves it. Prepare fails closed on dirty `main`; cleanup is forgiving. `terminal-cleanup` is the run's last act at every park/fail boundary (called from `parkTask` in standard/bugfix/chore): releases the merge lock unconditionally and reclaims the worktree+branch only when the task branch is fully merged into main — unmerged work is preserved for the human by design, and a dirty worktree is reported, never force-removed. `post-deploy` reports worktree removal honestly (a lying "removed" echo hid real leftovers — canary run 9, 2026-09-12). Requires both `CREW_REPO` and `CREW_HOME` (fail closed: BLOCKED, exit 2 when either is unset); `CREW_REPO` is exported so internal merge-lock.sh calls inherit the repo being worked on; `LIB_DIR` defaults to `$CREW_HOME/lib` (`CREW_LIB` override). `lock-status` reports the merge-lock state explicitly (`UNLOCKED`, or key=value: locked=true, task_id, holder, acquired_at, lease_seconds, age_seconds, remaining_seconds — always exit 0) so Publish can distinguish an empty-diff Integrate (no lock taken) from a refresh failure.
10
+ - `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/verify-merge/status/post-deploy/terminal-cleanup/refresh-lock/lock-status over git worktrees (`integrate` reconciles `origin/main` under the merge lock after the task merge — canary `a6d8b0c8`, 2026-09-11 — so the push is a fast-forward; fail-soft with no remote, fail closed on conflict) (`.worktrees/<id>`, branch `task/<id>`). Branch resolution order: the crew registry (`<repo>/.worktrees/.registry/<id>`), then the canonical `task/<full-id>` ref, then a `task/<id-prefix>` ref matched by strict prefix enumeration (task 4e1a1bba — a Build agent may create the branch with raw git from an abbreviated id, bypassing prepare; prefix names are tolerated, never created; ambiguous prefixes fail closed). `resolve-branch` prints the resolved branch for agent-side one-liners. Nothing reconstructs the branch name — every command resolves it. Prepare fails closed on dirty `main`; cleanup is forgiving. `terminal-cleanup` is the run's last act at every park/fail boundary (called from `parkTask` in standard/bugfix/chore): releases the merge lock unconditionally and reclaims the worktree+branch only when the task branch is fully merged into main — unmerged work is preserved for the human by design, and a dirty worktree is reported, never force-removed. `post-deploy` reports worktree removal honestly (a lying "removed" echo hid real leftovers — canary run 9, 2026-09-12). Requires both `CREW_REPO` and `CREW_HOME` (fail closed: BLOCKED, exit 2 when either is unset); `CREW_REPO` is exported so internal merge-lock.sh calls inherit the repo being worked on; `LIB_DIR` defaults to `$CREW_HOME/lib` (`CREW_LIB` override). `lock-status` reports the merge-lock state explicitly (`UNLOCKED`, or key=value: locked=true, task_id, holder, acquired_at, lease_seconds, age_seconds, remaining_seconds — always exit 0) so Publish can distinguish an empty-diff Integrate (no lock taken) from a refresh failure. post-deploy's `.worktrees/` guard is add-then-reset, never an all-negative pathspec: `git add -A -- ':!.worktrees/'` still exits 1 on git 2.43.0 against the crew's own gitignored non-empty `.worktrees/` (room #15 J3), killing post-deploy under `set -e` — a plain `add -A` never errors on ignored paths and the follow-up `git reset -q -- .worktrees/` keeps stray lock files out of main even on pre-gitignore-entry repos.
11
11
  - `test-worktree-backend.sh` — regression tests for the lifecycle script (validate, prepare/reuse, inspect, status, cleanup, idempotent cleanup, dirty-main preflight, integrate remote-reconcile with fixture sensitivity + no-remote fail-soft) on scratch repos
12
12
  - `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)
13
13
  - `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
14
14
  - `test-publish-skip.sh` — regression tests for the no-lock graceful publish skip (park 2026-09-11): `lock-status` returns exact `UNLOCKED`/`LOCKED by <holder> since <ts> (pid <pid>)`; a fake empty-diff integrate takes the MERGED_EMPTY branch and never takes the lock; the shipped step-5 block from publish-npm.sh (extracted by anchor) skips gracefully on UNLOCKED (no park, no `PUBLISH_COMPLETE`), follows the refresh path when a lock is held, and fails closed on a `lock-status` query failure; the gate sits inside the ALREADY_PUBLISHED=0 branch and mutation steps are gated on NO_LOCK_HELD=0
15
15
  - `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.
16
16
  - `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.
17
- - `compose-evidence-caption.js` — deterministic QA-evidence caption composer (2026-09-14): given `--crew-home`, `--task-id`, and the resolved `--audit-dir` name, extracts the task title/description, the QA verdict, the `publish: verified` commit (or the global provenance record when it belongs to this task), and the merge commit's subject line from the project repo — and prints the out-of-context transition Eric asked for (problem → solution → evidence handoff) to stdout. The caller appends the desktop/mobile attachment lines after this caption. Verdict source (2026-09-16): Hazel's `task-evidence/<taskId>/postchange/verdict.json` is authoritative, then the last line of the append-only `verdicts.jsonl` ledger, then the newest legacy `visual_verdict:` note (the parent visual-verdict protocol was retired 2026-09-15). Honest labels: "provenance not yet stamped" when no commit is known, "no QA verdict recorded" when none exists, and the visual-protocol-unavailable note only when no verdict evidence exists at all, the newest `baseline:` note is `baseline: none`, AND captures exist (a `baseline: none` note means no baseline captures to compare against — it never overrules a real QA verdict; the note's premise is the post-deploy capture, so without captures it would fabricate evidence again). Capture guard (2026-09-17, room #14): the composer resolves the audit dir itself (`<home>/workspace/ts-spaces/<deploy_slug>/audits/<auditDir>/`, the same root crew-api.js and readback-disk.js resolve) and verifies `screenshot.png` + `screenshot-mobile.png` are files before claiming "desktop + mobile captures below" — a bare `--audit-dir` name is never trusted. Missing deploy_slug, missing dir, or missing PNGs gets the honest no-captures opener and Evidence line (terminal surfaces named explicitly: "terminal surface — QA judges the CLI, not pixels"); the caller only appends attachment lines when its own freshness check passes. "Latest" is by event timestamp, sorted explicitly. Exits non-zero on missing input.
17
+ - `compose-evidence-caption.js` — deterministic QA-evidence caption composer (2026-09-14): given `--crew-home`, `--task-id`, and the resolved `--audit-dir` name, extracts the task title/description, the QA verdict, the `publish: verified` commit (or the task's project-scoped provenance record when it belongs to this task), and the merge commit's subject line from the project repo — and prints the out-of-context transition Eric asked for (problem → solution → evidence handoff) to stdout. The caller appends the desktop/mobile attachment lines after this caption. Verdict source (2026-09-16): Hazel's `task-evidence/<taskId>/postchange/verdict.json` is authoritative, then the last line of the append-only `verdicts.jsonl` ledger, then the newest legacy `visual_verdict:` note (the parent visual-verdict protocol was retired 2026-09-15). Honest labels: "provenance not yet stamped" when no commit is known, "no QA verdict recorded" when none exists, and the visual-protocol-unavailable note only when no verdict evidence exists at all, the newest `baseline:` note is `baseline: none`, AND captures exist (a `baseline: none` note means no baseline captures to compare against — it never overrules a real QA verdict; the note's premise is the post-deploy capture, so without captures it would fabricate evidence again). Capture guard (2026-09-17, room #14): the composer resolves the audit dir itself (`<home>/workspace/ts-spaces/<deploy_slug>/audits/<auditDir>/`, the same root crew-api.js and readback-disk.js resolve) and verifies `screenshot.png` + `screenshot-mobile.png` are files before claiming "desktop + mobile captures below" — a bare `--audit-dir` name is never trusted. Missing deploy_slug, missing dir, or missing PNGs gets the honest no-captures opener and Evidence line (terminal surfaces named explicitly: "terminal surface — QA judges the CLI, not pixels"); the caller only appends attachment lines when its own freshness check passes. "Latest" is by event timestamp, sorted explicitly. Exits non-zero on missing input.
18
18
  - `edit-image.py` — deterministic pixel-level evidence editing (2026-09-15, Pillow): `crop --region x,y,w,h` (pixel-exact, rejects out-of-bounds), `zoom --factor <f> [--center x,y]` (nearest-neighbor, returns to source frame size), `label --text <caption>` (caption bar), `nup --cols <n> --in <a.png> --in <b.png> [--labels "a|b"]` (side-by-side grid). Byte-deterministic PNG output; no network, no time, no randomness. Derivatives supplement raw frames — the source frame stays archived.
19
19
  - `render-html.js` — deterministic HTML evidence composition (2026-09-15, headless Chromium): renders a local HTML layout to PNG (fixed viewport width, device scale 1, full-page screenshot). Hermetic: remote HTTP(S) assets are blocked and fail the render loudly; relative image paths resolve against the HTML file; local/system fonts only. Exit 3 reports NOT POSSIBLE when Chromium is unavailable. The composition layer above edit-image.py's pixel layer.
20
20
  - `see-act.js` — single-step browser driver for experiential QA (2026-09-14): one browser action per invocation (`aria`, `shot`, `click`, `scroll`, `type`), one JSON line on stdout, exit 0/2/3 (3 = NOT POSSIBLE: missing playwright-core or Chromium). Spawns its own loopback forward proxy on an ephemeral port (Chromium blocks direct loopback). The QA work agent closes the OODA loop: run a step, read the screenshot/aria, decide the next. Determinism: no wall-clock reads, no randomness. `SEE_ACT_ARCHIVE_DIR=<phase-dir>` (2026-09-14): every screenshot is archived automatically as `001-shot-desktop.png`, `002-click-mobile.png`, ... (counter in `<dir>/.seq`); `--out` becomes optional; JSON carries `screenshot` + `archived`; explicit `--out` + archive copies into the archive; unusable dir is NOT POSSIBLE exit 3.
@@ -34,4 +34,4 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
34
34
  - `sample-project.js` — optional sample-project setup (2026-09-17): extracts `seed/sample-project.tar.gz` (stripping the `sample-project/` prefix), `git init`, initial commit. No remote required. Registration happens via the existing `create-project` action — no sample-specific endpoint. CLI: `--tarball <path> --dest <dir> --name <project-name>`.
35
35
  - `compute-publish-diff.js` — deterministic publish-diff computer (2026-09-18, room #14): replaces the agent-ferried `git diff` JSON string field (the JSON ferry dropped a valid 700-line diff — "Publish diff parsed to zero files"). Runs `git diff <base> <HEAD>` via argv (no shell), writes the raw diff bytes to `--out`, and prints only a small JSON summary (`commit`, `base`, `diff_file`, `sha256`, `bytes`, `file_count`, `changed_lines` = added+removed, never raw diff output lines, `files`, `has_binary`, `has_rename`). Empty-tree base is the first-publish ancestor (hardcoded, never `merge-base` on the empty tree); non-ancestor is semantic JSON (`ok:false, error:"not-ancestor"`, exit 0); usage errors exit 2 with nothing on stdout. Pinned per-run alongside the other lifecycle files (`COMPUTE_DIFF` in `PIN_BASENAMES`) so a mid-run release swap cannot change it under the workflow.
36
36
  - `classify-surface.js` — deterministic project-surface classifier (2026-09-18, room #14 defect): a pure, never-throwing scan of the registered repo — `space.json` present → `artifact` surface (deploy_slug suggestion = `space.json`'s valid slug, verbatim — presence alone still means "web app"), `package.json` with a non-empty `bin` → `terminal` surface, otherwise null (unclassified); artifact wins conflicts (the deployed artifact is what users see, same rule as workflows/standard.js). CLI `--repo-path` prints one JSON object (`{surface, deploy_slug, signals}`), always exit 0 (classification failure is a null result, exit 2 only for usage). `create-project` fills omitted `environment_type`/`deploy_type`/`deploy_slug` from the classification; explicit values (including explicit null) always win and drive the effective surface.
37
- - `advance-publish-base.js` — publish-base advance past the setup scaffold (2026-09-18, room #14): `create-project` stamps provenance at the pre-scaffold repo HEAD, then crew-init commits `.orchestration/` (+ the consented `.gitignore`) on top — the first task's publish diff would carry the whole scaffold (517 insertions) and blow the 200-line diff-transport budget. The script moves `source_commit` to the scaffold commit only when `--project-created` (the stamp is this init's, never a real publish's), the scaffold commit's parent is exactly the stamped base, and the scaffold touches only crew-owned paths (`.orchestration/`, `.gitignore`); no provenance + root scaffold commit stamps fresh. Every other case skips without touching provenance; `published_at` is preserved on advance. Called from crew-init.js (best-effort, non-fatal). CLI: `--crew-home <path> --repo <path> --scaffold-sha <sha> [--project-created]`.
37
+ - `advance-publish-base.js` — publish-base advance past the setup scaffold (2026-09-18, room #14): `create-project` stamps provenance at the pre-scaffold repo HEAD, then crew-init commits `.orchestration/` (+ the consented `.gitignore`) on top — the first task's publish diff would carry the whole scaffold (517 insertions) and blow the 200-line diff-transport budget. The script moves `source_commit` to the scaffold commit only when `--project-created` (the stamp is this init's, never a real publish's), the scaffold commit's parent is exactly the stamped base, and the scaffold touches only crew-owned paths (`.orchestration/`, `.gitignore`); no provenance + root scaffold commit stamps fresh. Every other case skips without touching provenance; `published_at` is preserved on advance. Called from crew-init.js (best-effort, non-fatal). Provenance is per-project (room #15 blocker 8): `--project-id` is required and every read/stamp is project-scoped. CLI: `--crew-home <path> --repo <path> --scaffold-sha <sha> --project-id <id> [--project-created]`.
@@ -39,7 +39,7 @@ const RELEASE_RE = /^([0-9a-f]{40}|pkg-\d+\.\d+\.\d+)$/;
39
39
 
40
40
  function usage() {
41
41
  console.error(
42
- "Usage: node lib/advance-publish-base.js --crew-home <path> --repo <path> --scaffold-sha <sha> --project-created"
42
+ "Usage: node lib/advance-publish-base.js --crew-home <path> --repo <path> --scaffold-sha <sha> --project-id <id> --project-created"
43
43
  );
44
44
  }
45
45
 
@@ -50,6 +50,7 @@ function parseArgv(argv) {
50
50
  if (a === "--crew-home") out.crewHome = argv[++i];
51
51
  else if (a === "--repo") out.repo = argv[++i];
52
52
  else if (a === "--scaffold-sha") out.scaffoldSha = argv[++i];
53
+ else if (a === "--project-id") out.projectId = argv[++i];
53
54
  else if (a === "--project-created") out.projectCreated = true;
54
55
  else {
55
56
  console.error("unknown argument: " + a);
@@ -94,10 +95,10 @@ function skip(reason) {
94
95
  }
95
96
 
96
97
  function main() {
97
- const { crewHome, repo, scaffoldSha, projectCreated } = parseArgv(
98
+ const { crewHome, repo, scaffoldSha, projectId, projectCreated } = parseArgv(
98
99
  process.argv.slice(2)
99
100
  );
100
- if (!crewHome || !repo || !scaffoldSha) {
101
+ if (!crewHome || !repo || !scaffoldSha || !projectId) {
101
102
  usage();
102
103
  process.exit(2);
103
104
  }
@@ -112,7 +113,7 @@ function main() {
112
113
 
113
114
  let provenance;
114
115
  try {
115
- provenance = crewApi(crewHome, "get-provenance").provenance;
116
+ provenance = crewApi(crewHome, "get-provenance", JSON.stringify({ project_id: projectId })).provenance;
116
117
  } catch (e) {
117
118
  console.error("get-provenance failed: " + (e.message || e));
118
119
  process.exit(1);
@@ -167,10 +168,17 @@ function main() {
167
168
  skip("no provenance stamped and the active release name is not a valid crew_release (" + crewRelease + ")");
168
169
  return;
169
170
  }
170
- const stamped = crewApi(crewHome, "set-provenance", JSON.stringify({
171
- source_commit: scaffoldSha,
172
- crew_release: crewRelease,
173
- }));
171
+ let stamped;
172
+ try {
173
+ stamped = crewApi(crewHome, "set-provenance", JSON.stringify({
174
+ project_id: projectId,
175
+ source_commit: scaffoldSha,
176
+ crew_release: crewRelease,
177
+ }));
178
+ } catch (e) {
179
+ console.error("set-provenance failed: " + (e.message || e));
180
+ process.exit(1);
181
+ }
174
182
  if (!stamped || stamped.ok !== true) {
175
183
  console.error("set-provenance did not return ok");
176
184
  process.exit(1);
@@ -215,19 +223,32 @@ function main() {
215
223
  return;
216
224
  }
217
225
 
218
- const stamped = crewApi(crewHome, "set-provenance", JSON.stringify({
219
- source_commit: scaffoldSha,
220
- crew_release: crewRelease,
221
- // A base-pointer move is not a publish: keep the existing published_at.
222
- ...(provenance.published_at ? { published_at: provenance.published_at } : {}),
223
- }));
226
+ let stamped;
227
+ try {
228
+ stamped = crewApi(crewHome, "set-provenance", JSON.stringify({
229
+ project_id: projectId,
230
+ source_commit: scaffoldSha,
231
+ crew_release: crewRelease,
232
+ // A base-pointer move is not a publish: keep the existing published_at.
233
+ ...(provenance.published_at ? { published_at: provenance.published_at } : {}),
234
+ }));
235
+ } catch (e) {
236
+ console.error("set-provenance failed: " + (e.message || e));
237
+ process.exit(1);
238
+ }
224
239
  if (!stamped || stamped.ok !== true) {
225
240
  console.error("set-provenance did not return ok");
226
241
  process.exit(1);
227
242
  }
228
243
  // Exact read-back: the stamp must match what we intended, or it is fiction
229
244
  // (same discipline as lib/verify-publish.js).
230
- const recheck = crewApi(crewHome, "get-provenance").provenance;
245
+ let recheck;
246
+ try {
247
+ recheck = crewApi(crewHome, "get-provenance", JSON.stringify({ project_id: projectId })).provenance;
248
+ } catch (e) {
249
+ console.error("post-stamp get-provenance failed: " + (e.message || e));
250
+ process.exit(1);
251
+ }
231
252
  if (!recheck || recheck.source_commit !== scaffoldSha) {
232
253
  console.error("post-stamp read-back mismatch: provenance does not point at the scaffold commit");
233
254
  process.exit(1);
@@ -154,7 +154,8 @@ if (!verdict) {
154
154
  }
155
155
 
156
156
  // Provenance: this task's own newest `publish: verified` event first; the
157
- // global provenance record only when it belongs to this task.
157
+ // project-scoped provenance record only when it belongs to this task
158
+ // (provenance is per-project — room #15 blocker 8).
158
159
  let commit = null;
159
160
  const verifiedNote = notesNewest
160
161
  .find((e) => (e.message || "").includes("publish: verified"));
@@ -162,9 +163,9 @@ if (verifiedNote) {
162
163
  const m = /\b([0-9a-f]{40})\b/.exec(verifiedNote.message);
163
164
  if (m) commit = m[1];
164
165
  }
165
- if (!commit) {
166
+ if (!commit && project) {
166
167
  try {
167
- const prov = api("get-provenance", {}).provenance;
168
+ const prov = api("get-provenance", { project_id: project.id }).provenance;
168
169
  if (prov && prov.task_id === taskId && /^[0-9a-f]{40}$/.test(prov.source_commit || "")) {
169
170
  commit = prov.source_commit;
170
171
  }