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.
@@ -50,6 +50,42 @@ const MAX_TASKS_PER_POLL = inputs.maxTasksPerPoll || 6;
50
50
  // inputs.registry. Fall back to the agent() load only when the caller
51
51
  // predates the registry — slow, but never broken.
52
52
  var registryResult = inputs.registry;
53
+ // Nested-registry seam (2026-09-20): a malformed nested registry object
54
+ // (e.g. a workflow key mapping to null/undefined, or a steps entry without
55
+ // a name) used to die deep in the lookup tables with a cryptic TypeError
56
+ // ("cannot convert 'null' or 'undefined' to object"). Validate the shape
57
+ // deterministically first; a malformed registry falls back to the slow
58
+ // agent load with a loud log, exactly like a missing one — the defect is
59
+ // named, never a crash.
60
+ function validateRegistry(reg) {
61
+ var wfNames = ["standard", "bugfix", "chore", "docs", "upgrade"];
62
+ if (!reg || typeof reg !== "object" || Array.isArray(reg)) {
63
+ return { ok: false, reason: "registry is missing or not an object" };
64
+ }
65
+ for (var i = 0; i < wfNames.length; i++) {
66
+ var wf = reg[wfNames[i]];
67
+ if (!wf || typeof wf !== "object") {
68
+ return { ok: false, reason: "workflow '" + wfNames[i] + "' is missing or not an object" };
69
+ }
70
+ if (!Array.isArray(wf.steps)) {
71
+ return { ok: false, reason: "workflow '" + wfNames[i] + "' has no steps array" };
72
+ }
73
+ for (var j = 0; j < wf.steps.length; j++) {
74
+ if (!wf.steps[j] || typeof wf.steps[j].name !== "string") {
75
+ return { ok: false, reason: "workflow '" + wfNames[i] + "' step " + j + " has no name" };
76
+ }
77
+ }
78
+ if (typeof wf.reworkTarget !== "string") {
79
+ return { ok: false, reason: "workflow '" + wfNames[i] + "' has no reworkTarget string" };
80
+ }
81
+ }
82
+ return { ok: true };
83
+ }
84
+ var registryCheck = validateRegistry(registryResult);
85
+ if (!registryCheck.ok) {
86
+ log("WARNING: registry failed shape validation (" + registryCheck.reason + ") — falling back to slow registry load");
87
+ registryResult = null;
88
+ }
53
89
  if (!registryResult) {
54
90
  log("WARNING: no registry in args — falling back to slow registry load");
55
91
  registryResult = await agent(
@@ -58,6 +58,29 @@ if (!crewHome) throw new Error("crewHome is required — e.g. ~/workspace/.crew
58
58
  // create projects later via crew-api.js). One without the other is an error.
59
59
  if (!!dashboardSlug !== !!dashboardRepoPath) throw new Error("dashboardSlug and dashboardRepoPath must be provided together, or neither (omit both for a CLI-only install without a dashboard)");
60
60
 
61
+ // ── Cron id override validation (2026-09-20, 0.14.6 REVIEW) ──
62
+ // Uninstall's discovery fallback only recognizes live ids that start with
63
+ // a manifest entry id + "-" (crew-poll-, crew-ack-scan-,
64
+ // crew-update-watch-). An override that breaks that convention would
65
+ // survive uninstall if .cron-registry.json were ever lost — so overrides
66
+ // must keep the manifest-id prefix. Fail closed at init, before any cron
67
+ // is created.
68
+ for (const manifestId of Object.keys(cronIds)) {
69
+ const liveId = cronIds[manifestId];
70
+ if (typeof liveId !== "string" || !liveId.startsWith(manifestId + "-")) {
71
+ return {
72
+ __hatchWorkflowControl: "blocked",
73
+ result: {
74
+ blocked_reason: "Invalid cronIds override",
75
+ message: "cronIds[\"" + manifestId + "\"] must start with \"" + manifestId + "-\" — got " +
76
+ JSON.stringify(liveId) + ". Uninstall's discovery fallback only recognizes live ids under " +
77
+ "the manifest-id prefix; an arbitrary id could survive uninstall if the cron registry " +
78
+ "were lost. Re-run init with a prefixed id."
79
+ }
80
+ };
81
+ }
82
+ }
83
+
61
84
  const orchDir = crewHome + "/.orchestration";
62
85
 
63
86
  // ── Dependency node_modules resolution (clean-room defect 2026-09-16) ──
@@ -158,11 +158,14 @@ try {
158
158
  "Steps:\n" +
159
159
  "1. Registry: if " + sq(crewHomeExpanded + "/.cron-registry.json") + " exists, read it\n" +
160
160
  " and take its crons[].id list as registry candidates.\n" +
161
- "2. Discovery: call cron_list. For every job whose id starts with 'crew-poll-',\n" +
161
+ "2. Discovery: call cron_list. For every job whose id starts with one of\n" +
162
+ " 'crew-poll-', 'crew-ack-scan-', or 'crew-update-watch-' (the three\n" +
163
+ " seed/crons.json manifest entry ids, each suffixed with the instance id),\n" +
162
164
  " call cron_view and keep it as a discovery candidate when the job body\n" +
163
- " contains the crewHome string '" + crewHomeExpanded + "' (the poll body template\n" +
164
- " embeds the crew home path). The bare id 'crew-poll' with no instance suffix\n" +
165
- " is never a live crew job — never remove it.\n" +
165
+ " contains the crewHome string '" + crewHomeExpanded + "' (the cron body templates\n" +
166
+ " embed the crew home path). A bare manifest id ('crew-poll', 'crew-ack-scan',\n" +
167
+ " 'crew-update-watch') with no instance suffix is never a live crew job —\n" +
168
+ " never remove it.\n" +
166
169
  "3. Removal list = registry candidates ∪ discovery candidates, deduped.\n" +
167
170
  " SAFETY: remove only jobs on this list. When in doubt about any job, fail\n" +
168
171
  " closed (passed: false) instead of guessing.\n" +