muse-crew 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/AGENTS.md +4 -1
- package/lib/advance-publish-base.js +74 -28
- package/lib/crew-api.js +8 -0
- package/lib/scaffold-crew.js +129 -0
- package/package.json +1 -1
- package/workflows/bugfix.js +7 -13
- package/workflows/chore.js +6 -12
- package/workflows/crew-dispatch.js +43 -8
- package/workflows/crew-init.js +333 -78
- package/workflows/crew-uninstall.js +57 -10
- package/workflows/standard.js +5 -7
- package/workflows/upgrade.js +27 -13
package/lib/AGENTS.md
CHANGED
|
@@ -36,4 +36,7 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
36
36
|
- `sample-project.js` — optional sample-project setup (2026-09-17): extracts `seed/sample-project.tar.gz` (stripping the `sample-project/` prefix), `git init`, initial commit. No remote required. Registration happens via the existing `create-project` action — no sample-specific endpoint. CLI: `--tarball <path> --dest <dir> --name <project-name>`.
|
|
37
37
|
- `compute-publish-diff.js` — deterministic publish-diff computer (2026-09-18, room #14): replaces the agent-ferried `git diff` JSON string field (the JSON ferry dropped a valid 700-line diff — "Publish diff parsed to zero files"). Runs `git diff <base> <HEAD>` via argv (no shell), writes the raw diff bytes to `--out`, and prints only a small JSON summary (`commit`, `base`, `diff_file`, `sha256`, `bytes`, `file_count`, `changed_lines` = added+removed, never raw diff output lines, `files`, `has_binary`, `has_rename`). Empty-tree base is the first-publish ancestor (hardcoded, never `merge-base` on the empty tree); non-ancestor is semantic JSON (`ok:false, error:"not-ancestor"`, exit 0); usage errors exit 2 with nothing on stdout. Pinned per-run alongside the other lifecycle files (`COMPUTE_DIFF` in `PIN_BASENAMES`) so a mid-run release swap cannot change it under the workflow.
|
|
38
38
|
- `classify-surface.js` — deterministic project-surface classifier (2026-09-18, room #14 defect): a pure, never-throwing scan of the registered repo — `space.json` present → `artifact` surface (deploy_slug suggestion = `space.json`'s valid slug, verbatim — presence alone still means "web app"), `package.json` with a non-empty `bin` → `terminal` surface, otherwise null (unclassified); artifact wins conflicts (the deployed artifact is what users see, same rule as workflows/standard.js). CLI `--repo-path` prints one JSON object (`{surface, deploy_slug, signals}`), always exit 0 (classification failure is a null result, exit 2 only for usage). `create-project` fills omitted `environment_type`/`deploy_type`/`deploy_slug` from the classification; explicit values (including explicit null) always win and drive the effective surface.
|
|
39
|
-
- `advance-publish-base.js` — publish-base advance past the setup scaffold (2026-09-18, room #14): `create-project` stamps provenance at the pre-scaffold repo HEAD, then crew-init commits `.orchestration/` (+ the consented `.gitignore`) on top — the first task's publish diff would carry the whole scaffold (517 insertions) and blow the 200-line diff-transport budget. The script
|
|
39
|
+
- `advance-publish-base.js` — publish-base advance past the setup scaffold (2026-09-18, room #14; testimony-free 2026-09-19 REVIEW): `create-project` stamps provenance at the pre-scaffold repo HEAD, then crew-init commits `.orchestration/` (+ the consented `.gitignore`) on top — the first task's publish diff would carry the whole scaffold (517 insertions) and blow the 200-line diff-transport budget. The script resolves the scaffold commit itself (`git rev-parse HEAD` in `--repo`; `--scaffold-sha` is an explicit override only) and moves `source_commit` to it only when every fail-closed condition holds deterministically: the existing stamp is not a real publish's (provenance.task_id is set by every real publish and null for initial stamps/base advances — the task_id invariant documented on stampProvenance; the old `--project-created` testimony flag is gone), the scaffold commit's parent is exactly the stamped base, and the scaffold touches only crew-owned paths (`.orchestration/`, `.gitignore`); no provenance + root scaffold commit stamps fresh. Every other case skips without touching provenance; `published_at` is preserved on advance. Every stdout JSON object also carries `repo_clean` (informational — whether `git status --porcelain` is empty — so the caller can warn loudly about a dirty tree; the first task's Integrate refuses to merge onto one). Called from crew-init.js (best-effort, non-fatal). Provenance is per-project (room #15 blocker 8): `--project-id` is required and every read/stamp is project-scoped. CLI: `--crew-home <path> --repo <path> --project-id <id> [--scaffold-sha <sha>]`.
|
|
40
|
+
- `scaffold-crew.js` — deterministic crew-home scaffold composer (2026-09-19, Room #19 D1): replaces the agent-authored scaffold report (the agent returned prose instead of the schema'd JSON and the platform rejected it, killing room #19's init on minute one — the copying itself always succeeded). The script copies the fixed file sets (identities *.md+*.png, personas *.md, seed/workflows *.md, seed/feedback/README.md) into `<orchDir>/{identities,personas,workflows,feedback}/` with mkdir -p + no-clobber semantics (existing files never overwritten), verifies every expected file on disk (the decision rule), then writes `<orchDir>/.scaffold-receipt.json` `{created, skipped}` (integer counts, durable evidence) and prints the same JSON to stdout. Idempotent: all-skipped is exit 0. Exit 1 with the reason on stderr on any failure; exit 2 on usage errors. crew-init's scaffold-1 call is a byte courier only — it runs this script and prints the receipt's exact bytes; a paraphrased ferry fails the mechanical parse loudly as a blocked init (tripwire, never a fabricated 0/0). The receipt bytes are the agent's report of script output, not proof the script ran: crew-init corroborates with a witness re-run of this same idempotent composer (a witness that still has files to create proves the ferry's work didn't land; the witness's own disk-verification then holds the end state). Fail-closed source guards (2026-09-19 REVIEW): a missing
|
|
41
|
+
or empty identities/, personas/, or seed/workflows/ source refuses loudly (exit 1,
|
|
42
|
+
naming the source) before creating anything — never an empty 0/0 receipt. No wall-clock reads, no randomness.
|
|
@@ -10,8 +10,14 @@
|
|
|
10
10
|
//
|
|
11
11
|
// This script moves provenance.source_commit to the scaffold commit only when
|
|
12
12
|
// every fail-closed condition holds:
|
|
13
|
-
// 1.
|
|
14
|
-
//
|
|
13
|
+
// 1. The existing stamp is not a real publish's: provenance.task_id is set
|
|
14
|
+
// by every real publish (lib/verify-publish.js and the workflow
|
|
15
|
+
// provenance-refresh paths) and is null for initial stamps and for this
|
|
16
|
+
// script's own base advances. A stamp with task_id set is never moved.
|
|
17
|
+
// (2026-09-19 REVIEW: this deterministic check replaces the old
|
|
18
|
+
// --project-created flag, which was agent testimony ferried through the
|
|
19
|
+
// workflow. The task_id invariant is documented on stampProvenance in
|
|
20
|
+
// lib/crew-api.js.)
|
|
15
21
|
// 2. The scaffold commit's parent IS the stamped source_commit: nothing
|
|
16
22
|
// slipped between the stamp and the scaffold, so no un-published task
|
|
17
23
|
// work can be silently dropped from a future diff.
|
|
@@ -19,10 +25,17 @@
|
|
|
19
25
|
// (.orchestration/ or .gitignore).
|
|
20
26
|
// 4. Special case: no provenance was ever stamped (empty repo at create
|
|
21
27
|
// time) and the scaffold commit is the root commit -> stamp fresh.
|
|
28
|
+
// The scaffold commit is resolved by this script itself (git rev-parse HEAD
|
|
29
|
+
// in --repo) unless --scaffold-sha overrides it — the caller never ferries
|
|
30
|
+
// the sha through agent testimony (2026-09-19 REVIEW: agent-mechanic boundary
|
|
31
|
+
// razor — consequential parameters come from deterministic reads, not
|
|
32
|
+
// testimony).
|
|
22
33
|
// Anything else yields { ok: true, action: "skipped", reason } — never an
|
|
23
34
|
// error, never a clobber. stdout carries exactly one JSON object; operational
|
|
24
35
|
// failures go to stderr with a non-zero exit (the caller treats failure as
|
|
25
|
-
// best-effort: init continues with the stamped base).
|
|
36
|
+
// best-effort: init continues with the stamped base). Every JSON object also
|
|
37
|
+
// carries repo_clean (boolean): whether `git status --porcelain` in --repo
|
|
38
|
+
// is empty — informational for the caller, never a gate.
|
|
26
39
|
//
|
|
27
40
|
// No wall-clock reads, no randomness — the published_at on a fresh stamp is
|
|
28
41
|
// left to crew-api's default; on an advance the existing published_at is
|
|
@@ -39,7 +52,7 @@ const RELEASE_RE = /^([0-9a-f]{40}|pkg-\d+\.\d+\.\d+)$/;
|
|
|
39
52
|
|
|
40
53
|
function usage() {
|
|
41
54
|
console.error(
|
|
42
|
-
"Usage: node lib/advance-publish-base.js --crew-home <path> --repo <path> --
|
|
55
|
+
"Usage: node lib/advance-publish-base.js --crew-home <path> --repo <path> --project-id <id> [--scaffold-sha <sha>]"
|
|
43
56
|
);
|
|
44
57
|
}
|
|
45
58
|
|
|
@@ -51,7 +64,6 @@ function parseArgv(argv) {
|
|
|
51
64
|
else if (a === "--repo") out.repo = argv[++i];
|
|
52
65
|
else if (a === "--scaffold-sha") out.scaffoldSha = argv[++i];
|
|
53
66
|
else if (a === "--project-id") out.projectId = argv[++i];
|
|
54
|
-
else if (a === "--project-created") out.projectCreated = true;
|
|
55
67
|
else {
|
|
56
68
|
console.error("unknown argument: " + a);
|
|
57
69
|
usage();
|
|
@@ -90,25 +102,47 @@ function isCrewOwnedPath(p) {
|
|
|
90
102
|
return p === ".gitignore" || p === ".orchestration" || p.startsWith(".orchestration/");
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
function skip(reason) {
|
|
94
|
-
console.log(JSON.stringify({ ok: true, action: "skipped", reason }));
|
|
105
|
+
function skip(reason, repoClean) {
|
|
106
|
+
console.log(JSON.stringify({ ok: true, action: "skipped", reason, repo_clean: repoClean }));
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
function main() {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (!crewHome || !repo || !
|
|
110
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
111
|
+
const { crewHome, repo, projectId } = parsed;
|
|
112
|
+
let scaffoldSha = parsed.scaffoldSha || null;
|
|
113
|
+
if (!crewHome || !repo || !projectId) {
|
|
102
114
|
usage();
|
|
103
115
|
process.exit(2);
|
|
104
116
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
// The scaffold commit is resolved by this script, not ferried through the
|
|
118
|
+
// caller: --scaffold-sha is an explicit override; otherwise HEAD is read
|
|
119
|
+
// directly from the repo.
|
|
120
|
+
if (scaffoldSha) {
|
|
121
|
+
if (!SHA_RE.test(scaffoldSha)) {
|
|
122
|
+
console.error("scaffold-sha is not a 40-char hex sha");
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
try {
|
|
127
|
+
scaffoldSha = git(repo, ["rev-parse", "HEAD"]);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error("rev-parse HEAD failed: " + (e.message || e));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (!SHA_RE.test(scaffoldSha)) {
|
|
133
|
+
console.error("rev-parse HEAD did not print a 40-char hex sha");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
108
136
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
|
|
138
|
+
// Informational for the caller (never a gate): whether the repo worktree
|
|
139
|
+
// is clean. The caller warns loudly on a dirty tree — the first task's
|
|
140
|
+
// Integrate refuses to merge onto one.
|
|
141
|
+
let repoClean = null;
|
|
142
|
+
try {
|
|
143
|
+
repoClean = git(repo, ["status", "--porcelain"]) === "";
|
|
144
|
+
} catch (e) {
|
|
145
|
+
repoClean = null;
|
|
112
146
|
}
|
|
113
147
|
|
|
114
148
|
let provenance;
|
|
@@ -119,6 +153,19 @@ function main() {
|
|
|
119
153
|
process.exit(1);
|
|
120
154
|
}
|
|
121
155
|
|
|
156
|
+
// Deterministic, not testimony: every real publish stamps provenance with
|
|
157
|
+
// its task_id (see the task_id invariant on stampProvenance in
|
|
158
|
+
// lib/crew-api.js); initial stamps and base advances leave it null. A
|
|
159
|
+
// publish's base is never moved.
|
|
160
|
+
if (provenance && provenance.task_id) {
|
|
161
|
+
skip(
|
|
162
|
+
"provenance was stamped by a real publish (task_id " + provenance.task_id +
|
|
163
|
+
") — a publish's base is never moved",
|
|
164
|
+
repoClean
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
122
169
|
// The scaffold commit's parent: empty output means S is the root commit.
|
|
123
170
|
let parent;
|
|
124
171
|
try {
|
|
@@ -144,8 +191,7 @@ function main() {
|
|
|
144
191
|
skip(
|
|
145
192
|
"scaffold commit touches non-crew-owned paths (" +
|
|
146
193
|
foreign.slice(0, 5).join(", ") +
|
|
147
|
-
") — provenance left untouched"
|
|
148
|
-
);
|
|
194
|
+
") — provenance left untouched", repoClean);
|
|
149
195
|
return;
|
|
150
196
|
}
|
|
151
197
|
|
|
@@ -154,18 +200,18 @@ function main() {
|
|
|
154
200
|
// scaffold commit is the root commit — anything else means history we
|
|
155
201
|
// did not create, and we do not invent a base over it.
|
|
156
202
|
if (parent !== null) {
|
|
157
|
-
skip("no provenance stamped and scaffold commit is not the root commit — provenance left untouched");
|
|
203
|
+
skip("no provenance stamped and scaffold commit is not the root commit — provenance left untouched", repoClean);
|
|
158
204
|
return;
|
|
159
205
|
}
|
|
160
206
|
let crewRelease = null;
|
|
161
207
|
try {
|
|
162
208
|
crewRelease = resolveActiveRelease(crewHome);
|
|
163
209
|
} catch (e) {
|
|
164
|
-
skip("no provenance stamped and the active crew release is unresolvable (" + (e.message || e) + ")");
|
|
210
|
+
skip("no provenance stamped and the active crew release is unresolvable (" + (e.message || e) + ")", repoClean);
|
|
165
211
|
return;
|
|
166
212
|
}
|
|
167
213
|
if (!RELEASE_RE.test(crewRelease)) {
|
|
168
|
-
skip("no provenance stamped and the active release name is not a valid crew_release (" + crewRelease + ")");
|
|
214
|
+
skip("no provenance stamped and the active release name is not a valid crew_release (" + crewRelease + ")", repoClean);
|
|
169
215
|
return;
|
|
170
216
|
}
|
|
171
217
|
let stamped;
|
|
@@ -183,17 +229,17 @@ function main() {
|
|
|
183
229
|
console.error("set-provenance did not return ok");
|
|
184
230
|
process.exit(1);
|
|
185
231
|
}
|
|
186
|
-
console.log(JSON.stringify({ ok: true, action: "stamped", to: scaffoldSha.slice(0, 12) }));
|
|
232
|
+
console.log(JSON.stringify({ ok: true, action: "stamped", to: scaffoldSha.slice(0, 12), repo_clean: repoClean }));
|
|
187
233
|
return;
|
|
188
234
|
}
|
|
189
235
|
|
|
190
236
|
const base = provenance.source_commit;
|
|
191
237
|
if (!SHA_RE.test(base || "")) {
|
|
192
|
-
skip("existing provenance.source_commit is not a 40-char hex sha — provenance left untouched");
|
|
238
|
+
skip("existing provenance.source_commit is not a 40-char hex sha — provenance left untouched", repoClean);
|
|
193
239
|
return;
|
|
194
240
|
}
|
|
195
241
|
if (base === scaffoldSha) {
|
|
196
|
-
skip("provenance already points at the scaffold commit");
|
|
242
|
+
skip("provenance already points at the scaffold commit", repoClean);
|
|
197
243
|
return;
|
|
198
244
|
}
|
|
199
245
|
if (parent !== base) {
|
|
@@ -202,8 +248,7 @@ function main() {
|
|
|
202
248
|
String(parent || "root").slice(0, 12) +
|
|
203
249
|
") is not the stamped base (" +
|
|
204
250
|
base.slice(0, 12) +
|
|
205
|
-
") — provenance left untouched"
|
|
206
|
-
);
|
|
251
|
+
") — provenance left untouched", repoClean);
|
|
207
252
|
return;
|
|
208
253
|
}
|
|
209
254
|
|
|
@@ -219,7 +264,7 @@ function main() {
|
|
|
219
264
|
}
|
|
220
265
|
}
|
|
221
266
|
if (!crewRelease) {
|
|
222
|
-
skip("no usable crew_release for the re-stamp — provenance left untouched");
|
|
267
|
+
skip("no usable crew_release for the re-stamp — provenance left untouched", repoClean);
|
|
223
268
|
return;
|
|
224
269
|
}
|
|
225
270
|
|
|
@@ -259,6 +304,7 @@ function main() {
|
|
|
259
304
|
action: "advanced",
|
|
260
305
|
from: base.slice(0, 12),
|
|
261
306
|
to: scaffoldSha.slice(0, 12),
|
|
307
|
+
repo_clean: repoClean,
|
|
262
308
|
})
|
|
263
309
|
);
|
|
264
310
|
}
|
package/lib/crew-api.js
CHANGED
|
@@ -2625,6 +2625,14 @@ commands["update-config"] = (db, args) => {
|
|
|
2625
2625
|
// projects carries N independent publish bases, and stamping one project
|
|
2626
2626
|
// never touches another's row.
|
|
2627
2627
|
function stampProvenance(db, { project_id, source_commit, crew_release, published_at, task_id }) {
|
|
2628
|
+
// task_id invariant (2026-09-19 REVIEW): task_id is set by every REAL
|
|
2629
|
+
// publish (lib/verify-publish.js and the workflow provenance-refresh paths)
|
|
2630
|
+
// and is null for initial stamps (tryStampInitialProvenance) and for
|
|
2631
|
+
// publish-base advances (lib/advance-publish-base.js). So
|
|
2632
|
+
// provenance.task_id != null ⟺ a real publish has stamped this project.
|
|
2633
|
+
// lib/advance-publish-base.js relies on this to deterministically refuse to
|
|
2634
|
+
// move a publish's base. Keep it true: any new real-publish path must pass
|
|
2635
|
+
// its task_id; anything that is not a publish must leave it null.
|
|
2628
2636
|
const pid = String(project_id ?? "").trim();
|
|
2629
2637
|
if (!pid) throw usageError("project_id is required — pass the project's id (see list-projects).");
|
|
2630
2638
|
requireProject(db, pid);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deterministic crew-home scaffold composer (2026-09-19, Room #19 D1).
|
|
3
|
+
//
|
|
4
|
+
// The old crew-init scaffold phase asked an agent to copy files AND report
|
|
5
|
+
// what it copied as JSON. The copying always worked; the JSON return didn't
|
|
6
|
+
// (room #19: the agent returned prose, the platform rejected it as non-JSON,
|
|
7
|
+
// init died on minute one). The step-back: copying files is deterministic
|
|
8
|
+
// work — no agent should be holding our data. This script does the copies,
|
|
9
|
+
// counts them, verifies every file on disk, and writes a receipt. The
|
|
10
|
+
// workflow consumes only this script's bytes; the agent's return is never
|
|
11
|
+
// parsed for data.
|
|
12
|
+
//
|
|
13
|
+
// CLI: node scaffold-crew.js --repo <crewRepoPath> --orch-dir <orchDir>
|
|
14
|
+
// Prints the receipt JSON {"created":n,"skipped":m} to stdout and writes it
|
|
15
|
+
// to <orchDir>/.scaffold-receipt.json (durable evidence). Exit 0 on success
|
|
16
|
+
// (idempotent: all-skipped is success). Exit 1 with the reason on stderr on
|
|
17
|
+
// any failure — including a missing or empty identities/personas/seed-workflows
|
|
18
|
+
// source (fail-closed: never stamp an empty 0/0 receipt). Exit 2 on usage
|
|
19
|
+
// errors. No wall-clock reads, no randomness.
|
|
20
|
+
|
|
21
|
+
const fs = require("node:fs");
|
|
22
|
+
const path = require("node:path");
|
|
23
|
+
|
|
24
|
+
function usageError(msg) {
|
|
25
|
+
process.stderr.write("scaffold-crew.js usage: --repo <crewRepoPath> --orch-dir <orchDir>\n" + msg + "\n");
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
let repo = null;
|
|
31
|
+
let orchDir = null;
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
if (args[i] === "--repo" && i + 1 < args.length) repo = args[++i];
|
|
34
|
+
else if (args[i] === "--orch-dir" && i + 1 < args.length) orchDir = args[++i];
|
|
35
|
+
else usageError("unknown or incomplete argument: " + args[i]);
|
|
36
|
+
}
|
|
37
|
+
if (!repo) usageError("--repo is required");
|
|
38
|
+
if (!orchDir) usageError("--orch-dir is required");
|
|
39
|
+
|
|
40
|
+
function fail(msg) {
|
|
41
|
+
process.stderr.write("scaffold-crew: " + msg + "\n");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(repo) || !fs.statSync(repo).isDirectory()) {
|
|
46
|
+
fail("crew repo not found or not a directory: " + repo);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fixed file sets. No judgment here: these globs are the scaffold.
|
|
50
|
+
//
|
|
51
|
+
// Fail-closed source guards (2026-09-19 REVIEW): an empty or missing source
|
|
52
|
+
// set used to sail through as a successful all-skipped scaffold (receipt
|
|
53
|
+
// 0/0). A crew with no identities, no personas, or no seed workflows is not
|
|
54
|
+
// a crew — refuse loudly (exit 1) before creating anything, instead of
|
|
55
|
+
// stamping an empty receipt. The feedback README stays optional.
|
|
56
|
+
function requireSource(dir, exts, label) {
|
|
57
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
58
|
+
fail("scaffold source missing: " + dir + " (" + label + " — refusing to scaffold an empty crew)");
|
|
59
|
+
}
|
|
60
|
+
const files = globFiles(dir, exts);
|
|
61
|
+
if (files.length === 0) {
|
|
62
|
+
fail("scaffold source empty: " + dir + " (no " + label + " files — refusing to scaffold an empty crew)");
|
|
63
|
+
}
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const identityFiles = requireSource(path.join(repo, "identities"), [".md", ".png"], "identity");
|
|
68
|
+
const personaFiles = requireSource(path.join(repo, "personas"), [".md"], "persona");
|
|
69
|
+
const seedWorkflowFiles = requireSource(path.join(repo, "seed", "workflows"), [".md"], "seed workflow");
|
|
70
|
+
|
|
71
|
+
const dirs = ["identities", "personas", "workflows", "feedback"];
|
|
72
|
+
for (const d of dirs) {
|
|
73
|
+
fs.mkdirSync(path.join(orchDir, d), { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function globFiles(dir, exts) {
|
|
77
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
78
|
+
return fs.readdirSync(dir)
|
|
79
|
+
.filter((f) => exts.some((e) => f.endsWith(e)))
|
|
80
|
+
.sort()
|
|
81
|
+
.map((f) => path.join(dir, f));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const copies = [];
|
|
85
|
+
for (const f of identityFiles) {
|
|
86
|
+
copies.push([f, path.join(orchDir, "identities", path.basename(f))]);
|
|
87
|
+
}
|
|
88
|
+
for (const f of personaFiles) {
|
|
89
|
+
copies.push([f, path.join(orchDir, "personas", path.basename(f))]);
|
|
90
|
+
}
|
|
91
|
+
for (const f of seedWorkflowFiles) {
|
|
92
|
+
copies.push([f, path.join(orchDir, "workflows", path.basename(f))]);
|
|
93
|
+
}
|
|
94
|
+
const feedbackReadme = path.join(repo, "seed", "feedback", "README.md");
|
|
95
|
+
if (fs.existsSync(feedbackReadme)) {
|
|
96
|
+
copies.push([feedbackReadme, path.join(orchDir, "feedback", "README.md")]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let created = 0;
|
|
100
|
+
let skipped = 0;
|
|
101
|
+
const expected = [];
|
|
102
|
+
for (const [src, dest] of copies) {
|
|
103
|
+
expected.push(dest);
|
|
104
|
+
if (fs.existsSync(dest)) {
|
|
105
|
+
skipped += 1; // no-clobber: existing files are never overwritten
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
fs.copyFileSync(src, dest);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
fail("copy failed: " + src + " -> " + dest + " (" + String(e.message || e) + ")");
|
|
112
|
+
}
|
|
113
|
+
created += 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Disk verification is the decision rule: every expected file must exist.
|
|
117
|
+
const missing = expected.filter((f) => !fs.existsSync(f));
|
|
118
|
+
if (missing.length > 0) {
|
|
119
|
+
fail("incomplete scaffold: " + missing.length + " expected file(s) missing after copy: " + missing[0]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const receipt = { created, skipped };
|
|
123
|
+
const receiptPath = path.join(orchDir, ".scaffold-receipt.json");
|
|
124
|
+
try {
|
|
125
|
+
fs.writeFileSync(receiptPath, JSON.stringify(receipt) + "\n");
|
|
126
|
+
} catch (e) {
|
|
127
|
+
fail("could not write receipt " + receiptPath + " (" + String(e.message || e) + ")");
|
|
128
|
+
}
|
|
129
|
+
process.stdout.write(JSON.stringify(receipt) + "\n");
|
package/package.json
CHANGED
package/workflows/bugfix.js
CHANGED
|
@@ -132,7 +132,7 @@ if (!taskId) {
|
|
|
132
132
|
// See docs/decisions/workflow-core.md#closeout-envelope: the work agent returns the runtime's native envelope; verdict extracted mechanically.
|
|
133
133
|
const VERDICT_STEPS = ["Build", "Review", "QA", "Reproduce", "Integrate", "Publish"];
|
|
134
134
|
function extractVerdict(workerText) {
|
|
135
|
-
|
|
135
|
+
// The verdict is the LAST VERDICT: PASS/FAIL in the report (contract: end
|
|
136
136
|
// your report with the verdict). This ignores literal VERDICT strings echoed
|
|
137
137
|
// from the worker's instructions (which contain quoted examples). Fail
|
|
138
138
|
// closed if: no verdict found, the last verdict is not in the trailing 100
|
|
@@ -442,9 +442,7 @@ async function recordPublishLedger(entry, rework) {
|
|
|
442
442
|
label: "Recording publish attempt in ledger",
|
|
443
443
|
schema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] } }
|
|
444
444
|
);
|
|
445
|
-
|
|
446
|
-
log("Publish ledger: outcome '" + entry.outcome + "' for task " + taskId +
|
|
447
|
-
(ok ? " recorded." : " NOT confirmed (" + ((res && res.result) || "no output") + ")"));
|
|
445
|
+
log("Noted publish outcome '" + entry.outcome + "' for task " + taskId + " in ledger");
|
|
448
446
|
} catch (e) {
|
|
449
447
|
log("Publish ledger: write failed for task " + taskId + " (non-fatal, observability only): " + (e && e.message ? e.message : e));
|
|
450
448
|
}
|
|
@@ -2192,11 +2190,11 @@ while (i < STEPS.length) {
|
|
|
2192
2190
|
if (!postDeploy.deployed) {
|
|
2193
2191
|
return await parkTask("Post-deploy failed after a skipped publish: " + (postDeploy.output || "no output") + ". Nothing was published; worktree cleanup and lock state unknown — human attention needed.");
|
|
2194
2192
|
}
|
|
2195
|
-
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy
|
|
2193
|
+
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy step finished");
|
|
2196
2194
|
} else {
|
|
2197
2195
|
if (publishFailure) {
|
|
2198
2196
|
return await parkTask(publishFailure + (postDeploy.deployed
|
|
2199
|
-
? " Post-deploy
|
|
2197
|
+
? " Post-deploy step finished (cleanup status unknown)."
|
|
2200
2198
|
: " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
|
|
2201
2199
|
}
|
|
2202
2200
|
if (!publishBuildLanded) {
|
|
@@ -2209,7 +2207,7 @@ while (i < STEPS.length) {
|
|
|
2209
2207
|
// commit (design §1.5: "the trigger was sent for commit <short-sha>").
|
|
2210
2208
|
if (publishUnknownFields) publishUnknownFields.commitShortSha = mergeCommitShortForPublish;
|
|
2211
2209
|
return await parkTask(composeUnattributedParkReason(publishUnknownFields) + (postDeploy.deployed
|
|
2212
|
-
? " Post-deploy
|
|
2210
|
+
? " Post-deploy step finished (cleanup status unknown)."
|
|
2213
2211
|
: " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
|
|
2214
2212
|
}
|
|
2215
2213
|
if (!postDeploy.deployed) {
|
|
@@ -2238,7 +2236,7 @@ while (i < STEPS.length) {
|
|
|
2238
2236
|
// artifact_edit would trigger a duplicate build.
|
|
2239
2237
|
if (publishSkippedNoLock) {
|
|
2240
2238
|
instructions = "Publish was skipped deterministically by the workflow before your step — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would disturb the finalized state.\n\n" +
|
|
2241
|
-
"Integrate reported MERGED_EMPTY (the task branch had no commits ahead of main), so no merge lock was taken and there is nothing to ship.
|
|
2239
|
+
"Integrate reported MERGED_EMPTY (the task branch had no commits ahead of main), so no merge lock was taken and there is nothing to ship. Post-deploy step finished per its return — do NOT run post-deploy yourself.\n\n" +
|
|
2242
2240
|
"Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
|
|
2243
2241
|
"The VERDICT line must be the last line of your report.";
|
|
2244
2242
|
} else {
|
|
@@ -2831,11 +2829,7 @@ while (i < STEPS.length) {
|
|
|
2831
2829
|
{ key: attemptKey("publish-provenance-refresh-" + taskId, totalReworkCount), label: "Refreshing dashboard provenance after crew release",
|
|
2832
2830
|
schema: { type: "object", properties: { refreshed: { type: "boolean" }, reason: { type: "string" }, crew_release: { type: "string" }, published_at: { type: "string" } }, required: ["refreshed"] } }
|
|
2833
2831
|
);
|
|
2834
|
-
if (provRefresh.refreshed) {
|
|
2835
|
-
log("Provenance refreshed for task " + taskId + ": crew_release " + provRefresh.crew_release);
|
|
2836
|
-
} else if (provRefresh.reason === "no-record") {
|
|
2837
|
-
log("Provenance refresh skipped for task " + taskId + ": no existing record to refresh (fresh instance — the dashboard's first artifact publish will create it)");
|
|
2838
|
-
} else {
|
|
2832
|
+
if (!provRefresh.refreshed && provRefresh.reason !== "no-record") {
|
|
2839
2833
|
return await parkTask("Provenance refresh failed after a verified npm publish (crew_release " + (provRefresh.crew_release || "unknown") + "). The release is live but the dashboard record is stale — fail-closed.");
|
|
2840
2834
|
}
|
|
2841
2835
|
} catch (e) {
|
package/workflows/chore.js
CHANGED
|
@@ -501,9 +501,7 @@ async function recordPublishLedger(entry, rework) {
|
|
|
501
501
|
label: "Recording publish attempt in ledger",
|
|
502
502
|
schema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] } }
|
|
503
503
|
);
|
|
504
|
-
|
|
505
|
-
log("Publish ledger: outcome '" + entry.outcome + "' for task " + taskId +
|
|
506
|
-
(ok ? " recorded." : " NOT confirmed (" + ((res && res.result) || "no output") + ")"));
|
|
504
|
+
log("Noted publish outcome '" + entry.outcome + "' for task " + taskId + " in ledger");
|
|
507
505
|
} catch (e) {
|
|
508
506
|
log("Publish ledger: write failed for task " + taskId + " (non-fatal, observability only): " + (e && e.message ? e.message : e));
|
|
509
507
|
}
|
|
@@ -2098,11 +2096,11 @@ while (i < STEPS.length) {
|
|
|
2098
2096
|
if (!postDeploy.deployed) {
|
|
2099
2097
|
return await parkTask("Post-deploy failed after a skipped publish: " + (postDeploy.output || "no output") + ". Nothing was published; worktree cleanup and lock state unknown — human attention needed.");
|
|
2100
2098
|
}
|
|
2101
|
-
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy
|
|
2099
|
+
log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy step finished");
|
|
2102
2100
|
} else {
|
|
2103
2101
|
if (publishFailure) {
|
|
2104
2102
|
return await parkTask(publishFailure + (postDeploy.deployed
|
|
2105
|
-
? " Post-deploy
|
|
2103
|
+
? " Post-deploy step finished (cleanup status unknown)."
|
|
2106
2104
|
: " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
|
|
2107
2105
|
}
|
|
2108
2106
|
if (!publishBuildLanded) {
|
|
@@ -2115,7 +2113,7 @@ while (i < STEPS.length) {
|
|
|
2115
2113
|
// commit (design §1.5: "the trigger was sent for commit <short-sha>").
|
|
2116
2114
|
if (publishUnknownFields) publishUnknownFields.commitShortSha = mergeCommitShortForPublish;
|
|
2117
2115
|
return await parkTask(composeUnattributedParkReason(publishUnknownFields) + (postDeploy.deployed
|
|
2118
|
-
? " Post-deploy
|
|
2116
|
+
? " Post-deploy step finished (cleanup status unknown)."
|
|
2119
2117
|
: " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
|
|
2120
2118
|
}
|
|
2121
2119
|
if (!postDeploy.deployed) {
|
|
@@ -2144,7 +2142,7 @@ while (i < STEPS.length) {
|
|
|
2144
2142
|
// artifact_edit would trigger a duplicate build.
|
|
2145
2143
|
if (publishSkippedNoLock) {
|
|
2146
2144
|
instructions = "Publish was skipped deterministically by the workflow before your step — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would disturb the finalized state.\n\n" +
|
|
2147
|
-
"Integrate reported MERGED_EMPTY (the task branch had no commits ahead of main), so no merge lock was taken and there is nothing to ship.
|
|
2145
|
+
"Integrate reported MERGED_EMPTY (the task branch had no commits ahead of main), so no merge lock was taken and there is nothing to ship. Post-deploy step finished per its return — do NOT run post-deploy yourself.\n\n" +
|
|
2148
2146
|
"Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
|
|
2149
2147
|
"The VERDICT line must be the last line of your report.";
|
|
2150
2148
|
} else {
|
|
@@ -2458,11 +2456,7 @@ while (i < STEPS.length) {
|
|
|
2458
2456
|
{ key: attemptKey("publish-provenance-refresh-" + taskId, reworkCount), label: "Refreshing dashboard provenance after crew release",
|
|
2459
2457
|
schema: { type: "object", properties: { refreshed: { type: "boolean" }, reason: { type: "string" }, crew_release: { type: "string" }, published_at: { type: "string" } }, required: ["refreshed"] } }
|
|
2460
2458
|
);
|
|
2461
|
-
if (provRefresh.refreshed) {
|
|
2462
|
-
log("Provenance refreshed for task " + taskId + ": crew_release " + provRefresh.crew_release);
|
|
2463
|
-
} else if (provRefresh.reason === "no-record") {
|
|
2464
|
-
log("Provenance refresh skipped for task " + taskId + ": no existing record to refresh (fresh instance — the dashboard's first artifact publish will create it)");
|
|
2465
|
-
} else {
|
|
2459
|
+
if (!provRefresh.refreshed && provRefresh.reason !== "no-record") {
|
|
2466
2460
|
return await parkTask("Provenance refresh failed after a verified npm publish (crew_release " + (provRefresh.crew_release || "unknown") + "). The release is live but the dashboard record is stale — fail-closed.");
|
|
2467
2461
|
}
|
|
2468
2462
|
} catch (e) {
|
|
@@ -794,13 +794,44 @@ if (eligible.length === 0) {
|
|
|
794
794
|
);
|
|
795
795
|
|
|
796
796
|
if (fileResult.task_id) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
797
|
+
// Razor: the filed task_id is testimony. Do an independent witness
|
|
798
|
+
// re-read of the board before entering eligible — the task must be
|
|
799
|
+
// observed in todo state, not assumed. The downstream atomic claim
|
|
800
|
+
// remains the authoritative guard; this is fail-fast, fail-closed.
|
|
801
|
+
var ptWitnessOk = false;
|
|
802
|
+
try {
|
|
803
|
+
var ptBoardRaw = await agent(
|
|
804
|
+
"Re-read the dispatch state to verify the filed playtest task.\\n" +
|
|
805
|
+
"Run in shell and return the stdout as a raw string:\\n" + crewCmd("get-dispatch-state", {}) + "\\n" +
|
|
806
|
+
"Return the command's stdout JSON as a plain string, byte-for-byte, unmodified. " +
|
|
807
|
+
"Do NOT parse the JSON — your return value must be the raw stdout string, never an object.",
|
|
808
|
+
{
|
|
809
|
+
key: "verify-playtest",
|
|
810
|
+
label: "Verifying filed playtest task"
|
|
811
|
+
}
|
|
812
|
+
);
|
|
813
|
+
var ptBoard = unwrapBoardResult(parseBoardJson(ptBoardRaw));
|
|
814
|
+
var ptTasks = (ptBoard.ready_tasks || []).map(projectTaskRecord).filter(function (t) { return t !== null; });
|
|
815
|
+
for (var pti2 = 0; pti2 < ptTasks.length; pti2++) {
|
|
816
|
+
if (ptTasks[pti2].id === fileResult.task_id && ptTasks[pti2].state === "todo") {
|
|
817
|
+
ptWitnessOk = true;
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
} catch (e) {
|
|
822
|
+
log("Playtest witness re-read failed (" + String(e.message || e) + ") — task not verified, skipping eligible");
|
|
823
|
+
}
|
|
824
|
+
if (ptWitnessOk) {
|
|
825
|
+
eligible.push({
|
|
826
|
+
task: { id: fileResult.task_id, title: ptTitle, description: ptDesc, project: ptProject, workflow: "standard", state: "todo" },
|
|
827
|
+
startStep: qaStepIndex("standard"),
|
|
828
|
+
reason: "playtest",
|
|
829
|
+
workflow: "standard"
|
|
830
|
+
});
|
|
831
|
+
log("Filed idle playtest \"" + ptTitle + "\" [" + ptProject + "] journey " + pti + "/" + J + ", persona " + ppi + "/" + P + ", max_filings " + maxFilings + " (witness observed todo)");
|
|
832
|
+
} else {
|
|
833
|
+
log("Idle playtest task " + fileResult.task_id + " not observed in todo state on re-read — skipping eligible (fail-closed)");
|
|
834
|
+
}
|
|
804
835
|
} else {
|
|
805
836
|
log("Idle playtest filing failed — cursors already advanced, assignment skipped");
|
|
806
837
|
}
|
|
@@ -1022,7 +1053,11 @@ if (recommended.length > 0) {
|
|
|
1022
1053
|
);
|
|
1023
1054
|
var reserveParsed = extractJsonObject(reserveOut);
|
|
1024
1055
|
if (reserveParsed && reserveParsed.acquired) {
|
|
1025
|
-
|
|
1056
|
+
// Razor: the acquired flag is testimony from the reservation agent's
|
|
1057
|
+
// stdout ferry, not proof. The downstream atomic claim-task remains
|
|
1058
|
+
// the authoritative guard. We route on the flag (fail-closed if
|
|
1059
|
+
// absent) but do not assert the reservation as established fact.
|
|
1060
|
+
log("Reservation agent returned acquired=true for task " + rec.task_id.slice(0, 8) + " (not independently verified; claim-task is authoritative)");
|
|
1026
1061
|
acquired.push(rec);
|
|
1027
1062
|
} else {
|
|
1028
1063
|
// FAIL CLOSED: Another dispatcher owns this task. Do not recommend it.
|