muse-crew 0.5.0 → 0.6.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
@@ -258,7 +258,7 @@ Set a global configuration value.
258
258
 
259
259
  ### `setprovenance`
260
260
 
261
- Stamp publication provenance. Called by the crew Publish phase after rebuilding the artifact.
261
+ Stamp publication provenance. Called by the crew Publish phase after rebuilding the artifact, and after installing/activating a crew release (npm path): in the release case Publish refreshes `crew_release` to the live release identity while preserving the existing `source_commit`.
262
262
 
263
263
  | Field | Type | Required | Notes |
264
264
  |-------|------|----------|-------|
package/docs/guide.md CHANGED
@@ -17,8 +17,7 @@ This is the full setup and operations reference. If you're new, start with the [
17
17
  - A **release** — the first immutable snapshot of the crew's runtime code.
18
18
  - An **`.orchestration/` directory** in the crew home with identities, personas, workflow docs, and feedback conventions.
19
19
  - A **project registration** — the task service registered as its own first project.
20
- - A **polling loop** — checks for work every 3 minutes via Muse's scheduling, even when nobody's in the conversation.
21
- - An **orphan sweep** — runs every 30 minutes to clean merged worktrees and break stale merge locks. It never touches worktrees belonging to tasks with a running dashboard session.
20
+ - **Cron jobs** — the scheduler state declared in `seed/crons.json`: a polling loop (every 3 minutes via Muse's scheduling, even when nobody's in the conversation) and an orphan sweep (every 30 minutes, cleaning merged worktrees and breaking stale merge locks; it never touches worktrees belonging to tasks with a running dashboard session). Owner is `space:<slug>`, so deleting the task service also removes the crons.
22
21
 
23
22
  The agent running in the main chat receives the dispatcher's claims and launches each task workflow. Workflows can't launch workflows, so this handoff is structural.
24
23
 
@@ -56,12 +55,11 @@ Required arguments:
56
55
 
57
56
  Optional arguments:
58
57
  - `dashboardName` — display name for the project registration (default: `"Muse Crew"`).
59
- - `cronId` — polling cron identifier (default: `"crew-poll"`).
60
- - `sweepCronId` — orphan sweep cron identifier (default: `"crew-orphan-sweep"`).
58
+ - `cronIds` — manifest id live id map for parallel instances on one account (default: `{}`). The manifest's ids are used as-is unless overridden, e.g. `{"crew-poll": "crew-poll-canary", "crew-orphan-sweep": "crew-sweep-canary"}`.
61
59
 
62
60
  ### What init creates
63
61
 
64
- Init runs five phases, each idempotent — re-running skips anything that already exists:
62
+ Init runs four phases, each idempotent — re-running converges anything that drifted and skips what already matches:
65
63
 
66
64
  1. **Release** — copies `crew-release.sh` to `crewHome`, runs `init` to create the first release from the source directory. From a Git checkout, it extracts `workflows/` and `lib/` via `git archive` and uses the commit hash as the release identity. From an npm install, it copies the directories directly and uses the package version (e.g., `pkg-0.1.0`). Creates a release directory, sets the `current` symlink.
67
65
 
@@ -69,9 +67,7 @@ Init runs five phases, each idempotent — re-running skips anything that alread
69
67
 
70
68
  3. **Project registration** — registers the task service as a project in its own database via `createproject`. The dashboard becomes its own first project, so the crew can work on the dashboard itself.
71
69
 
