muse-crew 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md CHANGED
@@ -258,7 +258,7 @@ Set a global configuration value.
258
258
 
259
259
  ### `setprovenance`
260
260
 
261
- Stamp publication provenance. Called by the crew Publish phase after rebuilding the artifact.
261
+ Stamp publication provenance. Called by the crew Publish phase after rebuilding the artifact, and after installing/activating a crew release (npm path): in the release case Publish refreshes `crew_release` to the live release identity while preserving the existing `source_commit`.
262
262
 
263
263
  | Field | Type | Required | Notes |
264
264
  |-------|------|----------|-------|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Opinionated orchestration for Muse \u2014 workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -22,6 +22,14 @@ const taskTitle = inputs.task_title || "";
22
22
  const taskDescription = inputs.task_description || "";
23
23
  const startStepIndex = inputs.start_step_index || 0;
24
24
 
25
+ // Dispatcher-carried facts for the claim-time persist: the workflow name the
26
+ // dispatcher actually resolved and launched, and whether the task record had
27
+ // no workflow (the standard fallback). The launched workflow writes the
28
+ // resolution back via updatetask in the self-claim below.
29
+ const RESOLVED_WORKFLOW = inputs.resolved_workflow || null;
30
+ const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
31
+ const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"workflow\": \"" + RESOLVED_WORKFLOW + "\"" : "";
32
+
25
33
  // Config from args — backward-compatible fallbacks for manual launches
26
34
  const DASHBOARD_SLUG = inputs.dashboardSlug || "orchestra-dashboard";
27
35
  const crewHome = inputs.crewHome || "~/workspace/.jarvis";
