muse-crew 0.7.14 → 0.7.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/crew-api.js +47 -2
- package/package.json +1 -1
- package/workflows/bugfix.js +41 -19
- package/workflows/chore.js +41 -19
- package/workflows/docs.js +6 -2
- package/workflows/standard.js +91 -22
package/lib/crew-api.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { readFileSync, existsSync, statSync, readlinkSync, readdirSync, appendFileSync } from "node:fs";
|
|
26
26
|
import { join, resolve, sep, basename } from "node:path";
|
|
27
27
|
import { randomUUID } from "node:crypto";
|
|
28
|
+
import { execFileSync } from "node:child_process";
|
|
28
29
|
import { DatabaseSync } from "node:sqlite";
|
|
29
30
|
import { homedir } from "node:os";
|
|
30
31
|
|
|
@@ -1540,7 +1541,50 @@ commands["record-phase"] = (db, args) => {
|
|
|
1540
1541
|
return { session: mapSession(sessionRow), event: mapEvent(eventRow) };
|
|
1541
1542
|
};
|
|
1542
1543
|
|
|
1543
|
-
|
|
1544
|
+
// Initial provenance stamp (2026-09-16, clean-room task ada95b71): the first
|
|
1545
|
+
// publish of an artifact project fell back to the empty-tree SHA as its diff
|
|
1546
|
+
// base because no provenance was ever stamped, turning the publish diff into
|
|
1547
|
+
// the whole repo tree (52 files / 4768 lines / 228KB) — unparseable through
|
|
1548
|
+
// the JSON transport and over the 200-line budget. The artifact is built from
|
|
1549
|
+
// the repo checkout at registration time, so create-project stamps
|
|
1550
|
+
// source_commit = repo HEAD for artifact-deploy projects; the first publish
|
|
1551
|
+
// then diffs only the task's own changes. The stamp never clobbers an
|
|
1552
|
+
// existing one (provenance storage is crew-home-global, so a second artifact
|
|
1553
|
+
// project keeps the first stamp — per-project provenance is a known gap this
|
|
1554
|
+
// does not introduce). A skipped stamp is reported, never silent, and never
|
|
1555
|
+
// fails project creation.
|
|
1556
|
+
function initialProvenancePlan({ deployType, provenanceExists, headSha, releaseName }) {
|
|
1557
|
+
if (deployType !== "artifact") return { stamped: false, reason: "deploy_type is not artifact" };
|
|
1558
|
+
if (provenanceExists) return { stamped: false, reason: "provenance already stamped" };
|
|
1559
|
+
if (!headSha) return { stamped: false, reason: "repo HEAD is unresolvable (unborn repo?)" };
|
|
1560
|
+
if (!releaseName) return { stamped: false, reason: "active crew release is unresolvable" };
|
|
1561
|
+
return { stamped: true, source_commit: headSha, crew_release: releaseName };
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function tryStampInitialProvenance(db, crewHome, repoPath, deployType) {
|
|
1565
|
+
const keys = db.prepare(
|
|
1566
|
+
"SELECT key FROM config WHERE key IN ('provenance.source_commit','provenance.crew_release','provenance.published_at')"
|
|
1567
|
+
).all().map((r) => r.key);
|
|
1568
|
+
const provenanceExists = keys.length === 3;
|
|
1569
|
+
let headSha = null;
|
|
1570
|
+
try {
|
|
1571
|
+
headSha = execFileSync("git", ["rev-parse", "HEAD"],
|
|
1572
|
+
{ cwd: repoPath, encoding: "utf8", timeout: 10000 }).trim();
|
|
1573
|
+
if (!/^[0-9a-f]{40}$/.test(headSha)) headSha = null;
|
|
1574
|
+
} catch { headSha = null; }
|
|
1575
|
+
let releaseName = null;
|
|
1576
|
+
try { releaseName = resolveActiveRelease(crewHome); } catch { releaseName = null; }
|
|
1577
|
+
const plan = initialProvenancePlan({ deployType, provenanceExists, headSha, releaseName });
|
|
1578
|
+
if (!plan.stamped) return plan;
|
|
1579
|
+
const upsert = db.prepare(
|
|
1580
|
+
"INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value");
|
|
1581
|
+
upsert.run("provenance.source_commit", plan.source_commit);
|
|
1582
|
+
upsert.run("provenance.crew_release", plan.crew_release);
|
|
1583
|
+
upsert.run("provenance.published_at", now());
|
|
1584
|
+
return plan;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
commands["create-project"] = (db, args, ctx) => {
|
|
1544
1588
|
const id = (args.id ?? "").trim();
|
|
1545
1589
|
if (!/^[a-z0-9-]+$/.test(id)) throw usageError("id must be a slug.");
|
|
1546
1590
|
const displayName = (args.display_name ?? "").trim();
|
|
@@ -1572,7 +1616,8 @@ commands["create-project"] = (db, args) => {
|
|
|
1572
1616
|
description, simultaneity, quiesced, visual_protocol, created_at, updated_at)
|
|
1573
1617
|
VALUES (@id, @display_name, @repo_path, @deploy_type, @deploy_slug,
|
|
1574
1618
|
@description, @simultaneity, @quiesced, @visual_protocol, @created_at, @updated_at)`).run(row);
|
|
1575
|
-
|
|
1619
|
+
const initialProvenance = tryStampInitialProvenance(db, ctx.crewHome, repoPath, deployType);
|
|
1620
|
+
return { project: mapProject(row), initial_provenance: initialProvenance };
|
|
1576
1621
|
};
|
|
1577
1622
|
|
|
1578
1623
|
commands["get-project"] = (db, args) => {
|
package/package.json
CHANGED
package/workflows/bugfix.js
CHANGED
|
@@ -50,8 +50,12 @@ const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"w
|
|
|
50
50
|
// Manual launches without the arg default to off (previous behavior).
|
|
51
51
|
var VISUAL_PROTOCOL_AVAILABLE = inputs.visual_protocol === true;
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
|
|
53
|
+
// crewHome is required — the dispatcher always passes it (crew-dispatch.js
|
|
54
|
+
// throws without it). Fail closed instead of silently defaulting to the dev
|
|
55
|
+
// home: a missing home is a loud error, a wrong home is silent corruption
|
|
56
|
+
// (2026-09-16: the silent default let a run resolve against the dev home).
|
|
57
|
+
if (!inputs.crewHome) throw new Error("crewHome is required — pass the crew home explicitly; no default");
|
|
58
|
+
const crewHome = inputs.crewHome;
|
|
55
59
|
// Crew API: the workflow calls the crew-owned CLI, not the dashboard.
|
|
56
60
|
// The CLI implements the API.md contract against $CREW_HOME/crew-state.db.
|
|
57
61
|
const CREW_API = crewHome + "/current/lib/crew-api.js";
|
|
@@ -1886,6 +1890,10 @@ while (i < STEPS.length) {
|
|
|
1886
1890
|
var pollSawOurBuild = false;
|
|
1887
1891
|
var pollSawStranger = false;
|
|
1888
1892
|
var lastObservedAgentId = null;
|
|
1893
|
+
// Poll-chunk failure record (2026-09-16, task 1febe8eb): chunk
|
|
1894
|
+
// agent calls that threw instead of returning a verdict —
|
|
1895
|
+
// recorded for the post-poll diagnosis, never terminal alone.
|
|
1896
|
+
var chunkFailures = [];
|
|
1889
1897
|
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
1890
1898
|
if (chunk > 1) {
|
|
1891
1899
|
var refreshPoll = await agent(
|
|
@@ -1906,22 +1914,36 @@ while (i < STEPS.length) {
|
|
|
1906
1914
|
var pollKey = (chunk === 1)
|
|
1907
1915
|
? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
|
|
1908
1916
|
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1917
|
+
try {
|
|
1918
|
+
buildPoll = await agent(
|
|
1919
|
+
"First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" \u2014 for OUR build only, the one whose agent_id is \"" + rebuildAgentId + "\" (the receipt captured when the edit was accepted; the agent_id is the artifact system's in-flight build correlation ID, stable across polls while the build runs). Check every 30 seconds, up to 7 checks (3.5 minutes max). On each check, read the raw build object:\n" +
|
|
1920
|
+
"On every check, record whether you have positively OBSERVED our build: a running build whose agent_id equals \"" + rebuildAgentId + "\", or a completed-build record whose agent_id equals \"" + rebuildAgentId + "\" (if the tool surfaces one \u2014 match it mechanically, never assume).\n" +
|
|
1921
|
+
"- If no build is running (build is null) and you have NOT observed our build: our build's completion is UNPROVEN. Absence of a running build is not evidence our build ran. Do NOT report done.\n" +
|
|
1922
|
+
"- If no build is running (build is null) and you previously observed our build running: our build finished. Stop and report done.\n" +
|
|
1923
|
+
"- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
|
|
1924
|
+
"- If the running build's agent_id is present but DIFFERENT: that is a stranger's build. Do NOT attribute its completion to our attempt and do NOT wait on it \u2014 keep checking within budget; if the budget expires without observing our build, report done=false. Record it in saw_stranger regardless of what else you observe.\n" +
|
|
1925
|
+
"Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\" } and nothing else.",
|
|
1926
|
+
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
1927
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
|
|
1928
|
+
timeoutMs: 270000 }
|
|
1929
|
+
);
|
|
1930
|
+
} catch (chunkErr) {
|
|
1931
|
+
// A hung or failed chunk is inconclusive, never terminal:
|
|
1932
|
+
// record it and continue to the next chunk. (2026-09-16,
|
|
1933
|
+
// clean-room task 1febe8eb: the platform's 270s agent
|
|
1934
|
+
// timeout killed chunk 2, which threw out of this loop —
|
|
1935
|
+
// skipping chunk 3 AND the STEP 1b audit-dir fallback and
|
|
1936
|
+
// parking on the exception path.) Fail-closed still applies
|
|
1937
|
+
// after chunk 3 and the fallback are exhausted.
|
|
1938
|
+
chunkFailures.push("chunk " + chunk + ": " + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr));
|
|
1939
|
+
log("Artifact build poll chunk " + chunk + " of 3 failed (" + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr) + ") \u2014 continuing to the next chunk; build completion still unproven.");
|
|
1940
|
+
}
|
|
1941
|
+
if (buildPoll) {
|
|
1942
|
+
pollSawOurBuild = pollSawOurBuild || (buildPoll.saw_our_build === true);
|
|
1943
|
+
pollSawStranger = pollSawStranger || (buildPoll.saw_stranger === true);
|
|
1944
|
+
lastObservedAgentId = buildPoll.observed_agent_id || null;
|
|
1945
|
+
if (buildPoll.build_done) { break; }
|
|
1946
|
+
}
|
|
1925
1947
|
}
|
|
1926
1948
|
if (!buildPoll || !buildPoll.build_done) {
|
|
1927
1949
|
buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
|
|
@@ -2044,7 +2066,7 @@ while (i < STEPS.length) {
|
|
|
2044
2066
|
var unattributableReason = strangerObserved ? "stranger-build-observed-during-poll"
|
|
2045
2067
|
: (pollEndState === "build-still-running-at-poll-end" ? "build-still-running-at-poll-end"
|
|
2046
2068
|
: (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing"));
|
|
2047
|
-
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2069
|
+
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + "; poll_chunks_failed=" + chunkFailures.length + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2048
2070
|
await recordPublishLedger({
|
|
2049
2071
|
commit: mergeCommitForPublish,
|
|
2050
2072
|
attempt: rebuildAttemptKey,
|
package/workflows/chore.js
CHANGED
|
@@ -47,8 +47,12 @@ const NEXT_PHASE_ROUTED = (typeof inputs.next_phase === "string" && inputs.next_
|
|
|
47
47
|
// Manual launches without the arg default to off (previous behavior).
|
|
48
48
|
var VISUAL_PROTOCOL_AVAILABLE = inputs.visual_protocol === true;
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
|
|
50
|
+
// crewHome is required — the dispatcher always passes it (crew-dispatch.js
|
|
51
|
+
// throws without it). Fail closed instead of silently defaulting to the dev
|
|
52
|
+
// home: a missing home is a loud error, a wrong home is silent corruption
|
|
53
|
+
// (2026-09-16: the silent default let a run resolve against the dev home).
|
|
54
|
+
if (!inputs.crewHome) throw new Error("crewHome is required — pass the crew home explicitly; no default");
|
|
55
|
+
const crewHome = inputs.crewHome;
|
|
52
56
|
// Crew API: the workflow calls the crew-owned CLI, not the dashboard.
|
|
53
57
|
// The CLI implements the API.md contract against $CREW_HOME/crew-state.db.
|
|
54
58
|
const CREW_API = crewHome + "/current/lib/crew-api.js";
|
|
@@ -1899,6 +1903,10 @@ while (i < STEPS.length) {
|
|
|
1899
1903
|
var pollSawOurBuild = false;
|
|
1900
1904
|
var pollSawStranger = false;
|
|
1901
1905
|
var lastObservedAgentId = null;
|
|
1906
|
+
// Poll-chunk failure record (2026-09-16, task 1febe8eb): chunk
|
|
1907
|
+
// agent calls that threw instead of returning a verdict —
|
|
1908
|
+
// recorded for the post-poll diagnosis, never terminal alone.
|
|
1909
|
+
var chunkFailures = [];
|
|
1902
1910
|
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
1903
1911
|
if (chunk > 1) {
|
|
1904
1912
|
var refreshPoll = await agent(
|
|
@@ -1919,22 +1927,36 @@ while (i < STEPS.length) {
|
|
|
1919
1927
|
var pollKey = (chunk === 1)
|
|
1920
1928
|
? attemptKey("publish-artifact-poll-" + taskId, reworkCount)
|
|
1921
1929
|
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, reworkCount);
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1930
|
+
try {
|
|
1931
|
+
buildPoll = await agent(
|
|
1932
|
+
"First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" \u2014 for OUR build only, the one whose agent_id is \"" + rebuildAgentId + "\" (the receipt captured when the edit was accepted; the agent_id is the artifact system's in-flight build correlation ID, stable across polls while the build runs). Check every 30 seconds, up to 7 checks (3.5 minutes max). On each check, read the raw build object:\n" +
|
|
1933
|
+
"On every check, record whether you have positively OBSERVED our build: a running build whose agent_id equals \"" + rebuildAgentId + "\", or a completed-build record whose agent_id equals \"" + rebuildAgentId + "\" (if the tool surfaces one \u2014 match it mechanically, never assume).\n" +
|
|
1934
|
+
"- If no build is running (build is null) and you have NOT observed our build: our build's completion is UNPROVEN. Absence of a running build is not evidence our build ran. Do NOT report done.\n" +
|
|
1935
|
+
"- If no build is running (build is null) and you previously observed our build running: our build finished. Stop and report done.\n" +
|
|
1936
|
+
"- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
|
|
1937
|
+
"- If the running build's agent_id is present but DIFFERENT: that is a stranger's build. Do NOT attribute its completion to our attempt and do NOT wait on it \u2014 keep checking within budget; if the budget expires without observing our build, report done=false. Record it in saw_stranger regardless of what else you observe.\n" +
|
|
1938
|
+
"Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\" } and nothing else.",
|
|
1939
|
+
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
1940
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
|
|
1941
|
+
timeoutMs: 270000 }
|
|
1942
|
+
);
|
|
1943
|
+
} catch (chunkErr) {
|
|
1944
|
+
// A hung or failed chunk is inconclusive, never terminal:
|
|
1945
|
+
// record it and continue to the next chunk. (2026-09-16,
|
|
1946
|
+
// clean-room task 1febe8eb: the platform's 270s agent
|
|
1947
|
+
// timeout killed chunk 2, which threw out of this loop —
|
|
1948
|
+
// skipping chunk 3 AND the STEP 1b audit-dir fallback and
|
|
1949
|
+
// parking on the exception path.) Fail-closed still applies
|
|
1950
|
+
// after chunk 3 and the fallback are exhausted.
|
|
1951
|
+
chunkFailures.push("chunk " + chunk + ": " + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr));
|
|
1952
|
+
log("Artifact build poll chunk " + chunk + " of 3 failed (" + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr) + ") \u2014 continuing to the next chunk; build completion still unproven.");
|
|
1953
|
+
}
|
|
1954
|
+
if (buildPoll) {
|
|
1955
|
+
pollSawOurBuild = pollSawOurBuild || (buildPoll.saw_our_build === true);
|
|
1956
|
+
pollSawStranger = pollSawStranger || (buildPoll.saw_stranger === true);
|
|
1957
|
+
lastObservedAgentId = buildPoll.observed_agent_id || null;
|
|
1958
|
+
if (buildPoll.build_done) { break; }
|
|
1959
|
+
}
|
|
1938
1960
|
}
|
|
1939
1961
|
if (!buildPoll || !buildPoll.build_done) {
|
|
1940
1962
|
buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
|
|
@@ -2055,7 +2077,7 @@ while (i < STEPS.length) {
|
|
|
2055
2077
|
var unattributableReason = strangerObserved ? "stranger-build-observed-during-poll"
|
|
2056
2078
|
: (pollEndState === "build-still-running-at-poll-end" ? "build-still-running-at-poll-end"
|
|
2057
2079
|
: (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing"));
|
|
2058
|
-
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2080
|
+
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + "; poll_chunks_failed=" + chunkFailures.length + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2059
2081
|
await recordPublishLedger({
|
|
2060
2082
|
commit: mergeCommitForPublish,
|
|
2061
2083
|
attempt: rebuildAttemptKey,
|
package/workflows/docs.js
CHANGED
|
@@ -32,8 +32,12 @@ const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
|
|
|
32
32
|
// what the dispatcher routed on.
|
|
33
33
|
const NEXT_PHASE_ROUTED = (typeof inputs.next_phase === "string" && inputs.next_phase.length > 0) ? inputs.next_phase : null;
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
35
|
+
// crewHome is required — the dispatcher always passes it (crew-dispatch.js
|
|
36
|
+
// throws without it). Fail closed instead of silently defaulting to the dev
|
|
37
|
+
// home: a missing home is a loud error, a wrong home is silent corruption
|
|
38
|
+
// (2026-09-16: the silent default let a run resolve against the dev home).
|
|
39
|
+
if (!inputs.crewHome) throw new Error("crewHome is required — pass the crew home explicitly; no default");
|
|
40
|
+
const crewHome = inputs.crewHome;
|
|
37
41
|
// Crew API: the workflow calls the crew-owned CLI, not the dashboard.
|
|
38
42
|
// The CLI implements the API.md contract against $CREW_HOME/crew-state.db.
|
|
39
43
|
const CREW_API = crewHome + "/current/lib/crew-api.js";
|
package/workflows/standard.js
CHANGED
|
@@ -49,8 +49,12 @@ const CLAIM_WORKFLOW_PERSIST = (WORKFLOW_WAS_NULL && RESOLVED_WORKFLOW) ? ", \"w
|
|
|
49
49
|
// Manual launches without the arg default to off (previous behavior).
|
|
50
50
|
var VISUAL_PROTOCOL_AVAILABLE = inputs.visual_protocol === true;
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
|
|
52
|
+
// crewHome is required — the dispatcher always passes it (crew-dispatch.js
|
|
53
|
+
// throws without it). Fail closed instead of silently defaulting to the dev
|
|
54
|
+
// home: a missing home is a loud error, a wrong home is silent corruption
|
|
55
|
+
// (2026-09-16: the silent default let a run resolve against the dev home).
|
|
56
|
+
if (!inputs.crewHome) throw new Error("crewHome is required — pass the crew home explicitly; no default");
|
|
57
|
+
const crewHome = inputs.crewHome;
|
|
54
58
|
// Crew API: the workflow calls the crew-owned CLI, not the dashboard.
|
|
55
59
|
// The CLI implements the API.md contract against $CREW_HOME/crew-state.db.
|
|
56
60
|
const CREW_API = crewHome + "/current/lib/crew-api.js";
|
|
@@ -67,6 +71,14 @@ const ORCH_PATH = crewHome + "/.orchestration";
|
|
|
67
71
|
const LIFECYCLE_SRC = crewHome + "/lib/worktree-lifecycle.sh";
|
|
68
72
|
const MERGE_LOCK_SRC = crewHome + "/lib/merge-lock.sh";
|
|
69
73
|
const RUN_LIB = crewHome + "/.pins/" + taskId; // persistent disk, NOT /tmp (tmpfs wiped by cell reboots — canary b5efd1b1)
|
|
74
|
+
// Deterministic spec path — computed by the workflow, never by the agent.
|
|
75
|
+
// Map writes the spec to exactly SPEC_PATH; the path is handed to the agent
|
|
76
|
+
// as a fact and verified mechanically after the step. Agents must never
|
|
77
|
+
// compose crew-home paths themselves (2026-09-16: a Map agent saved the spec
|
|
78
|
+
// under the platform's ~/workspace/.jarvis/workflow-runs/ run dir because the
|
|
79
|
+
// prompt named a directory and the agent invented the rest of the path).
|
|
80
|
+
const SPEC_DIR = crewHome + "/task-evidence/" + taskId + "/map";
|
|
81
|
+
const SPEC_PATH = SPEC_DIR + "/spec.md";
|
|
70
82
|
const LIFECYCLE = RUN_LIB + "/worktree-lifecycle.sh";
|
|
71
83
|
const MERGE_LOCK = RUN_LIB + "/merge-lock.sh";
|
|
72
84
|
const PUBLISH_NPM_SRC = crewHome + "/lib/publish-npm.sh";
|
|
@@ -1213,7 +1225,7 @@ while (i < STEPS.length) {
|
|
|
1213
1225
|
" If the baseline evidence is missing with no baseline:none recorded, do not write the spec — report 'baseline evidence missing — Map gate bounce required' and stop.\n" +
|
|
1214
1226
|
"Declare capture targets for the post-change visual capture: end your report with a line `capture_targets: <comma-separated views/controls this change affects>` (optional; falls back to the task description).";
|
|
1215
1227
|
}
|
|
1216
|
-
instructions = "Research the problem space, evaluate options, pick the shortest path.\nWrite a clear spec that a builder can execute without asking questions.\nThe builder will edit source files in a git worktree.\nProject: " + PROJECT_DESC + "\nSave the spec to
|
|
1228
|
+
instructions = "Research the problem space, evaluate options, pick the shortest path.\nWrite a clear spec that a builder can execute without asking questions.\nThe builder will edit source files in a git worktree.\nProject: " + PROJECT_DESC + "\nSave the spec to exactly this file: " + SPEC_PATH + " — run mkdir -p \"" + SPEC_DIR + "\" first. Do not save the spec anywhere else; this exact path is fixed and will be checked mechanically after your step.\nReport back in plain prose — what you specified." + mapGatePara;
|
|
1217
1229
|
|
|
1218
1230
|
} else if (step.name === "Build") {
|
|
1219
1231
|
instructions = "STEP 1: Prepare your worktree.\n" +
|
|
@@ -1265,7 +1277,7 @@ while (i < STEPS.length) {
|
|
|
1265
1277
|
}
|
|
1266
1278
|
}
|
|
1267
1279
|
instructions = "Review independently and cold. You have NOT seen any reasoning from the builder.\nDo NOT access the task dashboard, event log, or any comments. Your review is based solely on the spec and the code.\n\n" +
|
|
1268
|
-
(mapperSpec ? "MAPPER'S SPEC (the builder was asked to implement exactly this):\n" + mapperSpec + "\n\n" : "Read the spec (
|
|
1280
|
+
(mapperSpec ? "MAPPER'S SPEC (the builder was asked to implement exactly this):\n" + mapperSpec + "\n\n" : "Read the spec at exactly " + SPEC_PATH + " (fall back to the task description if the file is absent).\n\n") +
|
|
1269
1281
|
"Examine the code changes by running:\n" +
|
|
1270
1282
|
LIFECYCLE_ENV + LIFECYCLE + " inspect " + taskId + "\n\n" +
|
|
1271
1283
|
"The inspect output is authoritative: it prints the task branch's actual tip commit (TIP) and every commit ahead of main. Base your review ONLY on this output — do NOT run git log yourself to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n" +
|
|
@@ -1843,6 +1855,10 @@ while (i < STEPS.length) {
|
|
|
1843
1855
|
var pollSawOurBuild = false;
|
|
1844
1856
|
var pollSawStranger = false;
|
|
1845
1857
|
var lastObservedAgentId = null;
|
|
1858
|
+
// Poll-chunk failure record (2026-09-16, task 1febe8eb): chunk
|
|
1859
|
+
// agent calls that threw instead of returning a verdict —
|
|
1860
|
+
// recorded for the post-poll diagnosis, never terminal alone.
|
|
1861
|
+
var chunkFailures = [];
|
|
1846
1862
|
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
1847
1863
|
if (chunk > 1) {
|
|
1848
1864
|
var refreshPoll = await agent(
|
|
@@ -1863,22 +1879,36 @@ while (i < STEPS.length) {
|
|
|
1863
1879
|
var pollKey = (chunk === 1)
|
|
1864
1880
|
? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
|
|
1865
1881
|
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
+
try {
|
|
1883
|
+
buildPoll = await agent(
|
|
1884
|
+
"First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" \u2014 for OUR build only, the one whose agent_id is \"" + rebuildAgentId + "\" (the receipt captured when the edit was accepted; the agent_id is the artifact system's in-flight build correlation ID, stable across polls while the build runs). Check every 30 seconds, up to 7 checks (3.5 minutes max). On each check, read the raw build object:\n" +
|
|
1885
|
+
"On every check, record whether you have positively OBSERVED our build: a running build whose agent_id equals \"" + rebuildAgentId + "\", or a completed-build record whose agent_id equals \"" + rebuildAgentId + "\" (if the tool surfaces one \u2014 match it mechanically, never assume).\n" +
|
|
1886
|
+
"- If no build is running (build is null) and you have NOT observed our build: our build's completion is UNPROVEN. Absence of a running build is not evidence our build ran. Do NOT report done.\n" +
|
|
1887
|
+
"- If no build is running (build is null) and you previously observed our build running: our build finished. Stop and report done.\n" +
|
|
1888
|
+
"- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
|
|
1889
|
+
"- If the running build's agent_id is present but DIFFERENT: that is a stranger's build. Do NOT attribute its completion to our attempt and do NOT wait on it \u2014 keep checking within budget; if the budget expires without observing our build, report done=false. Record it in saw_stranger regardless of what else you observe.\n" +
|
|
1890
|
+
"Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\" } and nothing else.",
|
|
1891
|
+
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
1892
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
|
|
1893
|
+
timeoutMs: 270000 }
|
|
1894
|
+
);
|
|
1895
|
+
} catch (chunkErr) {
|
|
1896
|
+
// A hung or failed chunk is inconclusive, never terminal:
|
|
1897
|
+
// record it and continue to the next chunk. (2026-09-16,
|
|
1898
|
+
// clean-room task 1febe8eb: the platform's 270s agent
|
|
1899
|
+
// timeout killed chunk 2, which threw out of this loop —
|
|
1900
|
+
// skipping chunk 3 AND the STEP 1b audit-dir fallback and
|
|
1901
|
+
// parking on the exception path.) Fail-closed still applies
|
|
1902
|
+
// after chunk 3 and the fallback are exhausted.
|
|
1903
|
+
chunkFailures.push("chunk " + chunk + ": " + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr));
|
|
1904
|
+
log("Artifact build poll chunk " + chunk + " of 3 failed (" + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr) + ") \u2014 continuing to the next chunk; build completion still unproven.");
|
|
1905
|
+
}
|
|
1906
|
+
if (buildPoll) {
|
|
1907
|
+
pollSawOurBuild = pollSawOurBuild || (buildPoll.saw_our_build === true);
|
|
1908
|
+
pollSawStranger = pollSawStranger || (buildPoll.saw_stranger === true);
|
|
1909
|
+
lastObservedAgentId = buildPoll.observed_agent_id || null;
|
|
1910
|
+
if (buildPoll.build_done) { break; }
|
|
1911
|
+
}
|
|
1882
1912
|
}
|
|
1883
1913
|
if (!buildPoll || !buildPoll.build_done) {
|
|
1884
1914
|
buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
|
|
@@ -2001,7 +2031,7 @@ while (i < STEPS.length) {
|
|
|
2001
2031
|
var unattributableReason = strangerObserved ? "stranger-build-observed-during-poll"
|
|
2002
2032
|
: (pollEndState === "build-still-running-at-poll-end" ? "build-still-running-at-poll-end"
|
|
2003
2033
|
: (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing"));
|
|
2004
|
-
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2034
|
+
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + "; poll_chunks_failed=" + chunkFailures.length + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2005
2035
|
await recordPublishLedger({
|
|
2006
2036
|
commit: mergeCommitForPublish,
|
|
2007
2037
|
attempt: rebuildAttemptKey,
|
|
@@ -2427,11 +2457,40 @@ while (i < STEPS.length) {
|
|
|
2427
2457
|
}
|
|
2428
2458
|
}
|
|
2429
2459
|
|
|
2460
|
+
// Deterministic spec-path verification: the agent cannot self-certify where
|
|
2461
|
+
// it saved the spec. After Map passes, the workflow confirms mechanically
|
|
2462
|
+
// that the spec file exists at the workflow-computed SPEC_PATH (2026-09-16:
|
|
2463
|
+
// a Map agent saved the spec under the platform's
|
|
2464
|
+
// ~/workspace/.jarvis/workflow-runs/ run dir instead of the crew home,
|
|
2465
|
+
// because the prompt named a directory and the agent composed the path).
|
|
2466
|
+
// A missing spec file is an operational step failure, not a park.
|
|
2467
|
+
if (step.name === "Map" && passed) {
|
|
2468
|
+
var specVerifyOut = "";
|
|
2469
|
+
try {
|
|
2470
|
+
var specVerifyResult = await agent(
|
|
2471
|
+
"Run: test -f \"" + SPEC_PATH + "\" && echo SPEC_PRESENT || echo SPEC_MISSING\n" +
|
|
2472
|
+
"Return JSON { \"output\": \"<the command's full stdout, trimmed>\" } and nothing else.",
|
|
2473
|
+
{ key: attemptKey("verify-spec-" + taskId, totalReworkCount), label: "Verifying spec file landed",
|
|
2474
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
2475
|
+
);
|
|
2476
|
+
specVerifyOut = (specVerifyResult.output || "").trim();
|
|
2477
|
+
} catch (e) {
|
|
2478
|
+
specVerifyOut = "";
|
|
2479
|
+
}
|
|
2480
|
+
if (/^SPEC_PRESENT/m.test(specVerifyOut)) {
|
|
2481
|
+
log("Spec verified for task " + taskId + " at " + SPEC_PATH);
|
|
2482
|
+
} else {
|
|
2483
|
+
log("Spec verification failed for task " + taskId + ": no spec file at " + SPEC_PATH + " — marking failed for retry");
|
|
2484
|
+
stepResult.summary = (stepResult.summary || "") + "\nspec_verify: FAILED — no spec file at " + SPEC_PATH;
|
|
2485
|
+
passed = false;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2430
2489
|
// "rejected" is an explicit phase verdict routed through rework (Review/QA,
|
|
2431
2490
|
// and the Build/Reproduce reports that feed them). Integrate/Publish work
|
|
2432
2491
|
// that did not finish is operational — "failed", retryable under the
|
|
2433
2492
|
// dispatcher's consecutive-failure cap.
|
|
2434
|
-
const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
|
|
2493
|
+
const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" || step.name === "Map" ? "failed" : "rejected");
|
|
2435
2494
|
|
|
2436
2495
|
// Deterministic publish verification: the agent cannot self-certify a publish.
|
|
2437
2496
|
// Skip-aware (park 2026-09-11): when the deterministic publish script found
|
|
@@ -2616,6 +2675,16 @@ while (i < STEPS.length) {
|
|
|
2616
2675
|
return { status: "failed", task_id: taskId, reason: "Publish failed: " + summary };
|
|
2617
2676
|
}
|
|
2618
2677
|
|
|
2678
|
+
// Map spec failure is operational (the spec file did not land at the
|
|
2679
|
+
// workflow-computed SPEC_PATH), not a verdict: the session above is
|
|
2680
|
+
// recorded "failed" and the dispatcher retries at Map under its
|
|
2681
|
+
// consecutive-failure cap, parking after the cap. The retry re-runs Map
|
|
2682
|
+
// with the same exact-path instructions against the same SPEC_PATH.
|
|
2683
|
+
if (!passed && step.name === "Map") {
|
|
2684
|
+
log("Map spec verification failed for task " + taskId + " — returning failed for dispatcher retry");
|
|
2685
|
+
return { status: "failed", task_id: taskId, reason: "Map spec verification failed: no spec file at " + SPEC_PATH };
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2619
2688
|
// Publish verification park: the build landed and post-deploy finalized,
|
|
2620
2689
|
// but provenance is UNSTAMPED until the parent's independent read-back
|
|
2621
2690
|
// (docs/publish-verification.md) confirms the artifact's actual content
|