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-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,17 @@ 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 || {};
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 || "";
36
41
  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;
42
+ const PUBLISH_TYPE = projectConfig.deploy_type || "";
43
+ const PUBLISH_SLUG = projectConfig.deploy_slug || DASHBOARD_SLUG;
39
44
  const PROJECT_DESC = projectConfig.description || "React + TypeScript web dashboard (client/src/, server/src/, drizzle/)";
40
45
  const RELEASE_SCRIPT = crewHome + "/crew-release.sh";
41
46
 
@@ -58,10 +63,11 @@ const STEPS = [
58
63
  { name: "Build", identity: "wren" },
59
64
  { name: "Review", identity: "cass" },
60
65
  { name: "Integrate", identity: "wren" },
61
- { name: "Deploy", identity: "wren" }
66
+ { name: "Publish", identity: "wren" }
62
67
  ];
63
68
  const BUILD_INDEX = STEPS.findIndex(s => s.name === 'Build');
64
69
  if (BUILD_INDEX < 0) throw new Error("STEPS missing 'Build' step");
70
+ const REWORK_STEP = STEPS[BUILD_INDEX].name;
65
71
  const MAX_REWORK = 2;
66
72
  let reworkCount = 0;
67
73
  let rejectionNotes = inputs.rejection_notes || "";
@@ -71,7 +77,7 @@ let i = startStepIndex;
71
77
  // Pin lifecycle scripts
72
78
  await agent(
73
79
  "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,
80
+ "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
81
  { key: "pin-lifecycle", label: "Pinning lifecycle scripts", schema: { type: "object" } }
76
82
  );
77
83
 
