muse-crew 0.4.4 → 0.4.5
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 +2 -2
- package/lib/AGENTS.md +1 -0
- package/lib/publish-npm.sh +20 -3
- package/lib/test-publish-verify.sh +157 -0
- package/lib/test-version-write.sh +9 -4
- package/package.json +1 -1
- package/workflows/bugfix.js +62 -6
- package/workflows/chore.js +62 -6
- package/workflows/standard.js +59 -13
package/API.md
CHANGED
|
@@ -71,14 +71,14 @@ Send a stuck or failed task to a specific workflow phase for recovery.
|
|
|
71
71
|
|
|
72
72
|
### `parktask`
|
|
73
73
|
|
|
74
|
-
Atomically park a task for human attention: sets `state` to `parked
|
|
74
|
+
Atomically park a task for human attention: sets `state` to `parked`, writes the explanatory `note` event, and settles every `running` session for the task — in one transaction. Either all land or none do: callers never see a note without a park, a park without a note, or a parked task with a ghost `running` session left behind (a terminal task transition settles its sessions). Settled sessions move to `failed` — the status `recovertask` accepts, so recovery works immediately after a park — with `ended_at` stamped and `Parked: <message>` appended to their notes; `failure_reason` stays null.
|
|
75
75
|
|
|
76
76
|
| Field | Type | Required | Notes |
|
|
77
77
|
|-------|------|----------|-------|
|
|
78
78
|
| `task_id` | uuid | yes | |
|
|
79
79
|
| `message` | string (1–1000) | yes | Why the task needs a human; written to the activity feed as a `note` |
|
|
80
80
|
|
|
81
|
-
Returns `{ "ok": true, "task": {...} }` with the updated task record.
|
|
81
|
+
Returns `{ "ok": true, "task": {...}, "settled_sessions": <n> }` with the updated task record and the count of sessions settled (`0` when none were running).
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
package/lib/AGENTS.md
CHANGED
|
@@ -8,6 +8,7 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
8
8
|
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/status/post-deploy over git worktrees (`.worktrees/<id>`, branch `task/<id>`). The crew registry (`<repo>/.worktrees/.registry/<id>`) is the source of truth for task→branch/path — never reconstruct it from git state. Prepare fails closed on dirty `main`; cleanup is forgiving.
|
|
9
9
|
- `test-worktree-backend.sh` — regression tests for the lifecycle script (validate, prepare/reuse, inspect, status, cleanup, idempotent cleanup, dirty-main preflight) on a scratch repo
|
|
10
10
|
- `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)
|
|
11
|
+
- `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
|
|
11
12
|
- `orphan-sweep.sh` — find and clean stale worktrees and merge locks
|
|
12
13
|
- `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.
|
|
13
14
|
- `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.
|
package/lib/publish-npm.sh
CHANGED
|
@@ -150,11 +150,28 @@ if [ "$ALREADY_PUBLISHED" = "0" ]; then
|
|
|
150
150
|
rm -f "$TGZ"
|
|
151
151
|
|
|
152
152
|
# 12. Verify: ground truth is the registry, not any agent's summary.
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
# The registry is eventually consistent: a publish followed by an
|
|
154
|
+
# immediate read can observe the pre-publish version (canary 5a027278
|
|
155
|
+
# hit a stale read replica ~6s after publish). A single stale read must
|
|
156
|
+
# never park a landed publish, so this block retries with backoff and
|
|
157
|
+
# client cache-busting instead of failing on the first mismatch.
|
|
158
|
+
# STEP-12-ANCHOR: publish verification (retry-tolerant)
|
|
159
|
+
VERIFY_ATTEMPTS="${VERIFY_ATTEMPTS:-12}"
|
|
160
|
+
VERIFY_SLEEP_SECS="${VERIFY_SLEEP_SECS:-10}"
|
|
161
|
+
REG=""
|
|
162
|
+
for attempt in $(seq 1 "$VERIFY_ATTEMPTS"); do
|
|
163
|
+
# --prefer-online busts npm's client-side packument cache; the retry
|
|
164
|
+
# loop absorbs server-side replica lag. A non-zero exit is a miss,
|
|
165
|
+
# not an immediate failure.
|
|
166
|
+
REG="$(npm view "$PKG" version --prefer-online 2>/dev/null || true)"
|
|
167
|
+
[ "$REG" = "$TARGET_VERSION" ] && break
|
|
168
|
+
echo "PUBLISH_VERIFY_RETRY=$attempt registry=$REG"
|
|
169
|
+
[ "$attempt" -lt "$VERIFY_ATTEMPTS" ] && sleep "$VERIFY_SLEEP_SECS"
|
|
170
|
+
done
|
|
155
171
|
[ "$REG" = "$TARGET_VERSION" ] \
|
|
156
|
-
|| fail "verify" "registry=$REG"
|
|
172
|
+
|| fail "verify" "registry=$REG after $VERIFY_ATTEMPTS attempts"
|
|
157
173
|
echo "PUBLISH_VERIFIED=$TARGET_VERSION"
|
|
174
|
+
# STEP-12-END
|
|
158
175
|
fi
|
|
159
176
|
|
|
160
177
|
# 13. Push the version-bump commit (idempotent: no-op if already pushed).
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-publish-verify.sh — regression test for the retry-tolerant step-12
|
|
3
|
+
# publish verification in publish-npm.sh (canary 5a027278).
|
|
4
|
+
#
|
|
5
|
+
# Self-contained: extracts the shipped step-12 block from lib/publish-npm.sh
|
|
6
|
+
# by anchor and runs it against a fake `npm` on PATH whose read replica
|
|
7
|
+
# lags — non-zero exits, then the old version, then the target version —
|
|
8
|
+
# and requires the block to converge on success. Also asserts fail-closed
|
|
9
|
+
# exhaustion when the replica never converges.
|
|
10
|
+
#
|
|
11
|
+
# Anchors: `# STEP-12-ANCHOR` ... `# STEP-12-END` in lib/publish-npm.sh.
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
PUBLISH="$SCRIPT_DIR/publish-npm.sh"
|
|
17
|
+
|
|
18
|
+
PKG="muse-crew"
|
|
19
|
+
OLD_VERSION="9.9.8"
|
|
20
|
+
TARGET_VERSION="9.9.9"
|
|
21
|
+
|
|
22
|
+
pass=0
|
|
23
|
+
fail_count=0
|
|
24
|
+
ok() { echo "PASS: $1"; pass=$((pass + 1)); }
|
|
25
|
+
no() { echo "FAIL: $1"; fail_count=$((fail_count + 1)); }
|
|
26
|
+
|
|
27
|
+
TMPBASE="$(mktemp -d)"
|
|
28
|
+
trap 'rm -rf "$TMPBASE"' EXIT
|
|
29
|
+
|
|
30
|
+
# --- Extract the shipped step-12 block by anchor ---
|
|
31
|
+
BLOCK="$TMPBASE/step12.sh"
|
|
32
|
+
sed -n '/# STEP-12-ANCHOR/,/# STEP-12-END/p' "$PUBLISH" > "$BLOCK"
|
|
33
|
+
if grep -q 'PUBLISH_VERIFIED' "$BLOCK" && grep -q 'fail "verify"' "$BLOCK"; then
|
|
34
|
+
ok "extracted step-12 block from publish-npm.sh"
|
|
35
|
+
else
|
|
36
|
+
no "step-12 extraction failed (anchors missing)"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# --- Runner: fake npm first in PATH, behavior baked in at generation time ---
|
|
40
|
+
FAKEBIN="$TMPBASE/fakebin"
|
|
41
|
+
mkdir -p "$FAKEBIN"
|
|
42
|
+
|
|
43
|
+
# write_fake_npm <behavior>
|
|
44
|
+
# laggy: exit 1 twice (npm failure), old version twice (stale replica),
|
|
45
|
+
# then the target version (replica converged)
|
|
46
|
+
# stale: old version forever (replica never converges)
|
|
47
|
+
write_fake_npm() {
|
|
48
|
+
case "$1" in
|
|
49
|
+
laggy) BEHAVIOR='
|
|
50
|
+
if [ "$n" -le 2 ]; then exit 1; fi
|
|
51
|
+
if [ "$n" -le 4 ]; then echo "'"$OLD_VERSION"'"; exit 0; fi
|
|
52
|
+
echo "'"$TARGET_VERSION"'"; exit 0' ;;
|
|
53
|
+
stale) BEHAVIOR='echo "'"$OLD_VERSION"'"; exit 0' ;;
|
|
54
|
+
*) echo "unknown behavior: $1" >&2; exit 1 ;;
|
|
55
|
+
esac
|
|
56
|
+
cat > "$FAKEBIN/npm" <<EOF
|
|
57
|
+
#!/usr/bin/env bash
|
|
58
|
+
# Records its own argv (for the cache-busting assertion) and its call count.
|
|
59
|
+
echo "\$*" >> "$TMPBASE/argv"
|
|
60
|
+
n=\$(cat "$TMPBASE/calls" 2>/dev/null || echo 0)
|
|
61
|
+
n=\$((n + 1))
|
|
62
|
+
echo "\$n" > "$TMPBASE/calls"
|
|
63
|
+
$BEHAVIOR
|
|
64
|
+
EOF
|
|
65
|
+
chmod +x "$FAKEBIN/npm"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# run_step12: runs the shipped block; env: PKG TARGET_VERSION
|
|
69
|
+
# VERIFY_ATTEMPTS VERIFY_SLEEP_SECS; returns the block's exit code and
|
|
70
|
+
# captures its stdout in $OUT.
|
|
71
|
+
run_step12() {
|
|
72
|
+
local runner="$TMPBASE/run-$RANDOM.sh"
|
|
73
|
+
{
|
|
74
|
+
echo 'fail() { echo "PUBLISH_FAILED=$1"; shift; echo "$1"; exit 1; }'
|
|
75
|
+
cat "$BLOCK"
|
|
76
|
+
} > "$runner"
|
|
77
|
+
chmod +x "$runner"
|
|
78
|
+
rm -f "$TMPBASE/calls" "$TMPBASE/argv"
|
|
79
|
+
OUT="$(PATH="$FAKEBIN:$PATH" \
|
|
80
|
+
PKG="$PKG" TARGET_VERSION="$TARGET_VERSION" \
|
|
81
|
+
VERIFY_ATTEMPTS="${VERIFY_ATTEMPTS:-12}" \
|
|
82
|
+
VERIFY_SLEEP_SECS="${VERIFY_SLEEP_SECS:-0}" \
|
|
83
|
+
bash "$runner" 2>&1)"
|
|
84
|
+
local code=$?
|
|
85
|
+
echo "$OUT"
|
|
86
|
+
return "$code"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# --- Case 1: lagging replica converges — block must succeed ---
|
|
90
|
+
write_fake_npm laggy
|
|
91
|
+
OUT="$(run_step12)"
|
|
92
|
+
CODE=$?
|
|
93
|
+
if [ "$CODE" -eq 0 ]; then
|
|
94
|
+
ok "laggy replica converges: block exits 0"
|
|
95
|
+
else
|
|
96
|
+
no "laggy replica converges: block exited $CODE (output: $OUT)"
|
|
97
|
+
fi
|
|
98
|
+
case "$OUT" in
|
|
99
|
+
*"PUBLISH_VERIFIED=$TARGET_VERSION"*)
|
|
100
|
+
ok "prints PUBLISH_VERIFIED=$TARGET_VERSION on convergence" ;;
|
|
101
|
+
*) no "missing PUBLISH_VERIFIED=$TARGET_VERSION (output: $OUT)" ;;
|
|
102
|
+
esac
|
|
103
|
+
case "$OUT" in
|
|
104
|
+
*"PUBLISH_FAILED"*) no "PUBLISH_FAILED printed despite convergence" ;;
|
|
105
|
+
*) ok "no PUBLISH_FAILED on convergence" ;;
|
|
106
|
+
esac
|
|
107
|
+
CALLS="$(cat "$TMPBASE/calls" 2>/dev/null || echo 0)"
|
|
108
|
+
if [ "$CALLS" = "5" ]; then
|
|
109
|
+
ok "converged after exactly 5 reads (2 failures, 2 stale, 1 good)"
|
|
110
|
+
else
|
|
111
|
+
no "expected 5 npm calls on convergence, saw $CALLS"
|
|
112
|
+
fi
|
|
113
|
+
RETRIES="$(grep -c 'PUBLISH_VERIFY_RETRY=' <<< "$OUT" || true)"
|
|
114
|
+
if [ "$RETRIES" = "4" ]; then
|
|
115
|
+
ok "prints a PUBLISH_VERIFY_RETRY progress line per miss"
|
|
116
|
+
else
|
|
117
|
+
no "expected 4 PUBLISH_VERIFY_RETRY lines, saw $RETRIES"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# --- Case 2: replica never converges — fail closed with the verify marker ---
|
|
121
|
+
write_fake_npm stale
|
|
122
|
+
VERIFY_ATTEMPTS=3 OUT="$(run_step12)"
|
|
123
|
+
CODE=$?
|
|
124
|
+
if [ "$CODE" -ne 0 ]; then
|
|
125
|
+
ok "stale replica: block fails closed (non-zero exit)"
|
|
126
|
+
else
|
|
127
|
+
no "stale replica: block exited 0 despite persistent mismatch"
|
|
128
|
+
fi
|
|
129
|
+
case "$OUT" in
|
|
130
|
+
*"PUBLISH_FAILED=verify"*)
|
|
131
|
+
ok "exhaustion keeps the PUBLISH_FAILED=verify marker contract" ;;
|
|
132
|
+
*) no "missing PUBLISH_FAILED=verify (output: $OUT)" ;;
|
|
133
|
+
esac
|
|
134
|
+
case "$OUT" in
|
|
135
|
+
*"registry=$OLD_VERSION after 3 attempts"*)
|
|
136
|
+
ok "exhaustion detail names registry value and attempt count" ;;
|
|
137
|
+
*) no "missing exhaustion detail (output: $OUT)" ;;
|
|
138
|
+
esac
|
|
139
|
+
CALLS="$(cat "$TMPBASE/calls" 2>/dev/null || echo 0)"
|
|
140
|
+
if [ "$CALLS" = "3" ]; then
|
|
141
|
+
ok "bounded: stops after VERIFY_ATTEMPTS reads"
|
|
142
|
+
else
|
|
143
|
+
no "expected 3 npm calls on exhaustion, saw $CALLS"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# --- Case 3: every read goes through --prefer-online (client cache-bust) ---
|
|
147
|
+
write_fake_npm laggy
|
|
148
|
+
run_step12 >/dev/null
|
|
149
|
+
if grep -q -- '--prefer-online' "$TMPBASE/argv"; then
|
|
150
|
+
ok "every npm view read passes --prefer-online"
|
|
151
|
+
else
|
|
152
|
+
no "npm view reads missing --prefer-online"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
echo ""
|
|
156
|
+
echo "$pass passed, $fail_count failed."
|
|
157
|
+
[ "$fail_count" -eq 0 ]
|
|
@@ -16,6 +16,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
16
16
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
17
17
|
PUBLISH="$SCRIPT_DIR/publish-npm.sh"
|
|
18
18
|
|
|
19
|
+
# Target the next patch after the repo's current version. (A hardcoded target
|
|
20
|
+
# goes stale the moment the repo releases it: writing the version that's
|
|
21
|
+
# already there produces no diff and the preservation check fails closed.)
|
|
22
|
+
TARGET_VERSION="$(node -e 'const fs=require("fs");const v=JSON.parse(fs.readFileSync(process.argv[1],"utf8")).version.split(".");v[v.length-1]=String(Number(v[v.length-1])+1);console.log(v.join("."))' "$REPO_DIR/package.json")"
|
|
23
|
+
|
|
19
24
|
pass=0
|
|
20
25
|
fail_count=0
|
|
21
26
|
ok() { echo "PASS: $1"; pass=$((pass + 1)); }
|
|
@@ -58,7 +63,7 @@ RUNNER="$TMPBASE/run.sh"
|
|
|
58
63
|
} > "$RUNNER"
|
|
59
64
|
chmod +x "$RUNNER"
|
|
60
65
|
|
|
61
|
-
if REPO_PATH="$FIX" TARGET_VERSION="
|
|
66
|
+
if REPO_PATH="$FIX" TARGET_VERSION="$TARGET_VERSION" bash "$RUNNER" >/dev/null 2>&1; then
|
|
62
67
|
ok "shipped step-8 block exits 0 on escape-carrying package.json"
|
|
63
68
|
else
|
|
64
69
|
no "shipped step-8 block failed closed on escape-carrying package.json"
|
|
@@ -66,10 +71,10 @@ fi
|
|
|
66
71
|
|
|
67
72
|
# --- Assertions on the result ---
|
|
68
73
|
VER="$(node -p "JSON.parse(require('fs').readFileSync('$FIX/package.json','utf8')).version")"
|
|
69
|
-
if [ "$VER" = "
|
|
70
|
-
ok "package.json version is
|
|
74
|
+
if [ "$VER" = "$TARGET_VERSION" ]; then
|
|
75
|
+
ok "package.json version is $TARGET_VERSION after write"
|
|
71
76
|
else
|
|
72
|
-
no "package.json version is $VER, expected
|
|
77
|
+
no "package.json version is $VER, expected $TARGET_VERSION"
|
|
73
78
|
fi
|
|
74
79
|
|
|
75
80
|
NUMSTAT="$(git -C "$FIX" diff --numstat -- package.json | tr '\t' ' ')"
|
package/package.json
CHANGED
package/workflows/bugfix.js
CHANGED
|
@@ -36,6 +36,9 @@ const PUBLISH_NPM_SRC = crewHome + "/lib/publish-npm.sh";
|
|
|
36
36
|
const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
37
37
|
const ORPHAN_SWEEP_SRC = crewHome + "/lib/orphan-sweep.sh";
|
|
38
38
|
const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
|
|
39
|
+
// The four basenames the pin step must materialize — asserted mechanically
|
|
40
|
+
// by workflow code from the verbatim listing, never from agent prose.
|
|
41
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, ORPHAN_SWEEP].map(function (p) { return p.split("/").pop(); });
|
|
39
42
|
|
|
40
43
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
41
44
|
const projectConfig = inputs.project_config || {};
|
|
@@ -172,6 +175,27 @@ function workRetryKey(stepName, reworkSuffix, attempt) {
|
|
|
172
175
|
function attemptKey(base, reworkCount) {
|
|
173
176
|
return base + (reworkCount > 0 ? "-r" + reworkCount : "");
|
|
174
177
|
}
|
|
178
|
+
// pinLifecycle(key) — snapshot the lifecycle scripts into RUN_LIB and return
|
|
179
|
+
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the four pinned
|
|
180
|
+
// basenames; the agent cannot self-certify. (The pin step was the one place
|
|
181
|
+
// the workflows trusted agent prose: task 24be1cd6 walked to Publish on an
|
|
182
|
+
// empty pin dir.) Byte-identical across standard/bugfix/chore — pinned by
|
|
183
|
+
// tests/pin-location.test.js.
|
|
184
|
+
function pinLifecycle(key) {
|
|
185
|
+
return agent(
|
|
186
|
+
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
187
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " " + ORPHAN_SWEEP + " && ls -1 " + RUN_LIB + "\n" +
|
|
188
|
+
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
189
|
+
{ key: key, label: "Pinning lifecycle scripts",
|
|
190
|
+
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
// parsePinListing(result) — basenames from a pin/verify listing.
|
|
194
|
+
// Byte-identical across standard/bugfix/chore — pinned by
|
|
195
|
+
// tests/pin-location.test.js.
|
|
196
|
+
function parsePinListing(result) {
|
|
197
|
+
return (result && result.listing ? result.listing : "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
|
|
198
|
+
}
|
|
175
199
|
function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
|
|
176
200
|
// reason: "discarded" (the runtime threw the output away — it could not be
|
|
177
201
|
// machine-read) or "empty" (agent() returned without throwing but produced
|
|
@@ -474,12 +498,17 @@ async function parkTask(reason) {
|
|
|
474
498
|
}
|
|
475
499
|
let i = startStepIndex;
|
|
476
500
|
|
|
477
|
-
// Pin lifecycle scripts
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
);
|
|
501
|
+
// ── Pin lifecycle scripts ────────────────────────────────────────────
|
|
502
|
+
// Copy lifecycle scripts into a per-task temp dir so this run is immune
|
|
503
|
+
// to upgrades that land while it's in flight. Verified mechanically:
|
|
504
|
+
// workflow code asserts the four basenames from the verbatim listing —
|
|
505
|
+
// the agent cannot self-certify. Any miss parks the task before Triage.
|
|
506
|
+
const initialPins = parsePinListing(await pinLifecycle("pin-lifecycle"));
|
|
507
|
+
const missingInitialPins = PIN_BASENAMES.filter(function (b) { return initialPins.indexOf(b) === -1; });
|
|
508
|
+
if (missingInitialPins.length > 0) {
|
|
509
|
+
return await parkTask("Lifecycle pin incomplete before Triage — missing " + missingInitialPins.join(", ") + " in " + RUN_LIB + ".");
|
|
510
|
+
}
|
|
511
|
+
log("Lifecycle scripts pinned to " + RUN_LIB);
|
|
483
512
|
|
|
484
513
|
while (i < STEPS.length) {
|
|
485
514
|
const step = STEPS[i];
|
|
@@ -525,6 +554,33 @@ while (i < STEPS.length) {
|
|
|
525
554
|
}
|
|
526
555
|
}
|
|
527
556
|
|
|
557
|
+
// ── Pre-Publish pin guard ──────────────────────────────────────────
|
|
558
|
+
// Verify the pinned lifecycle scripts still exist on every Publish
|
|
559
|
+
// pass (first pass and rework passes): the pin step trusted agent
|
|
560
|
+
// prose instead of a mechanical check, so task 24be1cd6 walked to
|
|
561
|
+
// Publish on an empty pin dir. One re-pin and re-verify on a miss;
|
|
562
|
+
// still missing parks the task. Keys are attemptKey-scoped so a
|
|
563
|
+
// rework Publish never re-mints a first-pass key.
|
|
564
|
+
if (step.name === "Publish") {
|
|
565
|
+
let publishPins = parsePinListing(await agent(
|
|
566
|
+
"Verify the pinned lifecycle scripts are present.\n" +
|
|
567
|
+
"Run: ls -1 " + RUN_LIB + " 2>/dev/null || echo PIN_DIR_MISSING\n" +
|
|
568
|
+
"Return the verbatim output as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
569
|
+
{ key: attemptKey("pins-verify-publish", totalReworkCount), label: "Verifying pinned lifecycle scripts",
|
|
570
|
+
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
571
|
+
));
|
|
572
|
+
let missingAtPublish = PIN_BASENAMES.filter(function (b) { return publishPins.indexOf(b) === -1; });
|
|
573
|
+
if (missingAtPublish.length > 0) {
|
|
574
|
+
log("Pin guard at Publish: missing " + missingAtPublish.join(", ") + " — re-pinning once");
|
|
575
|
+
publishPins = parsePinListing(await pinLifecycle(attemptKey("pin-lifecycle-republish", totalReworkCount)));
|
|
576
|
+
missingAtPublish = PIN_BASENAMES.filter(function (b) { return publishPins.indexOf(b) === -1; });
|
|
577
|
+
}
|
|
578
|
+
if (missingAtPublish.length > 0) {
|
|
579
|
+
return await parkTask("Pinned lifecycle scripts missing at Publish even after re-pin: " + missingAtPublish.join(", ") + " in " + RUN_LIB + ".");
|
|
580
|
+
}
|
|
581
|
+
log("Pin guard: all four lifecycle scripts present at Publish");
|
|
582
|
+
}
|
|
583
|
+
|
|
528
584
|
// Publish is optional and target-based. Empty target = prototyping project: skip the phase.
|
|
529
585
|
// Unknown target (incl. legacy "repo") = config error: block.
|
|
530
586
|
if (step.name === "Publish" && !PUBLISH_TYPE) {
|
package/workflows/chore.js
CHANGED
|
@@ -34,6 +34,9 @@ const PUBLISH_NPM_SRC = crewHome + "/lib/publish-npm.sh";
|
|
|
34
34
|
const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
35
35
|
const ORPHAN_SWEEP_SRC = crewHome + "/lib/orphan-sweep.sh";
|
|
36
36
|
const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
|
|
37
|
+
// The four basenames the pin step must materialize — asserted mechanically
|
|
38
|
+
// by workflow code from the verbatim listing, never from agent prose.
|
|
39
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, ORPHAN_SWEEP].map(function (p) { return p.split("/").pop(); });
|
|
37
40
|
|
|
38
41
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
39
42
|
const projectConfig = inputs.project_config || {};
|
|
@@ -170,6 +173,27 @@ function workRetryKey(stepName, reworkSuffix, attempt) {
|
|
|
170
173
|
function attemptKey(base, reworkCount) {
|
|
171
174
|
return base + (reworkCount > 0 ? "-r" + reworkCount : "");
|
|
172
175
|
}
|
|
176
|
+
// pinLifecycle(key) — snapshot the lifecycle scripts into RUN_LIB and return
|
|
177
|
+
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the four pinned
|
|
178
|
+
// basenames; the agent cannot self-certify. (The pin step was the one place
|
|
179
|
+
// the workflows trusted agent prose: task 24be1cd6 walked to Publish on an
|
|
180
|
+
// empty pin dir.) Byte-identical across standard/bugfix/chore — pinned by
|
|
181
|
+
// tests/pin-location.test.js.
|
|
182
|
+
function pinLifecycle(key) {
|
|
183
|
+
return agent(
|
|
184
|
+
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
185
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " " + ORPHAN_SWEEP + " && ls -1 " + RUN_LIB + "\n" +
|
|
186
|
+
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
187
|
+
{ key: key, label: "Pinning lifecycle scripts",
|
|
188
|
+
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
// parsePinListing(result) — basenames from a pin/verify listing.
|
|
192
|
+
// Byte-identical across standard/bugfix/chore — pinned by
|
|
193
|
+
// tests/pin-location.test.js.
|
|
194
|
+
function parsePinListing(result) {
|
|
195
|
+
return (result && result.listing ? result.listing : "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
|
|
196
|
+
}
|
|
173
197
|
function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
|
|
174
198
|
// reason: "discarded" (the runtime threw the output away — it could not be
|
|
175
199
|
// machine-read) or "empty" (agent() returned without throwing but produced
|
|
@@ -432,12 +456,17 @@ async function parkTask(reason) {
|
|
|
432
456
|
}
|
|
433
457
|
let i = startStepIndex;
|
|
434
458
|
|
|
435
|
-
// Pin lifecycle scripts
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
);
|
|
459
|
+
// ── Pin lifecycle scripts ────────────────────────────────────────────
|
|
460
|
+
// Copy lifecycle scripts into a per-task temp dir so this run is immune
|
|
461
|
+
// to upgrades that land while it's in flight. Verified mechanically:
|
|
462
|
+
// workflow code asserts the four basenames from the verbatim listing —
|
|
463
|
+
// the agent cannot self-certify. Any miss parks the task before Triage.
|
|
464
|
+
const initialPins = parsePinListing(await pinLifecycle("pin-lifecycle"));
|
|
465
|
+
const missingInitialPins = PIN_BASENAMES.filter(function (b) { return initialPins.indexOf(b) === -1; });
|
|
466
|
+
if (missingInitialPins.length > 0) {
|
|
467
|
+
return await parkTask("Lifecycle pin incomplete before Triage — missing " + missingInitialPins.join(", ") + " in " + RUN_LIB + ".");
|
|
468
|
+
}
|
|
469
|
+
log("Lifecycle scripts pinned to " + RUN_LIB);
|
|
441
470
|
|
|
442
471
|
while (i < STEPS.length) {
|
|
443
472
|
const step = STEPS[i];
|
|
@@ -483,6 +512,33 @@ while (i < STEPS.length) {
|
|
|
483
512
|
}
|
|
484
513
|
}
|
|
485
514
|
|
|
515
|
+
// ── Pre-Publish pin guard ──────────────────────────────────────────
|
|
516
|
+
// Verify the pinned lifecycle scripts still exist on every Publish
|
|
517
|
+
// pass (first pass and rework passes): the pin step trusted agent
|
|
518
|
+
// prose instead of a mechanical check, so task 24be1cd6 walked to
|
|
519
|
+
// Publish on an empty pin dir. One re-pin and re-verify on a miss;
|
|
520
|
+
// still missing parks the task. Keys are attemptKey-scoped so a
|
|
521
|
+
// rework Publish never re-mints a first-pass key.
|
|
522
|
+
if (step.name === "Publish") {
|
|
523
|
+
let publishPins = parsePinListing(await agent(
|
|
524
|
+
"Verify the pinned lifecycle scripts are present.\n" +
|
|
525
|
+
"Run: ls -1 " + RUN_LIB + " 2>/dev/null || echo PIN_DIR_MISSING\n" +
|
|
526
|
+
"Return the verbatim output as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
527
|
+
{ key: attemptKey("pins-verify-publish", reworkCount), label: "Verifying pinned lifecycle scripts",
|
|
528
|
+
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
529
|
+
));
|
|
530
|
+
let missingAtPublish = PIN_BASENAMES.filter(function (b) { return publishPins.indexOf(b) === -1; });
|
|
531
|
+
if (missingAtPublish.length > 0) {
|
|
532
|
+
log("Pin guard at Publish: missing " + missingAtPublish.join(", ") + " — re-pinning once");
|
|
533
|
+
publishPins = parsePinListing(await pinLifecycle(attemptKey("pin-lifecycle-republish", reworkCount)));
|
|
534
|
+
missingAtPublish = PIN_BASENAMES.filter(function (b) { return publishPins.indexOf(b) === -1; });
|
|
535
|
+
}
|
|
536
|
+
if (missingAtPublish.length > 0) {
|
|
537
|
+
return await parkTask("Pinned lifecycle scripts missing at Publish even after re-pin: " + missingAtPublish.join(", ") + " in " + RUN_LIB + ".");
|
|
538
|
+
}
|
|
539
|
+
log("Pin guard: all four lifecycle scripts present at Publish");
|
|
540
|
+
}
|
|
541
|
+
|
|
486
542
|
// Publish is optional and target-based. Empty target = prototyping project: skip the phase.
|
|
487
543
|
// Unknown target (incl. legacy "repo") = config error: block.
|
|
488
544
|
if (step.name === "Publish" && !PUBLISH_TYPE) {
|
package/workflows/standard.js
CHANGED
|
@@ -36,6 +36,9 @@ const PUBLISH_NPM_SRC = crewHome + "/lib/publish-npm.sh";
|
|
|
36
36
|
const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
37
37
|
const ORPHAN_SWEEP_SRC = crewHome + "/lib/orphan-sweep.sh";
|
|
38
38
|
const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
|
|
39
|
+
// The four basenames the pin step must materialize — asserted mechanically
|
|
40
|
+
// by workflow code from the verbatim listing, never from agent prose.
|
|
41
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, ORPHAN_SWEEP].map(function (p) { return p.split("/").pop(); });
|
|
39
42
|
|
|
40
43
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
41
44
|
const projectConfig = inputs.project_config || {};
|
|
@@ -172,6 +175,27 @@ function workRetryKey(stepName, reworkSuffix, attempt) {
|
|
|
172
175
|
function attemptKey(base, reworkCount) {
|
|
173
176
|
return base + (reworkCount > 0 ? "-r" + reworkCount : "");
|
|
174
177
|
}
|
|
178
|
+
// pinLifecycle(key) — snapshot the lifecycle scripts into RUN_LIB and return
|
|
179
|
+
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the four pinned
|
|
180
|
+
// basenames; the agent cannot self-certify. (The pin step was the one place
|
|
181
|
+
// the workflows trusted agent prose: task 24be1cd6 walked to Publish on an
|
|
182
|
+
// empty pin dir.) Byte-identical across standard/bugfix/chore — pinned by
|
|
183
|
+
// tests/pin-location.test.js.
|
|
184
|
+
function pinLifecycle(key) {
|
|
185
|
+
return agent(
|
|
186
|
+
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
187
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " " + ORPHAN_SWEEP + " && ls -1 " + RUN_LIB + "\n" +
|
|
188
|
+
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
189
|
+
{ key: key, label: "Pinning lifecycle scripts",
|
|
190
|
+
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
// parsePinListing(result) — basenames from a pin/verify listing.
|
|
194
|
+
// Byte-identical across standard/bugfix/chore — pinned by
|
|
195
|
+
// tests/pin-location.test.js.
|
|
196
|
+
function parsePinListing(result) {
|
|
197
|
+
return (result && result.listing ? result.listing : "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
|
|
198
|
+
}
|
|
175
199
|
function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
|
|
176
200
|
// reason: "discarded" (the runtime threw the output away — it could not be
|
|
177
201
|
// machine-read) or "empty" (agent() returned without throwing but produced
|
|
@@ -475,19 +499,14 @@ let i = startStepIndex;
|
|
|
475
499
|
|
|
476
500
|
// ── Pin lifecycle scripts ────────────────────────────────────────────
|
|
477
501
|
// Copy lifecycle scripts into a per-task temp dir so this run is immune
|
|
478
|
-
// to upgrades that land while it's in flight.
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
"
|
|
485
|
-
|
|
486
|
-
" cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + "\n" +
|
|
487
|
-
" chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " " + ORPHAN_SWEEP + "\n" +
|
|
488
|
-
"Confirm the files exist by listing " + RUN_LIB + ".",
|
|
489
|
-
{ key: "pin-lifecycle", label: "Pinning lifecycle scripts", schema: { type: "object" } }
|
|
490
|
-
);
|
|
502
|
+
// to upgrades that land while it's in flight. Verified mechanically:
|
|
503
|
+
// workflow code asserts the four basenames from the verbatim listing —
|
|
504
|
+
// the agent cannot self-certify. Any miss parks the task before Triage.
|
|
505
|
+
const initialPins = parsePinListing(await pinLifecycle("pin-lifecycle"));
|
|
506
|
+
const missingInitialPins = PIN_BASENAMES.filter(function (b) { return initialPins.indexOf(b) === -1; });
|
|
507
|
+
if (missingInitialPins.length > 0) {
|
|
508
|
+
return await parkTask("Lifecycle pin incomplete before Triage — missing " + missingInitialPins.join(", ") + " in " + RUN_LIB + ".");
|
|
509
|
+
}
|
|
491
510
|
log("Lifecycle scripts pinned to " + RUN_LIB);
|
|
492
511
|
|
|
493
512
|
while (i < STEPS.length) {
|
|
@@ -534,6 +553,33 @@ while (i < STEPS.length) {
|
|
|
534
553
|
}
|
|
535
554
|
}
|
|
536
555
|
|
|
556
|
+
// ── Pre-Publish pin guard ──────────────────────────────────────────
|
|
557
|
+
// Verify the pinned lifecycle scripts still exist on every Publish
|
|
558
|
+
// pass (first pass and rework passes): the pin step trusted agent
|
|
559
|
+
// prose instead of a mechanical check, so task 24be1cd6 walked to
|
|
560
|
+
// Publish on an empty pin dir. One re-pin and re-verify on a miss;
|
|
561
|
+
// still missing parks the task. Keys are attemptKey-scoped so a
|
|
562
|
+
// rework Publish never re-mints a first-pass key.
|
|
563
|
+
if (step.name === "Publish") {
|
|
564
|
+
let publishPins = parsePinListing(await agent(
|
|
565
|
+
"Verify the pinned lifecycle scripts are present.\n" +
|
|
566
|
+
"Run: ls -1 " + RUN_LIB + " 2>/dev/null || echo PIN_DIR_MISSING\n" +
|
|
567
|
+
"Return the verbatim output as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
568
|
+
{ key: attemptKey("pins-verify-publish", totalReworkCount), label: "Verifying pinned lifecycle scripts",
|
|
569
|
+
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
570
|
+
));
|
|
571
|
+
let missingAtPublish = PIN_BASENAMES.filter(function (b) { return publishPins.indexOf(b) === -1; });
|
|
572
|
+
if (missingAtPublish.length > 0) {
|
|
573
|
+
log("Pin guard at Publish: missing " + missingAtPublish.join(", ") + " — re-pinning once");
|
|
574
|
+
publishPins = parsePinListing(await pinLifecycle(attemptKey("pin-lifecycle-republish", totalReworkCount)));
|
|
575
|
+
missingAtPublish = PIN_BASENAMES.filter(function (b) { return publishPins.indexOf(b) === -1; });
|
|
576
|
+
}
|
|
577
|
+
if (missingAtPublish.length > 0) {
|
|
578
|
+
return await parkTask("Pinned lifecycle scripts missing at Publish even after re-pin: " + missingAtPublish.join(", ") + " in " + RUN_LIB + ".");
|
|
579
|
+
}
|
|
580
|
+
log("Pin guard: all four lifecycle scripts present at Publish");
|
|
581
|
+
}
|
|
582
|
+
|
|
537
583
|
// Publish is optional and target-based. Empty target = prototyping project: skip the phase.
|
|
538
584
|
// Unknown target (incl. legacy "repo") = config error: block.
|
|
539
585
|
if (step.name === "Publish" && !PUBLISH_TYPE) {
|