muse-crew 0.6.8 → 0.7.1

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/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
@@ -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.1",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
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/"]`.
@@ -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.
Binary file
@@ -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 a pure client and is never a dependency.
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)
@@ -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 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 first project" },
7
8
  { name: "crons", title: "Create and converge cron jobs" }
8
9
  ]
9
10
  };
@@ -13,12 +14,18 @@ const inputs = args ?? {};
13
14
  const crewRepoPath = inputs.crewRepoPath;
14
15
  const crewHome = inputs.crewHome;
15
16
  const dashboardSlug = inputs.dashboardSlug;
17
+ const dashboardRepoPath = inputs.dashboardRepoPath;
16
18
  const dashboardName = inputs.dashboardName || "Muse Crew";
19
+ const crewName = inputs.crewName || null;
17
20
  const cronIds = inputs.cronIds || {};
18
21
 
19
22
  if (!crewRepoPath) throw new Error("crewRepoPath is required — path to muse-crew (git checkout or npm install)");
20
23
  if (!crewHome) throw new Error("crewHome is required — e.g. ~/workspace/.crew (must be inside the workspace)");
21
- if (!dashboardSlug) throw new Error("dashboardSlug is required slug of the task service artifact (must already exist)");
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)");
22
29
 
23
30
  const orchDir = crewHome + "/.orchestration";
24
31
 
@@ -30,10 +37,13 @@ const orchDir = crewHome + "/.orchestration";
30
37
  // copies). Fail closed here so no such instance can ever be created.
31
38
  // The dashboard repo is validated too: init registers the dashboard
32
39
  // project against it, and a non-git path would only fail later at the
33
- // 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.
34
44
  // Gate 0 decision logic is pure JS — the agent is a sensor, not a judge.
35
45
  // It reports raw facts (HOME, expanded paths, the git rev-parse exit
36
- // code); the launchable/repo_valid verdicts are computed here,
46
+ // codes); the launchable/repo_valid verdicts are computed here,
37
47
  // deterministically. An agent asked for a verdict can mis-apply the
38
48
  // prefix rule (e.g. the "$ws-evil" trailing-slash trick); code cannot.
39
49
  // (Prompt hardening is a smell — this is the mechanical version.)
@@ -43,23 +53,39 @@ function expandTilde(p, home) {
43
53
  return p;
44
54
  }
45
55
  function gate0Decide(facts) {
46
- // 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).
47
59
  var workspace = facts.home + "/workspace";
48
60
  var launchable = facts.crewHomeExpanded.indexOf(workspace + "/") === 0;
49
61
  var repoValid = facts.gitExitCode === 0;
50
- return { workspace: workspace, launchable: launchable, repoValid: repoValid };
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 };
51
65
  }
52
66
  var gateFacts;
53
67
  try {
54
68
  gateFacts = await agent(
55
- "Report raw facts about the requested crew home. Do NOT judge it just report.\\n\\n" +
56
- "Requested crewHome: " + crewHome + "\\n\\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" +
57
72
  "Steps (run in shell):\\n" +
58
73
  "1. home=$(echo $HOME)\\n" +
59
74
  "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.",
75
+ "3. If the expanded crewHome does not exist: mkdir -p <expanded crewHome>\\n" +
76
+ "4. Run: git -C <expanded crewHome> rev-parse --git-dir; echo EXIT:$?\\n" +
77
+ "5. If EXIT is not 0: run git -C <expanded crewHome> init -b main (or git init if -b not supported)\\n" +
78
+ "6. Re-run: git -C <expanded crewHome> rev-parse --git-dir; echo EXIT:$?\\n" +
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.",
63
89
  {
64
90
  key: "gate-0",
65
91
  label: "Validate crew home",
@@ -68,9 +94,11 @@ try {
68
94
  properties: {
69
95
  home: { type: "string" },
70
96
  crewHomeExpanded: { type: "string" },
71
- gitExitCode: { type: "number" }
97
+ gitExitCode: { type: "number" },
98
+ dashboardRepoExpanded: { type: ["string", "null"] },
99
+ dashboardRepoGitExitCode: { type: ["number", "null"] }
72
100
  },
73
- required: ["home", "crewHomeExpanded", "gitExitCode"]
101
+ required: ["home", "crewHomeExpanded", "gitExitCode", "dashboardRepoExpanded", "dashboardRepoGitExitCode"]
74
102
  }
75
103
  }
76
104
  );
@@ -91,12 +119,56 @@ if (!gateResult.repoValid) {
91
119
  return {
92
120
  __hatchWorkflowControl: "blocked",
93
121
  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."
122
+ blocked_reason: "crewHome git init failed",
123
+ message: "crewHome (" + gateFacts.crewHomeExpanded + ") could not be initialized as a git repository (git exit code " + gateFacts.gitExitCode + "). Check permissions and try again."
96
124
  }
97
125
  };
98
126
  }