@@ -82,6 +88,62 @@ while (i < STEPS.length) {
82
88
  phase(step.name);
83
89
  log(step.name + " step (" + step.identity + ") for task " + taskId);
84
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
+
129
+ // Publish is optional and target-based. Empty target = prototyping project: skip the phase.
130
+ // Unknown target (incl. legacy "repo") = config error: block.
131
+ if (step.name === "Publish" && !PUBLISH_TYPE) {
132
+ log("Publish skipped for task " + taskId + " — no publish target configured (deploy_type empty)");
133
+ await agent(
134
+ "Release the merge lock and clean up without publishing.\n" +
135
+ "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
136
+ "If the output contains DEPLOYED, the lock is released and the worktree is cleaned up.",
137
+ { key: "publish-skip-cleanup", label: "Skipping Publish (no target)", schema: { type: "object" } }
138
+ );
139
+ i++;
140
+ continue;
141
+ }
142
+ if (step.name === "Publish" && PUBLISH_TYPE !== "npm" && PUBLISH_TYPE !== "artifact" && PUBLISH_TYPE !== "vercel") {
143
+ log("Unknown publish target for task " + taskId + ": " + PUBLISH_TYPE);
144
+ return { status: "blocked", task_id: taskId, reason: "Unknown publish target '" + PUBLISH_TYPE + "' — expected 'npm', 'artifact', 'vercel', or empty (skip publish)." };
145
+ }
146
+
85
147
  let activeSessionId;
86
148
  if (isFirstClaim && firstSessionId) {
87
149
  activeSessionId = firstSessionId;
@@ -121,8 +183,10 @@ while (i < STEPS.length) {
121
183
  (mapperSpec ? "MAPPER'S SPEC (implement exactly this):\n" + mapperSpec + "\n\n" : "") +
122
184
  "Your working directory: " + REPO_PATH + "/.worktrees/" + taskId + "/\n" +
123
185
  "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" +
186
+ "Edit the TypeScript source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
187
+ "Do not add unrequested features.\n" +
188
+ "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" +
189
+ (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
190
  "STEP 3: Commit your changes.\n" +
127
191
  "cd " + REPO_PATH + "/.worktrees/" + taskId + "\n" +
128
192
  "git add -A\n" +
@@ -135,9 +199,12 @@ while (i < STEPS.length) {
135
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") +
136
200
  "Examine the code changes by running:\n" +
137
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" +
138
203
  "You can also read specific files in the worktree at:\n" +
139
204
  REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
140
205
  "Check quality, correctness, spec compliance.\n" +
206
+ "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" +
207
+ (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
208
  "If it passes, your final response MUST be valid JSON and nothing else: { \"passed\": true, \"summary\": \"approval notes\" }.\n" +
142
209
  "If it fails, your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"rejection notes\" }.\n" +
143
210
  "No prose, no markdown, just the JSON object.";
@@ -147,40 +214,76 @@ while (i < STEPS.length) {
147
214
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " integrate " + taskId + " \"merge: chore: " + safeTitle + "\"\n\n" +
148
215
  "Read the output:\n" +
149
216
  "- 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" +
217
+ "- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Set passed to false.\n" +
151
218
  "- If it contains CONFLICT, a merge conflict occurred. Set passed to false with details.\n" +
152
219
  "- If it contains ERROR, something else failed. Set passed to false.\n\n" +
220
+ "\n" +
221
+ "STEP 2: Push the merged main to the remote repository.\n" +
222
+ "Run: cd " + REPO_PATH + " && git push origin main\n" +
223
+ "- If the push succeeds, report the merged commit hash.\n" +
224
+ "- If the push is rejected as non-fast-forward (the remote has commits not present locally),\n" +
225
+ " NEVER force-push. Do not run any --force variant. Set passed to false with summary:\n" +
226
+ " 'git push origin main rejected as non-fast-forward — remote main has diverged; manual resolution required'.\n\n" +
153
227
  "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true/false }. No prose, no markdown, just the JSON object.";
154
228
 
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" +
229
+ } else if (step.name === "Publish") {
230
+ if (PUBLISH_TYPE === "npm") {
231
+ // npm packages: immutable release + pack + publish to the registry (push is universal in Integrate)
232
+ instructions = "Publish the npm package to the registry.\n\n" +
233
+ "The repo push already happened in Integrate — do NOT push to git in this phase, and NEVER force-push.\n" +
234
+ "Version discipline: publish ships the exact version merged in Integrate (Build applied it, Review validated it). Do not bump the version here.\n\n" +
235
+ "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
160
236
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
161
- "STEP 1: Install and activate the new release.\n" +
237
+ "STEP 1: Install and activate the immutable release.\n" +
162
238
  "Run: " + RELEASE_SCRIPT + " deploy " + REPO_PATH + "\n" +
163
239
  "Verify the output contains INSTALLED and ACTIVATED (or EXISTS and ACTIVATED if unchanged).\n\n" +
164
- "STEP 2: Finalize.\n" +
240
+ "STEP 2: Check whether the package version needs publishing.\n" +
241
+ "Read the version from: cd " + REPO_PATH + " && node -p \"require('./package.json').version\"\n" +
242
+ "Check the registry: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
243
+ "If the local version matches the registry version, the version is already live — skip to STEP 5.\n\n" +
244
+ "STEP 3: Pack and publish.\n" +
245
+ "Run: cd " + REPO_PATH + " && npm pack\n" +
246
+ "Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-$(node -p \"require('" + REPO_PATH + "/package.json').version\").tgz\n" +
247
+ "If publish fails with 'You cannot publish over the previously published versions', the version is already live — continue to STEP 4.\n\n" +
248
+ "STEP 4: Verify.\n" +
249
+ "Run: npm view muse-crew version\n" +
250
+ "Confirm it matches the local package.json version.\n\n" +
251
+ "STEP 5: Finalize.\n" +
165
252
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
166
253
  "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" +
254
+ "Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
168
255
  "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" +
256
+ } else if (PUBLISH_TYPE === "artifact") {
257
+ // Artifact projects: rebuild the live artifact via artifact_edit, then finalize
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" +
260
+ "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
261
+ "STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
172
262
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
173
- "STEP 1: Deploy to the live artifact.\n" +
263
+ "STEP 1: Publish to the live artifact.\n" +
174
264
  "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" +
265
+ "Then call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
176
266
  "'Rebuild the application from current source. Do not modify any source files — just rebuild and deploy what is on disk.'\n" +
177
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" +
178
275
  "STEP 2: Finalize.\n" +
179
276
  "Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
180
- "If the output contains DEPLOYED, deployment is complete.\n\n" +
277
+ "If the output contains DEPLOYED, publishing is complete.\n\n" +
181
278
  "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" +
279
+ "Report the failure: { \"passed\": false, \"summary\": \"artifact publish failed: [details]\" }.\n\n" +
280
+ "Your final response MUST be valid JSON and nothing else: { \"summary\": \"published changes\", \"passed\": true }.\n" +
281
+ "No prose, no markdown, just the JSON object.";
282
+ } else if (PUBLISH_TYPE === "vercel") {
283
+ // vercel publish is not yet implemented — block without inventing behavior
284
+ instructions = "The project's publish target is \"vercel\", which is not yet implemented.\n" +
285
+ "Do NOT invent publish behavior — do not guess CLI commands, APIs, or deployment steps.\n" +
286
+ "Your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"vercel publish not yet implemented\" }.\n" +
184
287
  "No prose, no markdown, just the JSON object.";
185
288
  }
186
289
  }
@@ -189,8 +292,8 @@ while (i < STEPS.length) {
189
292
  var eventPreamble = "";
190
293
  if (step.name !== "Review") {
191
294
  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";
295
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"task_id\": \"" + taskId + "\" }.\n" +
296
+ "The returned events are filtered to this task. They contain notes and decisions from prior phases.\n\n";
194
297
  }
195
298
 
196
299
  var stepResult;
@@ -273,9 +376,9 @@ while (i < STEPS.length) {
273
376
  return { status: "blocked", task_id: taskId, reason: "Integration failed: " + summary };
274
377
  }
275
378
 
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 };
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 };
279
382
  }
280
383
 
281
384
  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,
@@ -284,6 +285,7 @@ for (var p = 0; p < toProcess.length; p++) {
284
285
  start_step_index: item.startStep,
285
286
  rejection_notes: item.rejectionNotes || "",
286
287
  project_config: projectCfg,
288
+ project_id: taskProject,
287
289
  dashboardSlug: DASHBOARD_SLUG,
288
290
  crewHome: crewHome
289
291
  };
@@ -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,14 @@ 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
+ // 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 || "";
31
+ const REPO_PATH = projectConfig.repo_path || crewHome;
32
+
25
33
  if (!taskId) {
26
34
  throw new Error("task_id is required in args");
27
35
  }
@@ -34,6 +42,7 @@ const STEPS = [
34
42
  ];
35
43
  const WRITE_INDEX = STEPS.findIndex(s => s.name === 'Write');
36
44
  if (WRITE_INDEX < 0) throw new Error("STEPS missing 'Write' step");
45
+ const REWORK_STEP = STEPS[WRITE_INDEX].name;
37
46
  const MAX_REWORK = 2;
38
47
  let reworkCount = 0;
39
48
  let rejectionNotes = inputs.rejection_notes || "";
@@ -46,6 +55,42 @@ while (i < STEPS.length) {
46
55
  phase(step.name);
47
56
  log(step.name + " step (" + step.identity + ") for task " + taskId);
48
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
+
49
94
  let activeSessionId;
50
95
  if (isFirstClaim && firstSessionId) {
51
96
  activeSessionId = firstSessionId;
@@ -72,7 +117,7 @@ while (i < STEPS.length) {
72
117
  if (step.name === "Triage") {
73
118
  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
119
  } 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 + "/." +
120
+ 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
121
  (rejectionNotes ? "\n\nREWORK after review rejection. Address:\n" + rejectionNotes : "") +
77
122
  "\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
123
  } else if (step.name === "Review") {
@@ -83,7 +128,7 @@ while (i < STEPS.length) {
83
128
  "Read the identity file at " + ORCH_PATH + "/identities/" + step.identity + ".md using the read tool, and embody that character fully.\n\n" +
84
129
  "## Your Assignment\n\n" +
85
130
  "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 + "/.",
131
+ "## 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
132
  {
88
133
  key: "work-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
89
134
  label: step.identity + ": " + step.name + " on \"" + taskTitle + "\"",