muse-crew 0.4.4 → 0.4.6
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 +3 -2
- package/lib/merge-lock.sh +149 -68
- package/lib/orphan-sweep.sh +38 -34
- package/lib/publish-npm.sh +28 -3
- package/lib/test-merge-lock.sh +77 -34
- package/lib/test-orphan-sweep.sh +19 -5
- package/lib/test-publish-verify.sh +157 -0
- package/lib/test-version-write.sh +9 -4
- package/lib/worktree-lifecycle.sh +33 -7
- package/package.json +1 -1
- package/workflows/bugfix.js +117 -16
- package/workflows/chore.js +117 -16
- package/workflows/standard.js +114 -23
|
@@ -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' ' ')"
|
|
@@ -202,13 +202,39 @@ cmd_integrate() {
|
|
|
202
202
|
return 0
|
|
203
203
|
fi
|
|
204
204
|
|
|
205
|
-
# Acquire merge lock
|
|
205
|
+
# Acquire the merge lock (serializes version assignment across concurrent
|
|
206
|
+
# runs). The holder is the opaque task+run identity from WORKFLOW_RUN_ID —
|
|
207
|
+
# never a PID (bug 2fc8f52f: short-lived agent PIDs made every concurrent
|
|
208
|
+
# acquire take the stale path). Another task mid Integrate/Publish is
|
|
209
|
+
# normal under simultaneity>1: bounded backoff (30s sleeps, 10min cap,
|
|
210
|
+
# overridable via MERGE_LOCK_RETRY_SECS / MERGE_LOCK_RETRY_CAP_SECS), then
|
|
211
|
+
# give up with LOCK_HELD so the workflow records the failure with a reason
|
|
212
|
+
# (dispatcher retries under its cap, parks after the cap).
|
|
213
|
+
local holder="${WORKFLOW_RUN_ID:-$task_id}"
|
|
214
|
+
local retry_secs="${MERGE_LOCK_RETRY_SECS:-30}"
|
|
215
|
+
local retry_cap="${MERGE_LOCK_RETRY_CAP_SECS:-600}"
|
|
216
|
+
local waited=0
|
|
206
217
|
local lock_result
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
218
|
+
while true; do
|
|
219
|
+
if lock_result=$("$MERGE_LOCK" acquire "$task_id" "$holder"); then
|
|
220
|
+
echo "$lock_result"
|
|
221
|
+
break
|
|
222
|
+
fi
|
|
223
|
+
case "$lock_result" in
|
|
224
|
+
HELD*)
|
|
225
|
+
if [ "$waited" -ge "$retry_cap" ]; then
|
|
226
|
+
echo "LOCK_HELD: $lock_result (waited ${waited}s of ${retry_cap}s backoff)"
|
|
227
|
+
exit 2
|
|
228
|
+
fi
|
|
229
|
+
sleep "$retry_secs"
|
|
230
|
+
waited=$((waited + retry_secs))
|
|
231
|
+
;;
|
|
232
|
+
*)
|
|
233
|
+
echo "LOCK_ERROR: $lock_result"
|
|
234
|
+
exit 2
|
|
235
|
+
;;
|
|
236
|
+
esac
|
|
237
|
+
done
|
|
212
238
|
|
|
213
239
|
# Merge
|
|
214
240
|
if ! git merge --no-ff "$branch" -m "$commit_msg" 2>&1; then
|
|
@@ -338,7 +364,7 @@ cmd_sweep() {
|
|
|
338
364
|
cmd_refresh_lock() {
|
|
339
365
|
local task_id="$1"
|
|
340
366
|
validate_task_id "$task_id"
|
|
341
|
-
"$MERGE_LOCK" refresh "$task_id"
|
|
367
|
+
"$MERGE_LOCK" refresh "$task_id"
|
|
342
368
|
}
|
|
343
369
|
|
|
344
370
|
# --- dispatch ---
|
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,23 @@ 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);
|
|
512
|
+
|
|
513
|
+
// Merge-lock holder identity (bug 2fc8f52f): the opaque task+run identity
|
|
514
|
+
// minted at this run's first claim (never a PID — short-lived agent PIDs
|
|
515
|
+
// made every concurrent acquire take the stale path). Set exactly once on
|
|
516
|
+
// the first claim; stable for the rest of the run's lifetime.
|
|
517
|
+
let lockHolder = taskId;
|
|
483
518
|
|
|
484
519
|
while (i < STEPS.length) {
|
|
485
520
|
const step = STEPS[i];
|
|
@@ -525,6 +560,33 @@ while (i < STEPS.length) {
|
|
|
525
560
|
}
|
|
526
561
|
}
|
|
527
562
|
|
|
563
|
+
// ── Pre-Publish pin guard ──────────────────────────────────────────
|
|
564
|
+
// Verify the pinned lifecycle scripts still exist on every Publish
|
|
565
|
+
// pass (first pass and rework passes): the pin step trusted agent
|
|
566
|
+
// prose instead of a mechanical check, so task 24be1cd6 walked to
|
|
567
|
+
// Publish on an empty pin dir. One re-pin and re-verify on a miss;
|
|
568
|
+
// still missing parks the task. Keys are attemptKey-scoped so a
|
|
569
|
+
// rework Publish never re-mints a first-pass key.
|
|
570
|
+
if (step.name === "Publish") {
|
|
571
|
+
let publishPins = parsePinListing(await agent(
|
|
572
|
+
"Verify the pinned lifecycle scripts are present.\n" +
|
|
573
|
+
"Run: ls -1 " + RUN_LIB + " 2>/dev/null || echo PIN_DIR_MISSING\n" +
|
|
574
|
+
"Return the verbatim output as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
575
|
+
{ key: attemptKey("pins-verify-publish", totalReworkCount), label: "Verifying pinned lifecycle scripts",
|
|
576
|
+
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
577
|
+
));
|
|
578
|
+
let missingAtPublish = PIN_BASENAMES.filter(function (b) { return publishPins.indexOf(b) === -1; });
|
|
579
|
+
if (missingAtPublish.length > 0) {
|
|
580
|
+
log("Pin guard at Publish: missing " + missingAtPublish.join(", ") + " — re-pinning once");
|
|
581
|
+
publishPins = parsePinListing(await pinLifecycle(attemptKey("pin-lifecycle-republish", totalReworkCount)));
|
|
582
|
+
missingAtPublish = PIN_BASENAMES.filter(function (b) { return publishPins.indexOf(b) === -1; });
|
|
583
|
+
}
|
|
584
|
+
if (missingAtPublish.length > 0) {
|
|
585
|
+
return await parkTask("Pinned lifecycle scripts missing at Publish even after re-pin: " + missingAtPublish.join(", ") + " in " + RUN_LIB + ".");
|
|
586
|
+
}
|
|
587
|
+
log("Pin guard: all four lifecycle scripts present at Publish");
|
|
588
|
+
}
|
|
589
|
+
|
|
528
590
|
// Publish is optional and target-based. Empty target = prototyping project: skip the phase.
|
|
529
591
|
// Unknown target (incl. legacy "repo") = config error: block.
|
|
530
592
|
if (step.name === "Publish" && !PUBLISH_TYPE) {
|
|
@@ -637,6 +699,13 @@ while (i < STEPS.length) {
|
|
|
637
699
|
activeSessionId = claimResult.session_id;
|
|
638
700
|
}
|
|
639
701
|
|
|
702
|
+
// Mint the merge-lock holder identity once: task+session of this run's
|
|
703
|
+
// first claim. The task ID alone cannot distinguish two runs of the same
|
|
704
|
+
// task (re-dispatch after park), and a PID is not a valid holder at all.
|
|
705
|
+
if (lockHolder === taskId && activeSessionId) {
|
|
706
|
+
lockHolder = taskId + "/" + activeSessionId;
|
|
707
|
+
}
|
|
708
|
+
|
|
640
709
|
// ── Capture: baseline evidence for experiential tasks ─────────────
|
|
641
710
|
// Hazel's QA capture pass runs right after Triage, before Map, for tasks
|
|
642
711
|
// Sage flagged experiential. The capture itself is parent-driven (the
|
|
@@ -814,11 +883,11 @@ while (i < STEPS.length) {
|
|
|
814
883
|
|
|
815
884
|
} else if (step.name === "Integrate") {
|
|
816
885
|
instructions = "Merge the approved task branch into main.\n\n" +
|
|
817
|
-
"Run: "+ LIFECYCLE_ENV + " integrate " + taskId + " \"merge: fix: " + safeTitle + "\"\n\n" +
|
|
886
|
+
"Run: "+ LIFECYCLE_ENV + "WORKFLOW_RUN_ID=" + lockHolder + " integrate " + taskId + " \"merge: fix: " + safeTitle + "\"\n\n" +
|
|
818
887
|
"Read the output:\n" +
|
|
819
888
|
"- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
|
|
820
889
|
"- If it contains MERGED_EMPTY, the branch had no commits ahead of main (a runtime-state deliverable, declared by Build as repo_diff: none). Integration succeeded vacuously: the merge lock was NOT taken and there is no new commit. Report 'merged empty: no repo changes — deliverable was runtime state', then end your report with exactly this line: VERDICT: PASS. SKIP STEP 2 (push): there is no new commit to push.\n" +
|
|
821
|
-
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Report 'merge lock held', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
890
|
+
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish) and the 10-minute bounded backoff is exhausted. Report 'merge lock held after bounded backoff', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
822
891
|
"- If it contains CONFLICT, the plain merge failed — the merge was aborted, main is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
|
|
823
892
|
"RESOLUTION:\n" +
|
|
824
893
|
"R1. Refresh the merge lock FIRST (a long resolution must not silently lose the lock to the orphan sweep): "+ LIFECYCLE_ENV + " refresh-lock " + taskId + ". Create a scratch worktree WITH A NEW BRANCH (main is already checked out in the repo checkout, so git forbids checking it out a second time): cd " + REPO_PATH + " && git worktree add -b resolve/" + taskId + " /tmp/crew-resolve-" + taskId + " main. Reproduce the conflict in the scratch worktree: cd /tmp/crew-resolve-" + taskId + " && git merge " + TASK_BRANCH + ". This reproduces the exact conflict (main has not moved — the lock was held throughout). The task branch " + TASK_BRANCH + " is never modified.\n" +
|
|
@@ -906,14 +975,46 @@ while (i < STEPS.length) {
|
|
|
906
975
|
}
|
|
907
976
|
var publishFailure = null;
|
|
908
977
|
if (rebuildTrigger.edit_started) {
|
|
909
|
-
// STEP 1b (mechanical): bounded poll for build completion
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
978
|
+
// STEP 1b (mechanical): bounded poll for build completion, chunked so
|
|
979
|
+
// the merge-lock lease is refreshed before it can expire. The 600s
|
|
980
|
+
// lease is shorter than the worst-case 10-minute build poll, so the
|
|
981
|
+
// poll runs in three chunks (7 checks x 30s ~= 3.5 min each) with a
|
|
982
|
+
// holder-only lease refresh between chunks. If a refresh fails, the
|
|
983
|
+
// lock was lost: stop the run and park the task — never continue to
|
|
984
|
+
// a provenance stamp or version assignment without holding the lock.
|
|
985
|
+
var buildPoll = null;
|
|
986
|
+
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
987
|
+
if (chunk > 1) {
|
|
988
|
+
var refreshPoll = await agent(
|
|
989
|
+
"Refresh the merge lock for task " + taskId + ".\n" +
|
|
990
|
+
"Run: " + LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
|
|
991
|
+
"If the output contains REFRESHED, return JSON { \"held\": true } and nothing else. Otherwise return JSON { \"held\": false, \"output\": \"<verbatim output>\" } and nothing else.",
|
|
992
|
+
{ key: attemptKey("publish-lock-refresh-poll-" + taskId + "-c" + chunk, totalReworkCount), label: "Refreshing merge lock during build poll",
|
|
993
|
+
schema: { type: "object", properties: { held: { type: "boolean" }, output: { type: "string" } }, required: ["held"] },
|
|
994
|
+
timeoutMs: 60000 }
|
|
995
|
+
);
|
|
996
|
+
if (!refreshPoll || refreshPoll.held !== true) {
|
|
997
|
+
return await parkTask("Merge-lock lease lost during the artifact build poll (refresh before chunk " + chunk + " of 3 failed: " + ((refreshPoll && refreshPoll.output) || "no output") + "). Fail-closed: stopping before any provenance stamp or version assignment.");
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// Chunk 1 keeps the original stable key; later chunks use -c<N>
|
|
1001
|
+
// suffixed keys. All are attemptKey-scoped so rework passes stay
|
|
1002
|
+
// disjoint (replay-key contract).
|
|
1003
|
+
var pollKey = (chunk === 1)
|
|
1004
|
+
? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
|
|
1005
|
+
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
|
|
1006
|
+
buildPoll = await agent(
|
|
1007
|
+
"Poll artifact_status for slug \"" + PUBLISH_SLUG + "\" until no build is running. Check every 30 seconds, up to 7 checks (3.5 minutes max).\n" +
|
|
1008
|
+
"Return JSON { \"build_done\": <true if no build is running within budget, false on timeout>, \"status\": \"<final status or timeout note>\" } and nothing else.",
|
|
1009
|
+
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
1010
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" } }, required: ["build_done"] },
|
|
1011
|
+
timeoutMs: 270000 }
|
|
1012
|
+
);
|
|
1013
|
+
if (buildPoll && buildPoll.build_done) { break; }
|
|
1014
|
+
}
|
|
1015
|
+
if (!buildPoll || !buildPoll.build_done) {
|
|
1016
|
+
buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
|
|
1017
|
+
}
|
|
917
1018
|
if (buildPoll.build_done) {
|
|
918
1019
|
// STEP 1c (mechanical): stamp provenance from workflow-computed values.
|
|
919
1020
|
var provStamp = await agent(
|
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,23 @@ 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);
|
|
470
|
+
|
|
471
|
+
// Merge-lock holder identity (bug 2fc8f52f): the opaque task+run identity
|
|
472
|
+
// minted at this run's first claim (never a PID — short-lived agent PIDs
|
|
473
|
+
// made every concurrent acquire take the stale path). Set exactly once on
|
|
474
|
+
// the first claim; stable for the rest of the run's lifetime.
|
|
475
|
+
let lockHolder = taskId;
|
|
441
476
|
|
|
442
477
|
while (i < STEPS.length) {
|
|
443
478
|
const step = STEPS[i];
|
|
@@ -483,6 +518,33 @@ while (i < STEPS.length) {
|
|
|
483
518
|
}
|
|
484
519
|
}
|
|
485
520
|
|
|
521
|
+
// ── Pre-Publish pin guard ──────────────────────────────────────────
|
|
522
|
+
// Verify the pinned lifecycle scripts still exist on every Publish
|
|
523
|
+
// pass (first pass and rework passes): the pin step trusted agent
|
|
524
|
+
// prose instead of a mechanical check, so task 24be1cd6 walked to
|
|
525
|
+
// Publish on an empty pin dir. One re-pin and re-verify on a miss;
|
|
526
|
+
// still missing parks the task. Keys are attemptKey-scoped so a
|
|
527
|
+
// rework Publish never re-mints a first-pass key.
|
|
528
|
+
if (step.name === "Publish") {
|
|
529
|
+
let publishPins = parsePinListing(await agent(
|
|
530
|
+
"Verify the pinned lifecycle scripts are present.\n" +
|
|
531
|
+
"Run: ls -1 " + RUN_LIB + " 2>/dev/null || echo PIN_DIR_MISSING\n" +
|
|
532
|
+
"Return the verbatim output as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
533
|
+
{ key: attemptKey("pins-verify-publish", reworkCount), label: "Verifying pinned lifecycle scripts",
|
|
534
|
+
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
535
|
+
));
|
|
536
|
+
let missingAtPublish = PIN_BASENAMES.filter(function (b) { return publishPins.indexOf(b) === -1; });
|
|
537
|
+
if (missingAtPublish.length > 0) {
|
|
538
|
+
log("Pin guard at Publish: missing " + missingAtPublish.join(", ") + " — re-pinning once");
|
|
539
|
+
publishPins = parsePinListing(await pinLifecycle(attemptKey("pin-lifecycle-republish", reworkCount)));
|
|
540
|
+
missingAtPublish = PIN_BASENAMES.filter(function (b) { return publishPins.indexOf(b) === -1; });
|
|
541
|
+
}
|
|
542
|
+
if (missingAtPublish.length > 0) {
|
|
543
|
+
return await parkTask("Pinned lifecycle scripts missing at Publish even after re-pin: " + missingAtPublish.join(", ") + " in " + RUN_LIB + ".");
|
|
544
|
+
}
|
|
545
|
+
log("Pin guard: all four lifecycle scripts present at Publish");
|
|
546
|
+
}
|
|
547
|
+
|
|
486
548
|
// Publish is optional and target-based. Empty target = prototyping project: skip the phase.
|
|
487
549
|
// Unknown target (incl. legacy "repo") = config error: block.
|
|
488
550
|
if (step.name === "Publish" && !PUBLISH_TYPE) {
|
|
@@ -595,6 +657,13 @@ while (i < STEPS.length) {
|
|
|
595
657
|
activeSessionId = claimResult.session_id;
|
|
596
658
|
}
|
|
597
659
|
|
|
660
|
+
// Mint the merge-lock holder identity once: task+session of this run's
|
|
661
|
+
// first claim. The task ID alone cannot distinguish two runs of the same
|
|
662
|
+
// task (re-dispatch after park), and a PID is not a valid holder at all.
|
|
663
|
+
if (lockHolder === taskId && activeSessionId) {
|
|
664
|
+
lockHolder = taskId + "/" + activeSessionId;
|
|
665
|
+
}
|
|
666
|
+
|
|
598
667
|
// ── Capture: baseline evidence for experiential tasks ─────────────
|
|
599
668
|
// Hazel's QA capture pass runs right after Triage, before Map, for tasks
|
|
600
669
|
// Sage flagged experiential. The capture itself is parent-driven (the
|
|
@@ -751,11 +820,11 @@ while (i < STEPS.length) {
|
|
|
751
820
|
|
|
752
821
|
} else if (step.name === "Integrate") {
|
|
753
822
|
instructions = "Merge the approved task branch into main.\n\n" +
|
|
754
|
-
"Run: "+ LIFECYCLE_ENV + " integrate " + taskId + " \"merge: chore: " + safeTitle + "\"\n\n" +
|
|
823
|
+
"Run: "+ LIFECYCLE_ENV + "WORKFLOW_RUN_ID=" + lockHolder + " integrate " + taskId + " \"merge: chore: " + safeTitle + "\"\n\n" +
|
|
755
824
|
"Read the output:\n" +
|
|
756
825
|
"- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
|
|
757
826
|
"- If it contains MERGED_EMPTY, the branch had no commits ahead of main (a runtime-state deliverable, declared by Build as repo_diff: none). Integration succeeded vacuously: the merge lock was NOT taken and there is no new commit. Report 'merged empty: no repo changes — deliverable was runtime state', then end your report with exactly this line: VERDICT: PASS. SKIP STEP 2 (push): there is no new commit to push.\n" +
|
|
758
|
-
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Report 'merge lock held', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
827
|
+
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish) and the 10-minute bounded backoff is exhausted. Report 'merge lock held after bounded backoff', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
759
828
|
"- If it contains CONFLICT, the plain merge failed — the merge was aborted, main is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
|
|
760
829
|
"RESOLUTION:\n" +
|
|
761
830
|
"R1. Refresh the merge lock FIRST (a long resolution must not silently lose the lock to the orphan sweep): "+ LIFECYCLE_ENV + " refresh-lock " + taskId + ". Create a scratch worktree WITH A NEW BRANCH (main is already checked out in the repo checkout, so git forbids checking it out a second time): cd " + REPO_PATH + " && git worktree add -b resolve/" + taskId + " /tmp/crew-resolve-" + taskId + " main. Reproduce the conflict in the scratch worktree: cd /tmp/crew-resolve-" + taskId + " && git merge " + TASK_BRANCH + ". This reproduces the exact conflict (main has not moved — the lock was held throughout). The task branch " + TASK_BRANCH + " is never modified.\n" +
|
|
@@ -843,14 +912,46 @@ while (i < STEPS.length) {
|
|
|
843
912
|
}
|
|
844
913
|
var publishFailure = null;
|
|
845
914
|
if (rebuildTrigger.edit_started) {
|
|
846
|
-
// STEP 1b (mechanical): bounded poll for build completion
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
915
|
+
// STEP 1b (mechanical): bounded poll for build completion, chunked so
|
|
916
|
+
// the merge-lock lease is refreshed before it can expire. The 600s
|
|
917
|
+
// lease is shorter than the worst-case 10-minute build poll, so the
|
|
918
|
+
// poll runs in three chunks (7 checks x 30s ~= 3.5 min each) with a
|
|
919
|
+
// holder-only lease refresh between chunks. If a refresh fails, the
|
|
920
|
+
// lock was lost: stop the run and park the task — never continue to
|
|
921
|
+
// a provenance stamp or version assignment without holding the lock.
|
|
922
|
+
var buildPoll = null;
|
|
923
|
+
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
924
|
+
if (chunk > 1) {
|
|
925
|
+
var refreshPoll = await agent(
|
|
926
|
+
"Refresh the merge lock for task " + taskId + ".\n" +
|
|
927
|
+
"Run: " + LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
|
|
928
|
+
"If the output contains REFRESHED, return JSON { \"held\": true } and nothing else. Otherwise return JSON { \"held\": false, \"output\": \"<verbatim output>\" } and nothing else.",
|
|
929
|
+
{ key: attemptKey("publish-lock-refresh-poll-" + taskId + "-c" + chunk, reworkCount), label: "Refreshing merge lock during build poll",
|
|
930
|
+
schema: { type: "object", properties: { held: { type: "boolean" }, output: { type: "string" } }, required: ["held"] },
|
|
931
|
+
timeoutMs: 60000 }
|
|
932
|
+
);
|
|
933
|
+
if (!refreshPoll || refreshPoll.held !== true) {
|
|
934
|
+
return await parkTask("Merge-lock lease lost during the artifact build poll (refresh before chunk " + chunk + " of 3 failed: " + ((refreshPoll && refreshPoll.output) || "no output") + "). Fail-closed: stopping before any provenance stamp or version assignment.");
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
// Chunk 1 keeps the original stable key; later chunks use -c<N>
|
|
938
|
+
// suffixed keys. All are attemptKey-scoped so rework passes stay
|
|
939
|
+
// disjoint (replay-key contract).
|
|
940
|
+
var pollKey = (chunk === 1)
|
|
941
|
+
? attemptKey("publish-artifact-poll-" + taskId, reworkCount)
|
|
942
|
+
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, reworkCount);
|
|
943
|
+
buildPoll = await agent(
|
|
944
|
+
"Poll artifact_status for slug \"" + PUBLISH_SLUG + "\" until no build is running. Check every 30 seconds, up to 7 checks (3.5 minutes max).\n" +
|
|
945
|
+
"Return JSON { \"build_done\": <true if no build is running within budget, false on timeout>, \"status\": \"<final status or timeout note>\" } and nothing else.",
|
|
946
|
+
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
947
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" } }, required: ["build_done"] },
|
|
948
|
+
timeoutMs: 270000 }
|
|
949
|
+
);
|
|
950
|
+
if (buildPoll && buildPoll.build_done) { break; }
|
|
951
|
+
}
|
|
952
|
+
if (!buildPoll || !buildPoll.build_done) {
|
|
953
|
+
buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
|
|
954
|
+
}
|
|
854
955
|
if (buildPoll.build_done) {
|
|
855
956
|
// STEP 1c (mechanical): stamp provenance from workflow-computed values.
|
|
856
957
|
var provStamp = await agent(
|