muse-crew 0.14.13 → 0.15.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 +2 -0
- package/lib/crew-api.js +49 -2
- package/lib/schema.sql +7 -0
- package/package.json +1 -1
- package/workflows/bugfix.js +52 -0
- package/workflows/crew-dispatch.js +7 -1
- package/workflows/standard.js +52 -0
package/API.md
CHANGED
|
@@ -489,6 +489,8 @@ Register a new project.
|
|
|
489
489
|
| `quiesced` | boolean | no | Start paused; defaults to false |
|
|
490
490
|
| `visual_protocol` | boolean or null | no | Tri-state: `null` = inherit crew default (off), `true` = enable for this project, `false` = explicit off. Defaults to null. |
|
|
491
491
|
| `environment_type` | `artifact` · `terminal` · null | no | User-facing surface for experiential QA routing: `artifact` = a rendered web UI (Hazel drives it with the see-act browser loop), `terminal` = a CLI (Hazel drives it herself, keeping transcripts). `null` = unclassified: no experiential QA. `deploy_type` names the deployment target, but `deploy_type: "artifact"` remains a legacy artifact-surface signal so pre-field projects keep today's experiential QA (the migration does not backfill the column); on conflicting config artifact wins. Omitted means auto-classify from `repo_path` (see surface classification below); an explicit value, including explicit `null`, always wins. |
|
|
492
|
+
| `versioned_build` | boolean | no | Opt-in to the QA staleness gate (issue #3): when true, Integrate bumps `BUILD_NUMBER` in `version_file` after each merge (separate commit, pushed), and QA verifies the tested bundle's footer `build <n>` is at/past the recorded version before testing. Defaults to false (unversioned); the migration does not backfill. |
|
|
493
|
+
| `version_file` | string or null | no | Repo-relative path of the TypeScript file exporting `BUILD_NUMBER` (e.g. `client/src/buildNumber.ts`). Only meaningful when `versioned_build` is true; defaults to `client/src/buildNumber.ts` when omitted. |
|
|
492
494
|
|
|
493
495
|
**Surface classification (repo-derived defaults):** when `repo_path` is provided and `environment_type` is omitted, `create-project` mechanically classifies the repo at registration via `lib/classify-surface.js` — `space.json` present → `artifact`; else a non-empty `package.json` `bin` → `terminal`; otherwise unclassified (`null`). The classifier never throws: bad paths and malformed files classify as unclassified. Classification only fills omitted fields — an explicitly supplied `environment_type` (including explicit `null`) always wins, and deploy defaults follow the *effective* surface, so an explicit `"terminal"` never triggers artifact deploy defaults.
|
|
494
496
|
|
package/lib/crew-api.js
CHANGED
|
@@ -73,6 +73,21 @@ function validateEnvironmentType(value) {
|
|
|
73
73
|
if (value === "artifact" || value === "terminal") return value;
|
|
74
74
|
throw usageError("environment_type must be 'artifact', 'terminal', or null.");
|
|
75
75
|
}
|
|
76
|
+
// versioned_build validation: boolean. undefined means "not provided"
|
|
77
|
+
// (create: defaults to false; update: leaves the field alone). Coerces
|
|
78
|
+
// truthy/falsy to 1/0 for the INTEGER column.
|
|
79
|
+
function validateVersionedBuild(value) {
|
|
80
|
+
if (value === undefined) return undefined;
|
|
81
|
+
return value ? 1 : 0;
|
|
82
|
+
}
|
|
83
|
+
// version_file validation: string or null. undefined means "not provided"
|
|
84
|
+
// (create: defaults to null; update: leaves the field alone). Empty string
|
|
85
|
+
// is rejected — use null to clear.
|
|
86
|
+
function validateVersionFile(value) {
|
|
87
|
+
if (value === undefined || value === null) return null;
|
|
88
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
89
|
+
throw usageError("version_file must be a non-empty string or null.");
|
|
90
|
+
}
|
|
76
91
|
|
|
77
92
|
// already_merged_sha validation (room #16 blocker 11, 2026-09-18): the
|
|
78
93
|
// workflow-verified already-merged sha as structured session control state.
|
|
@@ -315,6 +330,24 @@ function openDb(crewHome) {
|
|
|
315
330
|
if (!/duplicate column name/i.test(e.message)) throw e;
|
|
316
331
|
}
|
|
317
332
|
}
|
|
333
|
+
// Migration: add versioned_build + version_file columns if missing (QA
|
|
334
|
+
// staleness gate, emojimanegg1/muse-crew#3, 2026-09-24). Same idempotent
|
|
335
|
+
// PRAGMA-check pattern; versioned_build defaults 0 (unversioned by
|
|
336
|
+
// design, no backfill), version_file stays NULL.
|
|
337
|
+
if (!projectCols.some((c) => c.name === "versioned_build")) {
|
|
338
|
+
try {
|
|
339
|
+
db.exec("ALTER TABLE projects ADD COLUMN versioned_build INTEGER NOT NULL DEFAULT 0 CHECK (versioned_build IN (0, 1))");
|
|
340
|
+
} catch (e) {
|
|
341
|
+
if (!/duplicate column name/i.test(e.message)) throw e;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (!projectCols.some((c) => c.name === "version_file")) {
|
|
345
|
+
try {
|
|
346
|
+
db.exec("ALTER TABLE projects ADD COLUMN version_file TEXT");
|
|
347
|
+
} catch (e) {
|
|
348
|
+
if (!/duplicate column name/i.test(e.message)) throw e;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
318
351
|
// Migration: per-project provenance columns (room #15 blocker 8,
|
|
319
352
|
// 2026-09-18). Same idempotent PRAGMA-check pattern; ADD COLUMN without
|
|
320
353
|
// a default leaves existing rows NULL, which is "no provenance yet" by
|
|
@@ -446,6 +479,9 @@ function mapProject(row) {
|
|
|
446
479
|
// environment_type: 'artifact' | 'terminal' | null (unclassified).
|
|
447
480
|
// Null stays null — no coercion, no default.
|
|
448
481
|
environment_type: row.environment_type == null ? null : row.environment_type,
|
|
482
|
+
// versioned_build: 0/1 INTEGER → boolean. version_file: string or null.
|
|
483
|
+
versioned_build: !!row.versioned_build,
|
|
484
|
+
version_file: row.version_file == null ? null : row.version_file,
|
|
449
485
|
created_at: row.created_at,
|
|
450
486
|
updated_at: row.updated_at,
|
|
451
487
|
};
|
|
@@ -3074,6 +3110,8 @@ commands["create-project"] = (db, args, ctx) => {
|
|
|
3074
3110
|
quiesced: args.quiesced ? 1 : 0,
|
|
3075
3111
|
visual_protocol: visualProtocol == null ? null : (visualProtocol ? 1 : 0),
|
|
3076
3112
|
environment_type: environmentType,
|
|
3113
|
+
versioned_build: validateVersionedBuild(args.versioned_build) ?? 0,
|
|
3114
|
+
version_file: validateVersionFile(args.version_file),
|
|
3077
3115
|
created_at: timestamp, updated_at: timestamp,
|
|
3078
3116
|
};
|
|
3079
3117
|
if (!Number.isInteger(row.simultaneity) || row.simultaneity < 1 || row.simultaneity > 100) {
|
|
@@ -3081,9 +3119,9 @@ commands["create-project"] = (db, args, ctx) => {
|
|
|
3081
3119
|
}
|
|
3082
3120
|
db.prepare(
|
|
3083
3121
|
`INSERT INTO projects (id, display_name, repo_path, deploy_type, deploy_slug,
|
|
3084
|
-
description, simultaneity, quiesced, visual_protocol, environment_type, created_at, updated_at)
|
|
3122
|
+
description, simultaneity, quiesced, visual_protocol, environment_type, versioned_build, version_file, created_at, updated_at)
|
|
3085
3123
|
VALUES (@id, @display_name, @repo_path, @deploy_type, @deploy_slug,
|
|
3086
|
-
@description, @simultaneity, @quiesced, @visual_protocol, @environment_type, @created_at, @updated_at)`).run(row);
|
|
3124
|
+
@description, @simultaneity, @quiesced, @visual_protocol, @environment_type, @versioned_build, @version_file, @created_at, @updated_at)`).run(row);
|
|
3087
3125
|
// A deferred backfill (zero/ambiguous matches at the last look) gets one
|
|
3088
3126
|
// more evaluation now that the match set changed: a new project whose
|
|
3089
3127
|
// repo descends from the legacy commit inherits the attribution instead
|
|
@@ -3142,6 +3180,15 @@ commands["update-project"] = (db, args) => {
|
|
|
3142
3180
|
if (args.environment_type !== undefined) {
|
|
3143
3181
|
patch.environment_type = validateEnvironmentType(args.environment_type);
|
|
3144
3182
|
}
|
|
3183
|
+
// versioned_build / version_file are NOT covered by the context-change guard:
|
|
3184
|
+
// like environment_type, they are read once from launch args at dispatch
|
|
3185
|
+
// time, so a mid-run change only affects future launches.
|
|
3186
|
+
if (args.versioned_build !== undefined) {
|
|
3187
|
+
patch.versioned_build = validateVersionedBuild(args.versioned_build) ?? 0;
|
|
3188
|
+
}
|
|
3189
|
+
if (args.version_file !== undefined) {
|
|
3190
|
+
patch.version_file = validateVersionFile(args.version_file);
|
|
3191
|
+
}
|
|
3145
3192
|
|
|
3146
3193
|
const contextChanged =
|
|
3147
3194
|
(args.repo_path !== undefined && args.repo_path !== current.repo_path) ||
|
package/lib/schema.sql
CHANGED
|
@@ -46,6 +46,13 @@ CREATE TABLE IF NOT EXISTS projects (
|
|
|
46
46
|
provenance_crew_release TEXT,
|
|
47
47
|
provenance_published_at TEXT,
|
|
48
48
|
provenance_task_id TEXT,
|
|
49
|
+
-- versioned_build: the QA staleness gate (emojimanegg1/muse-crew#3). When 1,
|
|
50
|
+
-- Integrate bumps the version file and QA verifies the tested bundle is at
|
|
51
|
+
-- or past the required version. version_file is the repo-relative path to
|
|
52
|
+
-- the TS file exporting BUILD_NUMBER. Both nullable/0 by design: existing
|
|
53
|
+
-- rows stay unversioned (no backfill).
|
|
54
|
+
versioned_build INTEGER NOT NULL DEFAULT 0 CHECK (versioned_build IN (0, 1)),
|
|
55
|
+
version_file TEXT,
|
|
49
56
|
created_at TEXT NOT NULL,
|
|
50
57
|
updated_at TEXT NOT NULL
|
|
51
58
|
);
|
package/package.json
CHANGED
package/workflows/bugfix.js
CHANGED
|
@@ -563,6 +563,18 @@ function decideIntegrateRetry(o) {
|
|
|
563
563
|
if (rec.malformed) return "park";
|
|
564
564
|
return ancestor ? "skip-to-publish" : "proceed";
|
|
565
565
|
}
|
|
566
|
+
// QA version gate (issue #3): S=footer build, R=required. S>=R proves the
|
|
567
|
+
// bundle is at/past the merge (single-branch: version order = containment).
|
|
568
|
+
// Pure — pinned by tests/version-gate.test.js. Null = unreadable/missing.
|
|
569
|
+
function decideVersionGate(S, R) {
|
|
570
|
+
if (!(R > 0)) return { proceed: false, reason: "qa-version-missing",
|
|
571
|
+
remedy: "no build version recorded — re-run Integrate or record dashboard-version." };
|
|
572
|
+
if (!(S > 0)) return { proceed: false, reason: "qa-version-unreadable",
|
|
573
|
+
remedy: "no readable build number in bundle footer — rebuild from versioned source." };
|
|
574
|
+
if (S >= R) return { proceed: true };
|
|
575
|
+
return { proceed: false, reason: "qa-bundle-stale",
|
|
576
|
+
remedy: "bundle build " + S + " behind required " + R + " — builder did not rebuild from current source." };
|
|
577
|
+
}
|
|
566
578
|
// On a dispatcher retry resumed at Integrate, the run-local releaseDecision
|
|
567
579
|
// is null (Build doesn't re-run). Hydrate it from the merge record so the
|
|
568
580
|
// npm Publish gate doesn't park a publishable merge. Pure — pinned by
|
|
@@ -1355,10 +1367,26 @@ while (i < STEPS.length) {
|
|
|
1355
1367
|
// verdicts.jsonl). No parent verdict gate remains.
|
|
1356
1368
|
var qaArtifact = false;
|
|
1357
1369
|
var qaTerminal = false;
|
|
1370
|
+
var qaRequiredVersion = null;
|
|
1358
1371
|
if (step.name === "QA") {
|
|
1359
1372
|
var qaExp = (await resolveExperiential()) === "yes";
|
|
1360
1373
|
qaArtifact = qaExp && SURFACE_ARTIFACT;
|
|
1361
1374
|
qaTerminal = qaExp && SURFACE_TERMINAL;
|
|
1375
|
+
if (qaArtifact && projectConfig.versioned_build === true) {
|
|
1376
|
+
var vfile = projectConfig.version_file || "client/src/buildNumber.ts";
|
|
1377
|
+
var vgPre = await agent(
|
|
1378
|
+
"Return ONLY JSON. 1. Run, return stdout verbatim:\n" + crewCmd("get-events", { task_id: taskId }) + "\n" +
|
|
1379
|
+
"R = integer after `dashboard-version: ` in newest matching note. 2. If none, run, return stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
1380
|
+
"R = BUILD_NUMBER from: cd " + REPO_PATH + " && git show <source_commit>:" + vfile + " | grep -o 'BUILD_NUMBER = [0-9]*'. If unknown: {\"ok\":false,\"park\":\"qa-version-missing: no build version recorded — re-run Integrate or record dashboard-version.\"}. " +
|
|
1381
|
+
"3. cd " + REPO_PATH + " && git fetch origin && V=$(grep -o 'BUILD_NUMBER = [0-9]*' " + vfile + " | grep -o '[0-9]*'|head -1); if [ $V -lt R ]; then git pull --ff-only; V=$(grep -o 'BUILD_NUMBER = [0-9]*' " + vfile + " | grep -o '[0-9]*'|head -1); fi; " +
|
|
1382
|
+
"if [ $V -lt R ]: {\"ok\":false,\"park\":\"qa-bundle-stale: checkout \" + V + \" < required \" + R + \" — sync past the merge, re-run QA.\"}. 4. Else {\"ok\":true,\"R\":R}.",
|
|
1383
|
+
{ key: "qa-version-pre-" + taskId, label: "QA version-gate pre-check",
|
|
1384
|
+
schema: { type: "object", required: ["ok"],
|
|
1385
|
+
properties: { ok: { type: "boolean" }, R: { type: "integer" }, park: { type: "string" } } } }
|
|
1386
|
+
);
|
|
1387
|
+
if (!vgPre.ok) return await parkTask(vgPre.park);
|
|
1388
|
+
qaRequiredVersion = vgPre.R;
|
|
1389
|
+
}
|
|
1362
1390
|
}
|
|
1363
1391
|
|
|
1364
1392
|
// Reproduce-layer routing (2026-09-16): Sage's Triage classifies the bug's
|
|
@@ -1687,6 +1715,14 @@ while (i < STEPS.length) {
|
|
|
1687
1715
|
"R5-PUSH (manual R5 resolution only — the normal path pushed inline). Run: " + LIFECYCLE_ENV + LIFECYCLE + " push-target " + taskId + "\n" +
|
|
1688
1716
|
"PUSHED — report the merged hash (detached prints PUSHED: origin/main (refspec HEAD:main)), VERDICT: PASS; PUSH_SKIPPED — no push attempted (no record + no lock); NO_REMOTE_PUSH — no remote; VERDICT: PASS; ERROR or CONFLICT — report it, VERDICT: FAIL.\n" +
|
|
1689
1717
|
"NEVER force-push.\n\n" +
|
|
1718
|
+
(projectConfig.versioned_build === true ?
|
|
1719
|
+
"VERSION BUMP (issue #3): after MERGED+PUSHED, bump so QA can prove the bundle contains this fix — QA parks without it.\n" +
|
|
1720
|
+
"1. cd " + REPO_PATH + " && git fetch origin; BR=<branch-from-integration-target>; F=" + (projectConfig.version_file || "client/src/buildNumber.ts") + "\n" +
|
|
1721
|
+
"2. N=$(git show origin/$BR:$F | grep -o 'BUILD_NUMBER = [0-9]*' | grep -o '[0-9]*'); if missing/unparsable: VERDICT: FAIL.\n" +
|
|
1722
|
+
"3. Edit $F: `export const BUILD_NUMBER = $N;` → `export const BUILD_NUMBER = $((N+1));` (keep header comment).\n" +
|
|
1723
|
+
"4. git add $F && git commit -m \"build-number: $((N+1)) - QA version gate\" (SEPARATE commit, never amend).\n" +
|
|
1724
|
+
"5. git push origin $BR; if rejected retry 3x (fetch, re-read N, re-bump, re-commit, push). NEVER force-push. Push MUST succeed or VERDICT: FAIL.\n" +
|
|
1725
|
+
"6. Run, return stdout verbatim:\n" + crewCmd("log-event", { task_id: taskId, type: "note", identity: step.identity, message: "dashboard-version: <new> — QA must test a bundle built from source at or after the merge that recorded this (build <new> or later)." }) + "\n(substitute <new>).\n\n" : "") +
|
|
1690
1726
|
"Report what happened at each step, ending with exactly one line: VERDICT: PASS or VERDICT: FAIL.";
|
|
1691
1727
|
|
|
1692
1728
|
} else if (step.name === "Publish") {
|
|
@@ -2140,6 +2176,8 @@ while (i < STEPS.length) {
|
|
|
2140
2176
|
"c3. Start with: SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ node " + crewHome + "/current/lib/see-act.js --url http://localhost:<N>/ aria — read the JSON, log the step. Then: SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ node " + crewHome + "/current/lib/see-act.js --url http://localhost:<N>/ shot — READ the screenshot, log the step. Act on what you see: click, scroll, type, then re-observe, logging each step. Prefer aria (cheap text) to find controls; screenshot when the view changes and for your final verdict frames (one desktop, one mobile). If a click exits non-zero, do NOT retry the same ref blindly: re-run aria first (refs go stale between invocations), then click the fresh ref exactly once. If it still fails, log the failure and move on — a flaky control is a finding, not a loop.\n" +
|
|
2141
2177
|
"d. Reach: with the session protocol, any flow reachable by N in-page actions is drivable — open the dialog, then confirm it, then judge the result. Without a session (one-shot invocations), anything reachable by (navigate, one action) is testable and sequences needing prior in-page state are not — use a session for those. Report NOT POSSIBLE only when the tooling itself fails (session-start exits 3): a flow you could not reach is not NOT POSSIBLE — name the exact step that stopped you in verdict.json's missing evidence and continue with the mechanical checks.\n" +
|
|
2142
2178
|
"e. Judge as a user against the task description: is the reported bug fixed AND is nothing else visibly broken? Look for broken layout, overflow, missing or wrong content, stale data, and console errors. Compare against the task's expected behavior, never against source code (you are code-blind). Every frame you captured is already archived under " + crewHome + "/task-evidence/" + taskId + "/postchange/ and indexed in ooda-log.jsonl. A frame you did not read is not evidence. Loading, error, or blank frames never pass. If you cannot complete the loop, say exactly which steps are missing — unknown is not PASS.\n" +
|
|
2179
|
+
(projectConfig.versioned_build === true ?
|
|
2180
|
+
"e2. VERSION (issue #3): footer shows `build <n>` — report `footer_build: <n>` on its own line, or `footer_build: unreadable`. Required — the gate cannot pass without it.\n" : "") +
|
|
2143
2181
|
"f. Kill ONLY the server you started: pkill -f 'serve-artifact[.]js.*--tag " + taskId + "-qa' — never another task's server. (The [.] keeps pkill from matching its own command line.) Do not leave it running.\n" +
|
|
2144
2182
|
"Then continue with the mechanical checks below. Your VERDICT covers both the visual and the mechanical checks.\n\n" +
|
|
2145
2183
|
"MECHANICAL CHECKS:\n" +
|
|
@@ -2622,6 +2660,20 @@ while (i < STEPS.length) {
|
|
|
2622
2660
|
// reports FAIL, it stands — finding attribution informs follow-up
|
|
2623
2661
|
// filing only.
|
|
2624
2662
|
if (step.name === "QA") {
|
|
2663
|
+
if (projectConfig.versioned_build === true && qaArtifact && qaRequiredVersion !== null) {
|
|
2664
|
+
var vgM = /footer_build:\s*(\d+|unreadable)/i.exec(workerText || "");
|
|
2665
|
+
var vgS = (vgM && /^\d+$/.test(vgM[1])) ? parseInt(vgM[1], 10) : null;
|
|
2666
|
+
var vgD = decideVersionGate(vgS, qaRequiredVersion);
|
|
2667
|
+
if (!vgD.proceed) {
|
|
2668
|
+
return await parkTask(vgD.reason + ": " + vgD.remedy + " [built=" + vgS + " required=" + qaRequiredVersion + "]");
|
|
2669
|
+
}
|
|
2670
|
+
await agent(
|
|
2671
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("log-event", {
|
|
2672
|
+
task_id: taskId, type: "note", identity: step.identity,
|
|
2673
|
+
message: "version-check: built=" + vgS + " >= required=" + qaRequiredVersion + " → testing now" }),
|
|
2674
|
+
{ key: "record-version-check-" + taskId, label: "Recording version-gate pass" }
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2625
2677
|
var contentFindings = extractContentFindings(workerText);
|
|
2626
2678
|
if (!contentFindings.ok) {
|
|
2627
2679
|
// Unknown attribution fails closed: a missing or malformed
|
|
@@ -476,7 +476,13 @@ for (var pi = 0; pi < projects.length; pi++) {
|
|
|
476
476
|
// The user-facing surface for experiential QA routing (artifact |
|
|
477
477
|
// terminal | null=unclassified). Carried alongside deploy_type — it is a
|
|
478
478
|
// separate axis, not a redeclaration of the deployment target.
|
|
479
|
-
environment_type: proj.environment_type || null
|
|
479
|
+
environment_type: proj.environment_type || null,
|
|
480
|
+
// QA staleness gate (emojimanegg1/muse-crew#3): versioned_build opts the
|
|
481
|
+
// project into the Integrate version bump + QA bundle-freshness gate;
|
|
482
|
+
// version_file is the repo-relative path of the TS file exporting
|
|
483
|
+
// BUILD_NUMBER (null when unversioned).
|
|
484
|
+
versioned_build: !!proj.versioned_build,
|
|
485
|
+
version_file: proj.version_file || null
|
|
480
486
|
};
|
|
481
487
|
}
|
|
482
488
|
|
package/workflows/standard.js
CHANGED
|
@@ -571,6 +571,18 @@ function decideIntegrateRetry(o) {
|
|
|
571
571
|
if (rec.malformed) return "park";
|
|
572
572
|
return ancestor ? "skip-to-publish" : "proceed";
|
|
573
573
|
}
|
|
574
|
+
// QA version gate (issue #3): S=footer build, R=required. S>=R proves the
|
|
575
|
+
// bundle is at/past the merge (single-branch: version order = containment).
|
|
576
|
+
// Pure — pinned by tests/version-gate.test.js. Null = unreadable/missing.
|
|
577
|
+
function decideVersionGate(S, R) {
|
|
578
|
+
if (!(R > 0)) return { proceed: false, reason: "qa-version-missing",
|
|
579
|
+
remedy: "no build version recorded — re-run Integrate or record dashboard-version." };
|
|
580
|
+
if (!(S > 0)) return { proceed: false, reason: "qa-version-unreadable",
|
|
581
|
+
remedy: "no readable build number in bundle footer — rebuild from versioned source." };
|
|
582
|
+
if (S >= R) return { proceed: true };
|
|
583
|
+
return { proceed: false, reason: "qa-bundle-stale",
|
|
584
|
+
remedy: "bundle build " + S + " behind required " + R + " — builder did not rebuild from current source." };
|
|
585
|
+
}
|
|
574
586
|
// On a dispatcher retry resumed at Integrate, the run-local releaseDecision
|
|
575
587
|
// is null (Build doesn't re-run). Hydrate it from the merge record so the
|
|
576
588
|
// npm Publish gate doesn't park a publishable merge. Pure — pinned by
|
|
@@ -1300,6 +1312,22 @@ while (i < STEPS.length) {
|
|
|
1300
1312
|
var qaExperiential = false;
|
|
1301
1313
|
if (step.name === "QA") {
|
|
1302
1314
|
qaExperiential = (await resolveExperiential()) === "yes" && SURFACE_CLASSIFIED;
|
|
1315
|
+
var qaRequiredVersion = null;
|
|
1316
|
+
if (qaExperiential && SURFACE_ARTIFACT && projectConfig.versioned_build === true) {
|
|
1317
|
+
var vfileStd = projectConfig.version_file || "client/src/buildNumber.ts";
|
|
1318
|
+
var vgPreStd = await agent(
|
|
1319
|
+
"Return ONLY JSON. 1. Run, return stdout verbatim:\n" + crewCmd("get-events", { task_id: taskId }) + "\n" +
|
|
1320
|
+
"R = integer after `dashboard-version: ` in newest matching note. 2. If none, run, return stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
1321
|
+
"R = BUILD_NUMBER from: cd " + REPO_PATH + " && git show <source_commit>:" + vfileStd + " | grep -o 'BUILD_NUMBER = [0-9]*'. If unknown: {\"ok\":false,\"park\":\"qa-version-missing: no build version recorded — re-run Integrate or record dashboard-version.\"}. " +
|
|
1322
|
+
"3. cd " + REPO_PATH + " && git fetch origin && V=$(grep -o 'BUILD_NUMBER = [0-9]*' " + vfileStd + " | grep -o '[0-9]*'|head -1); if [ $V -lt R ]; then git pull --ff-only; V=$(grep -o 'BUILD_NUMBER = [0-9]*' " + vfileStd + " | grep -o '[0-9]*'|head -1); fi; " +
|
|
1323
|
+
"if [ $V -lt R ]: {\"ok\":false,\"park\":\"qa-bundle-stale: checkout \" + V + \" < required \" + R + \" — sync past the merge, re-run QA.\"}. 4. Else {\"ok\":true,\"R\":R}.",
|
|
1324
|
+
{ key: "qa-version-pre-" + taskId, label: "QA version-gate pre-check",
|
|
1325
|
+
schema: { type: "object", required: ["ok"],
|
|
1326
|
+
properties: { ok: { type: "boolean" }, R: { type: "integer" }, park: { type: "string" } } } }
|
|
1327
|
+
);
|
|
1328
|
+
if (!vgPreStd.ok) return await parkTask(vgPreStd.park);
|
|
1329
|
+
qaRequiredVersion = vgPreStd.R;
|
|
1330
|
+
}
|
|
1303
1331
|
}
|
|
1304
1332
|
|
|
1305
1333
|
// Step-specific instructions
|
|
@@ -1576,6 +1604,14 @@ while (i < STEPS.length) {
|
|
|
1576
1604
|
"R5-PUSH (manual R5 resolution only — the normal path pushed inline). Run: " + LIFECYCLE_ENV + LIFECYCLE + " push-target " + taskId + "\n" +
|
|
1577
1605
|
"PUSHED — report the merged hash (detached prints PUSHED: origin/main (refspec HEAD:main)), VERDICT: PASS; PUSH_SKIPPED — no push attempted (no record + no lock); NO_REMOTE_PUSH — no remote; VERDICT: PASS; ERROR or CONFLICT — report it, VERDICT: FAIL.\n" +
|
|
1578
1606
|
"NEVER force-push.\n\n" +
|
|
1607
|
+
(projectConfig.versioned_build === true ?
|
|
1608
|
+
"VERSION BUMP (issue #3): after MERGED+PUSHED, bump so QA can prove the bundle contains this fix — QA parks without it.\n" +
|
|
1609
|
+
"1. cd " + REPO_PATH + " && git fetch origin; BR=<branch-from-integration-target>; F=" + (projectConfig.version_file || "client/src/buildNumber.ts") + "\n" +
|
|
1610
|
+
"2. N=$(git show origin/$BR:$F | grep -o 'BUILD_NUMBER = [0-9]*' | grep -o '[0-9]*'); if missing/unparsable: VERDICT: FAIL.\n" +
|
|
1611
|
+
"3. Edit $F: `export const BUILD_NUMBER = $N;` → `export const BUILD_NUMBER = $((N+1));` (keep header comment).\n" +
|
|
1612
|
+
"4. git add $F && git commit -m \"build-number: $((N+1)) - QA version gate\" (SEPARATE commit, never amend).\n" +
|
|
1613
|
+
"5. git push origin $BR; if rejected retry 3x (fetch, re-read N, re-bump, re-commit, push). NEVER force-push. Push MUST succeed or VERDICT: FAIL.\n" +
|
|
1614
|
+
"6. Run, return stdout verbatim:\n" + crewCmd("log-event", { task_id: taskId, type: "note", identity: step.identity, message: "dashboard-version: <new> — QA must test a bundle built from source at or after the merge that recorded this (build <new> or later)." }) + "\n(substitute <new>).\n\n" : "") +
|
|
1579
1615
|
"Report what happened at each step, ending with exactly one line: VERDICT: PASS or VERDICT: FAIL.";
|
|
1580
1616
|
|
|
1581
1617
|
} else if (step.name === "Publish") {
|
|
@@ -2029,6 +2065,8 @@ while (i < STEPS.length) {
|
|
|
2029
2065
|
"c3. Start with: SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ node " + crewHome + "/current/lib/see-act.js --url http://localhost:<N>/ aria — read the JSON, log the step. Then: SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ node " + crewHome + "/current/lib/see-act.js --url http://localhost:<N>/ shot — READ the screenshot, log the step. Act on what you see: click, scroll, type, then re-observe, logging each step. Prefer aria (cheap text) to find controls; screenshot when the view changes and for your final verdict frames (one desktop, one mobile). If a click exits non-zero, do NOT retry the same ref blindly: re-run aria first (refs go stale between invocations), then click the fresh ref exactly once. If it still fails, log the failure and move on — a flaky control is a finding, not a loop.\n" +
|
|
2030
2066
|
"d. Reach: with the session protocol, any flow reachable by N in-page actions is drivable — open the deck, then click Study, then judge the study view. Without a session (one-shot invocations), anything reachable by (navigate, one action) is testable and sequences needing prior in-page state are not — use a session for those. Report NOT POSSIBLE only when the tooling itself fails (session-start exits 3): a flow you could not reach is not NOT POSSIBLE — name the exact step that stopped you in verdict.json's missing evidence and judge what you did reach.\n" +
|
|
2031
2067
|
"e. Judge as a user against the task description: does the change render correctly? Look for broken layout, overflow, missing or wrong content, stale data, and console errors. Compare against the task's expected behavior, never against source code (you are code-blind). Every frame you captured is already archived under " + crewHome + "/task-evidence/" + taskId + "/postchange/ and indexed in ooda-log.jsonl. A frame you did not read is not evidence. Loading, error, or blank frames never pass. If you cannot complete the loop, say exactly which steps are missing — unknown is not PASS.\n" +
|
|
2068
|
+
(projectConfig.versioned_build === true ?
|
|
2069
|
+
"e2. VERSION (issue #3): footer shows `build <n>` — report `footer_build: <n>` on its own line, or `footer_build: unreadable`. Required — the gate cannot pass without it.\n" : "") +
|
|
2032
2070
|
"f. Kill ONLY the server you started: pkill -f 'serve-artifact[.]js.*--tag " + taskId + "-qa' — never another task's server. (The [.] keeps pkill from matching its own command line.) Do not leave it running.\n" +
|
|
2033
2071
|
"Then continue with the mechanical checks below. Your VERDICT covers both the visual and the mechanical checks.\n\n" +
|
|
2034
2072
|
"STEP 2: Verify data integrity via the crew API.\n" +
|
|
@@ -2374,6 +2412,20 @@ while (i < STEPS.length) {
|
|
|
2374
2412
|
// reports FAIL, it stands — finding attribution informs follow-up
|
|
2375
2413
|
// filing only.
|
|
2376
2414
|
if (step.name === "QA") {
|
|
2415
|
+
if (projectConfig.versioned_build === true && SURFACE_ARTIFACT && qaRequiredVersion !== null) {
|
|
2416
|
+
var vgM = /footer_build:\s*(\d+|unreadable)/i.exec(workerText || "");
|
|
2417
|
+
var vgS = (vgM && /^\d+$/.test(vgM[1])) ? parseInt(vgM[1], 10) : null;
|
|
2418
|
+
var vgD = decideVersionGate(vgS, qaRequiredVersion);
|
|
2419
|
+
if (!vgD.proceed) {
|
|
2420
|
+
return await parkTask(vgD.reason + ": " + vgD.remedy + " [built=" + vgS + " required=" + qaRequiredVersion + "]");
|
|
2421
|
+
}
|
|
2422
|
+
await agent(
|
|
2423
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("log-event", {
|
|
2424
|
+
task_id: taskId, type: "note", identity: step.identity,
|
|
2425
|
+
message: "version-check: built=" + vgS + " >= required=" + qaRequiredVersion + " → testing now" }),
|
|
2426
|
+
{ key: "record-version-check-" + taskId, label: "Recording version-gate pass" }
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2377
2429
|
var contentFindings = extractContentFindings(workerText);
|
|
2378
2430
|
if (!contentFindings.ok) {
|
|
2379
2431
|
// Unknown attribution fails closed: a missing or malformed
|