muse-crew 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +35 -4
- package/docs/publish-verification.md +11 -3
- package/lib/AGENTS.md +4 -4
- package/lib/advance-publish-base.js +36 -15
- package/lib/compose-evidence-caption.js +4 -3
- package/lib/crew-api.js +249 -46
- package/lib/schema.sql +12 -0
- package/lib/test-worktree-backend.sh +29 -0
- package/lib/verify-publish.js +4 -3
- package/lib/worktree-lifecycle.sh +9 -3
- package/package.json +1 -1
- package/seed/cron-body-template.md +5 -3
- package/workflows/bugfix.js +12 -7
- package/workflows/chore.js +11 -6
- package/workflows/crew-init.js +20 -4
- package/workflows/standard.js +12 -7
- package/workflows/upgrade.js +8 -3
package/API.md
CHANGED
|
@@ -474,18 +474,49 @@ Set a global configuration value.
|
|
|
474
474
|
|
|
475
475
|
### `setprovenance`
|
|
476
476
|
|
|
477
|
-
Stamp publication provenance.
|
|
477
|
+
Stamp publication provenance. Provenance is per-project (2026-09-18): each
|
|
478
|
+
project row carries its own publish base (`provenance_source_commit`,
|
|
479
|
+
`provenance_crew_release`, `provenance_published_at`, `provenance_task_id`);
|
|
480
|
+
a crew home with N artifact projects carries N independent bases. There are
|
|
481
|
+
two stamp call paths, and they are different operations on the same row:
|
|
482
|
+
|
|
483
|
+
- **Full stamp** — called by the parent's publish verification (`lib/verify-publish.js`)
|
|
484
|
+
after independently verifying the rebuilt artifact: `source_commit` is the
|
|
485
|
+
verified build commit, `crew_release` the active release at publish time.
|
|
486
|
+
- **Refresh** — called by the release installer (`lib/advance-publish-base.js`)
|
|
487
|
+
after installing/activating a crew release: `crew_release` is refreshed to
|
|
488
|
+
the live release identity while preserving the existing `source_commit`.
|
|
489
|
+
A base-pointer move is not a publish.
|
|
490
|
+
|
|
491
|
+
Both paths flow through the single writer in `lib/crew-api.js`; an unknown
|
|
492
|
+
`project_id` fails closed (exit 3), and omitting `project_id` is a usage
|
|
493
|
+
error (exit 2) — there is no silent global fallback.
|
|
478
494
|
|
|
479
495
|
| Field | Type | Required | Notes |
|
|
480
496
|
|-------|------|----------|-------|
|
|
497
|
+
| `project_id` | string | yes | The project whose publish base is stamped |
|
|
481
498
|
| `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
|
|
483
|
-
| `published_at` | ISO 8601 datetime (UTC) | yes | |
|
|
499
|
+
| `crew_release` | 40-char hex, or `pkg-<semver>` | yes | Active crew release (`$CREW_HOME/current`) at stamp time |
|
|
500
|
+
| `published_at` | ISO 8601 datetime (UTC) | yes | Defaults to now when omitted |
|
|
484
501
|
| `task_id` | string | no | Publishing task, for traceability |
|
|
485
502
|
|
|
503
|
+
A one-time migration attributes a pre-per-project legacy `config.provenance.*`
|
|
504
|
+
triple to a project row only when its `source_commit` is an ancestor of
|
|
505
|
+
exactly one project's repo HEAD (verified with git); zero or ambiguous
|
|
506
|
+
matches are recorded as `deferred:<reason>` and never attributed. A triple
|
|
507
|
+
that matches exactly one project but violates the writer's shape rules
|
|
508
|
+
(e.g. a hand-written abbreviated sha, which git resolves for the ancestor
|
|
509
|
+
check but which fails the 40-char storage rule) is recorded as
|
|
510
|
+
`deferred:stamp-failed:<reason>` — the migration never throws, so a bad
|
|
511
|
+
triple can never brick the home. A `deferred:*` watermark is re-evaluated
|
|
512
|
+
when a project is created (the only event that changes the match set), so
|
|
513
|
+
a triple that predates the home's first project is attributed once a
|
|
514
|
+
descendant project exists. The legacy keys are inert dead storage after a
|
|
515
|
+
successful migration — new code never reads them.
|
|
516
|
+
|
|
486
517
|
### `getprovenance`
|
|
487
518
|
|
|
488
|
-
Read
|
|
519
|
+
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
520
|
|
|
490
521
|
---
|
|
491
522
|
|
|
@@ -223,7 +223,9 @@ For a task parked with `publish: verification-requested <commit>`:
|
|
|
223
223
|
slug (the project's publish target).
|
|
224
224
|
2. **Expected change.** The publish delta is `git diff <base> <commit>`
|
|
225
225
|
in `repo_path`, where `<base>` is the previously-stamped provenance
|
|
226
|
-
`source_commit` (read it via
|
|
226
|
+
`source_commit` for this project (read it via
|
|
227
|
+
`get-provenance --json '{"project_id":"<id>"}'` — provenance is
|
|
228
|
+
per-project; without `project_id` the call exits 2; use the empty-tree sha
|
|
227
229
|
`4b825dc642cb6eb9a060e54bf8d69288fbee4904` when no provenance is
|
|
228
230
|
stamped yet — a first publish). Never use `commit^1` as the base and
|
|
229
231
|
never take the expected change from the builder's report: push-time
|
|
@@ -293,9 +295,15 @@ For a task parked with `publish: verification-requested <commit>`:
|
|
|
293
295
|
(the crew-owned store). Do NOT use the artifact's `setprovenance`
|
|
294
296
|
action — it writes a different, non-authoritative store that QA never
|
|
295
297
|
reads, so the stamp would be invisible to every gate:
|
|
296
|
-
`set-provenance --json '{"source_commit":"<commit>","crew_release":"<release>","task_id":"<task>"}'`
|
|
298
|
+
`set-provenance --json '{"project_id":"<id>","source_commit":"<commit>","crew_release":"<release>","task_id":"<task>"}'`
|
|
297
299
|
(crew_release is the basename of the active release, e.g.
|
|
298
|
-
`pkg-0.7.10`).
|
|
300
|
+
`pkg-0.7.10`). This is the full publish-verification stamp — it
|
|
301
|
+
records a verified publish. Do not confuse it with the
|
|
302
|
+
provenance *refresh* (re-pointing `crew_release` at the active
|
|
303
|
+
release after a crew upgrade without a new publish), which also goes
|
|
304
|
+
through `set-provenance` but keeps the existing `source_commit`.
|
|
305
|
+
Then read the stamp back with
|
|
306
|
+
`get-provenance --json '{"project_id":"<id>"}'`
|
|
299
307
|
and confirm source_commit, crew_release, and task_id match exactly
|
|
300
308
|
what was sent — a stamp that cannot be read back is not a stamp. Only
|
|
301
309
|
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
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
}
|
package/lib/crew-api.js
CHANGED
|
@@ -88,12 +88,148 @@ function resolveCrewHome(flagValue) {
|
|
|
88
88
|
|
|
89
89
|
const SCHEMA_PATH = join(import.meta.dirname, "schema.sql");
|
|
90
90
|
|
|
91
|
+
// Provenance shape rules, shared by the writer and the backfill's
|
|
92
|
+
// pre-validation so they can never diverge (room #15, 2026-09-18).
|
|
93
|
+
const PROVENANCE_SOURCE_COMMIT_RE = /^[0-9a-f]{40}$/;
|
|
94
|
+
const PROVENANCE_CREW_RELEASE_RE = /^([0-9a-f]{40}|pkg-\d+\.\d+\.\d+)$/;
|
|
95
|
+
|
|
96
|
+
// One-time backfill of the legacy crew-home-global provenance triple
|
|
97
|
+
// (config.provenance.*) into the per-project provenance columns (room #15
|
|
98
|
+
// blocker 8, 2026-09-18). Attribution is never fabricated: the legacy base
|
|
99
|
+
// is stamped onto a project ONLY when it is an ancestor of exactly one
|
|
100
|
+
// project's repo HEAD (verified with git, never assumed). Zero or ambiguous
|
|
101
|
+
// matches are recorded as deferred:<reason> and the legacy keys are left
|
|
102
|
+
// untouched — new readers see null (empty-tree first publish) while old
|
|
103
|
+
// readers keep their global triple. Watermarked in config, so it runs at
|
|
104
|
+
// most once per crew home. Benign edge: a legacy key written by the very
|
|
105
|
+
// command whose open triggers the backfill is re-created after the DELETE
|
|
106
|
+
// and lingers in config — inert dead storage, never read by new code.
|
|
107
|
+
function backfillProjectProvenance(db) {
|
|
108
|
+
let wm = db.prepare("SELECT value FROM config WHERE key = 'provenance.backfill'").get();
|
|
109
|
+
if (wm && wm.value === "none") {
|
|
110
|
+
// "none" means "no legacy triple at the last look". A triple that
|
|
111
|
+
// arrives later (hand-written via update-config — the key space is
|
|
112
|
+
// open) is new evidence, not a settled state: clear the watermark and
|
|
113
|
+
// evaluate it. Any other watermark (stamped:*, deferred:*) is terminal.
|
|
114
|
+
const legacy = db.prepare(
|
|
115
|
+
"SELECT key FROM config WHERE key LIKE 'provenance.%' AND key != 'provenance.backfill'"
|
|
116
|
+
).all();
|
|
117
|
+
if (legacy.length > 0) {
|
|
118
|
+
db.prepare("DELETE FROM config WHERE key = 'provenance.backfill'").run();
|
|
119
|
+
wm = null;
|
|
120
|
+
} else {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (wm) return;
|
|
125
|
+
const setWatermark = (v) =>
|
|
126
|
+
db.prepare("INSERT OR IGNORE INTO config (key, value) VALUES ('provenance.backfill', ?)").run(v);
|
|
127
|
+
const rows = db.prepare(
|
|
128
|
+
"SELECT key, value FROM config WHERE key IN ('provenance.source_commit','provenance.crew_release','provenance.published_at','provenance.task_id')"
|
|
129
|
+
).all();
|
|
130
|
+
const byKey = Object.fromEntries(rows.map((r) => [r.key, r.value]));
|
|
131
|
+
const sc = byKey["provenance.source_commit"];
|
|
132
|
+
const cr = byKey["provenance.crew_release"];
|
|
133
|
+
const pa = byKey["provenance.published_at"];
|
|
134
|
+
if (!sc && !cr && !pa && !byKey["provenance.task_id"]) {
|
|
135
|
+
// No legacy provenance at all: nothing to attribute, ever.
|
|
136
|
+
setWatermark("none");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!sc || !cr || !pa) {
|
|
140
|
+
// Incomplete triple (partial hand-write): do NOT watermark — the next
|
|
141
|
+
// open retries. Watermarking here would strand keys that arrive later.
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const matches = [];
|
|
145
|
+
for (const p of db.prepare("SELECT id, repo_path FROM projects").all()) {
|
|
146
|
+
try {
|
|
147
|
+
execFileSync("git", ["merge-base", "--is-ancestor", sc, "HEAD"],
|
|
148
|
+
{ cwd: p.repo_path, encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "ignore"] });
|
|
149
|
+
matches.push(p.id);
|
|
150
|
+
} catch {
|
|
151
|
+
// Not an ancestor, or the repo is unreadable: not a match.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (matches.length === 1) {
|
|
155
|
+
// Validate the legacy triple against the writer's shape rules BEFORE
|
|
156
|
+
// touching the database: a hand-written abbreviated sha passes
|
|
157
|
+
// `git merge-base --is-ancestor` (git resolves abbreviations) but
|
|
158
|
+
// violates the storage CHECK — and a throw here would escape openDb
|
|
159
|
+
// and hard-fail every CLI command against the home. The backfill's
|
|
160
|
+
// whole contract is best-effort attribution, so a bad shape is a
|
|
161
|
+
// deferred watermark, never an exception. Legacy keys are retained
|
|
162
|
+
// read-only for old readers, exactly like the other deferred cases.
|
|
163
|
+
const shapeProblem =
|
|
164
|
+
!PROVENANCE_SOURCE_COMMIT_RE.test(sc ?? "")
|
|
165
|
+
? `source_commit not 40-char hex (${String(sc).slice(0, 24)})`
|
|
166
|
+
: !PROVENANCE_CREW_RELEASE_RE.test(cr ?? "")
|
|
167
|
+
? `crew_release not 40-char hex or pkg-<semver> (${String(cr).slice(0, 24)})`
|
|
168
|
+
: null;
|
|
169
|
+
if (shapeProblem) {
|
|
170
|
+
setWatermark(`deferred:stamp-failed:${shapeProblem}`);
|
|
171
|
+
} else {
|
|
172
|
+
db.exec("BEGIN");
|
|
173
|
+
try {
|
|
174
|
+
db.prepare(
|
|
175
|
+
`UPDATE projects SET provenance_source_commit = ?, provenance_crew_release = ?,
|
|
176
|
+
provenance_published_at = ?, provenance_task_id = ?, updated_at = ? WHERE id = ?`
|
|
177
|
+
).run(sc, cr, pa, byKey["provenance.task_id"] ?? null, now(), matches[0]);
|
|
178
|
+
db.prepare("DELETE FROM config WHERE key LIKE 'provenance.%' AND key != 'provenance.backfill'").run();
|
|
179
|
+
setWatermark(`stamped:${matches[0]}`);
|
|
180
|
+
db.exec("COMMIT");
|
|
181
|
+
} catch (e) {
|
|
182
|
+
db.exec("ROLLBACK");
|
|
183
|
+
const reason = String((e && e.message) || e).replace(/[\r\n]+/g, " ").slice(0, 100);
|
|
184
|
+
setWatermark(`deferred:stamp-failed:${reason}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else if (matches.length === 0) {
|
|
188
|
+
setWatermark(`deferred:no-ancestor-match:${String(sc).slice(0, 7)}`);
|
|
189
|
+
} else {
|
|
190
|
+
setWatermark(`deferred:ambiguous:${matches.length}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Re-run a deferred backfill when the match set may have changed: a new
|
|
195
|
+
// project was just created, and a `deferred:no-ancestor-match` or
|
|
196
|
+
// `deferred:ambiguous` watermark from an earlier look can now resolve —
|
|
197
|
+
// e.g. a home whose legacy triple predates its first project. Only
|
|
198
|
+
// deferred:* watermarks are retried, and only while the legacy keys still
|
|
199
|
+
// exist (a successful stamp deleted them). Returns the stamped project id,
|
|
200
|
+
// or null. Never throws for a bad triple — the evaluation watermarks
|
|
201
|
+
// instead (see above); a throw here means the database itself is broken,
|
|
202
|
+
// and failing project creation is then the honest outcome.
|
|
203
|
+
function retryDeferredBackfill(db) {
|
|
204
|
+
const wm = db.prepare("SELECT value FROM config WHERE key = 'provenance.backfill'").get();
|
|
205
|
+
if (!wm || !String(wm.value).startsWith("deferred:")) return null;
|
|
206
|
+
const legacy = db.prepare(
|
|
207
|
+
"SELECT key FROM config WHERE key IN ('provenance.source_commit','provenance.crew_release','provenance.published_at')"
|
|
208
|
+
).all();
|
|
209
|
+
if (legacy.length === 0) return null;
|
|
210
|
+
// Lift the terminal watermark, re-evaluate, and let the evaluation write
|
|
211
|
+
// the new one. If the re-evaluation throws (it shouldn't — every path
|
|
212
|
+
// watermarks), restore the old watermark so the deferral isn't lost.
|
|
213
|
+
const old = wm.value;
|
|
214
|
+
db.prepare("DELETE FROM config WHERE key = 'provenance.backfill'").run();
|
|
215
|
+
try {
|
|
216
|
+
backfillProjectProvenance(db);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
db.prepare("INSERT OR IGNORE INTO config (key, value) VALUES ('provenance.backfill', ?)").run(old);
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
221
|
+
const wm2 = db.prepare("SELECT value FROM config WHERE key = 'provenance.backfill'").get();
|
|
222
|
+
const m = wm2 && /^stamped:(.+)$/.exec(String(wm2.value));
|
|
223
|
+
return m ? m[1] : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
91
226
|
function openDb(crewHome) {
|
|
92
227
|
const dbPath = join(crewHome, "crew-state.db");
|
|
93
228
|
const fresh = !existsSync(dbPath);
|
|
94
229
|
const db = new DatabaseSync(dbPath);
|
|
95
230
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
96
231
|
db.exec("PRAGMA foreign_keys = ON;");
|
|
232
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
97
233
|
if (fresh) {
|
|
98
234
|
const schema = readFileSync(SCHEMA_PATH, "utf8");
|
|
99
235
|
db.exec(schema);
|
|
@@ -139,6 +275,29 @@ function openDb(crewHome) {
|
|
|
139
275
|
if (!/duplicate column name/i.test(e.message)) throw e;
|
|
140
276
|
}
|
|
141
277
|
}
|
|
278
|
+
// Migration: per-project provenance columns (room #15 blocker 8,
|
|
279
|
+
// 2026-09-18). Same idempotent PRAGMA-check pattern; ADD COLUMN without
|
|
280
|
+
// a default leaves existing rows NULL, which is "no provenance yet" by
|
|
281
|
+
// design — attribution is never fabricated, so there is no backfill step
|
|
282
|
+
// here beyond the watermarked legacy-triple attribution below.
|
|
283
|
+
const provMigration = [
|
|
284
|
+
["provenance_source_commit", "TEXT CHECK (provenance_source_commit IS NULL OR length(provenance_source_commit) = 40)"],
|
|
285
|
+
["provenance_crew_release", "TEXT"],
|
|
286
|
+
["provenance_published_at", "TEXT"],
|
|
287
|
+
["provenance_task_id", "TEXT"],
|
|
288
|
+
];
|
|
289
|
+
const provColNames = new Set(db.prepare("PRAGMA table_info(projects)").all().map((c) => c.name));
|
|
290
|
+
for (const [colName, colType] of provMigration) {
|
|
291
|
+
if (!provColNames.has(colName)) {
|
|
292
|
+
try {
|
|
293
|
+
db.exec(`ALTER TABLE projects ADD COLUMN ${colName} ${colType}`);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
if (!/duplicate column name/i.test(e.message)) throw e;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Legacy global-provenance backfill (watermarked, runs at most once).
|
|
300
|
+
backfillProjectProvenance(db);
|
|
142
301
|
}
|
|
143
302
|
return db;
|
|
144
303
|
}
|
|
@@ -276,7 +435,7 @@ function requireTask(db, id) {
|
|
|
276
435
|
|
|
277
436
|
function requireProject(db, id) {
|
|
278
437
|
const row = db.prepare("SELECT * FROM projects WHERE id = ?").get(id);
|
|
279
|
-
if (!row) throw notFound(
|
|
438
|
+
if (!row) throw notFound(`Project not found: "${id}" — check list-projects, or create it with create-project.`);
|
|
280
439
|
return row;
|
|
281
440
|
}
|
|
282
441
|
|
|
@@ -1263,16 +1422,27 @@ commands["get-events"] = (db, args) => {
|
|
|
1263
1422
|
// against this code's own realpath (the release it is actually running
|
|
1264
1423
|
// from). Unresolvable or disputed => throw fail-closed BEFORE the claim
|
|
1265
1424
|
// transaction: provenance is never stamped with an unknown release.
|
|
1266
|
-
|
|
1425
|
+
//
|
|
1426
|
+
// Active-release resolution is split in two (room #15, 2026-09-18):
|
|
1427
|
+
// resolveActiveReleaseName (symlink only — for writers) below, and
|
|
1428
|
+
// resolveActiveRelease (symlink + self-path cross-check — for verifiers).
|
|
1429
|
+
function resolveActiveReleaseName(crewHome) {
|
|
1267
1430
|
let linkTarget;
|
|
1268
1431
|
try {
|
|
1269
1432
|
linkTarget = readlinkSync(join(crewHome, "current"));
|
|
1270
1433
|
} catch (e) {
|
|
1271
1434
|
throw usageError(
|
|
1272
|
-
`
|
|
1435
|
+
`cannot resolve the active release: ${join(crewHome, "current")} is not a readable symlink (${e.message}).`
|
|
1273
1436
|
);
|
|
1274
1437
|
}
|
|
1275
|
-
|
|
1438
|
+
return basename(resolve(crewHome, linkTarget));
|
|
1439
|
+
}
|
|
1440
|
+
// The strict resolver for verifiers (scan-verification-pending): the symlink
|
|
1441
|
+
// answer PLUS the self-path cross-check — the running code must provably be
|
|
1442
|
+
// the active release's own crew-api.js before it stamps verification claims.
|
|
1443
|
+
// Stale cron bodies and mid-deploy swaps throw fail-closed here.
|
|
1444
|
+
function resolveActiveRelease(crewHome) {
|
|
1445
|
+
const activeRelease = resolveActiveReleaseName(crewHome);
|
|
1276
1446
|
const selfPath = new URL(import.meta.url).pathname;
|
|
1277
1447
|
const selfMatch = selfPath.match(/releases\/([^/]+)\/lib\/crew-api\.js$/);
|
|
1278
1448
|
if (!selfMatch) {
|
|
@@ -1575,39 +1745,49 @@ commands["record-phase"] = (db, args) => {
|
|
|
1575
1745
|
// the JSON transport and over the 200-line budget. The artifact is built from
|
|
1576
1746
|
// the repo checkout at registration time, so create-project stamps
|
|
1577
1747
|
// source_commit = repo HEAD for artifact-deploy projects; the first publish
|
|
1578
|
-
//
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1581
|
-
//
|
|
1582
|
-
//
|
|
1583
|
-
|
|
1748
|
+
// The initial-provenance stamp at create-project time: an artifact project
|
|
1749
|
+
// opens with its publish base set to the repo HEAD at registration, so the
|
|
1750
|
+
// first publish diffs only the task's own changes. Per-project (room #15
|
|
1751
|
+
// blocker 8): the project row was just inserted, so it is unstamped by
|
|
1752
|
+
// construction — there is no "already stamped" skip; a second artifact
|
|
1753
|
+
// project gets its own row, never the first project's base. A skipped stamp
|
|
1754
|
+
// is reported, never silent, and never fails project creation.
|
|
1755
|
+
function initialProvenancePlan({ deployType, headSha, releaseName }) {
|
|
1584
1756
|
if (deployType !== "artifact") return { stamped: false, reason: "deploy_type is not artifact" };
|
|
1585
|
-
if (provenanceExists) return { stamped: false, reason: "provenance already stamped" };
|
|
1586
1757
|
if (!headSha) return { stamped: false, reason: "repo HEAD is unresolvable (unborn repo?)" };
|
|
1587
1758
|
if (!releaseName) return { stamped: false, reason: "active crew release is unresolvable" };
|
|
1588
1759
|
return { stamped: true, source_commit: headSha, crew_release: releaseName };
|
|
1589
1760
|
}
|
|
1590
1761
|
|
|
1591
|
-
function tryStampInitialProvenance(db, crewHome, repoPath, deployType) {
|
|
1592
|
-
const keys = db.prepare(
|
|
1593
|
-
"SELECT key FROM config WHERE key IN ('provenance.source_commit','provenance.crew_release','provenance.published_at')"
|
|
1594
|
-
).all().map((r) => r.key);
|
|
1595
|
-
const provenanceExists = keys.length === 3;
|
|
1762
|
+
function tryStampInitialProvenance(db, projectId, crewHome, repoPath, deployType) {
|
|
1596
1763
|
let headSha = null;
|
|
1597
1764
|
try {
|
|
1598
1765
|
headSha = execFileSync("git", ["rev-parse", "HEAD"],
|
|
1599
|
-
{ cwd: repoPath, encoding: "utf8", timeout: 10000 }).trim();
|
|
1766
|
+
{ cwd: repoPath, encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1600
1767
|
if (!/^[0-9a-f]{40}$/.test(headSha)) headSha = null;
|
|
1601
1768
|
} catch { headSha = null; }
|
|
1602
1769
|
let releaseName = null;
|
|
1603
|
-
|
|
1604
|
-
|
|
1770
|
+
// Symlink-only resolution: the stamp records the crew home's ACTIVE
|
|
1771
|
+
// release — it must not depend on which copy of this file is running.
|
|
1772
|
+
// The strict self-path cross-check lives in resolveActiveRelease, used by
|
|
1773
|
+
// scan-verification-pending; requiring it here silently skipped the stamp
|
|
1774
|
+
// for every setup path that invokes crew-api.js outside the release tree
|
|
1775
|
+
// (room #15: create-project through the npm package's own lib/).
|
|
1776
|
+
try { releaseName = resolveActiveReleaseName(crewHome); } catch { releaseName = null; }
|
|
1777
|
+
const plan = initialProvenancePlan({ deployType, headSha, releaseName });
|
|
1605
1778
|
if (!plan.stamped) return plan;
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1779
|
+
try {
|
|
1780
|
+
stampProvenance(db, {
|
|
1781
|
+
project_id: projectId,
|
|
1782
|
+
source_commit: plan.source_commit,
|
|
1783
|
+
crew_release: plan.crew_release,
|
|
1784
|
+
});
|
|
1785
|
+
} catch (e) {
|
|
1786
|
+
// The stamp is best-effort: a writer rejection (e.g. a release name
|
|
1787
|
+
// outside the stampable shape) skips the stamp, never the project.
|
|
1788
|
+
// Reported via the reason, never silent.
|
|
1789
|
+
return { stamped: false, reason: `initial stamp rejected: ${e.message}` };
|
|
1790
|
+
}
|
|
1611
1791
|
return plan;
|
|
1612
1792
|
}
|
|
1613
1793
|
|
|
@@ -1675,7 +1855,17 @@ commands["create-project"] = (db, args, ctx) => {
|
|
|
1675
1855
|
description, simultaneity, quiesced, visual_protocol, environment_type, created_at, updated_at)
|
|
1676
1856
|
VALUES (@id, @display_name, @repo_path, @deploy_type, @deploy_slug,
|
|
1677
1857
|
@description, @simultaneity, @quiesced, @visual_protocol, @environment_type, @created_at, @updated_at)`).run(row);
|
|
1678
|
-
|
|
1858
|
+
// A deferred backfill (zero/ambiguous matches at the last look) gets one
|
|
1859
|
+
// more evaluation now that the match set changed: a new project whose
|
|
1860
|
+
// repo descends from the legacy commit inherits the attribution instead
|
|
1861
|
+
// of stranding the triple in config forever. When the backfill stamps
|
|
1862
|
+
// this project, the initial stamp below is skipped — the legacy triple
|
|
1863
|
+
// is a real publish record and must not be overwritten by the
|
|
1864
|
+
// creation-time HEAD.
|
|
1865
|
+
const backfillStampedId = retryDeferredBackfill(db);
|
|
1866
|
+
const initialProvenance = backfillStampedId === id
|
|
1867
|
+
? { stamped: true, reason: "legacy backfill attributed this project; initial stamp skipped" }
|
|
1868
|
+
: tryStampInitialProvenance(db, id, ctx.crewHome, repoPath, deployType);
|
|
1679
1869
|
return { project: mapProject(row), initial_provenance: initialProvenance };
|
|
1680
1870
|
};
|
|
1681
1871
|
|
|
@@ -1819,35 +2009,48 @@ commands["update-config"] = (db, args) => {
|
|
|
1819
2009
|
return { ok: true };
|
|
1820
2010
|
};
|
|
1821
2011
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2012
|
+
// The single provenance writer (room #15 blocker 8). Every stamp flows
|
|
2013
|
+
// through here: set-provenance and create-project's initial stamp are thin
|
|
2014
|
+
// shells over it. Per-project by design — a crew home with N artifact
|
|
2015
|
+
// projects carries N independent publish bases, and stamping one project
|
|
2016
|
+
// never touches another's row.
|
|
2017
|
+
function stampProvenance(db, { project_id, source_commit, crew_release, published_at, task_id }) {
|
|
2018
|
+
const pid = String(project_id ?? "").trim();
|
|
2019
|
+
if (!pid) throw usageError("project_id is required — pass the project's id (see list-projects).");
|
|
2020
|
+
requireProject(db, pid);
|
|
2021
|
+
if (!PROVENANCE_SOURCE_COMMIT_RE.test(source_commit ?? "")) throw usageError("source_commit must be 40-char hex.");
|
|
2022
|
+
if (!PROVENANCE_CREW_RELEASE_RE.test(crew_release ?? "")) {
|
|
1825
2023
|
throw usageError("crew_release must be 40-char hex or pkg-<semver>.");
|
|
1826
2024
|
}
|
|
1827
2025
|
// task_id is optional; when present it must be 1-80 chars (matches dashboard contract).
|
|
1828
|
-
const
|
|
1829
|
-
if (
|
|
2026
|
+
const tid = task_id ?? null;
|
|
2027
|
+
if (tid !== null && (typeof tid !== "string" || tid.trim().length < 1 || tid.length > 80)) {
|
|
1830
2028
|
throw usageError("task_id must be 1-80 chars when provided.");
|
|
1831
2029
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2030
|
+
db.prepare(
|
|
2031
|
+
`UPDATE projects SET provenance_source_commit = ?, provenance_crew_release = ?,
|
|
2032
|
+
provenance_published_at = ?, provenance_task_id = ?, updated_at = ?
|
|
2033
|
+
WHERE id = ?`
|
|
2034
|
+
).run(source_commit, crew_release, published_at ?? now(), tid, now(), pid);
|
|
2035
|
+
return { ok: true, project_id: pid };
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
commands["set-provenance"] = (db, args) => {
|
|
2039
|
+
return stampProvenance(db, args ?? {});
|
|
1839
2040
|
};
|
|
1840
2041
|
|
|
1841
|
-
commands["get-provenance"] = (db) => {
|
|
1842
|
-
const
|
|
1843
|
-
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2042
|
+
commands["get-provenance"] = (db, args) => {
|
|
2043
|
+
const pid = String((args ?? {}).project_id ?? "").trim();
|
|
2044
|
+
if (!pid) throw usageError("project_id is required — pass the project's id (see list-projects).");
|
|
2045
|
+
const row = requireProject(db, pid);
|
|
2046
|
+
if (!row.provenance_source_commit || !row.provenance_crew_release || !row.provenance_published_at) {
|
|
2047
|
+
return { provenance: null };
|
|
2048
|
+
}
|
|
1848
2049
|
return { provenance: {
|
|
1849
|
-
source_commit:
|
|
1850
|
-
|
|
2050
|
+
source_commit: row.provenance_source_commit,
|
|
2051
|
+
crew_release: row.provenance_crew_release,
|
|
2052
|
+
published_at: row.provenance_published_at,
|
|
2053
|
+
task_id: row.provenance_task_id ?? null,
|
|
1851
2054
|
} };
|
|
1852
2055
|
};
|
|
1853
2056
|
|
package/lib/schema.sql
CHANGED
|
@@ -34,6 +34,18 @@ CREATE TABLE IF NOT EXISTS projects (
|
|
|
34
34
|
-- deploy_type is NOT reused for this (it names the deployment target, not
|
|
35
35
|
-- the UX surface a user experiences).
|
|
36
36
|
environment_type TEXT CHECK (environment_type IN ('artifact', 'terminal')),
|
|
37
|
+
-- Per-project provenance (2026-09-18, room #15 blocker 8): each project
|
|
38
|
+
-- carries its own publish base. Provenance was crew-home-global
|
|
39
|
+
-- (config.provenance.*), which broke any multi-project home: the first
|
|
40
|
+
-- artifact project's stamp was read as the second project's base, parking
|
|
41
|
+
-- the second task with "not an ancestor". Nullable by design: a project
|
|
42
|
+
-- with no provenance reads null and its first publish diffs from the
|
|
43
|
+
-- empty tree. Never fabricated — the one-time backfill in openDb stamps
|
|
44
|
+
-- only an exactly-one-ancestor match, otherwise records deferred.
|
|
45
|
+
provenance_source_commit TEXT CHECK (provenance_source_commit IS NULL OR length(provenance_source_commit) = 40),
|
|
46
|
+
provenance_crew_release TEXT,
|
|
47
|
+
provenance_published_at TEXT,
|
|
48
|
+
provenance_task_id TEXT,
|
|
37
49
|
created_at TEXT NOT NULL,
|
|
38
50
|
updated_at TEXT NOT NULL
|
|
39
51
|
);
|
|
@@ -239,6 +239,35 @@ if git -C "$T5" log --name-only --format= | grep -q '^\.worktrees/'; then
|
|
|
239
239
|
fi
|
|
240
240
|
rm -rf "$T5"
|
|
241
241
|
|
|
242
|
+
# post-deploy survives the crew's own gitignored non-empty .worktrees/
|
|
243
|
+
# (2026-09-18, room #15 J3): the old `git add -A -- ':!.worktrees/'`
|
|
244
|
+
# exited 1 on git 2.43.0 whenever setup's `.gitignore` entry ignored a
|
|
245
|
+
# non-empty .worktrees/, and set -e killed post-deploy before "DEPLOYED:".
|
|
246
|
+
T6=/tmp/crew-git-postdeploy-gitignore-test
|
|
247
|
+
rm -rf "$T6"
|
|
248
|
+
mkdir -p "$T6"
|
|
249
|
+
git init -q -b main "$T6"
|
|
250
|
+
git -C "$T6" config user.email test@test.t
|
|
251
|
+
git -C "$T6" config user.name test
|
|
252
|
+
echo base > "$T6/f.txt"
|
|
253
|
+
echo ".worktrees/" > "$T6/.gitignore"
|
|
254
|
+
git -C "$T6" add -A
|
|
255
|
+
git -C "$T6" commit -qm base
|
|
256
|
+
export CREW_REPO="$T6"
|
|
257
|
+
export CREW_LIB="$REPO_DIR/lib"
|
|
258
|
+
# Non-empty ignored .worktrees/, exactly the shape of real integrate state
|
|
259
|
+
mkdir -p "$T6/.worktrees/pd2"
|
|
260
|
+
printf 'task_id=pd2\nholder=test\n' > "$T6/.worktrees/pd2/.merge-lock"
|
|
261
|
+
# A genuine user change the post-deploy commit should pick up
|
|
262
|
+
echo userchange > "$T6/user.txt"
|
|
263
|
+
out=$(bash "$LIFECYCLE" post-deploy "pd2") || fail "postdeploy-gitignore-test: post-deploy failed: $out"
|
|
264
|
+
echo "$out" | grep -q '^DEPLOYED: ' || fail "postdeploy-gitignore-test: no DEPLOYED: line: $out"
|
|
265
|
+
git -C "$T6" log --name-only --format= -1 | grep -q '^user\.txt$' || fail "postdeploy-gitignore-test: user change not committed"
|
|
266
|
+
if git -C "$T6" log --name-only --format= | grep -q '^\.worktrees/'; then
|
|
267
|
+
fail "postdeploy-gitignore-test: .worktrees/ content committed into main"
|
|
268
|
+
fi
|
|
269
|
+
rm -rf "$T6"
|
|
270
|
+
|
|
242
271
|
rm -rf "$T2" "$T3" "$T4"
|
|
243
272
|
export CREW_REPO="$T"
|
|
244
273
|
rm -rf "$T"
|
package/lib/verify-publish.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// node verify-publish.js --crew-home <path> --task-id <uuid>
|
|
24
24
|
// --commit <sha> --base <sha> --repo-path <path> --slug <slug>
|
|
25
25
|
// --crew-release <release> --inspection-id <uuid> --result-file <path>
|
|
26
|
-
// [--build-agent-id <uuid>]
|
|
26
|
+
// --project-id <project-id> [--build-agent-id <uuid>]
|
|
27
27
|
//
|
|
28
28
|
// --base is the previously-stamped provenance source_commit (or the
|
|
29
29
|
// empty-tree sha 4b825dc642cb6eb9a060e54bf8d69288fbee4904 for a first
|
|
@@ -66,6 +66,7 @@ const inspectionId = arg("--inspection-id");
|
|
|
66
66
|
const resultFile = arg("--result-file");
|
|
67
67
|
const crewRelease = arg("--crew-release");
|
|
68
68
|
const buildAgentId = arg("--build-agent-id", false);
|
|
69
|
+
const projectId = arg("--project-id");
|
|
69
70
|
|
|
70
71
|
if (!/^[0-9a-f]{40}$/.test(commit)) fail("usage", "commit must be a 40-char hex sha.", 2);
|
|
71
72
|
if (!/^[0-9a-f]{40}$/.test(base)) fail("usage", "base must be a 40-char hex sha.", 2);
|
|
@@ -334,7 +335,7 @@ if (buildAgentId && resultText.includes(buildAgentId)) buildNote = `build-id-cor
|
|
|
334
335
|
// --- 7. Stamp (only after the machine-checked comparison succeeded) ---------
|
|
335
336
|
let stamp;
|
|
336
337
|
try {
|
|
337
|
-
stamp = api("set-provenance", { source_commit: commit, crew_release: crewRelease, task_id: taskId });
|
|
338
|
+
stamp = api("set-provenance", { project_id: projectId, source_commit: commit, crew_release: crewRelease, task_id: taskId });
|
|
338
339
|
} catch (e) {
|
|
339
340
|
terminal("stamp-failed", `set-provenance threw: ${e.message}`);
|
|
340
341
|
}
|
|
@@ -342,7 +343,7 @@ if (!stamp || stamp.ok !== true) terminal("stamp-failed", "set-provenance did no
|
|
|
342
343
|
// Exact read-back: the stamp must match what we intended, or it is fiction.
|
|
343
344
|
let prov;
|
|
344
345
|
try {
|
|
345
|
-
prov = api("get-provenance", {});
|
|
346
|
+
prov = api("get-provenance", { project_id: projectId });
|
|
346
347
|
} catch (e) {
|
|
347
348
|
terminal("stamp-failed", `get-provenance threw: ${e.message}`);
|
|
348
349
|
}
|
|
@@ -416,9 +416,15 @@ cmd_post_deploy() {
|
|
|
416
416
|
|
|
417
417
|
# Commit any changes the artifact builder left behind.
|
|
418
418
|
# The crew's own .worktrees/ dir (merge lock, registry) is crew-owned
|
|
419
|
-
# state, never user content —
|
|
420
|
-
# be committed into main (J1 canary: .merge-lock landed
|
|
421
|
-
|
|
419
|
+
# state, never user content — unstage it after the add so a stray lock
|
|
420
|
+
# file can never be committed into main (J1 canary: .merge-lock landed
|
|
421
|
+
# in main via add -A). Never use an all-negative pathspec here: on git
|
|
422
|
+
# 2.43.0 `git add -A -- ':!.worktrees/'` still exits 1 against the
|
|
423
|
+
# crew's own gitignored .worktrees/ (room #15, J3), and set -e then
|
|
424
|
+
# kills post-deploy before "DEPLOYED:". A plain add never errors on
|
|
425
|
+
# ignored paths; the reset covers pre-gitignore-entry repos too.
|
|
426
|
+
git add -A
|
|
427
|
+
git reset -q -- .worktrees/ 2>/dev/null || true
|
|
422
428
|
if ! git diff --cached --quiet 2>/dev/null; then
|
|
423
429
|
git commit -m "rebuild: $task_id"
|
|
424
430
|
echo "COMMITTED: artifact builder changes"
|
package/package.json
CHANGED
|
@@ -40,11 +40,13 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
|
|
|
40
40
|
4.5. **Parent publish verification (docs/publish-verification.md):** The publisher parks instead of stamping provenance; the parent — this tick, the live root agent — verifies content and stamps. Deterministic code detects, reads back, and certifies; you are only the ferry between the deterministic steps (scan → sensor → verifier).
|
|
41
41
|
- Scan (code): `node {crewHome}/lib/crew-api.js --crew-home {crewHome} scan-verification-pending`
|
|
42
42
|
This atomically claims each verification-pending task (1-hour lease, so a second tick cannot double-verify) and reconciles verified-but-still-parked tasks to `in_progress`. It returns `{ to_verify: [...], reconciled: [...] }`. Log both lists. If the scan exits 2 (e.g. the active release cannot be resolved), log the error loudly and continue — do NOT work around it.
|
|
43
|
-
- **Read-back (deterministic sensor, 2026-09-15):** no platform inspection tool is needed — `lib/readback-disk.js` reads the platform's on-disk working copy of the artifact source (`~/workspace/ts-spaces/<slug>/`) and emits the machine-readable findings block the verifier parses. For each entry in `to_verify`, resolve the base via `get-provenance` (`source_commit`; the empty-tree sha `4b825dc642cb6eb9a060e54bf8d69288fbee4904` when nothing is stamped yet — a first publish)
|
|
43
|
+
- **Read-back (deterministic sensor, 2026-09-15):** no platform inspection tool is needed — `lib/readback-disk.js` reads the platform's on-disk working copy of the artifact source (`~/workspace/ts-spaces/<slug>/`) and emits the machine-readable findings block the verifier parses. For each entry in `to_verify`, resolve the base via `get-provenance` (`source_commit`; the empty-tree sha `4b825dc642cb6eb9a060e54bf8d69288fbee4904` when nothing is stamped yet — a first publish):
|
|
44
|
+
`node {crewHome}/lib/crew-api.js --crew-home {crewHome} get-provenance --json '{"project_id": "<project_id>"}'`
|
|
45
|
+
(use the entry's `project_id` verbatim), then run:
|
|
44
46
|
`node {crewHome}/lib/readback-disk.js --repo-path "<repo_path>" --commit <commit> --base <base> --slug "<deploy_slug>" --task-id <task_id> > /tmp/readback-<task_id>.txt 2> /tmp/readback-<task_id>.err`
|
|
45
47
|
Use the entry's `repo_path` and `deploy_slug` verbatim. If the sensor exits 0, the result file holds the findings block — hand it to the verify step below. If it exits non-zero, do NOT save or use stdout: log `publish: verification-procedural-error <commit> <first line of the .err file>` and leave the task parked — the next tick retries. A sensor failure is procedural (the read could not be performed), never a content verdict. Do NOT judge content yourself, and do NOT stamp provenance.
|
|
46
|
-
- **Verify (code):** `node {crewHome}/lib/verify-publish.js --crew-home {crewHome} --task-id <task_id> --commit <commit> --base <base> --repo-path "<repo_path>" --slug "<deploy_slug>" --crew-release <crew_release> --inspection-id <task_id>-disk --result-file /tmp/readback-<task_id>.txt`
|
|
47
|
-
Pass `--build-agent-id <id>` from the entry's `build_agent_id` when it is present. Use the SAME `<base>` the sensor ran with. The verifier parses the findings, compares mechanically against the base..commit diff, checks supersession, and stamps only on a match. Its terminal verdicts (`publish: verified` → task re-queued; `publish: verification-failed` → stays parked) are final — log them and continue.
|
|
48
|
+
- **Verify (code):** `node {crewHome}/lib/verify-publish.js --crew-home {crewHome} --task-id <task_id> --commit <commit> --base <base> --repo-path "<repo_path>" --slug "<deploy_slug>" --crew-release <crew_release> --project-id <project_id> --inspection-id <task_id>-disk --result-file /tmp/readback-<task_id>.txt`
|
|
49
|
+
Pass `--build-agent-id <id>` from the entry's `build_agent_id` when it is present. Use the SAME `<base>` the sensor ran with and the entry's `project_id` verbatim. The verifier parses the findings, compares mechanically against the base..commit diff, checks supersession, and stamps only on a match. Its terminal verdicts (`publish: verified` → task re-queued; `publish: verification-failed` → stays parked) are final — log them and continue.
|
|
48
50
|
- Never stamp provenance from prose. Never infer a verdict from an inspector's summary text. The verify script's machine-checked comparison is the only certification.
|
|
49
51
|
|
|
50
52
|
5. **Monitor launched workflows until terminal (stay-alive — 2026-09-13):** The platform ties async workflow `agent()` authorization to the launcher's lifetime: if THIS tick ends while a workflow is still running, the workflow's next `agent()` call fails with "subagent bootstrap is no longer authorized" / "subagent reservation owner is terminal". Prevention beats recovery here, so this tick is configured with a 90-minute execution timeout (`timeout_secs: 5400` in seed/crons.json) and you MUST stay alive until every launched run reaches a terminal state. Do not exit early while a launched run is still `running` — your death is what kills it.
|
package/workflows/bugfix.js
CHANGED
|
@@ -83,13 +83,18 @@ const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
|
83
83
|
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
84
84
|
const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
|
|
85
85
|
const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
|
|
86
|
-
|
|
86
|
+
const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
|
|
87
|
+
const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
|
|
88
|
+
// The seven basenames the pin step must materialize — asserted mechanically
|
|
87
89
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
88
90
|
// COMPUTE_DIFF is the deterministic publish-diff computer (room #14,
|
|
89
91
|
// 2026-09-17): the diff is computed by this script, never ferried as an
|
|
90
92
|
// agent JSON string. Pinned like the other publish-critical modules so a
|
|
91
93
|
// mid-run release swap cannot change it under the workflow.
|
|
92
|
-
|
|
94
|
+
// CLASSIFY_SURFACE is the surface classifier (room #15, 2026-09-18):
|
|
95
|
+
// crew-api.js statically imports it, so the pin must carry it — a pin
|
|
96
|
+
// without it kills every claim with ERR_MODULE_NOT_FOUND.
|
|
97
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
|
|
93
98
|
|
|
94
99
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
95
100
|
const projectConfig = inputs.project_config || {};
|
|
@@ -273,7 +278,7 @@ function attemptKey(base, reworkCount) {
|
|
|
273
278
|
function pinLifecycle(key) {
|
|
274
279
|
return agent(
|
|
275
280
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
276
|
-
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
281
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
277
282
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
278
283
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
279
284
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
@@ -1548,7 +1553,7 @@ while (i < STEPS.length) {
|
|
|
1548
1553
|
// first publish (no provenance stamped yet).
|
|
1549
1554
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1550
1555
|
var provResult = await agent(
|
|
1551
|
-
crewCmd("get-provenance", {}) + "\n" +
|
|
1556
|
+
crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
1552
1557
|
"Return JSON { \"provenance\": <the CLI's provenance object, or null when nothing is stamped> } and nothing else. Do not interpret it.",
|
|
1553
1558
|
{ key: attemptKey("publish-provenance-base-" + taskId, totalReworkCount), label: "Reading stamped publish base",
|
|
1554
1559
|
schema: { type: "object", properties: { provenance: { type: ["object", "null"] } }, required: ["provenance"] } }
|
|
@@ -2399,7 +2404,7 @@ while (i < STEPS.length) {
|
|
|
2399
2404
|
if (PUBLISH_TYPE === "artifact") {
|
|
2400
2405
|
instructions = "PROVENANCE CHECK (this project publishes to a dashboard artifact).\n" +
|
|
2401
2406
|
"Provenance is crew-owned state: read it from the Crew API, never from the artifact's own getprovenance action (a different, non-authoritative store).\n" +
|
|
2402
|
-
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", {}) + "\n" +
|
|
2407
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
2403
2408
|
"If provenance is null, report 'provenance missing — publish did not stamp source/crew release', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
2404
2409
|
"Run: cd " + REPO_PATH + " && git rev-parse HEAD — call this LIVE_HEAD.\n" +
|
|
2405
2410
|
"Run: test -d " + crewHome + "/releases/<provenance.crew_release> (substitute the real stamped hash; do not run the literal placeholder). If the directory does not exist, report 'provenance mismatch: crew_release [value from get-provenance] not found in release registry', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
@@ -2944,11 +2949,11 @@ while (i < STEPS.length) {
|
|
|
2944
2949
|
// dashboard QA source check).
|
|
2945
2950
|
try {
|
|
2946
2951
|
var provRefresh = await agent(
|
|
2947
|
-
"Run in shell and read the stdout JSON:\n" + crewCmd("get-provenance", {}) + "\n" +
|
|
2952
|
+
"Run in shell and read the stdout JSON:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
2948
2953
|
"If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
|
|
2949
2954
|
"Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
|
|
2950
2955
|
"run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
|
|
2951
|
-
"Then run in shell: node " + CREW_API + " --crew-home " + crewHome + " set-provenance --json '{\"source_commit\":\"<the existing provenance.source_commit value>\",\"crew_release\":\"<REL>\",\"published_at\":\"<TS>\",\"task_id\":\"" + taskId + "\"}' " +
|
|
2956
|
+
"Then run in shell: node " + CREW_API + " --crew-home " + crewHome + " set-provenance --json '{\"project_id\":\"" + LAUNCH_PROJECT_ID + "\",\"source_commit\":\"<the existing provenance.source_commit value>\",\"crew_release\":\"<REL>\",\"published_at\":\"<TS>\",\"task_id\":\"" + taskId + "\"}' " +
|
|
2952
2957
|
"(substitute the real values; the JSON must be single-quote-wrapped for the shell). " +
|
|
2953
2958
|
"Return JSON { \"refreshed\": <true if the set-provenance stdout contains ok: true, false otherwise>, \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\" } and nothing else.",
|
|
2954
2959
|
{ key: attemptKey("publish-provenance-refresh-" + taskId, totalReworkCount), label: "Refreshing dashboard provenance after crew release",
|
package/workflows/chore.js
CHANGED
|
@@ -140,13 +140,18 @@ const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
|
140
140
|
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
141
141
|
const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
|
|
142
142
|
const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
|
|
143
|
-
|
|
143
|
+
const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
|
|
144
|
+
const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
|
|
145
|
+
// The seven basenames the pin step must materialize — asserted mechanically
|
|
144
146
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
145
147
|
// COMPUTE_DIFF is the deterministic publish-diff computer (room #14,
|
|
146
148
|
// 2026-09-17): the diff is computed by this script, never ferried as an
|
|
147
149
|
// agent JSON string. Pinned like the other publish-critical modules so a
|
|
148
150
|
// mid-run release swap cannot change it under the workflow.
|
|
149
|
-
|
|
151
|
+
// CLASSIFY_SURFACE is the surface classifier (room #15, 2026-09-18):
|
|
152
|
+
// crew-api.js statically imports it, so the pin must carry it — a pin
|
|
153
|
+
// without it kills every claim with ERR_MODULE_NOT_FOUND.
|
|
154
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
|
|
150
155
|
|
|
151
156
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
152
157
|
const projectConfig = inputs.project_config || {};
|
|
@@ -325,7 +330,7 @@ function attemptKey(base, reworkCount) {
|
|
|
325
330
|
function pinLifecycle(key) {
|
|
326
331
|
return agent(
|
|
327
332
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
328
|
-
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
333
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
329
334
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
330
335
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
331
336
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
@@ -1435,7 +1440,7 @@ while (i < STEPS.length) {
|
|
|
1435
1440
|
// first publish (no provenance stamped yet).
|
|
1436
1441
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1437
1442
|
var provResult = await agent(
|
|
1438
|
-
crewCmd("get-provenance", {}) + "\n" +
|
|
1443
|
+
crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
1439
1444
|
"Return JSON { \"provenance\": <the CLI's provenance object, or null when nothing is stamped> } and nothing else. Do not interpret it.",
|
|
1440
1445
|
{ key: attemptKey("publish-provenance-base-" + taskId, reworkCount), label: "Reading stamped publish base",
|
|
1441
1446
|
schema: { type: "object", properties: { provenance: { type: ["object", "null"] } }, required: ["provenance"] } }
|
|
@@ -2521,11 +2526,11 @@ while (i < STEPS.length) {
|
|
|
2521
2526
|
// dashboard QA source check).
|
|
2522
2527
|
try {
|
|
2523
2528
|
var provRefresh = await agent(
|
|
2524
|
-
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", {}) + "\n" +
|
|
2529
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
2525
2530
|
"If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
|
|
2526
2531
|
"Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
|
|
2527
2532
|
"run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
|
|
2528
|
-
"Then run in shell:\n" + crewCmd("set-provenance", { source_commit: "<existing provenance.source_commit>", crew_release: "<REL trimmed>", published_at: "<TS trimmed>", task_id: taskId }) + "\n" +
|
|
2533
|
+
"Then run in shell:\n" + crewCmd("set-provenance", { project_id: LAUNCH_PROJECT_ID, source_commit: "<existing provenance.source_commit>", crew_release: "<REL trimmed>", published_at: "<TS trimmed>", task_id: taskId }) + "\n" +
|
|
2529
2534
|
"(substitute the real existing source_commit, REL, and TS for the placeholders). " +
|
|
2530
2535
|
"Return JSON { \"refreshed\": <true if the set-provenance stdout contains ok: true, false otherwise>, \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\" } and nothing else.",
|
|
2531
2536
|
{ key: attemptKey("publish-provenance-refresh-" + taskId, reworkCount), label: "Refreshing dashboard provenance after crew release",
|
package/workflows/crew-init.js
CHANGED
|
@@ -350,6 +350,7 @@ log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped"
|
|
|
350
350
|
// projects later via crew-api.js create-project.
|
|
351
351
|
phase("project");
|
|
352
352
|
var projectResult;
|
|
353
|
+
var initialProvenanceWarning = null;
|
|
353
354
|
if (!dashboardSlug) {
|
|
354
355
|
log("Project: skipped (CLI-only mode — no dashboard; create projects via crew-api.js create-project)");
|
|
355
356
|
projectResult = { action: "skipped", project_id: null };
|
|
@@ -380,7 +381,8 @@ try {
|
|
|
380
381
|
" Return { action: \"repaired\", project_id: \"" + dashboardSlug + "\" }.\n" +
|
|
381
382
|
"3. If not found (error), create it:\n" +
|
|
382
383
|
" node " + crewHome + "/current/lib/crew-api.js --crew-home " + crewHome + " create-project --json '{\"id\": \"" + dashboardSlug + "\", \"display_name\": \"" + safeDashboardName + "\", \"repo_path\": \"" + safeDashboardRepo + "\", \"deploy_type\": \"artifact\", \"deploy_slug\": \"" + dashboardSlug + "\", \"environment_type\": \"artifact\", \"description\": \"The dashboard task service — the crew\\u0027s first project\"}'\n" +
|
|
383
|
-
"
|
|
384
|
+
" The create-project response carries initial_provenance { stamped, reason?, source_commit?, crew_release? } — pass it through verbatim.\n" +
|
|
385
|
+
" Return { action: \"created\", project_id: \"" + dashboardSlug + "\", initial_provenance: <the verbatim initial_provenance object> }.\n\n" +
|
|
384
386
|
"4. Seed the update watcher's trusted dashboard base (enrollment) — the\n" +
|
|
385
387
|
" commit this installation starts from. Only ever seeds when no base\n" +
|
|
386
388
|
" exists; an established base is never moved by a re-init:\n" +
|
|
@@ -394,7 +396,7 @@ try {
|
|
|
394
396
|
" dashboard upgrade with oldSha unknown and park it with enrollment\n" +
|
|
395
397
|
" guidance, fail-closed.\n\n" +
|
|
396
398
|
"The repo_path must be the dashboard git repository — never the crew home.\n" +
|
|
397
|
-
"Return JSON with action (\"exists\", \"repaired\", or \"created\")
|
|
399
|
+
"Return JSON with action (\"exists\", \"repaired\", or \"created\"), project_id (string), and — only when action is \"created\" — the create-project response's initial_provenance object verbatim.",
|
|
398
400
|
{
|
|
399
401
|
key: "project-1",
|
|
400
402
|
label: "Register dashboard project",
|
|
@@ -402,7 +404,8 @@ try {
|
|
|
402
404
|
type: "object",
|
|
403
405
|
properties: {
|
|
404
406
|
action: { type: "string", enum: ["exists", "repaired", "created"] },
|
|
405
|
-
project_id: { type: "string" }
|
|
407
|
+
project_id: { type: "string" },
|
|
408
|
+
initial_provenance: { type: "object" }
|
|
406
409
|
},
|
|
407
410
|
required: ["action", "project_id"]
|
|
408
411
|
}
|
|
@@ -412,6 +415,18 @@ try {
|
|
|
412
415
|
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Project registration failed", message: String(e.message || e) } };
|
|
413
416
|
}
|
|
414
417
|
log("Project: " + dashboardSlug + " " + projectResult.action + " (repo_path=" + gateFacts.dashboardRepoExpanded + ")");
|
|
418
|
+
if (projectResult && projectResult.action === "created" &&
|
|
419
|
+
projectResult.initial_provenance && !projectResult.initial_provenance.stamped) {
|
|
420
|
+
// Room #15 (reliability): create-project's initial stamp is best-effort,
|
|
421
|
+
// but "reported" used to mean "returned into the void" — zero production
|
|
422
|
+
// code read it, so a skipped stamp (the J1/J2 shape) stayed invisible.
|
|
423
|
+
// Surface it here; the publish-base advance below re-stamps right after,
|
|
424
|
+
// and fails loudly (exit 1) if the re-stamp itself cannot stamp.
|
|
425
|
+
initialProvenanceWarning = "Project created but its initial provenance stamp was skipped (" +
|
|
426
|
+
(projectResult.initial_provenance.reason || "unknown reason") +
|
|
427
|
+
") — the publish-base advance re-stamps it next; if that also fails, the first task's publish diffs from the empty tree.";
|
|
428
|
+
log("WARNING: " + initialProvenanceWarning);
|
|
429
|
+
}
|
|
415
430
|
} // end dashboard-mode project registration
|
|
416
431
|
|
|
417
432
|
// ── Project repo setup (2026-09-18) ─────────────────────────────────
|
|
@@ -512,7 +527,7 @@ if (dashboardRepoPath && projectResult && projectResult.action === "created" &&
|
|
|
512
527
|
publishBaseResult = await agent(
|
|
513
528
|
"Run the deterministic publish-base advance script. Do not decide anything — just run the command and return its JSON output.\n\n" +
|
|
514
529
|
"Command:\n" +
|
|
515
|
-
"node " + crewHome + "/current/lib/advance-publish-base.js --crew-home \"" + crewHome + "\" --repo \"" + gateFacts.dashboardRepoExpanded + "\" --scaffold-sha " + setupRepoResult.commit.sha + " --project-created\n\n" +
|
|
530
|
+
"node " + crewHome + "/current/lib/advance-publish-base.js --crew-home \"" + crewHome + "\" --repo \"" + gateFacts.dashboardRepoExpanded + "\" --scaffold-sha " + setupRepoResult.commit.sha + " --project-id " + projectResult.project_id + " --project-created\n\n" +
|
|
516
531
|
"The script prints one JSON object: { ok, action, reason?, from?, to? }.\n" +
|
|
517
532
|
"Return that JSON verbatim.",
|
|
518
533
|
{
|
|
@@ -801,6 +816,7 @@ return {
|
|
|
801
816
|
scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
|
|
802
817
|
project: projectResult.action,
|
|
803
818
|
repoSetupWarning: setupRepoWarning,
|
|
819
|
+
initialProvenanceWarning: initialProvenanceWarning,
|
|
804
820
|
crons: cronsResult.summary.crons,
|
|
805
821
|
registry: cronsResult.summary.registry,
|
|
806
822
|
autoUpdate: {
|
package/workflows/standard.js
CHANGED
|
@@ -91,13 +91,18 @@ const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
|
91
91
|
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
92
92
|
const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
|
|
93
93
|
const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
|
|
94
|
-
|
|
94
|
+
const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
|
|
95
|
+
const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
|
|
96
|
+
// The seven basenames the pin step must materialize — asserted mechanically
|
|
95
97
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
96
98
|
// COMPUTE_DIFF is the deterministic publish-diff computer (room #14,
|
|
97
99
|
// 2026-09-17): the diff is computed by this script, never ferried as an
|
|
98
100
|
// agent JSON string. Pinned like the other publish-critical modules so a
|
|
99
101
|
// mid-run release swap cannot change it under the workflow.
|
|
100
|
-
|
|
102
|
+
// CLASSIFY_SURFACE is the surface classifier (room #15, 2026-09-18):
|
|
103
|
+
// crew-api.js statically imports it, so the pin must carry it — a pin
|
|
104
|
+
// without it kills every claim with ERR_MODULE_NOT_FOUND.
|
|
105
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
|
|
101
106
|
|
|
102
107
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
103
108
|
const projectConfig = inputs.project_config || {};
|
|
@@ -281,7 +286,7 @@ function attemptKey(base, reworkCount) {
|
|
|
281
286
|
function pinLifecycle(key) {
|
|
282
287
|
return agent(
|
|
283
288
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
284
|
-
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
289
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
285
290
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
286
291
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
287
292
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
@@ -1429,7 +1434,7 @@ while (i < STEPS.length) {
|
|
|
1429
1434
|
// first publish (no provenance stamped yet).
|
|
1430
1435
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1431
1436
|
var provResult = await agent(
|
|
1432
|
-
crewCmd("get-provenance", {}) + "\n" +
|
|
1437
|
+
crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
1433
1438
|
"Return JSON { \"provenance\": <the CLI's provenance object, or null when nothing is stamped> } and nothing else. Do not interpret it.",
|
|
1434
1439
|
{ key: attemptKey("publish-provenance-base-" + taskId, totalReworkCount), label: "Reading stamped publish base",
|
|
1435
1440
|
schema: { type: "object", properties: { provenance: { type: ["object", "null"] } }, required: ["provenance"] } }
|
|
@@ -2226,7 +2231,7 @@ while (i < STEPS.length) {
|
|
|
2226
2231
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-state", { events_limit: 1 }) + "\n" +
|
|
2227
2232
|
"Use the returned tasks, sessions, and events to check the task's data-level effects.\n" +
|
|
2228
2233
|
"DOCS GATE: If the change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), verify the public docs describe it. If public docs are missing or stale for a public-affecting change, report 'public docs missing/stale for [the change]', then end your report with exactly this line: VERDICT: FAIL. QA always fails when public-affecting changes lack public docs. Guide/tutorial gaps are lower priority — file a follow-up task for those instead of failing.\n\n" +
|
|
2229
|
-
"PROVENANCE CHECK: Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", {}) + "\n" +
|
|
2234
|
+
"PROVENANCE CHECK: Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
2230
2235
|
"If provenance is null, report 'provenance missing — publish did not stamp source/crew release', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
2231
2236
|
"Run: cd " + REPO_PATH + " && git rev-parse HEAD — call this LIVE_HEAD.\n" +
|
|
2232
2237
|
"Run: test -d " + crewHome + "/releases/<provenance.crew_release> (substitute the real stamped hash; do not run the literal placeholder). If the directory does not exist, FAIL: { \"passed\": false, \"summary\": \"provenance mismatch: crew_release [value from get-provenance] not found in release registry\" }.\n" +
|
|
@@ -2703,11 +2708,11 @@ while (i < STEPS.length) {
|
|
|
2703
2708
|
// dashboard QA source check).
|
|
2704
2709
|
try {
|
|
2705
2710
|
var provRefresh = await agent(
|
|
2706
|
-
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", {}) + "\n" +
|
|
2711
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
2707
2712
|
"If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
|
|
2708
2713
|
"Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
|
|
2709
2714
|
"run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
|
|
2710
|
-
"Then run in shell:\n" + crewCmd("set-provenance", { source_commit: "<existing provenance.source_commit>", crew_release: "<REL trimmed>", published_at: "<TS trimmed>", task_id: taskId }) + "\n" +
|
|
2715
|
+
"Then run in shell:\n" + crewCmd("set-provenance", { project_id: LAUNCH_PROJECT_ID, source_commit: "<existing provenance.source_commit>", crew_release: "<REL trimmed>", published_at: "<TS trimmed>", task_id: taskId }) + "\n" +
|
|
2711
2716
|
"(substitute the real existing source_commit, REL, and TS for the placeholders). " +
|
|
2712
2717
|
"Return JSON { \"refreshed\": <true if the set-provenance stdout contains ok: true, false otherwise>, \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\" } and nothing else.",
|
|
2713
2718
|
{ key: attemptKey("publish-provenance-refresh-" + taskId, totalReworkCount), label: "Refreshing dashboard provenance after crew release",
|
package/workflows/upgrade.js
CHANGED
|
@@ -157,9 +157,14 @@ const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
|
157
157
|
const CREW_API_PINNED = RUN_LIB + "/crew-api.js";
|
|
158
158
|
const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
159
159
|
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
160
|
-
|
|
160
|
+
const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
|
|
161
|
+
const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
|
|
162
|
+
// The six basenames the pin step must materialize — asserted mechanically
|
|
161
163
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
162
|
-
|
|
164
|
+
// CLASSIFY_SURFACE is the surface classifier (room #15, 2026-09-18):
|
|
165
|
+
// crew-api.js statically imports it, so the pin must carry it — a pin
|
|
166
|
+
// without it kills every claim with ERR_MODULE_NOT_FOUND.
|
|
167
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
|
|
163
168
|
|
|
164
169
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
165
170
|
const projectConfig = inputs.project_config || {};
|
|
@@ -190,7 +195,7 @@ if (!taskId) {
|
|
|
190
195
|
function pinLifecycle(key) {
|
|
191
196
|
return agent(
|
|
192
197
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
193
|
-
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
198
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
194
199
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
195
200
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
196
201
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|