99
- log("Gate 0: crewHome " + gateFacts.crewHomeExpanded + " is workspace-contained and a valid git repo");
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
+ }
100
172
 
101
173
  // ── Phase 1: Release ──────────────────────────────────────────────────
102
174
  phase("release");
@@ -109,11 +181,11 @@ try {
109
181
  "Release script (in repo): " + crewRepoPath + "/lib/crew-release.sh\n\n" +
110
182
  "Steps:\n" +
111
183
  "1. Run: test -f " + crewHome + "/crew-release.sh && echo EXISTS || echo MISSING\n" +
112
- "2. If EXISTS, run: " + crewHome + "/crew-release.sh current\n" +
184
+ "2. If EXISTS, run: " + crewHome + "/crew-release.sh current " + crewHome + "\n" +
113
185
  " Return { existed: true, hash: <current hash> }\n" +
114
186
  "3. If MISSING, bootstrap:\n" +
115
187
  " a. Run: bash " + crewRepoPath + "/lib/crew-release.sh init " + crewHome + " " + crewRepoPath + "\n" +
116
- " b. Verify: " + crewHome + "/crew-release.sh current\n" +
188
+ " b. Verify: " + crewHome + "/crew-release.sh current " + crewHome + "\n" +
117
189
  " c. Return { existed: false, hash: <hash from current> }\n\n" +
118
190
  "Return JSON with existed (boolean) and hash (string).",
119
191
  {
@@ -179,19 +251,87 @@ var scaffoldCreated = scaffoldResult.created ? scaffoldResult.created.length : 0
179
251
  var scaffoldSkipped = scaffoldResult.skipped ? scaffoldResult.skipped.length : 0;
180
252
  log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped");
181
253
 
182
- // ── Phase 3: Cron jobs (declarative manifest) ──────────────────────────
254
+ // ── Phase 3: Project registration ─────────────────────────────────────
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.
265
+ phase("project");
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 {
271
+ var safeDashboardName = dashboardName.split('"').join('\\"');
272
+ var safeDashboardRepo = gateFacts.dashboardRepoExpanded.split('"').join('\\"');
273
+ try {
274
+ projectResult = await agent(
275
+ "Register the dashboard as a project in the crew's task database.\n\n" +
276
+ "Crew home: " + crewHome + "\n" +
277
+ "Crew API: " + crewHome + "/current/lib/crew-api.js\n" +
278
+ "Dashboard slug (project id): " + dashboardSlug + "\n" +
279
+ "Dashboard name (display_name): " + dashboardName + "\n" +
280
+ "Dashboard repo path (repo_path): " + gateFacts.dashboardRepoExpanded + "\n\n" +
281
+ "Steps:\n" +
282
+ "1. Check if the project already exists:\n" +
283
+ " node " + crewHome + "/current/lib/crew-api.js --crew-home " + crewHome + " get-project --json '{\"id\": \"" + dashboardSlug + "\"}'\n" +
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" +
292
+ " Return { action: \"created\", project_id: \"" + dashboardSlug + "\" }.\n\n" +
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).",
295
+ {
296
+ key: "project-1",
297
+ label: "Register dashboard project",
298
+ schema: {
299
+ type: "object",
300
+ properties: {
301
+ action: { type: "string", enum: ["exists", "repaired", "created"] },
302
+ project_id: { type: "string" }
303
+ },
304
+ required: ["action", "project_id"]
305
+ }
306
+ }
307
+ );
308
+ } catch (e) {
309
+ return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Project registration failed", message: String(e.message || e) } };
310
+ }
311
+ log("Project: " + dashboardSlug + " " + projectResult.action + " (repo_path=" + gateFacts.dashboardRepoExpanded + ")");
312
+ } // end dashboard-mode project registration
313
+
314
+ // ── Phase 4: Cron jobs (declarative manifest) ──────────────────────────
183
315
  // The manifest lives in the repo at seed/crons.json. Body templates live
184
316
  // in seed/ with {crewHome} placeholders. Missing jobs are
185
317
  // created from the manifest; existing jobs converge to it — except `enabled`,
186
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.
187
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);
188
327
  var cronsResult;