72
- 4. **Polling cron** — creates a 3-minute interval cron from the template at `seed/cron-body-template.md`, with `crewHome` and the dashboard slug substituted in. Owner is `space:<slug>`, so deleting the task service also removes the cron.
73
-
74
- 5. **Sweep cron** — creates a 30-minute interval cron that runs the orphan sweep to clean merged worktrees and break stale merge locks. Owner is `space:<slug>`.
70
+ 4. **Crons** — reads the manifest at `seed/crons.json` and makes the scheduler match it: missing jobs are created from the entry (title, enabled, mode, schedule, owner, timeout_secs, and the body built from the entry's template with `crewHome` and the dashboard slug substituted in); existing jobs are viewed and converged — drifted fields are updated, unchanged jobs are left alone. One exception: init never touches `enabled` on an existing job. `enabled` is a creation-time default only — a disabled job is a deliberate human decision (the orphan sweep ships disabled as a holding state), and re-init must not silently resurrect it. Jobs removed from the manifest are left alone; deletion is a human decision.
75
71
 
76
72
  ### Verification
77
73
 
@@ -83,16 +79,17 @@ After init completes, it returns a summary:
83
79
  "crewHome": "~/workspace/.jarvis",
84
80
  "dashboardSlug": "orchestra-dashboard",
85
81
  "dashboardName": "Muse Crew",
86
- "cronId": "crew-poll",
87
82
  "releaseHash": "<git commit hash>",
88
83
  "scaffold": { "created": 23, "skipped": 0 },
89
84
  "project": "registered",
90
- "cron": "created",
91
- "sweepCron": "created"
85
+ "crons": [
86
+ { "id": "crew-poll", "action": "created", "updated_fields": [] },
87
+ { "id": "crew-orphan-sweep", "action": "created", "updated_fields": [] }
88
+ ]
92
89
  }
