muse-crew 0.14.3 → 0.14.4

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.
Files changed (40) hide show
  1. package/AGENTS.md +1 -1
  2. package/docs/decisions/AGENTS.md +1 -0
  3. package/docs/decisions/publish-path.md +43 -3
  4. package/docs/publish-verification.md +9 -1
  5. package/docs/release-integrity.md +60 -0
  6. package/docs/reviews/critic-0144.md +83 -0
  7. package/lib/AGENTS.md +10 -3
  8. package/lib/advance-publish-base.js +8 -0
  9. package/lib/append-ooda-step.js +12 -3
  10. package/lib/build-readback-request.js +10 -0
  11. package/lib/build-registry.js +8 -3
  12. package/lib/classify-publish-absence.js +11 -0
  13. package/lib/classify-surface.js +12 -2
  14. package/lib/commit-scaffold.js +22 -6
  15. package/lib/compose-evidence-caption.js +15 -4
  16. package/lib/compute-publish-diff.js +9 -0
  17. package/lib/crew-api.js +54 -21
  18. package/lib/crew-release.sh +233 -1
  19. package/lib/gitignore.js +23 -5
  20. package/lib/package.json +1 -0
  21. package/lib/publish-note-vocabulary.js +44 -0
  22. package/lib/read-ooda-verdict.js +11 -3
  23. package/lib/readback-disk.js +9 -0
  24. package/lib/render-html.js +17 -7
  25. package/lib/repo-orchestration.js +21 -5
  26. package/lib/retry-publish.js +77 -45
  27. package/lib/sample-project.js +22 -6
  28. package/lib/scaffold-crew.js +11 -2
  29. package/lib/see-act.js +17 -8
  30. package/lib/serve-artifact.js +12 -6
  31. package/lib/setup-project-repo.js +26 -7
  32. package/lib/update-watch.js +34 -17
  33. package/lib/ux-doctrine.js +31 -6
  34. package/lib/verify-publish.js +38 -2
  35. package/lib/write-ooda-verdict.js +12 -3
  36. package/package.json +1 -1
  37. package/workflows/bugfix.js +33 -35
  38. package/workflows/chore.js +33 -35
  39. package/workflows/standard.js +33 -16
  40. package/workflows/upgrade.js +4 -2
@@ -70,8 +70,10 @@ const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
70
70
  const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
71
71
  const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
72
72
  const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
73
+ const NOTE_VOCAB_SRC = crewHome + "/current/lib/publish-note-vocabulary.js";
74
+ const NOTE_VOCAB = RUN_LIB + "/publish-note-vocabulary.js";
73
75
  // See docs/decisions/qa-reproduce.md#pin-basenames: the pin step materializes the required scripts.
74
- const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
76
+ const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE, NOTE_VOCAB].map(function (p) { return p.split("/").pop(); });
75
77
 
76
78
  // Project config — passed by dispatcher, falls back to dashboard defaults
77
79
  const projectConfig = inputs.project_config || {};
@@ -191,19 +193,12 @@ async function reaskVerdict(stepName, reworkSuffix, workerText) {
191
193
  return verdict;
192
194
  }
193
195
  // Transport retry: the work-agent agent() call can throw even when the agent