189
328
  try {
190
329
  cronsResult = await agent(
191
330
  "Ensure the Muse Crew cron jobs match the repo's declarative manifest.\n\n" +
192
331
  "Manifest: " + crewRepoPath + "/seed/crons.json\n" +
193
332
  "crewHome: " + crewHome + "\n" +
194
- "dashboardSlug: " + dashboardSlug + "\n" +
333
+ "Instance id (cron id suffix): " + instanceId + "\n" +
334
+ "Cron owner: " + cronOwner + "\n" +
195
335
  "Id overrides (manifest id -> live id): " + JSON.stringify(cronIds) + "\n\n" +
196
336
  "Steps:\n" +
197
337
  "1. Read the manifest at " + crewRepoPath + "/seed/crons.json and parse it as JSON.\n" +
@@ -199,16 +339,18 @@ try {
199
339
  " or any entry lacks a required field (id, title, enabled, mode, schedule,\n" +
200
340
  " owner, body_template).\n" +
201
341
  "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" +
342
+ " a. The live id is the override for entry.id when present (inputs.cronIds[entry.id]).\n" +
343
+ " Otherwise, the live id is entry.id + '-' + \"" + instanceId + "\" (instance-safe by\n" +
344
+ " construction — two crew instances never share a cron id). The bare manifest\n" +
345
+ " id is never used as a live id.\n" +
203
346
  " b. Read the body template at " + crewRepoPath + "/seed/<entry.body_template>.\n" +
204
347
  " c. Replace all occurrences of {crewHome} with: " + crewHome + ".\n" +
205
- " Replace all occurrences of {dashboardSlug} with: " + dashboardSlug + ".\n" +
206
348
  " d. Call cron_list and look for a job with the live id.\n" +
207
349
  " e. If missing, call cron_add with:\n" +
208
350
  " - id: the live id\n" +
209
351
  " - title, enabled, mode from the entry\n" +
210
352
  " - schedule: the entry's schedule object\n" +
211
- " - owner: the entry's owner with {dashboardSlug} replaced by: " + dashboardSlug + "\n" +
353
+ " - owner: " + cronOwner + "\n" +
212
354
  " - timeout_secs: the entry's timeout_secs when present\n" +
213
355
  " - body: the resolved template text\n" +
214
356
  " Record action 'created' with no updated fields.\n" +
@@ -267,9 +409,12 @@ log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" +
267
409
  return {
268
410
  message: "Muse Crew initialized.",
269
411
  crewHome: crewHome,
412
+ crewName: crewName,
270
413
  dashboardSlug: dashboardSlug,
414
+ dashboardRepoPath: gateFacts.dashboardRepoExpanded,
271
415
  dashboardName: dashboardName,
272
416
  releaseHash: releaseResult.hash,
273
417
  scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
418
+ project: projectResult.action,
274
419
  crons: cronsResult.summary.crons
275
420
  };