muse-crew 0.2.1 → 0.4.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/API.md +25 -3
- package/docs/guide.md +1 -1
- package/lib/crew-release.sh +49 -2
- package/lib/worktree-lifecycle.sh +5 -3
- package/package.json +1 -1
- package/workflows/bugfix.js +99 -17
- package/workflows/chore.js +92 -17
- package/workflows/crew-dispatch.js +5 -3
- package/workflows/crew-init.js +33 -0
- package/workflows/standard.js +99 -17
package/API.md
CHANGED
|
@@ -16,7 +16,7 @@ Create a new task on the board.
|
|
|
16
16
|
|-------|------|----------|-------|
|
|
17
17
|
| `title` | string (1–160 chars) | yes | |
|
|
18
18
|
| `description` | string (≤ 5000) | no | Defaults to `""` |
|
|
19
|
-
| `project` | slug | no |
|
|
19
|
+
| `project` | slug | no | Implicit project resolution: an explicit `project` wins; otherwise the service resolves `config.default_project` (set by `setdefaultproject`, `crew-init`, or automatically for the first registered project); as a last resort it falls back to the legacy `"orchestra-dashboard"` project id. Agents filing tasks normally omit `project` and inherit the dashboard's default. |
|
|
20
20
|
| `workflow` | slug or null | no | Routing label (e.g. `standard`, `bugfix`, `chore`, `docs`) |
|
|
21
21
|
| `priority` | `high` · `normal` · `low` | no | Defaults to `normal` |
|
|
22
22
|
| `state` | `todo` · `in_progress` · `parked` · `done` | no | Defaults to `todo` |
|
|
@@ -33,7 +33,7 @@ Update an existing task's fields. Only `id` is required; all other fields are op
|
|
|
33
33
|
| `description` | string (≤ 3000) | no | |
|
|
34
34
|
| `state` | `todo` · `in_progress` · `parked` · `done` | no | |
|
|
35
35
|
| `priority` | `high` · `normal` · `low` | no | |
|
|
36
|
-
| `project` | slug | no | |
|
|
36
|
+
| `project` | slug | no | **Project-move guard:** changing this to a different project throws while the task has an active run (an agent session with status `running` that started within the last hour), because the live run keeps the old project's repo context and moving it mid-phase would work on the wrong repo. Wait for the run to finish, or recover/park the task first, then move it. |
|
|
37
37
|
| `workflow` | slug or null | no | |
|
|
38
38
|
| `deps` | string[] | no | |
|
|
39
39
|
|
|
@@ -143,17 +143,31 @@ Register a new project.
|
|
|
143
143
|
| `simultaneity` | integer (1–100) | no | Max concurrent tasks; defaults to 2 |
|
|
144
144
|
| `quiesced` | boolean | no | Start paused; defaults to false |
|
|
145
145
|
|
|
146
|
+
The first registered project automatically becomes the `default_project` (see State) when no default is set yet.
|
|
147
|
+
|
|
146
148
|
### `updateproject`
|
|
147
149
|
|
|
148
150
|
Update project fields. Only `id` is required; all others are optional patch fields. Same fields as `createproject`, except `simultaneity` minimum is 0 (for kill switch state).
|
|
149
151
|
|
|
152
|
+
**Context-change guard:** changing `repo_path`, `deploy_type`, or `deploy_slug` is blocked while any task in the project has an active run (an agent session with status `running` that started within the last hour): the call throws, because live runs keep the old project config and the change would split the project context mid-run. Wait for the runs to finish, or recover/park those tasks first, then retry.
|
|
153
|
+
|
|
154
|
+
### `setdefaultproject`
|
|
155
|
+
|
|
156
|
+
Set the dashboard's default project — the project the CLI and agents use implicitly when no explicit project is given. The dispatcher resolves a task's project as its explicit label first, then this default, then the first registered project as a last resort.
|
|
157
|
+
|
|
158
|
+
| Field | Type | Required | Notes |
|
|
159
|
+
|-------|------|----------|-------|
|
|
160
|
+
| `project_id` | slug | yes | Must be a registered project id, else the call throws `"Project not found."` |
|
|
161
|
+
|
|
162
|
+
Returns `{ "ok": true, "project_id": "<slug>" }`. Idempotent: sets the `default_project` config key via upsert.
|
|
163
|
+
|
|
150
164
|
### `deleteproject`
|
|
151
165
|
|
|
152
166
|
Remove a project registration.
|
|
153
167
|
|
|
154
168
|
| Field | Type | Required | Notes |
|
|
155
169
|
|-------|------|----------|-------|
|
|
156
|
-
| `id` | slug | yes | |
|
|
170
|
+
| `id` | slug | yes | If this project was the `default_project`, the default pointer is cleared so it never aims at a missing project. |
|
|
157
171
|
|
|
158
172
|
### `listprojects`
|
|
159
173
|
|
|
@@ -201,6 +215,14 @@ Read the full dashboard state: tasks, sessions, events, and projects. Used by wo
|
|
|
201
215
|
|
|
202
216
|
Read global configuration. Takes no arguments.
|
|
203
217
|
|
|
218
|
+
**Known keys:**
|
|
219
|
+
|
|
220
|
+
| Key | Value | Set by |
|
|
221
|
+
|-----|-------|--------|
|
|
222
|
+
| `default_project` | project slug | `setdefaultproject` (explicit upsert), `createproject` (first project only), `crew-init` (the dashboard's own project) |
|
|
223
|
+
|
|
224
|
+
`default_project` is the dashboard's configured default project: agents and the CLI inherit it when no explicit project is given. Deleting the project it points at clears it. No migration is needed — the key simply appears once set, and the dispatcher reads it from `getdispatchstate`'s `config` object.
|
|
225
|
+
|
|
204
226
|
### `updateconfig`
|
|
205
227
|
|
|
206
228
|
Set a global configuration value.
|
package/docs/guide.md
CHANGED
|
@@ -213,7 +213,7 @@ Each phase has an assigned identity — a character with a defined personality:
|
|
|
213
213
|
| Map | **Mara** | Designer |
|
|
214
214
|
| Build | **Wren** | Quietest one, trusts the plan |
|
|
215
215
|
| Review | **Cass** | Fair but exacting — holds the spec as the contract |
|
|
216
|
-
| Integrate | **Wren** | Merges the work, pushes `main` to the repo |
|
|
216
|
+
| Integrate | **Wren** | Merges the work, pushes `main` to the repo (succeeds vacuously when the task branch is empty — runtime-state deliverable) |
|
|
217
217
|
| Publish | **Wren** | Ships the merged code to the publish target (skipped when none) |
|
|
218
218
|
| QA | **Hazel** | Code-blind, persistent, wears persona costumes |
|
|
219
219
|
| Reproduce | **Hazel** | Reproduces bugs before fixing |
|
package/lib/crew-release.sh
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Layout:
|
|
8
8
|
# ~/.jarvis/
|
|
9
|
-
# releases/<full-commit-hash>/ # immutable snapshot (workflows/ + lib/)
|
|
9
|
+
# releases/<full-commit-hash>/ # immutable snapshot (workflows/ + lib/ + seed/workflows/ docs)
|
|
10
10
|
# current -> releases/<hash> # active release
|
|
11
11
|
# workflows -> current/workflows # convenience (cron/dispatcher reads this)
|
|
12
12
|
# lib -> current/lib # convenience
|
|
13
|
+
# .orchestration/workflows/ # live workflow documents the dashboard
|
|
14
|
+
# # reads; re-synced from the activated
|
|
15
|
+
# # release on every deploy/rollback
|
|
13
16
|
#
|
|
14
17
|
# The Muse workflow runtime snapshots .js scripts at launch.
|
|
15
18
|
# /tmp/crew-lib-* pins lifecycle scripts per run.
|
|
@@ -91,6 +94,37 @@ _validate_workflows() {
|
|
|
91
94
|
echo "VALIDATED: workflow scripts parse"
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
# ── workflow doc sync ───────────────────────────────────────────────
|
|
98
|
+
# Decision (2026-09-09): the dashboard reads the workflow documents live
|
|
99
|
+
# from $CREW_HOME/.orchestration/workflows/, and `recovertask` validates
|
|
100
|
+
# target_phase against them — so those documents must always reflect the
|
|
101
|
+
# phases the dispatcher actually runs. Both the workflow scripts and the
|
|
102
|
+
# .md docs ship in the same repo commit, so we re-sync the docs from the
|
|
103
|
+
# activated release on every release activation (deploy AND rollback),
|
|
104
|
+
# automatically. A manual re-sync step is exactly what drifted here: the
|
|
105
|
+
# copy was seeded once by crew-init's `cp -n` and never refreshed, leaving
|
|
106
|
+
# the dashboard on pre-publish-architecture phases (no Integrate, no
|
|
107
|
+
# Publish) while the dispatcher ran the current phase lists.
|
|
108
|
+
_sync_workflow_docs() {
|
|
109
|
+
local home="${1:?usage: _sync_workflow_docs <jarvis-home>}"
|
|
110
|
+
local src="$home/current/seed/workflows"
|
|
111
|
+
local dest="$home/.orchestration/workflows"
|
|
112
|
+
if [ ! -d "$src" ]; then
|
|
113
|
+
echo "WORKFLOW-DOCS: no seed/workflows in activated release; skipping sync" >&2
|
|
114
|
+
return 0
|
|
115
|
+
fi
|
|
116
|
+
mkdir -p "$dest"
|
|
117
|
+
local f base copied=0
|
|
118
|
+
for f in "$src"/*.md; do
|
|
119
|
+
[ -f "$f" ] || continue
|
|
120
|
+
base="$(basename "$f")"
|
|
121
|
+
[ "$base" = "AGENTS.md" ] && continue # folder docs, not a workflow
|
|
122
|
+
cp -f "$f" "$dest/$base"
|
|
123
|
+
copied=$((copied + 1))
|
|
124
|
+
done
|
|
125
|
+
echo "WORKFLOW-DOCS: synced $copied workflow docs to $dest"
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
# ── deploy ────────────────────────────────────────────────────────────
|
|
95
129
|
# Build, validate, and atomically activate a release from repo HEAD.
|
|
96
130
|
# Single command — no cross-step lock needed.
|
|
@@ -124,11 +158,18 @@ cmd_deploy() {
|
|
|
124
158
|
mkdir -p "$staging_dir"
|
|
125
159
|
if [ -d .git ]; then
|
|
126
160
|
# Extract from committed tree (not working directory)
|
|
127
|
-
|
|
161
|
+
_seed=""
|
|
162
|
+
git cat-file -e "HEAD:seed/workflows" 2>/dev/null && _seed="seed/workflows" || true
|
|
163
|
+
# shellcheck disable=SC2086
|
|
164
|
+
git archive HEAD -- workflows lib $_seed | tar -xC "$staging_dir"
|
|
128
165
|
else
|
|
129
166
|
# Plain directory (npm install): copy directly
|
|
130
167
|
[ -d workflows ] && cp -r workflows "$staging_dir/"
|
|
131
168
|
[ -d lib ] && cp -r lib "$staging_dir/"
|
|
169
|
+
if [ -d seed/workflows ]; then
|
|
170
|
+
mkdir -p "$staging_dir/seed"
|
|
171
|
+
cp -r seed/workflows "$staging_dir/seed/"
|
|
172
|
+
fi
|
|
132
173
|
fi
|
|
133
174
|
# Gate: refuse to install a release whose workflow scripts don't parse.
|
|
134
175
|
if ! _validate_workflows "$staging_dir"; then
|
|
@@ -147,6 +188,9 @@ cmd_deploy() {
|
|
|
147
188
|
|
|
148
189
|
echo "ACTIVATED: $hash"
|
|
149
190
|
|
|
191
|
+
# Keep the dashboard's workflow documents in sync with the release.
|
|
192
|
+
_sync_workflow_docs "$CREW_HOME"
|
|
193
|
+
|
|
150
194
|
# Prune old releases — keep the 5 most recent
|
|
151
195
|
_prune_releases "$home" 5
|
|
152
196
|
}
|
|
@@ -172,6 +216,9 @@ cmd_rollback() {
|
|
|
172
216
|
mv -T "$tmp_link" "$CREW_HOME/current"
|
|
173
217
|
|
|
174
218
|
echo "ROLLED_BACK: $current_hash -> $prev"
|
|
219
|
+
|
|
220
|
+
# Keep the dashboard's workflow documents in sync with the release.
|
|
221
|
+
_sync_workflow_docs "$CREW_HOME"
|
|
175
222
|
}
|
|
176
223
|
|
|
177
224
|
# ── current ───────────────────────────────────────────────────────────
|
|
@@ -158,8 +158,11 @@ cmd_integrate() {
|
|
|
158
158
|
local ahead
|
|
159
159
|
ahead=$(git rev-list --count "main..task/$task_id")
|
|
160
160
|
if [ "$ahead" -eq 0 ]; then
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
# Approved empty diff: the deliverable was runtime state (cron,
|
|
162
|
+
# scheduler, dashboard config), not a repo change. Nothing to merge,
|
|
163
|
+
# nothing to serialize — the merge lock is intentionally not taken.
|
|
164
|
+
echo "MERGED_EMPTY: task/$task_id has no commits ahead of main — runtime-state deliverable, nothing to merge"
|
|
165
|
+
return 0
|
|
163
166
|
fi
|
|
164
167
|
|
|
165
168
|
# Acquire merge lock
|
|
@@ -174,7 +177,6 @@ cmd_integrate() {
|
|
|
174
177
|
if ! git merge --no-ff "task/$task_id" -m "$commit_msg" 2>&1; then
|
|
175
178
|
echo "CONFLICT: merge failed — aborting"
|
|
176
179
|
git merge --abort 2>/dev/null || true
|
|
177
|
-
"$MERGE_LOCK" release "$task_id" >/dev/null 2>&1
|
|
178
180
|
exit 3
|
|
179
181
|
fi
|
|
180
182
|
|
package/package.json
CHANGED
package/workflows/bugfix.js
CHANGED
|
@@ -78,6 +78,24 @@ const MAX_TOTAL_REWORK = 2;
|
|
|
78
78
|
let totalReworkCount = 0;
|
|
79
79
|
let rejectionNotes = inputs.rejection_notes || "";
|
|
80
80
|
let mapperSpec = "";
|
|
81
|
+
// Merge-time versioning: the release decision is extracted deterministically
|
|
82
|
+
// from the accepted Build summary (extractReleaseDecision) so the Publish
|
|
83
|
+
// agent never decides whether to publish. Two consecutive Publish runs
|
|
84
|
+
// rationalized a skip against explicit instruction text — text alone did not
|
|
85
|
+
// hold, so the decision now lives in workflow code, not agent judgment.
|
|
86
|
+
let releaseDecision = null; // { release: "yes"|"no", version_bump: "patch"|"minor"|"major"|null }
|
|
87
|
+
function extractReleaseDecision(text) {
|
|
88
|
+
const t = text || "";
|
|
89
|
+
const r = /^release:\s*(yes|no)\s*$/im.exec(t);
|
|
90
|
+
if (!r) return null;
|
|
91
|
+
const b = /^version_bump:\s*(patch|minor|major)\s*$/im.exec(t);
|
|
92
|
+
if (r[1].toLowerCase() === "yes" && !b) return null;
|
|
93
|
+
return { release: r[1].toLowerCase(), version_bump: b ? b[1].toLowerCase() : null };
|
|
94
|
+
}
|
|
95
|
+
function releaseDecisionText() {
|
|
96
|
+
if (!releaseDecision) return "no parseable release:/version_bump: decision";
|
|
97
|
+
return "release: " + releaseDecision.release + (releaseDecision.version_bump ? ", version_bump: " + releaseDecision.version_bump : " (no version_bump line)");
|
|
98
|
+
}
|
|
81
99
|
let i = startStepIndex;
|
|
82
100
|
|
|
83
101
|
// Pin lifecycle scripts
|
|
@@ -150,6 +168,28 @@ while (i < STEPS.length) {
|
|
|
150
168
|
return { status: "blocked", task_id: taskId, reason: "Unknown publish target '" + PUBLISH_TYPE + "' — expected 'npm', 'artifact', 'vercel', or empty (skip publish)." };
|
|
151
169
|
}
|
|
152
170
|
|
|
171
|
+
// Merge-time versioning, decided deterministically: the release decision was
|
|
172
|
+
// extracted from the accepted Build summary above. release: no never reaches
|
|
173
|
+
// the Publish agent (the workflow skips it here, exactly like the no-target
|
|
174
|
+
// case); an unparseable decision blocks fail-closed. The Publish agent below
|
|
175
|
+
// therefore has no decision point to rationalize into a skip.
|
|
176
|
+
if (step.name === "Publish" && PUBLISH_TYPE === "npm") {
|
|
177
|
+
if (!releaseDecision) {
|
|
178
|
+
return { status: "blocked", task_id: taskId, reason: "Build summary has no parseable release:/version_bump: lines — cannot assign version at publish time." };
|
|
179
|
+
}
|
|
180
|
+
if (releaseDecision.release === "no") {
|
|
181
|
+
log("Publish skipped for task " + taskId + " — accepted Build summary declared release: no");
|
|
182
|
+
await agent(
|
|
183
|
+
"Release the merge lock and clean up without publishing.\n" +
|
|
184
|
+
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
185
|
+
"If the output contains DEPLOYED, the lock is released and the worktree is cleaned up.",
|
|
186
|
+
{ key: "publish-skip-release-no", label: "Skipping Publish (release: no)", schema: { type: "object" } }
|
|
187
|
+
);
|
|
188
|
+
i++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
153
193
|
// Claim session
|
|
154
194
|
let activeSessionId;
|
|
155
195
|
if (isFirstClaim && firstSessionId) {
|
|
@@ -204,11 +244,15 @@ while (i < STEPS.length) {
|
|
|
204
244
|
"Edit the TypeScript source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
|
|
205
245
|
"Do not add unrequested features. Build exactly what the spec calls for.\n" +
|
|
206
246
|
"PUBLIC DOCS: If your change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), update the public docs in the same commit — API.md for API changes. Documentation and implementation ship together.\n\n" +
|
|
207
|
-
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry
|
|
247
|
+
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry. Versions are assigned at PUBLISH time — never in your branch. Do NOT touch the `version` field in package.json (or package-lock). Instead, end your summary with exactly these two lines:\n" +
|
|
248
|
+
"release: yes|no — 'yes' if this change warrants a published release (anything a consumer can observe: workflow behavior, phase lists, identities, published docs, API); 'no' if internal-only.\n" +
|
|
249
|
+
"version_bump: patch|minor|major — patch for fixes (default), minor for new behavior, major for breaking changes. Omit this line only when release is no.\n" +
|
|
250
|
+
"Example: release: yes\\nversion_bump: minor\n\n" : "") +
|
|
208
251
|
"STEP 3: Commit your changes.\n" +
|
|
209
252
|
"cd " + REPO_PATH + "/.worktrees/" + taskId + "\n" +
|
|
210
253
|
"git add -A\n" +
|
|
211
254
|
"git commit -m \"fix: " + safeTitle + "\"\n\n" +
|
|
255
|
+
"If the task's deliverable is runtime state (a cron definition, scheduler change, or dashboard/config state created outside the repo) and the repository genuinely needs no change, do NOT fabricate a commit: leave the branch with no commits ahead of main and declare `repo_diff: none` in your summary, naming the runtime-state deliverable. Otherwise commit your changes normally.\n\n" +
|
|
212
256
|
(rejectionNotes ? "This is REWORK after rejection. Address these specific issues:\n" + rejectionNotes + "\n\n" : "") +
|
|
213
257
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"what you built\", \"passed\": true }. No prose, no markdown, just the JSON object.";
|
|
214
258
|
|
|
@@ -222,7 +266,13 @@ while (i < STEPS.length) {
|
|
|
222
266
|
REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
|
|
223
267
|
"Check quality, correctness, and spec compliance.\n" +
|
|
224
268
|
"Check that public-affecting changes have matching public doc updates (API.md or the published API contract). If the docs are missing or inaccurate, reject with notes on what is stale.\n" +
|
|
225
|
-
|
|
269
|
+
"If the branch has no commits ahead of main (inspect shows an empty commit log), approve ONLY if the Build summary declares `repo_diff: none` with a plausible runtime-state deliverable (e.g. a cron created via the cron tool). Otherwise reject: 'no commits ahead of main and no repo_diff: none declaration — the builder likely forgot to commit'.\n" +
|
|
270
|
+
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, and versions are assigned at publish time — never in branches. Two checks:\n" +
|
|
271
|
+
"(a) The task branch must NOT have changed package.json's `version` field. Check: cd " + REPO_PATH + " && git diff main...task/" + taskId + " -- package.json. If the branch touched `version` in any way, REJECT with notes: 'versions are assigned at publish time, never in branches — remove the version change'.\n" +
|
|
272
|
+
"(b) The accepted Build summary declares: " + releaseDecisionText() + ". " +
|
|
273
|
+
(releaseDecision
|
|
274
|
+
? "Validate this decision against the change: release must be 'yes' when the change is consumer-observable and 'no' when internal-only; the version_bump scope must fit the change (patch for fixes, minor for new behavior, major for breaking changes). If the decision is wrong or mis-scoped, reject with notes."
|
|
275
|
+
: "The decision is missing or malformed — REJECT with notes: 'Build summary must end with release: yes|no and (when release is yes) version_bump: patch|minor|major lines'.") + "\n" : "") +
|
|
226
276
|
"If the work passes review, your final response MUST be valid JSON and nothing else: { \"passed\": true, \"summary\": \"approval notes\" }.\n" +
|
|
227
277
|
"If the work fails review, your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"rejection notes explaining what needs to change\" }.\n" +
|
|
228
278
|
"No prose, no markdown, just the JSON object.";
|
|
@@ -232,8 +282,17 @@ while (i < STEPS.length) {
|
|
|
232
282
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " integrate " + taskId + " \"merge: fix: " + safeTitle + "\"\n\n" +
|
|
233
283
|
"Read the output:\n" +
|
|
234
284
|
"- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
|
|
285
|
+
"- If it contains MERGED_EMPTY, the branch had no commits ahead of main (a runtime-state deliverable, declared by Build as repo_diff: none). Integration succeeded vacuously: the merge lock was NOT taken and there is no new commit. Set passed to true with summary 'merged empty: no repo changes — deliverable was runtime state'. SKIP STEP 2 (push): there is no new commit to push.\n" +
|
|
235
286
|
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Set passed to false.\n" +
|
|
236
|
-
"- If it contains CONFLICT,
|
|
287
|
+
"- If it contains CONFLICT, the plain merge failed — the merge was aborted, main is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
|
|
288
|
+
"RESOLUTION:\n" +
|
|
289
|
+
"R1. Refresh the merge lock FIRST (a long resolution must not silently lose the lock to the orphan sweep): CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + ". Create a scratch worktree WITH A NEW BRANCH (main is already checked out in the repo checkout, so git forbids checking it out a second time): cd " + REPO_PATH + " && git worktree add -b resolve/" + taskId + " /tmp/crew-resolve-" + taskId + " main. Reproduce the conflict in the scratch worktree: cd /tmp/crew-resolve-" + taskId + " && git merge task/" + taskId + ". This reproduces the exact conflict (main has not moved — the lock was held throughout). The task branch task/" + taskId + " is never modified.\n" +
|
|
290
|
+
"R2. For each conflicted file, read the three sides: git show :1:<file> (base), git show :2:<file> (ours = main), git show :3:<file> (theirs = task branch). Resolve each hunk by keeping both sides' changes when they do not semantically overlap. Version-only hunks resolve to the higher semver (safety net). Never invent new behavior. Leave no markers.\n" +
|
|
291
|
+
"R3. Verify in the scratch worktree, in this order: (a) git diff --check is clean; (b) git grep -n '^<<<<<<<' -- . returns nothing; (c) node --check every changed .js file (conflicted files plus everything listed by git diff --name-only). If any check fails, refresh the merge lock (CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "), then retry the resolution using the failure output as context — max 3 attempts total.\n" +
|
|
292
|
+
"R4. Commit the resolution on resolve/" + taskId + ": git add -A && git commit -m \"resolve conflicts: " + taskId + "\".\n" +
|
|
293
|
+
"R5. Back in " + REPO_PATH + ": git checkout main && git merge --ff-only resolve/" + taskId + ". This fast-forwards — main has not moved while the lock was held. Then STEP 2 applies: git push origin main. Report the merged commit hash.\n" +
|
|
294
|
+
"R6. Clean up: cd " + REPO_PATH + " && git worktree remove --force /tmp/crew-resolve-" + taskId + " && git branch -D resolve/" + taskId + ".\n" +
|
|
295
|
+
"ESCALATE — return {\"passed\": false, \"summary\": \"conflict needs human resolution\"} — when: 3 attempts are exhausted; the conflict touches generated files, migrations, or public API contracts; or 'looks right + checks pass' is not sufficient for any other reason. On escalation, RELEASE THE LOCK so the task can be reworked later: run CREW_REPO=" + REPO_PATH + " " + MERGE_LOCK + " release " + taskId + " (release is keyed on task id; no PID needed). Do NOT run post-deploy on escalation — it would delete the untouched task branch the human still needs.\n" +
|
|
237
296
|
"- If it contains ERROR, something else failed. Set passed to false.\n\n" +
|
|
238
297
|
"\n" +
|
|
239
298
|
"STEP 2: Push the merged main to the remote repository.\n" +
|
|
@@ -247,28 +306,34 @@ while (i < STEPS.length) {
|
|
|
247
306
|
} else if (step.name === "Publish") {
|
|
248
307
|
if (PUBLISH_TYPE === "npm") {
|
|
249
308
|
// npm packages: immutable release + pack + publish to the registry (push is universal in Integrate)
|
|
309
|
+
// The release decision arrived deterministically from the workflow (release: yes) —
|
|
310
|
+
// these steps are unconditional. There is no decision to make and no skip path.
|
|
250
311
|
instructions = "Publish the npm package to the registry.\n\n" +
|
|
251
|
-
"The
|
|
252
|
-
"
|
|
312
|
+
"The release decision is already made and recorded — it is not yours to make: the accepted Build summary (validated by Review) declares " + releaseDecisionText() + ". Execute every step below in order.\n\n" +
|
|
313
|
+
"The repo push already happened in Integrate — do NOT push to git in this phase except STEP 7, and NEVER force-push.\n\n" +
|
|
253
314
|
"STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
|
|
254
315
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
|
|
255
316
|
"STEP 1: Install and activate the immutable release.\n" +
|
|
256
317
|
"Run: " + RELEASE_SCRIPT + " deploy " + REPO_PATH + "\n" +
|
|
257
318
|
"Verify the output contains INSTALLED and ACTIVATED (or EXISTS and ACTIVATED if unchanged).\n\n" +
|
|
258
|
-
"STEP 2:
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
"STEP
|
|
319
|
+
"STEP 2: Read the registry base version.\n" +
|
|
320
|
+
"Run: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
|
|
321
|
+
"If NOT_FOUND, use the local package.json version as the base instead.\n\n" +
|
|
322
|
+
"STEP 3: Apply the version_bump scope (" + releaseDecision.version_bump + ") to the base version: patch increments the last segment; minor increments the middle and resets the last to 0; major increments the first and resets the rest to 0. Example: base 1.2.3 + minor → 1.3.0. Call the result <new-version>.\n" +
|
|
323
|
+
"STEP 4: Write <new-version> into package.json (only the `version` field), then commit it under the still-held merge lock: cd " + REPO_PATH + " && git add package.json && git commit -m \"release: muse-crew@<new-version>\". The lock serializes Publish per repo, so two tasks can never pick the same version.\n" +
|
|
324
|
+
"STEP 5: Pack and publish.\n" +
|
|
263
325
|
"Run: cd " + REPO_PATH + " && npm pack\n" +
|
|
264
|
-
"Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew
|
|
265
|
-
"If publish fails with 'You cannot publish over the previously published versions',
|
|
266
|
-
"STEP
|
|
326
|
+
"Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-<new-version>.tgz\n" +
|
|
327
|
+
"If publish fails with 'You cannot publish over the previously published versions', <new-version> is already on the registry (a retried Publish — the merge lock guarantees no other task picked this version): continue to STEP 6 verification. Any other publish failure: set passed to false with the failure details.\n\n" +
|
|
328
|
+
"STEP 6: Verify.\n" +
|
|
267
329
|
"Run: npm view muse-crew version\n" +
|
|
268
|
-
"
|
|
269
|
-
"STEP
|
|
330
|
+
"It must equal <new-version>. If not, set passed to false with the mismatch details.\n\n" +
|
|
331
|
+
"STEP 7: Push the version-bump commit: cd " + REPO_PATH + " && git push origin main. Never use --force.\n\n" +
|
|
332
|
+
"STEP 8: Finalize.\n" +
|
|
270
333
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
271
334
|
"If the output contains DEPLOYED, finalization is complete.\n\n" +
|
|
335
|
+
"Do NOT compare the local package.json version to the registry version: with versions assigned at publish time, local==registry is the normal steady state before assignment — not a signal to skip. Execute every step above.\n\n" +
|
|
336
|
+
"End your summary with exactly this line: published: muse-crew@<new-version>\n\n" +
|
|
272
337
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
|
|
273
338
|
"No prose, no markdown, just the JSON object.";
|
|
274
339
|
} else if (PUBLISH_TYPE === "artifact") {
|
|
@@ -305,6 +370,12 @@ while (i < STEPS.length) {
|
|
|
305
370
|
"No prose, no markdown, just the JSON object.";
|
|
306
371
|
}
|
|
307
372
|
} else if (step.name === "QA") {
|
|
373
|
+
// Backstop for merge-time versioning: when the accepted Build summary
|
|
374
|
+
// declared release: yes, QA verifies the registry actually moved. A silent
|
|
375
|
+
// publish skip becomes a loud QA failure with evidence, not a pass.
|
|
376
|
+
var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
|
|
377
|
+
? "NPM PUBLISH CHECK: the accepted Build summary declared release: yes, so this run's Publish phase must have published. Find this task's recorded Publish result: call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }, then find the session for this task_id with step \"Publish\" (status completed) in the returned sessions array and extract its `published: muse-crew@<version>` line from the session notes (the Publish agent ends its summary with exactly that line — event history does NOT carry it). Then run: npm view muse-crew version. The registry version MUST equal the published version. If there is no completed Publish session with a published: line, or the registry version does not match, FAIL with { \"passed\": false, \"summary\": \"npm publish verification failed: [details]\" }.\n"
|
|
378
|
+
: "";
|
|
308
379
|
instructions = "Final QA testing. You are CODE-BLIND — do NOT read source code.\n" +
|
|
309
380
|
"Public docs (API.md, README, published action schemas) are NOT source code — read them freely, exactly as a user would.\n" +
|
|
310
381
|
"DOCS GATE: If the fix is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), verify the public docs describe it. If public docs are missing or stale, FAIL with { \"passed\": false, \"summary\": \"public docs missing/stale for [the change]\" }. QA always fails when public-affecting changes lack public docs. Guide/tutorial gaps are lower priority — file a follow-up task for those instead of failing.\n" +
|
|
@@ -313,6 +384,7 @@ while (i < STEPS.length) {
|
|
|
313
384
|
"You can also check specific data with the getevents action.\n" +
|
|
314
385
|
"Do NOT use artifact_inspect — it is async and will not return results inline.\n" +
|
|
315
386
|
"File follow-up tasks via artifact_invoke_action createtask on slug \"" + DASHBOARD_SLUG + "\" for related issues.\n" +
|
|
387
|
+
npmPublishCheck +
|
|
316
388
|
"If testing passes, your final response MUST be valid JSON and nothing else: { \"passed\": true, \"summary\": \"test results\" }.\n" +
|
|
317
389
|
"If testing fails, your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"failure details\" }.\n" +
|
|
318
390
|
"No prose, no markdown, just the JSON object.";
|
|
@@ -327,9 +399,12 @@ while (i < STEPS.length) {
|
|
|
327
399
|
}
|
|
328
400
|
}
|
|
329
401
|
|
|
330
|
-
// Task event history —
|
|
402
|
+
// Task event history — Review and Publish are excluded. Review is cold by
|
|
403
|
+
// design; Publish receives its release decision deterministically from the
|
|
404
|
+
// workflow, and prior rounds' history contains the exact skip deviation being
|
|
405
|
+
// eliminated — agents imitate recorded precedent.
|
|
331
406
|
var eventPreamble = "";
|
|
332
|
-
if (step.name !== "Review") {
|
|
407
|
+
if (step.name !== "Review" && step.name !== "Publish") {
|
|
333
408
|
eventPreamble = "CONTEXT: First, fetch this task's event history for background.\n" +
|
|
334
409
|
"Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"task_id\": \"" + taskId + "\" }.\n" +
|
|
335
410
|
"The returned events are filtered to this task. They contain notes and decisions from prior phases.\n\n";
|
|
@@ -390,6 +465,13 @@ while (i < STEPS.length) {
|
|
|
390
465
|
mapperSpec = summary;
|
|
391
466
|
}
|
|
392
467
|
|
|
468
|
+
// Capture the accepted Build summary's machine-readable release decision.
|
|
469
|
+
// Parsed from the RAW summary (before the 2000-char slice above) — the
|
|
470
|
+
// release:/version_bump: lines sit at the very end and must survive truncation.
|
|
471
|
+
if (step.name === "Build" && passed) {
|
|
472
|
+
releaseDecision = extractReleaseDecision(stepResult.summary || "");
|
|
473
|
+
}
|
|
474
|
+
|
|
393
475
|
await agent(
|
|
394
476
|
"Update the session and log the event.\n" +
|
|
395
477
|
"Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"upsertagentsession\", args:\n" +
|
package/workflows/chore.js
CHANGED
|
@@ -72,6 +72,24 @@ const MAX_REWORK = 2;
|
|
|
72
72
|
let reworkCount = 0;
|
|
73
73
|
let rejectionNotes = inputs.rejection_notes || "";
|
|
74
74
|
let mapperSpec = "";
|
|
75
|
+
// Merge-time versioning: the release decision is extracted deterministically
|
|
76
|
+
// from the accepted Build summary (extractReleaseDecision) so the Publish
|
|
77
|
+
// agent never decides whether to publish. Two consecutive Publish runs
|
|
78
|
+
// rationalized a skip against explicit instruction text — text alone did not
|
|
79
|
+
// hold, so the decision now lives in workflow code, not agent judgment.
|
|
80
|
+
let releaseDecision = null; // { release: "yes"|"no", version_bump: "patch"|"minor"|"major"|null }
|
|
81
|
+
function extractReleaseDecision(text) {
|
|
82
|
+
const t = text || "";
|
|
83
|
+
const r = /^release:\s*(yes|no)\s*$/im.exec(t);
|
|
84
|
+
if (!r) return null;
|
|
85
|
+
const b = /^version_bump:\s*(patch|minor|major)\s*$/im.exec(t);
|
|
86
|
+
if (r[1].toLowerCase() === "yes" && !b) return null;
|
|
87
|
+
return { release: r[1].toLowerCase(), version_bump: b ? b[1].toLowerCase() : null };
|
|
88
|
+
}
|
|
89
|
+
function releaseDecisionText() {
|
|
90
|
+
if (!releaseDecision) return "no parseable release:/version_bump: decision";
|
|
91
|
+
return "release: " + releaseDecision.release + (releaseDecision.version_bump ? ", version_bump: " + releaseDecision.version_bump : " (no version_bump line)");
|
|
92
|
+
}
|
|
75
93
|
let i = startStepIndex;
|
|
76
94
|
|
|
77
95
|
// Pin lifecycle scripts
|
|
@@ -144,6 +162,28 @@ while (i < STEPS.length) {
|
|
|
144
162
|
return { status: "blocked", task_id: taskId, reason: "Unknown publish target '" + PUBLISH_TYPE + "' — expected 'npm', 'artifact', 'vercel', or empty (skip publish)." };
|
|
145
163
|
}
|
|
146
164
|
|
|
165
|
+
// Merge-time versioning, decided deterministically: the release decision was
|
|
166
|
+
// extracted from the accepted Build summary above. release: no never reaches
|
|
167
|
+
// the Publish agent (the workflow skips it here, exactly like the no-target
|
|
168
|
+
// case); an unparseable decision blocks fail-closed. The Publish agent below
|
|
169
|
+
// therefore has no decision point to rationalize into a skip.
|
|
170
|
+
if (step.name === "Publish" && PUBLISH_TYPE === "npm") {
|
|
171
|
+
if (!releaseDecision) {
|
|
172
|
+
return { status: "blocked", task_id: taskId, reason: "Build summary has no parseable release:/version_bump: lines — cannot assign version at publish time." };
|
|
173
|
+
}
|
|
174
|
+
if (releaseDecision.release === "no") {
|
|
175
|
+
log("Publish skipped for task " + taskId + " — accepted Build summary declared release: no");
|
|
176
|
+
await agent(
|
|
177
|
+
"Release the merge lock and clean up without publishing.\n" +
|
|
178
|
+
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
179
|
+
"If the output contains DEPLOYED, the lock is released and the worktree is cleaned up.",
|
|
180
|
+
{ key: "publish-skip-release-no", label: "Skipping Publish (release: no)", schema: { type: "object" } }
|
|
181
|
+
);
|
|
182
|
+
i++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
147
187
|
let activeSessionId;
|
|
148
188
|
if (isFirstClaim && firstSessionId) {
|
|
149
189
|
activeSessionId = firstSessionId;
|
|
@@ -186,11 +226,15 @@ while (i < STEPS.length) {
|
|
|
186
226
|
"Edit the TypeScript source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
|
|
187
227
|
"Do not add unrequested features.\n" +
|
|
188
228
|
"PUBLIC DOCS: If your change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), update the public docs in the same commit — API.md for API changes. Documentation and implementation ship together.\n\n" +
|
|
189
|
-
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry
|
|
229
|
+
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry. Versions are assigned at PUBLISH time — never in your branch. Do NOT touch the `version` field in package.json (or package-lock). Instead, end your summary with exactly these two lines:\n" +
|
|
230
|
+
"release: yes|no — 'yes' if this change warrants a published release (anything a consumer can observe: workflow behavior, phase lists, identities, published docs, API); 'no' if internal-only.\n" +
|
|
231
|
+
"version_bump: patch|minor|major — patch for fixes (default), minor for new behavior, major for breaking changes. Omit this line only when release is no.\n" +
|
|
232
|
+
"Example: release: yes\\nversion_bump: minor\n\n" : "") +
|
|
190
233
|
"STEP 3: Commit your changes.\n" +
|
|
191
234
|
"cd " + REPO_PATH + "/.worktrees/" + taskId + "\n" +
|
|
192
235
|
"git add -A\n" +
|
|
193
236
|
"git commit -m \"chore: " + safeTitle + "\"\n\n" +
|
|
237
|
+
"If the task's deliverable is runtime state (a cron definition, scheduler change, or dashboard/config state created outside the repo) and the repository genuinely needs no change, do NOT fabricate a commit: leave the branch with no commits ahead of main and declare `repo_diff: none` in your summary, naming the runtime-state deliverable. Otherwise commit your changes normally.\n\n" +
|
|
194
238
|
(rejectionNotes ? "REWORK after rejection. Address:\n" + rejectionNotes + "\n\n" : "") +
|
|
195
239
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"what you built\", \"passed\": true }. No prose, no markdown, just the JSON object.";
|
|
196
240
|
|
|
@@ -204,7 +248,13 @@ while (i < STEPS.length) {
|
|
|
204
248
|
REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
|
|
205
249
|
"Check quality, correctness, spec compliance.\n" +
|
|
206
250
|
"Check that public-affecting changes have matching public doc updates (API.md or the published API contract). If the docs are missing or inaccurate, reject with notes on what is stale.\n" +
|
|
207
|
-
|
|
251
|
+
"If the branch has no commits ahead of main (inspect shows an empty commit log), approve ONLY if the Build summary declares `repo_diff: none` with a plausible runtime-state deliverable (e.g. a cron created via the cron tool). Otherwise reject: 'no commits ahead of main and no repo_diff: none declaration — the builder likely forgot to commit'.\n" +
|
|
252
|
+
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, and versions are assigned at publish time — never in branches. Two checks:\n" +
|
|
253
|
+
"(a) The task branch must NOT have changed package.json's `version` field. Check: cd " + REPO_PATH + " && git diff main...task/" + taskId + " -- package.json. If the branch touched `version` in any way, REJECT with notes: 'versions are assigned at publish time, never in branches — remove the version change'.\n" +
|
|
254
|
+
"(b) The accepted Build summary declares: " + releaseDecisionText() + ". " +
|
|
255
|
+
(releaseDecision
|
|
256
|
+
? "Validate this decision against the change: release must be 'yes' when the change is consumer-observable and 'no' when internal-only; the version_bump scope must fit the change (patch for fixes, minor for new behavior, major for breaking changes). If the decision is wrong or mis-scoped, reject with notes."
|
|
257
|
+
: "The decision is missing or malformed — REJECT with notes: 'Build summary must end with release: yes|no and (when release is yes) version_bump: patch|minor|major lines'.") + "\n" : "") +
|
|
208
258
|
"If it passes, your final response MUST be valid JSON and nothing else: { \"passed\": true, \"summary\": \"approval notes\" }.\n" +
|
|
209
259
|
"If it fails, your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"rejection notes\" }.\n" +
|
|
210
260
|
"No prose, no markdown, just the JSON object.";
|
|
@@ -214,8 +264,17 @@ while (i < STEPS.length) {
|
|
|
214
264
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " integrate " + taskId + " \"merge: chore: " + safeTitle + "\"\n\n" +
|
|
215
265
|
"Read the output:\n" +
|
|
216
266
|
"- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
|
|
267
|
+
"- If it contains MERGED_EMPTY, the branch had no commits ahead of main (a runtime-state deliverable, declared by Build as repo_diff: none). Integration succeeded vacuously: the merge lock was NOT taken and there is no new commit. Set passed to true with summary 'merged empty: no repo changes — deliverable was runtime state'. SKIP STEP 2 (push): there is no new commit to push.\n" +
|
|
217
268
|
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Set passed to false.\n" +
|
|
218
|
-
"- If it contains CONFLICT,
|
|
269
|
+
"- If it contains CONFLICT, the plain merge failed — the merge was aborted, main is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
|
|
270
|
+
"RESOLUTION:\n" +
|
|
271
|
+
"R1. Refresh the merge lock FIRST (a long resolution must not silently lose the lock to the orphan sweep): CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + ". Create a scratch worktree WITH A NEW BRANCH (main is already checked out in the repo checkout, so git forbids checking it out a second time): cd " + REPO_PATH + " && git worktree add -b resolve/" + taskId + " /tmp/crew-resolve-" + taskId + " main. Reproduce the conflict in the scratch worktree: cd /tmp/crew-resolve-" + taskId + " && git merge task/" + taskId + ". This reproduces the exact conflict (main has not moved — the lock was held throughout). The task branch task/" + taskId + " is never modified.\n" +
|
|
272
|
+
"R2. For each conflicted file, read the three sides: git show :1:<file> (base), git show :2:<file> (ours = main), git show :3:<file> (theirs = task branch). Resolve each hunk by keeping both sides' changes when they do not semantically overlap. Version-only hunks resolve to the higher semver (safety net). Never invent new behavior. Leave no markers.\n" +
|
|
273
|
+
"R3. Verify in the scratch worktree, in this order: (a) git diff --check is clean; (b) git grep -n '^<<<<<<<' -- . returns nothing; (c) node --check every changed .js file (conflicted files plus everything listed by git diff --name-only). If any check fails, refresh the merge lock (CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "), then retry the resolution using the failure output as context — max 3 attempts total.\n" +
|
|
274
|
+
"R4. Commit the resolution on resolve/" + taskId + ": git add -A && git commit -m \"resolve conflicts: " + taskId + "\".\n" +
|
|
275
|
+
"R5. Back in " + REPO_PATH + ": git checkout main && git merge --ff-only resolve/" + taskId + ". This fast-forwards — main has not moved while the lock was held. Then STEP 2 applies: git push origin main. Report the merged commit hash.\n" +
|
|
276
|
+
"R6. Clean up: cd " + REPO_PATH + " && git worktree remove --force /tmp/crew-resolve-" + taskId + " && git branch -D resolve/" + taskId + ".\n" +
|
|
277
|
+
"ESCALATE — return {\"passed\": false, \"summary\": \"conflict needs human resolution\"} — when: 3 attempts are exhausted; the conflict touches generated files, migrations, or public API contracts; or 'looks right + checks pass' is not sufficient for any other reason. On escalation, RELEASE THE LOCK so the task can be reworked later: run CREW_REPO=" + REPO_PATH + " " + MERGE_LOCK + " release " + taskId + " (release is keyed on task id; no PID needed). Do NOT run post-deploy on escalation — it would delete the untouched task branch the human still needs.\n" +
|
|
219
278
|
"- If it contains ERROR, something else failed. Set passed to false.\n\n" +
|
|
220
279
|
"\n" +
|
|
221
280
|
"STEP 2: Push the merged main to the remote repository.\n" +
|
|
@@ -229,28 +288,34 @@ while (i < STEPS.length) {
|
|
|
229
288
|
} else if (step.name === "Publish") {
|
|
230
289
|
if (PUBLISH_TYPE === "npm") {
|
|
231
290
|
// npm packages: immutable release + pack + publish to the registry (push is universal in Integrate)
|
|
291
|
+
// The release decision arrived deterministically from the workflow (release: yes) —
|
|
292
|
+
// these steps are unconditional. There is no decision to make and no skip path.
|
|
232
293
|
instructions = "Publish the npm package to the registry.\n\n" +
|
|
233
|
-
"The
|
|
234
|
-
"
|
|
294
|
+
"The release decision is already made and recorded — it is not yours to make: the accepted Build summary (validated by Review) declares " + releaseDecisionText() + ". Execute every step below in order.\n\n" +
|
|
295
|
+
"The repo push already happened in Integrate — do NOT push to git in this phase except STEP 7, and NEVER force-push.\n\n" +
|
|
235
296
|
"STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
|
|
236
297
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
|
|
237
298
|
"STEP 1: Install and activate the immutable release.\n" +
|
|
238
299
|
"Run: " + RELEASE_SCRIPT + " deploy " + REPO_PATH + "\n" +
|
|
239
300
|
"Verify the output contains INSTALLED and ACTIVATED (or EXISTS and ACTIVATED if unchanged).\n\n" +
|
|
240
|
-
"STEP 2:
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"STEP
|
|
301
|
+
"STEP 2: Read the registry base version.\n" +
|
|
302
|
+
"Run: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
|
|
303
|
+
"If NOT_FOUND, use the local package.json version as the base instead.\n\n" +
|
|
304
|
+
"STEP 3: Apply the version_bump scope (" + releaseDecision.version_bump + ") to the base version: patch increments the last segment; minor increments the middle and resets the last to 0; major increments the first and resets the rest to 0. Example: base 1.2.3 + minor → 1.3.0. Call the result <new-version>.\n" +
|
|
305
|
+
"STEP 4: Write <new-version> into package.json (only the `version` field), then commit it under the still-held merge lock: cd " + REPO_PATH + " && git add package.json && git commit -m \"release: muse-crew@<new-version>\". The lock serializes Publish per repo, so two tasks can never pick the same version.\n" +
|
|
306
|
+
"STEP 5: Pack and publish.\n" +
|
|
245
307
|
"Run: cd " + REPO_PATH + " && npm pack\n" +
|
|
246
|
-
"Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew
|
|
247
|
-
"If publish fails with 'You cannot publish over the previously published versions',
|
|
248
|
-
"STEP
|
|
308
|
+
"Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-<new-version>.tgz\n" +
|
|
309
|
+
"If publish fails with 'You cannot publish over the previously published versions', <new-version> is already on the registry (a retried Publish — the merge lock guarantees no other task picked this version): continue to STEP 6 verification. Any other publish failure: set passed to false with the failure details.\n\n" +
|
|
310
|
+
"STEP 6: Verify.\n" +
|
|
249
311
|
"Run: npm view muse-crew version\n" +
|
|
250
|
-
"
|
|
251
|
-
"STEP
|
|
312
|
+
"It must equal <new-version>. If not, set passed to false with the mismatch details.\n\n" +
|
|
313
|
+
"STEP 7: Push the version-bump commit: cd " + REPO_PATH + " && git push origin main. Never use --force.\n\n" +
|
|
314
|
+
"STEP 8: Finalize.\n" +
|
|
252
315
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
253
316
|
"If the output contains DEPLOYED, finalization is complete.\n\n" +
|
|
317
|
+
"Do NOT compare the local package.json version to the registry version: with versions assigned at publish time, local==registry is the normal steady state before assignment — not a signal to skip. Execute every step above.\n\n" +
|
|
318
|
+
"End your summary with exactly this line: published: muse-crew@<new-version>\n\n" +
|
|
254
319
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
|
|
255
320
|
"No prose, no markdown, just the JSON object.";
|
|
256
321
|
} else if (PUBLISH_TYPE === "artifact") {
|
|
@@ -288,9 +353,12 @@ while (i < STEPS.length) {
|
|
|
288
353
|
}
|
|
289
354
|
}
|
|
290
355
|
|
|
291
|
-
// Task event history —
|
|
356
|
+
// Task event history — Review and Publish are excluded. Review is cold by
|
|
357
|
+
// design; Publish receives its release decision deterministically from the
|
|
358
|
+
// workflow, and prior rounds' history contains the exact skip deviation being
|
|
359
|
+
// eliminated — agents imitate recorded precedent.
|
|
292
360
|
var eventPreamble = "";
|
|
293
|
-
if (step.name !== "Review") {
|
|
361
|
+
if (step.name !== "Review" && step.name !== "Publish") {
|
|
294
362
|
eventPreamble = "CONTEXT: First, fetch this task's event history for background.\n" +
|
|
295
363
|
"Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"task_id\": \"" + taskId + "\" }.\n" +
|
|
296
364
|
"The returned events are filtered to this task. They contain notes and decisions from prior phases.\n\n";
|
|
@@ -346,6 +414,13 @@ while (i < STEPS.length) {
|
|
|
346
414
|
mapperSpec = summary;
|
|
347
415
|
}
|
|
348
416
|
|
|
417
|
+
// Capture the accepted Build summary's machine-readable release decision.
|
|
418
|
+
// Parsed from the RAW summary (before the 2000-char slice above) — the
|
|
419
|
+
// release:/version_bump: lines sit at the very end and must survive truncation.
|
|
420
|
+
if (step.name === "Build" && passed) {
|
|
421
|
+
releaseDecision = extractReleaseDecision(stepResult.summary || "");
|
|
422
|
+
}
|
|
423
|
+
|
|
349
424
|
await agent(
|
|
350
425
|
"Update session and log event.\n" +
|
|
351
426
|
"Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"upsertagentsession\", args:\n" +
|
|
@@ -83,8 +83,10 @@ const allTasks = boardResult.ready_tasks || [];
|
|
|
83
83
|
const config = boardResult.config || {};
|
|
84
84
|
const projects = boardResult.projects || [];
|
|
85
85
|
|
|
86
|
-
// Default project: explicit arg,
|
|
87
|
-
|
|
86
|
+
// Default project: explicit arg wins, then the dashboard's configured default
|
|
87
|
+
// project (config.default_project — set by crew-init or the dashboard UI),
|
|
88
|
+
// then the first registered project as a last resort.
|
|
89
|
+
const DEFAULT_PROJECT = inputs.defaultProject || config.default_project || (projects.length > 0 ? projects[0].id : "");
|
|
88
90
|
|
|
89
91
|
// Per-project quiesce: build a set of quiesced project IDs
|
|
90
92
|
const quiescedProjects = {};
|
|
@@ -290,7 +292,7 @@ for (var p = 0; p < toProcess.length; p++) {
|
|
|
290
292
|
crewHome: crewHome
|
|
291
293
|
};
|
|
292
294
|
|
|
293
|
-
log("Claimed " + iworkflow + " for \"" + itask.title + "\" at step " + nextStepName);
|
|
295
|
+
log("Claimed " + iworkflow + " for \"" + itask.title + "\" [" + taskProject + "] at step " + nextStepName);
|
|
294
296
|
results.push({ task_id: itask.id, workflow: iworkflow, step: nextStepName, action: "claimed", scriptPath: scriptPath, args: launchArgs });
|
|
295
297
|
}
|
|
296
298
|
|
package/workflows/crew-init.js
CHANGED
|
@@ -145,6 +145,39 @@ try {
|
|
|
145
145
|
}
|
|
146
146
|
log("Project: " + (projectResult.registered ? "registered" : "failed"));
|
|
147
147
|
|
|
148
|
+
// ── Phase 3b: Set the dashboard's project as the default ─────────────
|
|
149
|
+
// The CLI takes the project implicitly from the dashboard's configured
|
|
150
|
+
// default (config.default_project). Idempotent: setdefaultproject upserts.
|
|
151
|
+
phase("default-project");
|
|
152
|
+
var defaultProjectResult;
|
|
153
|
+
try {
|
|
154
|
+
defaultProjectResult = await agent(
|
|
155
|
+
"Set the dashboard's default project to the dashboard itself.\\n\\n" +
|
|
156
|
+
"Dashboard slug: " + dashboardSlug + "\\n\\n" +
|
|
157
|
+
"Steps:\\n" +
|
|
158
|
+
"1. Call artifact_invoke_action with:\\n" +
|
|
159
|
+
" slug: '" + dashboardSlug + "'\\n" +
|
|
160
|
+
" action_name: 'setdefaultproject'\\n" +
|
|
161
|
+
" args: { project_id: '" + dashboardSlug + "' }\\n" +
|
|
162
|
+
"2. Return { defaulted: true } on success.\\n\\n" +
|
|
163
|
+
"Return JSON with defaulted (boolean).",
|
|
164
|
+
{
|
|
165
|
+
key: "project-2",
|
|
166
|
+
label: "Set default project",
|
|
167
|
+
schema: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: {
|
|
170
|
+
defaulted: { type: "boolean" }
|
|
171
|
+
},
|
|
172
|
+
required: ["defaulted"]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Default project could not be set", message: String(e.message || e) } };
|
|
178
|
+
}
|
|
179
|
+
log("Default project: " + (defaultProjectResult.defaulted ? "set" : "failed"));
|
|
180
|
+
|
|
148
181
|
// ── Phase 4: Polling cron ─────────────────────────────────────────────
|
|
149
182
|
// The cron body template lives in the repo at seed/cron-body-template.md.
|
|
150
183
|
// Placeholders: {crewHome}, {dashboardSlug} are replaced with actual values.
|
package/workflows/standard.js
CHANGED
|
@@ -77,6 +77,24 @@ const MAX_TOTAL_REWORK = 2;
|
|
|
77
77
|
let totalReworkCount = 0;
|
|
78
78
|
let rejectionNotes = inputs.rejection_notes || "";
|
|
79
79
|
let mapperSpec = "";
|
|
80
|
+
// Merge-time versioning: the release decision is extracted deterministically
|
|
81
|
+
// from the accepted Build summary (extractReleaseDecision) so the Publish
|
|
82
|
+
// agent never decides whether to publish. Two consecutive Publish runs
|
|
83
|
+
// rationalized a skip against explicit instruction text — text alone did not
|
|
84
|
+
// hold, so the decision now lives in workflow code, not agent judgment.
|
|
85
|
+
let releaseDecision = null; // { release: "yes"|"no", version_bump: "patch"|"minor"|"major"|null }
|
|
86
|
+
function extractReleaseDecision(text) {
|
|
87
|
+
const t = text || "";
|
|
88
|
+
const r = /^release:\s*(yes|no)\s*$/im.exec(t);
|
|
89
|
+
if (!r) return null;
|
|
90
|
+
const b = /^version_bump:\s*(patch|minor|major)\s*$/im.exec(t);
|
|
91
|
+
if (r[1].toLowerCase() === "yes" && !b) return null;
|
|
92
|
+
return { release: r[1].toLowerCase(), version_bump: b ? b[1].toLowerCase() : null };
|
|
93
|
+
}
|
|
94
|
+
function releaseDecisionText() {
|
|
95
|
+
if (!releaseDecision) return "no parseable release:/version_bump: decision";
|
|
96
|
+
return "release: " + releaseDecision.release + (releaseDecision.version_bump ? ", version_bump: " + releaseDecision.version_bump : " (no version_bump line)");
|
|
97
|
+
}
|
|
80
98
|
let i = startStepIndex;
|
|
81
99
|
|
|
82
100
|
// ── Pin lifecycle scripts ────────────────────────────────────────────
|
|
@@ -158,6 +176,28 @@ while (i < STEPS.length) {
|
|
|
158
176
|
return { status: "blocked", task_id: taskId, reason: "Unknown publish target '" + PUBLISH_TYPE + "' — expected 'npm', 'artifact', 'vercel', or empty (skip publish)." };
|
|
159
177
|
}
|
|
160
178
|
|
|
179
|
+
// Merge-time versioning, decided deterministically: the release decision was
|
|
180
|
+
// extracted from the accepted Build summary above. release: no never reaches
|
|
181
|
+
// the Publish agent (the workflow skips it here, exactly like the no-target
|
|
182
|
+
// case); an unparseable decision blocks fail-closed. The Publish agent below
|
|
183
|
+
// therefore has no decision point to rationalize into a skip.
|
|
184
|
+
if (step.name === "Publish" && PUBLISH_TYPE === "npm") {
|
|
185
|
+
if (!releaseDecision) {
|
|
186
|
+
return { status: "blocked", task_id: taskId, reason: "Build summary has no parseable release:/version_bump: lines — cannot assign version at publish time." };
|
|
187
|
+
}
|
|
188
|
+
if (releaseDecision.release === "no") {
|
|
189
|
+
log("Publish skipped for task " + taskId + " — accepted Build summary declared release: no");
|
|
190
|
+
await agent(
|
|
191
|
+
"Release the merge lock and clean up without publishing.\n" +
|
|
192
|
+
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
193
|
+
"If the output contains DEPLOYED, the lock is released and the worktree is cleaned up.",
|
|
194
|
+
{ key: "publish-skip-release-no", label: "Skipping Publish (release: no)", schema: { type: "object" } }
|
|
195
|
+
);
|
|
196
|
+
i++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
161
201
|
// Claim session — reuse dispatcher's session for the very first step
|
|
162
202
|
let activeSessionId;
|
|
163
203
|
if (isFirstClaim && firstSessionId) {
|
|
@@ -202,11 +242,15 @@ while (i < STEPS.length) {
|
|
|
202
242
|
"Edit source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
|
|
203
243
|
"Do not add unrequested features. Build exactly what the spec calls for.\n" +
|
|
204
244
|
"PUBLIC DOCS: If your change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), update the public docs in the same commit — API.md for API changes. Documentation and implementation ship together.\n\n" +
|
|
205
|
-
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry
|
|
245
|
+
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry. Versions are assigned at PUBLISH time — never in your branch. Do NOT touch the `version` field in package.json (or package-lock). Instead, end your summary with exactly these two lines:\n" +
|
|
246
|
+
"release: yes|no — 'yes' if this change warrants a published release (anything a consumer can observe: workflow behavior, phase lists, identities, published docs, API); 'no' if internal-only.\n" +
|
|
247
|
+
"version_bump: patch|minor|major — patch for fixes (default), minor for new behavior, major for breaking changes. Omit this line only when release is no.\n" +
|
|
248
|
+
"Example: release: yes\\nversion_bump: minor\n\n" : "") +
|
|
206
249
|
"STEP 3: Commit your changes.\n" +
|
|
207
250
|
"cd " + REPO_PATH + "/.worktrees/" + taskId + "\n" +
|
|
208
251
|
"git add -A\n" +
|
|
209
252
|
"git commit -m \"" + safeTitle + "\"\n\n" +
|
|
253
|
+
"If the task's deliverable is runtime state (a cron definition, scheduler change, or dashboard/config state created outside the repo) and the repository genuinely needs no change, do NOT fabricate a commit: leave the branch with no commits ahead of main and declare `repo_diff: none` in your summary, naming the runtime-state deliverable. Otherwise commit your changes normally.\n\n" +
|
|
210
254
|
(rejectionNotes ? "This is REWORK after rejection. Address these specific issues:\n" + rejectionNotes + "\n\n" : "") +
|
|
211
255
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"what you built\", \"passed\": true }. No prose, no markdown, just the JSON object.";
|
|
212
256
|
|
|
@@ -220,7 +264,13 @@ while (i < STEPS.length) {
|
|
|
220
264
|
REPO_PATH + "/.worktrees/" + taskId + "/\n\n" +
|
|
221
265
|
"Check quality, correctness, and spec compliance.\n" +
|
|
222
266
|
"Check that public-affecting changes have matching public doc updates (API.md or the published API contract). If the docs are missing or inaccurate, reject with notes on what is stale.\n" +
|
|
223
|
-
|
|
267
|
+
"If the branch has no commits ahead of main (inspect shows an empty commit log), approve ONLY if the Build summary declares `repo_diff: none` with a plausible runtime-state deliverable (e.g. a cron created via the cron tool). Otherwise reject: 'no commits ahead of main and no repo_diff: none declaration — the builder likely forgot to commit'.\n" +
|
|
268
|
+
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, and versions are assigned at publish time — never in branches. Two checks:\n" +
|
|
269
|
+
"(a) The task branch must NOT have changed package.json's `version` field. Check: cd " + REPO_PATH + " && git diff main...task/" + taskId + " -- package.json. If the branch touched `version` in any way, REJECT with notes: 'versions are assigned at publish time, never in branches — remove the version change'.\n" +
|
|
270
|
+
"(b) The accepted Build summary declares: " + releaseDecisionText() + ". " +
|
|
271
|
+
(releaseDecision
|
|
272
|
+
? "Validate this decision against the change: release must be 'yes' when the change is consumer-observable and 'no' when internal-only; the version_bump scope must fit the change (patch for fixes, minor for new behavior, major for breaking changes). If the decision is wrong or mis-scoped, reject with notes."
|
|
273
|
+
: "The decision is missing or malformed — REJECT with notes: 'Build summary must end with release: yes|no and (when release is yes) version_bump: patch|minor|major lines'.") + "\n" : "") +
|
|
224
274
|
"If the work passes review, your final response MUST be valid JSON and nothing else: { \"passed\": true, \"summary\": \"approval notes\" }.\n" +
|
|
225
275
|
"If the work fails review, your final response MUST be valid JSON and nothing else: { \"passed\": false, \"summary\": \"rejection notes explaining what needs to change\" }.\n" +
|
|
226
276
|
"No prose, no markdown, just the JSON object.";
|
|
@@ -230,8 +280,17 @@ while (i < STEPS.length) {
|
|
|
230
280
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " integrate " + taskId + " \"merge: " + safeTitle + "\"\n\n" +
|
|
231
281
|
"Read the output:\n" +
|
|
232
282
|
"- If it contains MERGED, integration succeeded. Report the merged commit hash.\n" +
|
|
283
|
+
"- If it contains MERGED_EMPTY, the branch had no commits ahead of main (a runtime-state deliverable, declared by Build as repo_diff: none). Integration succeeded vacuously: the merge lock was NOT taken and there is no new commit. Set passed to true with summary 'merged empty: no repo changes — deliverable was runtime state'. SKIP STEP 2 (push): there is no new commit to push.\n" +
|
|
233
284
|
"- If it contains LOCK_HELD, another task holds the merge lock (mid Integrate/Publish). Set passed to false with summary 'merge lock held'.\n" +
|
|
234
|
-
"- If it contains CONFLICT,
|
|
285
|
+
"- If it contains CONFLICT, the plain merge failed — the merge was aborted, main is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
|
|
286
|
+
"RESOLUTION:\n" +
|
|
287
|
+
"R1. Refresh the merge lock FIRST (a long resolution must not silently lose the lock to the orphan sweep): CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + ". Create a scratch worktree WITH A NEW BRANCH (main is already checked out in the repo checkout, so git forbids checking it out a second time): cd " + REPO_PATH + " && git worktree add -b resolve/" + taskId + " /tmp/crew-resolve-" + taskId + " main. Reproduce the conflict in the scratch worktree: cd /tmp/crew-resolve-" + taskId + " && git merge task/" + taskId + ". This reproduces the exact conflict (main has not moved — the lock was held throughout). The task branch task/" + taskId + " is never modified.\n" +
|
|
288
|
+
"R2. For each conflicted file, read the three sides: git show :1:<file> (base), git show :2:<file> (ours = main), git show :3:<file> (theirs = task branch). Resolve each hunk by keeping both sides' changes when they do not semantically overlap. Version-only hunks resolve to the higher semver (safety net). Never invent new behavior. Leave no markers.\n" +
|
|
289
|
+
"R3. Verify in the scratch worktree, in this order: (a) git diff --check is clean; (b) git grep -n '^<<<<<<<' -- . returns nothing; (c) node --check every changed .js file (conflicted files plus everything listed by git diff --name-only). If any check fails, refresh the merge lock (CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "), then retry the resolution using the failure output as context — max 3 attempts total.\n" +
|
|
290
|
+
"R4. Commit the resolution on resolve/" + taskId + ": git add -A && git commit -m \"resolve conflicts: " + taskId + "\".\n" +
|
|
291
|
+
"R5. Back in " + REPO_PATH + ": git checkout main && git merge --ff-only resolve/" + taskId + ". This fast-forwards — main has not moved while the lock was held. Then STEP 2 applies: git push origin main. Report the merged commit hash.\n" +
|
|
292
|
+
"R6. Clean up: cd " + REPO_PATH + " && git worktree remove --force /tmp/crew-resolve-" + taskId + " && git branch -D resolve/" + taskId + ".\n" +
|
|
293
|
+
"ESCALATE — return {\"passed\": false, \"summary\": \"conflict needs human resolution\"} — when: 3 attempts are exhausted; the conflict touches generated files, migrations, or public API contracts; or 'looks right + checks pass' is not sufficient for any other reason. On escalation, RELEASE THE LOCK so the task can be reworked later: run CREW_REPO=" + REPO_PATH + " " + MERGE_LOCK + " release " + taskId + " (release is keyed on task id; no PID needed). Do NOT run post-deploy on escalation — it would delete the untouched task branch the human still needs.\n" +
|
|
235
294
|
"- If it contains ERROR, something else failed. Set passed to false with the error.\n\n" +
|
|
236
295
|
"\n" +
|
|
237
296
|
"STEP 2: Push the merged main to the remote repository.\n" +
|
|
@@ -245,28 +304,34 @@ while (i < STEPS.length) {
|
|
|
245
304
|
} else if (step.name === "Publish") {
|
|
246
305
|
if (PUBLISH_TYPE === "npm") {
|
|
247
306
|
// npm packages: immutable release + pack + publish to the registry (push is universal in Integrate)
|
|
307
|
+
// The release decision arrived deterministically from the workflow (release: yes) —
|
|
308
|
+
// these steps are unconditional. There is no decision to make and no skip path.
|
|
248
309
|
instructions = "Publish the npm package to the registry.\n\n" +
|
|
249
|
-
"The
|
|
250
|
-
"
|
|
310
|
+
"The release decision is already made and recorded — it is not yours to make: the accepted Build summary (validated by Review) declares " + releaseDecisionText() + ". Execute every step below in order.\n\n" +
|
|
311
|
+
"The repo push already happened in Integrate — do NOT push to git in this phase except STEP 7, and NEVER force-push.\n\n" +
|
|
251
312
|
"STEP 0: Refresh the merge lock to prevent stale-lock breaking during publish.\n" +
|
|
252
313
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " refresh-lock " + taskId + "\n\n" +
|
|
253
314
|
"STEP 1: Install and activate the immutable release.\n" +
|
|
254
315
|
"Run: " + RELEASE_SCRIPT + " deploy " + REPO_PATH + "\n" +
|
|
255
316
|
"Verify the output contains INSTALLED and ACTIVATED (or EXISTS and ACTIVATED if unchanged).\n\n" +
|
|
256
|
-
"STEP 2:
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
"STEP
|
|
317
|
+
"STEP 2: Read the registry base version.\n" +
|
|
318
|
+
"Run: npm view muse-crew version 2>/dev/null || echo NOT_FOUND\n" +
|
|
319
|
+
"If NOT_FOUND, use the local package.json version as the base instead.\n\n" +
|
|
320
|
+
"STEP 3: Apply the version_bump scope (" + releaseDecision.version_bump + ") to the base version: patch increments the last segment; minor increments the middle and resets the last to 0; major increments the first and resets the rest to 0. Example: base 1.2.3 + minor → 1.3.0. Call the result <new-version>.\n" +
|
|
321
|
+
"STEP 4: Write <new-version> into package.json (only the `version` field), then commit it under the still-held merge lock: cd " + REPO_PATH + " && git add package.json && git commit -m \"release: muse-crew@<new-version>\". The lock serializes Publish per repo, so two tasks can never pick the same version.\n" +
|
|
322
|
+
"STEP 5: Pack and publish.\n" +
|
|
261
323
|
"Run: cd " + REPO_PATH + " && npm pack\n" +
|
|
262
|
-
"Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew
|
|
263
|
-
"If publish fails with 'You cannot publish over the previously published versions',
|
|
264
|
-
"STEP
|
|
324
|
+
"Then publish: python3 ~/workspace/skills/npm/bin/npm-publish.py " + REPO_PATH + "/muse-crew-<new-version>.tgz\n" +
|
|
325
|
+
"If publish fails with 'You cannot publish over the previously published versions', <new-version> is already on the registry (a retried Publish — the merge lock guarantees no other task picked this version): continue to STEP 6 verification. Any other publish failure: set passed to false with the failure details.\n\n" +
|
|
326
|
+
"STEP 6: Verify.\n" +
|
|
265
327
|
"Run: npm view muse-crew version\n" +
|
|
266
|
-
"
|
|
267
|
-
"STEP
|
|
328
|
+
"It must equal <new-version>. If not, set passed to false with the mismatch details.\n\n" +
|
|
329
|
+
"STEP 7: Push the version-bump commit: cd " + REPO_PATH + " && git push origin main. Never use --force.\n\n" +
|
|
330
|
+
"STEP 8: Finalize.\n" +
|
|
268
331
|
"Run: CREW_REPO=" + REPO_PATH + " " + LIFECYCLE + " post-deploy " + taskId + "\n" +
|
|
269
332
|
"If the output contains DEPLOYED, finalization is complete.\n\n" +
|
|
333
|
+
"Do NOT compare the local package.json version to the registry version: with versions assigned at publish time, local==registry is the normal steady state before assignment — not a signal to skip. Execute every step above.\n\n" +
|
|
334
|
+
"End your summary with exactly this line: published: muse-crew@<new-version>\n\n" +
|
|
270
335
|
"Your final response MUST be valid JSON and nothing else: { \"summary\": \"result\", \"passed\": true }.\n" +
|
|
271
336
|
"No prose, no markdown, just the JSON object.";
|
|
272
337
|
} else if (PUBLISH_TYPE === "artifact") {
|
|
@@ -303,6 +368,12 @@ while (i < STEPS.length) {
|
|
|
303
368
|
"No prose, no markdown, just the JSON object.";
|
|
304
369
|
}
|
|
305
370
|
} else if (step.name === "QA") {
|
|
371
|
+
// Backstop for merge-time versioning: when the accepted Build summary
|
|
372
|
+
// declared release: yes, QA verifies the registry actually moved. A silent
|
|
373
|
+
// publish skip becomes a loud QA failure with evidence, not a pass.
|
|
374
|
+
var npmPublishCheck = (PUBLISH_TYPE === "npm" && releaseDecision && releaseDecision.release === "yes")
|
|
375
|
+
? "NPM PUBLISH CHECK: the accepted Build summary declared release: yes, so this run's Publish phase must have published. Find this task's recorded Publish result: call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getstate\", args: { \"events_limit\": 1 }, then find the session for this task_id with step \"Publish\" (status completed) in the returned sessions array and extract its `published: muse-crew@<version>` line from the session notes (the Publish agent ends its summary with exactly that line — event history does NOT carry it). Then run: npm view muse-crew version. The registry version MUST equal the published version. If there is no completed Publish session with a published: line, or the registry version does not match, FAIL with { \"passed\": false, \"summary\": \"npm publish verification failed: [details]\" }.\n"
|
|
376
|
+
: "";
|
|
306
377
|
if (PUBLISH_TYPE === "artifact") {
|
|
307
378
|
var safeDesc = taskDescription.replace(/"/g, "'").replace(/\\/g, "\\\\").slice(0, 500);
|
|
308
379
|
instructions = "You are code-blind QA. You NEVER read source files.\n" +
|
|
@@ -332,14 +403,18 @@ while (i < STEPS.length) {
|
|
|
332
403
|
"Verify the change is working as described in the task.\n" +
|
|
333
404
|
"DOCS GATE: If the change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), verify the public docs describe it. If public docs are missing or stale, FAIL with { \"passed\": false, \"summary\": \"public docs missing/stale for [the change]\" }. QA always fails when public-affecting changes lack public docs. Guide/tutorial gaps are lower priority — file a follow-up task for those instead of failing.\n" +
|
|
334
405
|
"File follow-up tasks via artifact_invoke_action createtask on slug \"" + DASHBOARD_SLUG + "\" for related issues found.\n\n" +
|
|
406
|
+
npmPublishCheck +
|
|
335
407
|
"Your final response MUST be valid JSON and nothing else: { \"passed\": true/false, \"summary\": \"what you tested and found\" }.\n" +
|
|
336
408
|
"No prose, no markdown, just the JSON object.";
|
|
337
409
|
}
|
|
338
410
|
}
|
|
339
411
|
|
|
340
|
-
// Task event history —
|
|
412
|
+
// Task event history — Review and Publish are excluded. Review is cold by
|
|
413
|
+
// design; Publish receives its release decision deterministically from the
|
|
414
|
+
// workflow, and prior rounds' history contains the exact skip deviation being
|
|
415
|
+
// eliminated — agents imitate recorded precedent.
|
|
341
416
|
var eventPreamble = "";
|
|
342
|
-
if (step.name !== "Review") {
|
|
417
|
+
if (step.name !== "Review" && step.name !== "Publish") {
|
|
343
418
|
eventPreamble = "CONTEXT: First, fetch this task's event history for background.\n" +
|
|
344
419
|
"Call artifact_invoke_action on slug \"" + DASHBOARD_SLUG + "\", action \"getevents\", args: { \"task_id\": \"" + taskId + "\" }.\n" +
|
|
345
420
|
"The returned events are filtered to this task. They contain notes and decisions from prior phases.\n\n";
|
|
@@ -401,6 +476,13 @@ while (i < STEPS.length) {
|
|
|
401
476
|
mapperSpec = summary;
|
|
402
477
|
}
|
|
403
478
|
|
|
479
|
+
// Capture the accepted Build summary's machine-readable release decision.
|
|
480
|
+
// Parsed from the RAW summary (before the 2000-char slice above) — the
|
|
481
|
+
// release:/version_bump: lines sit at the very end and must survive truncation.
|
|
482
|
+
if (step.name === "Build" && passed) {
|
|
483
|
+
releaseDecision = extractReleaseDecision(stepResult.summary || "");
|
|
484
|
+
}
|
|
485
|
+
|
|
404
486
|
// Record session result
|
|
405
487
|
await agent(
|
|
406
488
|
"Update the session and log the event.\n" +
|