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.
- package/AGENTS.md +1 -1
- package/API.md +53 -9
- package/docs/guide.md +9 -6
- package/docs/publish-verification.md +205 -0
- package/lib/AGENTS.md +5 -5
- package/lib/crew-api.js +1101 -0
- package/lib/crew-release.sh +19 -0
- package/lib/merge-lock.sh +18 -5
- package/lib/publish-npm.sh +6 -5
- package/lib/schema.sql +101 -0
- package/lib/test-merge-lock.sh +13 -0
- package/lib/test-publish-skip.sh +2 -1
- package/lib/test-terminal-cleanup.sh +131 -0
- package/lib/test-verify-merge.sh +1 -0
- package/lib/test-worktree-backend.sh +74 -0
- package/lib/worktree-lifecycle.sh +164 -16
- package/package.json +2 -2
- package/seed/AGENTS.md +0 -1
- package/seed/crons.json +0 -13
- package/workflows/AGENTS.md +4 -2
- package/workflows/bugfix.js +987 -193
- package/workflows/chore.js +936 -177
- package/workflows/crew-dispatch.js +201 -37
- package/workflows/crew-init.js +82 -54
- package/workflows/docs.js +117 -48
- package/workflows/standard.js +970 -195
- package/workflows/tests/read-board-parse.test.mjs +162 -0
- package/lib/orphan-sweep.sh +0 -287
- package/lib/test-orphan-sweep.sh +0 -276
- package/seed/cron-sweep-body-template.md +0 -11
|
@@ -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
|
-
|
|
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
|
-
"
|
|
77
|
-
"Return
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
"
|
|
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"
|
|
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
|
-
"
|
|
476
|
-
"
|
|
477
|
-
"
|
|
478
|
-
"
|
|
479
|
-
"
|
|
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
|
-
"
|
|
561
|
-
"
|
|
562
|
-
{ key: "done-" + itask.id, label: "Completing: " + itask.title
|
|
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.\
|
|
611
|
-
{ key: "ack", label: "Acknowledging poll"
|
|
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 = [];
|
package/workflows/crew-init.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
export const meta = {
|
|
2
2
|
name: "crew-init",
|
|
3
|
-
description: "Initialize Muse Crew: bootstrap release, scaffold orchestration,
|
|
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.
|
|
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:
|
|
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}
|
|
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 + "
|
|
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
|
|
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
|
};
|