muse-crew 0.6.8 → 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.8",
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
 
@@ -12,11 +12,10 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
12
12
 
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
- 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
  };
@@ -52,14 +53,17 @@ function gate0Decide(facts) {
52
53
  var gateFacts;
53
54
  try {
54
55
  gateFacts = await agent(
55
- "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" +
56
57
  "Requested crewHome: " + crewHome + "\\n\\n" +
57
58
  "Steps (run in shell):\\n" +
58
59
  "1. home=$(echo $HOME)\\n" +
59
60
  "2. Expand a leading ~/ in the requested crewHome against $HOME (leave other paths untouched).\\n" +
60
- "3. Run: git -C <expanded crewHome> rev-parse --git-dir; echo EXIT:$?\\n" +
61
- "4. Return JSON { home, crewHomeExpanded, gitExitCode }.\\n\\n" +
62
- "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.",
63
67
  {
64
68
  key: "gate-0",
65
69
  label: "Validate crew home",
@@ -91,8 +95,8 @@ if (!gateResult.repoValid) {
91
95
  return {
92
96
  __hatchWorkflowControl: "blocked",
93
97
  result: {
94
- blocked_reason: "crewHome is not a git repository",
95
- 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."
96
100
  }
97
101
  };
98
102
  }
@@ -109,11 +113,11 @@ try {
109
113
  "Release script (in repo): " + crewRepoPath + "/lib/crew-release.sh\n\n" +
110
114
  "Steps:\n" +
111
115
  "1. Run: test -f " + crewHome + "/crew-release.sh && echo EXISTS || echo MISSING\n" +
112
- "2. If EXISTS, run: " + crewHome + "/crew-release.sh current\n" +
116
+ "2. If EXISTS, run: " + crewHome + "/crew-release.sh current " + crewHome + "\n" +
113
117
  " Return { existed: true, hash: <current hash> }\n" +
114
118
  "3. If MISSING, bootstrap:\n" +
115
119
  " a. Run: bash " + crewRepoPath + "/lib/crew-release.sh init " + crewHome + " " + crewRepoPath + "\n" +
116
- " b. Verify: " + crewHome + "/crew-release.sh current\n" +
120
+ " b. Verify: " + crewHome + "/crew-release.sh current " + crewHome + "\n" +
117
121
  " c. Return { existed: false, hash: <hash from current> }\n\n" +
118
122
  "Return JSON with existed (boolean) and hash (string).",
119
123
  {
@@ -179,7 +183,48 @@ var scaffoldCreated = scaffoldResult.created ? scaffoldResult.created.length : 0
179
183
  var scaffoldSkipped = scaffoldResult.skipped ? scaffoldResult.skipped.length : 0;
180
184
  log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped");
181
185
 
182
- // ── 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) ──────────────────────────
183
228
  // The manifest lives in the repo at seed/crons.json. Body templates live
184
229
  // in seed/ with {crewHome} placeholders. Missing jobs are
185
230
  // created from the manifest; existing jobs converge to it — except `enabled`,
@@ -199,7 +244,10 @@ try {
199
244
  " or any entry lacks a required field (id, title, enabled, mode, schedule,\n" +
200
245
  " owner, body_template).\n" +
201
246
  "2. For each manifest entry, in order:\n" +
202
- " 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" +
203
251
  " b. Read the body template at " + crewRepoPath + "/seed/<entry.body_template>.\n" +
204
252
  " c. Replace all occurrences of {crewHome} with: " + crewHome + ".\n" +
205
253
  " Replace all occurrences of {dashboardSlug} with: " + dashboardSlug + ".\n" +
@@ -271,5 +319,6 @@ return {
271
319
  dashboardName: dashboardName,
272
320
  releaseHash: releaseResult.hash,
273
321
  scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
322
+ project: projectResult.action,
274
323
  crons: cronsResult.summary.crons
275
324
  };