@@ -315,8 +323,14 @@ function buildVisualCapturePlan(taskTitle, taskDescription, kind, captureTargets
315
323
  // opt-in — a missing or garbled line degrades to "unknown", which callers
316
324
  // treat as non-experiential. The task is never parked on a garbled line.
317
325
  async function resolveExperiential() {
318
- if (isExperiential === true) return "yes";
319
- if (isExperiential === false) return "no";
326
+ // Triage notes are immutable within a run: cache the resolved outcome —
327
+ // yes, no, AND unknown — so the dashboard lookup runs at most once per
328
+ // run. Without the unknown branch a missing experiential marker would
329
+ // re-fire the same "resolve-experiential-<taskId>" agent key on every
330
+ // Capture/Map entry and throw a duplicate replay key error.
331
+ if (experientialResolved !== null) return experientialResolved;
332
+ if (isExperiential === true) return (experientialResolved = "yes");
333
+ if (isExperiential === false) return (experientialResolved = "no");
320
334
  var expCheck = null;
321
335
  try {
322
336
  expCheck = await agent(
@@ -332,12 +346,14 @@ async function resolveExperiential() {
332
346
  );
333
347
  } catch (e) {
334
348
  log("resolveExperiential: agent call failed (" + (e && e.message ? e.message : e) + ") — treating as unknown");
335
- return "unknown";
349
+ experientialResolved = "unknown";
350
+ return experientialResolved;
336
351
  }
337
352
  var marker = extractExperiential(expCheck && expCheck.experiential_line ? expCheck.experiential_line : "");
338
- if (marker === true) return "yes";
339
- if (marker === false) return "no";
340
- return "unknown";
353
+ if (marker === true) experientialResolved = "yes";
354
+ else if (marker === false) experientialResolved = "no";
355
+ else experientialResolved = "unknown";
356
+ return experientialResolved;
341
357
  }
342
358
  // Baseline evidence status: reads the task's note events for the exact
343
359
  // protocol prefixes (explicit state, never English matching). Returns
@@ -448,6 +464,9 @@ let mapperSpec = "";
448
464
  // plan, and the Map-gate bounce counter (keeps agent stable-keys unique when
449
465
  // a Map-gate bounce re-runs Capture/Map in the same run).
450
466
  let isExperiential = null;
467
+ // Caches all three resolveExperiential() outcomes (yes/no/unknown); Triage
468
+ // notes are immutable within a run, so the lookup runs at most once.
469
+ let experientialResolved = null;
451
470
  let captureTargets = "";
452
471
  let mapGateBounceCount = 0;
453
472
  // Merge-time versioning: the release decision is extracted deterministically
@@ -536,7 +555,7 @@ while (i < STEPS.length) {
536
555
  "Find the task with id \"" + taskId + "\" in the returned tasks array.\n" +
537
556
  "Return exactly { \"project\": \"<the task's project field, or empty string if absent>\" } and nothing else.",
538
557
  {
539
- key: "project-check-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
558
+ key: "project-check-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : "") + (mapGateBounceCount > 0 ? "-g" + mapGateBounceCount : ""),
540
559
  label: "Checking project before " + step.name,
541
560
  schema: { type: "object", properties: { project: { type: "string" } }, required: ["project"] }
542
561
  }
@@ -660,7 +679,7 @@ while (i < STEPS.length) {
660
679
  if (isFirstClaim) {
661
680
  const claimResult = await agent(
662
681
  "Claim this task for the " + step.name + " step.\n" +
663
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\" }.\n" +
682
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\"" + CLAIM_WORKFLOW_PERSIST + " }.\n" +
664
683
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
665
684
  "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started\" }.\n" +
666
685
  "Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
@@ -1357,8 +1376,39 @@ while (i < STEPS.length) {
1357
1376
  return await parkTask("Publish verification failed: registry shows " + regVer + " but the release decision required " +
1358
1377
  publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land.");
1359
1378
  }
1360
- log("Publish verified for task " + taskId + ": registry at " + regVer);
1361
1379
  publishVerified = true;
1380
+ // Provenance refresh for self-publishes (task 7946d2a4): the npm path
1381
+ // installs and activates a new immutable release (crew-release.sh deploy
1382
+ // swaps the `current` symlink inside publish-npm.sh) but never stamped
1383
+ // the dashboard's provenance record — every crew release left
1384
+ // crew_release pointing at a pruned release. After a verified landed
1385
+ // publish, refresh the record's crew_release to the now-live release
1386
+ // identity, preserving the existing source_commit (the dashboard
1387
+ // artifact's build source — a crew-repo commit here would fail the
1388
+ // dashboard QA source check).
1389
+ try {
1390
+ var provRefresh = await agent(
1391
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getprovenance\", args: {}. " +
1392
+ "If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
1393
+ "Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
1394
+ "run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
1395
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"setprovenance\", args: " +
1396
+ "{ \"source_commit\": \"<existing provenance.source_commit>\", \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\", \"task_id\": \"" + taskId + "\" }. " +
1397
+ "Return JSON { \"refreshed\": <true if the setprovenance response contains ok: true, false otherwise>, \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\" } and nothing else.",
1398
+ { key: attemptKey("publish-provenance-refresh-" + taskId, totalReworkCount), label: "Refreshing dashboard provenance after crew release",
1399
+ schema: { type: "object", properties: { refreshed: { type: "boolean" }, reason: { type: "string" }, crew_release: { type: "string" }, published_at: { type: "string" } }, required: ["refreshed"] } }
1400
+ );
1401
+ if (provRefresh.refreshed) {
1402
+ log("Provenance refreshed for task " + taskId + ": crew_release " + provRefresh.crew_release);
1403
+ } else if (provRefresh.reason === "no-record") {
1404
+ log("Provenance refresh skipped for task " + taskId + ": no existing record to refresh (fresh instance — the dashboard's first artifact publish will create it)");
1405
+ } else {
1406
+ return await parkTask("Provenance refresh failed after a verified npm publish (crew_release " + (provRefresh.crew_release || "unknown") + "). The release is live but the dashboard record is stale — fail-closed.");
1407
+ }
1408
+ } catch (e) {
1409
+ return await parkTask("Provenance refresh failed: could not refresh dashboard provenance after a verified npm publish (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1410
+ }
1411
+
1362
1412
  } catch (e) {
1363
1413
  return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1364
1414
  }
@@ -20,6 +20,14 @@ const taskTitle = inputs.task_title || "";
20
20
  const taskDescription = inputs.task_description || "";
21
21
  const startStepIndex = inputs.start_step_index || 0;
22
22
 
23
+ // Dispatcher-carried facts for the claim-time persist: the workflow name the
24
+ // dispatcher actually resolved and launched, and whether the task record had
25
+ // no workflow (the standard fallback). The launched workflow writes the
26
+ // resolution back via updatetask in the self-claim below.
27
+ const RESOLVED_WORKFLOW = inputs.resolved_workflow || null;
28
+ const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
29
+ const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"workflow\": \"" + RESOLVED_WORKFLOW + "\"" : "";
30
+
23
31
  // Config from args — backward-compatible fallbacks for manual launches
24
32
  const DASHBOARD_SLUG = inputs.dashboardSlug || "orchestra-dashboard";
25
33
  const crewHome = inputs.crewHome || "~/workspace/.jarvis";
@@ -313,8 +321,14 @@ function buildVisualCapturePlan(taskTitle, taskDescription, kind, captureTargets
313
321
  // opt-in — a missing or garbled line degrades to "unknown", which callers
314
322
  // treat as non-experiential. The task is never parked on a garbled line.
315
323
  async function resolveExperiential() {
316
- if (isExperiential === true) return "yes";
317
- if (isExperiential === false) return "no";
324
+ // Triage notes are immutable within a run: cache the resolved outcome —
325
+ // yes, no, AND unknown — so the dashboard lookup runs at most once per
326
+ // run. Without the unknown branch a missing experiential marker would
327
+ // re-fire the same "resolve-experiential-<taskId>" agent key on every
328
+ // Capture/Map entry and throw a duplicate replay key error.
329
+ if (experientialResolved !== null) return experientialResolved;
330
+ if (isExperiential === true) return (experientialResolved = "yes");
331
+ if (isExperiential === false) return (experientialResolved = "no");
318
332
  var expCheck = null;
319
333
  try {
320
334
  expCheck = await agent(
@@ -330,12 +344,14 @@ async function resolveExperiential() {
330
344
  );
331
345
  } catch (e) {
332
346
  log("resolveExperiential: agent call failed (" + (e && e.message ? e.message : e) + ") — treating as unknown");
333
- return "unknown";
347
+ experientialResolved = "unknown";
348
+ return experientialResolved;
334
349
  }
335
350
  var marker = extractExperiential(expCheck && expCheck.experiential_line ? expCheck.experiential_line : "");
336
- if (marker === true) return "yes";
337
- if (marker === false) return "no";
338
- return "unknown";
351
+ if (marker === true) experientialResolved = "yes";
352
+ else if (marker === false) experientialResolved = "no";
353
+ else experientialResolved = "unknown";
354
+ return experientialResolved;
339
355
  }
340
356
  // Baseline evidence status: reads the task's note events for the exact
341
357
  // protocol prefixes (explicit state, never English matching). Returns
@@ -406,6 +422,9 @@ let mapperSpec = "";
406
422
  // a Map-gate bounce re-runs Capture/Map in the same run). Chore has no QA
407
423
  // phase, so there is no visual-verdict function — Capture only.
408
424
  let isExperiential = null;
425
+ // Caches all three resolveExperiential() outcomes (yes/no/unknown); Triage
426
+ // notes are immutable within a run, so the lookup runs at most once.
427
+ let experientialResolved = null;
409
428
  let captureTargets = "";
410
429
  let mapGateBounceCount = 0;
411
430
  // Merge-time versioning: the release decision is extracted deterministically
@@ -494,7 +513,7 @@ while (i < STEPS.length) {
494
513
  "Find the task with id \"" + taskId + "\" in the returned tasks array.\n" +
495
514
  "Return exactly { \"project\": \"<the task's project field, or empty string if absent>\" } and nothing else.",
496
515
  {
497
- key: "project-check-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
516
+ key: "project-check-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : "") + (mapGateBounceCount > 0 ? "-g" + mapGateBounceCount : ""),
498
517
  label: "Checking project before " + step.name,
499
518
  schema: { type: "object", properties: { project: { type: "string" } }, required: ["project"] }
500
519
  }
@@ -618,7 +637,7 @@ while (i < STEPS.length) {
618
637
  if (isFirstClaim) {
619
638
  const claimResult = await agent(
620
639
  "Claim this task for the " + step.name + " step.\n" +
621
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\" }.\n" +
640
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\"" + CLAIM_WORKFLOW_PERSIST + " }.\n" +
622
641
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
623
642
  "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started\" }.\n" +
624
643
  "Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
@@ -1232,8 +1251,39 @@ while (i < STEPS.length) {
1232
1251
  return await parkTask("Publish verification failed: registry shows " + regVer + " but the release decision required " +
1233
1252
  publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land.");
1234
1253
  }
1235
- log("Publish verified for task " + taskId + ": registry at " + regVer);
1236
1254
  publishVerified = true;
1255
+ // Provenance refresh for self-publishes (task 7946d2a4): the npm path
1256
+ // installs and activates a new immutable release (crew-release.sh deploy
1257
+ // swaps the `current` symlink inside publish-npm.sh) but never stamped
1258
+ // the dashboard's provenance record — every crew release left
1259
+ // crew_release pointing at a pruned release. After a verified landed
1260
+ // publish, refresh the record's crew_release to the now-live release
1261
+ // identity, preserving the existing source_commit (the dashboard
1262
+ // artifact's build source — a crew-repo commit here would fail the
1263
+ // dashboard QA source check).
1264
+ try {
1265
+ var provRefresh = await agent(
1266
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getprovenance\", args: {}. " +
1267
+ "If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
1268
+ "Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
1269
+ "run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
1270
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"setprovenance\", args: " +
1271
+ "{ \"source_commit\": \"<existing provenance.source_commit>\", \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\", \"task_id\": \"" + taskId + "\" }. " +
1272
+ "Return JSON { \"refreshed\": <true if the setprovenance response contains ok: true, false otherwise>, \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\" } and nothing else.",
1273
+ { key: attemptKey("publish-provenance-refresh-" + taskId, reworkCount), label: "Refreshing dashboard provenance after crew release",
1274
+ schema: { type: "object", properties: { refreshed: { type: "boolean" }, reason: { type: "string" }, crew_release: { type: "string" }, published_at: { type: "string" } }, required: ["refreshed"] } }
1275
+ );
1276
+ if (provRefresh.refreshed) {
1277
+ log("Provenance refreshed for task " + taskId + ": crew_release " + provRefresh.crew_release);
1278
+ } else if (provRefresh.reason === "no-record") {
1279
+ log("Provenance refresh skipped for task " + taskId + ": no existing record to refresh (fresh instance — the dashboard's first artifact publish will create it)");
1280
+ } else {
1281
+ return await parkTask("Provenance refresh failed after a verified npm publish (crew_release " + (provRefresh.crew_release || "unknown") + "). The release is live but the dashboard record is stale — fail-closed.");
1282
+ }
1283
+ } catch (e) {
1284
+ return await parkTask("Provenance refresh failed: could not refresh dashboard provenance after a verified npm publish (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1285
+ }
1286
+
1237
1287
  } catch (e) {
1238
1288
  return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1239
1289
  }
@@ -593,7 +593,13 @@ for (var p = 0; p < toProcess.length; p++) {
593
593
  project_config: projectCfg,
594
594
  project_id: taskProject,
595
595
  dashboardSlug: DASHBOARD_SLUG,
596
- crewHome: crewHome
596
+ crewHome: crewHome,
597
+ // Facts the launched workflow needs to persist the resolution at claim time:
598
+ // the workflow name the dispatcher actually resolved and is launching, and
599
+ // whether the task record had no workflow (playtest filings carry explicit
600
+ // workflow:'standard', so they correctly get false).
601
+ resolved_workflow: iworkflow,
602
+ workflow_was_null: (itask.workflow == null)
597
603
  };
598
604
 
599
605
  log("Recommended " + iworkflow + " for \"" + itask.title + "\" [" + taskProject + "] at step " + nextStepName);
package/workflows/docs.js CHANGED
@@ -16,6 +16,14 @@ const taskTitle = inputs.task_title || "";
16
16
  const taskDescription = inputs.task_description || "";
17
17
  const startStepIndex = inputs.start_step_index || 0;
18
18
 
19
+ // Dispatcher-carried facts for the claim-time persist: the workflow name the
20
+ // dispatcher actually resolved and launched, and whether the task record had
21
+ // no workflow (the standard fallback). The launched workflow writes the
22
+ // resolution back via updatetask in the self-claim below.
23
+ const RESOLVED_WORKFLOW = inputs.resolved_workflow || null;
24
+ const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
25
+ const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"workflow\": \"" + RESOLVED_WORKFLOW + "\"" : "";
26
+
19
27
  // Config from args — backward-compatible fallbacks for manual launches
20
28
  const DASHBOARD_SLUG = inputs.dashboardSlug || "orchestra-dashboard";
21
29
  const crewHome = inputs.crewHome || "~/workspace/.jarvis";
@@ -125,7 +133,7 @@ while (i < STEPS.length) {
125
133
  if (isFirstClaim) {
126
134
  const claimResult = await agent(
127
135
  "Claim this task for the " + step.name + " step.\n" +
128
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\" }.\n" +
136
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\"" + CLAIM_WORKFLOW_PERSIST + " }.\n" +
129
137
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
130
138
  "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started\" }.\n" +
131
139
  "Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
@@ -21,6 +21,14 @@ const taskTitle = inputs.task_title || "";
21
21
  const taskDescription = inputs.task_description || "";
22
22
  const startStepIndex = inputs.start_step_index || 0;
23
23
 
24
+ // Dispatcher-carried facts for the claim-time persist: the workflow name the
25
+ // dispatcher actually resolved and launched, and whether the task record had
26
+ // no workflow (the standard fallback). The launched workflow writes the
27
+ // resolution back via updatetask in the self-claim below.
28
+ const RESOLVED_WORKFLOW = inputs.resolved_workflow || null;
29
+ const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
30
+ const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"workflow\": \"" + RESOLVED_WORKFLOW + "\"" : "";
31
+
24
32
  // Config from args — backward-compatible fallbacks for manual launches
25
33
  const DASHBOARD_SLUG = inputs.dashboardSlug || "orchestra-dashboard";
26
34
  const crewHome = inputs.crewHome || "~/workspace/.jarvis";
@@ -315,8 +323,14 @@ function buildVisualCapturePlan(taskTitle, taskDescription, kind, captureTargets
315
323
  // opt-in — a missing or garbled line degrades to "unknown", which callers
316
324
  // treat as non-experiential. The task is never parked on a garbled line.
317
325
  async function resolveExperiential() {
318
- if (isExperiential === true) return "yes";
319
- if (isExperiential === false) return "no";
326
+ // Triage notes are immutable within a run: cache the resolved outcome —
327
+ // yes, no, AND unknown — so the dashboard lookup runs at most once per
328
+ // run. Without the unknown branch a missing experiential marker would
329
+ // re-fire the same "resolve-experiential-<taskId>" agent key on every
330
+ // Capture/Map entry and throw a duplicate replay key error.
331
+ if (experientialResolved !== null) return experientialResolved;
332
+ if (isExperiential === true) return (experientialResolved = "yes");
333
+ if (isExperiential === false) return (experientialResolved = "no");
320
334
  var expCheck = null;
321
335
  try {
322
336
  expCheck = await agent(
@@ -332,12 +346,14 @@ async function resolveExperiential() {
332
346
  );
333
347
  } catch (e) {
334
348
  log("resolveExperiential: agent call failed (" + (e && e.message ? e.message : e) + ") — treating as unknown");
335
- return "unknown";
349
+ experientialResolved = "unknown";
350
+ return experientialResolved;
336
351
  }
337
352
  var marker = extractExperiential(expCheck && expCheck.experiential_line ? expCheck.experiential_line : "");
338
- if (marker === true) return "yes";
339
- if (marker === false) return "no";
340
- return "unknown";
353
+ if (marker === true) experientialResolved = "yes";
354
+ else if (marker === false) experientialResolved = "no";
355
+ else experientialResolved = "unknown";
356
+ return experientialResolved;
341
357
  }
342
358
  // Baseline evidence status: reads the task's note events for the exact
343
359
  // protocol prefixes (explicit state, never English matching). Returns
@@ -447,6 +463,9 @@ let mapperSpec = "";
447
463
  // plan, and the Map-gate bounce counter (keeps agent stable-keys unique when
448
464
  // a Map-gate bounce re-runs Capture/Map in the same run).
449
465
  let isExperiential = null;
466
+ // Caches all three resolveExperiential() outcomes (yes/no/unknown); Triage
467
+ // notes are immutable within a run, so the lookup runs at most once.
468
+ let experientialResolved = null;
450
469
  let captureTargets = "";
451
470
  let mapGateBounceCount = 0;
452
471
  // Merge-time versioning: the release decision is extracted deterministically
@@ -535,7 +554,7 @@ while (i < STEPS.length) {
535
554
  "Find the task with id \"" + taskId + "\" in the returned tasks array.\n" +
536
555
  "Return exactly { \"project\": \"<the task's project field, or empty string if absent>\" } and nothing else.",
537
556
  {
538
- key: "project-check-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
557
+ key: "project-check-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : "") + (mapGateBounceCount > 0 ? "-g" + mapGateBounceCount : ""),
539
558
  label: "Checking project before " + step.name,
540
559
  schema: { type: "object", properties: { project: { type: "string" } }, required: ["project"] }
541
560
  }
@@ -659,7 +678,7 @@ while (i < STEPS.length) {
659
678
  if (isFirstClaim) {
660
679
  const claimResult = await agent(
661
680
  "Claim this task for the " + step.name + " step.\n" +
662
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\" }.\n" +
681
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\"" + CLAIM_WORKFLOW_PERSIST + " }.\n" +
663
682
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
664
683
  "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started\" }.\n" +
665
684
  "Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
@@ -1367,8 +1386,39 @@ while (i < STEPS.length) {
1367
1386
  return await parkTask("Publish verification failed: registry shows " + regVer + " but the release decision required " +
1368
1387
  publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land.");
1369
1388
  }
1370
- log("Publish verified for task " + taskId + ": registry at " + regVer);
1371
1389
  publishVerified = true;
1390
+ // Provenance refresh for self-publishes (task 7946d2a4): the npm path
1391
+ // installs and activates a new immutable release (crew-release.sh deploy
1392
+ // swaps the `current` symlink inside publish-npm.sh) but never stamped
1393
+ // the dashboard's provenance record — every crew release left
1394
+ // crew_release pointing at a pruned release. After a verified landed
1395
+ // publish, refresh the record's crew_release to the now-live release
1396
+ // identity, preserving the existing source_commit (the dashboard
1397
+ // artifact's build source — a crew-repo commit here would fail the
1398
+ // dashboard QA source check).
1399
+ try {
1400
+ var provRefresh = await agent(
1401
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getprovenance\", args: {}. " +
1402
+ "If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
1403
+ "Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
1404
+ "run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
1405
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"setprovenance\", args: " +
1406
+ "{ \"source_commit\": \"<existing provenance.source_commit>\", \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\", \"task_id\": \"" + taskId + "\" }. " +
1407
+ "Return JSON { \"refreshed\": <true if the setprovenance response contains ok: true, false otherwise>, \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\" } and nothing else.",
1408
+ { key: attemptKey("publish-provenance-refresh-" + taskId, totalReworkCount), label: "Refreshing dashboard provenance after crew release",
1409
+ schema: { type: "object", properties: { refreshed: { type: "boolean" }, reason: { type: "string" }, crew_release: { type: "string" }, published_at: { type: "string" } }, required: ["refreshed"] } }
1410
+ );
1411
+ if (provRefresh.refreshed) {
1412
+ log("Provenance refreshed for task " + taskId + ": crew_release " + provRefresh.crew_release);
1413
+ } else if (provRefresh.reason === "no-record") {
1414
+ log("Provenance refresh skipped for task " + taskId + ": no existing record to refresh (fresh instance — the dashboard's first artifact publish will create it)");
1415
+ } else {
1416
+ return await parkTask("Provenance refresh failed after a verified npm publish (crew_release " + (provRefresh.crew_release || "unknown") + "). The release is live but the dashboard record is stale — fail-closed.");
1417
+ }
1418
+ } catch (e) {
1419
+ return await parkTask("Provenance refresh failed: could not refresh dashboard provenance after a verified npm publish (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1420
+ }
1421
+
1372
1422
  } catch (e) {
1373
1423
  return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1374
1424
  }