muse-crew 0.1.1 → 0.2.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/API.md CHANGED
@@ -210,6 +210,21 @@ Set a global configuration value.
210
210
  | `key` | string (1–80) | yes | |
211
211
  | `value` | string (≤ 10000) | yes | |
212
212
 
213
+ ### `setprovenance`
214
+
215
+ Stamp publication provenance. Called by the crew Publish phase after rebuilding the artifact.
216
+
217
+ | Field | Type | Required | Notes |
218
+ |-------|------|----------|-------|
219
+ | `source_commit` | 40-char hex | yes | Repo commit the artifact was built from |
220
+ | `crew_release` | 40-char hex | yes | Active crew release (`$CREW_HOME/current`) at publish time |
221
+ | `published_at` | ISO 8601 datetime (UTC) | yes | |
222
+ | `task_id` | string | no | Publishing task, for traceability |
223
+
224
+ ### `getprovenance`
225
+
226
+ Read the last publication provenance. Returns `{ "provenance": { "source_commit", "crew_release", "published_at" } }`, or `{ "provenance": null }` when nothing has been published yet.
227
+
213
228
  ---
214
229
 
215
230
  ## Not part of this API
package/docs/guide.md CHANGED
@@ -190,7 +190,7 @@ Every 3 minutes, the `crew-poll` cron fires:
190
190
 
191
191
  5. **Main-chat agent launches workflows** — receives the claim records and calls `workflow_launch_async` for each one.
192
192
 
193
- 6. **Workflow executes phases** — each phase dispatches an agent in character (the identity assigned to that phase), records the result, and advances the task.
193
+ 6. **Workflow executes phases** — each phase dispatches an agent in character (the identity assigned to that phase), records the result, and advances the task. At every phase boundary the workflow re-reads the task's project: if the task was moved to another project mid-run, the stale run aborts (its session is marked `failed` at the Build/Write step and its worktree is removed from the old repo) so the dispatcher re-launches it with the new project's repo and config.
194
194
 
195
195
  7. **Task completes** — when all phases finish, the dispatcher marks the task `done` on the next tick.
196
196
 
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -59,6 +59,38 @@ cmd_init() {
59
59
  echo "CURRENT: $cur"
60
60
  }
61
61
 