194
- // did the work. Stochastic envelope non-compliance (bare prose instead of
195
- // the native {"status":"ok","result":"..."} envelope) trips the runtime's
196
- // JSON-candidate heuristic when the prose contains a {...}-looking
197
- // substring — canary 39457ee9's QA report quoted the change's own
198
- // {/* ... */} JSX comment, the runtime tried to parse it as JSON, threw,
199
- // and the workflow discarded a complete VERDICT: PASS report as "no output".
200
- // The verdict re-ask covers an unreadable verdict inside a RECEIVED report;
201
- // this covers the report never arriving. The assignment is retried boundedly
202
- // with fresh keys (never a cached replay) before failing closed. Re-entry is
203
- // safe: lifecycle scripts answer REUSED for existing worktrees/branches, the
204
- // retry trailer tells the agent to check existing state first and report
205
- // rather than duplicate completed side effects, and the rework path already
206
- // re-runs Build after rejection — Build re-entry is an established pattern.
196
+ // did the work — the runtime's JSON-candidate heuristic trips on brace-shaped
197
+ // prose. Retry the assignment boundedly with fresh keys (never a cached
198
+ // replay) before failing closed. Re-entry is safe: lifecycle scripts answer
199
+ // REUSED for existing worktrees/branches, the retry trailer tells the agent
200
+ // to check existing state first, and Build re-entry after rejection is an
201
+ // established pattern. Full history: docs/decisions/workflow-core.md.
207
202
  function workRetryKey(stepName, reworkSuffix, attempt) {
208
203
  return "work-" + stepName + reworkSuffix + "-t" + attempt;
209
204
  }
