muse-crew 0.13.3 → 0.14.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/docs/publish-unknown-recovery.md +113 -0
- package/lib/AGENTS.md +7 -2
- package/lib/advance-publish-base.js +74 -28
- package/lib/classify-publish-absence.js +451 -0
- package/lib/crew-api.js +584 -21
- package/lib/publish-content.js +154 -0
- package/lib/retry-publish.js +370 -0
- package/lib/scaffold-crew.js +129 -0
- package/lib/verify-publish.js +101 -84
- package/package.json +2 -2
- package/seed/cron-body-template.md +57 -2
- package/workflows/bugfix.js +38 -23
- package/workflows/chore.js +37 -22
- package/workflows/crew-dispatch.js +43 -8
- package/workflows/crew-init.js +333 -78
- package/workflows/crew-uninstall.js +57 -10
- package/workflows/standard.js +35 -16
- package/workflows/upgrade.js +28 -14
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Publish-unknown recovery (blocker 15, 2026-09-18)
|
|
2
|
+
|
|
3
|
+
When a standard/bugfix Publish parks with "Publish outcome unknown", the
|
|
4
|
+
artifact-edit trigger went out fire-and-forget and no receipt came back —
|
|
5
|
+
async was planned for, receipt-less was not. The unknown-recovery loop
|
|
6
|
+
closes that gap without re-issuing blindly.
|
|
7
|
+
|
|
8
|
+
## The note is the state machine
|
|
9
|
+
|
|
10
|
+
Recovery state lives in the task's `note` events, keyed on machine-written
|
|
11
|
+
`publish: <transition>` markers. Deterministic code (`lib/crew-api.js`) owns
|
|
12
|
+
every transition; the cron tick (Step 4.4) is only the ferry between the
|
|
13
|
+
deterministic steps. The initial park note is the workflow's
|
|
14
|
+
"Publish outcome unknown …" note (no `publish:` marker — the scan matches it
|
|
15
|
+
explicitly); every transition after that is machine-written.
|
|
16
|
+
|
|
17
|
+
State diagram (latest `publish:` note wins; history is the retry budget):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
"Publish outcome unknown" park
|
|
21
|
+
(< 30m) ──waiting──> scan: waiting (leave alone)
|
|
22
|
+
(>= 30m) ──scan──> publish: unknown-recovery-claimed <expiry> (1h lease)
|
|
23
|
+
│
|
|
24
|
+
├─ tick runs lib/classify-publish-absence.js, then
|
|
25
|
+
│ record-unknown-classification (CAS on the claim expiry):
|
|
26
|
+
│
|
|
27
|
+
├─ verified ──────────> publish: verification-requested <commit>
|
|
28
|
+
│ (content IS present; Step 4.5 verifies;
|
|
29
|
+
│ ledger: unknown-resolved)
|
|
30
|
+
├─ provably-dropped ──> publish: dropped <commit>
|
|
31
|
+
│ (queues the retry protocol, next tick)
|
|
32
|
+
├─ applied-not-built / ambiguous ──> publish: ambiguous <commit>
|
|
33
|
+
│ (terminal; ledger: unknown-classified)
|
|
34
|
+
├─ superseded ────────> publish: superseded <commit> (terminal)
|
|
35
|
+
└─ deferred ──────────> (no note; the claim expires; the next scan
|
|
36
|
+
re-claims and re-classifies)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Retry protocol (Step 4.4, for `publish: dropped` with a free budget):
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
publish: dropped
|
|
43
|
+
── tick: HEAD == commit? no ──> publish: retry-superseded (terminal)
|
|
44
|
+
── tick: acquire merge lock publish-retry:<task> (600s); held ──> stop, retry next tick
|
|
45
|
+
── tick: re-read content in the lock (classifier)
|
|
46
|
+
├─ not provably-dropped ──> record-retry-recheck routes it
|
|
47
|
+
│ (verified → verification-requested; ambiguous → terminal;
|
|
48
|
+
│ superseded → terminal; deferred → no-op)
|
|
49
|
+
└─ provably-dropped ──> publish: retry-intended <commit> <ts>
|
|
50
|
+
── trigger (same child shape as the first attempt)
|
|
51
|
+
├─ ARTIFACT_EDIT_REFUSED ──> publish: retry-refused (terminal)
|
|
52
|
+
└─ no refusal ──> publish: retry-issued <commit>
|
|
53
|
+
──> publish: verification-requested <commit> not-before=<ts+20m>
|
|
54
|
+
(Step 4.5 skips not-before entries until the window passes)
|
|
55
|
+
──> one -retry1 ledger entry (best-effort)
|
|
56
|
+
── release the merge lock (every path)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Crash recovery (fail closed, never re-trigger blind):
|
|
60
|
+
|
|
61
|
+
- `publish: retry-intended` without `retry-issued` → the next scan mirrors
|
|
62
|
+
`verification-requested` with `not-before=intended+20m`. The trigger may
|
|
63
|
+
or may not have gone out; content verification is the arbiter.
|
|
64
|
+
- `publish: retry-issued` without a mirrored request → the next scan writes
|
|
65
|
+
the missing mirror.
|
|
66
|
+
- Exactly one retry per task, enforced from note history: the scan emits
|
|
67
|
+
`retry_due` only when no `publish: retry-issued` exists in the task's
|
|
68
|
+
history — including a retry for an earlier unknown attempt on a reworked
|
|
69
|
+
task. When the budget is spent, the drop is terminal: `publish: ambiguous`.
|
|
70
|
+
|
|
71
|
+
## Commands
|
|
72
|
+
|
|
73
|
+
- `scan-publish-unknown` — the cron scan (Step 4.5 of the old numbering).
|
|
74
|
+
Returns `{ waiting, due, retry_due, mirrored, skipped }`. `due` entries
|
|
75
|
+
ferry the classifier inputs: `commit`, `trigger_ts` (from the `submitted`
|
|
76
|
+
ledger entry — never a time window), `park_ts`, `slug`, `repo_path`,
|
|
77
|
+
`base` (provenance `source_commit`, else the empty tree), and
|
|
78
|
+
`claim_expiry` for the record CAS. `retry_due` entries ferry the retry
|
|
79
|
+
inputs (`commit`, `attempt`, `slug`, `repo_path`, `base`, `ledger_path`).
|
|
80
|
+
- `record-unknown-classification --json '{task_id, claim_expiry, decision}'`
|
|
81
|
+
— routes the classifier's decision; CAS on the claim expiry (a stale tick
|
|
82
|
+
records nothing).
|
|
83
|
+
- `record-retry-recheck --json '{task_id, decision}'` — routes the retry
|
|
84
|
+
protocol's in-lock content re-read; CAS on latest being `publish: dropped`.
|
|
85
|
+
- `resolve-publish-unknown` — the manual one-shot for a single parked task
|
|
86
|
+
(unchanged; the audit-window contract, not the classifier).
|
|
87
|
+
|
|
88
|
+
## Classifier (lib/classify-publish-absence.js)
|
|
89
|
+
|
|
90
|
+
Decides, from the task's repo and the platform's on-disk state, whether the
|
|
91
|
+
dropped edit is proven:
|
|
92
|
+
|
|
93
|
+
- `verified` — the content IS present (the platform applied it; the receipt
|
|
94
|
+
was the only thing lost). Never re-issue.
|
|
95
|
+
- `provably-dropped` — the old source is live AND the manifest shows no
|
|
96
|
+
build since the trigger. Only this decision may retry.
|
|
97
|
+
- `applied-not-built` — the new source is live but no build ran (the
|
|
98
|
+
trigger reached the platform but the build didn't). Ambiguous outcome,
|
|
99
|
+
terminal: a retry would double-apply.
|
|
100
|
+
- `ambiguous` — the content check is inconclusive. Never retry blind.
|
|
101
|
+
- `deferred` — not yet quiesced; re-check next tick.
|
|
102
|
+
- `superseded` — HEAD moved past the attempt's commit. Terminal.
|
|
103
|
+
|
|
104
|
+
The classifier's verified path requires the CURRENT manifest to be a new
|
|
105
|
+
build identity: `built_at` advanced past the trigger AND `content_sha256`
|
|
106
|
+
differs from the pre-trigger baseline the workflow snapshots into the
|
|
107
|
+
submitted ledger entry (design §1.9 — defeats a replayed manifest). When
|
|
108
|
+
the baseline is unavailable the classifier falls back to the time-based
|
|
109
|
+
advance check and notes it; the Step 4.5 verifier (`lib/verify-publish.js`)
|
|
110
|
+
is the strict gate and fails closed without a baseline before stamping.
|
|
111
|
+
Build-in-flight is checked twice bracketing the content read via manifest
|
|
112
|
+
state change (no mtime heuristics), and the suite carries add-only and
|
|
113
|
+
removal-only regression fixtures.
|
package/lib/AGENTS.md
CHANGED
|
@@ -24,8 +24,10 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
24
24
|
- `ux-doctrine.js` — UX-surface doctrine page resolution (2026-09-17): the canonical map from `environment_type` to the crew's shared UX bar (`artifact` → `docs/artifact-ux.md`, `terminal` → `docs/terminal-ux.md`; null/unknown → no page). Pure and deterministic: `doctrinePage(env)`, `doctrinePath(crewHome, env)`, `doctrinePageExists(crewHome, env)`; CLI `--page <env>` / `--path <crewHome> <env>`. Workflows mirror the map inline (one line — the workflow runtime's relative-import support is unverified) and tests pin the mirror against this file.
|
|
25
25
|
- `serve-artifact.js` — local server for a built TS space for experiential QA (2026-09-14): serves `<space-dir>/client/dist` statically and dispatches POST `*/actions` to the compiled server actions with a locally-built Ctx. Prints `READY port=<n>` then serves until killed. Read-only w.r.t. the space directory. Fidelity: the served client and action handlers are the artifact's own built code; the Ctx is locally built (privileged handlers run from the space's own `server/dist/privileged.js` when present; blobs are stored in a per-run temp dir and served back at `/__blobs/<key>`); environment is inherited from the caller. It is not the hosted runtime — tasks that cannot be judged under it must report `NOT POSSIBLE: <reason>`.
|
|
26
26
|
- `readback-disk.js` — deterministic publish content sensor (2026-09-16): reads the on-disk tree the artifact is built/served from and emits the machine-readable findings block (`FILE:`/`ADDED:`/`REMOVED:`/`END_FILE`) that `verify-publish.js` judges. The primary sensor — the LLM-inspector path (`build-readback-request.js`) is manual-fallback only since `artifact_inspect` was removed by the platform 2026-09-14.
|
|
27
|
+
- `publish-content.js` — shared ESM content-primitives for publish verification (2026-09-18, blocker 15): diff parsing (`parseDiff`), findings parsing (`parseFindings`), old-tree occurrence counting (`makeOldCounter`), and the discriminating-line / collision-exemption logic (`discriminatingLines`). Unifies `verify-publish.js` and the unknown-recovery classifier on one judgment so the two paths can never disagree about what a diff proves.
|
|
28
|
+
- `classify-publish-absence.js` — deterministic six-way classifier for publish-parked UNKNOWN outcomes (2026-09-18, blocker 15): decides from durable signals only — the pre-trigger manifest baseline (captured by the workflow into the submitted ledger entry; design §1.9) vs the current manifest's `built_at`/`content_sha256`, `git diff <base> <commit>` discriminating lines against the on-disk source tree, and HEAD vs the publish commit. Outcomes: `provably-dropped` (source shows pre-edit state, manifest NOT advanced past the trigger, HEAD == commit, aged past quiesce — retry once), `verified` (manifest advanced past the trigger AND content_sha256 differs from the pre-trigger baseline — a new build identity, not a replayed manifest; falls back to the time-based advance check with a note when the baseline is absent, and the Step 4.5 verifier fails closed without a baseline), `applied-not-built` (platform build-emission failure; no retry — the 2026-09-12 re-trigger hazard), `ambiguous` (any inconclusive shape — no retry by design), `deferred` (build in flight — manifest changed during the content read, or built within the settle window — or park below quiesce; not a verdict, retry later), `superseded` (HEAD != commit — never retry the old commit). Retry budget is consumed by the classification itself, never by the edit attempt. Never reads the wall clock except for recovery timing; addition-only and removal-only diffs are vacuously satisfied on their empty side.
|
|
27
29
|
- `build-readback-request.js` — builds the LLM-inspector read-back `verbatim_request` from the merge commit's diff (2026-09-14): carries the merged diff as the expected change and asks for an independent read of the artifact's actual source. Retained as the manual fallback; the deterministic `readback-disk.js` is the primary sensor.
|
|
28
|
-
- `verify-publish.js` — mechanical publish verification judge (2026-09-14/16): certifies the read-back findings block against `git diff` (strict `FILE:`/`ADDED:`/`REMOVED:`/`END_FILE` parsing, every added line PRESENT / every removed line ABSENT, HEAD==commit supersession check) and only then stamps provenance. Binary files, mode-only changes, and fully-colliding added hunks fail closed as `unverifiable-content` (2026-09-16, critic findings 1/5) — they can never vacuously stamp. Content-mismatch, unreadable-result, superseded, and stamp failures exit 1 with `publish: verification-failed` and no stamp.
|
|
30
|
+
- `verify-publish.js` — mechanical publish verification judge (2026-09-14/16; shared primitives 2026-09-18): certifies the read-back findings block against `git diff` (strict `FILE:`/`ADDED:`/`REMOVED:`/`END_FILE` parsing, every discriminating added line PRESENT / every discriminating removed line ABSENT, HEAD==commit supersession check), then the design §1.9 manifest-freshness gate (current manifest `built_at` advanced past the trigger AND `content_sha256` differs from the workflow's pre-trigger baseline in the submitted ledger entry — a new build identity, not a replayed manifest; missing baseline fails closed), and only then stamps provenance. Diff parsing, findings parsing, and the collision-exemption rules come from the shared `lib/publish-content.js` (the unknown-recovery classifier's own judgment — one definition, never two). Binary files, mode-only changes, and fully-colliding added hunks fail closed as `unverifiable-content` (2026-09-16, critic findings 1/5) — they can never vacuously stamp. Content-mismatch, unreadable-result, superseded, and stamp failures exit 1 with `publish: verification-failed` and no stamp.
|
|
29
31
|
- `update-watch.js` — deterministic automatic update watcher (2026-09-16, zero deps): `node update-watch.js --crew-home <path>` (missing arg → usage, exit 2; every other path exits 0). Watches the public npm registry (`npm view muse-crew version` pinned to `https://registry.npmjs.org/`) vs `crew-release.sh current` and files a `workflow: "upgrade"` task with `source: npm@<version>` when policy (`auto_update_crew`, `update_channel`) and channel gating allow; watches `git ls-remote origin HEAD` on the first `deploy_type=artifact` project vs `$CREW_HOME/.update-watch.json` and files a `workflow: "chore"` task carrying the mechanical dashboard-upgrade journey. Reads the `.crew-version` compatibility anchor at the new ref via `git fetch` + `git show <sha>:.crew-version` (never the working tree) and orders dashboard-led: a declared newer crew files the crew upgrade task FIRST and the dashboard task notes it follows the crew upgrade (declaration bypasses `update_channel`, not the `auto_update_crew=false` opt-out); a declared older crew skips the dashboard leg entirely as a human decision; a missing/invalid/unfetchable anchor fails open to the dashboard leg as today. Idempotency via the same state file (records at file time); check failures log to `$CREW_HOME/update-watch.log` and are never thrown. Safety: only files tasks — never deploys, never touches the artifact/config/scheduler. Run by the daily `crew-update-watch` cron through the `current` symlink (latest release); deliberately NOT in the lib-pinning `PIN_BASENAMES`.
|
|
30
32
|
- `gitignore.js` — deterministic .gitignore management for crew-owned paths (2026-09-17): the crew touches exactly one user-owned file outside `.orchestration/` — the repo's `.gitignore`. `ensureGitignoreEntries(repoPath, entries)` creates the file when missing, appends missing entries (exact line match, no duplicates), preserves existing content byte-for-byte, and is idempotent. `describeGitignoreChange(repoPath, entries)` renders the exact diff for the setup consent conversation. Crew-owned entries: `.worktrees/`, `.orchestration/user/`. CLI: `--repo <path> [--dry-run]`.
|
|
31
33
|
- `repo-orchestration.js` — repository-local `.orchestration/` scaffold (2026-09-17): `scaffoldRepoOrchestration(repoPath, crewRepoPath)` creates `$REPO/.orchestration/{workflows,identities,phases,user}/`, seeds workflows/identities/phases from the crew repo's platform defaults with no-clobber semantics (existing project customizations never overwritten), and writes a README in `user/` explaining it's for local config. Idempotent. CLI: `--repo <path> --crew-repo <path>`.
|
|
@@ -34,4 +36,7 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
34
36
|
- `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
37
|
- `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
38
|
- `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
|
|
39
|
+
- `advance-publish-base.js` — publish-base advance past the setup scaffold (2026-09-18, room #14; testimony-free 2026-09-19 REVIEW): `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 resolves the scaffold commit itself (`git rev-parse HEAD` in `--repo`; `--scaffold-sha` is an explicit override only) and moves `source_commit` to it only when every fail-closed condition holds deterministically: the existing stamp is not a real publish's (provenance.task_id is set by every real publish and null for initial stamps/base advances — the task_id invariant documented on stampProvenance; the old `--project-created` testimony flag is gone), 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. Every stdout JSON object also carries `repo_clean` (informational — whether `git status --porcelain` is empty — so the caller can warn loudly about a dirty tree; the first task's Integrate refuses to merge onto one). 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> --project-id <id> [--scaffold-sha <sha>]`.
|
|
40
|
+
- `scaffold-crew.js` — deterministic crew-home scaffold composer (2026-09-19, Room #19 D1): replaces the agent-authored scaffold report (the agent returned prose instead of the schema'd JSON and the platform rejected it, killing room #19's init on minute one — the copying itself always succeeded). The script copies the fixed file sets (identities *.md+*.png, personas *.md, seed/workflows *.md, seed/feedback/README.md) into `<orchDir>/{identities,personas,workflows,feedback}/` with mkdir -p + no-clobber semantics (existing files never overwritten), verifies every expected file on disk (the decision rule), then writes `<orchDir>/.scaffold-receipt.json` `{created, skipped}` (integer counts, durable evidence) and prints the same JSON to stdout. Idempotent: all-skipped is exit 0. Exit 1 with the reason on stderr on any failure; exit 2 on usage errors. crew-init's scaffold-1 call is a byte courier only — it runs this script and prints the receipt's exact bytes; a paraphrased ferry fails the mechanical parse loudly as a blocked init (tripwire, never a fabricated 0/0). The receipt bytes are the agent's report of script output, not proof the script ran: crew-init corroborates with a witness re-run of this same idempotent composer (a witness that still has files to create proves the ferry's work didn't land; the witness's own disk-verification then holds the end state). Fail-closed source guards (2026-09-19 REVIEW): a missing
|
|
41
|
+
or empty identities/, personas/, or seed/workflows/ source refuses loudly (exit 1,
|
|
42
|
+
naming the source) before creating anything — never an empty 0/0 receipt. No wall-clock reads, no randomness.
|
|
@@ -10,8 +10,14 @@
|
|
|
10
10
|
//
|
|
11
11
|
// This script moves provenance.source_commit to the scaffold commit only when
|
|
12
12
|
// every fail-closed condition holds:
|
|
13
|
-
// 1.
|
|
14
|
-
//
|
|
13
|
+
// 1. The existing stamp is not a real publish's: provenance.task_id is set
|
|
14
|
+
// by every real publish (lib/verify-publish.js and the workflow
|
|
15
|
+
// provenance-refresh paths) and is null for initial stamps and for this
|
|
16
|
+
// script's own base advances. A stamp with task_id set is never moved.
|
|
17
|
+
// (2026-09-19 REVIEW: this deterministic check replaces the old
|
|
18
|
+
// --project-created flag, which was agent testimony ferried through the
|
|
19
|
+
// workflow. The task_id invariant is documented on stampProvenance in
|
|
20
|
+
// lib/crew-api.js.)
|
|
15
21
|
// 2. The scaffold commit's parent IS the stamped source_commit: nothing
|
|
16
22
|
// slipped between the stamp and the scaffold, so no un-published task
|
|
17
23
|
// work can be silently dropped from a future diff.
|
|
@@ -19,10 +25,17 @@
|
|
|
19
25
|
// (.orchestration/ or .gitignore).
|
|
20
26
|
// 4. Special case: no provenance was ever stamped (empty repo at create
|
|
21
27
|
// time) and the scaffold commit is the root commit -> stamp fresh.
|
|
28
|
+
// The scaffold commit is resolved by this script itself (git rev-parse HEAD
|
|
29
|
+
// in --repo) unless --scaffold-sha overrides it — the caller never ferries
|
|
30
|
+
// the sha through agent testimony (2026-09-19 REVIEW: agent-mechanic boundary
|
|
31
|
+
// razor — consequential parameters come from deterministic reads, not
|
|
32
|
+
// testimony).
|
|
22
33
|
// Anything else yields { ok: true, action: "skipped", reason } — never an
|
|
23
34
|
// error, never a clobber. stdout carries exactly one JSON object; operational
|
|
24
35
|
// failures go to stderr with a non-zero exit (the caller treats failure as
|
|
25
|
-
// best-effort: init continues with the stamped base).
|
|
36
|
+
// best-effort: init continues with the stamped base). Every JSON object also
|
|
37
|
+
// carries repo_clean (boolean): whether `git status --porcelain` in --repo
|
|
38
|
+
// is empty — informational for the caller, never a gate.
|
|
26
39
|
//
|
|
27
40
|
// No wall-clock reads, no randomness — the published_at on a fresh stamp is
|
|
28
41
|
// left to crew-api's default; on an advance the existing published_at is
|
|
@@ -39,7 +52,7 @@ const RELEASE_RE = /^([0-9a-f]{40}|pkg-\d+\.\d+\.\d+)$/;
|
|
|
39
52
|
|
|
40
53
|
function usage() {
|
|
41
54
|
console.error(
|
|
42
|
-
"Usage: node lib/advance-publish-base.js --crew-home <path> --repo <path> --
|
|
55
|
+
"Usage: node lib/advance-publish-base.js --crew-home <path> --repo <path> --project-id <id> [--scaffold-sha <sha>]"
|
|
43
56
|
);
|
|
44
57
|
}
|
|
45
58
|
|
|
@@ -51,7 +64,6 @@ function parseArgv(argv) {
|
|
|
51
64
|
else if (a === "--repo") out.repo = argv[++i];
|
|
52
65
|
else if (a === "--scaffold-sha") out.scaffoldSha = argv[++i];
|
|
53
66
|
else if (a === "--project-id") out.projectId = argv[++i];
|
|
54
|
-
else if (a === "--project-created") out.projectCreated = true;
|
|
55
67
|
else {
|
|
56
68
|
console.error("unknown argument: " + a);
|
|
57
69
|
usage();
|
|
@@ -90,25 +102,47 @@ function isCrewOwnedPath(p) {
|
|
|
90
102
|
return p === ".gitignore" || p === ".orchestration" || p.startsWith(".orchestration/");
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
function skip(reason) {
|
|
94
|
-
console.log(JSON.stringify({ ok: true, action: "skipped", reason }));
|
|
105
|
+
function skip(reason, repoClean) {
|
|
106
|
+
console.log(JSON.stringify({ ok: true, action: "skipped", reason, repo_clean: repoClean }));
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
function main() {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (!crewHome || !repo || !
|
|
110
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
111
|
+
const { crewHome, repo, projectId } = parsed;
|
|
112
|
+
let scaffoldSha = parsed.scaffoldSha || null;
|
|
113
|
+
if (!crewHome || !repo || !projectId) {
|
|
102
114
|
usage();
|
|
103
115
|
process.exit(2);
|
|
104
116
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
// The scaffold commit is resolved by this script, not ferried through the
|
|
118
|
+
// caller: --scaffold-sha is an explicit override; otherwise HEAD is read
|
|
119
|
+
// directly from the repo.
|
|
120
|
+
if (scaffoldSha) {
|
|
121
|
+
if (!SHA_RE.test(scaffoldSha)) {
|
|
122
|
+
console.error("scaffold-sha is not a 40-char hex sha");
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
try {
|
|
127
|
+
scaffoldSha = git(repo, ["rev-parse", "HEAD"]);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error("rev-parse HEAD failed: " + (e.message || e));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (!SHA_RE.test(scaffoldSha)) {
|
|
133
|
+
console.error("rev-parse HEAD did not print a 40-char hex sha");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
108
136
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
|
|
138
|
+
// Informational for the caller (never a gate): whether the repo worktree
|
|
139
|
+
// is clean. The caller warns loudly on a dirty tree — the first task's
|
|
140
|
+
// Integrate refuses to merge onto one.
|
|
141
|
+
let repoClean = null;
|
|
142
|
+
try {
|
|
143
|
+
repoClean = git(repo, ["status", "--porcelain"]) === "";
|
|
144
|
+
} catch (e) {
|
|
145
|
+
repoClean = null;
|
|
112
146
|
}
|
|
113
147
|
|
|
114
148
|
let provenance;
|
|
@@ -119,6 +153,19 @@ function main() {
|
|
|
119
153
|
process.exit(1);
|
|
120
154
|
}
|
|
121
155
|
|
|
156
|
+
// Deterministic, not testimony: every real publish stamps provenance with
|
|
157
|
+
// its task_id (see the task_id invariant on stampProvenance in
|
|
158
|
+
// lib/crew-api.js); initial stamps and base advances leave it null. A
|
|
159
|
+
// publish's base is never moved.
|
|
160
|
+
if (provenance && provenance.task_id) {
|
|
161
|
+
skip(
|
|
162
|
+
"provenance was stamped by a real publish (task_id " + provenance.task_id +
|
|
163
|
+
") — a publish's base is never moved",
|
|
164
|
+
repoClean
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
122
169
|
// The scaffold commit's parent: empty output means S is the root commit.
|
|
123
170
|
let parent;
|
|
124
171
|
try {
|
|
@@ -144,8 +191,7 @@ function main() {
|
|
|
144
191
|
skip(
|
|
145
192
|
"scaffold commit touches non-crew-owned paths (" +
|
|
146
193
|
foreign.slice(0, 5).join(", ") +
|
|
147
|
-
") — provenance left untouched"
|
|
148
|
-
);
|
|
194
|
+
") — provenance left untouched", repoClean);
|
|
149
195
|
return;
|
|
150
196
|
}
|
|
151
197
|
|
|
@@ -154,18 +200,18 @@ function main() {
|
|
|
154
200
|
// scaffold commit is the root commit — anything else means history we
|
|
155
201
|
// did not create, and we do not invent a base over it.
|
|
156
202
|
if (parent !== null) {
|
|
157
|
-
skip("no provenance stamped and scaffold commit is not the root commit — provenance left untouched");
|
|
203
|
+
skip("no provenance stamped and scaffold commit is not the root commit — provenance left untouched", repoClean);
|
|
158
204
|
return;
|
|
159
205
|
}
|
|
160
206
|
let crewRelease = null;
|
|
161
207
|
try {
|
|
162
208
|
crewRelease = resolveActiveRelease(crewHome);
|
|
163
209
|
} catch (e) {
|
|
164
|
-
skip("no provenance stamped and the active crew release is unresolvable (" + (e.message || e) + ")");
|
|
210
|
+
skip("no provenance stamped and the active crew release is unresolvable (" + (e.message || e) + ")", repoClean);
|
|
165
211
|
return;
|
|
166
212
|
}
|
|
167
213
|
if (!RELEASE_RE.test(crewRelease)) {
|
|
168
|
-
skip("no provenance stamped and the active release name is not a valid crew_release (" + crewRelease + ")");
|
|
214
|
+
skip("no provenance stamped and the active release name is not a valid crew_release (" + crewRelease + ")", repoClean);
|
|
169
215
|
return;
|
|
170
216
|
}
|
|
171
217
|
let stamped;
|
|
@@ -183,17 +229,17 @@ function main() {
|
|
|
183
229
|
console.error("set-provenance did not return ok");
|
|
184
230
|
process.exit(1);
|
|
185
231
|
}
|
|
186
|
-
console.log(JSON.stringify({ ok: true, action: "stamped", to: scaffoldSha.slice(0, 12) }));
|
|
232
|
+
console.log(JSON.stringify({ ok: true, action: "stamped", to: scaffoldSha.slice(0, 12), repo_clean: repoClean }));
|
|
187
233
|
return;
|
|
188
234
|
}
|
|
189
235
|
|
|
190
236
|
const base = provenance.source_commit;
|
|
191
237
|
if (!SHA_RE.test(base || "")) {
|
|
192
|
-
skip("existing provenance.source_commit is not a 40-char hex sha — provenance left untouched");
|
|
238
|
+
skip("existing provenance.source_commit is not a 40-char hex sha — provenance left untouched", repoClean);
|
|
193
239
|
return;
|
|
194
240
|
}
|
|
195
241
|
if (base === scaffoldSha) {
|
|
196
|
-
skip("provenance already points at the scaffold commit");
|
|
242
|
+
skip("provenance already points at the scaffold commit", repoClean);
|
|
197
243
|
return;
|
|
198
244
|
}
|
|
199
245
|
if (parent !== base) {
|
|
@@ -202,8 +248,7 @@ function main() {
|
|
|
202
248
|
String(parent || "root").slice(0, 12) +
|
|
203
249
|
") is not the stamped base (" +
|
|
204
250
|
base.slice(0, 12) +
|
|
205
|
-
") — provenance left untouched"
|
|
206
|
-
);
|
|
251
|
+
") — provenance left untouched", repoClean);
|
|
207
252
|
return;
|
|
208
253
|
}
|
|
209
254
|
|
|
@@ -219,7 +264,7 @@ function main() {
|
|
|
219
264
|
}
|
|
220
265
|
}
|
|
221
266
|
if (!crewRelease) {
|
|
222
|
-
skip("no usable crew_release for the re-stamp — provenance left untouched");
|
|
267
|
+
skip("no usable crew_release for the re-stamp — provenance left untouched", repoClean);
|
|
223
268
|
return;
|
|
224
269
|
}
|
|
225
270
|
|
|
@@ -259,6 +304,7 @@ function main() {
|
|
|
259
304
|
action: "advanced",
|
|
260
305
|
from: base.slice(0, 12),
|
|
261
306
|
to: scaffoldSha.slice(0, 12),
|
|
307
|
+
repo_clean: repoClean,
|
|
262
308
|
})
|
|
263
309
|
);
|
|
264
310
|
}
|