muse-crew 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,17 @@ 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 || {};
40
+ // Project this run was dispatched for — the mid-run project-change guard
41
+ // compares the task's live project against this on every phase boundary.
42
+ const LAUNCH_PROJECT_ID = inputs.project_id || "";
38
43
  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;
44
+ const PUBLISH_TYPE = projectConfig.deploy_type || "";
45
+ const PUBLISH_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
41
46
  const PROJECT_DESC = projectConfig.description || "React + TypeScript web dashboard (client/src/, server/src/, drizzle/)";
42
47
  const RELEASE_SCRIPT = crewHome + "/crew-release.sh";
43
48
 
@@ -60,13 +65,16 @@ const STEPS = [
60
65
  { name: "Build", identity: "wren" },
61
66
  { name: "Review", identity: "cass" },
62
67
  { name: "Integrate", identity: "wren" },
63
- { name: "Deploy", identity: "wren" },
68
+ { name: "Publish", identity: "wren" },
64
69
  { name: "QA", identity: "hazel" }
65
70
  ];
66
71
  const BUILD_INDEX = STEPS.findIndex(s => s.name === 'Build');
67
72
  if (BUILD_INDEX < 0) throw new Error("STEPS missing 'Build' step");
68
- const MAX_REWORK = 2;
69
- let reworkCount = 0;
73
+ const REWORK_STEP = STEPS[BUILD_INDEX].name;
74
+ // Shared rework budget: Review and QA rejections draw from the SAME pool of 2.
75
+ // E.g. 2 Review bounces + 1 QA bounce = 3 total > budget -> task blocks.
76
+ const MAX_TOTAL_REWORK = 2;
77
+ let totalReworkCount = 0;
70
78
  let rejectionNotes = inputs.rejection_notes || "";
71
79
  let mapperSpec = "";
72
80
  let i = startStepIndex;
@@ -80,7 +88,8 @@ await agent(
80
88
  " mkdir -p " + RUN_LIB + "\n" +
81
89
  " cp " + LIFECYCLE_SRC + " " + LIFECYCLE + "\n" +
82
90
  " cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + "\n" +
83
- " chmod +x " + LIFECYCLE + " " + MERGE_LOCK + "\n" +
91
+ " cp " + ORPHAN_SWEEP_SRC + " " + ORPHAN_SWEEP + "\n" +
92
+ " chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + ORPHAN_SWEEP + "\n" +
84
93
  "Confirm the files exist by listing " + RUN_LIB + ".",
85
94
  { key: "pin-lifecycle", label: "Pinning lifecycle scripts", schema: { type: "object" } }
86
95
  );
@@ -88,11 +97,67 @@ log("Lifecycle scripts pinned to " + RUN_LIB);
88
97
 