62
+ # ── workflow syntax validation ──────────────────────────────────────
63
+ # Every workflow script must parse before a release can install.
64
+ _validate_workflows() {
65
+ # The workflow runtime accepts top-level `export`, `return`, and `await`
66
+ # (it wraps scripts in an async function), so neither `node --check` on the
67
+ # raw .js (vacuous for ESM — exits 0 even on blatant syntax errors) nor a
68
+ # .mjs check (rejects the runtime-legal top-level `return`) is correct.
69
+ # Emulate the runtime instead: strip `export`, wrap the script in an async
70
+ # function, then node --check the result. Tokenizer errors (e.g. an
71
+ # unterminated string literal) still fail under the wrap.
72
+ local dir="$1"
73
+ local f tmp base
74
+ tmp="$(mktemp -d)"
75
+ for f in "$dir"/workflows/*.js; do
76
+ [ -f "$f" ] || continue
77
+ base="$(basename "$f" .js)"
78
+ {
79
+ echo "async function __crew_workflow__(args) {"
80
+ sed 's/^export //' "$f"
81
+ echo "}"
82
+ } > "$tmp/$base.js"
83
+ if ! node --check "$tmp/$base.js" 2>"$tmp/$base.err"; then
84
+ cat "$tmp/$base.err" >&2
85
+ echo "VALIDATION FAILED: $f does not parse" >&2
86
+ rm -rf "$tmp"
87
+ return 1
88
+ fi
89
+ done
90
+ rm -rf "$tmp"
91
+ echo "VALIDATED: workflow scripts parse"
92
+ }
93
+
62
94
  # ── deploy ────────────────────────────────────────────────────────────
63
95
  # Build, validate, and atomically activate a release from repo HEAD.
64
96
  # Single command — no cross-step lock needed.
@@ -98,6 +130,11 @@ cmd_deploy() {
98
130
  [ -d workflows ] && cp -r workflows "$staging_dir/"
99
131
  [ -d lib ] && cp -r lib "$staging_dir/"
100
132
  fi
133
+ # Gate: refuse to install a release whose workflow scripts don't parse.
134
+ if ! _validate_workflows "$staging_dir"; then
135
+ rm -rf "$staging_dir"
136
+ die "release $hash rejected: workflow syntax validation failed"
137
+ fi
101
138
  # Atomic rename into place
102
139
  mv "$staging_dir" "$release_dir"
103
140
  echo "INSTALLED: $hash"
@@ -125,6 +125,14 @@ cmd_inspect() {
125
125
  return 1
126
126
  fi
127
127
 
128
+ echo "=== Branch: task/$task_id ==="
129
+ local tip
130
+ tip=$(git rev-parse "task/$task_id")
131
+ echo "TIP: $tip"
132
+ echo ""
133
+ echo "=== Commits ahead of main ==="
134
+ git log --oneline "main..task/$task_id"
135
+ echo ""
128
136
  echo "=== Diff: main...task/$task_id ==="
129
137
  git diff --stat "main...task/$task_id"
130
138
  echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "workflows/",
13
- "identities/*.md",
13
+ "identities/",
14
14
  "personas/",
15
15
  "lib/",
16
16
  "seed/",
@@ -1,3 +1,7 @@
1
1
  # AGENTS.md
2
2
 
3
3
  Human-readable workflow definitions — the canonical source. `crew-init` copies these to `$CREW_HOME/.orchestration/workflows/` during setup. Each file describes one workflow type: its phases, identity assignments, and progression rules.
4
+
5
+ ## Sync policy
6
+
7
+ The workflow `.md` docs are synced from the activated release into `$CREW_HOME/.orchestration/workflows/` on every `crew-release.sh` release activation (deploy and rollback) — automatically, never as a manual step. The invariant is that the dashboard's workflow documents always reflect the phases the dispatcher actually runs: the dashboard reads those documents live from disk and `recovertask` validates target phases against them, so drift breaks recovery. Because the docs and the workflow scripts ship in the same repo commit, syncing at activation maintains the invariant by construction; the manual-step approach is what drifted (docs seeded once by `crew-init`'s `cp -n` went stale while the phases gained Integrate and Publish), so there is no manual re-sync step.
@@ -37,6 +37,9 @@ const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
37
37
 
38
38
  // Project config — passed by dispatcher, falls back to dashboard defaults
39
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 || "";
40
43
  const REPO_PATH = projectConfig.repo_path || "~/workspace/ts-spaces/orchestra-dashboard";
41
44
  const PUBLISH_TYPE = projectConfig.deploy_type || "";
42
45
  const PUBLISH_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
@@ -68,6 +71,7 @@ const STEPS = [
68
71
  ];
69
72
  const BUILD_INDEX = STEPS.findIndex(s => s.name === 'Build');
70
73
  if (BUILD_INDEX < 0) throw new Error("STEPS missing 'Build' step");
74
+ const REWORK_STEP = STEPS[BUILD_INDEX].name;
71
75
  // Shared rework budget: Review and QA rejections draw from the SAME pool of 2.
72
76
  // E.g. 2 Review bounces + 1 QA bounce = 3 total > budget -> task blocks.
73
77
  const MAX_TOTAL_REWORK = 2;
@@ -90,6 +94,44 @@ while (i < STEPS.length) {
90
94
  phase(step.name);
91
95
  log(step.name + " step (" + step.identity + ") for task " + taskId);
92
96
 
97
+ // ── Project-change guard ───────────────────────────────────────────
98
+ // A task moved to another project mid-run must not keep working in the
99
+ // old project's repo. Re-read the task's project at every phase
100
+ // boundary: if it differs from the project this run was dispatched for,
101
+ // abort the stale run (failed at the rework target) so the dispatcher
102
+ // re-launches the step with the new project's config.
103
+ if (LAUNCH_PROJECT_ID) {
104
+ const projectCheck = await agent(
105
+ "Read this task's current project from the dashboard.\n" +
106
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }.\n" +
107
+ "Find the task with id \"" + taskId + "\" in the returned tasks array.\n" +
108
+ "Return exactly { \"project\": \"<the task's project field, or empty string if absent>\" } and nothing else.",
109
+ {
110
+ key: "project-check-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
111
+ label: "Checking project before " + step.name,
112
+ schema: { type: "object", properties: { project: { type: "string" } }, required: ["project"] }
113
+ }
114
+ );
115
+ const currentProject = (projectCheck && projectCheck.project) ? projectCheck.project : LAUNCH_PROJECT_ID;
116
+ if (currentProject !== LAUNCH_PROJECT_ID) {
117
+ 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.";
118
+ log(abortMessage);
119
+ const abortSessionPatch = (isFirstClaim && firstSessionId) ? "\"id\": \"" + firstSessionId + "\", " : "";
120
+ await agent(
121
+ "Abort the stale run and remove its worktree from the old project's repo.\n" +
122
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"upsertagentsession\", args:\n" +
123
+ "{ \"task_id\": \"" + taskId + "\", " + abortSessionPatch + "\"identity\": \"" + step.identity + "\", \"step\": \"" + REWORK_STEP + "\", \"status\": \"failed\", " +
124
+ "\"notes\": " + JSON.stringify(abortMessage + " Rebuild from the Map session notes in the task's event history.") + " }.\n" +
125
+ "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
126
+ "{ \"task_id\": \"" + taskId + "\", \"type\": \"failed\", \"identity\": \"" + step.identity + "\", \"message\": " + JSON.stringify(abortMessage) + " }.\n" +
127
+ "Then run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " cleanup " + taskId + "\n" +
128
+ "The cleanup output should contain CLEANUP.",
129
+ { key: "abort-project-change", label: "Aborting stale run (project changed)", schema: { type: "object" } }
130
+ );
131
+ return { status: "failed", task_id: taskId, reason: abortMessage };
132
+ }
133
+ }
134
+
93
135
  // Publish is optional and target-based. Empty target = prototyping project: skip the phase.
94
136
  // Unknown target (incl. legacy "repo") = config error: block.
95
137
  if (step.name === "Publish" && !PUBLISH_TYPE) {
@@ -175,6 +217,7 @@ while (i < STEPS.length) {
175
217
  (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") +
176
218
  "Examine the code changes by running:\n" +
177
219
  "CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " inspect " + taskId + "\n\n" +
220
+ "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" +
178
221
  "You can also read specific files in the worktree at:\n" +
179
222
  REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
180
223
  "Check quality, correctness, and spec compliance.\n" +
@@ -198,8 +241,7 @@ while (i < STEPS.length) {
198
241
  "- If the push succeeds, report the merged commit hash.\n" +
199
242
  "- If the push is rejected as non-fast-forward (the remote has commits not present locally),\n" +
200
243
  " 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" +
244
+ " 'git push origin main rejected as non-fast-forward — remote main has diverged; manual resolution required'.\n\n" +
203
245
  "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true/false }. No prose, no markdown, just the JSON object.";
204
246
 
205
247
  } else if (step.name === "Publish") {
@@ -232,6 +274,7 @@ while (i < STEPS.length) {
232
274
  } else if (PUBLISH_TYPE === "artifact") {
233
275
  // Artifact projects: rebuild the live artifact via artifact_edit, then finalize
234
276
  instructions = "Publish the merged code to the live artifact.\n\n" +
277
+ "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" +
235
278
  "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
236
279
  "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
237
280
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
@@ -240,6 +283,13 @@ while (i < STEPS.length) {
240
283
  "Then call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
241
284
  "'Rebuild the application from current source. Do not modify any source files — just rebuild and deploy what is on disk.'\n" +
242
285
  "Wait for the build to complete by polling artifact_status until it is no longer running.\n\n" +
286
+ "STEP 1B: Stamp publication provenance.\n" +
287
+ "Run: cd " + REPO_PATH + " && git rev-parse HEAD\n" +
288
+ "Run: basename $(readlink " + crewHome + "/current)\n" +
289
+ "Run: date -u +%Y-%m-%dT%H:%M:%SZ\n" +
290
+ "Call artifact_invoke_action on slug \"" + PUBLISH_SLUG + "\", action \"setprovenance\", args:\n" +
291
+ "{ \"source_commit\": \"<rev-parse output>\", \"crew_release\": \"<basename output>\", \"published_at\": \"<date output>\", \"task_id\": \"" + taskId + "\" }.\n" +
292
+ "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" +
243
293
  "STEP 2: Finalize.\n" +
244
294
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
245
295
  "If the output contains DEPLOYED, publishing is complete.\n\n" +
@@ -266,6 +316,15 @@ while (i < STEPS.length) {
266
316
  "If testing passes, your final response MUST be valid JSON and nothing else: { \"passed\": true, \"summary\": \"test results\" }.\n" +
267
317
  "If testing fails, your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"failure details\" }.\n" +
268
318
  "No prose, no markdown, just the JSON object.";
319
+ if (PUBLISH_TYPE === "artifact") {
320
+ instructions = "PROVENANCE CHECK (this project publishes to a dashboard artifact).\n" +
321
+ "Call artifact_invoke_action on slug \"" + PUBLISH_SLUG + "\", action \"getprovenance\", args: {}.\n" +
322
+ "If provenance is null, FAIL: { \"passed\": false, \"summary\": \"provenance missing — publish did not stamp\" }.\n" +
323
+ "Run: cd " + REPO_PATH + " && git rev-parse HEAD\n" +
324
+ "Run: basename $(readlink " + crewHome + "/current)\n" +
325
+ "If either hash mismatches, FAIL: { \"passed\": false, \"summary\": \"provenance mismatch: [details]\" }.\n\n" +
326
+ instructions;
327
+ }
269
328
  }
270
329
 
271
330
  // Task event history — all phases except Review see the comment log
@@ -35,6 +35,9 @@ const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
35
35
 
36
36
  // Project config — passed by dispatcher, falls back to dashboard defaults
37
37
  const projectConfig = inputs.project_config || {};
38
+ // Project this run was dispatched for — the mid-run project-change guard
39
+ // compares the task's live project against this on every phase boundary.
40
+ const LAUNCH_PROJECT_ID = inputs.project_id || "";
38
41
  const REPO_PATH = projectConfig.repo_path || "~/workspace/ts-spaces/orchestra-dashboard";
39
42
  const PUBLISH_TYPE = projectConfig.deploy_type || "";
40
43
  const PUBLISH_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
@@ -64,6 +67,7 @@ const STEPS = [
64
67
  ];
65
68
  const BUILD_INDEX = STEPS.findIndex(s => s.name === 'Build');
66
69
  if (BUILD_INDEX < 0) throw new Error("STEPS missing 'Build' step");
70
+ const REWORK_STEP = STEPS[BUILD_INDEX].name;
67
71
  const MAX_REWORK = 2;
68
72
  let reworkCount = 0;
69
73
  let rejectionNotes = inputs.rejection_notes || "";
@@ -84,6 +88,44 @@ while (i < STEPS.length) {
84
88
  phase(step.name);
85
89
  log(step.name + " step (" + step.identity + ") for task " + taskId);
86
90
 
91
+ // ── Project-change guard ───────────────────────────────────────────
92
+ // A task moved to another project mid-run must not keep working in the
93
+ // old project's repo. Re-read the task's project at every phase
94
+ // boundary: if it differs from the project this run was dispatched for,
95
+ // abort the stale run (failed at the rework target) so the dispatcher
96
+ // re-launches the step with the new project's config.
97
+ if (LAUNCH_PROJECT_ID) {
98
+ const projectCheck = await agent(
99
+ "Read this task's current project from the dashboard.\n" +
100
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }.\n" +
101
+ "Find the task with id \"" + taskId + "\" in the returned tasks array.\n" +
102
+ "Return exactly { \"project\": \"<the task's project field, or empty string if absent>\" } and nothing else.",
103
+ {
104
+ key: "project-check-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
105
+ label: "Checking project before " + step.name,
106
+ schema: { type: "object", properties: { project: { type: "string" } }, required: ["project"] }
107
+ }
108
+ );
109
+ const currentProject = (projectCheck && projectCheck.project) ? projectCheck.project : LAUNCH_PROJECT_ID;
110
+ if (currentProject !== LAUNCH_PROJECT_ID) {
111
+ 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.";
112
+ log(abortMessage);
113
+ const abortSessionPatch = (isFirstClaim && firstSessionId) ? "\"id\": \"" + firstSessionId + "\", " : "";
114
+ await agent(
115
+ "Abort the stale run and remove its worktree from the old project's repo.\n" +
116
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"upsertagentsession\", args:\n" +
117
+ "{ \"task_id\": \"" + taskId + "\", " + abortSessionPatch + "\"identity\": \"" + step.identity + "\", \"step\": \"" + REWORK_STEP + "\", \"status\": \"failed\", " +
118
+ "\"notes\": " + JSON.stringify(abortMessage + " Rebuild from the Map session notes in the task's event history.") + " }.\n" +
119
+ "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
120
+ "{ \"task_id\": \"" + taskId + "\", \"type\": \"failed\", \"identity\": \"" + step.identity + "\", \"message\": " + JSON.stringify(abortMessage) + " }.\n" +
121
+ "Then run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " cleanup " + taskId + "\n" +
122
+ "The cleanup output should contain CLEANUP.",
123
+ { key: "abort-project-change", label: "Aborting stale run (project changed)", schema: { type: "object" } }
124
+ );
125
+ return { status: "failed", task_id: taskId, reason: abortMessage };
126
+ }
127
+ }
128
+
87
129
  // Publish is optional and target-based. Empty target = prototyping project: skip the phase.
88
130
  // Unknown target (incl. legacy "repo") = config error: block.
89
131
  if (step.name === "Publish" && !PUBLISH_TYPE) {
@@ -157,6 +199,7 @@ while (i < STEPS.length) {
157
199
  (mapperSpec ? "MAPPER'S SPEC (the builder was asked to implement exactly this):\n" + mapperSpec + "\n\n" : "Read the spec from the task description.\n\n") +
158
200
  "Examine the code changes by running:\n" +
159
201
  "CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " inspect " + taskId + "\n\n" +
202
+ "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" +
160
203
  "You can also read specific files in the worktree at:\n" +
161
204
  REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
162
205
  "Check quality, correctness, spec compliance.\n" +
@@ -180,8 +223,7 @@ while (i < STEPS.length) {
180
223
  "- If the push succeeds, report the merged commit hash.\n" +
181
224
  "- If the push is rejected as non-fast-forward (the remote has commits not present locally),\n" +
182
225
  " 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" +
226
+ " 'git push origin main rejected as non-fast-forward — remote main has diverged; manual resolution required'.\n\n" +
185
227
  "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true/false }. No prose, no markdown, just the JSON object.";
186
228
 
187
229
  } else if (step.name === "Publish") {
@@ -214,6 +256,7 @@ while (i < STEPS.length) {
214
256
  } else if (PUBLISH_TYPE === "artifact") {
215
257
  // Artifact projects: rebuild the live artifact via artifact_edit, then finalize
216
258
  instructions = "Publish the merged code to the live artifact.\n\n" +
259
+ "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" +
217
260
  "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
218
261
  "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
219
262
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
@@ -222,6 +265,13 @@ while (i < STEPS.length) {
222
265
  "Then call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
223
266
  "'Rebuild the application from current source. Do not modify any source files — just rebuild and deploy what is on disk.'\n" +
224
267
  "Wait for the build to complete by polling artifact_status until it is no longer running.\n\n" +
268
+ "STEP 1B: Stamp publication provenance.\n" +
269
+ "Run: cd " + REPO_PATH + " && git rev-parse HEAD\n" +
270
+ "Run: basename $(readlink " + crewHome + "/current)\n" +
271
+ "Run: date -u +%Y-%m-%dT%H:%M:%SZ\n" +
272
+ "Call artifact_invoke_action on slug \"" + PUBLISH_SLUG + "\", action \"setprovenance\", args:\n" +
273
+ "{ \"source_commit\": \"<rev-parse output>\", \"crew_release\": \"<basename output>\", \"published_at\": \"<date output>\", \"task_id\": \"" + taskId + "\" }.\n" +
274
+ "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" +
225
275
  "STEP 2: Finalize.\n" +
226
276
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
227
277
  "If the output contains DEPLOYED, publishing is complete.\n\n" +
@@ -285,6 +285,7 @@ for (var p = 0; p < toProcess.length; p++) {
285
285
  start_step_index: item.startStep,
286
286
  rejection_notes: item.rejectionNotes || "",
287
287
  project_config: projectCfg,
288
+ project_id: taskProject,
288
289
  dashboardSlug: DASHBOARD_SLUG,
289
290
  crewHome: crewHome
290
291
  };
package/workflows/docs.js CHANGED
@@ -25,6 +25,9 @@ const ORCH_PATH = crewHome + "/.orchestration";
25
25
  // Project repo — the docs workflow edits the project's own docs, not the crew home.
26
26
  // Falls back to crew home only for manual launches without a project config.
27
27
  const projectConfig = inputs.project_config || {};
28
+ // Project this run was dispatched for — the mid-run project-change guard
29
+ // compares the task's live project against this on every phase boundary.
30
+ const LAUNCH_PROJECT_ID = inputs.project_id || "";
28
31
  const REPO_PATH = projectConfig.repo_path || crewHome;
29
32
 
30
33
  if (!taskId) {
@@ -39,6 +42,7 @@ const STEPS = [
39
42
  ];
40
43
  const WRITE_INDEX = STEPS.findIndex(s => s.name === 'Write');
41
44
  if (WRITE_INDEX < 0) throw new Error("STEPS missing 'Write' step");
45
+ const REWORK_STEP = STEPS[WRITE_INDEX].name;
42
46
  const MAX_REWORK = 2;
43
47
  let reworkCount = 0;
44
48
  let rejectionNotes = inputs.rejection_notes || "";
@@ -51,6 +55,42 @@ while (i < STEPS.length) {
51
55
  phase(step.name);
52
56
  log(step.name + " step (" + step.identity + ") for task " + taskId);
53
57
 
58
+ // ── Project-change guard ───────────────────────────────────────────
59
+ // A task moved to another project mid-run must not keep editing the old
60
+ // project's repo. Re-read the task's project at every phase boundary: if
61
+ // it differs from the project this run was dispatched for, abort the
62
+ // stale run (failed at the rework target) so the dispatcher re-launches
63
+ // the step with the new project's config.
64
+ if (LAUNCH_PROJECT_ID) {
65
+ const projectCheck = await agent(
66
+ "Read this task's current project from the dashboard.\n" +
67
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }.\n" +
68
+ "Find the task with id \"" + taskId + "\" in the returned tasks array.\n" +
69
+ "Return exactly { \"project\": \"<the task's project field, or empty string if absent>\" } and nothing else.",
70
+ {
71
+ key: "project-check-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
72
+ label: "Checking project before " + step.name,
73
+ schema: { type: "object", properties: { project: { type: "string" } }, required: ["project"] }
74
+ }
75
+ );
76
+ const currentProject = (projectCheck && projectCheck.project) ? projectCheck.project : LAUNCH_PROJECT_ID;
77
+ if (currentProject !== LAUNCH_PROJECT_ID) {
78
+ 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.";
79
+ log(abortMessage);
80
+ const abortSessionPatch = (isFirstClaim && firstSessionId) ? "\"id\": \"" + firstSessionId + "\", " : "";
81
+ await agent(
82
+ "Abort the stale run.\n" +
83
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"upsertagentsession\", args:\n" +
84
+ "{ \"task_id\": \"" + taskId + "\", " + abortSessionPatch + "\"identity\": \"" + step.identity + "\", \"step\": \"" + REWORK_STEP + "\", \"status\": \"failed\", " +
85
+ "\"notes\": " + JSON.stringify(abortMessage) + " }.\n" +
86
+ "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
87
+ "{ \"task_id\": \"" + taskId + "\", \"type\": \"failed\", \"identity\": \"" + step.identity + "\", \"message\": " + JSON.stringify(abortMessage) + " }.",
88
+ { key: "abort-project-change", label: "Aborting stale run (project changed)", schema: { type: "object" } }
89
+ );
90
+ return { status: "failed", task_id: taskId, reason: abortMessage };
91
+ }
92
+ }
93
+
54
94
  let activeSessionId;
55
95
  if (isFirstClaim && firstSessionId) {
56
96
  activeSessionId = firstSessionId;
@@ -37,6 +37,9 @@ const ORPHAN_SWEEP = RUN_LIB + "/orphan-sweep.sh";
37
37
 
38
38
  // Project config — passed by dispatcher, falls back to dashboard defaults
39
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 || "";
40
43
  const REPO_PATH = projectConfig.repo_path || "~/workspace/ts-spaces/orchestra-dashboard";
41
44
  const PUBLISH_TYPE = projectConfig.deploy_type || "";
42
45
  const PUBLISH_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
@@ -67,6 +70,7 @@ const STEPS = [
67
70
  ];
68
71
  const BUILD_INDEX = STEPS.findIndex(s => s.name === 'Build');
69
72
  if (BUILD_INDEX < 0) throw new Error("STEPS missing 'Build' step");
73
+ const REWORK_STEP = STEPS[BUILD_INDEX].name;
70
74
  // Shared rework budget: Review and QA rejections draw from the SAME pool of 2.
71
75
  // E.g. 2 Review bounces + 1 QA bounce = 3 total > budget -> task blocks.
72
76
  const MAX_TOTAL_REWORK = 2;
@@ -98,6 +102,44 @@ while (i < STEPS.length) {
98
102
  phase(step.name);
99
103
  log(step.name + " step (" + step.identity + ") for task " + taskId);
100
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
+
101
143
  // Publish is optional and target-based. Empty target = prototyping project: skip the phase.
102
144
  // Unknown target (incl. legacy "repo") = config error: block.
103
145
  if (step.name === "Publish" && !PUBLISH_TYPE) {
@@ -173,6 +215,7 @@ while (i < STEPS.length) {
173
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") +
174
216
  "Examine the code changes by running:\n" +
175
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" +
176
219
  "You can also read specific files in the worktree at:\n" +
177
220
  REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
178
221
  "Check quality, correctness, and spec compliance.\n" +
@@ -229,6 +272,7 @@ while (i < STEPS.length) {
229
272
  } else if (PUBLISH_TYPE === "artifact") {
230
273
  // Artifact projects: rebuild the live artifact via artifact_edit, then finalize
231
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" +
232
276
  "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
233
277
  "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
234
278
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
@@ -237,6 +281,13 @@ while (i < STEPS.length) {
237
281
  "Then call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
238
282
  "'Rebuild the application from current source. Do not modify any source files — just rebuild and deploy what is on disk.'\n" +
239
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" +
240
291
  "STEP 2: Finalize.\n" +
241
292
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
242
293
  "If the output contains DEPLOYED, publishing is complete.\n\n" +
@@ -265,6 +316,11 @@ while (i < STEPS.length) {
265
316
  "STEP 2: Verify data integrity via the dashboard API.\n" +
266
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" +
267
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" +
268
324
  "STEP 3: File follow-up tasks for any related issues you discover.\n" +
269
325
  "Use artifact_invoke_action createtask on slug \"" + DASHBOARD_SLUG + "\" for each issue.\n\n" +
270
326
  "Your final response MUST be valid JSON and nothing else:\n" +