93
90
  ```
94
91
 
95
- Re-running init against an existing setup returns all `"existed"` / `"skipped"` results with no mutations.
92
+ Re-running init against an existing setup returns `"unchanged"` (or `"updated"` with the converged field names) for each cron, with no other mutations.
96
93
 
97
94
  ## Connecting an existing project
98
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Opinionated orchestration for Muse \u2014 workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
package/seed/AGENTS.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Init source data. Everything `crew-init.js` reads when setting up a new crew instance on a Muse account.
4
4
 
5
+ - `crons.json` — the declarative cron manifest; crew-init creates/updates each entry idempotently; `enabled` is a creation-time default only
5
6
  - `cron-body-template.md` — template for creating the dispatch cron job
6
7
  - `cron-sweep-body-template.md` — template for creating the orphan-sweep cron job
7
8
  - `posture.md` — PM posture that shapes the user's assistant for crew interaction
@@ -0,0 +1,31 @@
1
+ {
2
+ "crons": [
3
+ {
4
+ "body_template": "cron-body-template.md",
5
+ "enabled": true,
6
+ "id": "crew-poll",
7
+ "mode": "task",
8
+ "owner": "space:{dashboardSlug}",
9
+ "schedule": {
10
+ "every": "3m",
11
+ "kind": "interval"
12
+ },
13
+ "timeout_secs": 120,
14
+ "title": "Muse Crew polling loop"
15
+ },
16
+ {
17
+ "body_template": "cron-sweep-body-template.md",
18
+ "enabled": false,
19
+ "id": "crew-orphan-sweep",
20
+ "mode": "task",
21
+ "owner": "space:{dashboardSlug}",
22
+ "schedule": {
23
+ "every": "30m",
24
+ "kind": "interval"
25
+ },
26
+ "timeout_secs": 120,
27
+ "title": "Muse Crew orphan sweep"
28
+ }
29
+ ],
30
+ "version": 1
31
+ }
@@ -22,6 +22,14 @@ const taskTitle = inputs.task_title || "";
22
22
  const taskDescription = inputs.task_description || "";
23
23
  const startStepIndex = inputs.start_step_index || 0;
24
24
 
25
+ // Dispatcher-carried facts for the claim-time persist: the workflow name the
26
+ // dispatcher actually resolved and launched, and whether the task record had
27
+ // no workflow (the standard fallback). The launched workflow writes the
28
+ // resolution back via updatetask in the self-claim below.
29
+ const RESOLVED_WORKFLOW = inputs.resolved_workflow || null;
30
+ const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
31
+ const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"workflow\": \"" + RESOLVED_WORKFLOW + "\"" : "";
32
+
25
33
  // Config from args — backward-compatible fallbacks for manual launches
26
34
  const DASHBOARD_SLUG = inputs.dashboardSlug || "orchestra-dashboard";
27
35
  const crewHome = inputs.crewHome || "~/workspace/.jarvis";
@@ -660,7 +668,7 @@ while (i < STEPS.length) {
660
668
  if (isFirstClaim) {
661
669
  const claimResult = await agent(
662
670
  "Claim this task for the " + step.name + " step.\n" +
663
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\" }.\n" +
671
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\"" + CLAIM_WORKFLOW_PERSIST + " }.\n" +
664
672
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
665
673
  "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started\" }.\n" +
666
674
  "Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
@@ -1357,8 +1365,39 @@ while (i < STEPS.length) {
1357
1365
  return await parkTask("Publish verification failed: registry shows " + regVer + " but the release decision required " +
1358
1366
  publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land.");
1359
1367
  }
1360
- log("Publish verified for task " + taskId + ": registry at " + regVer);
1361
1368
  publishVerified = true;
1369
+ // Provenance refresh for self-publishes (task 7946d2a4): the npm path
1370
+ // installs and activates a new immutable release (crew-release.sh deploy
1371
+ // swaps the `current` symlink inside publish-npm.sh) but never stamped
1372
+ // the dashboard's provenance record — every crew release left
1373
+ // crew_release pointing at a pruned release. After a verified landed
1374
+ // publish, refresh the record's crew_release to the now-live release
1375
+ // identity, preserving the existing source_commit (the dashboard
1376
+ // artifact's build source — a crew-repo commit here would fail the
1377
+ // dashboard QA source check).
1378
+ try {
1379
+ var provRefresh = await agent(
1380
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getprovenance\", args: {}. " +
1381
+ "If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
1382
+ "Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
1383
+ "run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
1384
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"setprovenance\", args: " +
1385
+ "{ \"source_commit\": \"<existing provenance.source_commit>\", \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\", \"task_id\": \"" + taskId + "\" }. " +
1386
+ "Return JSON { \"refreshed\": <true if the setprovenance response contains ok: true, false otherwise>, \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\" } and nothing else.",
1387
+ { key: attemptKey("publish-provenance-refresh-" + taskId, totalReworkCount), label: "Refreshing dashboard provenance after crew release",
1388
+ schema: { type: "object", properties: { refreshed: { type: "boolean" }, reason: { type: "string" }, crew_release: { type: "string" }, published_at: { type: "string" } }, required: ["refreshed"] } }
1389
+ );
1390
+ if (provRefresh.refreshed) {
1391
+ log("Provenance refreshed for task " + taskId + ": crew_release " + provRefresh.crew_release);
1392
+ } else if (provRefresh.reason === "no-record") {
1393
+ log("Provenance refresh skipped for task " + taskId + ": no existing record to refresh (fresh instance — the dashboard's first artifact publish will create it)");
1394
+ } else {
1395
+ return await parkTask("Provenance refresh failed after a verified npm publish (crew_release " + (provRefresh.crew_release || "unknown") + "). The release is live but the dashboard record is stale — fail-closed.");
1396
+ }
1397
+ } catch (e) {
1398
+ return await parkTask("Provenance refresh failed: could not refresh dashboard provenance after a verified npm publish (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1399
+ }
1400
+
1362
1401
  } catch (e) {
1363
1402
  return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1364
1403
  }
@@ -20,6 +20,14 @@ const taskTitle = inputs.task_title || "";
20
20
  const taskDescription = inputs.task_description || "";
21
21
  const startStepIndex = inputs.start_step_index || 0;
22
22
 
23
+ // Dispatcher-carried facts for the claim-time persist: the workflow name the
24
+ // dispatcher actually resolved and launched, and whether the task record had
25
+ // no workflow (the standard fallback). The launched workflow writes the
26
+ // resolution back via updatetask in the self-claim below.
27
+ const RESOLVED_WORKFLOW = inputs.resolved_workflow || null;
28
+ const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
29
+ const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"workflow\": \"" + RESOLVED_WORKFLOW + "\"" : "";
30
+
23
31
  // Config from args — backward-compatible fallbacks for manual launches
24
32
  const DASHBOARD_SLUG = inputs.dashboardSlug || "orchestra-dashboard";
25
33
  const crewHome = inputs.crewHome || "~/workspace/.jarvis";
@@ -618,7 +626,7 @@ while (i < STEPS.length) {
618
626
  if (isFirstClaim) {
619
627
  const claimResult = await agent(
620
628
  "Claim this task for the " + step.name + " step.\n" +
621
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\" }.\n" +
629
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\"" + CLAIM_WORKFLOW_PERSIST + " }.\n" +
622
630
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
623
631
  "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started\" }.\n" +
624
632
  "Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
@@ -1232,8 +1240,39 @@ while (i < STEPS.length) {
1232
1240
  return await parkTask("Publish verification failed: registry shows " + regVer + " but the release decision required " +
1233
1241
  publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land.");
1234
1242
  }
1235
- log("Publish verified for task " + taskId + ": registry at " + regVer);
1236
1243
  publishVerified = true;
1244
+ // Provenance refresh for self-publishes (task 7946d2a4): the npm path
1245
+ // installs and activates a new immutable release (crew-release.sh deploy
1246
+ // swaps the `current` symlink inside publish-npm.sh) but never stamped
1247
+ // the dashboard's provenance record — every crew release left
1248
+ // crew_release pointing at a pruned release. After a verified landed
1249
+ // publish, refresh the record's crew_release to the now-live release
1250
+ // identity, preserving the existing source_commit (the dashboard
1251
+ // artifact's build source — a crew-repo commit here would fail the
1252
+ // dashboard QA source check).
1253
+ try {
1254
+ var provRefresh = await agent(
1255
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getprovenance\", args: {}. " +
1256
+ "If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
1257
+ "Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
1258
+ "run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
1259
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"setprovenance\", args: " +
1260
+ "{ \"source_commit\": \"<existing provenance.source_commit>\", \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\", \"task_id\": \"" + taskId + "\" }. " +
1261
+ "Return JSON { \"refreshed\": <true if the setprovenance response contains ok: true, false otherwise>, \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\" } and nothing else.",
1262
+ { key: attemptKey("publish-provenance-refresh-" + taskId, reworkCount), label: "Refreshing dashboard provenance after crew release",
1263
+ schema: { type: "object", properties: { refreshed: { type: "boolean" }, reason: { type: "string" }, crew_release: { type: "string" }, published_at: { type: "string" } }, required: ["refreshed"] } }
1264
+ );
1265
+ if (provRefresh.refreshed) {
1266
+ log("Provenance refreshed for task " + taskId + ": crew_release " + provRefresh.crew_release);
1267
+ } else if (provRefresh.reason === "no-record") {
1268
+ log("Provenance refresh skipped for task " + taskId + ": no existing record to refresh (fresh instance — the dashboard's first artifact publish will create it)");
1269
+ } else {
1270
+ return await parkTask("Provenance refresh failed after a verified npm publish (crew_release " + (provRefresh.crew_release || "unknown") + "). The release is live but the dashboard record is stale — fail-closed.");
1271
+ }
1272
+ } catch (e) {
1273
+ return await parkTask("Provenance refresh failed: could not refresh dashboard provenance after a verified npm publish (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1274
+ }
1275
+
1237
1276
  } catch (e) {
1238
1277
  return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1239
1278
  }
@@ -593,7 +593,13 @@ for (var p = 0; p < toProcess.length; p++) {
593
593
  project_config: projectCfg,
594
594
  project_id: taskProject,
595
595
  dashboardSlug: DASHBOARD_SLUG,
596
- crewHome: crewHome
596
+ crewHome: crewHome,
597
+ // Facts the launched workflow needs to persist the resolution at claim time:
598
+ // the workflow name the dispatcher actually resolved and is launching, and
599
+ // whether the task record had no workflow (playtest filings carry explicit
600
+ // workflow:'standard', so they correctly get false).
601
+ resolved_workflow: iworkflow,
602
+ workflow_was_null: (itask.workflow == null)
597
603
  };
598
604
 
599
605
  log("Recommended " + iworkflow + " for \"" + itask.title + "\" [" + taskProject + "] at step " + nextStepName);
@@ -1,12 +1,11 @@
1
1
  export const meta = {
2
2
  name: "crew-init",
3
- description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, register dashboard project, set up polling cron. Idempotent — safe to re-run.",
3
+ description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, register dashboard project, set up cron jobs. Idempotent — safe to re-run.",
4
4
  phases: [
5
5
  { name: "release", title: "Bootstrap release system" },
6
6
  { name: "scaffold", title: "Scaffold orchestration directory" },
7
7
  { name: "project", title: "Register dashboard project" },
8
- { name: "cron", title: "Create polling cron" },
9
- { name: "sweep-cron", title: "Create sweep cron" }
8
+ { name: "crons", title: "Create and converge cron jobs" }
10
9
  ]
11
10
  };
12
11
 
@@ -16,8 +15,7 @@ const crewRepoPath = inputs.crewRepoPath;
16
15
  const crewHome = inputs.crewHome;
17
16
  const dashboardSlug = inputs.dashboardSlug;
18
17
  const dashboardName = inputs.dashboardName || "Muse Crew";
19
- const cronId = inputs.cronId || "crew-poll";
20
- const sweepCronId = inputs.sweepCronId || "crew-orphan-sweep";
18
+ const cronIds = inputs.cronIds || {};
21
19
 
22
20
  if (!crewRepoPath) throw new Error("crewRepoPath is required — path to muse-crew (git checkout or npm install)");
23
21
  if (!crewHome) throw new Error("crewHome is required — e.g. ~/.crew");
@@ -145,98 +143,89 @@ try {
145
143
  }
146
144
  log("Project: " + (projectResult.registered ? "registered" : "failed"));
147
145
 
148
- // ── Phase 4: Polling cron ─────────────────────────────────────────────
149
- // The cron body template lives in the repo at seed/cron-body-template.md.
150
- // Placeholders: {crewHome}, {dashboardSlug} are replaced with actual values.
151
- // Owner is the dashboard artifact so deleting the dashboard kills the cron.
152
- phase("cron");
153
- var cronResult;
146
+ // ── Phase 4: Cron jobs (declarative manifest) ──────────────────────────
147
+ // The manifest lives in the repo at seed/crons.json. Body templates live
148
+ // in seed/ with {crewHome}/{dashboardSlug} placeholders. Missing jobs are
149
+ // created from the manifest; existing jobs converge to it except `enabled`,
150
+ // which is a creation-time default only and is never touched on update.
151
+ phase("crons");
152
+ var cronsResult;
154
153
  try {
155
- cronResult = await agent(
156
- "Check if the polling cron exists and create it if not.\n\n" +
157
- "Cron id: " + cronId + "\n" +
158
- "Template: " + crewRepoPath + "/seed/cron-body-template.md\n" +
154
+ cronsResult = await agent(
155
+ "Ensure the Muse Crew cron jobs match the repo's declarative manifest.\n\n" +
156
+ "Manifest: " + crewRepoPath + "/seed/crons.json\n" +
159
157
  "crewHome: " + crewHome + "\n" +
160
- "Dashboard slug: " + dashboardSlug + "\n\n" +
158
+ "Dashboard slug: " + dashboardSlug + "\n" +
159
+ "Id overrides (manifest id -> live id): " + JSON.stringify(cronIds) + "\n\n" +
161
160
  "Steps:\n" +
162
- "1. Call cron_list and look for a job with id '" + cronId + "'\n" +
163
- "2. If it exists, return { existed: true }\n" +
164
- "3. If not found:\n" +
165
- " a. Read the template at " + crewRepoPath + "/seed/cron-body-template.md\n" +
166
- " b. Replace all occurrences of {crewHome} with: " + crewHome + "\n" +
167
- " Replace all occurrences of {dashboardSlug} with: " + dashboardSlug + "\n" +
168
- " c. Call cron_add with:\n" +
169
- " - id: '" + cronId + "'\n" +
170
- " - title: 'Muse Crew polling loop'\n" +
171
- " - enabled: true\n" +
172
- " - mode: 'task'\n" +
173
- " - schedule: { kind: 'interval', every: '3m' }\n" +
174
- " - owner: 'space:" + dashboardSlug + "'\n" +
175
- " - timeout_secs: 120\n" +
176
- " - body: the processed template text\n" +
177
- " d. Return { existed: false }\n\n" +
178
- "Return JSON with existed (boolean).",
161
+ "1. Read the manifest at " + crewRepoPath + "/seed/crons.json and parse it as JSON.\n" +
162
+ " Fail closed (return passed: false) if the file is missing, is not valid JSON,\n" +
163
+ " or any entry lacks a required field (id, title, enabled, mode, schedule,\n" +
164
+ " owner, body_template).\n" +
165
+ "2. For each manifest entry, in order:\n" +
166
+ " a. The live id is the override for entry.id when present, else entry.id.\n" +
167
+ " b. Read the body template at " + crewRepoPath + "/seed/<entry.body_template>.\n" +
168
+ " c. Replace all occurrences of {crewHome} with: " + crewHome + " and all\n" +
169
+ " occurrences of {dashboardSlug} with: " + dashboardSlug + ".\n" +
170
+ " d. Call cron_list and look for a job with the live id.\n" +
171
+ " e. If missing, call cron_add with:\n" +
172
+ " - id: the live id\n" +
173
+ " - title, enabled, mode from the entry\n" +
174
+ " - schedule: the entry's schedule object\n" +
175
+ " - owner: the entry's owner with {dashboardSlug} replaced by " + dashboardSlug + "\n" +
176
+ " - timeout_secs: the entry's timeout_secs when present\n" +
177
+ " - body: the resolved template text\n" +
178
+ " Record action 'created' with no updated fields.\n" +
179
+ " f. If it exists, call cron_view and compare against the manifest-derived\n" +
180
+ " definition (title, mode, schedule, owner, timeout_secs, body).\n" +
181
+ " Never compare or pass enabled: a live job's enabled state is a human\n" +
182
+ " decision; re-init must not silently enable or disable jobs.\n" +
183
+ " Compare schedule only on the keys the manifest declares; ignore\n" +
184
+ " runtime-owned fields (timezone, at).\n" +
185
+ " Call cron_update with only the fields that differ. If nothing differs,\n" +
186
+ " do not call cron_update. Record action 'updated' with the updated field\n" +
187
+ " names, or 'unchanged' with no updated fields.\n" +
188
+ "3. Return { passed: true, summary: { crons: [...] } } with one entry per\n" +
189
+ " manifest entry, in manifest order: { id, action: 'created' | 'updated' |\n" +
190
+ " 'unchanged', updated_fields: [...] }.",
179
191
  {
180
- key: "cron-1",
181
- label: "Create polling cron",
192
+ key: "crons-1",
193
+ label: "Create and converge cron jobs",
182
194
  schema: {
183
195
  type: "object",
184
196
  properties: {
185
- existed: { type: "boolean" }
197
+ passed: { type: "boolean" },
198
+ summary: {
199
+ type: "object",
200
+ properties: {
201
+ crons: {
202
+ type: "array",
203
+ items: {
204
+ type: "object",
205
+ properties: {
206
+ id: { type: "string" },
207
+ action: { type: "string", enum: ["created", "updated", "unchanged"] },
208
+ updated_fields: { type: "array", items: { type: "string" } }
209
+ },
210
+ required: ["id", "action", "updated_fields"]
211
+ }
212
+ }
213
+ },
214
+ required: ["crons"]
215
+ }
186
216
  },
187
- required: ["existed"]
217
+ required: ["passed", "summary"]
188
218
  }
189
219
  }
190
220
  );
191
221
  } catch (e) {
192
- return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Cron creation failed", message: String(e.message || e) } };
222
+ return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Cron setup failed", message: String(e.message || e) } };
193
223
  }
194
- log("Cron: " + (cronResult.existed ? "already existed" : "created as " + cronId));
195
-
196
- // ── Phase 5: Sweep cron ──────────────────────────────────────────────
197
- phase("sweep-cron");
198
- var sweepResult;
199
- try {
200
- sweepResult = await agent(
201
- "Check if the orphan sweep cron exists and create it if not.\n\n" +
202
- "Cron id: " + sweepCronId + "\n" +
203
- "Template: " + crewRepoPath + "/seed/cron-sweep-body-template.md\n" +
204
- "crewHome: " + crewHome + "\n" +
205
- "Dashboard slug: " + dashboardSlug + "\n\n" +
206
- "Steps:\n" +
207
- "1. Call cron_list and look for a job with id '" + sweepCronId + "'\n" +
208
- "2. If it exists, return { existed: true } WITHOUT touching it — adopt means\n" +
209
- " adopt: do not enable it, rewrite its body, or change its schedule.\n" +
210
- "3. If not found:\n" +
211
- " a. Read the template at " + crewRepoPath + "/seed/cron-sweep-body-template.md\n" +
212
- " b. Replace all occurrences of {crewHome} with: " + crewHome + "\n" +
213
- " Replace all occurrences of {dashboardSlug} with: " + dashboardSlug + "\n" +
214
- " c. Call cron_add with:\n" +
215
- " - id: '" + sweepCronId + "'\n" +
216
- " - title: 'Muse Crew orphan sweep'\n" +
217
- " - enabled: false (deliberate holding state — the sweep stays disabled\n" +
218
- " until explicitly enabled by a human; never resurrect it enabled)\n" +
219
- " - mode: 'task'\n" +
220
- " - schedule: { kind: 'interval', every: '30m' }\n" +
221
- " - owner: 'space:" + dashboardSlug + "'\n" +
222
- " - timeout_secs: 120\n" +
223
- " - body: the processed template text\n" +
224
- " d. Return { existed: false }\n\n" +
225
- "Return JSON with existed (boolean).",
226
- {
227
- key: "sweep-cron-1",
228
- label: "Create sweep cron",
229
- schema: {
230
- type: "object",
231
- properties: { existed: { type: "boolean" } },
232
- required: ["existed"]
233
- }
234
- }
235
- );
236
- } catch (e) {
237
- return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Sweep cron creation failed", message: String(e.message || e) } };
224
+ if (!cronsResult.passed || !cronsResult.summary || !Array.isArray(cronsResult.summary.crons)) {
225
+ return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Cron manifest invalid", message: "seed/crons.json failed validation or produced no crons list" } };
238
226
  }
239
- log("Sweep cron: " + (sweepResult.existed ? "already existed" : "created as " + sweepCronId));
227
+ log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" + c.action; }).join(", "));
228
+
240
229
 
241
230
  // ── Summary ───────────────────────────────────────────────────────────
242
231
  return {
@@ -244,10 +233,8 @@ return {
244
233
  crewHome: crewHome,
245
234
  dashboardSlug: dashboardSlug,
246
235
  dashboardName: dashboardName,
247
- cronId: cronId,
248
236
  releaseHash: releaseResult.hash,
249
237
  scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
250
238
  project: projectResult.registered ? "registered" : "failed",
251
- cron: cronResult.existed ? "existed" : "created",
252
- sweepCron: sweepResult.existed ? "existed" : "created"
239
+ crons: cronsResult.summary.crons
253
240
  };
package/workflows/docs.js CHANGED
@@ -16,6 +16,14 @@ const taskTitle = inputs.task_title || "";
16
16
  const taskDescription = inputs.task_description || "";
17
17
  const startStepIndex = inputs.start_step_index || 0;
18
18
 
19
+ // Dispatcher-carried facts for the claim-time persist: the workflow name the
20
+ // dispatcher actually resolved and launched, and whether the task record had
21
+ // no workflow (the standard fallback). The launched workflow writes the
22
+ // resolution back via updatetask in the self-claim below.
23
+ const RESOLVED_WORKFLOW = inputs.resolved_workflow || null;
24
+ const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
25
+ const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"workflow\": \"" + RESOLVED_WORKFLOW + "\"" : "";
26
+
19
27
  // Config from args — backward-compatible fallbacks for manual launches
20
28
  const DASHBOARD_SLUG = inputs.dashboardSlug || "orchestra-dashboard";
21
29
  const crewHome = inputs.crewHome || "~/workspace/.jarvis";
@@ -125,7 +133,7 @@ while (i < STEPS.length) {
125
133
  if (isFirstClaim) {
126
134
  const claimResult = await agent(
127
135
  "Claim this task for the " + step.name + " step.\n" +
128
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\" }.\n" +
136
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\"" + CLAIM_WORKFLOW_PERSIST + " }.\n" +
129
137
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
130
138
  "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started\" }.\n" +
131
139
  "Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
@@ -21,6 +21,14 @@ const taskTitle = inputs.task_title || "";
21
21
  const taskDescription = inputs.task_description || "";
22
22
  const startStepIndex = inputs.start_step_index || 0;
23
23
 
24
+ // Dispatcher-carried facts for the claim-time persist: the workflow name the
25
+ // dispatcher actually resolved and launched, and whether the task record had
26
+ // no workflow (the standard fallback). The launched workflow writes the
27
+ // resolution back via updatetask in the self-claim below.
28
+ const RESOLVED_WORKFLOW = inputs.resolved_workflow || null;
29
+ const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
30
+ const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"workflow\": \"" + RESOLVED_WORKFLOW + "\"" : "";
31
+
24
32
  // Config from args — backward-compatible fallbacks for manual launches
25
33
  const DASHBOARD_SLUG = inputs.dashboardSlug || "orchestra-dashboard";
26
34
  const crewHome = inputs.crewHome || "~/workspace/.jarvis";
@@ -659,7 +667,7 @@ while (i < STEPS.length) {
659
667
  if (isFirstClaim) {
660
668
  const claimResult = await agent(
661
669
  "Claim this task for the " + step.name + " step.\n" +
662
- "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\" }.\n" +
670
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"updatetask\", args: { \"id\": \"" + taskId + "\", \"state\": \"in_progress\"" + CLAIM_WORKFLOW_PERSIST + " }.\n" +
663
671
  "Then call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"claimtask\", args:\n" +
664
672
  "{ \"task_id\": \"" + taskId + "\", \"identity\": \"" + step.identity + "\", \"step\": \"" + step.name + "\", \"notes\": \"" + step.name + " step started\" }.\n" +
665
673
  "Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
@@ -1367,8 +1375,39 @@ while (i < STEPS.length) {
1367
1375
  return await parkTask("Publish verification failed: registry shows " + regVer + " but the release decision required " +
1368
1376
  publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land.");
1369
1377
  }
1370
- log("Publish verified for task " + taskId + ": registry at " + regVer);
1371
1378
  publishVerified = true;
1379
+ // Provenance refresh for self-publishes (task 7946d2a4): the npm path
1380
+ // installs and activates a new immutable release (crew-release.sh deploy
1381
+ // swaps the `current` symlink inside publish-npm.sh) but never stamped
1382
+ // the dashboard's provenance record — every crew release left
1383
+ // crew_release pointing at a pruned release. After a verified landed
1384
+ // publish, refresh the record's crew_release to the now-live release
1385
+ // identity, preserving the existing source_commit (the dashboard
1386
+ // artifact's build source — a crew-repo commit here would fail the
1387
+ // dashboard QA source check).
1388
+ try {
1389
+ var provRefresh = await agent(
1390
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getprovenance\", args: {}. " +
1391
+ "If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
1392
+ "Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
1393
+ "run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
1394
+ "Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"setprovenance\", args: " +
1395
+ "{ \"source_commit\": \"<existing provenance.source_commit>\", \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\", \"task_id\": \"" + taskId + "\" }. " +
1396
+ "Return JSON { \"refreshed\": <true if the setprovenance response contains ok: true, false otherwise>, \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\" } and nothing else.",
1397
+ { key: attemptKey("publish-provenance-refresh-" + taskId, totalReworkCount), label: "Refreshing dashboard provenance after crew release",
1398
+ schema: { type: "object", properties: { refreshed: { type: "boolean" }, reason: { type: "string" }, crew_release: { type: "string" }, published_at: { type: "string" } }, required: ["refreshed"] } }
1399
+ );
1400
+ if (provRefresh.refreshed) {
1401
+ log("Provenance refreshed for task " + taskId + ": crew_release " + provRefresh.crew_release);
1402
+ } else if (provRefresh.reason === "no-record") {
1403
+ log("Provenance refresh skipped for task " + taskId + ": no existing record to refresh (fresh instance — the dashboard's first artifact publish will create it)");
1404
+ } else {
1405
+ return await parkTask("Provenance refresh failed after a verified npm publish (crew_release " + (provRefresh.crew_release || "unknown") + "). The release is live but the dashboard record is stale — fail-closed.");
1406
+ }
1407
+ } catch (e) {
1408
+ return await parkTask("Provenance refresh failed: could not refresh dashboard provenance after a verified npm publish (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1409
+ }
1410
+
1372
1411
  } catch (e) {
1373
1412
  return await parkTask("Publish verification failed: could not read the registry version (" + (e && e.message ? e.message : e) + "). Fail-closed.");
1374
1413
  }