89
98
  while (i < STEPS.length) {
90
99
  const step = STEPS[i];
91
- const isFirstClaim = (i === startStepIndex && reworkCount === 0);
100
+ const isFirstClaim = (i === startStepIndex && totalReworkCount === 0);
92
101
 
93
102
  phase(step.name);
94
103
  log(step.name + " step (" + step.identity + ") for task " + taskId);
95
104
 
105
+ // ── Project-change guard ───────────────────────────────────────────
106
+ // A task moved to another project mid-run must not keep working in the
107
+ // old project's repo. Re-read the task's project at every phase
108
+ // boundary: if it differs from the project this run was dispatched for,
109
+ // abort the stale run (failed at the rework target) so the dispatcher
110
+ // re-launches the step with the new project's config.
111
+ if (LAUNCH_PROJECT_ID) {
112
+ const projectCheck = await agent(
113
+ "Read this task's current project from the dashboard.\n" +
114
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }.\n" +
115
+ "Find the task with id \"" + taskId + "\" in the returned tasks array.\n" +
116
+ "Return exactly { \"project\": \"<the task's project field, or empty string if absent>\" } and nothing else.",
117
+ {
118
+ key: "project-check-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
119
+ label: "Checking project before " + step.name,
120
+ schema: { type: "object", properties: { project: { type: "string" } }, required: ["project"] }
121
+ }
122
+ );
123
+ const currentProject = (projectCheck && projectCheck.project) ? projectCheck.project : LAUNCH_PROJECT_ID;
124
+ if (currentProject !== LAUNCH_PROJECT_ID) {
125
+ const abortMessage = "Task project changed mid-run from '" + LAUNCH_PROJECT_ID + "' to '" + currentProject + "' — aborting stale run. The dispatcher will re-launch from " + REWORK_STEP + " with the new project context.";
126
+ log(abortMessage);
127
+ const abortSessionPatch = (isFirstClaim && firstSessionId) ? "\"id\": \"" + firstSessionId + "\", " : "";
128
+ await agent(
129
+ "Abort the stale run and remove its worktree from the old project's repo.\n" +
130
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"upsertagentsession\", args:\n" +
131
+ "{ \"task_id\": \"" + taskId + "\", " + abortSessionPatch + "\"identity\": \"" + step.identity + "\", \"step\": \"" + REWORK_STEP + "\", \"status\": \"failed\", " +
132
+ "\"notes\": " + JSON.stringify(abortMessage + " Rebuild from the Map session notes in the task's event history.") + " }.\n" +
133
+ "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
134
+ "{ \"task_id\": \"" + taskId + "\", \"type\": \"failed\", \"identity\": \"" + step.identity + "\", \"message\": " + JSON.stringify(abortMessage) + " }.\n" +
135
+ "Then run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " cleanup " + taskId + "\n" +
136
+ "The cleanup output should contain CLEANUP.",
137
+ { key: "abort-project-change", label: "Aborting stale run (project changed)", schema: { type: "object" } }
138
+ );
139
+ return { status: "failed", task_id: taskId, reason: abortMessage };
140
+ }
141
+ }
142
+
143
+ // Publish is optional and target-based. Empty target = prototyping project: skip the phase.
144
+ // Unknown target (incl. legacy "repo") = config error: block.
145
+ if (step.name === "Publish" && !PUBLISH_TYPE) {
146
+ log("Publish skipped for task " + taskId + " — no publish target configured (deploy_type empty)");
147
+ await agent(
148
+ "Release the merge lock and clean up without publishing.\n" +
149
+ "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
150
+ "If the output contains DEPLOYED, the lock is released and the worktree is cleaned up.",
151
+ { key: "publish-skip-cleanup", label: "Skipping Publish (no target)", schema: { type: "object" } }
152
+ );
153
+ i++;
154
+ continue;
155
+ }
156
+ if (step.name === "Publish" && PUBLISH_TYPE !== "npm" && PUBLISH_TYPE !== "artifact" && PUBLISH_TYPE !== "vercel") {
157
+ log("Unknown publish target for task " + taskId + ": " + PUBLISH_TYPE);
158
+ return { status: "blocked", task_id: taskId, reason: "Unknown publish target '" + PUBLISH_TYPE + "' — expected 'npm', 'artifact', 'vercel', or empty (skip publish)." };
159
+ }
160
+
96
161
  // Claim session — reuse dispatcher's session for the very first step
97
162
  let activeSessionId;
