muse-crew 0.6.7 → 0.7.0

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/lib/crew-api.js CHANGED
@@ -529,6 +529,12 @@ commands["claim-task"] = (db, args) => {
529
529
  const task = requireTask(db, args.task_id);
530
530
  const project = requireProject(db, task.project);
531
531
  if (project.quiesced) throw conflict("This project is paused. Resume it before claiming tasks.");
532
+ // Mechanical guard: never claim a task that is already in a terminal state.
533
+ // The dispatcher may recommend a task based on stale state (e.g. a task that
534
+ // was marked done between the dispatcher's read and the workflow's claim).
535
+ // The claim must fail closed here, not launch a duplicate run.
536
+ if (task.state === "done") throw conflict("Task is already done and cannot be claimed.");
537
+ if (task.state === "cancelled") throw conflict("Task is cancelled and cannot be claimed.");
532
538
 
533
539
  const row = {
534
540
  id: uuid(), task_id: args.task_id, identity,
@@ -30,12 +30,29 @@ TMPBASE="$(mktemp -d)"
30
30
  trap 'rm -rf "$TMPBASE"' EXIT
31
31
 
32
32
  # --- Fixture: temp git repo seeded with the repo's current package.json ---
33
+ # The test injects a \u2014 escape into the description field so the fixture
34
+ # carries the escape this regression is about, regardless of whether the
35
+ # repo's own package.json still has one. We write the JSON manually to ensure
36
+ # the escape appears as literal backslash-u bytes, not the decoded character.
33
37
  FIX="$TMPBASE/fixture"
34
38
  mkdir -p "$FIX"
35
39
  git init -q -b main "$FIX" >/dev/null
36
40
  git -C "$FIX" config user.email "test@example.com"
37
41
  git -C "$FIX" config user.name "test"
38
- cp "$REPO_DIR/package.json" "$FIX/package.json"
42
+ node -e '
43
+ const fs = require("fs");
44
+ const pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
45
+ pkg.version = pkg.version; // keep version from repo
46
+ const lines = JSON.stringify(pkg, null, 2).split("\n");
47
+ // Replace the description line with one carrying a literal \u2014 escape
48
+ const out = lines.map(function (l) {
49
+ if (l.match(/"description":/)) {
50
+ return " \"description\": \"Test description with em-dash escape \\u2014 here\",";
51
+ }
52
+ return l;
53
+ });
54
+ fs.writeFileSync(process.argv[2], out.join("\n") + "\n");
55
+ ' "$REPO_DIR/package.json" "$FIX/package.json"
39
56
  git -C "$FIX" add package.json
40
57
  git -C "$FIX" commit -qm "initial"
41
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.6.7",
3
+ "version": "0.7.0",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -1,6 +1,6 @@
1
1
  ## Muse Crew Dispatcher
2
2
 
3
- You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher workflow and report its claims. The main chat agent handles the actual task workflow launches.
3
+ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher workflow and launch its claims autonomously.
4
4
 
5
5
  ### Steps
6
6
 
@@ -8,15 +8,14 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
8
8
 
9
9
  2. **Load the workflow registry:** Read the file "{crewHome}/workflows/registry.json" with the read tool and parse it as JSON. If the file does not exist (the live release predates the registry), proceed without it — omit the `registry` arg and the dispatcher will load the registry the slow way and log a warning.
10
10
 
11
- 3. **Run the dispatcher:** Call workflow_launch with scriptPath "{crewHome}/workflows/crew-dispatch.js" and args {"dashboardSlug": "{dashboardSlug}", "crewHome": "{crewHome}", "registry": <parsed registry JSON, or omit the key when the file was missing>}.
11
+ 3. **Run the dispatcher:** Call workflow_launch with scriptPath "{crewHome}/workflows/crew-dispatch.js" and args {"crewHome": "{crewHome}", "registry": <parsed registry JSON, or omit the key when the file was missing>}.
12
12
 
13
- Wait for it to complete. It reads the board, determines eligibility, claims tasks, acknowledges the poll, and returns structured results.
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
- 4. **Report claims:** Extract the `claims` array from the dispatcher result. For each claim, emit one line:
16
- LAUNCH: script={claim.scriptPath} args={JSON.stringify(claim.args)}
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
+ - The launched workflow self-claims the task as its first action. 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.
17
18
 
18
19
  If the dispatcher returned no claims or the claims array is empty, report: NO_DISPATCH
19
20
 
20
- Do NOT call workflow_launch_async for any task workflow. The main chat agent does that.
21
-
22
21
  5. Exit silently.
@@ -1,9 +1,10 @@
1
1
  export const meta = {
2
2
  name: "crew-init",
3
- description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, set up cron jobs. Idempotent — safe to re-run.",
3
+ description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, register dashboard project, 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" },
7
8
  { name: "crons", title: "Create and converge cron jobs" }
8
9
  ]
9
10
  };
@@ -12,10 +13,13 @@ export const meta = {
12
13
  const inputs = args ?? {};
13
14
  const crewRepoPath = inputs.crewRepoPath;
14
15
  const crewHome = inputs.crewHome;
16
+ const dashboardSlug = inputs.dashboardSlug;
17
+ const dashboardName = inputs.dashboardName || "Muse Crew";
15
18
  const cronIds = inputs.cronIds || {};
16
19
 
17
20
  if (!crewRepoPath) throw new Error("crewRepoPath is required — path to muse-crew (git checkout or npm install)");
18
21
  if (!crewHome) throw new Error("crewHome is required — e.g. ~/workspace/.crew (must be inside the workspace)");
22
+ if (!dashboardSlug) throw new Error("dashboardSlug is required — slug of the task service artifact (must already exist)");
19
23
 
20
24
  const orchDir = crewHome + "/.orchestration";
21
25
 
@@ -49,14 +53,17 @@ function gate0Decide(facts) {
49
53
  var gateFacts;
50
54
  try {
51
55
  gateFacts = await agent(
52
- "Report raw facts about the requested crew home. Do NOT judge it just report.\\n\\n" +
56
+ "Ensure the requested crew home exists as a git repository. Report raw facts.\\n\\n" +
53
57
  "Requested crewHome: " + crewHome + "\\n\\n" +
54
58
  "Steps (run in shell):\\n" +
55
59
  "1. home=$(echo $HOME)\\n" +
56
60
  "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.",
61
+ "3. If the expanded path does not exist: mkdir -p <expanded crewHome>\\n" +
62
+ "4. Run: git -C <expanded crewHome> rev-parse --git-dir; echo EXIT:$?\\n" +
63
+ "5. If EXIT is not 0: run git -C <expanded crewHome> init -b main (or git init if -b not supported)\\n" +
64
+ "6. Re-run: git -C <expanded crewHome> rev-parse --git-dir; echo EXIT:$?\\n" +
65
+ "7. Return JSON { home, crewHomeExpanded, gitExitCode } where gitExitCode is the FINAL exit code after init.\\n\\n" +
66
+ "This ensures the crew home is a valid git repository. Do not judge — just ensure and report.",
60
67
  {
61
68
  key: "gate-0",
62
69
  label: "Validate crew home",
@@ -88,8 +95,8 @@ if (!gateResult.repoValid) {
88
95
  return {
89
96
  __hatchWorkflowControl: "blocked",
90
97
  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."
98
+ blocked_reason: "crewHome git init failed",
99
+ message: "crewHome (" + gateFacts.crewHomeExpanded + ") could not be initialized as a git repository (git exit code " + gateFacts.gitExitCode + "). Check permissions and try again."
93
100
  }
94
101
  };
95
102
  }
@@ -106,11 +113,11 @@ try {
106
113
  "Release script (in repo): " + crewRepoPath + "/lib/crew-release.sh\n\n" +
107
114
  "Steps:\n" +
108
115
  "1. Run: test -f " + crewHome + "/crew-release.sh && echo EXISTS || echo MISSING\n" +
109
- "2. If EXISTS, run: " + crewHome + "/crew-release.sh current\n" +
116
+ "2. If EXISTS, run: " + crewHome + "/crew-release.sh current " + crewHome + "\n" +
110
117
  " Return { existed: true, hash: <current hash> }\n" +
111
118
  "3. If MISSING, bootstrap:\n" +
112
119
  " a. Run: bash " + crewRepoPath + "/lib/crew-release.sh init " + crewHome + " " + crewRepoPath + "\n" +
113
- " b. Verify: " + crewHome + "/crew-release.sh current\n" +
120
+ " b. Verify: " + crewHome + "/crew-release.sh current " + crewHome + "\n" +
114
121
  " c. Return { existed: false, hash: <hash from current> }\n\n" +
115
122
  "Return JSON with existed (boolean) and hash (string).",
116
123
  {
@@ -176,7 +183,48 @@ var scaffoldCreated = scaffoldResult.created ? scaffoldResult.created.length : 0
176
183
  var scaffoldSkipped = scaffoldResult.skipped ? scaffoldResult.skipped.length : 0;
177
184
  log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped");
178
185
 
179
- // ── Phase 3: Cron jobs (declarative manifest) ──────────────────────────
186
+ // ── Phase 3: Project registration ─────────────────────────────────────
187
+ // Registers the dashboard as the crew's first project in crew-state.db via
188
+ // the Crew API. The dashboard becomes its own first project, so the crew can
189
+ // work on the dashboard itself. Idempotent: if the project already exists,
190
+ // it is left alone.
191
+ phase("project");
192
+ var projectResult;
193
+ var safeDashboardName = dashboardName.split('"').join('\\"');
194
+ try {
195
+ projectResult = await agent(
196
+ "Register the dashboard as a project in the crew's task database.\n\n" +
197
+ "Crew home: " + crewHome + "\n" +
198
+ "Crew API: " + crewHome + "/current/lib/crew-api.js\n" +
199
+ "Dashboard slug (project id): " + dashboardSlug + "\n" +
200
+ "Dashboard name (display_name): " + dashboardName + "\n\n" +
201
+ "Steps:\n" +
202
+ "1. Check if the project already exists:\n" +
203
+ " node " + crewHome + "/current/lib/crew-api.js --crew-home " + crewHome + " get-project --json '{\"id\": \"" + dashboardSlug + "\"}'\n" +
204
+ " If it returns a project (ok: true), return { action: \"exists\", project_id: \"" + dashboardSlug + "\" }.\n" +
205
+ "2. If not found (error), create it:\n" +
206
+ " node " + crewHome + "/current/lib/crew-api.js --crew-home " + crewHome + " create-project --json '{\"id\": \"" + dashboardSlug + "\", \"display_name\": \"" + safeDashboardName + "\", \"repo_path\": \"" + crewHome + "\", \"deploy_type\": \"artifact\", \"deploy_slug\": \"" + dashboardSlug + "\", \"description\": \"The dashboard task service — the crew\\u0027s first project\"}'\n" +
207
+ " Return { action: \"created\", project_id: \"" + dashboardSlug + "\" }.\n\n" +
208
+ "Return JSON with action (\"exists\" or \"created\") and project_id (string).",
209
+ {
210
+ key: "project-1",
211
+ label: "Register dashboard project",
212
+ schema: {
213
+ type: "object",
214
+ properties: {
215
+ action: { type: "string", enum: ["exists", "created"] },
216
+ project_id: { type: "string" }
217
+ },
218
+ required: ["action", "project_id"]
219
+ }
220
+ }
221
+ );
222
+ } catch (e) {
223
+ return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Project registration failed", message: String(e.message || e) } };
224
+ }
225
+ log("Project: " + dashboardSlug + " " + projectResult.action);
226
+
227
+ // ── Phase 4: Cron jobs (declarative manifest) ──────────────────────────
180
228
  // The manifest lives in the repo at seed/crons.json. Body templates live
181
229
  // in seed/ with {crewHome} placeholders. Missing jobs are
182
230
  // created from the manifest; existing jobs converge to it — except `enabled`,
@@ -188,6 +236,7 @@ try {
188
236
  "Ensure the Muse Crew cron jobs match the repo's declarative manifest.\n\n" +
189
237
  "Manifest: " + crewRepoPath + "/seed/crons.json\n" +
190
238
  "crewHome: " + crewHome + "\n" +
239
+ "dashboardSlug: " + dashboardSlug + "\n" +
191
240
  "Id overrides (manifest id -> live id): " + JSON.stringify(cronIds) + "\n\n" +
192
241
  "Steps:\n" +
193
242
  "1. Read the manifest at " + crewRepoPath + "/seed/crons.json and parse it as JSON.\n" +
@@ -195,15 +244,19 @@ try {
195
244
  " or any entry lacks a required field (id, title, enabled, mode, schedule,\n" +
196
245
  " owner, body_template).\n" +
197
246
  "2. For each manifest entry, in order:\n" +
198
- " a. The live id is the override for entry.id when present, else entry.id.\n" +
247
+ " 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 + '-' + dashboardSlug (instance-safe by\n" +
249
+ " construction — two crew instances never share a cron id). The bare manifest\n" +
250
+ " id is never used as a live id.\n" +
199
251
  " b. Read the body template at " + crewRepoPath + "/seed/<entry.body_template>.\n" +
200
252
  " c. Replace all occurrences of {crewHome} with: " + crewHome + ".\n" +
253
+ " Replace all occurrences of {dashboardSlug} with: " + dashboardSlug + ".\n" +
201
254
  " d. Call cron_list and look for a job with the live id.\n" +
202
255
  " e. If missing, call cron_add with:\n" +
203
256
  " - id: the live id\n" +
204
257
  " - title, enabled, mode from the entry\n" +
205
258
  " - schedule: the entry's schedule object\n" +
206
- " - owner: the entry's owner\n" +
259
+ " - owner: the entry's owner with {dashboardSlug} replaced by: " + dashboardSlug + "\n" +
207
260
  " - timeout_secs: the entry's timeout_secs when present\n" +
208
261
  " - body: the resolved template text\n" +
209
262
  " Record action 'created' with no updated fields.\n" +
@@ -262,7 +315,10 @@ log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" +
262
315
  return {
263
316
  message: "Muse Crew initialized.",
264
317
  crewHome: crewHome,
318
+ dashboardSlug: dashboardSlug,
319
+ dashboardName: dashboardName,
265
320
  releaseHash: releaseResult.hash,
266
321
  scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
322
+ project: projectResult.action,
267
323
  crons: cronsResult.summary.crons
268
324
  };