muse-crew 0.6.6 → 0.6.7

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/workflows/docs.js CHANGED
@@ -22,11 +22,19 @@ const startStepIndex = inputs.start_step_index || 0;
22
22
  // resolution back via updatetask in the self-claim below.
23
23
  const RESOLVED_WORKFLOW = inputs.resolved_workflow || null;
24
24
  const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
25
- const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"workflow\": \"" + RESOLVED_WORKFLOW + "\"" : "";
26
25
 
27
26
  // Config from args — backward-compatible fallbacks for manual launches
28
- const DASHBOARD_SLUG = inputs.dashboardSlug || "orchestra-dashboard";
29
27
  const crewHome = inputs.crewHome || "~/workspace/.jarvis";
28
+ // Crew API: the workflow calls the crew-owned CLI, not the dashboard.
29
+ // The CLI implements the API.md contract against $CREW_HOME/crew-state.db.
30
+ const CREW_API = crewHome + "/current/lib/crew-api.js";
31
+ // Build a shell command invoking the CLI. Args are JSON-encoded and
32
+ // single-quote-wrapped for safe shell passing. The agent runs this and
33
+ // returns the stdout verbatim (the CLI emits JSON on stdout).
34
+ function crewCmd(command, args) {
35
+ var json = JSON.stringify(args || {}).replace(/'/g, "'\\''");
36
+ return "node " + CREW_API + " --crew-home " + crewHome + " " + command + " --json '" + json + "'";
37
+ }
30
38
  const ORCH_PATH = crewHome + "/.orchestration";
31
39
 
32
40
  // Project repo — the docs workflow edits the project's own docs, not the crew home.
@@ -35,7 +43,12 @@ const projectConfig = inputs.project_config || {};
35
43
  // Project this run was dispatched for — the mid-run project-change guard
36
44
  // compares the task's live project against this on every phase boundary.
37
45
  const LAUNCH_PROJECT_ID = inputs.project_id || "";
38
- const REPO_PATH = projectConfig.repo_path || crewHome;
46
+ // Fail closed on a missing repo_path docs go in the project repo,
47
+ // never in the crew home.
48
+ const REPO_PATH = projectConfig.repo_path || "";
49
+ if (!REPO_PATH) {
50
+ throw new Error("Project '" + (inputs.project_id || "unknown") + "' has no repo_path configured — set it via updateproject before dispatching tasks.");
51
+ }
39
52
 
40
53
  if (!taskId) {
41
54
  throw new Error("task_id is required in args");
@@ -69,10 +82,9 @@ async function parkTask(reason) {
69
82
  try {
70
83
  await agent(
71
84
  "Park this task for human attention.\n" +
72
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"parktask\", args: " +
73
- JSON.stringify({ task_id: taskId, message: parkMessage }) + ".\n" +
85
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("park-task", { task_id: taskId, message: parkMessage }) + "\n" +
74
86
  "The parked state is the human-attention signal — the dispatcher skips parked tasks.",
75
- { key: "park-task", label: "Parking task for human attention", schema: { type: "object" } }
87
+ { key: "park-task", label: "Parking task for human attention" }
76
88
  );
77
89
  } catch (parkErr) {
78
90
  log("PARK FAILED for task " + taskId + ": " + (parkErr && parkErr.message ? parkErr.message : parkErr) + " — park did not land, reporting failed so the next tick retries");
@@ -97,8 +109,8 @@ while (i < STEPS.length) {
97
109
  // the step with the new project's config.
98
110
  if (LAUNCH_PROJECT_ID) {
99
111
  const projectCheck = await agent(
100
- "Read this task's current project from the dashboard.\n" +
101
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }.\n" +
112
+ "Read this task's current project from the crew API.\n" +
113
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("get-state", { events_limit: 1 }) + "\n" +
102
114
  "Find the task with id \"" + taskId + "\" in the returned tasks array.\n" +
103
115
  "Return exactly { \"project\": \"<the task's project field, or empty string if absent>\" } and nothing else.",
104
116
  {
@@ -113,12 +125,12 @@ while (i < STEPS.length) {
113
125
  log(abortMessage);
114
126
  await agent(
115
127
  "Abort the stale run.\n" +
116
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"upsertagentsession\", args:\n" +
117
- "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + REWORK_STEP + "\", \"status\": \"failed\", " +
118
- "\"notes\": " + JSON.stringify(abortMessage) + " }.\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) + " }.",
121
- { key: "abort-project-change", label: "Aborting stale run (project changed)", schema: { type: "object" } }
128
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
129
+ task_id: taskId,
130
+ session: { task_id: taskId, identity: step.identity, step: REWORK_STEP, status: "failed", notes: abortMessage },
131
+ event: { task_id: taskId, type: "failed", identity: step.identity, message: abortMessage }
132
+ }) + "\n",
133
+ { key: "abort-project-change", label: "Aborting stale run (project changed)" }
122
134
  );
123
135
  return { status: "failed", task_id: taskId, reason: abortMessage };
124
136
  }
@@ -131,11 +143,12 @@ while (i < STEPS.length) {
131
143
  // claimed:false and this run stands down as a duplicate.
132
144
  let activeSessionId;
133
145
  if (isFirstClaim) {
146
+ var firstClaimUpdateArgs = { id: taskId, state: "in_progress" };
147
+ if (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) firstClaimUpdateArgs.workflow = RESOLVED_WORKFLOW;
134
148
  const claimResult = await agent(
135
149
  "Claim this task for the " + step.name + " step.\n" +
136
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\"" + CLAIM_WORKFLOW_PERSIST + " }.\n" +
137
- "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
138
- "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started\" }.\n" +
150
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("update-task", firstClaimUpdateArgs) + "\n" +
151
+ "Then run in shell and return the stdout verbatim:\n" + crewCmd("claim-task", { task_id: taskId, identity: step.identity, step: step.name, notes: step.name + " step started" }) + "\n" +
139
152
  "Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
140
153
  "Return { claimed: <verbatim>, session_id: \"<...>\" }. If claimed is false there is no session_id; return { claimed: false, session_id: \"\" }.",
141
154
  {
@@ -156,8 +169,7 @@ while (i < STEPS.length) {
156
169
  } else {
157
170
  const claimResult = await agent(
158
171
  "Claim a session for this task step.\n" +
159
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
160
- "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started" + (reworkCount > 0 ? " (rework #" + reworkCount + ")" : "") + "\" }.\n" +
172
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("claim-task", { task_id: taskId, identity: step.identity, step: step.name, notes: step.name + " step started" + (reworkCount > 0 ? " (rework #" + reworkCount + ")" : "") }) + "\n" +
161
173
  "Return the session_id from the response.",
162
174
  {
163
175
  key: "claim-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
@@ -274,20 +286,58 @@ while (i < STEPS.length) {
274
286
  function workRetryKey(stepName, reworkSuffix, attempt) {
275
287
  return "work-" + stepName + reworkSuffix + "-t" + attempt;
276
288
  }
289
+ // TOOL_CHECK_PREAMBLE - every work agent runs this first. The artifact tool
290
+ // namespace is deferred for workflow children: present but invisible until
291
+ // the child loads it via tool_search.load_tool_namespace (bug 3472bf36 root
292
+ // cause, verified 2026-09-11 by direct probe: 5/5 children self-loaded it;
293
+ // the "non-deterministic platform flake" was children never being told to
294
+ // load it). The two signal lines are the ONLY machine-read tool-availability
295
+ // evidence - the workflow never guesses from English prose.
296
+ // Byte-identical across standard/bugfix/chore/docs - pinned by
297
+ // tests/artifact-tools.test.js.
298
+ var TOOL_CHECK_PREAMBLE =
299
+ "TOOL CHECK (do this first, before any other work):\n" +
300
+ "1. Call tool_search.load_tool_namespace with paths [\"artifact\"].\n" +
301
+ "2. Write exactly one line: artifact_tools: ok - or artifact_tools: missing if the call failed or the tool does not exist.\n" +
302
+ "3. Run the shell command: echo tool-probe-ok - then write exactly one line: shell_transport: ok - or shell_transport: unavailable if you cannot run shell commands.\n" +
303
+ "Then do the assignment below.\n\n";
277
304
  function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
278
- // reason: "discarded" (the runtime threw the output away it could not be
279
- // machine-read) or "empty" (agent() returned without throwing but produced
280
- // nothing usable). The trailer tells the retry what to expect, not just to
281
- // try again.
305
+ // reason: "discarded" (the runtime threw the output away - it could not be
306
+ // machine-read), "empty" (agent() returned without throwing but produced
307
+ // nothing usable), "no-tools" (the worker's TOOL CHECK reported
308
+ // artifact_tools: missing), or "no-transport" (the worker's TOOL CHECK
309
+ // reported shell_transport: unavailable).
310
+ // The trailer tells the retry what to expect, not just to try again.
282
311
  var why = reason === "empty"
283
312
  ? "your previous attempt returned no usable output"
313
+ : reason === "no-tools"
314
+ ? "your previous attempt's TOOL CHECK reported artifact_tools: missing (this is a fresh launch, so run the TOOL CHECK's load step again before the work)"
315
+ : reason === "no-transport"
316
+ ? "your previous attempt's TOOL CHECK reported shell_transport: unavailable (this is a fresh launch, so run the TOOL CHECK's shell probe again before the work)"
284
317
  : "your previous attempt's output could not be machine-read as JSON and was discarded";
285
318
  return "\n\nTRANSPORT RETRY (attempt " + attempt + " of 2): " + why + ". " +
286
- "First check existing state (worktree/branch at " + repoPath + "/.worktrees/" + taskId + ", the task branch, dashboard sessions for this task) " +
319
+ "First check existing state (worktree/branch at " + repoPath + "/.worktrees/" + taskId + ", the task branch, dashboard sessions for this task) - " +
287
320
  "if the " + stepName + " work is already complete, report on what was done rather than duplicating side effects. " +
288
321
  "Then return your report as JSON in exactly the shape specified above.";
289
322
  }
290
- function describeWorkAgentFailure(stepName, identity, attempts) {
323
+ // parseToolSignals - bug 3472bf36. The work agent's TOOL CHECK emits two
324
+ // exact signal lines: artifact_tools: ok|missing and
325
+ // shell_transport: ok|unavailable. The workflow reads ONLY these lines.
326
+ // It never guesses tool availability from English prose: the old regex
327
+ // misclassified ordinary inability-prose (e.g. a Map agent describing what
328
+ // it could not inspect) and burned all three attempts on useless retries.
329
+ // Byte-identical across standard/bugfix/chore/docs - pinned by
330
+ // tests/artifact-tools.test.js.
331
+ function parseToolSignals(text) {
332
+ var t = String(text || "");
333
+ var out = { artifactTools: "unknown", shellTransport: "unknown" };
334
+ var am = t.match(/^artifact_tools:\s*(ok|missing)\s*$/m);
335
+ if (am) out.artifactTools = am[1];
336
+ var sm = t.match(/^shell_transport:\s*(ok|unavailable)\s*$/m);
337
+ if (sm) out.shellTransport = sm[1];
338
+ return out;
339
+ }
340
+ function describeWorkAgentFailure(stepName, identity, attempts) {
291
341
  // Honest classification of a work-agent call that yielded no usable
292
342
  // report, with the per-attempt evidence preserved in the session notes.
293
343
  // Two distinct cases:
@@ -359,10 +409,11 @@ while (i < STEPS.length) {
359
409
  // string. The verdict is still extracted deterministically from the report
360
410
  // text by extractVerdict below — never by an agent.
361
411
  var workPromptBase =
412
+ TOOL_CHECK_PREAMBLE +
362
413
  "Read the identity file at " + ORCH_PATH + "/identities/" + step.identity + ".md using the read tool, and embody that character fully.\n\n" +
363
414
  "## Your Assignment\n\n" +
364
- "Task: " + taskTitle + "\nTask ID: " + taskId + "\nDescription: " + taskDescription + "\nStep: " + step.name + "\nDashboard slug: " + DASHBOARD_SLUG + "\n\n" +
365
- "## 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 + "/.\n\n" +
415
+ "Task: " + taskTitle + "\nTask ID: " + taskId + "\nDescription: " + taskDescription + "\nStep: " + step.name + "\n\n" +
416
+ "## Instructions\n\n" + instructions + "\n\nCONSTRAINT: Do NOT call the crew API directly — the workflow handles all phase tracking after your step completes.\n\nStay in character. All file work under " + REPO_PATH + "/.\n\n" +
366
417
  "Return your work as JSON in exactly this shape: {\"status\": \"ok\", \"result\": \"your report here\"}. " +
367
418
  "The result is plain prose describing what you did and found. For verdict steps, end the report with exactly one line: VERDICT: PASS or VERDICT: FAIL.";
368
419
  var workKeyBase = "work-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : "");
@@ -370,7 +421,8 @@ while (i < STEPS.length) {
370
421
  var workAttempts = [];
371
422
  for (var workAttempt = 0; workAttempt <= 2; workAttempt++) {
372
423
  var workKey = workAttempt === 0 ? workKeyBase : workRetryKey(step.name, (reworkCount > 0 ? "-r" + reworkCount : ""), workAttempt);
373
- var retryReason = workAttempt === 0 ? null : (workAttempts[workAttempt - 1].threw ? "discarded" : "empty");
424
+ var prevAttempt = workAttempt === 0 ? null : workAttempts[workAttempt - 1];
425
+ var retryReason = workAttempt === 0 ? null : (prevAttempt.threw ? "discarded" : (prevAttempt.outcome === "missing-artifact-tools" ? "no-tools" : (prevAttempt.outcome === "unavailable-shell-transport" ? "no-transport" : "empty")));
374
426
  try {
375
427
  workerResult = await agent(
376
428
  workPromptBase + (workAttempt === 0 ? "" : buildTransportRetryTrailer(step.name, REPO_PATH, taskId, workAttempt, retryReason)),
@@ -385,6 +437,22 @@ while (i < STEPS.length) {
385
437
  // always fails (regression shipped in ecef136 when Date.now() was
386
438
  // removed from this loop).
387
439
  if (typeof workerResult === "string" && workerResult.trim()) {
440
+ // Bug 3472bf36: a worker whose TOOL CHECK reports artifact_tools: missing
441
+ // (or shell_transport: unavailable) gets a fresh launch - the load is
442
+ // per-launch - instead of a useless report.
443
+ var toolSignals = parseToolSignals(workerResult);
444
+ if (toolSignals.artifactTools === "missing") {
445
+ workAttempts.push({ threw: false, error: "", outcome: "missing-artifact-tools" });
446
+ log(step.name + " work agent attempt " + (workAttempt + 1) + " of 3 reported artifact_tools: missing — retrying with a fresh launch");
447
+ workerResult = null;
448
+ continue;
449
+ }
450
+ if (toolSignals.shellTransport === "unavailable") {
451
+ workAttempts.push({ threw: false, error: "", outcome: "unavailable-shell-transport" });
452
+ log(step.name + " work agent attempt " + (workAttempt + 1) + " of 3 reported shell_transport: unavailable — retrying with a fresh launch");
453
+ workerResult = null;
454
+ continue;
455
+ }
388
456
  if (workAttempt > 0) log(step.name + " work agent transport retry " + workAttempt + " returned a machine-readable report");
389
457
  break;
390
458
  }
@@ -407,11 +475,12 @@ while (i < STEPS.length) {
407
475
  log(step.name + " " + workFailure.notes + " — marking failed for retry");
408
476
  await agent(
409
477
  "Record work failure.\n" +
410
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"upsertagentsession\", args:\n" +
411
- "{ \"id\": \"" + activeSessionId + "\", \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"status\": \"failed\", \"notes\": \"" + workFailure.notes + "\" }.\n" +
412
- "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
413
- "{ \"task_id\": \"" + taskId + "\", \"type\": \"failed\", \"message\": \"" + workFailure.eventMessage + "\" }.",
414
- { key: "record-block-" + step.name, label: "Recording work failure", schema: { type: "object" } }
478
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
479
+ task_id: taskId,
480
+ session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: "failed", notes: workFailure.notes },
481
+ event: { task_id: taskId, type: "failed", message: workFailure.eventMessage }
482
+ }) + "\n",
483
+ { key: "record-block-" + step.name, label: "Recording work failure" }
415
484
  );
416
485
  return {
417
486
  __hatchWorkflowControl: "blocked",
@@ -441,11 +510,12 @@ while (i < STEPS.length) {
441
510
  log(step.name + " verdict re-ask exhausted — marking failed for retry");
442
511
  await agent(
443
512
  "Record verdict failure.\n" +
444
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"upsertagentsession\", args:\n" +
445
- "{ \"id\": \"" + activeSessionId + "\", \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"status\": \"failed\", \"notes\": \"Worker report had no single unambiguous VERDICT: PASS/FAIL line (bounded re-ask exhausted)\" }.\n" +
446
- "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
447
- "{ \"task_id\": \"" + taskId + "\", \"type\": \"failed\", \"message\": \"" + step.name + " verdict line missing or ambiguous, re-ask exhausted — phase failed, dispatcher will retry\" }.",
448
- { key: "record-block-" + step.name, label: "Recording verdict failure", schema: { type: "object" } }
513
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
514
+ task_id: taskId,
515
+ session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: "failed", notes: "Worker report had no single unambiguous VERDICT: PASS/FAIL line (bounded re-ask exhausted)" },
516
+ event: { task_id: taskId, type: "failed", message: step.name + " verdict line missing or ambiguous, re-ask exhausted — phase failed, dispatcher will retry" }
517
+ }) + "\n",
518
+ { key: "record-block-" + step.name, label: "Recording verdict failure" }
449
519
  );
450
520
  return {
451
521
  __hatchWorkflowControl: "blocked",
@@ -475,14 +545,14 @@ while (i < STEPS.length) {
475
545
 
476
546
  await agent(
477
547
  "Update session and log event.\n" +
478
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"upsertagentsession\", args:\n" +
479
- "{ \"id\": \"" + activeSessionId + "\", \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"status\": \"" + status + "\", \"notes\": " + JSON.stringify(summary) + " }.\n" +
480
- "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
481
- "{ \"task_id\": \"" + taskId + "\", \"type\": \"" + status + "\", \"identity\": \"" + step.identity + "\", \"message\": \"" + step.name + " " + status + " by " + step.identity + "\" }.",
548
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
549
+ task_id: taskId,
550
+ session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: status, notes: summary },
551
+ event: { task_id: taskId, type: status, identity: step.identity, message: step.name + " " + status + " by " + step.identity }
552
+ }) + "\n",
482
553
  {
483
554
  key: "record-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : ""),
484
- label: "Recording " + step.name + " result",
485
- schema: { type: "object" }
555
+ label: "Recording " + step.name + " result"
486
556
  }
487
557
  );
488
558
 
@@ -504,10 +574,9 @@ while (i < STEPS.length) {
504
574
 
505
575
  await agent(
506
576
  "Mark this task as done.\n" +
507
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"done\" }.\n" +
508
- "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"logevent\", args:\n" +
509
- "{ \"task_id\": \"" + taskId + "\", \"type\": \"completed\", \"message\": \"All docs workflow steps complete.\" }.",
510
- { key: "task-done", label: "Completing task: " + taskTitle, schema: { type: "object" } }
577
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("update-task", { id: taskId, state: "done" }) + "\n" +
578
+ "Then run in shell and return the stdout verbatim:\n" + crewCmd("log-event", { task_id: taskId, type: "completed", message: "All docs workflow steps complete." }),
579
+ { key: "task-done", label: "Completing task: " + taskTitle }
511
580
  );
512
581
 
513
582
  log("Docs workflow complete for task " + taskId);