98
163
  if (isFirstClaim && firstSessionId) {
@@ -101,10 +166,10 @@ while (i < STEPS.length) {
101
166
  const claimResult = await agent(
102
167
  "Claim a session for this task step.\n" +
103
168
  "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" +
169
+ "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started" + (totalReworkCount > 0 ? " (rework #" + totalReworkCount + ")" : "") + "\" }.\n" +
105
170
  "Return the session_id from the response.",
106
171
  {
107
- key: "claim-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
172
+ key: "claim-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
108
173
  label: "Claiming " + step.name,
109
174
  schema: {
110
175
  type: "object",
@@ -134,8 +199,10 @@ while (i < STEPS.length) {
134
199
  (mapperSpec ? "MAPPER'S SPEC (implement exactly this):\n" + mapperSpec + "\n\n" : "") +
135
200
  "Your working directory: " + REPO_PATH + "/.worktrees/" + taskId + "/\n" +
136
201
  "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" +
202
+ "Edit source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
203
+ "Do not add unrequested features. Build exactly what the spec calls for.\n" +
204
+ "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" +
205
+ (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
206
  "STEP 3: Commit your changes.\n" +
140
207
  "cd " + REPO_PATH + "/.worktrees/" + taskId + "\n" +
141
208
  "git add -A\n" +
@@ -148,9 +215,12 @@ while (i < STEPS.length) {
148
215
  (mapperSpec ? "MAPPER'S SPEC (the builder was asked to implement exactly this):\n" + mapperSpec + "\n\n" : "Read the spec (from the task description or spec files under " + crewHome + "/).\n\n") +
149
216
  "Examine the code changes by running:\n" +
150
217
  "CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " inspect " + taskId + "\n\n" +
218
+ "The inspect output is authoritative: it prints the task branch's actual tip commit (TIP) and every commit ahead of main. Base your review ONLY on this output — do NOT run git log yourself to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n" +
151
219
  "You can also read specific files in the worktree at:\n" +
152
220
  REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
153
221
  "Check quality, correctness, and spec compliance.\n" +
222
+ "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" +
223
+ (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
224
  "If the work passes review, your final response MUST be valid JSON and nothing else: { \"passed\": true, \"summary\": \"approval notes\" }.\n" +
155
225
  "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
226
  "No prose, no markdown, just the JSON object.";
@@ -160,56 +230,97 @@ while (i < STEPS.length) {
160
230
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " integrate " + taskId + " \"merge: " + safeTitle + "\"\n\n" +
161
231
  "Read the output:\n" +
162
232
  "- 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" +
233
+ "- 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
234
  "- If it contains CONFLICT, a merge conflict occurred. Set passed to false with the conflict details.\n" +
165
235
  "- If it contains ERROR, something else failed. Set passed to false with the error.\n\n" +
236
+ "\n" +
237
+ "STEP 2: Push the merged main to the remote repository.\n" +
238
+ "Run: cd " + REPO_PATH + " && git push origin main\n" +
239
+ "- If the push succeeds, report the merged commit hash.\n" +
240
+ "- If the push is rejected as non-fast-forward (the remote has commits not present locally),\n" +
241
+ " NEVER force-push. Do not run any --force variant. Set passed to false with summary:\n" +
242
+ " 'git push origin main rejected as non-fast-forward — remote main has diverged; manual resolution required'.\n\n" +
166
243
  "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true/false }. No prose, no markdown, just the JSON object.";
167
244
 
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" +
245
+ } else if (step.name === "Publish") {
246
+ if (PUBLISH_TYPE === "npm") {
247
+ // npm packages: immutable release + pack + publish to the registry (push is universal in Integrate)
248
+ instructions = "Publish the npm package to the registry.\n\n" +
249
+ "The repo push already happened in Integrate — do NOT push to git in this phase, and NEVER force-push.\n" +
250
+ "Version discipline: publish ships the exact version merged in Integrate (Build applied it, Review validated it). Do not bump the version here.\n\n" +
251
+ "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
173
252
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
174
- "STEP 1: Install and activate the new release.\n" +
253
+ "STEP 1: Install and activate the immutable release.\n" +
175
254
  "Run: " + RELEASE_SCRIPT + " deploy " + REPO_PATH + "\n" +
176
255
  "Verify the output contains INSTALLED and ACTIVATED (or EXISTS and ACTIVATED if unchanged).\n\n" +
177
- "STEP 2: Finalize.\n" +
256
+ "STEP 2: Check whether the package version needs publishing.\n" +
257
+ "Read the version from: cd " + REPO_PATH + " && node -p \"require('./package.json').version\"\n" +
258
+ "Check the registry: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
259
+ "If the local version matches the registry version, the version is already live — skip to STEP 5.\n\n" +
260
+ "STEP 3: Pack and publish.\n" +
261
+ "Run: cd " + REPO_PATH + " && npm pack\n" +
262
+ "Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-$(node -p \"require('" + REPO_PATH + "/package.json').version\").tgz\n" +
263
+ "If publish fails with 'You cannot publish over the previously published versions', the version is already live — continue to STEP 4.\n\n" +
264
+ "STEP 4: Verify.\n" +
265
+ "Run: npm view muse-crew version\n" +
266
+ "Confirm it matches the local package.json version.\n\n" +
267
+ "STEP 5: Finalize.\n" +
178
268
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
179
269
  "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" +
270
+ "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
181
271
  "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" +
272
+ } else if (PUBLISH_TYPE === "artifact") {
273
+ // Artifact projects: rebuild the live artifact via artifact_edit, then finalize
274
+ instructions = "Publish the merged code to the live artifact.\n\n" +
275
+ "POLICY: The live artifact is rebuilt only in this phase, from the repo. Never use artifact_edit to change the artifact directly — fixes go through the repo and the loop. A source fix is not done until the artifact is rebuilt from it here.\n\n" +
276
+ "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
277
+ "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
186
278
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
187
- "STEP 1: Deploy to the live artifact.\n" +
279
+ "STEP 1: Publish to the live artifact.\n" +
188
280
  "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" +
281
+ "Then call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
190
282
  "'Rebuild the application from current source. Do not modify any source files — just rebuild and deploy what is on disk.'\n" +
191
283
  "Wait for the build to complete by polling artifact_status until it is no longer running.\n\n" +
284
+ "STEP 1B: Stamp publication provenance.\n" +
285
+ "Run: cd " + REPO_PATH + " && git rev-parse HEAD\n" +
286
+ "Run: basename $(readlink " + crewHome + "/current)\n" +
287
+ "Run: date -u +%Y-%m-%dT%H:%M:%SZ\n" +
288
+ "Call artifact_invoke_action on slug \"" + PUBLISH_SLUG + "\", action \"setprovenance\", args:\n" +
289
+ "{ \"source_commit\": \"<rev-parse output>\", \"crew_release\": \"<basename output>\", \"published_at\": \"<date output>\", \"task_id\": \"" + taskId + "\" }.\n" +
290
+ "Confirm the response contains ok: true. If setprovenance fails, report it in your summary but still run post-deploy to release the lock.\n\n" +
192
291
  "STEP 2: Finalize.\n" +
193
292
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
194
- "If the output contains DEPLOYED, deployment is complete.\n\n" +
293
+ "If the output contains DEPLOYED, publishing is complete.\n\n" +
195
294
  "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" +
295
+ "Report the failure: { \"passed\": false, \"summary\": \"artifact publish failed: [details]\" }.\n\n" +
296
+ "Your final response MUST be valid JSON and nothing else: { \"summary\": \"published changes\", \"passed\": true }.\n" +
297
+ "No prose, no markdown, just the JSON object.";
298
+ } else if (PUBLISH_TYPE === "vercel") {
299
+ // vercel publish is not yet implemented — block without inventing behavior
300
+ instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
301
+ "Do NOT invent publish behavior — do not guess CLI commands, APIs, or deployment steps.\n" +
302
+ "Your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"vercel publish not yet implemented\" }.\n" +
198
303
  "No prose, no markdown, just the JSON object.";
199
304
  }
200
-
201
305
  } else if (step.name === "QA") {
202
- if (DEPLOY_TYPE === "artifact") {
306
+ if (PUBLISH_TYPE === "artifact") {
203
307
  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" +
308
+ instructions = "You are code-blind QA. You NEVER read source files.\n" +
309
+ "Public docs (API.md, README, published action schemas) are NOT source code — read them freely, exactly as a user would.\n\n" +
310
+ "STEP 1: Trigger a visual inspection of the published artifact.\n" +
206
311
  "Call artifact_inspect with:\n" +
207
- " slug: \"" + DEPLOY_SLUG + "\"\n" +
312
+ " slug: \"" + PUBLISH_SLUG + "\"\n" +
208
313
  " repair_authorized: false\n" +
209
314
  " verbatim_request: \"Verify task: " + safeTitle + ". " + safeDesc + "\"\n\n" +
210
315
  "This call is asynchronous — it fires the inspection but results arrive outside this workflow. That is expected and correct.\n\n" +
211
316
  "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" +
317
+ "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" +
318
+ "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" +
319
+ "PROVENANCE CHECK: Call artifact_invoke_action on slug \"" + PUBLISH_SLUG + "\", action \"getprovenance\", args: {}.\n" +
320
+ "If provenance is null, FAIL: { \"passed\": false, \"summary\": \"provenance missing — publish did not stamp source/crew release\" }.\n" +
321
+ "Run: cd " + REPO_PATH + " && git rev-parse HEAD\n" +
322
+ "Run: basename $(readlink " + crewHome + "/current)\n" +
323
+ "If provenance.source_commit does not equal the rev-parse output or provenance.crew_release does not equal the basename output, FAIL: { \"passed\": false, \"summary\": \"provenance mismatch: [details]\" }.\n\n" +
213
324
  "STEP 3: File follow-up tasks for any related issues you discover.\n" +
214
325
  "Use artifact_invoke_action createtask on slug \"" + DASHBOARD_SLUG + "\" for each issue.\n\n" +
215
326
  "Your final response MUST be valid JSON and nothing else:\n" +
@@ -217,7 +328,9 @@ while (i < STEPS.length) {
217
328
  "No prose, no markdown, just the JSON object.";
218
329
  } else {
219
330
  instructions = "Test from a user's perspective. You are CODE-BLIND — do NOT read source code.\n" +
331
+ "Public docs (API.md, README) are NOT source code — read them freely, exactly as a user would.\n" +
220
332
  "Verify the change is working as described in the task.\n" +
333
+ "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
334
  "File follow-up tasks via artifact_invoke_action createtask on slug \"" + DASHBOARD_SLUG + "\" for related issues found.\n\n" +
222
335
  "Your final response MUST be valid JSON and nothing else: { \"passed\": true/false, \"summary\": \"what you tested and found\" }.\n" +
223
336
  "No prose, no markdown, just the JSON object.";
@@ -228,8 +341,8 @@ while (i < STEPS.length) {
228
341
  var eventPreamble = "";
229
342
  if (step.name !== "Review") {
230
343
  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";
344
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"task_id\": \"" + taskId + "\" }.\n" +
345
+ "The returned events are filtered to this task. They contain notes and decisions from prior phases.\n\n";
233
346
  }
234
347
 
235
348
  // Run work agent WITH schema — runtime retries on non-JSON via structured outputs
@@ -247,7 +360,7 @@ while (i < STEPS.length) {
247
360
  "CONSTRAINT: Do NOT call logevent or upsertagentsession — the workflow handles all phase tracking after your step completes.\n\n" +
248
361
  "Stay in character. Do the work thoroughly.",
249
362
  {
250
- key: "work-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
363
+ key: "work-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
251
364
  label: step.identity + ": " + step.name + " on \"" + taskTitle + "\"",
252
365
  timeoutMs: 3600000,
253
366
  schema: WORK_SCHEMA
@@ -296,7 +409,7 @@ while (i < STEPS.length) {
296
409
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
297
410
  "{ \"task_id\": \"" + taskId + "\", \"type\": \"" + status + "\", \"identity\": \"" + step.identity + "\", \"message\": \"" + step.name + " " + status + " by " + step.identity + "\" }.",
298
411
  {
299
- key: "record-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
412
+ key: "record-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
300
413
  label: "Recording " + step.name + " result",
301
414
  schema: { type: "object" }
302
415
  }
@@ -304,14 +417,14 @@ while (i < STEPS.length) {
304
417
 
305
418
  // Handle rejection — bounce back to Build
306
419
  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." };
420
+ totalReworkCount++;
421
+ if (totalReworkCount > MAX_TOTAL_REWORK) {
422
+ log("Shared rework budget exhausted for task " + taskId + " — worktree preserved at .worktrees/" + taskId + " for manual inspection");
423
+ 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
424
  }
312
425
  rejectionNotes = summary;
313
426
  i = BUILD_INDEX;
314
- log(step.name + " rejected — bouncing to Build (rework #" + reworkCount + ")");
427
+ log(step.name + " rejected — bouncing to Build (rework #" + totalReworkCount + " of " + MAX_TOTAL_REWORK + ")");
315
428
  continue;
316
429
  }
317
430
 
@@ -321,10 +434,10 @@ while (i < STEPS.length) {
321
434
  return { status: "blocked", task_id: taskId, reason: "Integration failed: " + summary };
322
435
  }
323
436
 
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 };
437
+ // Publish failure blocks the task
438
+ if (!passed && step.name === "Publish") {
439
+ log("Publish failed for task " + taskId + ": " + summary);
440
+ return { status: "blocked", task_id: taskId, reason: "Publish failed: " + summary };
328
441
  }
329
442
 
330
443
  i++;