muse-crew 0.4.7 → 0.6.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.
- package/docs/guide.md +9 -12
- package/package.json +1 -1
- package/seed/AGENTS.md +2 -0
- package/seed/cron-sweep-body-template.md +11 -0
- package/seed/crons.json +31 -0
- package/workflows/crew-init.js +73 -91
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
|
-
-
|
|
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
|
-
- `
|
|
60
|
-
- `sweepCronId` — orphan sweep cron identifier (default: `"crew-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
|
|
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. **
|
|
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
|
-
"
|
|
91
|
-
|
|
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
|
|
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
package/seed/AGENTS.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
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
|
|
7
|
+
- `cron-sweep-body-template.md` — template for creating the orphan-sweep cron job
|
|
6
8
|
- `posture.md` — PM posture that shapes the user's assistant for crew interaction
|
|
7
9
|
- `feedback/` — feedback feature placeholder
|
|
8
10
|
- `workflows/` — human-readable workflow definitions, copied to `$CREW_HOME/.orchestration/workflows/` during init
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## Muse Crew Orphan Sweep
|
|
2
|
+
|
|
3
|
+
1. Call artifact_invoke_action on slug "{dashboardSlug}", action "getdispatchstate", args {}.
|
|
4
|
+
2. From the response, extract the `id` field of each `ready_tasks` entry whose
|
|
5
|
+
latest_session.status is "running", verbatim — copy the full ID exactly
|
|
6
|
+
as it appears; do not shorten, truncate, or reformat it. Return them as a
|
|
7
|
+
JSON array of strings. Do NOT reproduce any other part of the response.
|
|
8
|
+
(Sessions older than 1h are already reclassified as timed_out by the dashboard — no extra math.)
|
|
9
|
+
3. Run: CREW_ACTIVE_TASKS="<space-separated ids>" {crewHome}/lib/orphan-sweep.sh clean
|
|
10
|
+
If the list is empty, pass an empty string — do NOT omit the variable (the sweep fails closed without it).
|
|
11
|
+
4. Report the output. If it says CLEAN, no action was needed.
|
package/seed/crons.json
ADDED
|
@@ -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
|
+
}
|
package/workflows/crew-init.js
CHANGED
|
@@ -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
|
|
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: "
|
|
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
|
|
20
|
-
const sweepCronId = inputs.sweepCronId || "crew-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,103 +143,89 @@ try {
|
|
|
145
143
|
}
|
|
146
144
|
log("Project: " + (projectResult.registered ? "registered" : "failed"));
|
|
147
145
|
|
|
148
|
-
// ── Phase 4:
|
|
149
|
-
// The
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
"
|
|
157
|
-
"
|
|
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
|
|
158
|
+
"Dashboard slug: " + dashboardSlug + "\n" +
|
|
159
|
+
"Id overrides (manifest id -> live id): " + JSON.stringify(cronIds) + "\n\n" +
|
|
161
160
|
"Steps:\n" +
|
|
162
|
-
"1.
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
" -
|
|
174
|
-
" -
|
|
175
|
-
" -
|
|
176
|
-
" -
|
|
177
|
-
"
|
|
178
|
-
"
|
|
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: "
|
|
181
|
-
label: "Create
|
|
192
|
+
key: "crons-1",
|
|
193
|
+
label: "Create and converge cron jobs",
|
|
182
194
|
schema: {
|
|
183
195
|
type: "object",
|
|
184
196
|
properties: {
|
|
185
|
-
|
|
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: ["
|
|
217
|
+
required: ["passed", "summary"]
|
|
188
218
|
}
|
|
189
219
|
}
|
|
190
220
|
);
|
|
191
221
|
} catch (e) {
|
|
192
|
-
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Cron
|
|
222
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Cron setup failed", message: String(e.message || e) } };
|
|
193
223
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// ── Phase 5: Sweep cron ──────────────────────────────────────────────
|
|
197
|
-
phase("sweep-cron");
|
|
198
|
-
var sweepResult;
|
|
199
|
-
try {
|
|
200
|
-
sweepResult = await agent(
|
|
201
|
-
"Check if the sweep cron exists and create it if not.\n\n" +
|
|
202
|
-
"Cron id: " + sweepCronId + "\n" +
|
|
203
|
-
"crewHome: " + crewHome + "\n" +
|
|
204
|
-
"Dashboard slug: " + dashboardSlug + "\n\n" +
|
|
205
|
-
"Steps:\n" +
|
|
206
|
-
"1. Call cron_list and look for a job with id '" + sweepCronId + "'\n" +
|
|
207
|
-
"2. If it exists, return { existed: true }\n" +
|
|
208
|
-
"3. If not found, call cron_add with:\n" +
|
|
209
|
-
" - id: '" + sweepCronId + "'\n" +
|
|
210
|
-
" - title: 'Muse Crew orphan sweep'\n" +
|
|
211
|
-
" - enabled: true\n" +
|
|
212
|
-
" - mode: 'task'\n" +
|
|
213
|
-
" - schedule: { kind: 'interval', every: '30m' }\n" +
|
|
214
|
-
" - owner: 'space:" + dashboardSlug + "'\n" +
|
|
215
|
-
" - timeout_secs: 120\n" +
|
|
216
|
-
" - body: the body text below\n" +
|
|
217
|
-
" Return { existed: false }\n\n" +
|
|
218
|
-
"Body text for the cron:\n" +
|
|
219
|
-
"---\n" +
|
|
220
|
-
"## Muse Crew Orphan Sweep\n\n" +
|
|
221
|
-
"1. Call artifact_invoke_action on slug \"" + dashboardSlug + "\", action \"getdispatchstate\", args {}.\n" +
|
|
222
|
-
"2. From the response, extract the `id` field of each `ready_tasks` entry whose\n" +
|
|
223
|
-
" latest_session.status is \"running\", verbatim — copy the full ID exactly\n" +
|
|
224
|
-
" as it appears; do not shorten, truncate, or reformat it. Return them as a\n" +
|
|
225
|
-
" JSON array of strings. Do NOT reproduce any other part of the response.\n" +
|
|
226
|
-
"3. Run: CREW_ACTIVE_TASKS=\"<space-separated ids>\" " + crewHome + "/lib/orphan-sweep.sh clean\n" +
|
|
227
|
-
" If the list is empty, pass an empty string — do NOT omit the variable (the sweep fails closed without it).\n" +
|
|
228
|
-
"4. Report the output. If it says CLEAN, no action was needed.\n" +
|
|
229
|
-
"---\n\n" +
|
|
230
|
-
"Return JSON with existed (boolean).",
|
|
231
|
-
{
|
|
232
|
-
key: "sweep-cron-1",
|
|
233
|
-
label: "Create sweep cron",
|
|
234
|
-
schema: {
|
|
235
|
-
type: "object",
|
|
236
|
-
properties: { existed: { type: "boolean" } },
|
|
237
|
-
required: ["existed"]
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
);
|
|
241
|
-
} catch (e) {
|
|
242
|
-
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" } };
|
|
243
226
|
}
|
|
244
|
-
log("
|
|
227
|
+
log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" + c.action; }).join(", "));
|
|
228
|
+
|
|
245
229
|
|
|
246
230
|
// ── Summary ───────────────────────────────────────────────────────────
|
|
247
231
|
return {
|
|
@@ -249,10 +233,8 @@ return {
|
|
|
249
233
|
crewHome: crewHome,
|
|
250
234
|
dashboardSlug: dashboardSlug,
|
|
251
235
|
dashboardName: dashboardName,
|
|
252
|
-
cronId: cronId,
|
|
253
236
|
releaseHash: releaseResult.hash,
|
|
254
237
|
scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
|
|
255
238
|
project: projectResult.registered ? "registered" : "failed",
|
|
256
|
-
|
|
257
|
-
sweepCron: sweepResult.existed ? "existed" : "created"
|
|
239
|
+
crons: cronsResult.summary.crons
|
|
258
240
|
};
|