muse-crew 0.14.6 → 0.14.8

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.
@@ -46,10 +46,43 @@ const MAX_TASKS_PER_POLL = inputs.maxTasksPerPoll || 6;
46
46
 
47
47
  // Workflow step registry: static per release. The poll cron body reads
48
48
  // <crewHome>/workflows/registry.json (generated at release time from the
49
- // workflow files, which stay the single source of truth) and passes it as
50
- // inputs.registry. Fall back to the agent() load only when the caller
51
- // predates the registry — slow, but never broken.
52
- var registryResult = inputs.registry;
49
+ // workflow files, which stay the single source of truth) and ferries the
50
+ // RAW FILE TEXT as inputs.registryText. The dispatcher parses it
51
+ // deterministically — the poller LLM never parses-and-re-emits a nested
52
+ // object into the tool call. (Room #26: 15+ "schema validation" launch
53
+ // rejections 09-19→09-21 from the LLM garbling the nested object, plus one
54
+ // hallucinated-but-shape-valid registry that silently drove dispatch with
55
+ // wrong step tables.) A string arg is trivially well-formed at the
56
+ // tool-call boundary — the same ferry pattern the board already uses.
57
+ // Fall back to the agent() load only when the caller predates the registry
58
+ // — slow, but never broken.
59
+ function parseRegistryText(text) {
60
+ if (text === undefined || text === null) {
61
+ return { ok: false, reason: "registryText is missing" };
62
+ }
63
+ if (typeof text !== "string") {
64
+ return { ok: false, reason: "registryText is not a string" };
65
+ }
66
+ try {
67
+ return { ok: true, registry: JSON.parse(text) };
68
+ } catch (e) {
69
+ return { ok: false, reason: "registryText is not valid JSON: " + e.message };
70
+ }
71
+ }
72
+ var registryResult = null;
73
+ var registryTextResult = parseRegistryText(inputs.registryText);
74
+ if (registryTextResult.ok) {
75
+ registryResult = registryTextResult.registry;
76
+ } else if (inputs.registryText !== undefined && inputs.registryText !== null) {
77
+ // Present but unusable: name the defect, then fall through to the slow load.
78
+ log("WARNING: " + registryTextResult.reason + " — falling back to slow registry load");
79
+ } else if (inputs.registry !== undefined && inputs.registry !== null) {
80
+ // Legacy: pre-string-ferry poller prompts passed the parsed object.
81
+ // Same validation applies; a garbled object falls through to the slow
82
+ // load exactly like a missing one.
83
+ log("WARNING: legacy inputs.registry object received — prefer the registryText string ferry");
84
+ registryResult = inputs.registry;
85
+ }
53
86
  // Nested-registry seam (2026-09-20): a malformed nested registry object
54
87
  // (e.g. a workflow key mapping to null/undefined, or a steps entry without
55
88
  // a name) used to die deep in the lookup tables with a cryptic TypeError
@@ -78,6 +111,18 @@ function validateRegistry(reg) {
78
111
  if (typeof wf.reworkTarget !== "string") {
79
112
  return { ok: false, reason: "workflow '" + wfNames[i] + "' has no reworkTarget string" };
80
113
  }
114
+ // Content pin (room #26): a shape-valid registry can still be
115
+ // hallucinated — 2026-09-21 a poller ferried docs.reworkTarget="Build"
116
+ // (docs.js has no Build step) and it passed shape validation, silently
117
+ // driving dispatch with wrong step tables. The rework target must name
118
+ // a real step of its own workflow; this invariant is release-independent.
119
+ var stepNames = {};
120
+ for (var j = 0; j < wf.steps.length; j++) {
121
+ stepNames[wf.steps[j].name] = true;
122
+ }
123
+ if (!stepNames[wf.reworkTarget]) {
124
+ return { ok: false, reason: "workflow '" + wfNames[i] + "' reworkTarget '" + wf.reworkTarget + "' names no step" };
125
+ }
81
126
  }
82
127
  return { ok: true };
83
128
  }
@@ -113,6 +158,14 @@ if (!registryResult) {
113
158
  }
114
159
  }
115
160
  );
161
+ // The slow agent load is the same untrusted ferry as the args path — a
162
+ // hallucinated registry would silently drive dispatch with wrong step
163
+ // tables. Re-run the existing validation on the slow-loaded result and
164
+ // fail closed with the same reason; there is no second fallback.
165
+ var slowCheck = validateRegistry(registryResult);
166
+ if (!slowCheck.ok) {
167
+ throw new Error("registry slow load failed validation (" + slowCheck.reason + ") — cannot derive step tables; aborting dispatch");
168
+ }
116
169
  }
117
170
 
118
171
  // Derive lookup tables from canonical definitions
package/workflows/docs.js CHANGED
@@ -543,7 +543,17 @@ function describeWorkAgentFailure(stepName, identity, attempts) {
543
543
  "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
544
544
  task_id: taskId,
545
545
  session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: "failed", notes: "Worker report had no single unambiguous VERDICT: PASS/FAIL line (bounded re-ask exhausted)" },
546
- event: { task_id: taskId, type: "failed", message: step.name + " verdict line missing or ambiguous, re-ask exhausted — phase failed, dispatcher will retry" }
546
+ event: { task_id: taskId, type: "failed", message: step.name + " verdict line missing or ambiguous, re-ask exhausted — phase failed, dispatcher will retry" },
547
+ // Room #26 blocker 33: INDETERMINATE verdict record — the report
548
+ // could not be read at all, but its full text is still preserved
549
+ // as grounds. (docs has Review as its only verdict step.)
550
+ verdict: {
551
+ step: "Review",
552
+ attempt: reworkCount,
553
+ reviewer: step.identity,
554
+ verdict: "INDETERMINATE",
555
+ grounds: workerText
556
+ }
547
557
  }) + "\n",
548
558
  { key: "record-block-" + step.name, label: "Recording verdict failure" }
549
559
  );
@@ -578,7 +588,19 @@ function describeWorkAgentFailure(stepName, identity, attempts) {
578
588
  "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
579
589
  task_id: taskId,
580
590
  session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: status, notes: summary },
581
- event: { task_id: taskId, type: status, identity: step.identity, message: step.name + " " + status + " by " + step.identity }
591
+ event: { task_id: taskId, type: status, identity: step.identity, message: step.name + " " + status + " by " + step.identity },
592
+ // Room #26 blocker 33: structured, non-lossy Review verdict record.
593
+ // The session note above is truncated; the verdict row carries the
594
+ // full worker report as grounds, written in the same transaction.
595
+ // attempt is the rework round (0 = first Review). (docs has Review
596
+ // as its only verdict step, so verdictPassed is always set here.)
597
+ verdict: (verdictPassed !== null ? {
598
+ step: "Review",
599
+ attempt: reworkCount,
600
+ reviewer: step.identity,
601
+ verdict: verdictPassed ? "PASS" : "FAIL",
602
+ grounds: workerText
603
+ } : null)
582
604
  }) + "\n",
583
605
  {
584
606
  key: "record-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),