muse-crew 0.7.0 → 0.7.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 +21 -0
- package/README.md +17 -1
- package/docs/guide.md +11 -2
- package/lib/crew-api.js +44 -0
- package/lib/schema.sql +17 -0
- package/package.json +1 -1
- package/seed/AGENTS.md +1 -0
- package/seed/cron-body-template.md +4 -2
- package/seed/sample-project.tar.gz +0 -0
- package/workflows/AGENTS.md +1 -1
- package/workflows/bugfix.js +1 -0
- package/workflows/chore.js +1 -0
- package/workflows/crew-dispatch.js +17 -0
- package/workflows/crew-init.js +126 -30
- package/workflows/standard.js +1 -0
package/API.md
CHANGED
|
@@ -190,6 +190,27 @@ Two namespaces, not one: the vocabulary above is the **dashboard API** (task sta
|
|
|
190
190
|
|
|
191
191
|
Record that a poll tick occurred. Takes no arguments. Used by the dispatcher at the end of each cycle to update the last-polled timestamp.
|
|
192
192
|
|
|
193
|
+
### `reserve-dispatch`
|
|
194
|
+
|
|
195
|
+
Create a dispatch reservation for a task after the cron worker launches its workflow. The dispatcher skips reserved tasks on subsequent ticks — without this, a task whose workflow takes longer than the poll interval to self-claim would be re-dispatched every tick.
|
|
196
|
+
|
|
197
|
+
| Field | Type | Notes |
|
|
198
|
+
|-------|------|-------|
|
|
199
|
+
| `task_id` | string | Required. The task that was dispatched. |
|
|
200
|
+
| `run_id` | string | Required. The workflow run ID returned by `workflow_launch_async`. |
|
|
201
|
+
| `workflow` | string | Required. Which workflow was launched (`standard`, `bugfix`, `chore`). |
|
|
202
|
+
| `ttl_seconds` | integer | Optional. Reservation lifetime, clamped to 60–3600 seconds. Default 900 (15 minutes). |
|
|
203
|
+
|
|
204
|
+
Upserts: re-reserving the same task replaces the existing reservation. Returns the reservation with `dispatched_at` and `expires_at` timestamps.
|
|
205
|
+
|
|
206
|
+
### `clear-reservation`
|
|
207
|
+
|
|
208
|
+
Remove a task's dispatch reservation. Called by the workflow after a successful self-claim. Takes `task_id` (required). Returns `{ ok: true, cleared: <boolean> }` — `cleared` is false if no reservation existed (idempotent).
|
|
209
|
+
|
|
210
|
+
### `list-reservations`
|
|
211
|
+
|
|
212
|
+
List all active (unexpired) dispatch reservations, ordered by dispatch time. Takes no arguments. The dispatcher consumes this via `getdispatchstate` (which includes `reservations`) rather than calling it directly.
|
|
213
|
+
|
|
193
214
|
---
|
|
194
215
|
|
|
195
216
|
## Projects
|
package/README.md
CHANGED
|
@@ -58,7 +58,8 @@ workflow_launch with:
|
|
|
58
58
|
args: {
|
|
59
59
|
crewRepoPath: "<install-path>",
|
|
60
60
|
crewHome: "~/workspace/.jarvis",
|
|
61
|
-
dashboardSlug: "orchestra-dashboard"
|
|
61
|
+
dashboardSlug: "orchestra-dashboard",
|
|
62
|
+
dashboardRepoPath: "<path-to-your-dashboard-clone>"
|
|
62
63
|
}
|
|
63
64
|
```
|
|
64
65
|
|
|
@@ -66,6 +67,21 @@ For the full reference — optional arguments, what init creates, and how to ver
|
|
|
66
67
|
|
|
67
68
|
The dashboard becomes the crew's first project — the crew works on itself.
|
|
68
69
|
|
|
70
|
+
### CLI-only install (no dashboard)
|
|
71
|
+
|
|
72
|
+
The dashboard is optional. For a CLI-only install, omit `dashboardSlug` and `dashboardRepoPath` together:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
workflow_launch with:
|
|
76
|
+
scriptPath: "<install-path>/workflows/crew-init.js"
|
|
77
|
+
args: {
|
|
78
|
+
crewRepoPath: "<install-path>",
|
|
79
|
+
crewHome: "~/workspace/.my-crew"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Init skips project registration; create projects later via the Crew API CLI (`node <install-path>/lib/crew-api.js --crew-home ~/workspace/.my-crew create-project --json '{...}'`). The polling cron is still created, owned by `cli:<crew-home-basename>`. See the Guide for the full CLI workflow.
|
|
84
|
+
|
|
69
85
|
## Documentation
|
|
70
86
|
|
|
71
87
|
- [**Guide**](docs/guide.md) — setup, project registration, task filing, the loop, pause/resume, releases, and current limitations
|
package/docs/guide.md
CHANGED
|
@@ -44,18 +44,26 @@ workflow_launch with:
|
|
|
44
44
|
args: {
|
|
45
45
|
crewRepoPath: "<path to muse-crew package or repo>",
|
|
46
46
|
crewHome: "~/workspace/.jarvis",
|
|
47
|
-
dashboardSlug: "orchestra-dashboard"
|
|
47
|
+
dashboardSlug: "orchestra-dashboard",
|
|
48
|
+
dashboardRepoPath: "<path to your dashboard repo clone>"
|
|
48
49
|
}
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
Required arguments:
|
|
52
53
|
- `crewRepoPath` — path to the `muse-crew` source. This can be an npm install (e.g., `node_modules/muse-crew`) or a Git checkout.
|
|
53
54
|
- `crewHome` — directory where the crew's runtime state lives.
|
|
55
|
+
|
|
56
|
+
Dashboard arguments (optional as a pair — provide both or neither):
|
|
54
57
|
- `dashboardSlug` — slug of the task service artifact (must already exist).
|
|
58
|
+
- `dashboardRepoPath` — path to the dashboard's git repository (your local clone from the Prerequisites). Must be a real git repo inside the workspace. Init registers the dashboard project against this path and the crew's worktree lifecycle works from it — it is never the crew home, and init will never create it for you.
|
|
59
|
+
|
|
60
|
+
Omit both dashboard arguments for a CLI-only install: init skips project registration (create projects later with `crew-api.js create-project`), and the polling cron is owned by `cli:<crew-home-basename>` instead of `space:<slug>`. The crew never requires a dashboard.
|
|
55
61
|
|
|
56
62
|
Optional arguments:
|
|
57
63
|
- `dashboardName` — display name for the project registration (default: `"Muse Crew"`).
|
|
64
|
+
- `crewName` — the human's chosen name for the crew. Stored as plain text at `$CREW_HOME/crew-name` and returned in the init summary. The setup conversation should always ask for one; init won't fail without it.
|
|
58
65
|
- `cronIds` — manifest id → live id map for parallel instances on one account (default: `{}`). The manifest's ids are used as-is unless overridden, e.g. `{"crew-poll": "crew-poll-canary"}`.
|
|
66
|
+
- `cronOwner` — explicit owner for the created cron jobs. Defaults to `space:<dashboardSlug>` in dashboard mode, `cli:<crew-home-basename>` in CLI-only mode.
|
|
59
67
|
|
|
60
68
|
### What init creates
|
|
61
69
|
|
|
@@ -65,7 +73,7 @@ Init runs four phases, each idempotent — re-running converges anything that dr
|
|
|
65
73
|
|
|
66
74
|
2. **Scaffold** — creates `crewHome/.orchestration/` with subdirectories for identities, personas, workflows (documentation, not scripts), and feedback. Copies all `.md` and `.png` files from the repo using `cp -n` (no-clobber), so existing customizations are preserved.
|
|
67
75
|
|
|
68
|
-
3. **Project registration** — registers the task service as a project in its own database via `createproject`. The dashboard becomes its own first project, so the crew can work on the dashboard itself.
|
|
76
|
+
3. **Project registration** — registers the task service as a project in its own database via `createproject`, with `repo_path` set to the validated `dashboardRepoPath`. The dashboard becomes its own first project, so the crew can work on the dashboard itself. Re-running init is self-healing: a project already registered with the correct path is left alone, while one with a different path (e.g. from an older init) is repaired via `update-project`. Skipped entirely in CLI-only mode (no `dashboardSlug`) — create projects with `crew-api.js create-project` instead.
|
|
69
77
|
|
|
70
78
|
4. **Crons** — reads the manifest at `seed/crons.json` and makes the scheduler match it: missing jobs are created from the entry (title, enabled, mode, schedule, owner, timeout_secs, and the body built from the entry's template with `crewHome` and the dashboard slug substituted in); existing jobs are viewed and converged — drifted fields are updated, unchanged jobs are left alone. One exception: init never touches `enabled` on an existing job. `enabled` is a creation-time default only — a disabled job is a deliberate human decision, and re-init must not silently resurrect it. Jobs removed from the manifest are left alone; deletion is a human decision.
|
|
71
79
|
|
|
@@ -78,6 +86,7 @@ After init completes, it returns a summary:
|
|
|
78
86
|
"message": "Muse Crew initialized.",
|
|
79
87
|
"crewHome": "~/workspace/.jarvis",
|
|
80
88
|
"dashboardSlug": "orchestra-dashboard",
|
|
89
|
+
"dashboardRepoPath": "/home/hatch/workspace/orchestra-dashboard",
|
|
81
90
|
"dashboardName": "Muse Crew",
|
|
82
91
|
"releaseHash": "<git commit hash>",
|
|
83
92
|
"scaffold": { "created": 23, "skipped": 0 },
|
package/lib/crew-api.js
CHANGED
|
@@ -371,6 +371,15 @@ commands["get-dispatch-state"] = (db) => {
|
|
|
371
371
|
total_sessions: sessionCount,
|
|
372
372
|
total_events: eventCount,
|
|
373
373
|
},
|
|
374
|
+
// Active dispatch reservations: tasks the worker launched but whose
|
|
375
|
+
// workflow has not yet self-claimed. The dispatcher skips these.
|
|
376
|
+
reservations: db.prepare(
|
|
377
|
+
`SELECT task_id, run_id, workflow, dispatched_at, expires_at
|
|
378
|
+
FROM dispatch_reservations WHERE expires_at > ? ORDER BY dispatched_at`
|
|
379
|
+
).all(now()).map((r) => ({
|
|
380
|
+
task_id: r.task_id, run_id: r.run_id, workflow: r.workflow,
|
|
381
|
+
dispatched_at: r.dispatched_at, expires_at: r.expires_at,
|
|
382
|
+
})),
|
|
374
383
|
};
|
|
375
384
|
};
|
|
376
385
|
|
|
@@ -562,6 +571,41 @@ commands["claim-task"] = (db, args) => {
|
|
|
562
571
|
return { ok: true, claimed: false, reason: "already_claimed", existing_session_id: existing.id };
|
|
563
572
|
};
|
|
564
573
|
|
|
574
|
+
// Dispatch reservations: the cron worker creates one after launching a
|
|
575
|
+
// workflow for a task; the dispatcher skips reserved tasks on the next tick;
|
|
576
|
+
// the workflow clears it on self-claim. Expired reservations are inert.
|
|
577
|
+
commands["reserve-dispatch"] = (db, args) => {
|
|
578
|
+
if (!args.task_id) throw usageError("task_id is required.");
|
|
579
|
+
if (!args.run_id) throw usageError("run_id is required.");
|
|
580
|
+
if (!args.workflow) throw usageError("workflow is required.");
|
|
581
|
+
requireTask(db, args.task_id);
|
|
582
|
+
const ttl = Math.max(60, Math.min(3600, Number(args.ttl_seconds) || 900));
|
|
583
|
+
const dispatchedAt = now();
|
|
584
|
+
const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
|
|
585
|
+
db.prepare(
|
|
586
|
+
`INSERT INTO dispatch_reservations (task_id, run_id, workflow, dispatched_at, expires_at)
|
|
587
|
+
VALUES (?, ?, ?, ?, ?)
|
|
588
|
+
ON CONFLICT(task_id) DO UPDATE SET
|
|
589
|
+
run_id = excluded.run_id, workflow = excluded.workflow,
|
|
590
|
+
dispatched_at = excluded.dispatched_at, expires_at = excluded.expires_at`
|
|
591
|
+
).run(args.task_id, args.run_id, args.workflow, dispatchedAt, expiresAt);
|
|
592
|
+
return { ok: true, task_id: args.task_id, run_id: args.run_id, workflow: args.workflow, dispatched_at: dispatchedAt, expires_at: expiresAt };
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
commands["clear-reservation"] = (db, args) => {
|
|
596
|
+
if (!args.task_id) throw usageError("task_id is required.");
|
|
597
|
+
const info = db.prepare("DELETE FROM dispatch_reservations WHERE task_id = ?").run(args.task_id);
|
|
598
|
+
return { ok: true, cleared: info.changes > 0 };
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
commands["list-reservations"] = (db) => {
|
|
602
|
+
const rows = db.prepare(
|
|
603
|
+
`SELECT task_id, run_id, workflow, dispatched_at, expires_at
|
|
604
|
+
FROM dispatch_reservations WHERE expires_at > ? ORDER BY dispatched_at`
|
|
605
|
+
).all(now());
|
|
606
|
+
return { ok: true, reservations: rows };
|
|
607
|
+
};
|
|
608
|
+
|
|
565
609
|
commands["park-task"] = (db, args) => {
|
|
566
610
|
if (!args.task_id) throw usageError("task_id is required.");
|
|
567
611
|
const message = (args.message ?? "").trim();
|
package/lib/schema.sql
CHANGED
|
@@ -99,3 +99,20 @@ CREATE INDEX IF NOT EXISTS events_task_id_idx ON events(task_id);
|
|
|
99
99
|
-- another session holds the claim.
|
|
100
100
|
CREATE UNIQUE INDEX IF NOT EXISTS agent_sessions_one_running_per_task_idx
|
|
101
101
|
ON agent_sessions(task_id) WHERE status = 'running';
|
|
102
|
+
|
|
103
|
+
-- Dispatch reservations: the dispatcher recommends, the cron worker launches,
|
|
104
|
+
-- and the launched workflow self-claims. The claim can take 15+ minutes for
|
|
105
|
+
-- cron-launched runs (canary 2026-09-13), far longer than the 3-minute poll
|
|
106
|
+
-- interval — without a reservation, every tick re-dispatches the same task.
|
|
107
|
+
-- The worker creates a reservation after launching; the dispatcher skips
|
|
108
|
+
-- reserved tasks; the workflow clears it on self-claim. Expired reservations
|
|
109
|
+
-- are ignored (the workflow died before claiming).
|
|
110
|
+
CREATE TABLE IF NOT EXISTS dispatch_reservations (
|
|
111
|
+
task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
|
|
112
|
+
run_id TEXT NOT NULL,
|
|
113
|
+
workflow TEXT NOT NULL,
|
|
114
|
+
dispatched_at TEXT NOT NULL,
|
|
115
|
+
expires_at TEXT NOT NULL
|
|
116
|
+
);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS dispatch_reservations_expires_idx
|
|
118
|
+
ON dispatch_reservations(expires_at);
|
package/package.json
CHANGED
package/seed/AGENTS.md
CHANGED
|
@@ -7,3 +7,4 @@ Init source data. Everything `crew-init.js` reads when setting up a new crew ins
|
|
|
7
7
|
- `posture.md` — PM posture that shapes the user's assistant for crew interaction
|
|
8
8
|
- `feedback/` — feedback feature placeholder
|
|
9
9
|
- `workflows/` — human-readable workflow definitions, copied to `$CREW_HOME/.orchestration/workflows/` during init
|
|
10
|
+
- `sample-project.tar.gz` — the pre-bundled flashcard study app (null-state project). When setup ends with no project in mind and no dashboard, the agent extracts this to a workspace directory, `git init`s it with an initial commit (the worktree lifecycle needs a HEAD), and registers it via `create-project`. Ships inside the npm package via `files: ["seed/"]`.
|
|
@@ -13,8 +13,10 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
|
|
|
13
13
|
Wait for it to complete. It reads the crew's task state, determines eligibility, claims tasks, acknowledges the poll, and returns structured results.
|
|
14
14
|
|
|
15
15
|
4. **Launch claims autonomously:** Extract the `claims` array from the dispatcher result. For each claim (up to 3 per tick — if more than 3, launch the first 3 and log the rest as deferred):
|
|
16
|
-
- Call workflow_launch_async with scriptPath={claim.scriptPath} and args={claim.args}.
|
|
17
|
-
-
|
|
16
|
+
- Call workflow_launch_async with scriptPath={claim.scriptPath} and args={claim.args}. Record the returned run_id.
|
|
17
|
+
- Immediately create a dispatch reservation so the next tick does not re-dispatch before the workflow self-claims (claims can take 15+ minutes for cron-launched runs). Run in shell:
|
|
18
|
+
`node {crewHome}/lib/crew-api.js --crew-home {crewHome} reserve-dispatch --json '{"task_id": "{claim.task_id}", "run_id": "<run_id>", "workflow": "{claim.workflow}"}'`
|
|
19
|
+
- The launched workflow self-claims the task and clears the reservation as its first actions. If the task was already claimed or is done, the claim fails closed and the run stands down quietly — this is the mechanical duplicate protection, not an error.
|
|
18
20
|
|
|
19
21
|
If the dispatcher returned no claims or the claims array is empty, report: NO_DISPATCH
|
|
20
22
|
|
|
Binary file
|
package/workflows/AGENTS.md
CHANGED
|
@@ -5,7 +5,7 @@ Executable Muse workflow scripts (JavaScript). These are what the workflow runti
|
|
|
5
5
|
- `crew-dispatch.js` — reads the board, recommends eligible tasks, returns structured launch records (the launched workflow self-claims; the dispatcher never writes claims). Skips tasks on quiesced projects and on projects with no repo_path configured.
|
|
6
6
|
|
|
7
7
|
All four workflow scripts share byte-identical transport helpers (`workRetryKey`, `buildTransportRetryTrailer`, `describeWorkAgentFailure`, `workerMissingArtifactTools` — pinned by `tests/closeout.test.js`). The transport-retry loop treats a worker report naming the missing artifact tool namespace (bug 3472bf36, a per-launch platform flake) as a retryable attempt with a fresh launch rather than accepting a useless report.
|
|
8
|
-
- `crew-init.js` — sets up a new crew instance: Gate 0 validates crewHome is workspace-contained and a valid git repo; bootstraps the release system; scaffolds orchestration folders; creates/converges cron jobs from the declarative manifest. Idempotent. The crew owns its state via the Crew API (lib/crew-api.js); the dashboard is
|
|
8
|
+
- `crew-init.js` — sets up a new crew instance: Gate 0 validates crewHome is workspace-contained and a valid git repo, and (in dashboard mode) validates dashboardRepoPath is workspace-contained and an existing git repo (never auto-created); bootstraps the release system; scaffolds orchestration folders; registers the first project with repo_path set to the validated dashboard repo — skipped in CLI-only mode (no dashboardSlug/dashboardRepoPath), where projects are created later via crew-api.js; creates/converges cron jobs from the declarative manifest (owner `space:<slug>` in dashboard mode, `cli:<crew-home-basename>` in CLI-only mode). Idempotent. The crew owns its state via the Crew API (lib/crew-api.js); the dashboard is an optional client and is never a dependency.
|
|
9
9
|
- `standard.js` — default task workflow: Triage → Capture → Map → Build → Review → Integrate → Publish → QA
|
|
10
10
|
- `bugfix.js` — adds Capture after Triage, then Reproduce
|
|
11
11
|
- `chore.js` — adds Capture after Triage, drops QA (low-risk)
|
package/workflows/bugfix.js
CHANGED
|
@@ -1060,6 +1060,7 @@ while (i < STEPS.length) {
|
|
|
1060
1060
|
"Claim this task for the " + step.name + " step.\n" +
|
|
1061
1061
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("update-task", firstClaimUpdateArgs) + "\n" +
|
|
1062
1062
|
"Then run in shell and return the stdout verbatim:\n" + crewCmd("claim-task", { task_id: taskId, identity: step.identity, step: step.name, notes: step.name + " step started" }) + "\n" +
|
|
1063
|
+
"If the claim response has claimed=true, then run in shell and return the stdout verbatim:\n" + crewCmd("clear-reservation", { task_id: taskId }) + "\n" +
|
|
1063
1064
|
"Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
|
|
1064
1065
|
"Return { claimed: <verbatim>, session_id: \"<...>\" }. If claimed is false there is no session_id; return { claimed: false, session_id: \"\" }.",
|
|
1065
1066
|
{
|
package/workflows/chore.js
CHANGED
|
@@ -1017,6 +1017,7 @@ while (i < STEPS.length) {
|
|
|
1017
1017
|
"Claim this task for the " + step.name + " step.\n" +
|
|
1018
1018
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("update-task", firstClaimUpdateArgs) + "\n" +
|
|
1019
1019
|
"Then run in shell and return the stdout verbatim:\n" + crewCmd("claim-task", { task_id: taskId, identity: step.identity, step: step.name, notes: step.name + " step started" }) + "\n" +
|
|
1020
|
+
"If the claim response has claimed=true, then run in shell and return the stdout verbatim:\n" + crewCmd("clear-reservation", { task_id: taskId }) + "\n" +
|
|
1020
1021
|
"Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
|
|
1021
1022
|
"Return { claimed: <verbatim>, session_id: \"<...>\" }. If claimed is false there is no session_id; return { claimed: false, session_id: \"\" }.",
|
|
1022
1023
|
{
|
|
@@ -237,6 +237,15 @@ const allTasks = boardData.ready_tasks.map(projectTaskRecord).filter(function (t
|
|
|
237
237
|
const config = boardData.config || {};
|
|
238
238
|
const projects = boardData.projects || [];
|
|
239
239
|
|
|
240
|
+
// Dispatch reservations: tasks the worker launched on a previous tick whose
|
|
241
|
+
// workflow has not yet self-claimed. The claim can take 15+ minutes for
|
|
242
|
+
// cron-launched runs (canary 2026-09-13) — far longer than the poll interval —
|
|
243
|
+
// so without this filter every tick would re-dispatch the same task.
|
|
244
|
+
const reservedTaskIds = new Set((boardData.reservations || []).map(function (r) { return r.task_id; }));
|
|
245
|
+
if (reservedTaskIds.size > 0) {
|
|
246
|
+
log("Reservations: skipping " + reservedTaskIds.size + " task(s) with an active dispatch reservation");
|
|
247
|
+
}
|
|
248
|
+
|
|
240
249
|
// Default project: explicit arg, or first registered project
|
|
241
250
|
const DEFAULT_PROJECT = inputs.defaultProject || (projects.length > 0 ? projects[0].id : "");
|
|
242
251
|
|
|
@@ -383,6 +392,14 @@ for (var t = 0; t < allTasks.length; t++) {
|
|
|
383
392
|
var task = allTasks[t];
|
|
384
393
|
if (task.blocked) continue;
|
|
385
394
|
|
|
395
|
+
// Skip tasks with an active dispatch reservation: the worker launched a
|
|
396
|
+
// workflow for this task on a previous tick, but the workflow has not yet
|
|
397
|
+
// self-claimed (claims can take 15+ minutes for cron-launched runs).
|
|
398
|
+
if (reservedTaskIds.has(task.id)) {
|
|
399
|
+
log("Skipped \"" + task.title + "\" — active dispatch reservation (workflow launched, claim pending)");
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
386
403
|
// Skip tasks from quiesced projects
|
|
387
404
|
var taskProject = task.project || DEFAULT_PROJECT;
|
|
388
405
|
if (quiescedProjects[taskProject]) {
|
package/workflows/crew-init.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const meta = {
|
|
2
2
|
name: "crew-init",
|
|
3
|
-
description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, register
|
|
3
|
+
description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, register the first project, set up cron jobs. Idempotent — safe to re-run. The dashboard is optional: omit dashboardSlug/dashboardRepoPath for a CLI-only install. Takes an optional crewName (the human's chosen name for the crew), stored at $CREW_HOME/crew-name.",
|
|
4
4
|
phases: [
|
|
5
5
|
{ name: "release", title: "Bootstrap release system" },
|
|
6
6
|
{ name: "scaffold", title: "Scaffold orchestration directory" },
|
|
7
|
-
{ name: "project", title: "Register
|
|
7
|
+
{ name: "project", title: "Register first project" },
|
|
8
8
|
{ name: "crons", title: "Create and converge cron jobs" }
|
|
9
9
|
]
|
|
10
10
|
};
|
|
@@ -14,12 +14,18 @@ const inputs = args ?? {};
|
|
|
14
14
|
const crewRepoPath = inputs.crewRepoPath;
|
|
15
15
|
const crewHome = inputs.crewHome;
|
|
16
16
|
const dashboardSlug = inputs.dashboardSlug;
|
|
17
|
+
const dashboardRepoPath = inputs.dashboardRepoPath;
|
|
17
18
|
const dashboardName = inputs.dashboardName || "Muse Crew";
|
|
19
|
+
const crewName = inputs.crewName || null;
|
|
18
20
|
const cronIds = inputs.cronIds || {};
|
|
19
21
|
|
|
20
22
|
if (!crewRepoPath) throw new Error("crewRepoPath is required — path to muse-crew (git checkout or npm install)");
|
|
21
23
|
if (!crewHome) throw new Error("crewHome is required — e.g. ~/workspace/.crew (must be inside the workspace)");
|
|
22
|
-
|
|
24
|
+
// The dashboard is optional. Provide both dashboardSlug and dashboardRepoPath
|
|
25
|
+
// together for the dashboard flow (the dashboard becomes the crew's first
|
|
26
|
+
// project); omit both for a CLI-only install (no dashboard required —
|
|
27
|
+
// create projects later via crew-api.js). One without the other is an error.
|
|
28
|
+
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)");
|
|
23
29
|
|
|
24
30
|
const orchDir = crewHome + "/.orchestration";
|
|
25
31
|
|
|
@@ -31,10 +37,13 @@ const orchDir = crewHome + "/.orchestration";
|
|
|
31
37
|
// copies). Fail closed here so no such instance can ever be created.
|
|
32
38
|
// The dashboard repo is validated too: init registers the dashboard
|
|
33
39
|
// project against it, and a non-git path would only fail later at the
|
|
34
|
-
// first Build (the canary's npm-package repo_path defect
|
|
40
|
+
// first Build (the canary's npm-package repo_path defect, 2026-09-13:
|
|
41
|
+
// init registered the crew home itself as the dashboard repo_path).
|
|
42
|
+
// Unlike the crew home, the dashboard repo is NEVER auto-created — it is
|
|
43
|
+
// the user's own project repository; a non-git path is a user error.
|
|
35
44
|
// Gate 0 decision logic is pure JS — the agent is a sensor, not a judge.
|
|
36
45
|
// It reports raw facts (HOME, expanded paths, the git rev-parse exit
|
|
37
|
-
//
|
|
46
|
+
// codes); the launchable/repo_valid verdicts are computed here,
|
|
38
47
|
// deterministically. An agent asked for a verdict can mis-apply the
|
|
39
48
|
// prefix rule (e.g. the "$ws-evil" trailing-slash trick); code cannot.
|
|
40
49
|
// (Prompt hardening is a smell — this is the mechanical version.)
|
|
@@ -44,26 +53,39 @@ function expandTilde(p, home) {
|
|
|
44
53
|
return p;
|
|
45
54
|
}
|
|
46
55
|
function gate0Decide(facts) {
|
|
47
|
-
// facts: { home, crewHomeExpanded, gitExitCode }
|
|
56
|
+
// facts: { home, crewHomeExpanded, gitExitCode, dashboardRepoExpanded, dashboardRepoGitExitCode }
|
|
57
|
+
// The dashboard fields are null in CLI-only mode (no dashboard) — the
|
|
58
|
+
// dashboard verdicts are then vacuously true (nothing to validate).
|
|
48
59
|
var workspace = facts.home + "/workspace";
|
|
49
60
|
var launchable = facts.crewHomeExpanded.indexOf(workspace + "/") === 0;
|
|
50
61
|
var repoValid = facts.gitExitCode === 0;
|
|
51
|
-
|
|
62
|
+
var dashboardRepoLaunchable = facts.dashboardRepoExpanded == null || facts.dashboardRepoExpanded.indexOf(workspace + "/") === 0;
|
|
63
|
+
var dashboardRepoValid = facts.dashboardRepoGitExitCode == null || facts.dashboardRepoGitExitCode === 0;
|
|
64
|
+
return { workspace: workspace, launchable: launchable, repoValid: repoValid, dashboardRepoLaunchable: dashboardRepoLaunchable, dashboardRepoValid: dashboardRepoValid };
|
|
52
65
|
}
|
|
53
66
|
var gateFacts;
|
|
54
67
|
try {
|
|
55
68
|
gateFacts = await agent(
|
|
56
|
-
"
|
|
57
|
-
"Requested crewHome: " + crewHome + "\\n
|
|
69
|
+
"Validate the requested crew home" + (dashboardRepoPath ? " and dashboard repository" : "") + ". Report raw facts.\\n\\n" +
|
|
70
|
+
"Requested crewHome: " + crewHome + "\\n" +
|
|
71
|
+
(dashboardRepoPath ? "Requested dashboardRepoPath: " + dashboardRepoPath + "\\n" : "") + "\\n" +
|
|
58
72
|
"Steps (run in shell):\\n" +
|
|
59
73
|
"1. home=$(echo $HOME)\\n" +
|
|
60
74
|
"2. Expand a leading ~/ in the requested crewHome against $HOME (leave other paths untouched).\\n" +
|
|
61
|
-
"3. If the expanded
|
|
75
|
+
"3. If the expanded crewHome does not exist: mkdir -p <expanded crewHome>\\n" +
|
|
62
76
|
"4. Run: git -C <expanded crewHome> rev-parse --git-dir; echo EXIT:$?\\n" +
|
|
63
77
|
"5. If EXIT is not 0: run git -C <expanded crewHome> init -b main (or git init if -b not supported)\\n" +
|
|
64
78
|
"6. Re-run: git -C <expanded crewHome> rev-parse --git-dir; echo EXIT:$?\\n" +
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
(dashboardRepoPath ?
|
|
80
|
+
"7. Expand a leading ~/ in the requested dashboardRepoPath against $HOME (leave other paths untouched).\\n" +
|
|
81
|
+
"8. Run: git -C <expanded dashboardRepoPath> rev-parse --git-dir; echo EXIT:$?\\n" +
|
|
82
|
+
" Do NOT create or git-init the dashboard repo — it must already be a git repository.\\n" +
|
|
83
|
+
"9. Return JSON { home, crewHomeExpanded, gitExitCode, dashboardRepoExpanded, dashboardRepoGitExitCode }\\n" +
|
|
84
|
+
" where gitExitCode is the FINAL crew-home exit code after init, and dashboardRepoGitExitCode\\n" +
|
|
85
|
+
" is the dashboard repo's exit code (no init attempted).\\n" :
|
|
86
|
+
"7. Return JSON { home, crewHomeExpanded, gitExitCode, dashboardRepoExpanded: null, dashboardRepoGitExitCode: null }.\\n") +
|
|
87
|
+
"\\n" +
|
|
88
|
+
"This ensures the crew home is a valid git repository" + (dashboardRepoPath ? " and the dashboard repo exists as one" : "") + ". Do not judge — just ensure and report.",
|
|
67
89
|
{
|
|
68
90
|
key: "gate-0",
|
|
69
91
|
label: "Validate crew home",
|
|
@@ -72,9 +94,11 @@ try {
|
|
|
72
94
|
properties: {
|
|
73
95
|
home: { type: "string" },
|
|
74
96
|
crewHomeExpanded: { type: "string" },
|
|
75
|
-
gitExitCode: { type: "number" }
|
|
97
|
+
gitExitCode: { type: "number" },
|
|
98
|
+
dashboardRepoExpanded: { type: ["string", "null"] },
|
|
99
|
+
dashboardRepoGitExitCode: { type: ["number", "null"] }
|
|
76
100
|
},
|
|
77
|
-
required: ["home", "crewHomeExpanded", "gitExitCode"]
|
|
101
|
+
required: ["home", "crewHomeExpanded", "gitExitCode", "dashboardRepoExpanded", "dashboardRepoGitExitCode"]
|
|
78
102
|
}
|
|
79
103
|
}
|
|
80
104
|
);
|
|
@@ -100,7 +124,51 @@ if (!gateResult.repoValid) {
|
|
|
100
124
|
}
|
|
101
125
|
};
|
|
102
126
|
}
|
|
103
|
-
|
|
127
|
+
if (!gateResult.dashboardRepoLaunchable) {
|
|
128
|
+
return {
|
|
129
|
+
__hatchWorkflowControl: "blocked",
|
|
130
|
+
result: {
|
|
131
|
+
blocked_reason: "dashboardRepoPath is not workspace-contained",
|
|
132
|
+
message: "dashboardRepoPath must be inside the workspace (" + gateResult.workspace + ") because the crew works on it via worktrees launched from workspace-contained paths. Requested: " + gateFacts.dashboardRepoExpanded + ". Clone the dashboard repo under the workspace and re-run init."
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (!gateResult.dashboardRepoValid) {
|
|
137
|
+
return {
|
|
138
|
+
__hatchWorkflowControl: "blocked",
|
|
139
|
+
result: {
|
|
140
|
+
blocked_reason: "dashboardRepoPath is not a git repository",
|
|
141
|
+
message: "dashboardRepoPath (" + gateFacts.dashboardRepoExpanded + ") is not a git repository (git exit code " + gateFacts.dashboardRepoGitExitCode + "). Init registers the dashboard project against this path and the crew's worktree lifecycle requires a real git repo. Clone the dashboard repository first, then re-run init. Init will never create the dashboard repo for you."
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
log("Gate 0: crewHome " + gateFacts.crewHomeExpanded + " is workspace-contained and a valid git repo" + (gateFacts.dashboardRepoExpanded ? "; dashboard repo " + gateFacts.dashboardRepoExpanded + " is workspace-contained and a valid git repo" : " (CLI-only mode: no dashboard)"));
|
|
146
|
+
|
|
147
|
+
// ── Crew name ─────────────────────────────────────────────────────────
|
|
148
|
+
// The human names their crew during setup (SETUP-EXPERIENCE.md). Stored as
|
|
149
|
+
// plain text at $CREW_HOME/crew-name; returned in the init summary.
|
|
150
|
+
// Optional at the mechanical layer — the setup conversation should always
|
|
151
|
+
// ask, but init must not fail when no name was given.
|
|
152
|
+
if (crewName) {
|
|
153
|
+
try {
|
|
154
|
+
await agent(
|
|
155
|
+
"Record the crew's human-chosen name.\n\n" +
|
|
156
|
+
"Crew home: " + gateFacts.crewHomeExpanded + "\n" +
|
|
157
|
+
"Crew name: " + crewName + "\n\n" +
|
|
158
|
+
"Use the Write tool to write the exact name as a single line to " + gateFacts.crewHomeExpanded + "/crew-name (no extra whitespace or newlines beyond the one trailing newline). Do not use shell echo — the name may contain quoting-sensitive characters.\n" +
|
|
159
|
+
"This is a fire-and-forget write: no return value is needed.",
|
|
160
|
+
{
|
|
161
|
+
key: "crew-name-1",
|
|
162
|
+
label: "Record crew name"
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
log("Crew name: " + crewName);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Crew name recording failed", message: String(e.message || e) } };
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
log("Crew name: not provided (setup should ask the human for one)");
|
|
171
|
+
}
|
|
104
172
|
|
|
105
173
|
// ── Phase 1: Release ──────────────────────────────────────────────────
|
|
106
174
|
phase("release");
|
|
@@ -184,35 +252,53 @@ var scaffoldSkipped = scaffoldResult.skipped ? scaffoldResult.skipped.length : 0
|
|
|
184
252
|
log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped");
|
|
185
253
|
|
|
186
254
|
// ── Phase 3: Project registration ─────────────────────────────────────
|
|
187
|
-
// Registers the
|
|
188
|
-
//
|
|
189
|
-
// work on the dashboard itself
|
|
190
|
-
//
|
|
255
|
+
// Registers the first project in crew-state.db via the Crew API. In
|
|
256
|
+
// dashboard mode, the dashboard becomes the crew's first project (so the
|
|
257
|
+
// crew can work on the dashboard itself); the project's repo_path is the
|
|
258
|
+
// validated dashboard git repository from Gate 0 — never the crew home
|
|
259
|
+
// (the 2026-09-13 defect where init registered the crew home itself as
|
|
260
|
+
// the repo_path). Idempotent and self-healing: if the project already
|
|
261
|
+
// exists with the correct repo_path it is left alone; if it exists with
|
|
262
|
+
// a different repo_path (e.g. from a pre-fix init), the path is repaired.
|
|
263
|
+
// In CLI-only mode (no dashboardSlug) this phase is skipped — create
|
|
264
|
+
// projects later via crew-api.js create-project.
|
|
191
265
|
phase("project");
|
|
192
266
|
var projectResult;
|
|
267
|
+
if (!dashboardSlug) {
|
|
268
|
+
log("Project: skipped (CLI-only mode — no dashboard; create projects via crew-api.js create-project)");
|
|
269
|
+
projectResult = { action: "skipped", project_id: null };
|
|
270
|
+
} else {
|
|
193
271
|
var safeDashboardName = dashboardName.split('"').join('\\"');
|
|
272
|
+
var safeDashboardRepo = gateFacts.dashboardRepoExpanded.split('"').join('\\"');
|
|
194
273
|
try {
|
|
195
274
|
projectResult = await agent(
|
|
196
275
|
"Register the dashboard as a project in the crew's task database.\n\n" +
|
|
197
276
|
"Crew home: " + crewHome + "\n" +
|
|
198
277
|
"Crew API: " + crewHome + "/current/lib/crew-api.js\n" +
|
|
199
278
|
"Dashboard slug (project id): " + dashboardSlug + "\n" +
|
|
200
|
-
"Dashboard name (display_name): " + dashboardName + "\n
|
|
279
|
+
"Dashboard name (display_name): " + dashboardName + "\n" +
|
|
280
|
+
"Dashboard repo path (repo_path): " + gateFacts.dashboardRepoExpanded + "\n\n" +
|
|
201
281
|
"Steps:\n" +
|
|
202
282
|
"1. Check if the project already exists:\n" +
|
|
203
283
|
" node " + crewHome + "/current/lib/crew-api.js --crew-home " + crewHome + " get-project --json '{\"id\": \"" + dashboardSlug + "\"}'\n" +
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"
|
|
284
|
+
"2. If it returns a project (ok: true):\n" +
|
|
285
|
+
" a. Compare its repo_path to: " + gateFacts.dashboardRepoExpanded + "\n" +
|
|
286
|
+
" b. If they match, return { action: \"exists\", project_id: \"" + dashboardSlug + "\" }.\n" +
|
|
287
|
+
" c. If they differ, repair it:\n" +
|
|
288
|
+
" node " + crewHome + "/current/lib/crew-api.js --crew-home " + crewHome + " update-project --json '{\"id\": \"" + dashboardSlug + "\", \"repo_path\": \"" + safeDashboardRepo + "\"}'\n" +
|
|
289
|
+
" Return { action: \"repaired\", project_id: \"" + dashboardSlug + "\" }.\n" +
|
|
290
|
+
"3. If not found (error), create it:\n" +
|
|
291
|
+
" node " + crewHome + "/current/lib/crew-api.js --crew-home " + crewHome + " create-project --json '{\"id\": \"" + dashboardSlug + "\", \"display_name\": \"" + safeDashboardName + "\", \"repo_path\": \"" + safeDashboardRepo + "\", \"deploy_type\": \"artifact\", \"deploy_slug\": \"" + dashboardSlug + "\", \"description\": \"The dashboard task service — the crew\\u0027s first project\"}'\n" +
|
|
207
292
|
" Return { action: \"created\", project_id: \"" + dashboardSlug + "\" }.\n\n" +
|
|
208
|
-
"
|
|
293
|
+
"The repo_path must be the dashboard git repository — never the crew home.\n" +
|
|
294
|
+
"Return JSON with action (\"exists\", \"repaired\", or \"created\") and project_id (string).",
|
|
209
295
|
{
|
|
210
296
|
key: "project-1",
|
|
211
297
|
label: "Register dashboard project",
|
|
212
298
|
schema: {
|
|
213
299
|
type: "object",
|
|
214
300
|
properties: {
|
|
215
|
-
action: { type: "string", enum: ["exists", "created"] },
|
|
301
|
+
action: { type: "string", enum: ["exists", "repaired", "created"] },
|
|
216
302
|
project_id: { type: "string" }
|
|
217
303
|
},
|
|
218
304
|
required: ["action", "project_id"]
|
|
@@ -222,21 +308,30 @@ try {
|
|
|
222
308
|
} catch (e) {
|
|
223
309
|
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Project registration failed", message: String(e.message || e) } };
|
|
224
310
|
}
|
|
225
|
-
log("Project: " + dashboardSlug + " " + projectResult.action);
|
|
311
|
+
log("Project: " + dashboardSlug + " " + projectResult.action + " (repo_path=" + gateFacts.dashboardRepoExpanded + ")");
|
|
312
|
+
} // end dashboard-mode project registration
|
|
226
313
|
|
|
227
314
|
// ── Phase 4: Cron jobs (declarative manifest) ──────────────────────────
|
|
228
315
|
// The manifest lives in the repo at seed/crons.json. Body templates live
|
|
229
316
|
// in seed/ with {crewHome} placeholders. Missing jobs are
|
|
230
317
|
// created from the manifest; existing jobs converge to it — except `enabled`,
|
|
231
318
|
// which is a creation-time default only and is never touched on update.
|
|
319
|
+
// Instance identity: in dashboard mode the dashboard slug scopes cron ids
|
|
320
|
+
// and ownership (space:<slug>); in CLI-only mode the crew-home basename
|
|
321
|
+
// scopes them instead, so two crew instances never share a cron id and no
|
|
322
|
+
// dashboard is required.
|
|
232
323
|
phase("crons");
|
|
324
|
+
var crewHomeBase = gateFacts.crewHomeExpanded.split("/").pop().replace(/^\./, "") || "crew";
|
|
325
|
+
var instanceId = dashboardSlug || crewHomeBase;
|
|
326
|
+
var cronOwner = inputs.cronOwner || (dashboardSlug ? "space:" + dashboardSlug : "cli:" + instanceId);
|
|
233
327
|
var cronsResult;
|
|
234
328
|
try {
|
|
235
329
|
cronsResult = await agent(
|
|
236
330
|
"Ensure the Muse Crew cron jobs match the repo's declarative manifest.\n\n" +
|
|
237
331
|
"Manifest: " + crewRepoPath + "/seed/crons.json\n" +
|
|
238
332
|
"crewHome: " + crewHome + "\n" +
|
|
239
|
-
"
|
|
333
|
+
"Instance id (cron id suffix): " + instanceId + "\n" +
|
|
334
|
+
"Cron owner: " + cronOwner + "\n" +
|
|
240
335
|
"Id overrides (manifest id -> live id): " + JSON.stringify(cronIds) + "\n\n" +
|
|
241
336
|
"Steps:\n" +
|
|
242
337
|
"1. Read the manifest at " + crewRepoPath + "/seed/crons.json and parse it as JSON.\n" +
|
|
@@ -245,18 +340,17 @@ try {
|
|
|
245
340
|
" owner, body_template).\n" +
|
|
246
341
|
"2. For each manifest entry, in order:\n" +
|
|
247
342
|
" a. The live id is the override for entry.id when present (inputs.cronIds[entry.id]).\n" +
|
|
248
|
-
" Otherwise, the live id is entry.id + '-' +
|
|
343
|
+
" Otherwise, the live id is entry.id + '-' + \"" + instanceId + "\" (instance-safe by\n" +
|
|
249
344
|
" construction — two crew instances never share a cron id). The bare manifest\n" +
|
|
250
345
|
" id is never used as a live id.\n" +
|
|
251
346
|
" b. Read the body template at " + crewRepoPath + "/seed/<entry.body_template>.\n" +
|
|
252
347
|
" c. Replace all occurrences of {crewHome} with: " + crewHome + ".\n" +
|
|
253
|
-
" Replace all occurrences of {dashboardSlug} with: " + dashboardSlug + ".\n" +
|
|
254
348
|
" d. Call cron_list and look for a job with the live id.\n" +
|
|
255
349
|
" e. If missing, call cron_add with:\n" +
|
|
256
350
|
" - id: the live id\n" +
|
|
257
351
|
" - title, enabled, mode from the entry\n" +
|
|
258
352
|
" - schedule: the entry's schedule object\n" +
|
|
259
|
-
" - owner:
|
|
353
|
+
" - owner: " + cronOwner + "\n" +
|
|
260
354
|
" - timeout_secs: the entry's timeout_secs when present\n" +
|
|
261
355
|
" - body: the resolved template text\n" +
|
|
262
356
|
" Record action 'created' with no updated fields.\n" +
|
|
@@ -315,7 +409,9 @@ log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" +
|
|
|
315
409
|
return {
|
|
316
410
|
message: "Muse Crew initialized.",
|
|
317
411
|
crewHome: crewHome,
|
|
412
|
+
crewName: crewName,
|
|
318
413
|
dashboardSlug: dashboardSlug,
|
|
414
|
+
dashboardRepoPath: gateFacts.dashboardRepoExpanded,
|
|
319
415
|
dashboardName: dashboardName,
|
|
320
416
|
releaseHash: releaseResult.hash,
|
|
321
417
|
scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
|
package/workflows/standard.js
CHANGED
|
@@ -1059,6 +1059,7 @@ while (i < STEPS.length) {
|
|
|
1059
1059
|
"Claim this task for the " + step.name + " step.\n" +
|
|
1060
1060
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("update-task", firstClaimUpdateArgs) + "\n" +
|
|
1061
1061
|
"Then run in shell and return the stdout verbatim:\n" + crewCmd("claim-task", { task_id: taskId, identity: step.identity, step: step.name, notes: step.name + " step started" }) + "\n" +
|
|
1062
|
+
"If the claim response has claimed=true, then run in shell and return the stdout verbatim:\n" + crewCmd("clear-reservation", { task_id: taskId }) + "\n" +
|
|
1062
1063
|
"Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
|
|
1063
1064
|
"Return { claimed: <verbatim>, session_id: \"<...>\" }. If claimed is false there is no session_id; return { claimed: false, session_id: \"\" }.",
|
|
1064
1065
|
{
|