muse-crew 0.7.19 → 0.8.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 +179 -7
- package/docs/publish-verification.md +4 -2
- package/lib/AGENTS.md +6 -2
- package/lib/build-registry.js +3 -2
- package/lib/compose-evidence-caption.js +57 -14
- package/lib/publish-npm.sh +33 -0
- package/lib/test-publish-preflight.sh +210 -0
- package/lib/test-worktree-backend.sh +51 -1
- package/lib/update-watch.js +633 -0
- package/lib/verify-publish.js +51 -13
- package/lib/worktree-lifecycle.sh +68 -10
- package/package.json +1 -1
- package/seed/AGENTS.md +1 -0
- package/seed/cron-body-update-watch.md +13 -0
- package/seed/crons.json +14 -1
- package/seed/workflows/upgrade.md +21 -0
- package/workflows/AGENTS.md +1 -1
- package/workflows/bugfix.js +416 -74
- package/workflows/chore.js +223 -72
- package/workflows/crew-dispatch.js +43 -6
- package/workflows/crew-init.js +173 -10
- package/workflows/standard.js +231 -79
- package/workflows/upgrade.js +794 -0
package/workflows/crew-init.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export const meta = {
|
|
2
2
|
name: "crew-init",
|
|
3
|
-
description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, register the first project, set up cron jobs. Idempotent — safe to re-run. The dashboard is optional: omit dashboardSlug/dashboardRepoPath for a CLI-only install. Takes an optional crewName (the human's chosen name for the crew), stored at $CREW_HOME/crew-name.",
|
|
3
|
+
description: "Initialize Muse Crew: bootstrap release, scaffold orchestration, register the first project, set up cron jobs, and persist the automatic-update policy. Idempotent — safe to re-run. The dashboard is optional: omit dashboardSlug/dashboardRepoPath for a CLI-only install. Takes an optional crewName (the human's chosen name for the crew), stored at $CREW_HOME/crew-name. Takes optional autoUpdateCrew/autoUpdateDashboard/updateChannel (defaults true/true/latest) for the automatic update watcher policy.",
|
|
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 first project" },
|
|
8
|
-
{ name: "crons", title: "Create and converge cron jobs" }
|
|
8
|
+
{ name: "crons", title: "Create and converge cron jobs" },
|
|
9
|
+
{ name: "policy", title: "Persist update policy" }
|
|
9
10
|
]
|
|
10
11
|
};
|
|
11
12
|
|
|
@@ -19,6 +20,28 @@ const dashboardName = inputs.dashboardName || "Muse Crew";
|
|
|
19
20
|
const crewName = inputs.crewName || null;
|
|
20
21
|
const cronIds = inputs.cronIds || {};
|
|
21
22
|
|
|
23
|
+
// ── Automatic-update policy inputs ──────────────────────────────────
|
|
24
|
+
// All optional. autoUpdateCrew/autoUpdateDashboard default to true (the
|
|
25
|
+
// update watcher files upgrade tasks through the loop); updateChannel
|
|
26
|
+
// defaults to "latest" ("patch" narrows crew upgrades to patch releases).
|
|
27
|
+
// Explicit inputs win on every run; existing config values survive a
|
|
28
|
+
// re-init that passes no inputs; defaults apply only on first init —
|
|
29
|
+
// never silently resurrect a human's deliberate opt-out.
|
|
30
|
+
const autoUpdateCrew = inputs.autoUpdateCrew !== undefined ? !!inputs.autoUpdateCrew : true;
|
|
31
|
+
const autoUpdateDashboard = inputs.autoUpdateDashboard !== undefined ? !!inputs.autoUpdateDashboard : true;
|
|
32
|
+
const updateChannel = inputs.updateChannel || "latest";
|
|
33
|
+
|
|
34
|
+
// Validation in workflow code, before any agent call — fail closed.
|
|
35
|
+
if (updateChannel !== "latest" && updateChannel !== "patch") {
|
|
36
|
+
return {
|
|
37
|
+
__hatchWorkflowControl: "blocked",
|
|
38
|
+
result: {
|
|
39
|
+
blocked_reason: "Invalid updateChannel",
|
|
40
|
+
message: "updateChannel must be \"latest\" or \"patch\" — got " + JSON.stringify(updateChannel) + ". Re-run init with a valid channel."
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
if (!crewRepoPath) throw new Error("crewRepoPath is required — path to muse-crew (git checkout or npm install)");
|
|
23
46
|
if (!crewHome) throw new Error("crewHome is required — e.g. ~/workspace/.crew (must be inside the workspace)");
|
|
24
47
|
// The dashboard is optional. Provide both dashboardSlug and dashboardRepoPath
|
|
@@ -367,19 +390,76 @@ try {
|
|
|
367
390
|
log("Project: " + dashboardSlug + " " + projectResult.action + " (repo_path=" + gateFacts.dashboardRepoExpanded + ")");
|
|
368
391
|
} // end dashboard-mode project registration
|
|
369
392
|
|
|
393
|
+
// ── Scheduler identity (2026-09-16) ──────────────────────────────────
|
|
394
|
+
// The crew's scheduler identity is dashboard-independent and chosen once.
|
|
395
|
+
// First init uses the crew-home basename; a re-run keeps the instanceId
|
|
396
|
+
// recorded in .cron-registry.json. A registry that exists but is corrupt,
|
|
397
|
+
// or parses but carries no usable instanceId, fails closed — init will not
|
|
398
|
+
// guess the previous identity, because a wrong guess would orphan the old
|
|
399
|
+
// cron jobs and create duplicates. Attaching or removing a dashboard
|
|
400
|
+
// is a project registration, never an identity change, so it can neither
|
|
401
|
+
// orphan nor duplicate the polling loop. The loop is cli-owned: deleting a
|
|
402
|
+
// dashboard artifact must not stop the crew (the crew never requires a
|
|
403
|
+
// dashboard); crew removal is crew-uninstall's job (registry + discovery).
|
|
404
|
+
// The sensor reports raw; the decision below is pure JS.
|
|
405
|
+
function priorInstanceId(cronRegistryJson, registryPath) {
|
|
406
|
+
if (cronRegistryJson == null) return null;
|
|
407
|
+
var reg;
|
|
408
|
+
try {
|
|
409
|
+
reg = JSON.parse(cronRegistryJson);
|
|
410
|
+
} catch (e) {
|
|
411
|
+
throw new Error("existing " + registryPath + " is not valid JSON (" + String(e.message || e) + "). Inspect or remove it, then re-run init — init will not guess the previous scheduler identity.");
|
|
412
|
+
}
|
|
413
|
+
var id = reg && reg.instanceId;
|
|
414
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
415
|
+
throw new Error("existing " + registryPath + " has no usable instanceId. Repair the registry or remove it, then re-run init — init will not guess the previous scheduler identity.");
|
|
416
|
+
}
|
|
417
|
+
return id;
|
|
418
|
+
}
|
|
419
|
+
|
|
370
420
|
// ── Phase 4: Cron jobs (declarative manifest) ──────────────────────────
|
|
371
421
|
// The manifest lives in the repo at seed/crons.json. Body templates live
|
|
372
422
|
// in seed/ with {crewHome} placeholders. Missing jobs are
|
|
373
423
|
// created from the manifest; existing jobs converge to it — except `enabled`,
|
|
374
424
|
// which is a creation-time default only and is never touched on update.
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// scopes them instead, so two crew instances never share a cron id and no
|
|
378
|
-
// dashboard is required.
|
|
425
|
+
// Live ids are entry.id + '-' + instanceId (instance-safe by construction —
|
|
426
|
+
// two crew instances never share a cron id) unless overridden via cronIds.
|
|
379
427
|
phase("crons");
|
|
380
|
-
var
|
|
381
|
-
var
|
|
382
|
-
|
|
428
|
+
var registryPath = gateFacts.crewHomeExpanded + "/.cron-registry.json";
|
|
429
|
+
var identityFacts;
|
|
430
|
+
try {
|
|
431
|
+
identityFacts = await agent(
|
|
432
|
+
"Read this crew's existing scheduler identity, if any. Report raw facts.\\n\\n" +
|
|
433
|
+
"Crew home: " + gateFacts.crewHomeExpanded + "\\n\\n" +
|
|
434
|
+
"Steps (run in shell):\\n" +
|
|
435
|
+
"1. If " + registryPath + " exists, print its exact bytes with cat.\\n" +
|
|
436
|
+
"2. If it does not exist, print nothing.\\n" +
|
|
437
|
+
"3. Return JSON { registryJson }: registryJson is the file's exact text, or null when the file does not exist. Do not judge, summarize, or repair — report raw.",
|
|
438
|
+
{
|
|
439
|
+
key: "crons-identity-1",
|
|
440
|
+
label: "Read existing scheduler identity",
|
|
441
|
+
schema: {
|
|
442
|
+
type: "object",
|
|
443
|
+
properties: {
|
|
444
|
+
registryJson: { type: ["string", "null"] }
|
|
445
|
+
},
|
|
446
|
+
required: ["registryJson"]
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
} catch (e) {
|
|
451
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Scheduler identity read failed", message: String(e.message || e) } };
|
|
452
|
+
}
|
|
453
|
+
var instanceId;
|
|
454
|
+
try {
|
|
455
|
+
instanceId = priorInstanceId(identityFacts.registryJson, registryPath) ||
|
|
456
|
+
gateFacts.crewHomeExpanded.split("/").pop().replace(/^\./, "") || "crew";
|
|
457
|
+
} catch (e) {
|
|
458
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Scheduler identity unreadable", message: String(e.message || e) } };
|
|
459
|
+
}
|
|
460
|
+
var cronOwner = inputs.cronOwner || ("cli:" + instanceId);
|
|
461
|
+
log("Scheduler identity: instanceId=" + instanceId + " owner=" + cronOwner +
|
|
462
|
+
(identityFacts.registryJson ? " (kept from existing registry)" : " (first init; crew-home basename)"));
|
|
383
463
|
var cronsResult;
|
|
384
464
|
try {
|
|
385
465
|
cronsResult = await agent(
|
|
@@ -480,6 +560,83 @@ log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" +
|
|
|
480
560
|
"; registry: " + cronsResult.summary.registry.path + " (" + cronsResult.summary.registry.ids.join(", ") + ")");
|
|
481
561
|
|
|
482
562
|
|
|
563
|
+
// ── Phase 5: Update policy ────────────────────────────────────────────
|
|
564
|
+
// Persists the automatic-update policy (auto_update_crew,
|
|
565
|
+
// auto_update_dashboard, update_channel) via the Crew API's update-config,
|
|
566
|
+
// and seeds $CREW_HOME/.update-watch.json as {} only when it does not
|
|
567
|
+
// exist. Re-init semantics mirror the crons manifest rule: explicit inputs
|
|
568
|
+
// win; existing config values are kept; defaults apply only on first init —
|
|
569
|
+
// a re-init with no inputs never silently resurrects a human's opt-out.
|
|
570
|
+
// No wall-clock reads anywhere in this phase or its prompt.
|
|
571
|
+
phase("policy");
|
|
572
|
+
var policyInputCrew = inputs.autoUpdateCrew !== undefined ? (!!inputs.autoUpdateCrew ? "true" : "false") : null;
|
|
573
|
+
var policyInputDashboard = inputs.autoUpdateDashboard !== undefined ? (!!inputs.autoUpdateDashboard ? "true" : "false") : null;
|
|
574
|
+
var policyInputChannel = inputs.updateChannel !== undefined ? updateChannel : null;
|
|
575
|
+
var safeExpandedHome = gateFacts.crewHomeExpanded.split('"').join('\\"');
|
|
576
|
+
var policyResult;
|
|
577
|
+
try {
|
|
578
|
+
policyResult = await agent(
|
|
579
|
+
"Persist the automatic-update policy for the crew's update watcher.\n\n" +
|
|
580
|
+
"Crew home (expanded): " + gateFacts.crewHomeExpanded + "\n" +
|
|
581
|
+
"Crew API: " + gateFacts.crewHomeExpanded + "/current/lib/crew-api.js\n\n" +
|
|
582
|
+
"Explicit inputs for this run (null means the human gave no input):\n" +
|
|
583
|
+
"- auto_update_crew: " + policyInputCrew + " (default true)\n" +
|
|
584
|
+
"- auto_update_dashboard: " + policyInputDashboard + " (default true)\n" +
|
|
585
|
+
"- update_channel: " + policyInputChannel + " (default latest)\n\n" +
|
|
586
|
+
"Steps:\n" +
|
|
587
|
+
"1. Read the current config:\n" +
|
|
588
|
+
" node " + gateFacts.crewHomeExpanded + "/current/lib/crew-api.js --crew-home " + gateFacts.crewHomeExpanded + " get-config\n" +
|
|
589
|
+
" The response carries { config: { key: value, ... } }.\n" +
|
|
590
|
+
"2. For each key in (auto_update_crew, auto_update_dashboard, update_channel):\n" +
|
|
591
|
+
" - When an explicit input is present for the key, write the input value.\n" +
|
|
592
|
+
" - Otherwise, when the config already holds a value for the key, keep it —\n" +
|
|
593
|
+
" do NOT overwrite it with the default. A human's deliberate opt-out\n" +
|
|
594
|
+
" (e.g. auto_update_crew=false) must survive a re-init that passes no inputs.\n" +
|
|
595
|
+
" - Otherwise write the default (true / true / latest).\n" +
|
|
596
|
+
" Write via:\n" +
|
|
597
|
+
" node " + gateFacts.crewHomeExpanded + "/current/lib/crew-api.js --crew-home " + gateFacts.crewHomeExpanded + " update-config --json '{\"key\": \"<key>\", \"value\": \"<value>\"}'\n" +
|
|
598
|
+
" Values are the strings \"true\"/\"false\" for the booleans and \"latest\"/\"patch\"\n" +
|
|
599
|
+
" for the channel. Only issue update-config for a key whose final value\n" +
|
|
600
|
+
" differs from the current config value.\n" +
|
|
601
|
+
"3. Seed the watcher state file only when absent — never overwrite it,\n" +
|
|
602
|
+
" it is the idempotency record for filed upgrade tasks:\n" +
|
|
603
|
+
" test -f \"" + safeExpandedHome + "/.update-watch.json\" && echo STATE_EXISTS || (printf '%s' '{}' > \"" + safeExpandedHome + "/.update-watch.json\" && echo STATE_SEEDED)\n" +
|
|
604
|
+
"4. Return JSON { persisted: { auto_update_crew: \"<value>\", auto_update_dashboard: \"<value>\", update_channel: \"<value>\" }, state_file: \"seeded\" | \"exists\" }\n" +
|
|
605
|
+
" with the values that are in effect after this run (explicit input,\n" +
|
|
606
|
+
" kept config value, or default — whichever applied).",
|
|
607
|
+
{
|
|
608
|
+
key: "policy-1",
|
|
609
|
+
label: "Persist update policy",
|
|
610
|
+
schema: {
|
|
611
|
+
type: "object",
|
|
612
|
+
properties: {
|
|
613
|
+
persisted: {
|
|
614
|
+
type: "object",
|
|
615
|
+
properties: {
|
|
616
|
+
auto_update_crew: { type: "string" },
|
|
617
|
+
auto_update_dashboard: { type: "string" },
|
|
618
|
+
update_channel: { type: "string" }
|
|
619
|
+
},
|
|
620
|
+
required: ["auto_update_crew", "auto_update_dashboard", "update_channel"]
|
|
621
|
+
},
|
|
622
|
+
state_file: { type: "string", enum: ["seeded", "exists"] }
|
|
623
|
+
},
|
|
624
|
+
required: ["persisted", "state_file"]
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
} catch (e) {
|
|
629
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Update policy persist failed", message: String(e.message || e) } };
|
|
630
|
+
}
|
|
631
|
+
var persistedPolicy = policyResult.persisted || {};
|
|
632
|
+
if (!persistedPolicy.auto_update_crew || !persistedPolicy.auto_update_dashboard || !persistedPolicy.update_channel) {
|
|
633
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Update policy incomplete", message: "policy phase returned no persisted policy values" } };
|
|
634
|
+
}
|
|
635
|
+
log("Update policy: auto_update_crew=" + persistedPolicy.auto_update_crew +
|
|
636
|
+
" auto_update_dashboard=" + persistedPolicy.auto_update_dashboard +
|
|
637
|
+
" update_channel=" + persistedPolicy.update_channel +
|
|
638
|
+
" state_file=" + policyResult.state_file);
|
|
639
|
+
|
|
483
640
|
// ── Summary ───────────────────────────────────────────────────────────
|
|
484
641
|
return {
|
|
485
642
|
message: "Muse Crew initialized.",
|
|
@@ -492,5 +649,11 @@ return {
|
|
|
492
649
|
scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
|
|
493
650
|
project: projectResult.action,
|
|
494
651
|
crons: cronsResult.summary.crons,
|
|
495
|
-
registry: cronsResult.summary.registry
|
|
652
|
+
registry: cronsResult.summary.registry,
|
|
653
|
+
autoUpdate: {
|
|
654
|
+
crew: persistedPolicy.auto_update_crew === "true",
|
|
655
|
+
dashboard: persistedPolicy.auto_update_dashboard === "true",
|
|
656
|
+
channel: persistedPolicy.update_channel
|
|
657
|
+
},
|
|
658
|
+
announcement: "Your crew and dashboard will update themselves automatically; say the word if you'd rather approve each one."
|
|
496
659
|
};
|