muse-crew 0.14.4 → 0.14.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/AGENTS.md +1 -1
- package/API.md +61 -7
- package/docs/decisions/AGENTS.md +3 -0
- package/docs/decisions/publish-path.md +244 -1
- package/docs/guide.md +42 -6
- package/docs/publish-unknown-recovery.md +28 -113
- package/docs/publish-verification.md +148 -445
- package/docs/release-integrity.md +5 -3
- package/docs/reviews/critic-0145.md +106 -0
- package/lib/AGENTS.md +4 -8
- package/lib/advance-publish-base.js +3 -3
- package/lib/compose-evidence-caption.js +1 -2
- package/lib/compute-publish-diff.js +113 -15
- package/lib/crew-api.js +855 -725
- package/lib/publish-note-vocabulary.js +137 -24
- package/lib/schema.sql +19 -0
- package/package.json +1 -1
- package/seed/AGENTS.md +1 -0
- package/seed/cron-body-ack-scan.md +44 -0
- package/seed/cron-body-template.md +65 -65
- package/seed/crons.json +12 -0
- package/workflows/bugfix.js +146 -740
- package/workflows/chore.js +146 -740
- package/workflows/crew-dispatch.js +36 -0
- package/workflows/crew-init.js +23 -0
- package/workflows/crew-uninstall.js +7 -4
- package/workflows/standard.js +146 -763
- package/lib/build-readback-request.js +0 -140
- package/lib/classify-publish-absence.js +0 -462
- package/lib/publish-content.js +0 -154
- package/lib/readback-disk.js +0 -195
- package/lib/retry-publish.js +0 -405
- package/lib/verify-publish.js +0 -414
package/workflows/bugfix.js
CHANGED
|
@@ -237,14 +237,6 @@ var TOOL_CHECK_PREAMBLE =
|
|
|
237
237
|
"2. Write exactly one line: artifact_tools: ok - or artifact_tools: missing if the call failed or the tool does not exist.\n" +
|
|
238
238
|
"3. Run the shell command: echo tool-probe-ok - then write exactly one line: shell_transport: ok - or shell_transport: unavailable if you cannot run shell commands.\n" +
|
|
239
239
|
"Then do the assignment below.\n\n";
|
|
240
|
-
// ARTIFACT_LOAD_PREAMBLE - the one-line load instruction that precedes every
|
|
241
|
-
// special-purpose prompt calling artifact tools (rebuild trigger, provenance
|
|
242
|
-
// stamp, publish verification). The artifact tool namespace is deferred for
|
|
243
|
-
// workflow children: present but invisible until the child loads it via
|
|
244
|
-
// tool_search.load_tool_namespace (bug 3472bf36 root cause). Byte-identical
|
|
245
|
-
// across standard/bugfix/chore - pinned by tests/artifact-tools.test.js.
|
|
246
|
-
var ARTIFACT_LOAD_PREAMBLE =
|
|
247
|
-
"First call tool_search.load_tool_namespace with paths [\"artifact\"] - the artifact tool namespace is deferred and invisible until loaded.\n";
|
|
248
240
|
function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
|
|
249
241
|
// reason: "discarded" (the runtime threw the output away - it could not be
|
|
250
242
|
// machine-read), "empty" (agent() returned without throwing but produced
|
|
@@ -392,10 +384,10 @@ function hydrateReleaseDecision(rec) {
|
|
|
392
384
|
}
|
|
393
385
|
|
|
394
386
|
// See docs/decisions/publish-path.md#publish-attempt-ledger: every trigger outcome is recorded in the durable ledger.
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
async function recordPublishLedger(entry, rework
|
|
387
|
+
// (2026-09-20, one-party worker-owned publish) No keyTag: every ledger
|
|
388
|
+
// write in a Publish pass is followed by a return, so no pass can write two
|
|
389
|
+
// same-outcome entries — the runtime replay key can never collide in-pass.
|
|
390
|
+
async function recordPublishLedger(entry, rework) {
|
|
399
391
|
try {
|
|
400
392
|
var ledgerDir = crewHome + "/.publish-ledger";
|
|
401
393
|
var line = JSON.stringify({
|
|
@@ -413,6 +405,20 @@ async function recordPublishLedger(entry, rework, keyTag) {
|
|
|
413
405
|
// D1 (2026-09-19): issued_at = upper bound on the trigger-issuance
|
|
414
406
|
// instant (null when no trigger); ts = ledger-write instant.
|
|
415
407
|
issued_at: entry.issued_at || null,
|
|
408
|
+
// (2026-09-20, one-party worker-owned publish) issuer = which party
|
|
409
|
+
// wrote the entry ("workflow" for the intent entry, "tick-worker" for
|
|
410
|
+
// the worker's own issuance); diff_path/diff_sha256 locate the
|
|
411
|
+
// checksummed diff the worker issues. The workflow never writes an
|
|
412
|
+
// issuance ("submitted") entry — it did not issue.
|
|
413
|
+
issuer: entry.issuer || null,
|
|
414
|
+
diff_path: entry.diff_path || null,
|
|
415
|
+
diff_sha256: entry.diff_sha256 || null,
|
|
416
|
+
base: entry.base || null,
|
|
417
|
+
// (0.14.6) Numeric publication attempt + the attempt's deterministic
|
|
418
|
+
// version. Attempt 1 is staged here by the workflow; attempts 2-3 are
|
|
419
|
+
// staged by scan-ack-pending's re-issue path with fresh versions.
|
|
420
|
+
publish_attempt: entry.publish_attempt || null,
|
|
421
|
+
version: entry.version || null,
|
|
416
422
|
});
|
|
417
423
|
var sq = function(s) { return "'" + String(s).split("'").join("'\\''") + "'"; };
|
|
418
424
|
var res = await agent(
|
|
@@ -420,13 +426,19 @@ async function recordPublishLedger(entry, rework, keyTag) {
|
|
|
420
426
|
"Run: mkdir -p " + sq(ledgerDir) + " && printf '%s\n' " + sq(line) +
|
|
421
427
|
" | sed \"s/@LEDGER_TS@/$(date -u +%Y-%m-%dT%H:%M:%SZ)/\" >> " + sq(ledgerDir + "/" + PUBLISH_SLUG + ".jsonl") + " && echo LEDGER_OK\n" +
|
|
422
428
|
"Return JSON { \"result\": \"<verbatim output>\" } and nothing else.",
|
|
423
|
-
{ key: attemptKey("publish-ledger-" + taskId + "-" + entry.outcome
|
|
429
|
+
{ key: attemptKey("publish-ledger-" + taskId + "-" + entry.outcome, rework),
|
|
424
430
|
label: "Recording publish attempt in ledger",
|
|
425
431
|
schema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] } }
|
|
426
432
|
);
|
|
427
|
-
|
|
433
|
+
// (2026-09-20 REVIEW) The intent entry is the machine's anchor — the
|
|
434
|
+
// caller gates on this return. LEDGER_OK in the agent's verbatim output
|
|
435
|
+
// is the only mechanical proof the line landed.
|
|
436
|
+
var ok = res && typeof res.result === "string" && res.result.indexOf("LEDGER_OK") !== -1;
|
|
437
|
+
log("Noted publish outcome '" + entry.outcome + "' for task " + taskId + " in ledger" + (ok ? "" : " (WRITE UNCONFIRMED)"));
|
|
438
|
+
return ok;
|
|
428
439
|
} catch (e) {
|
|
429
440
|
log("Publish ledger: write failed for task " + taskId + " (non-fatal, observability only): " + (e && e.message ? e.message : e));
|
|
441
|
+
return false;
|
|
430
442
|
}
|
|
431
443
|
}
|
|
432
444
|
|
|
@@ -452,11 +464,6 @@ function extractAlreadyMerged(workerText) {
|
|
|
452
464
|
return m ? { sha: m[1].toLowerCase() } : { sha: null };
|
|
453
465
|
}
|
|
454
466
|
|
|
455
|
-
// See docs/decisions/publish-path.md#refusal-signal: the refusal signal must be the ENTIRE trimmed turn output.
|
|
456
|
-
function extractRefusal(workerText) {
|
|
457
|
-
var m = /^ARTIFACT_EDIT_REFUSED:\s*(.+?)\s*$/.exec(String(workerText || "").trim());
|
|
458
|
-
return m ? m[1].slice(0, 300) : null;
|
|
459
|
-
}
|
|
460
467
|
|
|
461
468
|
// See docs/decisions/qa-reproduce.md#worktree-confinement: the Build agent must declare its worktree.
|
|
462
469
|
function extractWorktree(workerText) {
|
|
@@ -1319,10 +1326,41 @@ while (i < STEPS.length) {
|
|
|
1319
1326
|
"VERDICT: PASS\n\n";
|
|
1320
1327
|
} else if (PUBLISH_TYPE === "artifact") {
|
|
1321
1328
|
// See docs/decisions/publish-path.md#deterministic-artifact-publish: the work agent never publishes; the parent runs the deterministic publish script.
|
|
1322
|
-
|
|
1329
|
+
// Blocker 22 (one-party publish, room #24): verified re-entry guard.
|
|
1330
|
+
// The workflow parks at publish intent; the tick worker issues
|
|
1331
|
+
// artifact_edit, verifies by read-back, stamps provenance, and
|
|
1332
|
+
// re-queues. The dispatcher resumes this task at the Publish step
|
|
1333
|
+
// (the parked session's own step — the failed→retry path). If the
|
|
1334
|
+
// stamped provenance names THIS task and its source_commit is the
|
|
1335
|
+
// repo's current HEAD, the merge commit was already published and
|
|
1336
|
+
// verified by the parent: issuance is skipped entirely (the
|
|
1337
|
+
// empty-diff park below must NOT fire on this branch) and the
|
|
1338
|
+
// Publish work agent runs with the verified framing instead. A guard
|
|
1339
|
+
// failure never claims verification — it proceeds down the normal path.
|
|
1340
|
+
var publishAlreadyVerified = false;
|
|
1341
|
+
try {
|
|
1342
|
+
var reentryCheck = await agent(
|
|
1343
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
1344
|
+
"Then run in shell and return the stdout verbatim: cd " + REPO_PATH + " && git rev-parse HEAD\n" +
|
|
1345
|
+
"Return JSON { \"source_commit\": \"<provenance.source_commit, empty string when null>\", \"stamp_task_id\": \"<provenance.task_id, empty string when null>\", \"head\": \"<the rev-parse output, trimmed>\" } and nothing else.",
|
|
1346
|
+
{ key: attemptKey("publish-reentry-guard-" + taskId, totalReworkCount), label: "Checking for a verified publish re-entry",
|
|
1347
|
+
schema: { type: "object", properties: { source_commit: { type: "string" }, stamp_task_id: { type: "string" }, head: { type: "string" } }, required: ["source_commit", "stamp_task_id", "head"] } }
|
|
1348
|
+
);
|
|
1349
|
+
var reentrySC = String((reentryCheck && reentryCheck.source_commit) || "").trim();
|
|
1350
|
+
var reentryTID = String((reentryCheck && reentryCheck.stamp_task_id) || "").trim();
|
|
1351
|
+
var reentryHEAD = String((reentryCheck && reentryCheck.head) || "").trim();
|
|
1352
|
+
if (reentrySC && reentryHEAD && reentrySC === reentryHEAD && reentryTID === taskId) {
|
|
1353
|
+
publishAlreadyVerified = true;
|
|
1354
|
+
log("Publish verified re-entry for task " + taskId + ": provenance stamps " + reentrySC + " == HEAD for this task — the parent already published and verified it; issuance skipped");
|
|
1355
|
+
}
|
|
1356
|
+
} catch (reentryErr) {
|
|
1357
|
+
log("Publish re-entry guard threw for task " + taskId + " (" + (reentryErr && reentryErr.message ? reentryErr.message : reentryErr) + ") — proceeding with the normal publish path; a guard failure never claims verification");
|
|
1358
|
+
}
|
|
1359
|
+
// publishAlreadyVerified is consumed by the intent-tail edit (next
|
|
1360
|
+
// chunk): on the verified branch the workflow skips to the Publish
|
|
1361
|
+
// work agent with the verified framing and proceeds to QA.
|
|
1323
1362
|
var publishLockRefreshed = false;
|
|
1324
1363
|
var publishSkippedNoLock = false;
|
|
1325
|
-
var publishBuildLanded = false; // true once the artifact build poll completes: content exists to verify; the provenance stamp is deferred to the parent (docs/publish-verification.md)
|
|
1326
1364
|
try {
|
|
1327
1365
|
// STEP 0 (mechanical): read the merge-lock state explicitly — never
|
|
1328
1366
|
// infer it from prose. An empty-diff Integrate (MERGED_EMPTY)
|
|
@@ -1346,7 +1384,11 @@ while (i < STEPS.length) {
|
|
|
1346
1384
|
publishLockRefreshed = true;
|
|
1347
1385
|
}
|
|
1348
1386
|
// See docs/decisions/publish-path.md#step1-builder-source: the builder's source tree is NOT the crew's repo; verify report before stamping.
|
|
1349
|
-
|
|
1387
|
+
// (2026-09-20, one-party worker-owned publish) A verified re-entry
|
|
1388
|
+
// skips the whole issuance tail: the edit already went out, was
|
|
1389
|
+
// verified by the parent read-back, and was stamped. The empty-diff
|
|
1390
|
+
// park must not fire on this branch.
|
|
1391
|
+
if (!publishSkippedNoLock && !publishAlreadyVerified) {
|
|
1350
1392
|
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
1351
1393
|
// Minted once here (not re-minted per use site) so the ledger always
|
|
1352
1394
|
// records the exact key that was issued — and so a re-minted duplicate
|
|
@@ -1446,7 +1488,7 @@ while (i < STEPS.length) {
|
|
|
1446
1488
|
var publishDiffFile = crewHome + "/.publish-diffs/" + taskId + (totalReworkCount > 0 ? "-r" + totalReworkCount : "") + ".diff";
|
|
1447
1489
|
var diffResult = await agent(
|
|
1448
1490
|
"Run exactly one command and nothing else:\n" +
|
|
1449
|
-
"node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --out \"" + publishDiffFile + "\"\n" +
|
|
1491
|
+
"node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --task-id \"" + taskId + "\" --attempt 1 --out \"" + publishDiffFile + "\"\n" +
|
|
1450
1492
|
"The script prints one small JSON object to stdout. Return the stdout verbatim as { \"output\": \"<verbatim stdout>\" } and nothing else. Do not interpret it.",
|
|
1451
1493
|
{ key: attemptKey("publish-artifact-diff-" + taskId, totalReworkCount), label: "Computing publish diff from stamped base",
|
|
1452
1494
|
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
@@ -1469,6 +1511,18 @@ while (i < STEPS.length) {
|
|
|
1469
1511
|
var mergeCommitForPublish = String(diffSummary.commit || "").trim();
|
|
1470
1512
|
var mergeCommitShortForPublish = String(mergeCommitForPublish).substring(0, 7) || "unknown";
|
|
1471
1513
|
var publishDiffSha256 = String(diffSummary.sha256 || "");
|
|
1514
|
+
// (0.14.6) The per-attempt publish version, synthesized into the
|
|
1515
|
+
// staged diff as .crew-publish-version-<task_id> (per-task receipt —
|
|
1516
|
+
// two same-project tasks can publish concurrently) by the
|
|
1517
|
+
// deterministic script.
|
|
1518
|
+
// The workflow never invents it: the script derives it from
|
|
1519
|
+
// task+commit+attempt, and the intent scan re-derives it before
|
|
1520
|
+
// claiming. A missing/malformed version parks — never publish blind.
|
|
1521
|
+
var publishVersion = String(diffSummary.version || "");
|
|
1522
|
+
// Mirrors isPublishVersion() in lib/crew-api.js: <40-hex commit>:<attempt>.
|
|
1523
|
+
if (!/^[0-9a-f]{40}:[1-9][0-9]*$/.test(publishVersion)) {
|
|
1524
|
+
return await parkTask("Publish diff for commit " + (mergeCommitForPublish || "unknown") + " carries no usable publish version — cannot stage a verifiable intent. Human attention needed.");
|
|
1525
|
+
}
|
|
1472
1526
|
if (!diffSummary.bytes) {
|
|
1473
1527
|
return await parkTask("Publish diff is empty for commit " + (mergeCommitForPublish || "unknown") + " — a merge lock was held but there is no change to carry. Human attention needed.");
|
|
1474
1528
|
}
|
|
@@ -1490,685 +1544,59 @@ while (i < STEPS.length) {
|
|
|
1490
1544
|
if (!diffSummary.file_count) {
|
|
1491
1545
|
return await parkTask("Publish diff parsed to zero files for commit " + (mergeCommitForPublish || "unknown") + " — cannot verify application. Human attention needed.");
|
|
1492
1546
|
}
|
|
1493
|
-
// The
|
|
1547
|
+
// The tick worker reads the diff from the checksummed file — the
|
|
1494
1548
|
// workflow never holds diff bytes (room #14, 2026-09-17). The file
|
|
1495
|
-
// is small by construction: the 200-line budget above gates
|
|
1496
|
-
//
|
|
1497
|
-
// a mismatch stops
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
"No report is needed: do not return JSON, do not summarize what you did, do not echo the diff. End your turn after the artifact_edit call.\n";
|
|
1522
|
-
// See docs/decisions/publish-path.md#agent-id-attribution: the artifact build's agent_id is attributed to the edit call.
|
|
1523
|
-
var rebuildAgentId = null;
|
|
1524
|
-
// See docs/decisions/publish-path.md#applied-report-gone: the builder's applied report is gone; the workflow verifies differently.
|
|
1525
|
-
var publishAppliedObservation = "missing-report";
|
|
1526
|
-
// See docs/decisions/publish-path.md#durable-evidence-snapshot: snapshot the audit-dir listing BEFORE the trigger; fallback diffs before/after.
|
|
1527
|
-
var auditDirsBeforeTrigger = [];
|
|
1528
|
-
var auditBeforeOk = false;
|
|
1529
|
-
// Pre-trigger baselines (design §1.9): the audit-dir listing and the
|
|
1530
|
-
// manifest snapshot. The verified-path freshness check compares the
|
|
1531
|
-
// post-trigger manifest against the baseline (built_at advance +
|
|
1532
|
-
// content_sha256 change). Best-effort, never gates — a missing
|
|
1533
|
-
// manifest baseline fails the verified path closed.
|
|
1534
|
-
var preTriggerManifest = null;
|
|
1535
|
-
try {
|
|
1536
|
-
var auditBefore = await agent(
|
|
1537
|
-
"Capture pre-trigger baselines for slug \"" + PUBLISH_SLUG + "\" (best-effort snapshots, never gates).\n" +
|
|
1538
|
-
"Run: ls -1 ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/ 2>/dev/null; echo ---MANIFEST---; cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/.space-build/manifest.json 2>/dev/null\n" +
|
|
1539
|
-
"Return JSON { \"dirs\": \"<newline-separated names, empty string when missing>\", \"manifest\": \"<the manifest's full text, or empty string when missing/unreadable>\" } and nothing else.",
|
|
1540
|
-
{ key: attemptKey("publish-baseline-before-" + taskId, totalReworkCount), label: "Snapshotting baselines before rebuild trigger",
|
|
1541
|
-
schema: { type: "object", properties: { dirs: { type: "string" }, manifest: { type: "string" } }, required: ["dirs", "manifest"] } }
|
|
1542
|
-
);
|
|
1543
|
-
auditDirsBeforeTrigger = String((auditBefore && auditBefore.dirs) || "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
|
|
1544
|
-
var manifestText = String((auditBefore && auditBefore.manifest) || "").trim();
|
|
1545
|
-
if (manifestText) {
|
|
1546
|
-
var manifestJson = JSON.parse(manifestText);
|
|
1547
|
-
preTriggerManifest = {
|
|
1548
|
-
built_at: typeof manifestJson.built_at === "string" ? manifestJson.built_at : null,
|
|
1549
|
-
content_sha256: typeof manifestJson.content_sha256 === "string" ? manifestJson.content_sha256 : null,
|
|
1550
|
-
};
|
|
1551
|
-
}
|
|
1552
|
-
// Arm only after both baselines parse.
|
|
1553
|
-
auditBeforeOk = true;
|
|
1554
|
-
log("Publish pre-trigger baselines for task " + taskId + ": " + auditDirsBeforeTrigger.length + " audit dirs, manifest " + (preTriggerManifest ? "built_at=" + preTriggerManifest.built_at : "none"));
|
|
1555
|
-
} catch (auditBeforeErr) {
|
|
1556
|
-
log("Publish pre-trigger baselines failed for task " + taskId + " (non-fatal): audit fallback DISABLED for this attempt — without a baseline, historical dirs would look new: " + (auditBeforeErr && auditBeforeErr.message ? auditBeforeErr.message : auditBeforeErr));
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
// See docs/decisions/publish-path.md#fire-and-forget-trigger: the trigger child returns immediately; the workflow owns observation and verdict.
|
|
1560
|
-
var publishToolsOk = false;
|
|
1561
|
-
var publishToolsMissing = false;
|
|
1562
|
-
for (var toolcheckAttempt = 1; toolcheckAttempt <= 2 && !publishToolsOk; toolcheckAttempt++) {
|
|
1563
|
-
try {
|
|
1564
|
-
var toolcheckResult = await agent(
|
|
1565
|
-
"Check whether the artifact tool namespace is available.\n" +
|
|
1566
|
-
"Call tool_search.load_tool_namespace with paths [\"artifact\"].\n" +
|
|
1567
|
-
"Then emit exactly one line and nothing else: ARTIFACT_TOOLS: <ok if the load succeeded and artifact_edit and artifact_status are now functions, missing otherwise>.\n" +
|
|
1568
|
-
"Return JSON { \"signal\": \"<the exact ARTIFACT_TOOLS line>\" } and nothing else.",
|
|
1569
|
-
{ key: attemptKey("publish-artifact-toolcheck-" + taskId + (toolcheckAttempt === 1 ? "" : "-retry2"), totalReworkCount),
|
|
1570
|
-
label: "Checking artifact tool availability" + (toolcheckAttempt === 1 ? "" : " (retry)"),
|
|
1571
|
-
schema: { type: "object", properties: { signal: { type: "string" } }, required: ["signal"] } }
|
|
1572
|
-
);
|
|
1573
|
-
var toolSignal = String((toolcheckResult && toolcheckResult.signal) || "");
|
|
1574
|
-
if (/ARTIFACT_TOOLS:\s*ok/.test(toolSignal)) {
|
|
1575
|
-
publishToolsOk = true;
|
|
1576
|
-
} else if (/ARTIFACT_TOOLS:\s*missing/.test(toolSignal)) {
|
|
1577
|
-
publishToolsMissing = true;
|
|
1578
|
-
}
|
|
1579
|
-
log("Publish artifact toolcheck for task " + taskId + " (attempt " + toolcheckAttempt + " of 2): " +
|
|
1580
|
-
(publishToolsOk ? "tools ok" : publishToolsMissing ? "tools missing (explicit parsed signal)" : "inconclusive (no ARTIFACT_TOOLS signal parsed)"));
|
|
1581
|
-
} catch (toolcheckErr) {
|
|
1582
|
-
log("Publish artifact toolcheck for task " + taskId + " (attempt " + toolcheckAttempt + " of 2) threw (" + (toolcheckErr && toolcheckErr.message ? toolcheckErr.message : toolcheckErr) + ") — inconclusive: a throw proves nothing about tool availability, never counted as missing");
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
if (!publishToolsOk && !publishToolsMissing) {
|
|
1586
|
-
await recordPublishLedger({
|
|
1587
|
-
commit: mergeCommitForPublish,
|
|
1588
|
-
attempt: rebuildAttemptKey,
|
|
1589
|
-
agent_id: null,
|
|
1590
|
-
applied_report: null,
|
|
1591
|
-
outcome: "unknown",
|
|
1592
|
-
detail: "artifact toolcheck inconclusive after two attempts (throws or unparseable signals — never an explicit ARTIFACT_TOOLS: missing): tool availability unproven, so the trigger was NOT issued; unknown parks fail closed with no blind retry"
|
|
1593
|
-
}, totalReworkCount);
|
|
1594
|
-
return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact toolcheck was inconclusive after two attempts (no explicit ARTIFACT_TOOLS signal parsed — a throw is transport noise, not evidence). Tool availability is unproven, so no edit was issued and nothing was retried blindly. Human attention needed.");
|
|
1595
|
-
}
|
|
1596
|
-
if (!publishToolsOk) {
|
|
1597
|
-
await recordPublishLedger({
|
|
1598
|
-
commit: mergeCommitForPublish,
|
|
1599
|
-
attempt: rebuildAttemptKey,
|
|
1600
|
-
agent_id: null,
|
|
1601
|
-
applied_report: null,
|
|
1602
|
-
outcome: "rejected",
|
|
1603
|
-
detail: "artifact tool namespace explicitly missing (parsed ARTIFACT_TOOLS: missing signal, one bounded retry spent): the edit provably did not go through — no trigger issued, no blind retry"
|
|
1604
|
-
}, totalReworkCount);
|
|
1605
|
-
return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact tool namespace was explicitly missing (parsed signal — the edit provably did not go through, so no trigger was issued and nothing was retried blindly). Human attention needed.");
|
|
1606
|
-
}
|
|
1607
|
-
// See docs/decisions/publish-path.md#pretrigger-baseline: a tiny schema'd baseline is captured before the trigger.
|
|
1608
|
-
var baselineAgentId = null;
|
|
1609
|
-
var baselineFailed = false;
|
|
1610
|
-
try {
|
|
1611
|
-
var publishBaseline = await agent(
|
|
1612
|
-
ARTIFACT_LOAD_PREAMBLE +
|
|
1613
|
-
"Call artifact_status with slug \"" + PUBLISH_SLUG + "\" once.\n" +
|
|
1614
|
-
"Return JSON { \"build\": <the raw \"build\" value exactly as returned, or null when there is none> } and nothing else.",
|
|
1615
|
-
{ key: attemptKey("publish-artifact-baseline-" + taskId, totalReworkCount), label: "Reading pre-trigger build state",
|
|
1616
|
-
schema: { type: "object", properties: { build: { type: ["object", "null"] } }, required: ["build"] } }
|
|
1617
|
-
);
|
|
1618
|
-
baselineAgentId = (publishBaseline && publishBaseline.build && typeof publishBaseline.build.agent_id === "string" && publishBaseline.build.agent_id) || null;
|
|
1619
|
-
log("Publish pre-trigger baseline for task " + taskId + ": " + (baselineAgentId ? "build " + baselineAgentId + " already in flight (predates the trigger — never attributed to this edit)" : "no build in flight"));
|
|
1620
|
-
} catch (baselineErr) {
|
|
1621
|
-
baselineFailed = true;
|
|
1622
|
-
log("Publish pre-trigger baseline read failed for task " + taskId + " (" + (baselineErr && baselineErr.message ? baselineErr.message : baselineErr) + ") — receipt attribution skipped; durable audit-dir evidence is the only positive signal");
|
|
1623
|
-
}
|
|
1624
|
-
// See docs/decisions/publish-path.md#trigger-await: the artifact_edit call is awaited.
|
|
1625
|
-
var rebuildTrigger = null, triggerThrew = false;
|
|
1626
|
-
try {
|
|
1627
|
-
var triggerText = String(await agent(rebuildPrompt,
|
|
1628
|
-
{ key: rebuildAttemptKey, label: "Triggering artifact rebuild" }) || "");
|
|
1629
|
-
log("Publish rebuild trigger for task " + taskId + " returned (" + triggerText.length + " chars; awaited; scanned only for the explicit refusal signal)");
|
|
1630
|
-
// See docs/decisions/publish-path.md#explicit-refusal-16: the child must explicitly refuse artifact work.
|
|
1631
|
-
var refusalText = extractRefusal(triggerText);
|
|
1632
|
-
if (refusalText) {
|
|
1633
|
-
await recordPublishLedger({
|
|
1634
|
-
commit: mergeCommitForPublish,
|
|
1635
|
-
attempt: rebuildAttemptKey,
|
|
1636
|
-
agent_id: null,
|
|
1637
|
-
applied_report: null,
|
|
1638
|
-
outcome: "rejected",
|
|
1639
|
-
detail: "artifact_edit explicitly refused the edit (parsed ARTIFACT_EDIT_REFUSED signal): " + refusalText + " — conclusive negative: the edit provably did not go through, no observation polling, no blind retry"
|
|
1640
|
-
}, totalReworkCount);
|
|
1641
|
-
return await parkTask("Publish cannot proceed for task " + taskId + ": artifact_edit explicitly refused the edit (" + refusalText + "). This is conclusive (rejected, not unknown): the edit did not go through. Repair or provision the artifact target, then re-run Publish. Human attention needed.");
|
|
1642
|
-
}
|
|
1643
|
-
} catch (triggerErr) {
|
|
1644
|
-
log("Publish rebuild trigger for task " + taskId + " threw (" + (triggerErr && triggerErr.message ? triggerErr.message : triggerErr) + ") — outcome unknown until observation confirms it; the edit may have gone through");
|
|
1645
|
-
triggerThrew = true;
|
|
1646
|
-
}
|
|
1647
|
-
// D1 (2026-09-19; F-A1 upper-bound 2026-09-20): issued_at is an UPPER
|
|
1648
|
-
// bound on issuance — captured AFTER the trigger-agent call returns,
|
|
1649
|
-
// ferried from one shell date call (workflows have no clock) and
|
|
1650
|
-
// validated mechanically; null when unobserved (fallback: ts).
|
|
1651
|
-
// See docs/decisions/publish-path.md#d1-issuance-time.
|
|
1652
|
-
var triggerIssuedAt = null;
|
|
1653
|
-
try {
|
|
1654
|
-
var issuedAtFerry = await agent(
|
|
1655
|
-
"Run: date -u +%Y-%m-%dT%H:%M:%SZ\n" +
|
|
1656
|
-
"Return JSON { \"ts\": \"<the date output, verbatim>\" } and nothing else.",
|
|
1657
|
-
{ key: attemptKey("publish-issued-at-" + taskId, totalReworkCount), label: "Capturing post-trigger instant (upper bound on issuance)",
|
|
1658
|
-
schema: { type: "object", properties: { ts: { type: "string" } }, required: ["ts"] } }
|
|
1659
|
-
);
|
|
1660
|
-
var issuedAtRaw = String((issuedAtFerry && issuedAtFerry.ts) || "").trim();
|
|
1661
|
-
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(issuedAtRaw)) triggerIssuedAt = issuedAtRaw;
|
|
1662
|
-
} catch (issuedAtErr) { /* issued_at stays null: the verifier falls back to ts */ }
|
|
1663
|
-
if (!triggerIssuedAt) log("Publish issuance instant unobserved for task " + taskId + " — issued_at null; the freshness gate falls back to ledger-write ts");
|
|
1664
|
-
// (2026-09-19, room #22) Submitted-on-issuance: see docs/decisions/publish-path.md#submitted-on-issuance.
|
|
1665
|
-
await recordPublishLedger({
|
|
1549
|
+
// is small by construction: the 200-line budget above gates it. The
|
|
1550
|
+
// worker verifies the sha256 from the intent entry before issuing;
|
|
1551
|
+
// a mismatch stops issuance. (2026-09-20, one-party worker-owned
|
|
1552
|
+
// publish: the workflow no longer issues the edit itself.)
|
|
1553
|
+
// (2026-09-20, one-party worker-owned publish) The workflow exits
|
|
1554
|
+
// Publish at intent. Everything above — preflight, provenance base,
|
|
1555
|
+
// checksummed diff — is read-only. (2026-09-20 REVIEW: the
|
|
1556
|
+
// pre-trigger baselines, artifact toolcheck, and pre-trigger build
|
|
1557
|
+
// state agent steps were deleted — their outputs fed only the
|
|
1558
|
+
// retired content-verdict/receipt-attribution machinery, and the
|
|
1559
|
+
// toolcheck ran in a workflow child that cannot reach artifact_edit.
|
|
1560
|
+
// Issuance belongs to the session-carrying tick worker, which
|
|
1561
|
+
// handles tool-unavailability as inconclusive per the tick protocol.)
|
|
1562
|
+
// Publish at intent. Everything above — preflight, provenance base,
|
|
1563
|
+
// checksummed diff, toolcheck, pre-trigger baselines — is read-only.
|
|
1564
|
+
// Issuance belongs to the session-carrying tick worker
|
|
1565
|
+
// (scan-publish-intent's intent bucket), the only caller class that
|
|
1566
|
+
// can reach artifact_edit. This entry is issuer-written BY THE
|
|
1567
|
+
// WORKFLOW and names the intent: outcome "publish-intent", never
|
|
1568
|
+
// "submitted" — the workflow issued nothing. The worker claims it
|
|
1569
|
+
// via the park note below.
|
|
1570
|
+
// (2026-09-20 REVIEW) The intent ledger entry is the anchor the
|
|
1571
|
+
// whole machine depends on. If the ferry drops it, the task would
|
|
1572
|
+
// park with publish-requested and no anchor — unclaimable forever.
|
|
1573
|
+
// Fail loudly instead of parking an orphan intent.
|
|
1574
|
+
var intentAnchored = await recordPublishLedger({
|
|
1666
1575
|
commit: mergeCommitForPublish,
|
|
1667
1576
|
attempt: rebuildAttemptKey,
|
|
1577
|
+
// (0.14.6) Attempt 1, staged here. The version is the attempt's
|
|
1578
|
+
// deterministic receipt string, synthesized into the staged diff;
|
|
1579
|
+
// the ack scan re-derives it from task+commit+attempt before the
|
|
1580
|
+
// tick worker may issue the edit.
|
|
1581
|
+
publish_attempt: 1,
|
|
1582
|
+
version: publishVersion,
|
|
1668
1583
|
agent_id: null,
|
|
1669
|
-
applied_report:
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
// never "did not go through". No blind retry — record the attempt
|
|
1684
|
-
// and park fail-closed; correlate via the ledger, never by
|
|
1685
|
-
// re-issuing.
|
|
1686
|
-
log("Publish rebuild trigger issued for task " + taskId + " — observing build state to attribute the edit");
|
|
1687
|
-
var buildState = null;
|
|
1688
|
-
var buildStateFailed = false;
|
|
1689
|
-
try {
|
|
1690
|
-
buildState = await agent(
|
|
1691
|
-
ARTIFACT_LOAD_PREAMBLE +
|
|
1692
|
-
"Call artifact_status with slug \"" + PUBLISH_SLUG + "\".\n" +
|
|
1693
|
-
"Poll up to 3 times, about 20 seconds apart, until the response shows a build (the \"build\" value is an object, not null). " +
|
|
1694
|
-
"Return the raw \"build\" value verbatim as JSON — the build object exactly as returned, with its agent_id, operation, status, and any other fields untouched. " +
|
|
1695
|
-
"Do not summarize, interpret, or derive booleans from it. " +
|
|
1696
|
-
"If no build appears after 3 polls, return null. " +
|
|
1697
|
-
"Return JSON { \"build\": <the raw build object or null> } and nothing else.",
|
|
1698
|
-
{ key: attemptKey("publish-artifact-buildcheck-" + taskId, totalReworkCount), label: "Reading artifact build state after trigger",
|
|
1699
|
-
schema: { type: "object", properties: { build: { type: ["object", "null"] } }, required: ["build"] } }
|
|
1700
|
-
);
|
|
1701
|
-
} catch (buildCheckErr) {
|
|
1702
|
-
buildStateFailed = true;
|
|
1703
|
-
log("Publish post-trigger build-state check failed for task " + taskId + " (" + (buildCheckErr && buildCheckErr.message ? buildCheckErr.message : buildCheckErr) + ") — this signal is unknown, not negative");
|
|
1704
|
-
}
|
|
1705
|
-
var observedAgentId = (buildState && buildState.build && typeof buildState.build.agent_id === "string" && buildState.build.agent_id) || null;
|
|
1706
|
-
// See docs/decisions/publish-path.md#attribution-limitation: attribution is timing-based; content verification bounds the risk.
|
|
1707
|
-
var receiptAgentId = (!buildStateFailed && !baselineFailed && observedAgentId && observedAgentId !== baselineAgentId) ? observedAgentId : null;
|
|
1708
|
-
// auditReportOk: pure tri-state read of a report.json body —
|
|
1709
|
-
// true (build ok), false (build failed), null (missing or
|
|
1710
|
-
// unreadable — not evidence either way). The child returns the
|
|
1711
|
-
// raw body verbatim; interpretation lives here, never in prose.
|
|
1712
|
-
// Hoisted to Publish-step scope (before the receipt branch) so both
|
|
1713
|
-
// the immediate and post-poll audit fallbacks share it on every path —
|
|
1714
|
-
// the receipt path skips the else below, which must not leave these
|
|
1715
|
-
// undefined.
|
|
1716
|
-
var auditReportOk = function (raw) {
|
|
1717
|
-
if (typeof raw !== "string") return null;
|
|
1718
|
-
var trimmed = raw.trim();
|
|
1719
|
-
if (trimmed === "" || trimmed === "MISSING") return null;
|
|
1720
|
-
var parsed;
|
|
1721
|
-
try { parsed = JSON.parse(trimmed); } catch (e) { return null; }
|
|
1722
|
-
if (parsed && typeof parsed.ok === "boolean") return parsed.ok;
|
|
1723
|
-
return null;
|
|
1724
|
-
};
|
|
1725
|
-
// (2026-09-18, H2 verdict-first) Publish-verdict vocabulary. The
|
|
1726
|
-
// verdict is one of "landed" | "unknown". Decided once,
|
|
1727
|
-
// before the receipt poll, and dispatched on — never re-derived.
|
|
1728
|
-
// Pure and self-contained: unit-tested by
|
|
1729
|
-
// tests/publish-verdict-first.test.js.
|
|
1730
|
-
// See docs/decisions/publish-path.md#h2-verdict-dispatch.
|
|
1731
|
-
var decidePublishVerdict = function (opts) {
|
|
1732
|
-
var immediateReport = (opts && "immediateReport" in opts) ? opts.immediateReport : null;
|
|
1733
|
-
var newDirCount = (opts && typeof opts.newDirCount === "number") ? opts.newDirCount : 0;
|
|
1734
|
-
if (newDirCount === 1 && immediateReport === true) return { verdict: "landed", unattributableReason: null };
|
|
1735
|
-
if (newDirCount === 1 && immediateReport === false) return { verdict: "unknown", unattributableReason: "audit-report-ok-false" };
|
|
1736
|
-
if (newDirCount === 1) return { verdict: "unknown", unattributableReason: "audit-report-unreadable-or-missing" };
|
|
1737
|
-
if (newDirCount === 0) return { verdict: "unknown", unattributableReason: "no-new-audit-dir-in-window" };
|
|
1738
|
-
return { verdict: "unknown", unattributableReason: "audit-dir-ambiguity", ambiguousDirCount: newDirCount };
|
|
1739
|
-
};
|
|
1740
|
-
// (2026-09-18, H2 message contract, design §1.5) The human line is
|
|
1741
|
-
// the output of a mechanical field→template mapping: the trigger
|
|
1742
|
-
// commit short-sha, one plain clause per unattributable reason, the
|
|
1743
|
-
// no-republish warning, the ledger path with (outcome: unknown), and
|
|
1744
|
-
// the unknown-recovery clause. The appendix carries every machine
|
|
1745
|
-
// field. Pure and self-contained: unit-tested by
|
|
1746
|
-
// tests/publish-verdict-first.test.js.
|
|
1747
|
-
var composeUnattributedParkReason = function (fields) {
|
|
1748
|
-
var f = fields || {};
|
|
1749
|
-
var task = f.taskId || taskId;
|
|
1750
|
-
var reason = f.unattributableReason || "unknown-outcome";
|
|
1751
|
-
var shortSha = String(f.commitShortSha || "").substring(0, 7) || "unknown";
|
|
1752
|
-
var reasonClauses = {
|
|
1753
|
-
"no-new-audit-dir-in-window": "no build could be tied to this attempt",
|
|
1754
|
-
"audit-report-unreadable-or-missing": "the build's report is unreadable",
|
|
1755
|
-
"audit-dir-ambiguity": "more than one build appeared in the check window",
|
|
1756
|
-
"stranger-build-observed-during-poll": "a different build was running during the check",
|
|
1757
|
-
"status-read-errors-during-poll": "status reads kept failing"
|
|
1758
|
-
};
|
|
1759
|
-
var clause = reasonClauses[reason] || "no build could be tied to this attempt";
|
|
1760
|
-
return "Publish outcome unknown for task " + task +
|
|
1761
|
-
": the trigger was sent for commit " + shortSha + " but the outcome could not be confirmed — " + clause + ". " +
|
|
1762
|
-
"Do NOT republish: if the trigger was accepted, a retry duplicates the build (2026-09-12). " +
|
|
1763
|
-
"The attempt is in the ledger at " + crewHome + "/.publish-ledger/" + PUBLISH_SLUG + ".jsonl (outcome: unknown) " +
|
|
1764
|
-
"and the crew's unknown-recovery will re-examine it. " +
|
|
1765
|
-
"Appendix: unattributable_reason=" + reason +
|
|
1766
|
-
"; poll_end_state=" + (f.pollEndState || "not-polled") +
|
|
1767
|
-
"; saw_our_build=" + (f.sawOurBuild ? "true" : "false") +
|
|
1768
|
-
"; new_audit_dirs=" + (f.newAuditDirCount == null ? 0 : f.newAuditDirCount) +
|
|
1769
|
-
"; poll_chunks_failed=" + (f.chunkFailures == null ? 0 : f.chunkFailures) +
|
|
1770
|
-
"; poll_status_errors=" + (f.pollStatusErrors == null ? 0 : f.pollStatusErrors) + ".";
|
|
1771
|
-
};
|
|
1772
|
-
// publishFailure is declared here (per-Publish-pass scope) so the
|
|
1773
|
-
// post-poll explicit build failure survives to the final routing
|
|
1774
|
-
// below; publishUnknownFields carries the structured unknown fields
|
|
1775
|
-
// for the single composed fail-closed park. Both re-initialize on
|
|
1776
|
-
// every pass — a rework re-entry never leaks a stale verdict.
|
|
1777
|
-
var publishFailure = null;
|
|
1778
|
-
var publishUnknownFields = null;
|
|
1779
|
-
if (receiptAgentId) {
|
|
1780
|
-
// The edit went through — a build with a new agent_id appeared
|
|
1781
|
-
// after the trigger. The parent's independent read-back
|
|
1782
|
-
// (docs/publish-verification.md) is the verification, not any
|
|
1783
|
-
// builder report.
|
|
1784
|
-
rebuildTrigger = { edit_started: true };
|
|
1785
|
-
rebuildAgentId = receiptAgentId;
|
|
1786
|
-
log("Publish rebuild trigger for task " + taskId + ": artifact_status shows build " + receiptAgentId + " for slug " + PUBLISH_SLUG + " (new relative to the pre-trigger baseline) — the edit went through.");
|
|
1787
|
-
await recordPublishLedger({
|
|
1788
|
-
commit: mergeCommitForPublish,
|
|
1789
|
-
attempt: rebuildAttemptKey,
|
|
1790
|
-
agent_id: rebuildAgentId,
|
|
1791
|
-
applied_report: publishAppliedObservation,
|
|
1792
|
-
manifest_before: preTriggerManifest,
|
|
1793
|
-
outcome: "submitted",
|
|
1794
|
-
issued_at: triggerIssuedAt,
|
|
1795
|
-
detail: "fire-and-forget trigger; build receipt captured by workflow-owned build-state observation (pre/post-trigger diff); observation, not re-issuance"
|
|
1796
|
-
}, totalReworkCount, "receipt");
|
|
1797
|
-
} else {
|
|
1798
|
-
var newAuditDirs = [];
|
|
1799
|
-
try {
|
|
1800
|
-
var auditAfter = await agent(
|
|
1801
|
-
"List the artifact audit directories for slug \"" + PUBLISH_SLUG + "\" (best-effort, never a gate).\n" +
|
|
1802
|
-
"Run: ls -1 ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/ 2>/dev/null\n" +
|
|
1803
|
-
"Return JSON { \"dirs\": \"<newline-separated names, empty string when the audits directory does not exist or is empty>\" } and nothing else.",
|
|
1804
|
-
{ key: attemptKey("publish-audit-after-" + taskId, totalReworkCount), label: "Re-listing audit dirs after trigger",
|
|
1805
|
-
schema: { type: "object", properties: { dirs: { type: "string" } }, required: ["dirs"] } }
|
|
1806
|
-
);
|
|
1807
|
-
var auditDirsAfterTrigger = String((auditAfter && auditAfter.dirs) || "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
|
|
1808
|
-
// Only timestamped build dirs count — the "latest" symlink
|
|
1809
|
-
// and anything else are not builds. Gated on auditBeforeOk:
|
|
1810
|
-
// without a baseline every historical dir would look new.
|
|
1811
|
-
newAuditDirs = auditBeforeOk ? auditDirsAfterTrigger.filter(function (d) {
|
|
1812
|
-
return auditDirsBeforeTrigger.indexOf(d) === -1 && /^20\d\d-\d\d-\d\dT\d\d-\d\d-\d\dZ-/.test(d);
|
|
1813
|
-
}) : [];
|
|
1814
|
-
} catch (auditAfterErr) {
|
|
1815
|
-
log("Publish audit-dir re-list after trigger failed for task " + taskId + " (non-fatal, durable-evidence check degraded): " + (auditAfterErr && auditAfterErr.message ? auditAfterErr.message : auditAfterErr));
|
|
1816
|
-
}
|
|
1817
|
-
if (newAuditDirs.length > 0) {
|
|
1818
|
-
rebuildAgentId = null;
|
|
1819
|
-
newAuditDirs.sort();
|
|
1820
|
-
var newestImmediateDir = newAuditDirs[newAuditDirs.length - 1];
|
|
1821
|
-
log("Publish rebuild trigger for task " + taskId + ": new audit dir(s) during the trigger window (" + newAuditDirs.join(", ") + ") — the edit went through and a build completed; no in-flight receipt was observed.");
|
|
1822
|
-
// (2026-09-18, H2 verdict-first) Durable audit evidence exists,
|
|
1823
|
-
// but there is no receipt agent_id to chain the completion poll
|
|
1824
|
-
// to — polling with a null receipt can only observe strangers
|
|
1825
|
-
// (any running build differs from "null") or nothing, burning
|
|
1826
|
-
// 10.5 minutes to park unknown. Read the build report now instead
|
|
1827
|
-
// of polling, then decide the verdict ONCE via
|
|
1828
|
-
// decidePublishVerdict: exactly one new dir with ok=true lands
|
|
1829
|
-
// (attribution by window, not identity — never poll blind);
|
|
1830
|
-
// ok=false is UNKNOWN with the failure evidence preserved in the
|
|
1831
|
-
// ledger detail (the evidence is explicit, the attribution is
|
|
1832
|
-
// not); unreadable / zero / ambiguous dirs are UNKNOWN.
|
|
1833
|
-
// Verdict-first: landed bypasses the poll below, unknown falls
|
|
1834
|
-
// through to post-deploy. STEP 2 runs on every path.
|
|
1835
|
-
// See docs/decisions/publish-path.md#h2-verdict-dispatch.
|
|
1836
|
-
var immediateReportOk = null;
|
|
1837
|
-
// (N1) Read the report only when exactly one new dir exists:
|
|
1838
|
-
// ambiguity (>1) forces UNKNOWN regardless — don't shell out to
|
|
1839
|
-
// read a report that will be discarded.
|
|
1840
|
-
if (newAuditDirs.length === 1) {
|
|
1841
|
-
try {
|
|
1842
|
-
var immediateOkRead = await agent(
|
|
1843
|
-
"Read the artifact build report for slug \"" + PUBLISH_SLUG + "\".\n" +
|
|
1844
|
-
"Run: cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/" + newestImmediateDir + "/report.json 2>/dev/null || echo MISSING\n" +
|
|
1845
|
-
"Return JSON { \"raw\": \"<verbatim file contents, or the literal string MISSING when the file does not exist>\" } and nothing else.",
|
|
1846
|
-
{ key: attemptKey("publish-audit-ok-immediate-" + taskId, totalReworkCount), label: "Reading build report for audit-confirmed build",
|
|
1847
|
-
schema: { type: "object", properties: { raw: { type: "string" } }, required: ["raw"] } }
|
|
1848
|
-
);
|
|
1849
|
-
immediateReportOk = auditReportOk(immediateOkRead && immediateOkRead.raw);
|
|
1850
|
-
} catch (immediateOkErr) {
|
|
1851
|
-
log("Publish build-report read for audit-confirmed dir failed for task " + taskId + " (treated as unknown): " + (immediateOkErr && immediateOkErr.message ? immediateOkErr.message : immediateOkErr));
|
|
1852
|
-
immediateReportOk = null;
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
var immediateVerdict = decidePublishVerdict({ editStarted: true, immediateReport: immediateReportOk, newDirCount: newAuditDirs.length });
|
|
1856
|
-
if (immediateVerdict.verdict === "landed") {
|
|
1857
|
-
publishBuildLanded = true;
|
|
1858
|
-
artifactPublish = { source_commit: mergeCommitForPublish, pending_parent_verification: true };
|
|
1859
|
-
await recordPublishLedger({
|
|
1860
|
-
commit: mergeCommitForPublish,
|
|
1861
|
-
attempt: rebuildAttemptKey,
|
|
1862
|
-
agent_id: null,
|
|
1863
|
-
applied_report: publishAppliedObservation,
|
|
1864
|
-
manifest_before: preTriggerManifest,
|
|
1865
|
-
outcome: "build-observed",
|
|
1866
|
-
detail: "durable audit evidence shows a build completed during the attempt window (no receipt agent_id — attribution by window, not identity; receipt poll bypassed (verdict decided), routed to parent verification)"
|
|
1867
|
-
}, totalReworkCount);
|
|
1868
|
-
log("Publish verdict LANDED for task " + taskId + ": a completed build was observed during the attempt window — receipt poll bypassed (verdict decided, no receipt to chain to), routing directly to parent verification.");
|
|
1869
|
-
} else {
|
|
1870
|
-
// Verdict UNKNOWN on the immediate path. ok=false is explicit
|
|
1871
|
-
// failure evidence but not an attributable failure — the ledger
|
|
1872
|
-
// records unknown with the evidence preserved in the detail,
|
|
1873
|
-
// and the flow continues to post-deploy (never parks early).
|
|
1874
|
-
publishUnknownFields = {
|
|
1875
|
-
taskId: taskId,
|
|
1876
|
-
unattributableReason: immediateVerdict.unattributableReason,
|
|
1877
|
-
pollEndState: "not-polled",
|
|
1878
|
-
sawOurBuild: false,
|
|
1879
|
-
newAuditDirCount: newAuditDirs.length,
|
|
1880
|
-
chunkFailures: 0,
|
|
1881
|
-
pollStatusErrors: 0
|
|
1882
|
-
};
|
|
1883
|
-
var immediateDetail = "durable audit-dir fallback could not prove a completed build for this attempt (unattributable_reason=" + immediateVerdict.unattributableReason + ", no receipt agent_id)";
|
|
1884
|
-
if (immediateReportOk === false) {
|
|
1885
|
-
immediateDetail += "; explicit failure evidence preserved: report ok=false for audit dir " + newestImmediateDir;
|
|
1886
|
-
}
|
|
1887
|
-
if (immediateVerdict.unattributableReason === "audit-dir-ambiguity") {
|
|
1888
|
-
immediateDetail += "; audit-dir ambiguity: " + newAuditDirs.length + " new dirs in window";
|
|
1889
|
-
}
|
|
1890
|
-
await recordPublishLedger({
|
|
1891
|
-
commit: mergeCommitForPublish,
|
|
1892
|
-
attempt: rebuildAttemptKey,
|
|
1893
|
-
agent_id: null,
|
|
1894
|
-
applied_report: publishAppliedObservation,
|
|
1895
|
-
outcome: "unknown",
|
|
1896
|
-
detail: immediateDetail
|
|
1897
|
-
}, totalReworkCount);
|
|
1898
|
-
log("Publish verdict UNKNOWN for task " + taskId + ": " + immediateDetail + " — continuing to post-deploy; never polling blind and never parking early.");
|
|
1899
|
-
}
|
|
1900
|
-
} else {
|
|
1901
|
-
// See docs/decisions/publish-path.md#no-attributable-build: no attributable build and no durable evidence means no publish.
|
|
1902
|
-
publishUnknownFields = {
|
|
1903
|
-
taskId: taskId,
|
|
1904
|
-
unattributableReason: decidePublishVerdict({ editStarted: true, immediateReport: null, newDirCount: 0 }).unattributableReason,
|
|
1905
|
-
pollEndState: "not-polled",
|
|
1906
|
-
sawOurBuild: false,
|
|
1907
|
-
newAuditDirCount: 0,
|
|
1908
|
-
chunkFailures: 0,
|
|
1909
|
-
pollStatusErrors: 0
|
|
1910
|
-
};
|
|
1911
|
-
await recordPublishLedger({
|
|
1912
|
-
commit: mergeCommitForPublish,
|
|
1913
|
-
attempt: rebuildAttemptKey,
|
|
1914
|
-
agent_id: null,
|
|
1915
|
-
applied_report: publishAppliedObservation,
|
|
1916
|
-
outcome: "unknown",
|
|
1917
|
-
detail: "no new audit dir appeared in the trigger window and no receipt agent_id was observed — the edit was issued fire-and-forget, so completion is unproven; never poll blind on a null receipt"
|
|
1918
|
-
}, totalReworkCount);
|
|
1919
|
-
log("Publish verdict UNKNOWN for task " + taskId + ": no new audit dir in window and no receipt — continuing to post-deploy; never polling blind and never parking early.");
|
|
1920
|
-
}
|
|
1584
|
+
applied_report: null,
|
|
1585
|
+
outcome: "publish-intent",
|
|
1586
|
+
issuer: "workflow",
|
|
1587
|
+
diff_path: publishDiffFile,
|
|
1588
|
+
diff_sha256: publishDiffSha256,
|
|
1589
|
+
// (2026-09-20, critic-0145 F-A4) The base the diff was computed
|
|
1590
|
+
// against: scan-publish-intent's intent branch compares it with
|
|
1591
|
+
// current stamped provenance and re-queues the task when it went
|
|
1592
|
+
// stale — never issues the stale diff.
|
|
1593
|
+
base: publishBase,
|
|
1594
|
+
detail: "publish intent: preflight and checksummed diff are complete; issuance is owned by the session-carrying tick worker. The workflow performed no artifact_edit and wrote no issuance."
|
|
1595
|
+
}, totalReworkCount);
|
|
1596
|
+
if (!intentAnchored) {
|
|
1597
|
+
throw new Error("publish-intent ledger anchor write unconfirmed for task " + taskId + " — refusing to park an unclaimable intent; the Publish phase fails loudly instead");
|
|
1921
1598
|
}
|
|
1922
|
-
|
|
1923
|
-
// attempt key and commit are in scope. Every attempt lands here with
|
|
1924
|
-
// its outcome — submitted, rejected, or unknown (unknown is recorded
|
|
1925
|
-
// at the park site above). A later run or human matches commit hash +
|
|
1926
|
-
// attempt key against the builder's eventual completion.
|
|
1927
|
-
// (2026-09-18, H2 verdict-first) The verdict was decided exactly
|
|
1928
|
-
// once above; dispatch on it. landed bypasses the receipt poll
|
|
1929
|
-
// (the audit evidence already proved completion); an explicit
|
|
1930
|
-
// publishFailure is preserved verbatim through post-deploy.
|
|
1931
|
-
// Otherwise the verdict is open — but the poll below is only
|
|
1932
|
-
// legitimate against a real receipt: the null-safe assertion records
|
|
1933
|
-
// UNKNOWN and continues to STEP 2 instead of polling blind.
|
|
1934
|
-
// See docs/decisions/publish-path.md#h2-verdict-dispatch.
|
|
1935
|
-
if (publishBuildLanded) {
|
|
1936
|
-
log("Publish verdict already LANDED for task " + taskId + " — bypassing receipt poll.");
|
|
1937
|
-
} else if (publishFailure) {
|
|
1938
|
-
// design §1.2 pre-poll branch — currently unassigned; kept for the converged dispatch shape.
|
|
1939
|
-
log("Publish verdict already FAILED for task " + taskId + " — preserved verbatim through post-deploy.");
|
|
1940
|
-
} else if (!rebuildTrigger || !rebuildTrigger.edit_started) {
|
|
1941
|
-
// Loud defensive assertion (replaces the old lying "Unreachable"
|
|
1942
|
-
// else): with no receipt state the poll would observe strangers or
|
|
1943
|
-
// nothing — record UNKNOWN and continue to STEP 2. Never park
|
|
1944
|
-
// early here; never poll blind.
|
|
1945
|
-
if (!publishUnknownFields) {
|
|
1946
|
-
publishUnknownFields = {
|
|
1947
|
-
taskId: taskId,
|
|
1948
|
-
unattributableReason: "no-receipt-state",
|
|
1949
|
-
pollEndState: "not-polled",
|
|
1950
|
-
sawOurBuild: false,
|
|
1951
|
-
newAuditDirCount: 0,
|
|
1952
|
-
chunkFailures: 0,
|
|
1953
|
-
pollStatusErrors: 0
|
|
1954
|
-
};
|
|
1955
|
-
await recordPublishLedger({
|
|
1956
|
-
commit: mergeCommitForPublish,
|
|
1957
|
-
attempt: rebuildAttemptKey,
|
|
1958
|
-
agent_id: null,
|
|
1959
|
-
applied_report: publishAppliedObservation,
|
|
1960
|
-
outcome: "unknown",
|
|
1961
|
-
detail: "no receipt state was recorded for this attempt — never polling blind; continuing to post-deploy"
|
|
1962
|
-
}, totalReworkCount);
|
|
1963
|
-
}
|
|
1964
|
-
log("Publish verdict UNKNOWN for task " + taskId + ": no receipt state recorded — never polling blind, continuing to post-deploy.");
|
|
1965
|
-
} else {
|
|
1966
|
-
// (2026-09-16) There is no builder report: the fire-and-forget
|
|
1967
|
-
// trigger carries no JSON contract, so there is nothing to
|
|
1968
|
-
// compare and no pre-hash diagnostic. The builder's old
|
|
1969
|
-
// self-report was circular by construction (canary run 8) with a
|
|
1970
|
-
// demonstrated false-negative mode (task 23ca8f3f, 2026-09-12:
|
|
1971
|
-
// applied:[] for a diff the builder had applied). The flow
|
|
1972
|
-
// proceeds to the build poll regardless; real verification is the
|
|
1973
|
-
// parent's independent read-back (docs/publish-verification.md)
|
|
1974
|
-
// before the provenance stamp.
|
|
1975
|
-
// STEP 1b (mechanical): bounded poll for build completion, chunked so
|
|
1976
|
-
// the merge-lock lease is refreshed before it can expire. The 600s
|
|
1977
|
-
// lease is shorter than the worst-case 10-minute build poll, so the
|
|
1978
|
-
// poll runs in three chunks (7 checks x 30s ~= 3.5 min each) with a
|
|
1979
|
-
// holder-only lease refresh between chunks. If a refresh fails, the
|
|
1980
|
-
// lock was lost: stop the run and park the task — never continue to
|
|
1981
|
-
// a provenance stamp or version assignment without holding the lock.
|
|
1982
|
-
var buildPoll = null;
|
|
1983
|
-
// STEP 1b poll-signal accumulators (2026-09-15, task aadeccc3):
|
|
1984
|
-
// the durable audit-dir fallback below needs the poll's own
|
|
1985
|
-
// observations, not just its final verdict — whether our build was
|
|
1986
|
-
// ever seen, whether a stranger's build was ever in flight, and
|
|
1987
|
-
// what the last check observed. OR-ed across all three chunks so
|
|
1988
|
-
// a signal seen in any chunk survives the chunk boundary.
|
|
1989
|
-
var pollSawOurBuild = false;
|
|
1990
|
-
var pollSawStranger = false;
|
|
1991
|
-
var lastObservedAgentId = null;
|
|
1992
|
-
// Poll-chunk failure record (2026-09-16, task 1febe8eb): chunk
|
|
1993
|
-
// agent calls that threw instead of returning a verdict —
|
|
1994
|
-
// recorded for the post-poll diagnosis, never terminal alone.
|
|
1995
|
-
var chunkFailures = [];
|
|
1996
|
-
// Poll status-read error count (2026-09-16): a failed
|
|
1997
|
-
// artifact_status read (rate limiting / TOO_MANY_REQUESTS /
|
|
1998
|
-
// 429, network error, or any non-build response) is
|
|
1999
|
-
// inconclusive — it is NOT evidence of absence and must never
|
|
2000
|
-
// be folded into the no-build-observed signal. Counted for
|
|
2001
|
-
// the post-poll diagnosis only; never terminal alone.
|
|
2002
|
-
var pollStatusErrors = 0;
|
|
2003
|
-
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
2004
|
-
if (chunk > 1) {
|
|
2005
|
-
var refreshPoll = await agent(
|
|
2006
|
-
"Refresh the merge lock for task " + taskId + ".\n" +
|
|
2007
|
-
"Run: " + LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
|
|
2008
|
-
"If the output contains REFRESHED, return JSON { \"held\": true } and nothing else. Otherwise return JSON { \"held\": false, \"output\": \"<verbatim output>\" } and nothing else.",
|
|
2009
|
-
{ key: attemptKey("publish-lock-refresh-poll-" + taskId + "-c" + chunk, totalReworkCount), label: "Refreshing merge lock during build poll",
|
|
2010
|
-
schema: { type: "object", properties: { held: { type: "boolean" }, output: { type: "string" } }, required: ["held"] },
|
|
2011
|
-
timeoutMs: 60000 }
|
|
2012
|
-
);
|
|
2013
|
-
if (!refreshPoll || refreshPoll.held !== true) {
|
|
2014
|
-
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.");
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
// Chunk 1 keeps the original stable key; later chunks use -c<N>
|
|
2018
|
-
// suffixed keys. All are attemptKey-scoped so rework passes stay
|
|
2019
|
-
// disjoint (replay-key contract).
|
|
2020
|
-
var pollKey = (chunk === 1)
|
|
2021
|
-
? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
|
|
2022
|
-
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
|
|
2023
|
-
try {
|
|
2024
|
-
buildPoll = await agent(
|
|
2025
|
-
"First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" \u2014 for OUR build only, the one whose agent_id is \"" + rebuildAgentId + "\" (the receipt captured when the edit was accepted; the agent_id is the artifact system's in-flight build correlation ID, stable across polls while the build runs). Check every 30 seconds, up to 7 checks (3.5 minutes max). On each check, read the raw build object:\n" +
|
|
2026
|
-
"If an artifact_status call itself FAILS (rate limiting / TOO_MANY_REQUESTS / 429, network error, or any response that is not a build object): that check is inconclusive \u2014 do NOT record it as \"no build\". Stop polling immediately and return with status_error: true. A failed read is not evidence of absence and must never be folded into the no-build-observed signal.\n" +
|
|
2027
|
-
"On every check, record whether you have positively OBSERVED our build: a running build whose agent_id equals \"" + rebuildAgentId + "\", or a completed-build record whose agent_id equals \"" + rebuildAgentId + "\" (if the tool surfaces one \u2014 match it mechanically, never assume).\n" +
|
|
2028
|
-
"- If no build is running (build is null) and you have NOT observed our build: our build's completion is UNPROVEN. Absence of a running build is not evidence our build ran. Do NOT report done.\n" +
|
|
2029
|
-
"- If no build is running (build is null) and you previously observed our build running: our build finished. Stop and report done.\n" +
|
|
2030
|
-
"- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
|
|
2031
|
-
"- If the running build's agent_id is present but DIFFERENT: that is a stranger's build. Do NOT attribute its completion to our attempt and do NOT wait on it \u2014 keep checking within budget; if the budget expires without observing our build, report done=false. Record it in saw_stranger regardless of what else you observe.\n" +
|
|
2032
|
-
"Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\", \"status_error\": <true ONLY when a status read failed as described above, false or omitted otherwise> } and nothing else.",
|
|
2033
|
-
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
2034
|
-
schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] }, status_error: { type: "boolean" } }, required: ["build_done"] },
|
|
2035
|
-
timeoutMs: 270000 }
|
|
2036
|
-
);
|
|
2037
|
-
} catch (chunkErr) {
|
|
2038
|
-
// See docs/decisions/publish-path.md#hung-chunk: a hung or failed chunk is inconclusive, never terminal.
|
|
2039
|
-
chunkFailures.push("chunk " + chunk + ": " + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr));
|
|
2040
|
-
log("Artifact build poll chunk " + chunk + " of 3 failed (" + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr) + ") \u2014 continuing to the next chunk; build completion still unproven.");
|
|
2041
|
-
}
|
|
2042
|
-
if (buildPoll) {
|
|
2043
|
-
pollSawOurBuild = pollSawOurBuild || (buildPoll.saw_our_build === true);
|
|
2044
|
-
pollSawStranger = pollSawStranger || (buildPoll.saw_stranger === true);
|
|
2045
|
-
lastObservedAgentId = buildPoll.observed_agent_id || null;
|
|
2046
|
-
if (buildPoll.status_error === true) { pollStatusErrors++; log("Artifact build poll chunk " + chunk + " of 3 hit a status-read error \u2014 recorded as inconclusive, continuing to the next chunk; a failed read is not evidence of absence."); }
|
|
2047
|
-
if (buildPoll.build_done) { break; }
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
if (!buildPoll || !buildPoll.build_done) {
|
|
2051
|
-
buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
|
|
2052
|
-
}
|
|
2053
|
-
if (buildPoll.build_done && pollSawOurBuild) {
|
|
2054
|
-
// See docs/decisions/publish-path.md#step-1c-no-stamp: no provenance stamp in STEP 1c.
|
|
2055
|
-
publishBuildLanded = true;
|
|
2056
|
-
artifactPublish = { source_commit: mergeCommitForPublish, pending_parent_verification: true };
|
|
2057
|
-
log("Publish build landed for task " + taskId + " — provenance stamp deferred to parent content verification");
|
|
2058
|
-
} else {
|
|
2059
|
-
// STEP 1b durable audit-dir fallback (2026-09-15, task aadeccc3):
|
|
2060
|
-
// the poll observes only in-flight builds; a build that finished
|
|
2061
|
-
// inside the window leaves a durable audit dir. Diff the audit
|
|
2062
|
-
// listing against the pre-trigger snapshot: a new timestamped dir
|
|
2063
|
-
// is evidence a build completed. Attribution is by window, not by build identity: any
|
|
2064
|
-
// observed stranger blocks it. Never re-issues, never stamps;
|
|
2065
|
-
// ok=true routes to the parent's read-back (the verification).
|
|
2066
|
-
//
|
|
2067
|
-
// The poll end-state is read from the poll's own observations,
|
|
2068
|
-
// not from build_done alone: a build in flight at the last check
|
|
2069
|
-
// means the budget was shorter than the latency (or the build is
|
|
2070
|
-
// stuck) — NOT that no build ever started; nothing observed at
|
|
2071
|
-
// any check is the never-started signal.
|
|
2072
|
-
var pollEndState = lastObservedAgentId ? "build-still-running-at-poll-end"
|
|
2073
|
-
: (pollSawOurBuild ? "our-build-observed-then-unconfirmed" : "no-build-observed-in-window");
|
|
2074
|
-
var strangerObserved = pollSawStranger;
|
|
2075
|
-
var newAuditDirsAfterPoll = [];
|
|
2076
|
-
try {
|
|
2077
|
-
var auditAfterPoll = await agent(
|
|
2078
|
-
"List the artifact audit directories for slug \"" + PUBLISH_SLUG + "\" (best-effort, never a gate).\n" +
|
|
2079
|
-
"Run: ls -1 ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/ 2>/dev/null\n" +
|
|
2080
|
-
"Return JSON { \"dirs\": \"<newline-separated names, empty string when the audits directory does not exist or is empty>\" } and nothing else.",
|
|
2081
|
-
{ key: attemptKey("publish-audit-after-poll-" + taskId, totalReworkCount), label: "Re-listing audit dirs after build poll",
|
|
2082
|
-
schema: { type: "object", properties: { dirs: { type: "string" } }, required: ["dirs"] } }
|
|
2083
|
-
);
|
|
2084
|
-
var auditDirsAfterPollList = String((auditAfterPoll && auditAfterPoll.dirs) || "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
|
|
2085
|
-
// Gated on auditBeforeOk (critic finding 4): without a baseline
|
|
2086
|
-
// every historical dir would look new.
|
|
2087
|
-
newAuditDirsAfterPoll = auditBeforeOk ? auditDirsAfterPollList.filter(function (d) {
|
|
2088
|
-
return auditDirsBeforeTrigger.indexOf(d) === -1 && /^20\d\d-\d\d-\d\dT\d\d-\d\d-\d\dZ-/.test(d);
|
|
2089
|
-
}) : [];
|
|
2090
|
-
log("Publish audit-dir re-list after build poll for task " + taskId + ": " + newAuditDirsAfterPoll.length + " new timestamped dir(s)");
|
|
2091
|
-
} catch (auditAfterPollErr) {
|
|
2092
|
-
log("Publish audit-dir re-list after build poll failed for task " + taskId + " (non-fatal, durable-evidence check degraded): " + (auditAfterPollErr && auditAfterPollErr.message ? auditAfterPollErr.message : auditAfterPollErr));
|
|
2093
|
-
}
|
|
2094
|
-
// The shared auditReportOk (defined with the immediate fallback
|
|
2095
|
-
// above) interprets the raw body here too.
|
|
2096
|
-
var auditOkAfterPoll = null;
|
|
2097
|
-
var newestAuditDirAfterPoll = null;
|
|
2098
|
-
if (newAuditDirsAfterPoll.length > 0 && !strangerObserved) {
|
|
2099
|
-
newAuditDirsAfterPoll.sort();
|
|
2100
|
-
newestAuditDirAfterPoll = newAuditDirsAfterPoll[newAuditDirsAfterPoll.length - 1];
|
|
2101
|
-
try {
|
|
2102
|
-
var auditOkRead = await agent(
|
|
2103
|
-
"Read the build report for artifact slug \"" + PUBLISH_SLUG + "\", audit dir \"" + newestAuditDirAfterPoll + "\" (verbatim read, never interpreted, never a gate).\n" +
|
|
2104
|
-
"Run: cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/" + newestAuditDirAfterPoll + "/report.json 2>/dev/null || echo MISSING\n" +
|
|
2105
|
-
"Return JSON { \"raw\": \"<verbatim file contents, or the literal string MISSING when the file does not exist>\" } and nothing else.",
|
|
2106
|
-
{ key: attemptKey("publish-audit-ok-after-poll-" + taskId, totalReworkCount), label: "Reading build report after build poll",
|
|
2107
|
-
schema: { type: "object", properties: { raw: { type: "string" } }, required: ["raw"] } }
|
|
2108
|
-
);
|
|
2109
|
-
auditOkAfterPoll = auditReportOk(auditOkRead && auditOkRead.raw);
|
|
2110
|
-
} catch (auditOkReadErr) {
|
|
2111
|
-
log("Publish build-report read after build poll failed for task " + taskId + " (non-fatal, treated as unknown): " + (auditOkReadErr && auditOkReadErr.message ? auditOkReadErr.message : auditOkReadErr));
|
|
2112
|
-
auditOkAfterPoll = null;
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
if (auditOkAfterPoll === true) {
|
|
2116
|
-
publishBuildLanded = true;
|
|
2117
|
-
artifactPublish = { source_commit: mergeCommitForPublish, pending_parent_verification: true };
|
|
2118
|
-
log("Publish build landed for task " + taskId + " via durable audit evidence — provenance stamp deferred to parent content verification");
|
|
2119
|
-
await recordPublishLedger({
|
|
2120
|
-
commit: mergeCommitForPublish,
|
|
2121
|
-
attempt: rebuildAttemptKey,
|
|
2122
|
-
agent_id: rebuildAgentId,
|
|
2123
|
-
applied_report: publishAppliedObservation,
|
|
2124
|
-
manifest_before: preTriggerManifest,
|
|
2125
|
-
outcome: "build-observed",
|
|
2126
|
-
detail: "durable audit evidence shows a build completed during the attempt window (audit dir " + newestAuditDirAfterPoll + ", report ok=true); routed to parent verification"
|
|
2127
|
-
}, totalReworkCount);
|
|
2128
|
-
} else if (auditOkAfterPoll === false) {
|
|
2129
|
-
// Explicit negative evidence under the stranger guard: a build
|
|
2130
|
-
// ran and failed during the poll window with no stranger in
|
|
2131
|
-
// flight. This is an attributable failure — preserved verbatim
|
|
2132
|
-
// through post-deploy. Message contract unchanged.
|
|
2133
|
-
publishFailure = "Artifact build FAILED for slug " + PUBLISH_SLUG + " (audit dir " + newestAuditDirAfterPoll + ", report ok=false). Explicit negative evidence: a build ran and failed (attribution by window, not by build identity — no stranger build was observed in flight during the poll). The publish did not land — provenance was not stamped — your change was NOT published. Build log: ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/" + newestAuditDirAfterPoll + "/. This explicit failure is not auto-retried. Appendix: attribution=window; report=ok=false; provenance=unstamped.";
|
|
2134
|
-
await recordPublishLedger({
|
|
2135
|
-
commit: mergeCommitForPublish,
|
|
2136
|
-
attempt: rebuildAttemptKey,
|
|
2137
|
-
agent_id: rebuildAgentId,
|
|
2138
|
-
applied_report: publishAppliedObservation,
|
|
2139
|
-
outcome: "failed",
|
|
2140
|
-
detail: "a build ran and failed (attribution by window, not by build identity): audit dir " + newestAuditDirAfterPoll + " report ok=false; no stranger build observed in flight during the poll"
|
|
2141
|
-
}, totalReworkCount);
|
|
2142
|
-
} else {
|
|
2143
|
-
var unattributableReason = strangerObserved ? "stranger-build-observed-during-poll"
|
|
2144
|
-
: ((pollStatusErrors > 0 && !pollSawOurBuild) ? "status-read-errors-during-poll"
|
|
2145
|
-
: (pollEndState === "build-still-running-at-poll-end" ? "build-still-running-at-poll-end"
|
|
2146
|
-
: (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing")));
|
|
2147
|
-
// Verdict UNKNOWN (poll path): the build cannot be attributed
|
|
2148
|
-
// to this attempt. The fields feed the single composed
|
|
2149
|
-
// fail-closed park reason after post-deploy — never a
|
|
2150
|
-
// fabricated verdict, never a silent pass.
|
|
2151
|
-
publishUnknownFields = {
|
|
2152
|
-
taskId: taskId,
|
|
2153
|
-
unattributableReason: unattributableReason,
|
|
2154
|
-
pollEndState: pollEndState,
|
|
2155
|
-
sawOurBuild: pollSawOurBuild,
|
|
2156
|
-
newAuditDirCount: newAuditDirsAfterPoll.length,
|
|
2157
|
-
chunkFailures: chunkFailures.length,
|
|
2158
|
-
pollStatusErrors: pollStatusErrors
|
|
2159
|
-
};
|
|
2160
|
-
await recordPublishLedger({
|
|
2161
|
-
commit: mergeCommitForPublish,
|
|
2162
|
-
attempt: rebuildAttemptKey,
|
|
2163
|
-
agent_id: rebuildAgentId,
|
|
2164
|
-
applied_report: publishAppliedObservation,
|
|
2165
|
-
outcome: "unknown",
|
|
2166
|
-
detail: "durable audit-dir fallback could not attribute a completed build to this attempt (unattributable_reason=" + unattributableReason + ", poll_end_state=" + pollEndState + ")"
|
|
2167
|
-
}, totalReworkCount);
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
|
|
1599
|
+
return await parkTask("publish: publish-requested " + mergeCommitForPublish + " attempt=1 — Publish intent recorded for task " + taskId + ": the merged change is staged as a checksummed diff (" + publishDiffFile + ", sha256 " + publishDiffSha256 + ") carrying the per-attempt publish version for the crew's publish worker to issue. The worker issues the edit directly and the acknowledgement scan owns the task from there; no action needed.");
|
|
2172
1600
|
} // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
|
|
2173
1601
|
// STEP 2 (mechanical, always — skip path included): post-deploy
|
|
2174
1602
|
// commits builder leftovers if any, removes the worktree, and releases
|
|
@@ -2181,34 +1609,17 @@ while (i < STEPS.length) {
|
|
|
2181
1609
|
{ key: attemptKey("publish-postdeploy-" + taskId, totalReworkCount), label: "Finalizing publish (post-deploy)",
|
|
2182
1610
|
schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
|
|
2183
1611
|
);
|
|
2184
|
-
|
|
1612
|
+
// (2026-09-20, one-party worker-owned publish) Verified re-entry:
|
|
1613
|
+
// the intent park's terminal-cleanup already released the lock and
|
|
1614
|
+
// reclaimed the worktree. Post-deploy above is best-effort hygiene
|
|
1615
|
+
// on this branch — never a gate, never a park.
|
|
1616
|
+
if (publishAlreadyVerified) {
|
|
1617
|
+
log("Publish verified re-entry for task " + taskId + " — post-deploy ran forgivingly; continuing to the verified work-agent framing");
|
|
1618
|
+
} else if (publishSkippedNoLock) {
|
|
2185
1619
|
if (!postDeploy.deployed) {
|
|
2186
1620
|
return await parkTask("Post-deploy failed after a skipped publish: " + (postDeploy.output || "no output") + ". Nothing was published; worktree cleanup and lock state unknown — human attention needed.");
|
|
2187
1621
|
}
|
|
2188
1622
|
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy step finished");
|
|
2189
|
-
} else {
|
|
2190
|
-
if (publishFailure) {
|
|
2191
|
-
return await parkTask(publishFailure + (postDeploy.deployed
|
|
2192
|
-
? " Post-deploy step finished (cleanup status unknown)."
|
|
2193
|
-
: " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
|
|
2194
|
-
}
|
|
2195
|
-
if (!publishBuildLanded) {
|
|
2196
|
-
// Verdict UNKNOWN (single fail-closed park — post-deploy always
|
|
2197
|
-
// runs first; no early parks anywhere above). Machine contract:
|
|
2198
|
-
// "unattributable_reason=" and "poll_end_state=" are always
|
|
2199
|
-
// present; attribution is by window, not identity.
|
|
2200
|
-
// (2026-09-18, H2 message contract) Thread the commit short-sha
|
|
2201
|
-
// through the unknown fields so the human line names the trigger
|
|
2202
|
-
// commit (design §1.5: "the trigger was sent for commit <short-sha>").
|
|
2203
|
-
if (publishUnknownFields) publishUnknownFields.commitShortSha = mergeCommitShortForPublish;
|
|
2204
|
-
return await parkTask(composeUnattributedParkReason(publishUnknownFields) + (postDeploy.deployed
|
|
2205
|
-
? " Post-deploy step finished (cleanup status unknown)."
|
|
2206
|
-
: " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
|
|
2207
|
-
}
|
|
2208
|
-
if (!postDeploy.deployed) {
|
|
2209
|
-
return await parkTask("Post-deploy failed after the artifact build landed: " + (postDeploy.output || "no output") + ". The build may have landed but worktree cleanup and lock release are unknown — human attention needed.");
|
|
2210
|
-
}
|
|
2211
|
-
log("Deterministic artifact publish completed for task " + taskId + ": build at " + artifactPublish.source_commit + ", provenance PENDING parent content verification");
|
|
2212
1623
|
}
|
|
2213
1624
|
} catch (pubErr) {
|
|
2214
1625
|
// Best-effort cleanup: if the lock was refreshed, try to release it
|
|
@@ -2229,7 +1640,16 @@ while (i < STEPS.length) {
|
|
|
2229
1640
|
// The work agent no longer performs the publish — it reports on the
|
|
2230
1641
|
// mechanical outcome above. It must not rebuild or re-stamp: a second
|
|
2231
1642
|
// artifact_edit would trigger a duplicate build.
|
|
2232
|
-
|
|
1643
|
+
// (2026-09-20, one-party worker-owned publish) On a verified re-entry
|
|
1644
|
+
// the publish was issued by the crew worker, verified by the parent
|
|
1645
|
+
// read-back, and stamped — the work agent reports that, VERDICT: PASS.
|
|
1646
|
+
if (publishAlreadyVerified) {
|
|
1647
|
+
instructions = "Publish was already completed and verified for this task — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would disturb the finalized state.\n\n" +
|
|
1648
|
+
"The crew's publish worker issued the artifact edit for the merged commit, the parent's independent content read-back confirmed the artifact contains the merged change, and provenance was stamped (docs/publish-verification.md). This Publish pass is a verified re-entry after the stamp: there is nothing to issue and nothing to re-verify.\n\n" +
|
|
1649
|
+
"For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
|
|
1650
|
+
"Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
|
|
1651
|
+
"The VERDICT line must be the last line of your report.";
|
|
1652
|
+
} else if (publishSkippedNoLock) {
|
|
2233
1653
|
instructions = "Publish was skipped deterministically by the workflow before your step — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would disturb the finalized state.\n\n" +
|
|
2234
1654
|
"Integrate reported MERGED_EMPTY (the task branch had no commits ahead of the integration target), so no merge lock was taken and there is nothing to ship. Post-deploy step finished per its return — do NOT run post-deploy yourself.\n\n" +
|
|
2235
1655
|
"Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
|
|
@@ -2837,10 +2257,10 @@ while (i < STEPS.length) {
|
|
|
2837
2257
|
} // end: !npmPublishSkipped — a skipped publish has nothing to verify
|
|
2838
2258
|
}
|
|
2839
2259
|
|
|
2840
|
-
//
|
|
2841
|
-
// scan
|
|
2842
|
-
// the
|
|
2843
|
-
//
|
|
2260
|
+
// Version acknowledgement (docs/publish-verification.md): the tick worker
|
|
2261
|
+
// issues the edit directly and the ack scan owns the task from there —
|
|
2262
|
+
// the exact per-attempt version on the artifact's disk is the sole positive
|
|
2263
|
+
// completion criterion. A skipped or failed publish has nothing to ack.
|
|
2844
2264
|
let summary;
|
|
2845
2265
|
var workerMarkers = extractMarkerLines(workerText);
|
|
2846
2266
|
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
|
|
@@ -2957,20 +2377,6 @@ while (i < STEPS.length) {
|
|
|
2957
2377
|
return { status: "failed", task_id: taskId, reason: "Publish failed: " + summary };
|
|
2958
2378
|
}
|
|
2959
2379
|
|
|
2960
|
-
// See docs/decisions/publish-path.md#verification-park: the build landed but provenance is unstamped until parent verification.
|
|
2961
|
-
if (passed && step.name === "Publish" && PUBLISH_TYPE === "artifact" && PUBLISH_SLUG && !publishSkippedNoLock && publishBuildLanded) {
|
|
2962
|
-
// Success contract (2026-09-18, H2): the parent asked for exactly
|
|
2963
|
-
// one build for this publish. The build landed — do NOT republish: a
|
|
2964
|
-
// duplicate build would re-publish the same change. content_check=pending
|
|
2965
|
-
// means parent read-back is pending; this
|
|
2966
|
-
// park is NOT proof the content is correct.
|
|
2967
|
-
return await parkTask("publish: verification-requested " + mergeCommitForPublish +
|
|
2968
|
-
" attempt=" + rebuildAttemptKey +
|
|
2969
|
-
" Do NOT republish: a duplicate build would re-publish the same change. " +
|
|
2970
|
-
"Artifact build landed, post-deploy finalized, provenance not stamped — the crew has not yet independently confirmed the live artifact contains exactly the change; waiting on the manual read-back in docs/publish-verification.md. " +
|
|
2971
|
-
"Appendix: build=" + (rebuildAgentId || "agent_id unobserved") + "; provenance=unstamped; content_check=pending.");
|
|
2972
|
-
}
|
|
2973
|
-
|
|
2974
2380
|
i++;
|
|
2975
2381
|
}
|
|
2976
2382
|
|