@@ -215,7 +210,7 @@ function attemptKey(base, reworkCount) {
215
210
  function pinLifecycle(key) {
216
211
  return agent(
217
212
  "Snapshot lifecycle scripts for version pinning.\n" +
218
- "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
213
+ "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && cp " + NOTE_VOCAB_SRC + " " + NOTE_VOCAB + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
219
214
  "Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
220
215
  { key: key, label: "Pinning lifecycle scripts",
221
216
  schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
@@ -396,25 +391,6 @@ function hydrateReleaseDecision(rec) {
396
391
  return null;
397
392
  }
398
393
 
399
- // Publish read-back request (currently unavailable): the verbatim_request
400
- // the parent protocol (docs/publish-verification.md) would hand to an
401
- // independent read-back tool after the artifact build lands. artifact_inspect
402
- // was removed by the platform (2026-09-14); artifact.inspect is malfunction
403
- // diagnosis, not a substitute — so no agent-callable read-back tool exists
404
- // and this request cannot currently be issued. Pure function — no I/O, no
405
- // clock. The request carries the merged diff as the expected change and asks
406
- // for an independent read of the artifact's actual source: for each file, the
407
- // exact current text of the changed regions plus a per-line present/absent
408
- // finding. Until a read-back path exists, the parent cannot independently
409
- // confirm content and verification parks at "publish: verification-requested"
410
- // (see docs/publish-verification.md). This preserves the circularity break
411
- // that hollowed canary run 8 (2026-09-11): the old verifyAppliedChanges
412
- // compared the builder's applied-report against the diff the report was
413
- // derived from — a fabricated report passed by construction. The report
414
- // itself is gone now (2026-09-16 fire-and-forget trigger). Independent
415
- // read-back cannot be
416
- // fabricated from the diff; it must match the artifact's real content.
417
-
418
394
  // See docs/decisions/publish-path.md#publish-attempt-ledger: every trigger outcome is recorded in the durable ledger.
419
395
  // keyTag (optional): same-outcome ledger writes in one pass need distinct
420
396
  // agent-call keys — the runtime replays cached calls by key, so a shared key
@@ -433,7 +409,10 @@ async function recordPublishLedger(entry, rework, keyTag) {
433
409
  applied_report: entry.applied_report || null,
434
410
  manifest_before: entry.manifest_before || null,
435
411
  outcome: entry.outcome,
436
- detail: entry.detail || ""
412
+ detail: entry.detail || "",
413
+ // D1 (2026-09-19): issued_at = upper bound on the trigger-issuance
414
+ // instant (null when no trigger); ts = ledger-write instant.
415
+ issued_at: entry.issued_at || null,
437
416
  });
438
417
  var sq = function(s) { return "'" + String(s).split("'").join("'\\''") + "'"; };
439
418
  var res = await agent(
@@ -1665,6 +1644,23 @@ while (i < STEPS.length) {
1665
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");
1666
1645
  triggerThrew = true;
1667
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");
1668
1664
  // (2026-09-19, room #22) Submitted-on-issuance: see docs/decisions/publish-path.md#submitted-on-issuance.
1669
1665
  await recordPublishLedger({
1670
1666
  commit: mergeCommitForPublish,
@@ -1673,6 +1669,7 @@ while (i < STEPS.length) {
1673
1669
  applied_report: publishAppliedObservation,
1674
1670
  manifest_before: preTriggerManifest,
1675
1671
  outcome: "submitted",
1672
+ issued_at: triggerIssuedAt,
1676
1673
  detail: "trigger issuance: rebuild-trigger agent invoked; receipt/observation pending. Invocation only - not evidence of artifact_edit or build acceptance." + (triggerThrew ? "; trigger-threw" : "; trigger-returned")
1677
1674
  }, totalReworkCount, "issuance");
1678
1675
  // Post-trigger observation (primary, not fallback): the workflow
@@ -1794,6 +1791,7 @@ while (i < STEPS.length) {
1794
1791
  applied_report: publishAppliedObservation,
1795
1792
  manifest_before: preTriggerManifest,
1796
1793
  outcome: "submitted",
1794
+ issued_at: triggerIssuedAt,
1797
1795
  detail: "fire-and-forget trigger; build receipt captured by workflow-owned build-state observation (pre/post-trigger diff); observation, not re-issuance"
1798
1796
  }, totalReworkCount, "receipt");
1799
1797
  } else {
@@ -127,8 +127,10 @@ const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
127
127
  const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
128
128
  const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
129
129
  const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
130
+ const NOTE_VOCAB_SRC = crewHome + "/current/lib/publish-note-vocabulary.js";
131
+ const NOTE_VOCAB = RUN_LIB + "/publish-note-vocabulary.js";
130
132
  // See docs/decisions/qa-reproduce.md#pin-basenames: the pin step materializes the required scripts.
131
- const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
133
+ const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE, NOTE_VOCAB].map(function (p) { return p.split("/").pop(); });
132
134
 
133
135
  // Project config — passed by dispatcher, falls back to dashboard defaults
134
136
  const projectConfig = inputs.project_config || {};
@@ -250,19 +252,12 @@ async function reaskVerdict(stepName, reworkSuffix, workerText) {
250
252
  return verdict;
251
253
  }
252
254
  // Transport retry: the work-agent agent() call can throw even when the agent
253
- // did the work. Stochastic envelope non-compliance (bare prose instead of
254
- // the native {"status":"ok","result":"..."} envelope) trips the runtime's
255
- // JSON-candidate heuristic when the prose contains a {...}-looking
256
- // substring — canary 39457ee9's QA report quoted the change's own
257
- // {/* ... */} JSX comment, the runtime tried to parse it as JSON, threw,
258
- // and the workflow discarded a complete VERDICT: PASS report as "no output".
259
- // The verdict re-ask covers an unreadable verdict inside a RECEIVED report;
260
- // this covers the report never arriving. The assignment is retried boundedly
261
- // with fresh keys (never a cached replay) before failing closed. Re-entry is
262
- // safe: lifecycle scripts answer REUSED for existing worktrees/branches, the
263
- // retry trailer tells the agent to check existing state first and report
264
- // rather than duplicate completed side effects, and the rework path already
265
- // re-runs Build after rejection — Build re-entry is an established pattern.
255
+ // did the work — the runtime's JSON-candidate heuristic trips on brace-shaped
256
+ // prose. Retry the assignment boundedly with fresh keys (never a cached
257
+ // replay) before failing closed. Re-entry is safe: lifecycle scripts answer
258
+ // REUSED for existing worktrees/branches, the retry trailer tells the agent
259
+ // to check existing state first, and Build re-entry after rejection is an
260
+ // established pattern. Full history: docs/decisions/workflow-core.md.
266
261
  function workRetryKey(stepName, reworkSuffix, attempt) {
267
262
  return "work-" + stepName + reworkSuffix + "-t" + attempt;
268
263
  }
@@ -274,7 +269,7 @@ function attemptKey(base, reworkCount) {
274
269
  function pinLifecycle(key) {
275
270
  return agent(
276
271
  "Snapshot lifecycle scripts for version pinning.\n" +
277
- "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
272
+ "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && cp " + NOTE_VOCAB_SRC + " " + NOTE_VOCAB + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
278
273
  "Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
279
274
  { key: key, label: "Pinning lifecycle scripts",
280
275
  schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
@@ -455,25 +450,6 @@ function hydrateReleaseDecision(rec) {
455
450
  return null;
456
451
  }
457
452
 
458
- // Publish read-back request (currently unavailable): the verbatim_request
459
- // the parent protocol (docs/publish-verification.md) would hand to an
460
- // independent read-back tool after the artifact build lands. artifact_inspect
461
- // was removed by the platform (2026-09-14); artifact.inspect is malfunction
462
- // diagnosis, not a substitute — so no agent-callable read-back tool exists
463
- // and this request cannot currently be issued. Pure function — no I/O, no
464
- // clock. The request carries the merged diff as the expected change and asks
465
- // for an independent read of the artifact's actual source: for each file, the
466
- // exact current text of the changed regions plus a per-line present/absent
467
- // finding. Until a read-back path exists, the parent cannot independently
468
- // confirm content and verification parks at "publish: verification-requested"
469
- // (see docs/publish-verification.md). This preserves the circularity break
470
- // that hollowed canary run 8 (2026-09-11): the old verifyAppliedChanges
471
- // compared the builder's applied-report against the diff the report was
472
- // derived from — a fabricated report passed by construction. The report
473
- // itself is gone now (2026-09-16 fire-and-forget trigger). Independent
474
- // read-back cannot be
475
- // fabricated from the diff; it must match the artifact's real content.
476
-
477
453
  // See docs/decisions/publish-path.md#publish-attempt-ledger: every trigger outcome is recorded in the durable ledger.
478
454
  // keyTag (optional): same-outcome ledger writes in one pass need distinct
479
455
  // agent-call keys — the runtime replays cached calls by key, so a shared key
@@ -492,7 +468,10 @@ async function recordPublishLedger(entry, rework, keyTag) {
492
468
  applied_report: entry.applied_report || null,
493
469
  manifest_before: entry.manifest_before || null,
494
470
  outcome: entry.outcome,
495
- detail: entry.detail || ""
471
+ detail: entry.detail || "",
472
+ // D1 (2026-09-19): issued_at = upper bound on the trigger-issuance
473
+ // instant (null when no trigger); ts = ledger-write instant.
474
+ issued_at: entry.issued_at || null,
496
475
  });
497
476
  var sq = function(s) { return "'" + String(s).split("'").join("'\\''") + "'"; };
498
477
  var res = await agent(
@@ -1571,6 +1550,23 @@ while (i < STEPS.length) {
1571
1550
  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");
1572
1551
  triggerThrew = true;
1573
1552
  }
1553
+ // D1 (2026-09-19; F-A1 upper-bound 2026-09-20): issued_at is an UPPER
1554
+ // bound on issuance — captured AFTER the trigger-agent call returns,
1555
+ // ferried from one shell date call (workflows have no clock) and
1556
+ // validated mechanically; null when unobserved (fallback: ts).
1557
+ // See docs/decisions/publish-path.md#d1-issuance-time.
1558
+ var triggerIssuedAt = null;
1559
+ try {
1560
+ var issuedAtFerry = await agent(
1561
+ "Run: date -u +%Y-%m-%dT%H:%M:%SZ\n" +
1562
+ "Return JSON { \"ts\": \"<the date output, verbatim>\" } and nothing else.",
1563
+ { key: attemptKey("publish-issued-at-" + taskId, reworkCount), label: "Capturing post-trigger instant (upper bound on issuance)",
1564
+ schema: { type: "object", properties: { ts: { type: "string" } }, required: ["ts"] } }
1565
+ );
1566
+ var issuedAtRaw = String((issuedAtFerry && issuedAtFerry.ts) || "").trim();
1567
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(issuedAtRaw)) triggerIssuedAt = issuedAtRaw;
1568
+ } catch (issuedAtErr) { /* issued_at stays null: the verifier falls back to ts */ }
1569
+ if (!triggerIssuedAt) log("Publish issuance instant unobserved for task " + taskId + " — issued_at null; the freshness gate falls back to ledger-write ts");
1574
1570
  // (2026-09-19, room #22) Submitted-on-issuance: see docs/decisions/publish-path.md#submitted-on-issuance.
1575
1571
  await recordPublishLedger({
1576
1572
  commit: mergeCommitForPublish,
@@ -1579,6 +1575,7 @@ while (i < STEPS.length) {
1579
1575
  applied_report: publishAppliedObservation,
1580
1576
  manifest_before: preTriggerManifest,
1581
1577
  outcome: "submitted",
1578
+ issued_at: triggerIssuedAt,
1582
1579
  detail: "trigger issuance: rebuild-trigger agent invoked; receipt/observation pending. Invocation only - not evidence of artifact_edit or build acceptance." + (triggerThrew ? "; trigger-threw" : "; trigger-returned")
1583
1580
  }, reworkCount, "issuance");
1584
1581
  // Post-trigger observation (primary, not fallback): the workflow
@@ -1700,6 +1697,7 @@ while (i < STEPS.length) {
1700
1697
  applied_report: publishAppliedObservation,
1701
1698
  manifest_before: preTriggerManifest,
1702
1699
  outcome: "submitted",
1700
+ issued_at: triggerIssuedAt,
1703
1701
  detail: "fire-and-forget trigger; build receipt captured by workflow-owned build-state observation (pre/post-trigger diff); observation, not re-issuance"
1704
1702
  }, reworkCount, "receipt");
1705
1703
  } else {
@@ -78,8 +78,10 @@ const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
78
78
  const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
79
79
  const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
80
80
  const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
81
+ const NOTE_VOCAB_SRC = crewHome + "/current/lib/publish-note-vocabulary.js";
82
+ const NOTE_VOCAB = RUN_LIB + "/publish-note-vocabulary.js";
81
83
  // See docs/decisions/qa-reproduce.md#pin-basenames: the pin step materializes the required scripts.
82
- const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
84
+ const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE, NOTE_VOCAB].map(function (p) { return p.split("/").pop(); });
83
85
 
84
86
  // Project config — passed by dispatcher, falls back to dashboard defaults
85
87
  const projectConfig = inputs.project_config || {};
@@ -199,19 +201,12 @@ async function reaskVerdict(stepName, reworkSuffix, workerText) {
199
201
  return verdict;
200
202
  }
201
203
  // Transport retry: the work-agent agent() call can throw even when the agent
202
- // did the work. Stochastic envelope non-compliance (bare prose instead of
203
- // the native {"status":"ok","result":"..."} envelope) trips the runtime's
204
- // JSON-candidate heuristic when the prose contains a {...}-looking
205
- // substring — canary 39457ee9's QA report quoted the change's own
206
- // {/* ... */} JSX comment, the runtime tried to parse it as JSON, threw,
207
- // and the workflow discarded a complete VERDICT: PASS report as "no output".
208
- // The verdict re-ask covers an unreadable verdict inside a RECEIVED report;
209
- // this covers the report never arriving. The assignment is retried boundedly
210
- // with fresh keys (never a cached replay) before failing closed. Re-entry is
211
- // safe: lifecycle scripts answer REUSED for existing worktrees/branches, the
212
- // retry trailer tells the agent to check existing state first and report
213
- // rather than duplicate completed side effects, and the rework path already
214
- // re-runs Build after rejection — Build re-entry is an established pattern.
204
+ // did the work — the runtime's JSON-candidate heuristic trips on brace-shaped
205
+ // prose. Retry the assignment boundedly with fresh keys (never a cached
206
+ // replay) before failing closed. Re-entry is safe: lifecycle scripts answer
207
+ // REUSED for existing worktrees/branches, the retry trailer tells the agent
208
+ // to check existing state first, and Build re-entry after rejection is an
209
+ // established pattern. Full history: docs/decisions/workflow-core.md.
215
210
  function workRetryKey(stepName, reworkSuffix, attempt) {
216
211
  return "work-" + stepName + reworkSuffix + "-t" + attempt;
217
212
  }
@@ -223,7 +218,7 @@ function attemptKey(base, reworkCount) {
223
218
  function pinLifecycle(key) {
224
219
  return agent(
225
220
  "Snapshot lifecycle scripts for version pinning.\n" +
226
- "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
221
+ "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && cp " + NOTE_VOCAB_SRC + " " + NOTE_VOCAB + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
227
222
  "Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
228
223
  { key: key, label: "Pinning lifecycle scripts",
229
224
  schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
@@ -445,7 +440,10 @@ async function recordPublishLedger(entry, rework, keyTag) {
445
440
  applied_report: entry.applied_report || null,
446
441
  manifest_before: entry.manifest_before || null,
447
442
  outcome: entry.outcome,
448
- detail: entry.detail || ""
443
+ detail: entry.detail || "",
444
+ // D1 (2026-09-19): issued_at = upper bound on the trigger-issuance
445
+ // instant (null when no trigger); ts = ledger-write instant.
446
+ issued_at: entry.issued_at || null,
449
447
  });
450
448
  var sq = function(s) { return "'" + String(s).split("'").join("'\\''") + "'"; };
451
449
  var res = await agent(
@@ -1558,6 +1556,23 @@ while (i < STEPS.length) {
1558
1556
  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");
1559
1557
  triggerThrew = true;
1560
1558
  }
1559
+ // D1 (2026-09-19; F-A1 upper-bound 2026-09-20): issued_at is an UPPER
1560
+ // bound on issuance — captured AFTER the trigger-agent call returns,
1561
+ // ferried from one shell date call (workflows have no clock) and
1562
+ // validated mechanically; null when unobserved (fallback: ts).
1563
+ // See docs/decisions/publish-path.md#d1-issuance-time.
1564
+ var triggerIssuedAt = null;
1565
+ try {
1566
+ var issuedAtFerry = await agent(
1567
+ "Run: date -u +%Y-%m-%dT%H:%M:%SZ\n" +
1568
+ "Return JSON { \"ts\": \"<the date output, verbatim>\" } and nothing else.",
1569
+ { key: attemptKey("publish-issued-at-" + taskId, totalReworkCount), label: "Capturing post-trigger instant (upper bound on issuance)",
1570
+ schema: { type: "object", properties: { ts: { type: "string" } }, required: ["ts"] } }
1571
+ );
1572
+ var issuedAtRaw = String((issuedAtFerry && issuedAtFerry.ts) || "").trim();
1573
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(issuedAtRaw)) triggerIssuedAt = issuedAtRaw;
1574
+ } catch (issuedAtErr) { /* issued_at stays null: the verifier falls back to ts */ }
1575
+ if (!triggerIssuedAt) log("Publish issuance instant unobserved for task " + taskId + " — issued_at null; the freshness gate falls back to ledger-write ts");
1561
1576
  // (2026-09-19, room #22) Submitted-on-issuance: see docs/decisions/publish-path.md#submitted-on-issuance.
1562
1577
  await recordPublishLedger({
1563
1578
  commit: mergeCommitForPublish,
@@ -1566,6 +1581,7 @@ while (i < STEPS.length) {
1566
1581
  applied_report: publishAppliedObservation,
1567
1582
  manifest_before: preTriggerManifest,
1568
1583
  outcome: "submitted",
1584
+ issued_at: triggerIssuedAt,
1569
1585
  detail: "trigger issuance: rebuild-trigger agent invoked; receipt/observation pending. Invocation only - not evidence of artifact_edit or build acceptance." + (triggerThrew ? "; trigger-threw" : "; trigger-returned")
1570
1586
  }, totalReworkCount, "issuance");
1571
1587
  // Post-trigger observation (primary, not fallback): the workflow
@@ -1687,6 +1703,7 @@ while (i < STEPS.length) {
1687
1703
  applied_report: publishAppliedObservation,
1688
1704
  manifest_before: preTriggerManifest,
1689
1705
  outcome: "submitted",
1706
+ issued_at: triggerIssuedAt,
1690
1707
  detail: "fire-and-forget trigger; build receipt captured by workflow-owned build-state observation (pre/post-trigger diff); observation, not re-issuance"
1691
1708
  }, totalReworkCount, "receipt");
1692
1709
  } else {
@@ -165,12 +165,14 @@ const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
165
165
  const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
166
166
  const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
167
167
  const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
168
+ const NOTE_VOCAB_SRC = crewHome + "/current/lib/publish-note-vocabulary.js";
169
+ const NOTE_VOCAB = RUN_LIB + "/publish-note-vocabulary.js";
168
170
  // The six basenames the pin step must materialize — asserted mechanically
169
171
  // by workflow code from the verbatim listing, never from agent prose.
170
172
  // CLASSIFY_SURFACE is the surface classifier (room #15, 2026-09-18):
171
173
  // crew-api.js statically imports it, so the pin must carry it — a pin
172
174
  // without it kills every claim with ERR_MODULE_NOT_FOUND.
173
- const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
175
+ const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, CLASSIFY_SURFACE, NOTE_VOCAB].map(function (p) { return p.split("/").pop(); });
174
176
 
175
177
  // Project config — passed by dispatcher, falls back to dashboard defaults
176
178
  const projectConfig = inputs.project_config || {};
@@ -201,7 +203,7 @@ if (!taskId) {
201
203
  function pinLifecycle(key) {
202
204
  return agent(
203
205
  "Snapshot lifecycle scripts for version pinning.\n" +
204
- "Run: mkdir -p " + sq(RUN_LIB) + " && cp " + sq(LIFECYCLE_SRC) + " " + sq(LIFECYCLE) + " && cp " + sq(MERGE_LOCK_SRC) + " " + sq(MERGE_LOCK) + " && cp " + sq(PUBLISH_NPM_SRC) + " " + sq(PUBLISH_NPM) + " && cp " + sq(CREW_API_SRC) + " " + sq(CREW_API_PINNED) + " && cp " + sq(SCHEMA_SQL_SRC) + " " + sq(SCHEMA_SQL_PINNED) + " && cp " + sq(CLASSIFY_SURFACE_SRC) + " " + sq(CLASSIFY_SURFACE) + " && chmod +x " + sq(LIFECYCLE) + " " + sq(MERGE_LOCK) + " " + sq(PUBLISH_NPM) + " && ls -1 " + sq(RUN_LIB) + "\n" +
206
+ "Run: mkdir -p " + sq(RUN_LIB) + " && cp " + sq(LIFECYCLE_SRC) + " " + sq(LIFECYCLE) + " && cp " + sq(MERGE_LOCK_SRC) + " " + sq(MERGE_LOCK) + " && cp " + sq(PUBLISH_NPM_SRC) + " " + sq(PUBLISH_NPM) + " && cp " + sq(CREW_API_SRC) + " " + sq(CREW_API_PINNED) + " && cp " + sq(SCHEMA_SQL_SRC) + " " + sq(SCHEMA_SQL_PINNED) + " && cp " + sq(CLASSIFY_SURFACE_SRC) + " " + sq(CLASSIFY_SURFACE) + " && cp " + sq(NOTE_VOCAB_SRC) + " " + sq(NOTE_VOCAB) + " && chmod +x " + sq(LIFECYCLE) + " " + sq(MERGE_LOCK) + " " + sq(PUBLISH_NPM) + " && ls -1 " + sq(RUN_LIB) + "\n" +
205
207
  "Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
206
208
  { key: key, label: "Pinning lifecycle scripts",
207
209
  schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }