muse-crew 0.6.7 → 0.7.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/lib/crew-api.js +6 -0
- package/lib/test-version-write.sh +18 -1
- package/package.json +1 -1
- package/seed/cron-body-template.md +6 -7
- package/workflows/crew-init.js +68 -12
package/lib/crew-api.js
CHANGED
|
@@ -529,6 +529,12 @@ commands["claim-task"] = (db, args) => {
|
|
|
529
529
|
const task = requireTask(db, args.task_id);
|
|
530
530
|
const project = requireProject(db, task.project);
|
|
531
531
|
if (project.quiesced) throw conflict("This project is paused. Resume it before claiming tasks.");
|
|
532
|
+
// Mechanical guard: never claim a task that is already in a terminal state.
|
|
533
|
+
// The dispatcher may recommend a task based on stale state (e.g. a task that
|
|
534
|
+
// was marked done between the dispatcher's read and the workflow's claim).
|
|
535
|
+
// The claim must fail closed here, not launch a duplicate run.
|
|
536
|
+
if (task.state === "done") throw conflict("Task is already done and cannot be claimed.");
|
|
537
|
+
if (task.state === "cancelled") throw conflict("Task is cancelled and cannot be claimed.");
|
|
532
538
|
|
|
533
539
|
const row = {
|
|
534
540
|
id: uuid(), task_id: args.task_id, identity,
|
|
@@ -30,12 +30,29 @@ TMPBASE="$(mktemp -d)"
|
|
|
30
30
|
trap 'rm -rf "$TMPBASE"' EXIT
|
|
31
31
|
|
|
32
32
|
# --- Fixture: temp git repo seeded with the repo's current package.json ---
|
|
33
|
+
# The test injects a \u2014 escape into the description field so the fixture
|
|
34
|
+
# carries the escape this regression is about, regardless of whether the
|
|
35
|
+
# repo's own package.json still has one. We write the JSON manually to ensure
|
|
36
|
+
# the escape appears as literal backslash-u bytes, not the decoded character.
|
|
33
37
|
FIX="$TMPBASE/fixture"
|
|
34
38
|
mkdir -p "$FIX"
|
|
35
39
|
git init -q -b main "$FIX" >/dev/null
|
|
36
40
|
git -C "$FIX" config user.email "test@example.com"
|
|
37
41
|
git -C "$FIX" config user.name "test"
|
|
38
|
-
|
|
42
|
+
node -e '
|
|
43
|
+
const fs = require("fs");
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
45
|
+
pkg.version = pkg.version; // keep version from repo
|
|
46
|
+
const lines = JSON.stringify(pkg, null, 2).split("\n");
|
|
47
|
+
// Replace the description line with one carrying a literal \u2014 escape
|
|
48
|
+
const out = lines.map(function (l) {
|
|
49
|
+
if (l.match(/"description":/)) {
|
|
50
|
+
return " \"description\": \"Test description with em-dash escape \\u2014 here\",";
|
|
51
|
+
}
|
|
52
|
+
return l;
|
|
53
|
+
});
|
|
54
|
+
fs.writeFileSync(process.argv[2], out.join("\n") + "\n");
|
|
55
|
+
' "$REPO_DIR/package.json" "$FIX/package.json"
|
|
39
56
|
git -C "$FIX" add package.json
|
|
40
57
|
git -C "$FIX" commit -qm "initial"
|
|
41
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Muse Crew Dispatcher
|
|
2
2
|
|
|
3
|
-
You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher workflow and
|
|
3
|
+
You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher workflow and launch its claims autonomously.
|
|
4
4
|
|
|
5
5
|
### Steps
|
|
6
6
|
|
|
@@ -8,15 +8,14 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
|
|
|
8
8
|
|
|
9
9
|
2. **Load the workflow registry:** Read the file "{crewHome}/workflows/registry.json" with the read tool and parse it as JSON. If the file does not exist (the live release predates the registry), proceed without it — omit the `registry` arg and the dispatcher will load the registry the slow way and log a warning.
|
|
10
10
|
|
|
11
|
-
3. **Run the dispatcher:** Call workflow_launch with scriptPath "{crewHome}/workflows/crew-dispatch.js" and args {"
|
|
11
|
+
3. **Run the dispatcher:** Call workflow_launch with scriptPath "{crewHome}/workflows/crew-dispatch.js" and args {"crewHome": "{crewHome}", "registry": <parsed registry JSON, or omit the key when the file was missing>}.
|
|
12
12
|
|
|
13
|
-
Wait for it to complete. It reads the
|
|
13
|
+
Wait for it to complete. It reads the crew's task state, determines eligibility, claims tasks, acknowledges the poll, and returns structured results.
|
|
14
14
|
|
|
15
|
-
4. **
|
|
16
|
-
|
|
15
|
+
4. **Launch claims autonomously:** Extract the `claims` array from the dispatcher result. For each claim (up to 3 per tick — if more than 3, launch the first 3 and log the rest as deferred):
|
|
16
|
+
- Call workflow_launch_async with scriptPath={claim.scriptPath} and args={claim.args}.
|
|
17
|
+
- The launched workflow self-claims the task as its first action. If the task was already claimed or is done, the claim fails closed and the run stands down quietly — this is the mechanical duplicate protection, not an error.
|
|
17
18
|
|
|
18
19
|
If the dispatcher returned no claims or the claims array is empty, report: NO_DISPATCH
|
|
19
20
|
|
|
20
|
-
Do NOT call workflow_launch_async for any task workflow. The main chat agent does that.
|
|
21
|
-
|
|
22
21
|
5. Exit silently.
|
package/workflows/crew-init.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export const meta = {
|
|
2
2
|
name: "crew-init",
|
|
3
|
-
description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, set up cron jobs. 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
|
+
{ name: "project", title: "Register dashboard project" },
|
|
7
8
|
{ name: "crons", title: "Create and converge cron jobs" }
|
|
8
9
|
]
|
|
9
10
|
};
|
|
@@ -12,10 +13,13 @@ export const meta = {
|
|
|
12
13
|
const inputs = args ?? {};
|
|
13
14
|
const crewRepoPath = inputs.crewRepoPath;
|
|
14
15
|
const crewHome = inputs.crewHome;
|
|
16
|
+
const dashboardSlug = inputs.dashboardSlug;
|
|
17
|
+
const dashboardName = inputs.dashboardName || "Muse Crew";
|
|
15
18
|
const cronIds = inputs.cronIds || {};
|
|
16
19
|
|
|
17
20
|
if (!crewRepoPath) throw new Error("crewRepoPath is required — path to muse-crew (git checkout or npm install)");
|
|
18
21
|
if (!crewHome) throw new Error("crewHome is required — e.g. ~/workspace/.crew (must be inside the workspace)");
|
|
22
|
+
if (!dashboardSlug) throw new Error("dashboardSlug is required — slug of the task service artifact (must already exist)");
|
|
19
23
|
|
|
20
24
|
const orchDir = crewHome + "/.orchestration";
|
|
21
25
|
|
|
@@ -49,14 +53,17 @@ function gate0Decide(facts) {
|
|
|
49
53
|
var gateFacts;
|
|
50
54
|
try {
|
|
51
55
|
gateFacts = await agent(
|
|
52
|
-
"
|
|
56
|
+
"Ensure the requested crew home exists as a git repository. Report raw facts.\\n\\n" +
|
|
53
57
|
"Requested crewHome: " + crewHome + "\\n\\n" +
|
|
54
58
|
"Steps (run in shell):\\n" +
|
|
55
59
|
"1. home=$(echo $HOME)\\n" +
|
|
56
60
|
"2. Expand a leading ~/ in the requested crewHome against $HOME (leave other paths untouched).\\n" +
|
|
57
|
-
"3.
|
|
58
|
-
"4.
|
|
59
|
-
"
|
|
61
|
+
"3. If the expanded path does not exist: mkdir -p <expanded crewHome>\\n" +
|
|
62
|
+
"4. Run: git -C <expanded crewHome> rev-parse --git-dir; echo EXIT:$?\\n" +
|
|
63
|
+
"5. If EXIT is not 0: run git -C <expanded crewHome> init -b main (or git init if -b not supported)\\n" +
|
|
64
|
+
"6. Re-run: git -C <expanded crewHome> rev-parse --git-dir; echo EXIT:$?\\n" +
|
|
65
|
+
"7. Return JSON { home, crewHomeExpanded, gitExitCode } where gitExitCode is the FINAL exit code after init.\\n\\n" +
|
|
66
|
+
"This ensures the crew home is a valid git repository. Do not judge — just ensure and report.",
|
|
60
67
|
{
|
|
61
68
|
key: "gate-0",
|
|
62
69
|
label: "Validate crew home",
|
|
@@ -88,8 +95,8 @@ if (!gateResult.repoValid) {
|
|
|
88
95
|
return {
|
|
89
96
|
__hatchWorkflowControl: "blocked",
|
|
90
97
|
result: {
|
|
91
|
-
blocked_reason: "crewHome
|
|
92
|
-
message: "crewHome (" + gateFacts.crewHomeExpanded + ")
|
|
98
|
+
blocked_reason: "crewHome git init failed",
|
|
99
|
+
message: "crewHome (" + gateFacts.crewHomeExpanded + ") could not be initialized as a git repository (git exit code " + gateFacts.gitExitCode + "). Check permissions and try again."
|
|
93
100
|
}
|
|
94
101
|
};
|
|
95
102
|
}
|
|
@@ -106,11 +113,11 @@ try {
|
|
|
106
113
|
"Release script (in repo): " + crewRepoPath + "/lib/crew-release.sh\n\n" +
|
|
107
114
|
"Steps:\n" +
|
|
108
115
|
"1. Run: test -f " + crewHome + "/crew-release.sh && echo EXISTS || echo MISSING\n" +
|
|
109
|
-
"2. If EXISTS, run: " + crewHome + "/crew-release.sh current\n" +
|
|
116
|
+
"2. If EXISTS, run: " + crewHome + "/crew-release.sh current " + crewHome + "\n" +
|
|
110
117
|
" Return { existed: true, hash: <current hash> }\n" +
|
|
111
118
|
"3. If MISSING, bootstrap:\n" +
|
|
112
119
|
" a. Run: bash " + crewRepoPath + "/lib/crew-release.sh init " + crewHome + " " + crewRepoPath + "\n" +
|
|
113
|
-
" b. Verify: " + crewHome + "/crew-release.sh current\n" +
|
|
120
|
+
" b. Verify: " + crewHome + "/crew-release.sh current " + crewHome + "\n" +
|
|
114
121
|
" c. Return { existed: false, hash: <hash from current> }\n\n" +
|
|
115
122
|
"Return JSON with existed (boolean) and hash (string).",
|
|
116
123
|
{
|
|
@@ -176,7 +183,48 @@ var scaffoldCreated = scaffoldResult.created ? scaffoldResult.created.length : 0
|
|
|
176
183
|
var scaffoldSkipped = scaffoldResult.skipped ? scaffoldResult.skipped.length : 0;
|
|
177
184
|
log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped");
|
|
178
185
|
|
|
179
|
-
// ── Phase 3:
|
|
186
|
+
// ── Phase 3: Project registration ─────────────────────────────────────
|
|
187
|
+
// Registers the dashboard as the crew's first project in crew-state.db via
|
|
188
|
+
// the Crew API. The dashboard becomes its own first project, so the crew can
|
|
189
|
+
// work on the dashboard itself. Idempotent: if the project already exists,
|
|
190
|
+
// it is left alone.
|
|
191
|
+
phase("project");
|
|
192
|
+
var projectResult;
|
|
193
|
+
var safeDashboardName = dashboardName.split('"').join('\\"');
|
|
194
|
+
try {
|
|
195
|
+
projectResult = await agent(
|
|
196
|
+
"Register the dashboard as a project in the crew's task database.\n\n" +
|
|
197
|
+
"Crew home: " + crewHome + "\n" +
|
|
198
|
+
"Crew API: " + crewHome + "/current/lib/crew-api.js\n" +
|
|
199
|
+
"Dashboard slug (project id): " + dashboardSlug + "\n" +
|
|
200
|
+
"Dashboard name (display_name): " + dashboardName + "\n\n" +
|
|
201
|
+
"Steps:\n" +
|
|
202
|
+
"1. Check if the project already exists:\n" +
|
|
203
|
+
" node " + crewHome + "/current/lib/crew-api.js --crew-home " + crewHome + " get-project --json '{\"id\": \"" + dashboardSlug + "\"}'\n" +
|
|
204
|
+
" If it returns a project (ok: true), return { action: \"exists\", project_id: \"" + dashboardSlug + "\" }.\n" +
|
|
205
|
+
"2. If not found (error), create it:\n" +
|
|
206
|
+
" node " + crewHome + "/current/lib/crew-api.js --crew-home " + crewHome + " create-project --json '{\"id\": \"" + dashboardSlug + "\", \"display_name\": \"" + safeDashboardName + "\", \"repo_path\": \"" + crewHome + "\", \"deploy_type\": \"artifact\", \"deploy_slug\": \"" + dashboardSlug + "\", \"description\": \"The dashboard task service — the crew\\u0027s first project\"}'\n" +
|
|
207
|
+
" Return { action: \"created\", project_id: \"" + dashboardSlug + "\" }.\n\n" +
|
|
208
|
+
"Return JSON with action (\"exists\" or \"created\") and project_id (string).",
|
|
209
|
+
{
|
|
210
|
+
key: "project-1",
|
|
211
|
+
label: "Register dashboard project",
|
|
212
|
+
schema: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
action: { type: "string", enum: ["exists", "created"] },
|
|
216
|
+
project_id: { type: "string" }
|
|
217
|
+
},
|
|
218
|
+
required: ["action", "project_id"]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Project registration failed", message: String(e.message || e) } };
|
|
224
|
+
}
|
|
225
|
+
log("Project: " + dashboardSlug + " " + projectResult.action);
|
|
226
|
+
|
|
227
|
+
// ── Phase 4: Cron jobs (declarative manifest) ──────────────────────────
|
|
180
228
|
// The manifest lives in the repo at seed/crons.json. Body templates live
|
|
181
229
|
// in seed/ with {crewHome} placeholders. Missing jobs are
|
|
182
230
|
// created from the manifest; existing jobs converge to it — except `enabled`,
|
|
@@ -188,6 +236,7 @@ try {
|
|
|
188
236
|
"Ensure the Muse Crew cron jobs match the repo's declarative manifest.\n\n" +
|
|
189
237
|
"Manifest: " + crewRepoPath + "/seed/crons.json\n" +
|
|
190
238
|
"crewHome: " + crewHome + "\n" +
|
|
239
|
+
"dashboardSlug: " + dashboardSlug + "\n" +
|
|
191
240
|
"Id overrides (manifest id -> live id): " + JSON.stringify(cronIds) + "\n\n" +
|
|
192
241
|
"Steps:\n" +
|
|
193
242
|
"1. Read the manifest at " + crewRepoPath + "/seed/crons.json and parse it as JSON.\n" +
|
|
@@ -195,15 +244,19 @@ try {
|
|
|
195
244
|
" or any entry lacks a required field (id, title, enabled, mode, schedule,\n" +
|
|
196
245
|
" owner, body_template).\n" +
|
|
197
246
|
"2. For each manifest entry, in order:\n" +
|
|
198
|
-
" a. The live id is the override for entry.id when present
|
|
247
|
+
" a. The live id is the override for entry.id when present (inputs.cronIds[entry.id]).\n" +
|
|
248
|
+
" Otherwise, the live id is entry.id + '-' + dashboardSlug (instance-safe by\n" +
|
|
249
|
+
" construction — two crew instances never share a cron id). The bare manifest\n" +
|
|
250
|
+
" id is never used as a live id.\n" +
|
|
199
251
|
" b. Read the body template at " + crewRepoPath + "/seed/<entry.body_template>.\n" +
|
|
200
252
|
" c. Replace all occurrences of {crewHome} with: " + crewHome + ".\n" +
|
|
253
|
+
" Replace all occurrences of {dashboardSlug} with: " + dashboardSlug + ".\n" +
|
|
201
254
|
" d. Call cron_list and look for a job with the live id.\n" +
|
|
202
255
|
" e. If missing, call cron_add with:\n" +
|
|
203
256
|
" - id: the live id\n" +
|
|
204
257
|
" - title, enabled, mode from the entry\n" +
|
|
205
258
|
" - schedule: the entry's schedule object\n" +
|
|
206
|
-
" - owner: the entry's owner\n" +
|
|
259
|
+
" - owner: the entry's owner with {dashboardSlug} replaced by: " + dashboardSlug + "\n" +
|
|
207
260
|
" - timeout_secs: the entry's timeout_secs when present\n" +
|
|
208
261
|
" - body: the resolved template text\n" +
|
|
209
262
|
" Record action 'created' with no updated fields.\n" +
|
|
@@ -262,7 +315,10 @@ log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" +
|
|
|
262
315
|
return {
|
|
263
316
|
message: "Muse Crew initialized.",
|
|
264
317
|
crewHome: crewHome,
|
|
318
|
+
dashboardSlug: dashboardSlug,
|
|
319
|
+
dashboardName: dashboardName,
|
|
265
320
|
releaseHash: releaseResult.hash,
|
|
266
321
|
scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
|
|
322
|
+
project: projectResult.action,
|
|
267
323
|
crons: cronsResult.summary.crons
|
|
268
324
|
};
|