muse-crew 0.6.6 → 0.6.7

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.
@@ -6,16 +6,36 @@ export const meta = {
6
6
 
7
7
  // Config from args — no instance constants
8
8
  const inputs = args ?? {};
9
- const DASHBOARD_SLUG = inputs.dashboardSlug;
10
9
  const crewHome = inputs.crewHome;
11
10
 
12
- if (!DASHBOARD_SLUG) throw new Error("dashboardSlug is required");
13
11
  if (!crewHome) throw new Error("crewHome is required");
12
+ // Crew API: the dispatcher calls the crew-owned CLI, not the dashboard.
13
+ // The CLI implements the API.md contract against $CREW_HOME/crew-state.db.
14
+ const CREW_API = crewHome + "/current/lib/crew-api.js";
15
+ // Build a shell command invoking the CLI. Args are JSON-encoded and
16
+ // single-quote-wrapped for safe shell passing. The agent runs this and
17
+ // returns the stdout verbatim (the CLI emits JSON on stdout).
18
+ function crewCmd(command, args) {
19
+ var json = JSON.stringify(args || {}).replace(/'/g, "'\\''");
20
+ return "node " + CREW_API + " --crew-home " + crewHome + " " + command + " --json '" + json + "'";
21
+ }
22
+ // Launchability invariant: workflow_launch only accepts workspace-contained
23
+ // scripts. crew-init's Gate 0 fails closed on any crewHome outside the
24
+ // workspace, so WORKFLOW_DIR below is always launchable — the dispatcher
25
+ // never needs to stage or copy scripts (the Gate 1 canary's ad-hoc copies
26
+ // are the failure mode this invariant eliminates).
14
27
  const WORKFLOW_DIR = crewHome + "/workflows";
15
28
 
16
29
  // Project config is built from dashboard project records (getdispatchstate)
17
30
  // — no separate registry file needed.
18
31
 
32
+ // Effective visual protocol for a launch. Crew default is off; a project
33
+ // opts in per-project with visual_protocol:true on its project record.
34
+ // null (inherit) and false (explicit off) both resolve to false here.
35
+ function effectiveVisualProtocol(project) {
36
+ return !!project && project.visual_protocol === true;
37
+ }
38
+
19
39
  // Per-poll processing cap. The launching cron times out at 300s; each
20
40
  // processed task costs roughly one agent() round trip, so cap the number of
21
41
  // tasks processed per poll and defer the rest to the next tick. Wall-clock
@@ -71,33 +91,151 @@ for (var wi = 0; wi < wfNames.length; wi++) {
71
91
  phase("dispatch");
72
92
 
73
93
  // ── 1. Read the board ────────────────────────────────────────────────
74
- const boardResult = await agent(
94
+ // The agent ferries the getdispatchstate stdout back as a RAW STRING — it
95
+ // must not parse, select, rewrite, summarize, paraphrase, or reformat the
96
+ // JSON. All board shaping happens deterministically in JS below
97
+ // (parseBoardJson, then unwrapBoardResult as defense-in-depth), so the LLM
98
+ // has no formatting discretion at all.
99
+ // NO schema on this call. A schema is a contract on a consumed return value
100
+ // only, and here the contract belongs on the deterministic parse, not on
101
+ // the agent's formatting choice. (Prompt/schema contradiction, 2026-09-12:
102
+ // the prompt demanded "stdout verbatim, byte-for-byte" — a string — while
103
+ // the schema demanded a top-level object with ready_tasks. The LLM resolved
104
+ // the contradiction non-deterministically: some ticks validated, some
105
+ // burned retries and blocked the whole dispatcher.)
106
+ const boardReturn = await agent(
75
107
  "Read the dispatch state.\n" +
76
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getdispatchstate\", args {}.\n" +
77
- "Return ONLY the following projection as JSON. Do not reproduce any other fields:\n" +
78
- "{\n" +
79
- " \"ready_tasks\": [ { \"id\", \"title\", \"description\", \"state\", \"project\", \"workflow\", \"blocked\", \"latest_session\": { \"id\", \"status\", \"step\", \"notes\" } or null } ],\n" +
80
- " \"projects\": [ { \"id\", \"simultaneity\", \"quiesced\", \"repo_path\", \"deploy_type\", \"deploy_slug\", \"description\" } ],\n" +
81
- " \"config\": { ... } // the config object as-is\n" +
82
- "}",
108
+ "Run in shell and return the stdout as a raw string:\n" + crewCmd("get-dispatch-state", {}) + "\n" +
109
+ "Return the command's stdout JSON as a plain string, byte-for-byte, unmodified. " +
110
+ "Do NOT parse the JSON — your return value must be the raw stdout string, never an object. " +
111
+ "Do not select fields, do not rewrite, summarize, paraphrase, or reformat anything. " +
112
+ "The result has keys ready_tasks, projects, config, counts ferry the string exactly as received.",
83
113
  {
84
114
  key: "read-board",
85
- label: "Reading board state",
86
- schema: {
87
- type: "object",
88
- properties: {
89
- ready_tasks: { type: "array" },
90
- projects: { type: "array" },
91
- config: { type: "object" }
92
- },
93
- required: ["ready_tasks"]
94
- }
115
+ label: "Reading board state"
95
116
  }
96
117
  );
97
118
 
98
- const allTasks = boardResult.ready_tasks || [];
99
- const config = boardResult.config || {};
100
- const projects = boardResult.projects || [];
119
+ // Deterministic board parse the read-board agent is told to return the
120
+ // CLI stdout as a raw string; the workflow parses it here. Fails closed:
121
+ // an unparseable return throws, never a silent empty task set.
122
+ function parseBoardJson(boardReturn) {
123
+ if (typeof boardReturn === "string") {
124
+ try {
125
+ return JSON.parse(boardReturn);
126
+ } catch (e) {
127
+ throw new Error("parseBoardJson: read-board stdout is not valid JSON: " + e.message);
128
+ }
129
+ }
130
+ // The agent may still hand back the parsed board object instead of the
131
+ // raw string (the non-deterministic behavior this fix removes from the
132
+ // contract). JSON values round-trip deterministically, so the object is
133
+ // accepted as-is — the LLM has no formatting discretion that affects the
134
+ // outcome, and every return shape converges on the same boardData.
135
+ if (boardReturn !== null && typeof boardReturn === "object") {
136
+ return boardReturn;
137
+ }
138
+ throw new Error(
139
+ "parseBoardJson: expected the read-board return to be a raw stdout string or a parsed board object, got " +
140
+ (boardReturn === null ? "null" : typeof boardReturn)
141
+ );
142
+ }
143
+
144
+ // Deterministic board projection — the ONLY place task records are shaped.
145
+ // Pure function, no I/O: covered by tests/board-projection.test.js. The
146
+ // retry field is dashboard-owned state (consecutive failures / rejections
147
+ // since reset, computed server-side); carrying it through here is what
148
+ // makes the retry cap below enforceable instead of fail-open.
149
+ function projectTaskRecord(t) {
150
+ if (!t || typeof t !== "object") return null;
151
+ var ls = (t.latest_session && typeof t.latest_session === "object") ? t.latest_session : null;
152
+ var retry = (t.retry && typeof t.retry === "object") ? t.retry : null;
153
+ var cf = retry ? retry.consecutive_failures : undefined;
154
+ var rsr = retry ? retry.rejections_since_reset : undefined;
155
+ return {
156
+ id: t.id,
157
+ title: t.title,
158
+ description: t.description,
159
+ state: t.state,
160
+ project: t.project,
161
+ workflow: t.workflow,
162
+ blocked: t.blocked,
163
+ deps: Array.isArray(t.deps) ? t.deps.slice() : [],
164
+ priority: t.priority,
165
+ latest_session: ls ? { id: ls.id, status: ls.status, step: ls.step, notes: ls.notes } : null,
166
+ retry: {
167
+ consecutive_failures: (typeof cf === "number" && cf >= 0) ? cf : 0,
168
+ rejections_since_reset: (typeof rsr === "number" && rsr >= 0) ? rsr : 0
169
+ }
170
+ };
171
+ }
172
+
173
+ // Unwrap the board result — the read-board agent may return the action
174
+ // result wrapped in an envelope (e.g. {status, result: {data: {...}}})
175
+ // instead of verbatim. We need ready_tasks at the top level.
176
+ // Fails closed: unknown envelopes throw instead of silently producing no tasks.
177
+ function unwrapBoardResult(boardResult) {
178
+ var boardData = boardResult;
179
+ // The read-board agent runs the CLI in shell and returns stdout. If it
180
+ // wraps the stdout in {"stdout": "<json string>"}, parse the inner JSON.
181
+ if (boardData && typeof boardData === 'object' && typeof boardData.stdout === 'string' && !Array.isArray(boardData.ready_tasks)) {
182
+ try {
183
+ var parsed = JSON.parse(boardData.stdout);
184
+ if (parsed && typeof parsed === 'object') {
185
+ boardData = parsed;
186
+ }
187
+ } catch (e) {
188
+ // Fall through to the fail-closed error below.
189
+ }
190
+ }
191
+ if (boardData && typeof boardData === 'object' && !Array.isArray(boardData.ready_tasks)) {
192
+ if (boardData.data && typeof boardData.data === 'object' && Array.isArray(boardData.data.ready_tasks)) {
193
+ boardData = boardData.data;
194
+ } else if (boardData.result && typeof boardData.result === 'object') {
195
+ var inner = boardData.result;
196
+ if (inner.data && typeof inner.data === 'object' && Array.isArray(inner.data.ready_tasks)) {
197
+ boardData = inner.data;
198
+ } else if (Array.isArray(inner.ready_tasks)) {
199
+ boardData = inner;
200
+ }
201
+ }
202
+ }
203
+ // The read-board agent may hand back the envelope with ready_tasks as a
204
+ // JSON string — {"ready_tasks": "<json string>"} — the get-dispatch-state
205
+ // stdout placed in the envelope instead of parsed (14:24 PDT tick,
206
+ // 2026-09-12). Parse it deterministically in JS: a string that parses to
207
+ // an array is used directly; a string that parses to an object is the
208
+ // whole board stdout, so re-run the unwrap on it. Unparseable or scalar
209
+ // strings fall through to the fail-closed throw below.
210
+ if (boardData && typeof boardData === 'object' && typeof boardData.ready_tasks === 'string') {
211
+ try {
212
+ var parsedReadyTasks = JSON.parse(boardData.ready_tasks);
213
+ if (Array.isArray(parsedReadyTasks)) {
214
+ boardData.ready_tasks = parsedReadyTasks;
215
+ } else if (parsedReadyTasks && typeof parsedReadyTasks === 'object') {
216
+ return unwrapBoardResult(parsedReadyTasks);
217
+ }
218
+ } catch (e) {
219
+ // Unparseable or scalar — fall through to fail-closed below.
220
+ }
221
+ }
222
+ // Fail closed: if we still don't have ready_tasks, the envelope is unknown.
223
+ // Silently returning [] would masquerade as "no tasks" — that's a lie.
224
+ if (!boardData || typeof boardData !== 'object' || !Array.isArray(boardData.ready_tasks)) {
225
+ throw new Error(
226
+ "unwrapBoardResult: unknown board envelope — expected ready_tasks at top level, " +
227
+ "in .data, in .result, in .result.data, or as a JSON string of a ready_tasks array " +
228
+ "or of a whole board object. Got keys: " +
229
+ (boardData && typeof boardData === 'object' ? Object.keys(boardData).join(",") : typeof boardData)
230
+ );
231
+ }
232
+ return boardData;
233
+ }
234
+
235
+ var boardData = unwrapBoardResult(parseBoardJson(boardReturn));
236
+ const allTasks = boardData.ready_tasks.map(projectTaskRecord).filter(function (t) { return t !== null; });
237
+ const config = boardData.config || {};
238
+ const projects = boardData.projects || [];
101
239
 
102
240
  // Default project: explicit arg, or first registered project
103
241
  const DEFAULT_PROJECT = inputs.defaultProject || (projects.length > 0 ? projects[0].id : "");
@@ -252,6 +390,16 @@ for (var t = 0; t < allTasks.length; t++) {
252
390
  continue;
253
391
  }
254
392
 
393
+ // Skip tasks on projects with no repo configured. Dispatching them
394
+ // would fail closed in the workflow (no silent fallback to another
395
+ // checkout); skip here with the reason so the board shows why the
396
+ // task never moves. Set repo_path via updateproject to re-enable.
397
+ var taskProjCfg = PROJECTS[taskProject];
398
+ if (!taskProjCfg || !taskProjCfg.repo_path) {
399
+ log("Skipped \"" + task.title + "\" — project " + taskProject + " has no repo_path configured");
400
+ continue;
401
+ }
402
+
255
403
  var latest = task.latest_session;
256
404
  var workflow = task.workflow || "standard";
257
405
  var steps = WORKFLOWS[workflow] || WORKFLOWS.standard;
@@ -372,13 +520,17 @@ if (parkJobs.length > 0) {
372
520
  var pj = parkJobs[pji];
373
521
  log("Parking \"" + pj.task.title + "\" — " + pj.message);
374
522
  parkSteps.push(
375
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"parktask\", args: { \"task_id\": \"" + pj.task.id + "\", \"message\": " + JSON.stringify(pj.message) + " }."
523
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("park-task", { task_id: pj.task.id, message: pj.message })
376
524
  );
377
525
  }
378
526
  try {
527
+ // Fire-and-forget: the park side effects are the contract and the agent's
528
+ // return is discarded, so no schema. A schema on an unconsumed return is
529
+ // pure failure surface — validation can only abort a call whose result
530
+ // nobody reads.
379
531
  await agent(
380
532
  "Park " + parkJobs.length + " task(s) for human attention, in order:\n\n" + parkSteps.join("\n\n"),
381
- { key: "park-batch", label: "Parking " + parkJobs.length + " task(s) at retry cap", schema: { type: "object" } }
533
+ { key: "park-batch", label: "Parking " + parkJobs.length + " task(s) at retry cap" }
382
534
  );
383
535
  log("Parked " + parkJobs.length + " task(s)");
384
536
  } catch (parkErr) {
@@ -472,12 +624,11 @@ if (eligible.length === 0) {
472
624
 
473
625
  var fileResult = await agent(
474
626
  "Advance the playtest cursors, then file the idle playtest task.\n" +
475
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updateconfig\", args: { \"key\": \"playtest." + ptProject + ".journey_cursor\", \"value\": \"" + nextJ + "\" }.\n" +
476
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updateconfig\", args: { \"key\": \"playtest." + ptProject + ".persona_cursor\", \"value\": \"" + nextP + "\" }.\n" +
477
- "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"createtask\", args: { \"title\": \"" + safeTitle + "\", \"description\": \"SEE BELOW\", \"project\": \"" + ptProject + "\", \"workflow\": \"standard\", \"filed_by\": \"hazel\" }.\n" +
478
- "For the description, use EXACTLY the following text (it is the playtest assignment do not paraphrase):\n" +
479
- "---DESCRIPTION START---\n" + safeDesc + "\n---DESCRIPTION END---\n" +
480
- "Return { \"task_id\": \"<the created task's id>\" }. If createtask fails, return { \"task_id\": \"\" }.",
627
+ "Run each of these in shell, in order:\n" +
628
+ crewCmd("update-config", { key: "playtest." + ptProject + ".journey_cursor", value: String(nextJ) }) + "\n" +
629
+ crewCmd("update-config", { key: "playtest." + ptProject + ".persona_cursor", value: String(nextP) }) + "\n" +
630
+ crewCmd("create-task", { title: safeTitle, description: safeDesc, project: ptProject, workflow: "standard", filed_by: "hazel" }) + "\n" +
631
+ "Return { \"task_id\": \"<the created task's id from the create-task stdout JSON>\" }. If create-task fails, return { \"task_id\": \"\" }.",
481
632
  {
482
633
  key: "file-playtest",
483
634
  label: "Filing idle playtest",
@@ -555,11 +706,15 @@ for (var p = 0; p < toProcess.length; p++) {
555
706
  var isteps = WORKFLOWS[iworkflow] || WORKFLOWS.standard;
556
707
 
557
708
  if (item.action === "complete") {
709
+ // Fire-and-forget: the update-task/log-event side effects are the contract
710
+ // and the agent's return is discarded, so no schema. A schema here is pure
711
+ // failure surface — and it contradicts the "return the stdout verbatim"
712
+ // prompt, since verbatim stdout is a string that {type:"object"} rejects.
558
713
  await agent(
559
714
  "Mark task done and log completion.\n" +
560
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + itask.id + "\", \"state\": \"done\" }.\n" +
561
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args: { \"task_id\": \"" + itask.id + "\", \"type\": \"completed\", \"message\": \"All workflow steps complete.\" }.",
562
- { key: "done-" + itask.id, label: "Completing: " + itask.title, schema: { type: "object" } }
715
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("update-task", { id: itask.id, state: "done" }) + "\n" +
716
+ "Then run in shell and return the stdout verbatim:\n" + crewCmd("log-event", { task_id: itask.id, type: "completed", message: "All workflow steps complete." }),
717
+ { key: "done-" + itask.id, label: "Completing: " + itask.title }
563
718
  );
564
719
  results.push({ task_id: itask.id, action: "completed" });
565
720
  continue;
@@ -586,8 +741,11 @@ for (var p = 0; p < toProcess.length; p++) {
586
741
  rejection_notes: item.rejectionNotes || "",
587
742
  project_config: projectCfg,
588
743
  project_id: taskProject,
589
- dashboardSlug: DASHBOARD_SLUG,
590
744
  crewHome: crewHome,
745
+ // Effective visual protocol for this run, resolved from the project's
746
+ // visual_protocol setting (null=inherits crew default=off). Workflows
747
+ // must not re-derive from project_config — the resolution lives here.
748
+ visual_protocol: effectiveVisualProtocol(projectCfg),
591
749
  // Facts the launched workflow needs to persist the resolution at claim time:
592
750
  // the workflow name the dispatcher actually resolved and is launching, and
593
751
  // whether the task record had no workflow (playtest filings carry explicit
@@ -606,9 +764,15 @@ for (var p = 0; p < toProcess.length; p++) {
606
764
  // Single acknowledge path: always ack, even when nothing was processed or
607
765
  // the per-poll cap deferred tasks. acknowledge_poll is idempotent (it only updates
608
766
  // last_poll_at), so a partial ack followed by a later tick's ack is harmless.
767
+ // Fire-and-forget: the acknowledge-poll side effect is the whole contract and
768
+ // the agent's return is discarded, so no schema. A schema here is pure failure
769
+ // surface — and it contradicts the "return the stdout verbatim" prompt, since
770
+ // verbatim stdout is a string that {type:"object"} rejects. (2026-09-11: the
771
+ // agent returned the stdout as a string per the prompt, validation rejected it,
772
+ // and the entire dispatcher aborted.)
609
773
  await agent(
610
- "Acknowledge the poll.\nCall artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"acknowledge_poll\", args {}.",
611
- { key: "ack", label: "Acknowledging poll", schema: { type: "object" } }
774
+ "Acknowledge the poll.\nRun in shell and return the stdout verbatim:\n" + crewCmd("acknowledge-poll", {}),
775
+ { key: "ack", label: "Acknowledging poll" }
612
776
  );
613
777
 
614
778
  var recommended = [];
@@ -1,10 +1,9 @@
1
1
  export const meta = {
2
2
  name: "crew-init",
3
- description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, register dashboard project, set up cron jobs. Idempotent — safe to re-run.",
3
+ description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, set up cron jobs. Idempotent — safe to re-run.",
4
4
  phases: [
5
5
  { name: "release", title: "Bootstrap release system" },
6
6
  { name: "scaffold", title: "Scaffold orchestration directory" },
7
- { name: "project", title: "Register dashboard project" },
8
7
  { name: "crons", title: "Create and converge cron jobs" }
9
8
  ]
10
9
  };
@@ -13,16 +12,89 @@ export const meta = {
13
12
  const inputs = args ?? {};
14
13
  const crewRepoPath = inputs.crewRepoPath;
15
14
  const crewHome = inputs.crewHome;
16
- const dashboardSlug = inputs.dashboardSlug;
17
- const dashboardName = inputs.dashboardName || "Muse Crew";
18
15
  const cronIds = inputs.cronIds || {};
19
16
 
20
17
  if (!crewRepoPath) throw new Error("crewRepoPath is required — path to muse-crew (git checkout or npm install)");
21
- if (!crewHome) throw new Error("crewHome is required — e.g. ~/.crew");
22
- if (!dashboardSlug) throw new Error("dashboardSlug is required — create the dashboard artifact first, then pass its slug here");
18
+ if (!crewHome) throw new Error("crewHome is required — e.g. ~/workspace/.crew (must be inside the workspace)");
23
19
 
24
20
  const orchDir = crewHome + "/.orchestration";
25
21
 
22
+ // ── Gate 0: Launchable-path + repo validation ───────────────────────
23
+ // workflow_launch only accepts workspace-contained scripts. A crewHome
24
+ // outside the workspace would produce an instance whose workflows can
25
+ // never launch (the Gate 1 canary's /home/hatch/.crew-canary-gate1
26
+ // defect, 2026-09-11: workers resorted to changing, manually staged
27
+ // copies). Fail closed here so no such instance can ever be created.
28
+ // The dashboard repo is validated too: init registers the dashboard
29
+ // project against it, and a non-git path would only fail later at the
30
+ // first Build (the canary's npm-package repo_path defect).
31
+ // Gate 0 decision logic is pure JS — the agent is a sensor, not a judge.
32
+ // It reports raw facts (HOME, expanded paths, the git rev-parse exit
33
+ // code); the launchable/repo_valid verdicts are computed here,
34
+ // deterministically. An agent asked for a verdict can mis-apply the
35
+ // prefix rule (e.g. the "$ws-evil" trailing-slash trick); code cannot.
36
+ // (Prompt hardening is a smell — this is the mechanical version.)
37
+ function expandTilde(p, home) {
38
+ if (p === "~") return home;
39
+ if (p.indexOf("~/") === 0) return home + p.slice(1);
40
+ return p;
41
+ }
42
+ function gate0Decide(facts) {
43
+ // facts: { home, crewHomeExpanded, gitExitCode }
44
+ var workspace = facts.home + "/workspace";
45
+ var launchable = facts.crewHomeExpanded.indexOf(workspace + "/") === 0;
46
+ var repoValid = facts.gitExitCode === 0;
47
+ return { workspace: workspace, launchable: launchable, repoValid: repoValid };
48
+ }
49
+ var gateFacts;
50
+ try {
51
+ gateFacts = await agent(
52
+ "Report raw facts about the requested crew home. Do NOT judge it — just report.\\n\\n" +
53
+ "Requested crewHome: " + crewHome + "\\n\\n" +
54
+ "Steps (run in shell):\\n" +
55
+ "1. home=$(echo $HOME)\\n" +
56
+ "2. Expand a leading ~/ in the requested crewHome against $HOME (leave other paths untouched).\\n" +
57
+ "3. Run: git -C <expanded crewHome> rev-parse --git-dir; echo EXIT:$?\\n" +
58
+ "4. Return JSON { home, crewHomeExpanded, gitExitCode }.\\n\\n" +
59
+ "Do not create anything. This is a read-only check.",
60
+ {
61
+ key: "gate-0",
62
+ label: "Validate crew home",
63
+ schema: {
64
+ type: "object",
65
+ properties: {
66
+ home: { type: "string" },
67
+ crewHomeExpanded: { type: "string" },
68
+ gitExitCode: { type: "number" }
69
+ },
70
+ required: ["home", "crewHomeExpanded", "gitExitCode"]
71
+ }
72
+ }
73
+ );
74
+ } catch (e) {
75
+ return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Pre-init validation failed", message: String(e.message || e) } };
76
+ }
77
+ var gateResult = gate0Decide(gateFacts);
78
+ if (!gateResult.launchable) {
79
+ return {
80
+ __hatchWorkflowControl: "blocked",
81
+ result: {
82
+ blocked_reason: "crewHome is not workspace-contained",
83
+ message: "crewHome must be inside the workspace (" + gateResult.workspace + ") because workflow_launch only accepts workspace-contained scripts. Requested: " + gateFacts.crewHomeExpanded + ". Pick a home like " + gateResult.workspace + "/.crew and re-run init."
84
+ }
85
+ };
86
+ }
87
+ if (!gateResult.repoValid) {
88
+ return {
89
+ __hatchWorkflowControl: "blocked",
90
+ result: {
91
+ blocked_reason: "crewHome is not a git repository",
92
+ message: "crewHome (" + gateFacts.crewHomeExpanded + ") is not a valid git repository (git exit code " + gateFacts.gitExitCode + "). Initialize it with git init or pick an existing repo."
93
+ }
94
+ };
95
+ }
96
+ log("Gate 0: crewHome " + gateFacts.crewHomeExpanded + " is workspace-contained and a valid git repo");
97
+
26
98
  // ── Phase 1: Release ──────────────────────────────────────────────────
27
99
  phase("release");
28
100
  var releaseResult;
@@ -104,48 +176,9 @@ var scaffoldCreated = scaffoldResult.created ? scaffoldResult.created.length : 0
104
176
  var scaffoldSkipped = scaffoldResult.skipped ? scaffoldResult.skipped.length : 0;
105
177
  log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped");
106
178
 
107
- // ── Phase 3: Register dashboard as project ────────────────────────────
108
- // The dashboard IS the project. Register it so the crew knows what to work on.
109
- phase("project");
110
- var projectResult;
111
- try {
112
- projectResult = await agent(
113
- "Register the dashboard as a project in its own database.\n\n" +
114
- "Dashboard slug: " + dashboardSlug + "\n\n" +
115
- "Steps:\n" +
116
- "1. Call artifact_invoke_action with:\n" +
117
- " slug: '" + dashboardSlug + "'\n" +
118
- " action_name: 'createproject'\n" +
119
- " args: {\n" +
120
- " id: '" + dashboardSlug + "',\n" +
121
- " display_name: '" + dashboardName + "',\n" +
122
- " repo_path: '" + crewRepoPath + "',\n" +
123
- " deploy_type: 'artifact',\n" +
124
- " deploy_slug: '" + dashboardSlug + "',\n" +
125
- " description: 'The Muse Crew dashboard — task board, activity feed, and crew controls'\n" +
126
- " }\n" +
127
- "2. Return { registered: true } on success.\n\n" +
128
- "Return JSON with registered (boolean).",
129
- {
130
- key: "project-1",
131
- label: "Register dashboard project",
132
- schema: {
133
- type: "object",
134
- properties: {
135
- registered: { type: "boolean" }
136
- },
137
- required: ["registered"]
138
- }
139
- }
140
- );
141
- } catch (e) {
142
- return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Project registration failed", message: String(e.message || e) } };
143
- }
144
- log("Project: " + (projectResult.registered ? "registered" : "failed"));
145
-
146
- // ── Phase 4: Cron jobs (declarative manifest) ──────────────────────────
179
+ // ── Phase 3: Cron jobs (declarative manifest) ──────────────────────────
147
180
  // The manifest lives in the repo at seed/crons.json. Body templates live
148
- // in seed/ with {crewHome}/{dashboardSlug} placeholders. Missing jobs are
181
+ // in seed/ with {crewHome} placeholders. Missing jobs are
149
182
  // created from the manifest; existing jobs converge to it — except `enabled`,
150
183
  // which is a creation-time default only and is never touched on update.
151
184
  phase("crons");
@@ -155,7 +188,6 @@ try {
155
188
  "Ensure the Muse Crew cron jobs match the repo's declarative manifest.\n\n" +
156
189
  "Manifest: " + crewRepoPath + "/seed/crons.json\n" +
157
190
  "crewHome: " + crewHome + "\n" +
158
- "Dashboard slug: " + dashboardSlug + "\n" +
159
191
  "Id overrides (manifest id -> live id): " + JSON.stringify(cronIds) + "\n\n" +
160
192
  "Steps:\n" +
161
193
  "1. Read the manifest at " + crewRepoPath + "/seed/crons.json and parse it as JSON.\n" +
@@ -165,14 +197,13 @@ try {
165
197
  "2. For each manifest entry, in order:\n" +
166
198
  " a. The live id is the override for entry.id when present, else entry.id.\n" +
167
199
  " b. Read the body template at " + crewRepoPath + "/seed/<entry.body_template>.\n" +
168
- " c. Replace all occurrences of {crewHome} with: " + crewHome + " and all\n" +
169
- " occurrences of {dashboardSlug} with: " + dashboardSlug + ".\n" +
200
+ " c. Replace all occurrences of {crewHome} with: " + crewHome + ".\n" +
170
201
  " d. Call cron_list and look for a job with the live id.\n" +
171
202
  " e. If missing, call cron_add with:\n" +
172
203
  " - id: the live id\n" +
173
204
  " - title, enabled, mode from the entry\n" +
174
205
  " - schedule: the entry's schedule object\n" +
175
- " - owner: the entry's owner with {dashboardSlug} replaced by " + dashboardSlug + "\n" +
206
+ " - owner: the entry's owner\n" +
176
207
  " - timeout_secs: the entry's timeout_secs when present\n" +
177
208
  " - body: the resolved template text\n" +
178
209
  " Record action 'created' with no updated fields.\n" +
@@ -231,10 +262,7 @@ log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" +
231
262
  return {
232
263
  message: "Muse Crew initialized.",
233
264
  crewHome: crewHome,
234
- dashboardSlug: dashboardSlug,
235
- dashboardName: dashboardName,
236
265
  releaseHash: releaseResult.hash,
237
266
  scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
238
- project: projectResult.registered ? "registered" : "failed",
239
267
  crons: cronsResult.summary.crons
240
268
  };