muse-crew 0.1.0 → 0.1.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/AGENTS.md CHANGED
@@ -5,7 +5,7 @@ Muse Crew source repository. The repo is the product; the personal instance (`$C
5
5
  ## Structure
6
6
 
7
7
  - `API.md` — the Crew API contract: every action a task service must implement
8
- - `.orchestration/` — this project's crew configuration (project.json)
8
+ - `.orchestration/` — crew configuration notes (deploy config lives in the dashboard's project record — the single source of truth)
9
9
  - `identities/` — crew member character files and portraits
10
10
  - `lib/` — shell scripts for release, merge, worktree, and cleanup
11
11
  - `personas/` — QA perspective costumes for Hazel
package/API.md CHANGED
@@ -48,6 +48,13 @@ Atomically claim a task for a workflow step. Used by the dispatcher and by workf
48
48
  | `step` | string (1–120) | no | The workflow phase name |
49
49
  | `notes` | string (≤ 3000) | no | Defaults to `""` |
50
50
 
51
+ Returns one of:
52
+
53
+ - `{ "ok": true, "claimed": true, "session_id": "<uuid>", "session": {...} }`
54
+ - `{ "ok": true, "claimed": false, "reason": "already_claimed", "existing_session_id": "<uuid>" }`
55
+
56
+ `claimed` is the single field callers branch on. `reason` is present only when `claimed` is false.
57
+
51
58
  ### `recovertask`
52
59
 
53
60
  Send a stuck or failed task to a specific workflow phase for recovery.
@@ -102,6 +109,7 @@ Read the event timeline.
102
109
 
103
110
  | Field | Type | Required | Notes |
104
111
  |-------|------|----------|-------|
112
+ | `task_id` | uuid | no | Return only events for this task |
105
113
  | `limit` | integer (1–100) | no | Defaults to 50 |
106
114
 
107
115
  ---
@@ -129,12 +137,11 @@ Register a new project.
129
137
  | `id` | slug | yes | Unique project identifier |
130
138
  | `display_name` | string (1–120) | yes | |
131
139
  | `repo_path` | string (1–1000) | yes | Absolute path to the project's Git repo |
132
- | `deploy_type` | `artifact` · `repo` | yes | |
140
+ | `deploy_type` | `npm` · `artifact` · `vercel` · empty string | yes | Publish target: `npm` publishes the package to the npm registry, `artifact` rebuilds the live artifact, `vercel` is not yet implemented (blocks the task), empty skips the Publish phase |
133
141
  | `deploy_slug` | slug or null | no | Required when `deploy_type` is `artifact` |
134
142
  | `description` | string (≤ 3000) | no | |
135
143
  | `simultaneity` | integer (1–100) | no | Max concurrent tasks; defaults to 2 |
136
144
  | `quiesced` | boolean | no | Start paused; defaults to false |
137
- | `idle_hunting` | boolean | no | Defaults to false |
138
145
 
139
146
  ### `updateproject`
140
147
 
package/README.md CHANGED
@@ -14,7 +14,7 @@ Five things make it go.
14
14
 
15
15
  **Identities** are the characters who do the work — values, taste, writing style. What makes the reviewer exacting and the QA agent persistent.
16
16
 
17
- **Projects** connect a repository to a deploy target. Tasks belong to projects; the dispatcher routes each task through its project's configuration.
17
+ **Projects** connect a repository to a publish target. Every task's merged work is pushed to the project's private Git repo; the optional Publish phase then ships it to the target. Tasks belong to projects; the dispatcher routes each task through its project's configuration.
18
18
 
19
19
  **Scheduling** uses [Muse's](https://muse.ai) always-on cron. The dispatcher checks the board every few minutes, claims what's ready, and puts the right agent on the right phase. Review rejects bounce back automatically. The system keeps going until the work is done or stuck.
20
20
 
@@ -26,15 +26,15 @@ The repo ships workflows and identities for a software development crew — one
26
26
  |----------|------|----------------|
27
27
  | **Sage** | Triage | Fast routing — classifies and prioritizes before anything else runs |
28
28
  | **Mara** | Designer | Makes tasks solution-oriented before building starts |
29
- | **Wren** | Builder | Trusts the plan, lets commits speak. Also handles integration and deployment |
29
+ | **Wren** | Builder | Trusts the plan, lets commits speak. Also handles integration and publishing |
30
30
  | **Cass** | Reviewer | Fair but exacting — holds the spec as the contract until the work is tight |
31
31
  | **Hazel** | QA | Code-blind by design. Persistent. Wears persona costumes for multi-lens testing |
32
32
  | **Tate** | Docs | Observational, jargon-hostile |
33
33
 
34
34
  ```
35
- standard: Triage → Map → Build → Review → Integrate → Deploy → QA
36
- bugfix: Triage → Reproduce → Map → Build → Review → Integrate → Deploy → QA
37
- chore: Triage → Map → Build → Review → Integrate → Deploy
35
+ standard: Triage → Map → Build → Review → Integrate → Publish → QA
36
+ bugfix: Triage → Reproduce → Map → Build → Review → Integrate → Publish → QA
37
+ chore: Triage → Map → Build → Review → Integrate → Publish
38
38
  docs: Triage → Write → Review
39
39
  ```
40
40
 
package/docs/guide.md CHANGED
@@ -5,7 +5,7 @@ This is the full setup and operations reference. If you're new, start with the [
5
5
  ## Components
6
6
 
7
7
  **In this repo:**
8
- - **Workflow scripts** — walk each task through its phases (Triage → Map → Build → Review → Integrate → Deploy → QA). The defaults are built for software, but you can create your own workflows for any kind of work.
8
+ - **Workflow scripts** — walk each task through its phases (Triage → Map → Build → Review → Integrate → Publish → QA). The defaults are built for software, but you can create your own workflows for any kind of work.
9
9
  - A **dispatcher** — reads the board and puts the crew to work.
10
10
  - A **release system** — immutable releases with atomic symlink swap. Works with Git checkouts or npm installs.
11
11
  - An **`.orchestration/` scaffold** — identity files, persona files, workflow documentation, and feedback conventions.
@@ -118,20 +118,19 @@ Required fields:
118
118
  - `id` — kebab-case identifier, unique across all projects.
119
119
  - `display_name` — human-readable name.
120
120
  - `repo_path` — absolute path to the project's Git repository.
121
- - `deploy_type` — `"artifact"` (deploys via artifact edit) or `"repo"` (changes stay in the repo).
121
+ - `deploy_type` — names the project's **publish target**: `"npm"` (installs the immutable release and publishes the package to the npm registry), `"artifact"` (rebuilds the live artifact from disk), `"vercel"` (not yet implemented — blocks the task), or empty (no publish target — the Publish phase is skipped, for prototyping projects). Every project pushes merged work to its private Git repo in Integrate regardless of target — never force-push; a non-fast-forward rejection blocks the task.
122
122
 
123
123
  Optional fields:
124
124
  - `deploy_slug` — the artifact slug, required when `deploy_type` is `"artifact"`.
125
125
  - `description` — free text.
126
126
  - `simultaneity` — max concurrent tasks for this project (default: 2, range: 1–100).
127
127
  - `quiesced` — start paused (default: false).
128
- - `idle_hunting` — not yet implemented (default: false).
129
128
 
130
129
  ### What a registered project needs
131
130
 
132
131
  The project must have:
133
132
  - A Git repository at the specified `repo_path`.
134
- - For artifact deploy type: an existing web artifact at the specified `deploy_slug`.
133
+ - For the `artifact` publish target: an existing web artifact at the specified `deploy_slug`.
135
134
 
136
135
  The project does **not** need:
137
136
  - A `.orchestration/` folder (that belongs to the crew home, not the project).
@@ -199,9 +198,9 @@ Every 3 minutes, the `crew-poll` cron fires:
199
198
 
200
199
  | Workflow | Phases |
201
200
  |----------|--------|
202
- | standard | Triage → Map → Build → Review → Integrate → Deploy → QA |
203
- | bugfix | Triage → Reproduce → Map → Build → Review → Integrate → Deploy → QA |
204
- | chore | Triage → Map → Build → Review → Integrate → Deploy |
201
+ | standard | Triage → Map → Build → Review → Integrate → Publish → QA |
202
+ | bugfix | Triage → Reproduce → Map → Build → Review → Integrate → Publish → QA |
203
+ | chore | Triage → Map → Build → Review → Integrate → Publish |
205
204
  | docs | Triage → Write → Review |
206
205
 
207
206
  ## Identities
@@ -214,8 +213,8 @@ Each phase has an assigned identity — a character with a defined personality:
214
213
  | Map | **Mara** | Designer |
215
214
  | Build | **Wren** | Quietest one, trusts the plan |
216
215
  | Review | **Cass** | Fair but exacting — holds the spec as the contract |
217
- | Integrate | **Wren** | Merges the work |
218
- | Deploy | **Wren** | Ships the release |
216
+ | Integrate | **Wren** | Merges the work, pushes `main` to the repo |
217
+ | Publish | **Wren** | Ships the merged code to the publish target (skipped when none) |
219
218
  | QA | **Hazel** | Code-blind, persistent, wears persona costumes |
220
219
  | Reproduce | **Hazel** | Reproduces bugs before fixing |
221
220
  | Write | **Tate** | Docs writer, observational voice |
@@ -281,7 +280,7 @@ The full pause/resume/dispatch cycle has been proven end-to-end.
281
280
 
282
281
  Releases are immutable snapshots of the crew's runtime code.
283
282
 
284
- ### Deploy
283
+ ### Installing a release
285
284
 
286
285
  ```bash
287
286
  crew-release.sh deploy <source-path>
@@ -294,7 +293,7 @@ crew-release.sh deploy <source-path>
294
293
  5. Atomically swaps the `current` symlink to point at the new release.
295
294
  6. Prunes old releases, keeping the 5 most recent.
296
295
 
297
- The `crew-release.sh` manager itself lives at `crewHome/crew-release.sh`, outside the managed releases, so it's never overwritten by a deploy.
296
+ The `crew-release.sh` manager itself lives at `crewHome/crew-release.sh`, outside the managed releases, so it's never overwritten by a release activation.
298
297
 
299
298
  ### Rollback
300
299
 
@@ -306,7 +305,7 @@ Activates the second-newest release by modification time.
306
305
 
307
306
  ### Safety
308
307
 
309
- Running workflows are safe from mid-deploy changes. Muse snapshots workflow JavaScript at launch time, so a deploy that swaps the symlink does not affect workflows already in flight.
308
+ Running workflows are safe from mid-publish changes. Muse snapshots workflow JavaScript at launch time, so a deploy that swaps the symlink does not affect workflows already in flight.
310
309
 
311
310
  ### Release identity
312
311
 
@@ -322,12 +321,10 @@ crew-release.sh current
322
321
 
323
322
  2. **Stuck sessions.** If a workflow dies mid-run, its session stays `running` indefinitely and the cron skips it every tick. Manual cleanup is currently required.
324
323
 
325
- 3. **No idle hunting.** The `idle_hunting` field exists in the project schema but is not implemented. No proactive QA runs when the board is empty.
324
+ 3. **No streaming.** The polling loop checks every 3 minutes. There is no webhook or event-driven dispatch.
326
325
 
327
- 4. **No streaming.** The polling loop checks every 3 minutes. There is no webhook or event-driven dispatch.
326
+ 4. **Single dispatcher.** Only one cron runs the dispatcher. Concurrent ticks are prevented by the session-claiming atomicity, but there is no explicit distributed lock.
328
327
 
329
- 5. **Single dispatcher.** Only one cron runs the dispatcher. Concurrent ticks are prevented by the session-claiming atomicity, but there is no explicit distributed lock.
328
+ 5. **No automatic retry escalation.** Failed tasks retry the same phase on the next tick. There is no backoff, retry limit, or escalation path.
330
329
 
331
- 6. **No automatic retry escalation.** Failed tasks retry the same phase on the next tick. There is no backoff, retry limit, or escalation path.
332
-
333
- 7. **QA is code-blind.** Hazel uses `artifact_inspect` for visual/functional testing. She cannot read source code — this is by design (context isolation), but it means QA catches only what's visible or inspectable through the artifact's public interface.
330
+ 6. **QA is code-blind.** Hazel uses `artifact_inspect` for visual/functional testing. She cannot read source code — this is by design (context isolation), but it means QA catches only what's visible or inspectable through the artifact's public interface.
@@ -2,9 +2,10 @@
2
2
  # orphan-sweep.sh — find and clean orphaned worktrees + stale merge locks
3
3
  #
4
4
  # Usage:
5
- # orphan-sweep.sh report — list orphans and their state (read-only)
5
+ # orphan-sweep.sh report — list orphans (read-only for worktrees);
6
+ # force-releases stale locks with dead PIDs
6
7
  # orphan-sweep.sh clean — remove safe-to-clean orphans (merged branches only)
7
- # and break stale merge locks (>30 min)
8
+ # and force-release stale merge locks (>30 min)
8
9
  #
9
10
  # "Safe to clean" means the task branch is fully merged into main.
10
11
  # Dirty or unmerged worktrees are always preserved and reported.
@@ -40,13 +41,11 @@ if [ -f "$LOCK_FILE" ]; then
40
41
  else
41
42
  echo "STALE_LOCK: held ${age_min}m by $lock_holder (pid ${lock_pid:-none} dead) — threshold ${STALE_LOCK_MIN}m"
42
43
  found=1
43
- if [ "$cmd" = "clean" ]; then
44
- rm -f "$LOCK_FILE"
45
- if [ ! -f "$LOCK_FILE" ]; then
46
- echo " → broken"
47
- else
48
- echo " → ERROR: lock file still exists after rm at $LOCK_FILE"
49
- fi
44
+ rm -f "$LOCK_FILE"
45
+ if [ ! -f "$LOCK_FILE" ]; then
46
+ echo " → released (dead PID)"
47
+ else
48
+ echo " → ERROR: lock file still exists after rm at $LOCK_FILE"
50
49
  fi
51
50
  fi
52
51
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -19,21 +19,24 @@ Update the task with a solution-oriented spec. Research options, pick the shorte
19
19
 
20
20
  ### Build
21
21
  **Identity:** Wren
22
- Execute the spec. Write the code, write the tests.
22
+ Execute the spec. Write the code, write the tests. Update public docs for public-affecting changes — docs ship with implementation.
23
23
 
24
24
  ### Review
25
25
  **Identity:** Cass
26
26
  **Constraint:** Independent context. Do not share the builder's context. Read the spec and review the changes cold.
27
+ Verify public docs are current for public-affecting changes; reject if stale.
27
28
 
28
29
  ### Integrate
29
30
  **Identity:** Wren
30
31
  Merge changes into the target branch. Create the commit with a clear message.
32
+ Then push `main` to the project's private remote. Never force-push: a non-fast-forward rejection blocks the task.
31
33
 
32
- ### Deploy
34
+ ### Publish
33
35
  **Identity:** Wren
34
- Deploy the changes.
36
+ Optional. Ships the merged code to the project's publish target (`deploy_type` on the project record): `npm` installs the immutable release and publishes the package to the registry; `artifact` rebuilds the live artifact from disk; `vercel` is not implemented and blocks the task; an empty target skips this phase. The repo push already happened in Integrate — Publish never touches git.
35
37
 
36
38
  ### QA
37
39
  **Identity:** Hazel
38
40
  **Personas:** Yes — Hazel wears a persona costume to test from that perspective.
39
41
  **Constraint:** No code context. Same tools and process as reproduction. Pass or fail. File follow-up tasks for related issues found during testing.
42
+ Fail if public-affecting changes lack public docs. Public docs are not code — Hazel reads them as a user would.
@@ -14,16 +14,18 @@ Research options, pick the path, write the spec.
14
14
 
15
15
  ### Build
16
16
  **Identity:** Wren
17
- Execute the spec.
17
+ Execute the spec. Update public docs for public-affecting changes — docs ship with implementation.
18
18
 
19
19
  ### Review
20
20
  **Identity:** Cass
21
21
  **Constraint:** Independent context. Review cold.
22
+ Verify public docs are current for public-affecting changes; reject if stale.
22
23
 
23
24
  ### Integrate
24
25
  **Identity:** Wren
25
26
  Merge changes into the target branch. Create the commit with a clear message.
27
+ Then push `main` to the project's private remote. Never force-push: a non-fast-forward rejection blocks the task.
26
28
 
27
- ### Deploy
29
+ ### Publish
28
30
  **Identity:** Wren
29
- Deploy the changes.
31
+ Optional. Ships the merged code to the project's publish target (`deploy_type` on the project record): `npm` installs the immutable release and publishes the package to the registry; `artifact` rebuilds the live artifact from disk; `vercel` is not implemented and blocks the task; an empty target skips this phase. The repo push already happened in Integrate — Publish never touches git.
@@ -14,21 +14,24 @@ Research options, pick the path, write the spec.
14
14
 
15
15
  ### Build
16
16
  **Identity:** Wren
17
- Execute the spec.
17
+ Execute the spec. Update public docs for public-affecting changes — docs ship with implementation.
18
18
 
19
19
  ### Review
20
20
  **Identity:** Cass
21
21
  **Constraint:** Independent context. Review cold.
22
+ Verify public docs are current for public-affecting changes; reject if stale.
22
23
 
23
24
  ### Integrate
24
25
  **Identity:** Wren
25
26
  Merge changes into the target branch. Create the commit with a clear message.
27
+ Then push `main` to the project's private remote. Never force-push: a non-fast-forward rejection blocks the task.
26
28
 
27
- ### Deploy
29
+ ### Publish
28
30
  **Identity:** Wren
29
- Deploy the changes.
31
+ Optional. Ships the merged code to the project's publish target (`deploy_type` on the project record): `npm` installs the immutable release and publishes the package to the registry; `artifact` rebuilds the live artifact from disk; `vercel` is not implemented and blocks the task; an empty target skips this phase. The repo push already happened in Integrate — Publish never touches git.
30
32
 
31
33
  ### QA
32
34
  **Identity:** Hazel
33
35
  **Personas:** Yes — Hazel wears a persona costume to test from that perspective.
34
36
  **Constraint:** No code context. Pass or fail. File follow-up tasks for related issues found during testing.
37
+ Fail if public-affecting changes lack public docs. Public docs are not code — Hazel reads them as a user would.
@@ -4,7 +4,7 @@ Executable Muse workflow scripts (JavaScript). These are what the workflow runti
4
4
 
5
5
  - `crew-dispatch.js` — reads the board, claims eligible tasks, returns structured launch records
6
6
  - `crew-init.js` — sets up a new crew instance: orchestration folders, dashboard, cron, sample project
7
- - `standard.js` — default task workflow: Triage → Map → Build → Review → Integrate → Deploy → QA
7
+ - `standard.js` — default task workflow: Triage → Map → Build → Review → Integrate → Publish → QA
8
8
  - `bugfix.js` — adds Reproduce after Triage
9
9
  - `chore.js` — drops QA (low-risk)
10
10
  - `docs.js` — Tate writes, Cass reviews
@@ -1,7 +1,7 @@
1
1
  export const meta = {
2
2
  name: "crew-bugfix",
3
- description: "Bugfix workflow: Triage → Reproduce → Map → Build → Review → Integrate → Deploy → QA",
4
- phases: ["Triage", "Reproduce", "Map", "Build", "Review", "Integrate", "Deploy", "QA"],
3
+ description: "Bugfix workflow: Triage → Reproduce → Map → Build → Review → Integrate → Publish → QA",
4
+ phases: ["Triage", "Reproduce", "Map", "Build", "Review", "Integrate", "Publish", "QA"],
5
5
  steps: [
6
6
  { name: "Triage", identity: "sage" },
7
7
  { name: "Reproduce", identity: "hazel" },
@@ -9,7 +9,7 @@ export const meta = {
9
9
  { name: "Build", identity: "wren" },
10
10
  { name: "Review", identity: "cass" },
11
11
  { name: "Integrate", identity: "wren" },
12
- { name: "Deploy", identity: "wren" },
12
+ { name: "Publish", identity: "wren" },
13
13
  { name: "QA", identity: "hazel" }
14
14
  ],
15
15
  reworkTarget: "Build"
@@ -32,12 +32,14 @@ const MERGE_LOCK_SRC = crewHome + "/lib/merge-lock.sh";
32
32
  const RUN_LIB = "/tmp/crew-lib-" + taskId;
33
33
  const LIFECYCLE = RUN_LIB + "/worktree-lifecycle.sh";
34
34
  const MERGE_LOCK = RUN_LIB + "/merge-lock.sh";
35
+ const ORPHAN_SWEEP_SRC = crewHome + "/lib/orphan-sweep.sh";
36
+ const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
35
37
 
36
38
  // Project config — passed by dispatcher, falls back to dashboard defaults
37
39
  const projectConfig = inputs.project_config || {};
38
40
  const REPO_PATH = projectConfig.repo_path || "~/workspace/ts-spaces/orchestra-dashboard";
39
- const DEPLOY_TYPE = projectConfig.deploy_type || "artifact";
40
- const DEPLOY_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
41
+ const PUBLISH_TYPE = projectConfig.deploy_type || "";
42
+ const PUBLISH_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
41
43
  const PROJECT_DESC = projectConfig.description || "React + TypeScript web dashboard (client/src/, server/src/, drizzle/)";
42
44
  const RELEASE_SCRIPT = crewHome + "/crew-release.sh";
43
45
 
@@ -61,13 +63,15 @@ const STEPS = [
61
63
  { name: "Build", identity: "wren" },
62
64
  { name: "Review", identity: "cass" },
63
65
  { name: "Integrate", identity: "wren" },
64
- { name: "Deploy", identity: "wren" },
66
+ { name: "Publish", identity: "wren" },
65
67
  { name: "QA", identity: "hazel" }
66
68
  ];
67
69
  const BUILD_INDEX = STEPS.findIndex(s => s.name === 'Build');
68
70
  if (BUILD_INDEX < 0) throw new Error("STEPS missing 'Build' step");
69
- const MAX_REWORK = 2;
70
- let reworkCount = 0;
71
+ // Shared rework budget: Review and QA rejections draw from the SAME pool of 2.
72
+ // E.g. 2 Review bounces + 1 QA bounce = 3 total > budget -> task blocks.
73
+ const MAX_TOTAL_REWORK = 2;
74
+ let totalReworkCount = 0;
71
75
  let rejectionNotes = inputs.rejection_notes || "";
72
76
  let mapperSpec = "";
73
77
  let i = startStepIndex;
@@ -75,17 +79,35 @@ let i = startStepIndex;
75
79
  // Pin lifecycle scripts
76
80
  await agent(
77
81
  "Snapshot lifecycle scripts for version pinning.\n" +
78
- "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK,
82
+ "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + ORPHAN_SWEEP,
79
83
  { key: "pin-lifecycle", label: "Pinning lifecycle scripts", schema: { type: "object" } }
80
84
  );
81
85
 
82
86
  while (i < STEPS.length) {
83
87
  const step = STEPS[i];
84
- const isFirstClaim = (i === startStepIndex && reworkCount === 0);
88
+ const isFirstClaim = (i === startStepIndex && totalReworkCount === 0);
85
89
 
86
90
  phase(step.name);
87
91
  log(step.name + " step (" + step.identity + ") for task " + taskId);
88
92
 
93
+ // Publish is optional and target-based. Empty target = prototyping project: skip the phase.
94
+ // Unknown target (incl. legacy "repo") = config error: block.
95
+ if (step.name === "Publish" && !PUBLISH_TYPE) {
96
+ log("Publish skipped for task " + taskId + " — no publish target configured (deploy_type empty)");
97
+ await agent(
98
+ "Release the merge lock and clean up without publishing.\n" +
99
+ "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
100
+ "If the output contains DEPLOYED, the lock is released and the worktree is cleaned up.",
101
+ { key: "publish-skip-cleanup", label: "Skipping Publish (no target)", schema: { type: "object" } }
102
+ );
103
+ i++;
104
+ continue;
105
+ }
106
+ if (step.name === "Publish" && PUBLISH_TYPE !== "npm" && PUBLISH_TYPE !== "artifact" && PUBLISH_TYPE !== "vercel") {
107
+ log("Unknown publish target for task " + taskId + ": " + PUBLISH_TYPE);
108
+ return { status: "blocked", task_id: taskId, reason: "Unknown publish target '" + PUBLISH_TYPE + "' — expected 'npm', 'artifact', 'vercel', or empty (skip publish)." };
109
+ }
110
+
89
111
  // Claim session
90
112
  let activeSessionId;
91
113
  if (isFirstClaim && firstSessionId) {
@@ -94,10 +116,10 @@ while (i < STEPS.length) {
94
116
  const claimResult = await agent(
95
117
  "Claim a session for this task step.\n" +
96
118
  "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
97
- "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started" + (reworkCount > 0 ? " (rework #" + reworkCount + ")" : "") + "\" }.\n" +
119
+ "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started" + (totalReworkCount > 0 ? " (rework #" + totalReworkCount + ")" : "") + "\" }.\n" +
98
120
  "Return the session_id from the response.",
99
121
  {
100
- key: "claim-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
122
+ key: "claim-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
101
123
  label: "Claiming " + step.name,
102
124
  schema: {
103
125
  type: "object",
@@ -137,8 +159,10 @@ while (i < STEPS.length) {
137
159
  (mapperSpec ? "MAPPER'S SPEC (implement exactly this):\n" + mapperSpec + "\n\n" : "") +
138
160
  "Your working directory: " + REPO_PATH + "/.worktrees/" + taskId + "/\n" +
139
161
  "This is the project source: " + PROJECT_DESC + "\n" +
140
- "Edit the TypeScript source files directly. Do NOT use artifact_edit — that happens in the Deploy phase.\n" +
141
- "Do not add unrequested features. Build exactly what the spec calls for.\n\n" +
162
+ "Edit the TypeScript source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
163
+ "Do not add unrequested features. Build exactly what the spec calls for.\n" +
164
+ "PUBLIC DOCS: If your change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), update the public docs in the same commit — API.md for API changes. Documentation and implementation ship together.\n\n" +
165
+ (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, so you choose the package version. If this change warrants a published release (anything a consumer can observe: workflow behavior, phase lists, identities, published docs, API), bump the version in package.json with semver (patch for fixes, minor for new behavior, major for breaking changes) and state the chosen version and why in your summary. If the change is internal-only, leave the version unchanged and say so. Check the registry first — npm view muse-crew version — and never re-publish an existing version.\n\n" : "") +
142
166
  "STEP 3: Commit your changes.\n" +
143
167
  "cd " + REPO_PATH + "/.worktrees/" + taskId + "\n" +
144
168
  "git add -A\n" +
@@ -154,6 +178,8 @@ while (i < STEPS.length) {
154
178
  "You can also read specific files in the worktree at:\n" +
155
179
  REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
156
180
  "Check quality, correctness, and spec compliance.\n" +
181
+ "Check that public-affecting changes have matching public doc updates (API.md or the published API contract). If the docs are missing or inaccurate, reject with notes on what is stale.\n" +
182
+ (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry. Validate the builder's version choice: package.json must hold valid semver; if the version was bumped it must be greater than the registry version (npm view muse-crew version), the bump scope (patch/minor/major) must fit the change, and exactly one version field may change. If the version is invalid, already published, or mis-scoped, reject with notes.\n" : "") +
157
183
  "If the work passes review, your final response MUST be valid JSON and nothing else: { \"passed\": true, \"summary\": \"approval notes\" }.\n" +
158
184
  "If the work fails review, your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"rejection notes explaining what needs to change\" }.\n" +
159
185
  "No prose, no markdown, just the JSON object.";
@@ -163,45 +189,75 @@ while (i < STEPS.length) {
163
189
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " integrate " + taskId + " \"merge: fix: " + safeTitle + "\"\n\n" +
164
190
  "Read the output:\n" +
165
191
  "- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
166
- "- If it contains LOCK_HELD, another task is deploying. Set passed to false.\n" +
192
+ "- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Set passed to false.\n" +
167
193
  "- If it contains CONFLICT, a merge conflict occurred. Set passed to false with details.\n" +
168
194
  "- If it contains ERROR, something else failed. Set passed to false.\n\n" +
195
+ "\n" +
196
+ "STEP 2: Push the merged main to the remote repository.\n" +
197
+ "Run: cd " + REPO_PATH + " && git push origin main\n" +
198
+ "- If the push succeeds, report the merged commit hash.\n" +
199
+ "- If the push is rejected as non-fast-forward (the remote has commits not present locally),\n" +
200
+ " NEVER force-push. Do not run any --force variant. Set passed to false with summary:\n" +
201
+ " 'git push origin main rejected as non-fast-forward — remote main has diverged; manual resolution required'.
202
+ \n\n" +
169
203
  "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true/false }. No prose, no markdown, just the JSON object.";
170
204
 
171
- } else if (step.name === "Deploy") {
172
- if (DEPLOY_TYPE === "repo") {
173
- // Repo projects: install immutable release and atomically activate
174
- instructions = "Deploy repo changes via the immutable release system.\n\n" +
175
- "STEP 0: Refresh the merge lock to prevent stale-lock breaking during deploy.\n" +
205
+ } else if (step.name === "Publish") {
206
+ if (PUBLISH_TYPE === "npm") {
207
+ // npm packages: immutable release + pack + publish to the registry (push is universal in Integrate)
208
+ instructions = "Publish the npm package to the registry.\n\n" +
209
+ "The repo push already happened in Integrate — do NOT push to git in this phase, and NEVER force-push.\n" +
210
+ "Version discipline: publish ships the exact version merged in Integrate (Build applied it, Review validated it). Do not bump the version here.\n\n" +
211
+ "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
176
212
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
177
- "STEP 1: Install and activate the new release.\n" +
213
+ "STEP 1: Install and activate the immutable release.\n" +
178
214
  "Run: " + RELEASE_SCRIPT + " deploy " + REPO_PATH + "\n" +
179
215
  "Verify the output contains INSTALLED and ACTIVATED (or EXISTS and ACTIVATED if unchanged).\n\n" +
180
- "STEP 2: Finalize.\n" +
216
+ "STEP 2: Check whether the package version needs publishing.\n" +
217
+ "Read the version from: cd " + REPO_PATH + " && node -p \"require('./package.json').version\"\n" +
218
+ "Check the registry: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
219
+ "If the local version matches the registry version, the version is already live — skip to STEP 5.\n\n" +
220
+ "STEP 3: Pack and publish.\n" +
221
+ "Run: cd " + REPO_PATH + " && npm pack\n" +
222
+ "Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-$(node -p \"require('" + REPO_PATH + "/package.json').version\").tgz\n" +
223
+ "If publish fails with 'You cannot publish over the previously published versions', the version is already live — continue to STEP 4.\n\n" +
224
+ "STEP 4: Verify.\n" +
225
+ "Run: npm view muse-crew version\n" +
226
+ "Confirm it matches the local package.json version.\n\n" +
227
+ "STEP 5: Finalize.\n" +
181
228
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
182
229
  "If the output contains DEPLOYED, finalization is complete.\n\n" +
183
- "Your final response MUST be valid JSON and nothing else: { \"summary\": \"release activated and finalized\", \"passed\": true }.\n" +
230
+ "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
184
231
  "No prose, no markdown, just the JSON object.";
185
- } else {
186
- instructions = "Deploy the merged code to the live artifact.\n\n" +
187
- "STEP 0: Refresh the merge lock to prevent stale-lock breaking during deploy.\n" +
232
+ } else if (PUBLISH_TYPE === "artifact") {
233
+ // Artifact projects: rebuild the live artifact via artifact_edit, then finalize
234
+ instructions = "Publish the merged code to the live artifact.\n\n" +
235
+ "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
236
+ "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
188
237
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
189
- "STEP 1: Deploy to the live artifact.\n" +
238
+ "STEP 1: Publish to the live artifact.\n" +
190
239
  "Get the change summary: cd " + REPO_PATH + " && git log -1 --stat\n" +
191
- "Then call artifact_edit with slug \"" + DEPLOY_SLUG + "\" and verbatim_request:\n" +
240
+ "Then call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
192
241
  "'Rebuild the application from current source. Do not modify any source files — just rebuild and deploy what is on disk.'\n" +
193
242
  "Wait for the build to complete by polling artifact_status until it is no longer running.\n\n" +
194
243
  "STEP 2: Finalize.\n" +
195
244
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
196
- "If the output contains DEPLOYED, deployment is complete.\n\n" +
245
+ "If the output contains DEPLOYED, publishing is complete.\n\n" +
197
246
  "If artifact_edit failed, still run post-deploy to release the merge lock and clean up.\n" +
198
- "Report the failure: { \"passed\": false, \"summary\": \"artifact deployment failed: [details]\" }.\n\n" +
199
- "Your final response MUST be valid JSON and nothing else: { \"summary\": \"deployed changes\", \"passed\": true }.\n" +
247
+ "Report the failure: { \"passed\": false, \"summary\": \"artifact publish failed: [details]\" }.\n\n" +
248
+ "Your final response MUST be valid JSON and nothing else: { \"summary\": \"published changes\", \"passed\": true }.\n" +
249
+ "No prose, no markdown, just the JSON object.";
250
+ } else if (PUBLISH_TYPE === "vercel") {
251
+ // vercel publish is not yet implemented — block without inventing behavior
252
+ instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
253
+ "Do NOT invent publish behavior — do not guess CLI commands, APIs, or deployment steps.\n" +
254
+ "Your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"vercel publish not yet implemented\" }.\n" +
200
255
  "No prose, no markdown, just the JSON object.";
201
256
  }
202
-
203
257
  } else if (step.name === "QA") {
204
258
  instructions = "Final QA testing. You are CODE-BLIND — do NOT read source code.\n" +
259
+ "Public docs (API.md, README, published action schemas) are NOT source code — read them freely, exactly as a user would.\n" +
260
+ "DOCS GATE: If the fix is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), verify the public docs describe it. If public docs are missing or stale, FAIL with { \"passed\": false, \"summary\": \"public docs missing/stale for [the change]\" }. QA always fails when public-affecting changes lack public docs. Guide/tutorial gaps are lower priority — file a follow-up task for those instead of failing.\n" +
205
261
  "To test, use artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\" with action \"getstate\" (args {}) to read current sessions, events, and tasks.\n" +
206
262
  "Verify the fix by checking that session notes in the returned data now handle newlines correctly.\n" +
207
263
  "You can also check specific data with the getevents action.\n" +
@@ -216,8 +272,8 @@ while (i < STEPS.length) {
216
272
  var eventPreamble = "";
217
273
  if (step.name !== "Review") {
218
274
  eventPreamble = "CONTEXT: First, fetch this task's event history for background.\n" +
219
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"limit\": 100 }.\n" +
220
- "Look through returned events for entries matching task_id \"" + taskId + "\". They contain notes and decisions from prior phases.\n\n";
275
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"task_id\": \"" + taskId + "\" }.\n" +
276
+ "The returned events are filtered to this task. They contain notes and decisions from prior phases.\n\n";
221
277
  }
222
278
 
223
279
  var stepResult;
@@ -234,7 +290,7 @@ while (i < STEPS.length) {
234
290
  "CONSTRAINT: Do NOT call logevent or upsertagentsession — the workflow handles all phase tracking after your step completes.\n\n" +
235
291
  "Stay in character. Do the work thoroughly.",
236
292
  {
237
- key: "work-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
293
+ key: "work-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
238
294
  label: step.identity + ": " + step.name + " on \"" + taskTitle + "\"",
239
295
  timeoutMs: 3600000,
240
296
  schema: WORK_SCHEMA
@@ -282,7 +338,7 @@ while (i < STEPS.length) {
282
338
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
283
339
  "{ \"task_id\": \"" + taskId + "\", \"type\": \"" + status + "\", \"identity\": \"" + step.identity + "\", \"message\": \"" + step.name + " " + status + " by " + step.identity + "\" }.",
284
340
  {
285
- key: "record-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
341
+ key: "record-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
286
342
  label: "Recording " + step.name + " result",
287
343
  schema: { type: "object" }
288
344
  }
@@ -302,14 +358,14 @@ while (i < STEPS.length) {
302
358
 
303
359
  // Review/QA rejection bounces to Build
304
360
  if (!passed && (step.name === "Review" || step.name === "QA")) {
305
- reworkCount++;
306
- if (reworkCount > MAX_REWORK) {
307
- log("Max rework attempts reached for task " + taskId + " — worktree preserved at .worktrees/" + taskId + " for manual inspection");
308
- return { status: "blocked", task_id: taskId, reason: "Exceeded " + MAX_REWORK + " rework attempts after " + step.name + " rejection. Worktree preserved." };
361
+ totalReworkCount++;
362
+ if (totalReworkCount > MAX_TOTAL_REWORK) {
363
+ log("Shared rework budget exhausted for task " + taskId + " — worktree preserved at .worktrees/" + taskId + " for manual inspection");
364
+ return { status: "blocked", task_id: taskId, reason: "Exceeded shared rework budget (" + MAX_TOTAL_REWORK + " total rework attempts across Review and QA) after " + step.name + " rejection. Worktree preserved." };
309
365
  }
310
366
  rejectionNotes = summary;
311
367
  i = BUILD_INDEX;
312
- log(step.name + " rejected — bouncing to Build (rework #" + reworkCount + ")");
368
+ log(step.name + " rejected — bouncing to Build (rework #" + totalReworkCount + " of " + MAX_TOTAL_REWORK + ")");
313
369
  continue;
314
370
  }
315
371
 
@@ -319,10 +375,10 @@ while (i < STEPS.length) {
319
375
  return { status: "blocked", task_id: taskId, reason: "Integration failed: " + summary };
320
376
  }
321
377
 
322
- // Deploy failure blocks the task
323
- if (!passed && step.name === "Deploy") {
324
- log("Deploy failed for task " + taskId + ": " + summary);
325
- return { status: "blocked", task_id: taskId, reason: "Deploy failed: " + summary };
378
+ // Publish failure blocks the task
379
+ if (!passed && step.name === "Publish") {
380
+ log("Publish failed for task " + taskId + ": " + summary);
381
+ return { status: "blocked", task_id: taskId, reason: "Publish failed: " + summary };
326
382
  }
327
383
 
328
384
  i++;
@@ -1,14 +1,14 @@
1
1
  export const meta = {
2
2
  name: "crew-chore",
3
- description: "Chore workflow: Triage → Map → Build → Review → Integrate → Deploy",
4
- phases: ["Triage", "Map", "Build", "Review", "Integrate", "Deploy"],
3
+ description: "Chore workflow: Triage → Map → Build → Review → Integrate → Publish",
4
+ phases: ["Triage", "Map", "Build", "Review", "Integrate", "Publish"],
5
5
  steps: [
6
6
  { name: "Triage", identity: "sage" },
7
7
  { name: "Map", identity: "mara" },
8
8
  { name: "Build", identity: "wren" },
9
9
  { name: "Review", identity: "cass" },
10
10
  { name: "Integrate", identity: "wren" },
11
- { name: "Deploy", identity: "wren" }
11
+ { name: "Publish", identity: "wren" }
12
12
  ],
13
13
  reworkTarget: "Build"
14
14
  };
@@ -30,12 +30,14 @@ const MERGE_LOCK_SRC = crewHome + "/lib/merge-lock.sh";
30
30
  const RUN_LIB = "/tmp/crew-lib-" + taskId;
31
31
  const LIFECYCLE = RUN_LIB + "/worktree-lifecycle.sh";
32
32
  const MERGE_LOCK = RUN_LIB + "/merge-lock.sh";
33
+ const ORPHAN_SWEEP_SRC = crewHome + "/lib/orphan-sweep.sh";
34
+ const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
33
35
 
34
36
  // Project config — passed by dispatcher, falls back to dashboard defaults
35
37
  const projectConfig = inputs.project_config || {};
36
38
  const REPO_PATH = projectConfig.repo_path || "~/workspace/ts-spaces/orchestra-dashboard";
37
- const DEPLOY_TYPE = projectConfig.deploy_type || "artifact";
38
- const DEPLOY_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
39
+ const PUBLISH_TYPE = projectConfig.deploy_type || "";
40
+ const PUBLISH_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
39
41
  const PROJECT_DESC = projectConfig.description || "React + TypeScript web dashboard (client/src/, server/src/, drizzle/)";
40
42
  const RELEASE_SCRIPT = crewHome + "/crew-release.sh";
41
43
 
@@ -58,7 +60,7 @@ const STEPS = [
58
60
  { name: "Build", identity: "wren" },
59
61
  { name: "Review", identity: "cass" },
60
62
  { name: "Integrate", identity: "wren" },
61
- { name: "Deploy", identity: "wren" }
63
+ { name: "Publish", identity: "wren" }
62
64
  ];
63
65
  const BUILD_INDEX = STEPS.findIndex(s => s.name === 'Build');
64
66
  if (BUILD_INDEX < 0) throw new Error("STEPS missing 'Build' step");
@@ -71,7 +73,7 @@ let i = startStepIndex;
71
73
  // Pin lifecycle scripts
72
74
  await agent(
73
75
  "Snapshot lifecycle scripts for version pinning.\n" +
74
- "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK,
76
+ "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + ORPHAN_SWEEP,
75
77
  { key: "pin-lifecycle", label: "Pinning lifecycle scripts", schema: { type: "object" } }
76
78
  );
77
79
 
@@ -82,6 +84,24 @@ while (i < STEPS.length) {
82
84
  phase(step.name);
83
85
  log(step.name + " step (" + step.identity + ") for task " + taskId);
84
86
 
87
+ // Publish is optional and target-based. Empty target = prototyping project: skip the phase.
88
+ // Unknown target (incl. legacy "repo") = config error: block.
89
+ if (step.name === "Publish" && !PUBLISH_TYPE) {
90
+ log("Publish skipped for task " + taskId + " — no publish target configured (deploy_type empty)");
91
+ await agent(
92
+ "Release the merge lock and clean up without publishing.\n" +
93
+ "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
94
+ "If the output contains DEPLOYED, the lock is released and the worktree is cleaned up.",
95
+ { key: "publish-skip-cleanup", label: "Skipping Publish (no target)", schema: { type: "object" } }
96
+ );
97
+ i++;
98
+ continue;
99
+ }
100
+ if (step.name === "Publish" && PUBLISH_TYPE !== "npm" && PUBLISH_TYPE !== "artifact" && PUBLISH_TYPE !== "vercel") {
101
+ log("Unknown publish target for task " + taskId + ": " + PUBLISH_TYPE);
102
+ return { status: "blocked", task_id: taskId, reason: "Unknown publish target '" + PUBLISH_TYPE + "' — expected 'npm', 'artifact', 'vercel', or empty (skip publish)." };
103
+ }
104
+
85
105
  let activeSessionId;
86
106
  if (isFirstClaim && firstSessionId) {
87
107
  activeSessionId = firstSessionId;
@@ -121,8 +141,10 @@ while (i < STEPS.length) {
121
141
  (mapperSpec ? "MAPPER'S SPEC (implement exactly this):\n" + mapperSpec + "\n\n" : "") +
122
142
  "Your working directory: " + REPO_PATH + "/.worktrees/" + taskId + "/\n" +
123
143
  "This is the project source: " + PROJECT_DESC + "\n" +
124
- "Edit the TypeScript source files directly. Do NOT use artifact_edit — that happens in the Deploy phase.\n" +
125
- "Do not add unrequested features.\n\n" +
144
+ "Edit the TypeScript source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
145
+ "Do not add unrequested features.\n" +
146
+ "PUBLIC DOCS: If your change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), update the public docs in the same commit — API.md for API changes. Documentation and implementation ship together.\n\n" +
147
+ (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, so you choose the package version. If this change warrants a published release (anything a consumer can observe: workflow behavior, phase lists, identities, published docs, API), bump the version in package.json with semver (patch for fixes, minor for new behavior, major for breaking changes) and state the chosen version and why in your summary. If the change is internal-only, leave the version unchanged and say so. Check the registry first — npm view muse-crew version — and never re-publish an existing version.\n\n" : "") +
126
148
  "STEP 3: Commit your changes.\n" +
127
149
  "cd " + REPO_PATH + "/.worktrees/" + taskId + "\n" +
128
150
  "git add -A\n" +
@@ -138,6 +160,8 @@ while (i < STEPS.length) {
138
160
  "You can also read specific files in the worktree at:\n" +
139
161
  REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
140
162
  "Check quality, correctness, spec compliance.\n" +
163
+ "Check that public-affecting changes have matching public doc updates (API.md or the published API contract). If the docs are missing or inaccurate, reject with notes on what is stale.\n" +
164
+ (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry. Validate the builder's version choice: package.json must hold valid semver; if the version was bumped it must be greater than the registry version (npm view muse-crew version), the bump scope (patch/minor/major) must fit the change, and exactly one version field may change. If the version is invalid, already published, or mis-scoped, reject with notes.\n" : "") +
141
165
  "If it passes, your final response MUST be valid JSON and nothing else: { \"passed\": true, \"summary\": \"approval notes\" }.\n" +
142
166
  "If it fails, your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"rejection notes\" }.\n" +
143
167
  "No prose, no markdown, just the JSON object.";
@@ -147,40 +171,69 @@ while (i < STEPS.length) {
147
171
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " integrate " + taskId + " \"merge: chore: " + safeTitle + "\"\n\n" +
148
172
  "Read the output:\n" +
149
173
  "- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
150
- "- If it contains LOCK_HELD, another task is deploying. Set passed to false.\n" +
174
+ "- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Set passed to false.\n" +
151
175
  "- If it contains CONFLICT, a merge conflict occurred. Set passed to false with details.\n" +
152
176
  "- If it contains ERROR, something else failed. Set passed to false.\n\n" +
177
+ "\n" +
178
+ "STEP 2: Push the merged main to the remote repository.\n" +
179
+ "Run: cd " + REPO_PATH + " && git push origin main\n" +
180
+ "- If the push succeeds, report the merged commit hash.\n" +
181
+ "- If the push is rejected as non-fast-forward (the remote has commits not present locally),\n" +
182
+ " NEVER force-push. Do not run any --force variant. Set passed to false with summary:\n" +
183
+ " 'git push origin main rejected as non-fast-forward — remote main has diverged; manual resolution required'.
184
+ \n\n" +
153
185
  "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true/false }. No prose, no markdown, just the JSON object.";
154
186
 
155
- } else if (step.name === "Deploy") {
156
- if (DEPLOY_TYPE === "repo") {
157
- // Repo projects: install immutable release and atomically activate
158
- instructions = "Deploy repo changes via the immutable release system.\n\n" +
159
- "STEP 0: Refresh the merge lock to prevent stale-lock breaking during deploy.\n" +
187
+ } else if (step.name === "Publish") {
188
+ if (PUBLISH_TYPE === "npm") {
189
+ // npm packages: immutable release + pack + publish to the registry (push is universal in Integrate)
190
+ instructions = "Publish the npm package to the registry.\n\n" +
191
+ "The repo push already happened in Integrate — do NOT push to git in this phase, and NEVER force-push.\n" +
192
+ "Version discipline: publish ships the exact version merged in Integrate (Build applied it, Review validated it). Do not bump the version here.\n\n" +
193
+ "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
160
194
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
161
- "STEP 1: Install and activate the new release.\n" +
195
+ "STEP 1: Install and activate the immutable release.\n" +
162
196
  "Run: " + RELEASE_SCRIPT + " deploy " + REPO_PATH + "\n" +
163
197
  "Verify the output contains INSTALLED and ACTIVATED (or EXISTS and ACTIVATED if unchanged).\n\n" +
164
- "STEP 2: Finalize.\n" +
198
+ "STEP 2: Check whether the package version needs publishing.\n" +
199
+ "Read the version from: cd " + REPO_PATH + " && node -p \"require('./package.json').version\"\n" +
200
+ "Check the registry: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
201
+ "If the local version matches the registry version, the version is already live — skip to STEP 5.\n\n" +
202
+ "STEP 3: Pack and publish.\n" +
203
+ "Run: cd " + REPO_PATH + " && npm pack\n" +
204
+ "Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-$(node -p \"require('" + REPO_PATH + "/package.json').version\").tgz\n" +
205
+ "If publish fails with 'You cannot publish over the previously published versions', the version is already live — continue to STEP 4.\n\n" +
206
+ "STEP 4: Verify.\n" +
207
+ "Run: npm view muse-crew version\n" +
208
+ "Confirm it matches the local package.json version.\n\n" +
209
+ "STEP 5: Finalize.\n" +
165
210
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
166
211
  "If the output contains DEPLOYED, finalization is complete.\n\n" +
167
- "Your final response MUST be valid JSON and nothing else: { \"summary\": \"release activated and finalized\", \"passed\": true }.\n" +
212
+ "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
168
213
  "No prose, no markdown, just the JSON object.";
169
- } else {
170
- instructions = "Deploy the merged code to the live artifact.\n\n" +
171
- "STEP 0: Refresh the merge lock to prevent stale-lock breaking during deploy.\n" +
214
+ } else if (PUBLISH_TYPE === "artifact") {
215
+ // Artifact projects: rebuild the live artifact via artifact_edit, then finalize
216
+ instructions = "Publish the merged code to the live artifact.\n\n" +
217
+ "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
218
+ "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
172
219
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
173
- "STEP 1: Deploy to the live artifact.\n" +
220
+ "STEP 1: Publish to the live artifact.\n" +
174
221
  "Get the change summary: cd " + REPO_PATH + " && git log -1 --stat\n" +
175
- "Then call artifact_edit with slug \"" + DEPLOY_SLUG + "\" and verbatim_request:\n" +
222
+ "Then call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
176
223
  "'Rebuild the application from current source. Do not modify any source files — just rebuild and deploy what is on disk.'\n" +
177
224
  "Wait for the build to complete by polling artifact_status until it is no longer running.\n\n" +
178
225
  "STEP 2: Finalize.\n" +
179
226
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
180
- "If the output contains DEPLOYED, deployment is complete.\n\n" +
227
+ "If the output contains DEPLOYED, publishing is complete.\n\n" +
181
228
  "If artifact_edit failed, still run post-deploy to release the merge lock and clean up.\n" +
182
- "Report the failure: { \"passed\": false, \"summary\": \"artifact deployment failed: [details]\" }.\n\n" +
183
- "Your final response MUST be valid JSON and nothing else: { \"summary\": \"deployed changes\", \"passed\": true }.\n" +
229
+ "Report the failure: { \"passed\": false, \"summary\": \"artifact publish failed: [details]\" }.\n\n" +
230
+ "Your final response MUST be valid JSON and nothing else: { \"summary\": \"published changes\", \"passed\": true }.\n" +
231
+ "No prose, no markdown, just the JSON object.";
232
+ } else if (PUBLISH_TYPE === "vercel") {
233
+ // vercel publish is not yet implemented — block without inventing behavior
234
+ instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
235
+ "Do NOT invent publish behavior — do not guess CLI commands, APIs, or deployment steps.\n" +
236
+ "Your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"vercel publish not yet implemented\" }.\n" +
184
237
  "No prose, no markdown, just the JSON object.";
185
238
  }
186
239
  }
@@ -189,8 +242,8 @@ while (i < STEPS.length) {
189
242
  var eventPreamble = "";
190
243
  if (step.name !== "Review") {
191
244
  eventPreamble = "CONTEXT: First, fetch this task's event history for background.\n" +
192
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"limit\": 100 }.\n" +
193
- "Look through returned events for entries matching task_id \"" + taskId + "\". They contain notes and decisions from prior phases.\n\n";
245
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"task_id\": \"" + taskId + "\" }.\n" +
246
+ "The returned events are filtered to this task. They contain notes and decisions from prior phases.\n\n";
194
247
  }
195
248
 
196
249
  var stepResult;
@@ -273,9 +326,9 @@ while (i < STEPS.length) {
273
326
  return { status: "blocked", task_id: taskId, reason: "Integration failed: " + summary };
274
327
  }
275
328
 
276
- if (!passed && step.name === "Deploy") {
277
- log("Deploy failed for task " + taskId + ": " + summary);
278
- return { status: "blocked", task_id: taskId, reason: "Deploy failed: " + summary };
329
+ if (!passed && step.name === "Publish") {
330
+ log("Publish failed for task " + taskId + ": " + summary);
331
+ return { status: "blocked", task_id: taskId, reason: "Publish failed: " + summary };
279
332
  }
280
333
 
281
334
  i++;
@@ -250,8 +250,9 @@ for (var p = 0; p < toProcess.length; p++) {
250
250
  "Claim the task for the " + nextStepName + " step.\n" +
251
251
  "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
252
252
  "{ \"task_id\": \"" + itask.id + "\", \"identity\": \"" + identity + "\", \"step\": \"" + nextStepName + "\", \"notes\": \"" + nextStepName + " step started\" }.\n" +
253
- "Check the response. If the task was successfully claimed (a new session was created), return { claimed: true, session_id: \"<the new session id>\" }.\n" +
254
- "If the task was already claimed by another tick (already has a running session), return { claimed: false, session_id: \"\" }.",
253
+ "Do not interpret the response. It already contains an explicit \"claimed\" field copy it verbatim.\n" +
254
+ "Return { claimed: <the response's claimed field exactly>, session_id: \"<the response's session_id field>\" }.\n" +
255
+ "If claimed is false there is no session_id; return { claimed: false, session_id: \"\" }.",
255
256
  {
256
257
  key: "claim-" + itask.id,
257
258
  label: "Claiming " + nextStepName + " for: " + itask.title,
@@ -172,7 +172,7 @@ try {
172
172
  " - mode: 'task'\n" +
173
173
  " - schedule: { kind: 'interval', every: '3m' }\n" +
174
174
  " - owner: 'space:" + dashboardSlug + "'\n" +
175
- " - timeout_secs: 180\n" +
175
+ " - timeout_secs: 120\n" +
176
176
  " - body: the processed template text\n" +
177
177
  " d. Return { existed: false }\n\n" +
178
178
  "Return JSON with existed (boolean).",
package/workflows/docs.js CHANGED
@@ -22,6 +22,11 @@ const DASHBOARD_SLUG = inputs.dashboardSlug || "orchestra-dashboard";
22
22
  const crewHome = inputs.crewHome || "~/workspace/.jarvis";
23
23
  const ORCH_PATH = crewHome + "/.orchestration";
24
24
 
25
+ // Project repo — the docs workflow edits the project's own docs, not the crew home.
26
+ // Falls back to crew home only for manual launches without a project config.
27
+ const projectConfig = inputs.project_config || {};
28
+ const REPO_PATH = projectConfig.repo_path || crewHome;
29
+
25
30
  if (!taskId) {
26
31
  throw new Error("task_id is required in args");
27
32
  }
@@ -72,7 +77,7 @@ while (i < STEPS.length) {
72
77
  if (step.name === "Triage") {
73
78
  instructions = "Validate the task, check clarity, confirm the docs workflow assignment.\nYour final response MUST be valid JSON and nothing else: { \"summary\": \"your assessment\", \"passed\": true }. No prose, no markdown, just the JSON object.";
74
79
  } else if (step.name === "Write") {
75
- instructions = "Write or revise the documentation the task asks for.\nFollow Tate's voice — clear, conversational, no jargon unless it earns its place.\nAll doc files go under " + crewHome + "/." +
80
+ instructions = "Write or revise the documentation the task asks for.\nFollow Tate's voice — clear, conversational, no jargon unless it earns its place.\nDo your work in the project repository at " + REPO_PATH + " — all doc files go there, not under the crew home." +
76
81
  (rejectionNotes ? "\n\nREWORK after review rejection. Address:\n" + rejectionNotes : "") +
77
82
  "\nYour final response MUST be valid JSON and nothing else: { \"summary\": \"what you wrote and where\", \"passed\": true }. No prose, no markdown, just the JSON object.";
78
83
  } else if (step.name === "Review") {
@@ -83,7 +88,7 @@ while (i < STEPS.length) {
83
88
  "Read the identity file at " + ORCH_PATH + "/identities/" + step.identity + ".md using the read tool, and embody that character fully.\n\n" +
84
89
  "## Your Assignment\n\n" +
85
90
  "Task: " + taskTitle + "\nTask ID: " + taskId + "\nDescription: " + taskDescription + "\nStep: " + step.name + "\nDashboard slug: " + DASHBOARD_SLUG + "\n\n" +
86
- "## Instructions\n\n" + instructions + "\n\nCONSTRAINT: Do NOT call logevent or upsertagentsession — the workflow handles all phase tracking after your step completes.\n\nStay in character. All file work under " + crewHome + "/.",
91
+ "## Instructions\n\n" + instructions + "\n\nCONSTRAINT: Do NOT call logevent or upsertagentsession — the workflow handles all phase tracking after your step completes.\n\nStay in character. All file work under " + REPO_PATH + "/.",
87
92
  {
88
93
  key: "work-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
89
94
  label: step.identity + ": " + step.name + " on \"" + taskTitle + "\"",
@@ -1,14 +1,14 @@
1
1
  export const meta = {
2
2
  name: "crew-standard",
3
- description: "Standard workflow: Triage → Map → Build → Review → Integrate → Deploy → QA",
4
- phases: ["Triage", "Map", "Build", "Review", "Integrate", "Deploy", "QA"],
3
+ description: "Standard workflow: Triage → Map → Build → Review → Integrate → Publish → QA",
4
+ phases: ["Triage", "Map", "Build", "Review", "Integrate", "Publish", "QA"],
5
5
  steps: [
6
6
  { name: "Triage", identity: "sage" },
7
7
  { name: "Map", identity: "mara" },
8
8
  { name: "Build", identity: "wren" },
9
9
  { name: "Review", identity: "cass" },
10
10
  { name: "Integrate", identity: "wren" },
11
- { name: "Deploy", identity: "wren" },
11
+ { name: "Publish", identity: "wren" },
12
12
  { name: "QA", identity: "hazel" }
13
13
  ],
14
14
  reworkTarget: "Build"
@@ -32,12 +32,14 @@ const MERGE_LOCK_SRC = crewHome + "/lib/merge-lock.sh";
32
32
  const RUN_LIB = "/tmp/crew-lib-" + taskId;
33
33
  const LIFECYCLE = RUN_LIB + "/worktree-lifecycle.sh";
34
34
  const MERGE_LOCK = RUN_LIB + "/merge-lock.sh";
35
+ const ORPHAN_SWEEP_SRC = crewHome + "/lib/orphan-sweep.sh";
36
+ const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
35
37
 
36
38
  // Project config — passed by dispatcher, falls back to dashboard defaults
37
39
  const projectConfig = inputs.project_config || {};
38
40
  const REPO_PATH = projectConfig.repo_path || "~/workspace/ts-spaces/orchestra-dashboard";
39
- const DEPLOY_TYPE = projectConfig.deploy_type || "artifact";
40
- const DEPLOY_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
41
+ const PUBLISH_TYPE = projectConfig.deploy_type || "";
42
+ const PUBLISH_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
41
43
  const PROJECT_DESC = projectConfig.description || "React + TypeScript web dashboard (client/src/, server/src/, drizzle/)";
42
44
  const RELEASE_SCRIPT = crewHome + "/crew-release.sh";
43
45
 
@@ -60,13 +62,15 @@ const STEPS = [
60
62
  { name: "Build", identity: "wren" },
61
63
  { name: "Review", identity: "cass" },
62
64
  { name: "Integrate", identity: "wren" },
63
- { name: "Deploy", identity: "wren" },
65
+ { name: "Publish", identity: "wren" },
64
66
  { name: "QA", identity: "hazel" }
65
67
  ];
66
68
  const BUILD_INDEX = STEPS.findIndex(s => s.name === 'Build');
67
69
  if (BUILD_INDEX < 0) throw new Error("STEPS missing 'Build' step");
68
- const MAX_REWORK = 2;
69
- let reworkCount = 0;
70
+ // Shared rework budget: Review and QA rejections draw from the SAME pool of 2.
71
+ // E.g. 2 Review bounces + 1 QA bounce = 3 total > budget -> task blocks.
72
+ const MAX_TOTAL_REWORK = 2;
73
+ let totalReworkCount = 0;
70
74
  let rejectionNotes = inputs.rejection_notes || "";
71
75
  let mapperSpec = "";
72
76
  let i = startStepIndex;
@@ -80,7 +84,8 @@ await agent(
80
84
  " mkdir -p " + RUN_LIB + "\n" +
81
85
  " cp " + LIFECYCLE_SRC + " " + LIFECYCLE + "\n" +
82
86
  " cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + "\n" +
83
- " chmod +x " + LIFECYCLE + " " + MERGE_LOCK + "\n" +
87
+ " cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + "\n" +
88
+ " chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + ORPHAN_SWEEP + "\n" +
84
89
  "Confirm the files exist by listing " + RUN_LIB + ".",
85
90
  { key: "pin-lifecycle", label: "Pinning lifecycle scripts", schema: { type: "object" } }
86
91
  );
@@ -88,11 +93,29 @@ log("Lifecycle scripts pinned to " + RUN_LIB);
88
93
 
89
94
  while (i < STEPS.length) {
90
95
  const step = STEPS[i];
91
- const isFirstClaim = (i === startStepIndex && reworkCount === 0);
96
+ const isFirstClaim = (i === startStepIndex && totalReworkCount === 0);
92
97
 
93
98
  phase(step.name);
94
99
  log(step.name + " step (" + step.identity + ") for task " + taskId);
95
100
 
101
+ // Publish is optional and target-based. Empty target = prototyping project: skip the phase.
102
+ // Unknown target (incl. legacy "repo") = config error: block.
103
+ if (step.name === "Publish" && !PUBLISH_TYPE) {
104
+ log("Publish skipped for task " + taskId + " — no publish target configured (deploy_type empty)");
105
+ await agent(
106
+ "Release the merge lock and clean up without publishing.\n" +
107
+ "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
108
+ "If the output contains DEPLOYED, the lock is released and the worktree is cleaned up.",
109
+ { key: "publish-skip-cleanup", label: "Skipping Publish (no target)", schema: { type: "object" } }
110
+ );
111
+ i++;
112
+ continue;
113
+ }
114
+ if (step.name === "Publish" && PUBLISH_TYPE !== "npm" && PUBLISH_TYPE !== "artifact" && PUBLISH_TYPE !== "vercel") {
115
+ log("Unknown publish target for task " + taskId + ": " + PUBLISH_TYPE);
116
+ return { status: "blocked", task_id: taskId, reason: "Unknown publish target '" + PUBLISH_TYPE + "' — expected 'npm', 'artifact', 'vercel', or empty (skip publish)." };
117
+ }
118
+
96
119
  // Claim session — reuse dispatcher's session for the very first step
97
120
  let activeSessionId;
98
121
  if (isFirstClaim && firstSessionId) {
@@ -101,10 +124,10 @@ while (i < STEPS.length) {
101
124
  const claimResult = await agent(
102
125
  "Claim a session for this task step.\n" +
103
126
  "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
104
- "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started" + (reworkCount > 0 ? " (rework #" + reworkCount + ")" : "") + "\" }.\n" +
127
+ "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started" + (totalReworkCount > 0 ? " (rework #" + totalReworkCount + ")" : "") + "\" }.\n" +
105
128
  "Return the session_id from the response.",
106
129
  {
107
- key: "claim-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
130
+ key: "claim-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
108
131
  label: "Claiming " + step.name,
109
132
  schema: {
110
133
  type: "object",
@@ -134,8 +157,10 @@ while (i < STEPS.length) {
134
157
  (mapperSpec ? "MAPPER'S SPEC (implement exactly this):\n" + mapperSpec + "\n\n" : "") +
135
158
  "Your working directory: " + REPO_PATH + "/.worktrees/" + taskId + "/\n" +
136
159
  "This is the project source: " + PROJECT_DESC + "\n" +
137
- "Edit source files directly. Do NOT use artifact_edit — that happens in the Deploy phase.\n" +
138
- "Do not add unrequested features. Build exactly what the spec calls for.\n\n" +
160
+ "Edit source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
161
+ "Do not add unrequested features. Build exactly what the spec calls for.\n" +
162
+ "PUBLIC DOCS: If your change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), update the public docs in the same commit — API.md for API changes. Documentation and implementation ship together.\n\n" +
163
+ (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, so you choose the package version. If this change warrants a published release (anything a consumer can observe: workflow behavior, phase lists, identities, published docs, API), bump the version in package.json with semver (patch for fixes, minor for new behavior, major for breaking changes) and state the chosen version and why in your summary. If the change is internal-only, leave the version unchanged and say so. Check the registry first — npm view muse-crew version — and never re-publish an existing version.\n\n" : "") +
139
164
  "STEP 3: Commit your changes.\n" +
140
165
  "cd " + REPO_PATH + "/.worktrees/" + taskId + "\n" +
141
166
  "git add -A\n" +
@@ -151,6 +176,8 @@ while (i < STEPS.length) {
151
176
  "You can also read specific files in the worktree at:\n" +
152
177
  REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
153
178
  "Check quality, correctness, and spec compliance.\n" +
179
+ "Check that public-affecting changes have matching public doc updates (API.md or the published API contract). If the docs are missing or inaccurate, reject with notes on what is stale.\n" +
180
+ (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry. Validate the builder's version choice: package.json must hold valid semver; if the version was bumped it must be greater than the registry version (npm view muse-crew version), the bump scope (patch/minor/major) must fit the change, and exactly one version field may change. If the version is invalid, already published, or mis-scoped, reject with notes.\n" : "") +
154
181
  "If the work passes review, your final response MUST be valid JSON and nothing else: { \"passed\": true, \"summary\": \"approval notes\" }.\n" +
155
182
  "If the work fails review, your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"rejection notes explaining what needs to change\" }.\n" +
156
183
  "No prose, no markdown, just the JSON object.";
@@ -160,56 +187,84 @@ while (i < STEPS.length) {
160
187
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " integrate " + taskId + " \"merge: " + safeTitle + "\"\n\n" +
161
188
  "Read the output:\n" +
162
189
  "- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
163
- "- If it contains LOCK_HELD, another task is deploying. Set passed to false with summary 'merge lock held'.\n" +
190
+ "- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Set passed to false with summary 'merge lock held'.\n" +
164
191
  "- If it contains CONFLICT, a merge conflict occurred. Set passed to false with the conflict details.\n" +
165
192
  "- If it contains ERROR, something else failed. Set passed to false with the error.\n\n" +
193
+ "\n" +
194
+ "STEP 2: Push the merged main to the remote repository.\n" +
195
+ "Run: cd " + REPO_PATH + " && git push origin main\n" +
196
+ "- If the push succeeds, report the merged commit hash.\n" +
197
+ "- If the push is rejected as non-fast-forward (the remote has commits not present locally),\n" +
198
+ " NEVER force-push. Do not run any --force variant. Set passed to false with summary:\n" +
199
+ " 'git push origin main rejected as non-fast-forward — remote main has diverged; manual resolution required'.\n\n" +
166
200
  "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true/false }. No prose, no markdown, just the JSON object.";
167
201
 
168
- } else if (step.name === "Deploy") {
169
- if (DEPLOY_TYPE === "repo") {
170
- // Repo projects: install immutable release and atomically activate
171
- instructions = "Deploy repo changes via the immutable release system.\n\n" +
172
- "STEP 0: Refresh the merge lock to prevent stale-lock breaking during deploy.\n" +
202
+ } else if (step.name === "Publish") {
203
+ if (PUBLISH_TYPE === "npm") {
204
+ // npm packages: immutable release + pack + publish to the registry (push is universal in Integrate)
205
+ instructions = "Publish the npm package to the registry.\n\n" +
206
+ "The repo push already happened in Integrate — do NOT push to git in this phase, and NEVER force-push.\n" +
207
+ "Version discipline: publish ships the exact version merged in Integrate (Build applied it, Review validated it). Do not bump the version here.\n\n" +
208
+ "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
173
209
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
174
- "STEP 1: Install and activate the new release.\n" +
210
+ "STEP 1: Install and activate the immutable release.\n" +
175
211
  "Run: " + RELEASE_SCRIPT + " deploy " + REPO_PATH + "\n" +
176
212
  "Verify the output contains INSTALLED and ACTIVATED (or EXISTS and ACTIVATED if unchanged).\n\n" +
177
- "STEP 2: Finalize.\n" +
213
+ "STEP 2: Check whether the package version needs publishing.\n" +
214
+ "Read the version from: cd " + REPO_PATH + " && node -p \"require('./package.json').version\"\n" +
215
+ "Check the registry: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
216
+ "If the local version matches the registry version, the version is already live — skip to STEP 5.\n\n" +
217
+ "STEP 3: Pack and publish.\n" +
218
+ "Run: cd " + REPO_PATH + " && npm pack\n" +
219
+ "Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-$(node -p \"require('" + REPO_PATH + "/package.json').version\").tgz\n" +
220
+ "If publish fails with 'You cannot publish over the previously published versions', the version is already live — continue to STEP 4.\n\n" +
221
+ "STEP 4: Verify.\n" +
222
+ "Run: npm view muse-crew version\n" +
223
+ "Confirm it matches the local package.json version.\n\n" +
224
+ "STEP 5: Finalize.\n" +
178
225
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
179
226
  "If the output contains DEPLOYED, finalization is complete.\n\n" +
180
- "Your final response MUST be valid JSON and nothing else: { \"summary\": \"release activated and finalized\", \"passed\": true }.\n" +
227
+ "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
181
228
  "No prose, no markdown, just the JSON object.";
182
- } else {
183
- // Artifact projects: deploy via artifact_edit then finalize
184
- instructions = "Deploy the merged code to the live artifact.\n\n" +
185
- "STEP 0: Refresh the merge lock to prevent stale-lock breaking during deploy.\n" +
229
+ } else if (PUBLISH_TYPE === "artifact") {
230
+ // Artifact projects: rebuild the live artifact via artifact_edit, then finalize
231
+ instructions = "Publish the merged code to the live artifact.\n\n" +
232
+ "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
233
+ "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
186
234
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
187
- "STEP 1: Deploy to the live artifact.\n" +
235
+ "STEP 1: Publish to the live artifact.\n" +
188
236
  "Get the change summary: cd " + REPO_PATH + " && git log -1 --stat\n" +
189
- "Then call artifact_edit with slug \"" + DEPLOY_SLUG + "\" and verbatim_request:\n" +
237
+ "Then call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
190
238
  "'Rebuild the application from current source. Do not modify any source files — just rebuild and deploy what is on disk.'\n" +
191
239
  "Wait for the build to complete by polling artifact_status until it is no longer running.\n\n" +
192
240
  "STEP 2: Finalize.\n" +
193
241
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
194
- "If the output contains DEPLOYED, deployment is complete.\n\n" +
242
+ "If the output contains DEPLOYED, publishing is complete.\n\n" +
195
243
  "If artifact_edit failed, still run post-deploy to release the merge lock and clean up.\n" +
196
- "Report the failure: { \"passed\": false, \"summary\": \"artifact deployment failed: [details]\" }.\n\n" +
197
- "Your final response MUST be valid JSON and nothing else: { \"summary\": \"deployed changes\", \"passed\": true }.\n" +
244
+ "Report the failure: { \"passed\": false, \"summary\": \"artifact publish failed: [details]\" }.\n\n" +
245
+ "Your final response MUST be valid JSON and nothing else: { \"summary\": \"published changes\", \"passed\": true }.\n" +
246
+ "No prose, no markdown, just the JSON object.";
247
+ } else if (PUBLISH_TYPE === "vercel") {
248
+ // vercel publish is not yet implemented — block without inventing behavior
249
+ instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
250
+ "Do NOT invent publish behavior — do not guess CLI commands, APIs, or deployment steps.\n" +
251
+ "Your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"vercel publish not yet implemented\" }.\n" +
198
252
  "No prose, no markdown, just the JSON object.";
199
253
  }
200
-
201
254
  } else if (step.name === "QA") {
202
- if (DEPLOY_TYPE === "artifact") {
255
+ if (PUBLISH_TYPE === "artifact") {
203
256
  var safeDesc = taskDescription.replace(/"/g, "'").replace(/\\/g, "\\\\").slice(0, 500);
204
- instructions = "You are code-blind QA. You NEVER read source files.\n\n" +
205
- "STEP 1: Trigger a visual inspection of the deployed artifact.\n" +
257
+ instructions = "You are code-blind QA. You NEVER read source files.\n" +
258
+ "Public docs (API.md, README, published action schemas) are NOT source code — read them freely, exactly as a user would.\n\n" +
259
+ "STEP 1: Trigger a visual inspection of the published artifact.\n" +
206
260
  "Call artifact_inspect with:\n" +
207
- " slug: \"" + DEPLOY_SLUG + "\"\n" +
261
+ " slug: \"" + PUBLISH_SLUG + "\"\n" +
208
262
  " repair_authorized: false\n" +
209
263
  " verbatim_request: \"Verify task: " + safeTitle + ". " + safeDesc + "\"\n\n" +
210
264
  "This call is asynchronous — it fires the inspection but results arrive outside this workflow. That is expected and correct.\n\n" +
211
265
  "STEP 2: Verify data integrity via the dashboard API.\n" +
212
- "Use artifact_invoke_action on slug \"" + DEPLOY_SLUG + "\" with read-only actions (e.g. gettasks, getagentsessions) to check the task's data-level effects.\n\n" +
266
+ "Use artifact_invoke_action on slug \"" + PUBLISH_SLUG + "\" with read-only actions (e.g. gettasks, getagentsessions) to check the task's data-level effects.\n" +
267
+ "DOCS GATE: If the change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), verify the public docs describe it. If public docs are missing or stale for a public-affecting change, FAIL with { \"passed\": false, \"summary\": \"public docs missing/stale for [the change]\" }. QA always fails when public-affecting changes lack public docs. Guide/tutorial gaps are lower priority — file a follow-up task for those instead of failing.\n\n" +
213
268
  "STEP 3: File follow-up tasks for any related issues you discover.\n" +
214
269
  "Use artifact_invoke_action createtask on slug \"" + DASHBOARD_SLUG + "\" for each issue.\n\n" +
215
270
  "Your final response MUST be valid JSON and nothing else:\n" +
@@ -217,7 +272,9 @@ while (i < STEPS.length) {
217
272
  "No prose, no markdown, just the JSON object.";
218
273
  } else {
219
274
  instructions = "Test from a user's perspective. You are CODE-BLIND — do NOT read source code.\n" +
275
+ "Public docs (API.md, README) are NOT source code — read them freely, exactly as a user would.\n" +
220
276
  "Verify the change is working as described in the task.\n" +
277
+ "DOCS GATE: If the change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), verify the public docs describe it. If public docs are missing or stale, FAIL with { \"passed\": false, \"summary\": \"public docs missing/stale for [the change]\" }. QA always fails when public-affecting changes lack public docs. Guide/tutorial gaps are lower priority — file a follow-up task for those instead of failing.\n" +
221
278
  "File follow-up tasks via artifact_invoke_action createtask on slug \"" + DASHBOARD_SLUG + "\" for related issues found.\n\n" +
222
279
  "Your final response MUST be valid JSON and nothing else: { \"passed\": true/false, \"summary\": \"what you tested and found\" }.\n" +
223
280
  "No prose, no markdown, just the JSON object.";
@@ -228,8 +285,8 @@ while (i < STEPS.length) {
228
285
  var eventPreamble = "";
229
286
  if (step.name !== "Review") {
230
287
  eventPreamble = "CONTEXT: First, fetch this task's event history for background.\n" +
231
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"limit\": 100 }.\n" +
232
- "Look through returned events for entries matching task_id \"" + taskId + "\". They contain notes and decisions from prior phases.\n\n";
288
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"task_id\": \"" + taskId + "\" }.\n" +
289
+ "The returned events are filtered to this task. They contain notes and decisions from prior phases.\n\n";
233
290
  }
234
291
 
235
292
  // Run work agent WITH schema — runtime retries on non-JSON via structured outputs
@@ -247,7 +304,7 @@ while (i < STEPS.length) {
247
304
  "CONSTRAINT: Do NOT call logevent or upsertagentsession — the workflow handles all phase tracking after your step completes.\n\n" +
248
305
  "Stay in character. Do the work thoroughly.",
249
306
  {
250
- key: "work-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
307
+ key: "work-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
251
308
  label: step.identity + ": " + step.name + " on \"" + taskTitle + "\"",
252
309
  timeoutMs: 3600000,
253
310
  schema: WORK_SCHEMA
@@ -296,7 +353,7 @@ while (i < STEPS.length) {
296
353
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
297
354
  "{ \"task_id\": \"" + taskId + "\", \"type\": \"" + status + "\", \"identity\": \"" + step.identity + "\", \"message\": \"" + step.name + " " + status + " by " + step.identity + "\" }.",
298
355
  {
299
- key: "record-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
356
+ key: "record-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
300
357
  label: "Recording " + step.name + " result",
301
358
  schema: { type: "object" }
302
359
  }
@@ -304,14 +361,14 @@ while (i < STEPS.length) {
304
361
 
305
362
  // Handle rejection — bounce back to Build
306
363
  if (!passed && (step.name === "Review" || step.name === "QA")) {
307
- reworkCount++;
308
- if (reworkCount > MAX_REWORK) {
309
- log("Max rework attempts reached for task " + taskId + " — worktree preserved at .worktrees/" + taskId + " for manual inspection");
310
- return { status: "blocked", task_id: taskId, reason: "Exceeded " + MAX_REWORK + " rework attempts after " + step.name + " rejection. Worktree preserved." };
364
+ totalReworkCount++;
365
+ if (totalReworkCount > MAX_TOTAL_REWORK) {
366
+ log("Shared rework budget exhausted for task " + taskId + " — worktree preserved at .worktrees/" + taskId + " for manual inspection");
367
+ return { status: "blocked", task_id: taskId, reason: "Exceeded shared rework budget (" + MAX_TOTAL_REWORK + " total rework attempts across Review and QA) after " + step.name + " rejection. Worktree preserved." };
311
368
  }
312
369
  rejectionNotes = summary;
313
370
  i = BUILD_INDEX;
314
- log(step.name + " rejected — bouncing to Build (rework #" + reworkCount + ")");
371
+ log(step.name + " rejected — bouncing to Build (rework #" + totalReworkCount + " of " + MAX_TOTAL_REWORK + ")");
315
372
  continue;
316
373
  }
317
374
 
@@ -321,10 +378,10 @@ while (i < STEPS.length) {
321
378
  return { status: "blocked", task_id: taskId, reason: "Integration failed: " + summary };
322
379
  }
323
380
 
324
- // Deploy failure blocks the task
325
- if (!passed && step.name === "Deploy") {
326
- log("Deploy failed for task " + taskId + ": " + summary);
327
- return { status: "blocked", task_id: taskId, reason: "Deploy failed: " + summary };
381
+ // Publish failure blocks the task
382
+ if (!passed && step.name === "Publish") {
383
+ log("Publish failed for task " + taskId + ": " + summary);
384
+ return { status: "blocked", task_id: taskId, reason: "Publish failed: " + summary };
328
385
  }
329
386
 
330
387
  i++;