@@ -0,0 +1,58 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "node:test";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = process.env.MOSAIC_CONTRACT_ROOT ?? join(HERE, "fixtures");
|
||||
|
||||
const proactive = readFileSync(join(ROOT, "skills-local", "ms-proactive-agent", "SKILL.md"), "utf8");
|
||||
const executive = readFileSync(join(ROOT, "skills-local", "ms-executive-update", "SKILL.md"), "utf8");
|
||||
const honesty = readFileSync(join(ROOT, "skills-local", "ms-honesty", "SKILL.md"), "utf8");
|
||||
|
||||
test("cross-seat waits require communication rather than agent-watch polling", () => {
|
||||
assert.equal(
|
||||
proactive.includes("A row waiting on someone else is not a candidate; it gets a watch"),
|
||||
false,
|
||||
"proactive contract must not contradict ms-agent-watch",
|
||||
);
|
||||
assert.match(proactive, /Use existing message delivery for another agent.s response/);
|
||||
assert.match(proactive, /Do not poll its private\s+files or pane/);
|
||||
assert.match(proactive, /external\s+conditions with no existing wake owner/);
|
||||
});
|
||||
|
||||
test("proactive turns close every communication obligation before reporting", () => {
|
||||
assert.match(proactive, /Close communication obligations/);
|
||||
for (const required of [
|
||||
"persist any required",
|
||||
"exact receipt",
|
||||
"return event",
|
||||
"escalation owner",
|
||||
"A report to the user is not delivery to another agent",
|
||||
]) {
|
||||
assert.equal(proactive.includes(required), true, `missing proactive closeout requirement: ${required}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("executive updates cannot replace direct owner communication", () => {
|
||||
assert.match(executive, /## Communication closeout before reporting/);
|
||||
assert.match(executive, /An executive update does not replace direct communication/);
|
||||
assert.match(executive, /A no-change line does not satisfy an unsent acknowledgement, handoff, review request,\s+blocker, or result/);
|
||||
});
|
||||
|
||||
test("honesty classifies communication state and routes access limits", () => {
|
||||
assert.match(honesty, /## Communication evidence/);
|
||||
assert.match(honesty, /Drafted is not sent\. Sent is not delivered\. Delivered is not acknowledged\. Acknowledged is not completed\./);
|
||||
assert.match(honesty, /A credential refusal is correct behavior, but it is not a terminal communication state/);
|
||||
assert.equal(
|
||||
honesty.includes("actually re-measured the thing you are waiting on, or a watch is armed to do so"),
|
||||
false,
|
||||
"another-seat waits must not inherit the stale remeasure-or-watch binary",
|
||||
);
|
||||
assert.match(
|
||||
honesty,
|
||||
/Another-seat and operator waits use the\s+recorded delivery receipt, required return event, escalation owner, and existing wake\s+path\. They do not require polling or an `agent-watch`\./,
|
||||
);
|
||||
assert.match(honesty, /External-condition waits require\s+a fresh measurement or a permitted watch\./);
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { goalDisplay, goalDetails } from "../lib/display.ts";
|
||||
import { initialState, setGoal, pauseGoal, blockGoal, resumeGoal, completeGoal, clearGoal } from "../lib/state.ts";
|
||||
import { saveState, loadState } from "../lib/store.ts";
|
||||
|
||||
const text = "Long goal\n" + "Full acceptance criteria, never truncate. ".repeat(100) + "\nEND-OF-GOAL";
|
||||
|
||||
test("footer maps each lifecycle state to text and a theme role", () => {
|
||||
const active = setGoal(initialState(), text);
|
||||
for (const [state, label, color] of [
|
||||
[initialState(), "None", "muted"],
|
||||
[active, "Active", "accent"],
|
||||
[{ ...active, activeWait: { owner: "reviewer" } }, "Waiting", "warning"],
|
||||
[{ ...active, activeWait: { owner: "reviewer", wakeSent: true } }, "Active", "accent"],
|
||||
[pauseGoal(active, "operator stop"), "Paused", "warning"],
|
||||
[pauseGoal(active, "blocked: legacy reason"), "Blocked", "error"],
|
||||
[blockGoal(active, "missing permission"), "Blocked", "error"],
|
||||
[completeGoal(active, "verified"), "Complete", "success"],
|
||||
] as const) assert.deepEqual(goalDisplay(state), { label, color });
|
||||
});
|
||||
|
||||
test("full multiline goal survives completion, persistence, and read-only recall", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-display-"));
|
||||
try {
|
||||
const active = setGoal(initialState(), text);
|
||||
const complete = completeGoal(active, "verified ".repeat(300));
|
||||
assert.equal(complete.lastOutcome?.text, text);
|
||||
assert.equal(complete.lastOutcome?.evidence.length, 1000);
|
||||
assert.equal(complete.status, "none");
|
||||
assert.deepEqual(resumeGoal(complete), complete);
|
||||
const path = join(dir, "state.json");
|
||||
saveState(complete, path);
|
||||
assert.deepEqual(loadState(path), complete);
|
||||
for (const state of [active, blockGoal(active, "denied"), pauseGoal(active, "stop"), loadState(path)]) {
|
||||
const before = JSON.stringify(state);
|
||||
assert.ok(goalDetails(state).includes(text));
|
||||
assert.equal(JSON.stringify(state), before);
|
||||
}
|
||||
assert.equal(goalDisplay(clearGoal(complete)).label, "None");
|
||||
assert.equal(setGoal(complete, "replacement").lastOutcome, undefined);
|
||||
assert.equal(resumeGoal(blockGoal(active, "denied")).status, "active");
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("old records and malformed outcome fields are safe to recall", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-display-"));
|
||||
const path = join(dir, "state.json");
|
||||
try {
|
||||
const old = pauseGoal(setGoal(initialState(), text), "blocked: legacy");
|
||||
saveState(old, path);
|
||||
assert.equal(goalDisplay(loadState(path)).label, "Blocked");
|
||||
for (const lastOutcome of [null, {}, { status: "complete", text: 123 }, { status: "complete", text, evidence: "yes", at: "invalid" }]) {
|
||||
saveState({ ...initialState(), lastOutcome } as any, path);
|
||||
assert.equal(goalDisplay(loadState(path)).label, "None");
|
||||
assert.doesNotThrow(() => goalDetails(loadState(path)));
|
||||
}
|
||||
saveState({ ...old, pausedReason: {} } as any, path);
|
||||
assert.doesNotThrow(() => goalDetails(loadState(path)));
|
||||
assert.doesNotThrow(() => goalDetails({ ...setGoal(initialState(), text), activeWait: { owner: "test", deadlineAt: 1e100 } }));
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
// T165 WP1 parser conformance fixtures derived from the cited Machine contract.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { parseExecutiveUpdate, validateAttestedGoalReport } from "../lib/executive-update.ts";
|
||||
import type { GoalPolicyPublication } from "../../mosaic-core/lib/goal-policy.ts";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const CONTRACT = readFileSync(join(HERE, "fixtures", "skills-local", "ms-executive-update", "SKILL.md"), "utf8");
|
||||
const NO_CHANGE = "No change since T165; still waiting on T165.";
|
||||
|
||||
const UPDATE = [
|
||||
"Just Completed:",
|
||||
"",
|
||||
"* T165: completed contracts",
|
||||
"",
|
||||
"Next Step:",
|
||||
"",
|
||||
"* `fleet/extensions/goal/index.ts`: validate policy",
|
||||
"",
|
||||
"Blocked:",
|
||||
"",
|
||||
"* GOAL-PROACTIVE-LOOP-REV-Q107: review complete \u2014 nothing from you",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
function publication(resolver: GoalPolicyPublication["resolver"]): GoalPolicyPublication {
|
||||
return {
|
||||
attestation: Object.freeze({
|
||||
schemaVersion: 1,
|
||||
role: "plan-ng",
|
||||
roleRevision: 4,
|
||||
manifestSha256: "a".repeat(64),
|
||||
format: "ms-executive-update/v1",
|
||||
contractPath: "skills-local/ms-executive-update/SKILL.md",
|
||||
contractSection: "Machine contract (for `goal_report` payloads and any parser)",
|
||||
contractBlob: "df30c6fbb54b4a65a298c9e51c07f742610d171c",
|
||||
contractSha256: "bbea48a46b1f8da7bc759f86856fb52830b7dde456b826317163c6dc6ccab319",
|
||||
enforcement: "pre-state-change-fail-closed",
|
||||
identifierResolution: "consumer-fail-closed",
|
||||
launchGeneration: 1,
|
||||
incarnationId: "inc-goal-parser",
|
||||
}),
|
||||
resolver,
|
||||
};
|
||||
}
|
||||
|
||||
function resolved(options: { changed?: boolean; evidence?: string | null; frozen?: boolean } = {}) {
|
||||
const value = {
|
||||
outcome: "resolved" as const,
|
||||
objectId: "tracked-object",
|
||||
objectSha256: "b".repeat(64),
|
||||
changedSincePreviousAcceptedReport: options.changed ?? true,
|
||||
completionEvidenceId: options.evidence === undefined ? "immutable-evidence" : options.evidence,
|
||||
};
|
||||
return options.frozen === false ? value : Object.freeze(value);
|
||||
}
|
||||
|
||||
test("C7: cited Machine contract remains the parser's one input authority", () => {
|
||||
assert.equal(CONTRACT.includes("## Machine contract (for `goal_report` payloads and any parser)"), true);
|
||||
assert.equal(CONTRACT.includes("update := section(\"Just Completed\")"), true);
|
||||
const parsed = parseExecutiveUpdate(UPDATE);
|
||||
assert.equal(parsed.ok, true, parsed.ok ? "" : parsed.reason);
|
||||
if (parsed.ok) {
|
||||
assert.deepEqual(parsed.items.map((item) => [item.section, item.token]), [
|
||||
["Just Completed", "T165"],
|
||||
["Next Step", "`fleet/extensions/goal/index.ts`"],
|
||||
["Blocked", "GOAL-PROACTIVE-LOOP-REV-Q107"],
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test("C7: heading, LF, identifier, bullet count, and Blocked disposition violations reject", () => {
|
||||
const tooMany = [
|
||||
"Just Completed:", "", "* none", "", "Next Step:", "",
|
||||
"* T1: one", "* T2: two", "* T3: three", "* T4: four", "* T5: five", "* T6: six",
|
||||
"", "Blocked:", "", "* T1: none \u2014 nothing from you", "",
|
||||
].join("\n");
|
||||
for (const invalid of [
|
||||
UPDATE.replace("Just Completed:", "just completed:"),
|
||||
UPDATE.replace("\n", "\r\n"),
|
||||
UPDATE.replace("T165", "task-165"),
|
||||
UPDATE.replace("\u2014 nothing from you", "waiting"),
|
||||
UPDATE.slice(0, -1),
|
||||
tooMany,
|
||||
]) {
|
||||
assert.equal(parseExecutiveUpdate(invalid).ok, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("C8: zero, multiple, unavailable, stale, malformed, and unproven completions reject", async () => {
|
||||
for (const outcome of ["zero", "multiple", "unavailable", "stale"] as const) {
|
||||
const result = await validateAttestedGoalReport(UPDATE, publication(async () => Object.freeze({ outcome })), "in_progress");
|
||||
assert.equal(result.ok, false, outcome);
|
||||
}
|
||||
assert.equal((await validateAttestedGoalReport(UPDATE, publication(async () => resolved({ frozen: false }) as never), "in_progress")).ok, false);
|
||||
assert.equal((await validateAttestedGoalReport(UPDATE, publication(async () => resolved({ evidence: null })), "in_progress")).ok, false);
|
||||
assert.equal((await validateAttestedGoalReport(UPDATE, publication(async () => resolved({ changed: false })), "in_progress")).ok, false);
|
||||
assert.equal((await validateAttestedGoalReport(UPDATE, publication(async () => resolved()), "in_progress")).ok, true);
|
||||
});
|
||||
|
||||
test("Q118/F5: contract-authorized no-change form is status-aware and requires unchanged tracked state", async () => {
|
||||
const parsed = parseExecutiveUpdate(NO_CHANGE);
|
||||
assert.equal(parsed.ok, true);
|
||||
if (parsed.ok) {
|
||||
assert.equal(parsed.kind, "no-change");
|
||||
assert.deepEqual(parsed.items.map((item) => [item.section, item.token]), [["Next Step", "T165"], ["Next Step", "T165"]]);
|
||||
}
|
||||
|
||||
let resolverCalls = 0;
|
||||
const unchanged = publication(async () => {
|
||||
resolverCalls += 1;
|
||||
return resolved({ changed: false, evidence: null });
|
||||
});
|
||||
assert.equal((await validateAttestedGoalReport(NO_CHANGE, unchanged, "in_progress")).ok, true);
|
||||
assert.equal(resolverCalls, 2, "both no-change references resolve through the attested consumer");
|
||||
|
||||
resolverCalls = 0;
|
||||
const terminal = await validateAttestedGoalReport(NO_CHANGE, unchanged, "satisfied");
|
||||
assert.equal(terminal.ok, false, "no-change is never a terminal report");
|
||||
assert.equal(resolverCalls, 0, "wrong status rejects before resolver or Goal state mutation");
|
||||
|
||||
assert.equal((await validateAttestedGoalReport(NO_CHANGE, publication(async () => resolved({ changed: true })), "in_progress")).ok, false);
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
// Hermetic NG-7 tests: goal-state incarnation fencing (Mercer F1 class).
|
||||
// Run: node --test test/fencing.test.ts
|
||||
//
|
||||
// SAFETY (NG7-SAFETY-V1W2): every arm uses an ISOLATED temp directory —
|
||||
// no live seat state, no Mercer/Marcie/marcie-2 files, no running sessions.
|
||||
// The extension source edit is confined to store resolution (store.ts) and
|
||||
// the one-line index.ts wiring.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, readdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
resolveStatePath,
|
||||
fencedStateFilePath,
|
||||
loadState,
|
||||
saveState,
|
||||
stateFilePath,
|
||||
} from "../lib/store.ts";
|
||||
import { setGoal, pauseGoal, initialState } from "../lib/state.ts";
|
||||
|
||||
function fixtureDir(): string {
|
||||
return mkdtempSync(join(tmpdir(), "goal-fence-"));
|
||||
}
|
||||
|
||||
function writeLegacy(dir: string, text: string): void {
|
||||
writeFileSync(stateFilePath(dir), JSON.stringify(setGoal(initialState(), text), null, 2) + "\n");
|
||||
}
|
||||
|
||||
// ---- (a) hostile two-incarnation collision: the measured class ---------------
|
||||
|
||||
test("(a) two incarnations of one seat never share or clobber state", () => {
|
||||
const dir = fixtureDir();
|
||||
try {
|
||||
const a = resolveStatePath(dir, { incarnationId: "inc-marcie" });
|
||||
const b = resolveStatePath(dir, { incarnationId: "inc-marcie-2" });
|
||||
assert.notEqual(a, b, "distinct incarnations must resolve distinct files");
|
||||
|
||||
// incarnation A sets an active goal
|
||||
const stateA = setGoal(initialState(), "ship the canary");
|
||||
saveState(stateA, a);
|
||||
// incarnation B (a second process of the SAME seat) loads ITS state
|
||||
const stateB = loadState(b);
|
||||
assert.equal(stateB.status, "none", "B must not inherit A's active focus");
|
||||
assert.equal(stateB.text, "", "B sees no goal text of A's");
|
||||
// B sets its own goal; A's file is untouched
|
||||
saveState(setGoal(initialState(), "B's own goal"), b);
|
||||
assert.equal(loadState(a).text, "ship the canary", "A's focus survives B's activity");
|
||||
assert.equal(loadState(b).text, "B's own goal");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("(a) B cannot pause or settle A's focus through any store operation", () => {
|
||||
const dir = fixtureDir();
|
||||
try {
|
||||
const a = resolveStatePath(dir, { incarnationId: "inc-a" });
|
||||
const b = resolveStatePath(dir, { incarnationId: "inc-b" });
|
||||
saveState(setGoal(initialState(), "A active"), a);
|
||||
|
||||
// Every mutation B performs addresses b's path only; A's bytes cannot
|
||||
// change through B's store API — proven by writing from B and re-reading A.
|
||||
saveState(pauseGoal(loadState(b), "blocked:sibling", "cap"), b);
|
||||
const aNow = loadState(a);
|
||||
assert.equal(aNow.status, "active", "A's focus is still active after B's pause");
|
||||
assert.equal(aNow.pausedReason, undefined);
|
||||
|
||||
// And the directory contains exactly the two fenced files.
|
||||
const names = readdirSync(dir).filter((n) => n.startsWith("goal-state"));
|
||||
assert.equal(names.length, 2);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("(a) session_start-style reload stays incarnation-scoped", () => {
|
||||
const dir = fixtureDir();
|
||||
try {
|
||||
const a = resolveStatePath(dir, { incarnationId: "inc-a" });
|
||||
saveState(setGoal(initialState(), "A goal v1"), a);
|
||||
// simulate: A's session reloads (new/resume/reload all re-load from ITS path)
|
||||
const reloaded = loadState(resolveStatePath(dir, { incarnationId: "inc-a" }));
|
||||
assert.equal(reloaded.text, "A goal v1");
|
||||
// a sibling's state never leaks into A's reload
|
||||
saveState(setGoal(initialState(), "sibling"), resolveStatePath(dir, { incarnationId: "inc-sib" }));
|
||||
assert.equal(loadState(resolveStatePath(dir, { incarnationId: "inc-a" })).text, "A goal v1");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ---- (b) legacy migration fails safe -------------------------------------------
|
||||
|
||||
test("(b) legacy unfenced state is QUARANTINED, never claimed (NG-7 F4)", () => {
|
||||
const dir = fixtureDir();
|
||||
try {
|
||||
writeLegacy(dir, "legacy active goal");
|
||||
const a = resolveStatePath(dir, { incarnationId: "inc-first" });
|
||||
// FAIL SAFE: the first incarnation must NOT inherit the unknown owner's
|
||||
// ACTIVE focus — it starts fresh.
|
||||
const state = loadState(a);
|
||||
assert.equal(state.status, "none", "no active focus inherited from legacy");
|
||||
assert.equal(state.text, "");
|
||||
// EVIDENCE PRESERVED: the legacy bytes live on in the quarantine file.
|
||||
const quarantined = JSON.parse(readFileSync(join(dir, "goal-state.legacy.json"), "utf8")) as { text: string };
|
||||
assert.equal(quarantined.text, "legacy active goal");
|
||||
const names = readdirSync(dir);
|
||||
assert.equal(names.includes("goal-state.json"), false, "legacy name vacated");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("(b/F4) ACTIVE legacy + fresh incarnation: no active focus inherited, evidence preserved", () => {
|
||||
const dir = fixtureDir();
|
||||
try {
|
||||
// the live canary shape: legacy state carrying an ACTIVE focus of a
|
||||
// previous (unknown-owner) incarnation
|
||||
writeLegacy(dir, "mercer's previous focus");
|
||||
for (const inc of ["inc-canary-1", "inc-canary-2"]) {
|
||||
const p = resolveStatePath(dir, { incarnationId: inc });
|
||||
assert.equal(loadState(p).status, "none", `${inc} inherits no active focus`);
|
||||
}
|
||||
// second quarantine slot when another legacy reappears (counter path)
|
||||
writeLegacy(dir, "second legacy");
|
||||
resolveStatePath(dir, { incarnationId: "inc-third" });
|
||||
assert.equal(
|
||||
readFileSync(join(dir, "goal-state.legacy.1.json"), "utf8").includes("second legacy"),
|
||||
true,
|
||||
"counter slot preserves the second legacy",
|
||||
);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("(b/F3) migration race: rename attempted with legacy present, failure -> fresh", () => {
|
||||
const dir = fixtureDir();
|
||||
try {
|
||||
writeLegacy(dir, "legacy goal");
|
||||
let renameCalls = 0;
|
||||
const path = resolveStatePath(dir, {
|
||||
incarnationId: "inc-raced",
|
||||
io: {
|
||||
// sequenced: fenced absent (first probe), legacy present (second)
|
||||
existsSync: (p: string) => p === stateFilePath(dir),
|
||||
renameSync: () => {
|
||||
renameCalls += 1;
|
||||
throw new Error("EEXIST: sibling quarantined it first");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(path, fencedStateFilePath(dir, "inc-raced"));
|
||||
assert.equal(renameCalls, 1, "the rename branch EXECUTED (not the fenced early-return)");
|
||||
assert.equal(loadState(path).status, "none", "race loses to fresh");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("(b/F2) traversal-shaped launcher claim is rejected at the identity source", async () => {
|
||||
const { incarnationIdentity, resetIncarnationGlobal } = await import("../../mosaic-core/lib/incarnation.ts");
|
||||
const prev = process.env.MOSAIC_LAUNCH_INCARNATION;
|
||||
resetIncarnationGlobal();
|
||||
try {
|
||||
for (const hostile of ["../../etc/pwn", "..", "/abs", "a/b", ".hidden", "x".repeat(200)]) {
|
||||
process.env.MOSAIC_LAUNCH_INCARNATION = hostile;
|
||||
const id = incarnationIdentity(() => `minted-for-${hostile.slice(0, 3)}`);
|
||||
assert.notEqual(id, hostile, `claim ${JSON.stringify(hostile)} must not become the identity`);
|
||||
assert.equal(id.startsWith("minted-for-"), true, `falls through to mint for ${JSON.stringify(hostile)}`);
|
||||
resetIncarnationGlobal();
|
||||
}
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.MOSAIC_LAUNCH_INCARNATION;
|
||||
else process.env.MOSAIC_LAUNCH_INCARNATION = prev;
|
||||
resetIncarnationGlobal();
|
||||
}
|
||||
});
|
||||
|
||||
test("(b/F2) hostile claim keeps BOTH consumer paths inside the state root", async () => {
|
||||
const { createJournal } = await import("../../mosaic-core/lib/journal.ts");
|
||||
const { incarnationIdentity, resetIncarnationGlobal } = await import("../../mosaic-core/lib/incarnation.ts");
|
||||
const prev = process.env.MOSAIC_LAUNCH_INCARNATION;
|
||||
resetIncarnationGlobal();
|
||||
try {
|
||||
process.env.MOSAIC_LAUNCH_INCARNATION = "../../outside";
|
||||
const id = incarnationIdentity(); // no-arg production form (F1)
|
||||
assert.notEqual(id, "../../outside");
|
||||
const j = createJournal({ incarnationId: id, stateHome: "/xdg/state", io: { mkdirSync() {}, appendFileSync() {} } });
|
||||
assert.equal(j.path.startsWith("/xdg/state/mosaic-core/"), true, "journal contained");
|
||||
assert.equal(j.path.includes(".."), false);
|
||||
const dir = fixtureDir();
|
||||
const gp = resolveStatePath(dir, { incarnationId: id });
|
||||
assert.equal(gp.startsWith(dir), true, "goal path contained");
|
||||
assert.equal(gp.includes(".."), false);
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.MOSAIC_LAUNCH_INCARNATION;
|
||||
else process.env.MOSAIC_LAUNCH_INCARNATION = prev;
|
||||
resetIncarnationGlobal();
|
||||
}
|
||||
});
|
||||
|
||||
test("(b) corrupt legacy never blocks fencing", () => {
|
||||
const dir = fixtureDir();
|
||||
try {
|
||||
writeFileSync(stateFilePath(dir), "{corrupt");
|
||||
const a = resolveStatePath(dir, { incarnationId: "inc-x" });
|
||||
assert.equal(loadState(a).status, "none", "corrupt legacy -> fresh initial state");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ---- (c) PI_SESSION_ID is never the incarnation --------------------------------
|
||||
|
||||
test("(c) fenced paths key on the process-launch identity, not PI_SESSION_ID", async () => {
|
||||
const dir = fixtureDir();
|
||||
try {
|
||||
const prev = process.env.PI_SESSION_ID;
|
||||
process.env.PI_SESSION_ID = "session-should-not-appear";
|
||||
const { incarnationIdentity } = await import("../../mosaic-core/lib/incarnation.ts");
|
||||
const id = incarnationIdentity(() => "minted-check");
|
||||
process.env.PI_SESSION_ID = prev === undefined ? "" : prev;
|
||||
if (prev === undefined) delete process.env.PI_SESSION_ID;
|
||||
assert.notEqual(id, "session-should-not-appear");
|
||||
const p = resolveStatePath(dir, { incarnationId: id });
|
||||
assert.doesNotMatch(p, /session-should-not-appear/);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ---- compatibility ---------------------------------------------------------------
|
||||
|
||||
test("goal_report compatibility: the state machine surface is unchanged", async () => {
|
||||
// The pure state machine (setGoal/pauseGoal/resume/recordReport/validateReport)
|
||||
// is untouched by fencing; the existing goal suite covers it. Here we pin
|
||||
// the compatibility contract fencing must not break: a state saved and
|
||||
// loaded through the fenced path round-trips exactly as the legacy path did.
|
||||
const dir = fixtureDir();
|
||||
try {
|
||||
const p = resolveStatePath(dir, { incarnationId: "inc-compat" });
|
||||
const s = setGoal(initialState(), "round trip");
|
||||
saveState(s, p);
|
||||
assert.deepEqual(loadState(p), s);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
skills-local/**/SKILL.md whitespace=-trailing-space
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: ms-executive-update
|
||||
description: "Use for every user-bound status update, progress report, and at the end of a user-originated operation."
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# Executive Update
|
||||
|
||||
The operator reads status to decide what to do next, not to relive the work. An update is
|
||||
three short lists derived from the tracking files, not from memory. If an item is not in a
|
||||
tracking file, it is not done — write the file first, then report.
|
||||
|
||||
Applies to every agent reporting upward: a seat to its orchestrator, an orchestrator to the
|
||||
operator, a `/goal` loop reporting through `goal_report`, a sub-agent returning to its parent.
|
||||
|
||||
## When to use
|
||||
|
||||
- Any time you would otherwise narrate progress: after a task lands, after a fleet event
|
||||
(monitor tick, agent message, review verdict, CI result) changes tracked state, at the end
|
||||
of a turn with work in flight, or when asked "status", "update", "where are we".
|
||||
- Every `goal_report` call: the `evidence` (satisfied) or `reason` (blocked / in_progress)
|
||||
payload is an executive update in this format, so the operator sees the same shape from a
|
||||
goal loop as from a conversation.
|
||||
- Do **not** emit a full update when nothing changed. One line with
|
||||
`No change since <anchor>; still waiting on <item>.` is the whole reply.
|
||||
|
||||
## Communication closeout before reporting
|
||||
|
||||
An executive update does not replace direct communication. Before writing the update:
|
||||
|
||||
1. Reply to every inbound actionable message with the action taken, routed owner, or
|
||||
precise blocker.
|
||||
2. Send every new tasking, handoff, review request, blocker, result, correction, and
|
||||
decision request directly to the responsible seat or operator through the required
|
||||
path in `docs/guides/FLEET-COMMS.md`.
|
||||
3. Commit the durable artifact before sending its pointer when the communication needs to
|
||||
survive a pane or session.
|
||||
4. Record the destination, request or task id, exact delivery receipt, required return
|
||||
event, and escalation owner in the tracking file.
|
||||
|
||||
A no-change line does not satisfy an unsent acknowledgement, handoff, review request,
|
||||
blocker, or result. Neither does a `goal_report`, lane edit, board row, or operator-facing
|
||||
reply. Finish communication closeout first, then report the resulting state.
|
||||
|
||||
## Format: exact, no preamble, no epilogue
|
||||
|
||||
``` markdown
|
||||
Just Completed:
|
||||
* <item>: short description
|
||||
|
||||
Next Step:
|
||||
* <item>: short description
|
||||
|
||||
Blocked:
|
||||
* <item>: short description
|
||||
```
|
||||
|
||||
- `<item>` is the tracked identifier: task id (`T158`), ledger row (`E6`), PR (`#1491`),
|
||||
review marker, file, or commit. Never a vague noun ("the fix").
|
||||
- One line per bullet. ≤5 bullets per section; if you have more, the lower ones belong in
|
||||
the ledger, not the update. Keep all three headings; write `* none` when a section is empty.
|
||||
- Paths as clickable links; commits and SHAs short (8 chars).
|
||||
- Only items whose state **changed since your previous update** go under Just Completed.
|
||||
Next Step lists what happens next and who does it. Blocked names the blocker, who
|
||||
unblocks it, and what (if anything) is needed from the reader.
|
||||
|
||||
## Source of truth — mandatory before writing
|
||||
|
||||
Read, in this order, whatever applies to the work in flight; the update is a projection of
|
||||
these files (paths relative to the brain, `$MOSAIC_BRAIN_HOME`, per `docs/STRUCTURE-CANON.md`):
|
||||
|
||||
1. Lane ledger: `fleet/lanes/<lane>/TASKS.md` (open rows, dated entries) and
|
||||
`TASKS-DONE.md` (what moved to done).
|
||||
2. Seat files for every agent involved: `fleet/agents/<seat>/TASKS.md`, `STATE.md`,
|
||||
`work/` artifacts (review verdicts, plans, evidence records).
|
||||
3. Board: `fleet/board/MASTER-TASKS.md` and `fleet/board/taskings/<date>_T<n>_*.md` for
|
||||
task ids, owners, and reassignments.
|
||||
4. Project-level `docs/TASKS.md` when the orchestrator protocol makes it the control plane.
|
||||
5. Live evidence for anything claimed done: `git log` on the target branch/trunk, the PR,
|
||||
CI state, the seat's pane (per `docs/guides/FLEET-COMMS.md`), delivery receipts.
|
||||
|
||||
Rules:
|
||||
|
||||
- **Record, then report.** A state change learned from a pane, a monitor event, or a
|
||||
message is written into the owning tracking file (dated, with ids and SHAs) *before* it
|
||||
appears in the update. An item in the update with no tracking-file row is a defect.
|
||||
- **Evidence for "completed".** A Just Completed bullet cites its evidence: commit, merge
|
||||
SHA, verdict artifact path, receipt. "Delivered" is not "done"; "dispatched" is not
|
||||
"done"; a message sent is Next Step for the recipient, not a completion.
|
||||
- **Events are not the operator.** Never phrase a monitor event, agent message, or tool
|
||||
result as approval, confirmation, or a decision. Decisions come only from the operator's
|
||||
own messages.
|
||||
- **Blocked is precise.** `* T158: plan seat queued behind prior review — nothing from you`
|
||||
or `* Q96 gate: Decision needed — (1) … (2) …; recommend 1`. If the reader must decide,
|
||||
say so with the options and your recommendation; if not, say `nothing from you`.
|
||||
- **Honest state.** Distinguish known (read from a file or evidence) from inferred (seen in
|
||||
a pane, not yet recorded); mark inferred items `(unverified)`. Report failures with the
|
||||
output, skipped steps as skipped. Apply `ms-honesty`.
|
||||
- **Staleness anchor.** Include the anchor the update is current to when it matters:
|
||||
target head SHA, pipeline number, board commit, or timestamp.
|
||||
- **Delegation stays visible.** Work handed to a seat is reported with the seat and task
|
||||
id. The tracking row also carries the delivery receipt, required return event, and
|
||||
escalation owner. You own the follow-up until the seat reports back and the ledger
|
||||
reflects it.
|
||||
|
||||
## Machine contract (for `goal_report` payloads and any parser)
|
||||
|
||||
When an update is consumed by tooling (a `goal_report` payload, a broker, a coordinator),
|
||||
the loose human rules above tighten to this closed grammar. Human-facing replies may add
|
||||
Markdown links; payloads may not.
|
||||
|
||||
```
|
||||
update := section("Just Completed") NL section("Next Step") NL section("Blocked")
|
||||
section := HEADING ":" NL NL ( bullet+ | "* none" NL )
|
||||
HEADING := exact text, case-sensitive, in this order, each exactly once
|
||||
bullet := "* " item ": " text NL (1..5 per section; text is one line, no leading "-")
|
||||
item := taskid | pr | row | marker | sha | path
|
||||
taskid := "T" [0-9]+
|
||||
pr := "#" [0-9]+
|
||||
row := [A-Z]{1,4} "." ? [0-9]+ ("." [0-9]+)* e.g. E6, A7, GOV.5
|
||||
marker := [A-Z0-9]+ ("-" [A-Z0-9]+)* "-Q" [0-9]+ e.g. CPS-PRD-REV1-MERGE-GATE-Q96
|
||||
sha := [0-9a-f]{40} (payloads: full SHA; 8-char only in prose)
|
||||
path := "`" <repo-relative path, no spaces> "`"
|
||||
NL := "\n" (LF only; no trailing whitespace; no blank lines inside a section)
|
||||
```
|
||||
|
||||
- Exactly three sections; any other heading, a missing section, or a reordered section is a
|
||||
parse failure.
|
||||
- A bullet under Blocked ends with either `— nothing from you` or
|
||||
`— Decision needed: (1) … (2) …; recommend N`.
|
||||
- Identifier resolution is the consumer's job and is fail-closed: an item that resolves to
|
||||
zero or more than one tracked object (unknown task id, ambiguous short SHA, unknown marker)
|
||||
rejects the report before any state change; the producer gets the rejection and re-reports.
|
||||
- A one-line "no change" reply (`No change since <item>; still waiting on <item>.`) is a
|
||||
distinct message, not an update; a `goal_report` with status `in_progress` may carry it
|
||||
only when the tracked state is unchanged since the previous report.
|
||||
- This grammar is the format's single definition. Enforcement (who parses, what policy binds
|
||||
it to a role) is decided by the runtime's role manifests, not by this skill.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Narrative paragraphs, "I'll continue by…", recaps of what the reader already knows.
|
||||
- Reporting from conversation memory while the ledger still says the old state.
|
||||
- Listing the same item under two sections.
|
||||
- Padding Just Completed with process ("read the file", "checked the pane").
|
||||
- Hiding a needed decision inside Next Step.
|
||||
- Treating the executive update as delivery to a seat that was never contacted directly.
|
||||
- Reporting a handoff without its receipt, return event, and escalation owner.
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
Just Completed:
|
||||
|
||||
* #1491: round-2 fix pushed at 3ae1411d; independent exact-head APPROVE (brain 0199a866); CI 3058 green
|
||||
* E6 ledger: gate seat result recorded as BLOCKED (capability), per roles/gate.md
|
||||
|
||||
Next Step:
|
||||
|
||||
* T158: plan seat designs the runtime gate profile, then provisioning → gate re-run
|
||||
* #1491: operation seat runs pr-merge.sh after the gate seat issues PASS
|
||||
|
||||
Blocked:
|
||||
|
||||
* T158: queued behind the plan seat's prior review — nothing from you
|
||||
```
|
||||
@@ -0,0 +1,190 @@
|
||||
---
|
||||
name: ms-honesty
|
||||
description: "Load before answering, reporting, or ruling; must always apply."
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# Honesty
|
||||
|
||||
An agent that always has an answer is not a reliable agent. Training rewards a fluent,
|
||||
confident reply over a correct one, so the default reflex under pressure is to produce
|
||||
something that sounds like an answer even when nothing was measured. This skill replaces
|
||||
that reflex with a procedure. It applies to every reply, every report, every message to
|
||||
another seat, and every rule you put on a board.
|
||||
|
||||
The two operator rules this skill enforces:
|
||||
|
||||
- It is acceptable not to have an answer. "I do not know" followed by what would settle it
|
||||
is a complete, correct reply.
|
||||
- A statement carries its evidence with it. The reader must be able to tell, from the
|
||||
sentence alone, whether you measured it, read it, inferred it, or guessed.
|
||||
|
||||
## Where the pressure comes from
|
||||
|
||||
Recognize the moment. Each of these is a point where the reflex fires:
|
||||
|
||||
1. The reader asked a direct question and you have a plausible answer in memory.
|
||||
2. A number, an id, a path, or a name would make the sentence complete.
|
||||
3. A tool returned nothing, an error, or something you did not expect, and a reason
|
||||
suggests itself.
|
||||
4. A prior record (a document, a ledger row, your own earlier message) says the answer.
|
||||
5. The reply is almost done and one more fact would close it neatly.
|
||||
6. You are reporting upward and a clean "done" reads better than "partly done".
|
||||
|
||||
At each of these points, stop and classify the claim before writing it.
|
||||
|
||||
## The evidence ladder
|
||||
|
||||
Every factual claim sits on exactly one rung. Name the rung when it is not obvious from
|
||||
the sentence.
|
||||
|
||||
| Rung | Meaning | How to write it |
|
||||
|---|---|---|
|
||||
| Measured | You ran the command, read the file, called the API, in this session, and the output says this | State the fact and the source: "main tip is 0bbb997d (branches/main API, 13:05)" |
|
||||
| Read | A record says it (a ledger, a doc, a pane, a message from another seat); you did not confirm it live | "STATE.md says X"; "orch-01 reports X"; mark `(unverified)` in updates |
|
||||
| Inferred | Measured facts plus a reasoning step you can show | "X, so probably Y" with the step visible; never as a flat fact |
|
||||
| Recalled | It is in your context or training but not in this session's evidence | "I recall X; not measured" or leave it out |
|
||||
| Guessed | None of the above | Do not write it as a fact. Write "I do not know" and what would settle it |
|
||||
|
||||
Rules that follow:
|
||||
|
||||
- A number, id, SHA, path, version, or quote is Measured or it is not in the reply. A
|
||||
half-remembered value is worse than none because it looks the same as a real one.
|
||||
- A tool result you did not read is not Measured. Empty output, a 403 body, an exit code
|
||||
from the wrong process in a pipe, and a cached ref are the four ways a measurement lies;
|
||||
show the control (the way the check could have come out differently) before trusting a
|
||||
zero, a green, or an empty result.
|
||||
- A prior record, including one you wrote, is Read, not Measured, until you re-measure it.
|
||||
- Another agent's claim is Read. Repeating it does not promote it.
|
||||
- Inference is allowed and useful. It is dishonest only when the reasoning step is hidden
|
||||
and the conclusion is written as if measured.
|
||||
|
||||
## "I do not know" is a full answer
|
||||
|
||||
When the honest rung is Guessed, reply with these three parts and nothing else:
|
||||
|
||||
1. What you do not know, in one sentence.
|
||||
2. What would settle it: the command, the file, the person, or the access needed.
|
||||
3. Whether you can run that now. If yes, do it instead of writing the reply. If no, say
|
||||
what blocks it.
|
||||
|
||||
Do not pad it with a guess "in case it helps". A guess next to an honest "I do not know"
|
||||
gets read as the answer.
|
||||
|
||||
## Widening and narrowing
|
||||
|
||||
- Report the case you measured, not the class it belongs to. One host checked is one host.
|
||||
One pipeline read is one pipeline. Say "on 5788" not "on main".
|
||||
- Do not round up. "Mostly green", "should be fine", "looks like it worked" are not states.
|
||||
The states are: measured green, measured red, not measured.
|
||||
- Do not round down either. If something is done with evidence, say done. Hedging a real
|
||||
result is as misleading as inflating a weak one.
|
||||
- Delivered is not done. Dispatched is not done. Sent is not received. Merged is not
|
||||
deployed. Use the word for the state you have evidence of.
|
||||
|
||||
## Communication evidence
|
||||
|
||||
Drafted is not sent. Sent is not delivered. Delivered is not acknowledged. Acknowledged is not completed.
|
||||
|
||||
Name the exact state and its source. A delivery receipt proves only the outcome the
|
||||
wrapper reports. It does not prove the recipient read, accepted, or completed the work.
|
||||
`queued-draft` and `unverifiable` are not permission to retry outside the wrapper contract.
|
||||
A later durable artifact or direct reply can prove acknowledgement or completion.
|
||||
|
||||
Honesty is not silence. If another authorized seat can measure or perform the missing
|
||||
step, send it a direct request before reporting. Record the destination, request or task
|
||||
id, exact delivery receipt, required return event, and escalation owner. Do not use an
|
||||
operator-facing update as a substitute for contacting the responsible seat.
|
||||
|
||||
A credential refusal is correct behavior, but it is not a terminal communication state.
|
||||
Route the request to the authorized owner. Call the goal blocked only when no authorized
|
||||
route or other meaningful work exists.
|
||||
|
||||
## Retraction
|
||||
|
||||
When you find that something you already said was wrong:
|
||||
|
||||
1. Say it in the next message, first line, labeled: "Retraction: <what I said> was wrong
|
||||
because <what I now measured>."
|
||||
2. Correct every place it was recorded (STATE, ledger, board, the other seat's inbox), with
|
||||
the date, leaving the superseded text legible rather than deleting it.
|
||||
3. Do not soften it, explain it away, or bury it under new results.
|
||||
|
||||
A retraction costs one message. An uncorrected error costs every decision built on it.
|
||||
|
||||
## How this binds the other skills
|
||||
|
||||
- `ms-executive-update`: each Just Completed bullet cites Measured evidence (SHA, verdict
|
||||
path, receipt). A bullet whose evidence is only Read carries `(unverified)`. Blocked
|
||||
names what is not known and who can know it. Another-seat and operator waits use the
|
||||
recorded delivery receipt, required return event, escalation owner, and existing wake
|
||||
path. They do not require polling or an `agent-watch`. External-condition waits require
|
||||
a fresh measurement or a permitted watch. A no-change line names the applicable record.
|
||||
- `ms-proactive-agent`: step 1 (record) writes the rung with the fact. A Guessed or
|
||||
Recalled item is never a task candidate; measuring it is the candidate. Two cycles that
|
||||
produce identical evidence are reported as no progress, not as progress.
|
||||
- `ms-watch`: a watch condition is a measurement. Arm it with a control run that shows it
|
||||
can return false, and record what it reads, not what you hope it reads.
|
||||
- Rulings and reviews: a ruling states the evidence rung it stands on. "Ruled on the render
|
||||
argument alone; protection settings unmeasured from this seat" is a valid ruling. A
|
||||
ruling that hides an unmeasured input is not.
|
||||
|
||||
## Self-check before sending
|
||||
|
||||
Read your draft once more and ask, per sentence with a fact in it:
|
||||
|
||||
- Which rung is this on, and would the reader guess the same rung from my wording?
|
||||
- If this turns out false, can I point to the exact tool output that misled me? If there
|
||||
is no such output, the sentence is Recalled or Guessed and must be rewritten or cut.
|
||||
- Did I write "done", "verified", "confirmed", "green", or "fixed" anywhere without a
|
||||
source next to it?
|
||||
- Is there a number or id I did not read from a tool this session?
|
||||
- Did I answer the question that was asked, or a nearby question I could answer better?
|
||||
- Did this interaction create an acknowledgement, tasking, handoff, review request,
|
||||
blocker, result, correction, or decision request that I have not sent directly?
|
||||
- Did I name a communication state more strongly than its receipt proves?
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Filling a gap with the most likely value.
|
||||
- Reading a document (including your own STATE file) and reporting it as a measurement.
|
||||
- Treating a tool's silence, a 403 body, or an empty grep as a finding.
|
||||
- "Should be", "presumably", "I believe" used as a fact with a softener, rather than as a
|
||||
flag that leads to a measurement.
|
||||
- Answering "is it fixed?" with "a newer version is deployed".
|
||||
- Quietly correcting an earlier claim in a later message without labeling it a retraction.
|
||||
- Promoting another seat's report to fact by repeating it in your own voice.
|
||||
- Describing what was checked in general terms ("ran the checks") so a partial check reads
|
||||
as a full one.
|
||||
- Calling a task blocked after an access refusal without routing it to an authorized owner.
|
||||
- Reporting a handoff upward while never sending it to the responsible seat.
|
||||
|
||||
## Examples
|
||||
|
||||
Asked whether branch protection requires status checks, with only a 403 in hand:
|
||||
|
||||
```
|
||||
Not measured. This seat's token gets 403 on branch_protections, so the settings are
|
||||
unknown from here. An admin token, or Jason reading the repo settings page, would settle
|
||||
it. I can ask orch-01 to read it with the admin identity.
|
||||
```
|
||||
|
||||
Reporting a merge learned from another seat's pane:
|
||||
|
||||
```
|
||||
* #3171: merged per orch-01's pane (unverified); verifying via the PR API now
|
||||
```
|
||||
|
||||
then, after the API read:
|
||||
|
||||
```
|
||||
* #3171: merged at 0bbb997d, merged_by orch-01, reviews 381+382 at head 4ce167c3 (PR API, 13:05)
|
||||
```
|
||||
|
||||
Retracting:
|
||||
|
||||
```
|
||||
Retraction: "main has no required status contexts" was read from a 403 body, not from the
|
||||
protection settings. Protection is unmeasured from this seat. Corrected in STATE.md (12:10)
|
||||
and sent to orch-01; issue #3170 body corrected by orch-01.
|
||||
```
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
name: ms-proactive-agent
|
||||
description: Continue an authorized Mosaic assignment through planning, execution, verification, and recovery while useful work remains. Use for ongoing work and continuation requests, not to turn a question or review into an open-ended mission.
|
||||
---
|
||||
|
||||
# Proactive Agent
|
||||
|
||||
Complete the authorized outcome, not just the first task. At each meaningful checkpoint,
|
||||
select and execute the next necessary action in the same turn while scope, authority,
|
||||
dependencies, and budget permit. A plan, task dispatch, or status report is not completion.
|
||||
|
||||
This skill governs work selection. [ms-goal](../ms-goal/SKILL.md) governs durable goal
|
||||
state, pause/resume, and completion; load it when establishing or recovering a continuing
|
||||
assignment. The dev Stack does not yet integrate the `/goal` extension. Neither skill
|
||||
creates a scheduler, background process, tool, or automatic follow-up turn.
|
||||
|
||||
## Establish execution context
|
||||
|
||||
Before writing state or choosing work:
|
||||
|
||||
1. Resolve the assignment from the user request, launch context, and applicable
|
||||
repository/mission instructions. Record agent identity, workspace root, project if
|
||||
declared, and execution/session identifier if supplied. Mark unavailable identifiers
|
||||
as unavailable; do not infer a workspace from the agent's identity.
|
||||
2. Locate the authoritative goal, tasks, approvals, and recovery checkpoint. Use existing
|
||||
declared paths. For example, this skill's source repository uses `docs/plans/CURRENT.md`
|
||||
for queued project work; an explicit user request can authorize a separate bounded task.
|
||||
Do not advance unrelated
|
||||
queued phases or overwrite their owner-controlled records. Fleet paths and a seat's
|
||||
general purpose are not default assignments.
|
||||
3. Check writable state locations and required tools. Discover communication, claim, and
|
||||
wake mechanisms when needed. Optional reporting or watch skills must not block ordinary
|
||||
local work. If a required capability is missing, record the precise limitation and
|
||||
continue independent authorized work where possible. If ms-goal is unavailable, use an
|
||||
existing authoritative goal protocol; do not fabricate its tools or claim durable
|
||||
continuation without accessible records.
|
||||
4. Identify ownership. Use the coordinator's claim operation when provided. A Markdown
|
||||
owner field is not a lock. Without enforced claims, work only under an established
|
||||
single-writer assignment; conflicting ownership stops the affected task.
|
||||
|
||||
Explicit scope and authorization persist. Do not ask again for routine actions already
|
||||
covered by them. Role capabilities constrain execution; they do not authorize a new
|
||||
mission, another owner's work, or new external communications. Preserve phase approvals,
|
||||
acceptance ownership, and budget limits. Silence, task creation, and elapsed time grant
|
||||
no additional authority.
|
||||
|
||||
## Run the loop
|
||||
|
||||
Run on assignment start/recovery, a substantive checkpoint, an actionable message, or a
|
||||
matching wake event. Bookkeeping tool results and reports do not recursively trigger it.
|
||||
|
||||
1. **Reconcile and record.** Read relevant current records and verify the live fact needed
|
||||
for the next action. Preserve unrelated changes. Resolve actions with unknown outcomes
|
||||
before retrying them. Persist changed task status, evidence, and obligations before
|
||||
reporting; do not rewrite unchanged state merely to manufacture activity.
|
||||
2. **Check control state.** Follow ms-goal. Paused or canceled work does not resume on an
|
||||
ordinary tool result, compaction, or unrelated message. Apply user steering promptly;
|
||||
a status question does not cancel work. Reconcile scope changes with remaining tasks
|
||||
before executing them.
|
||||
3. **Select useful work.** Finish an owned in-flight task that can proceed, then an owned
|
||||
blocker you can resolve, then ready assigned work. Claim other necessary work only
|
||||
within delegated assignment authority. Respect dependencies and explicit priority;
|
||||
prefer smaller reversible steps when candidates otherwise have equal priority.
|
||||
4. **Fill a planning gap.** If an acceptance criterion is unmet and no task covers it,
|
||||
decompose the authorized outcome into bounded tasks with dependencies and evidence
|
||||
requirements, record them, and take the first ready task. A new goal can start with an
|
||||
empty queue. Necessary fixes and verification belong in scope; unrelated improvements
|
||||
remain proposals. If decomposition exposes a new scope decision, route it and continue
|
||||
any independent work already authorized.
|
||||
5. **Execute and verify.** Take the task to a meaningful checkpoint. Run checks appropriate
|
||||
to its acceptance criteria, inspect the results, and correct in-scope failures. Persist
|
||||
a resumable checkpoint before a risky action, context limit, or handoff. Do not mark a
|
||||
task done because it was started, delegated, or merely produced an artifact.
|
||||
6. **Close communication obligations.** Use the procedure below. Update the goal record
|
||||
with the next action and actual evidence. If ready work remains, return to step 1 in
|
||||
this turn. Report significant results without making the report an artificial stop.
|
||||
7. **When nothing can run, classify honestly.** Verify satisfaction against acceptance
|
||||
criteria, not queue emptiness. Otherwise record waiting, paused, or blocked with the
|
||||
unresolved gate and exact resumption condition, using ms-goal.
|
||||
|
||||
## Failure and budget handling
|
||||
|
||||
- Diagnose failures before retrying. Retry transient failures only under the tool's
|
||||
documented bounded retry contract or with a concrete changed input. For an uncertain
|
||||
external side effect, reconcile its request/action identifier first.
|
||||
- Repeated actions producing no new task evidence require a different justified approach
|
||||
or a recorded blocker. An unchanged registered wait is not a failed attempt; it should
|
||||
yield instead of generating more reports.
|
||||
- Track actual usage when available; label estimates and unavailable measurements. Stay
|
||||
within explicit limits and reserve capacity for verification and a checkpoint. Budget
|
||||
exhaustion pauses work; it does not establish completion. Never reset usage by resuming
|
||||
or splitting a task. Ask for an extension only when one is actually needed.
|
||||
- If recording fails, preserve recoverable evidence and report the failure. Do not start
|
||||
further effects that require a durable record or claim they were checkpointed.
|
||||
|
||||
## Communication and waits
|
||||
|
||||
For authorized inbound work, record the change and acknowledge the sender through the
|
||||
available approved transport. A reaction or receipt alone does not require another reply.
|
||||
For authorized tasking, handoffs, review requests, or decisions, persist any required
|
||||
artifact before sending its pointer. Durable storage does not necessarily mean a Git
|
||||
commit; follow the repository's commit authority and workflow.
|
||||
|
||||
Record request/task id, destination and workspace, exact receipt, expected return event,
|
||||
and follow-up owner. Distinguish drafted, queued, delivered, acknowledged, and completed.
|
||||
Unknown delivery is not permission to resend; reconcile or follow the transport's retry
|
||||
contract. A report to the user is not delivery to another agent. If no transport is
|
||||
available, record the unsent request and route the missing capability to the user.
|
||||
|
||||
For a wait, record the last observation, condition that ends it, wake mechanism, and any
|
||||
deadline plus its escalation owner/action. Verify the mechanism exists. An owner name,
|
||||
file entry, or queued draft does not prove that another turn will occur.
|
||||
|
||||
- Use existing message delivery for another agent's response. Do not poll its private
|
||||
files or pane. Use an available coordinator's timeout mechanism for overdue requests.
|
||||
- Use ms-agent-watch only when installed, supported, and authorized, for external
|
||||
conditions with no existing wake owner. Verify a known-false control and the actual
|
||||
condition, distinguishing pending from errors; handle an already-met condition now.
|
||||
Verify registration, bind to the exact resource/version and destination, and record
|
||||
timeout/delivery behavior. Reuse valid existing watches and retire obsolete ones.
|
||||
- Without automatic wake support, a wait is explicitly **manual**: state who must send
|
||||
what reply or resume instruction. A deadline is checked on the next invocation unless
|
||||
an actual timer/coordinator owns it. Do not promise unattended escalation or resumption.
|
||||
|
||||
## Checkpoints and reporting
|
||||
|
||||
Persist current task, completed evidence, remaining acceptance gates, next concrete action,
|
||||
outstanding communication/wait records, ownership, budget, and uncertain outcomes in their
|
||||
authoritative records. Keep a compact checkpoint pointing to them; retain unresolved
|
||||
obligations and durable history regardless of checkpoint trimming.
|
||||
|
||||
Report what changed, its evidence, what happens next, and any decision needed. Use an
|
||||
installed reporting format only when the applicable environment requires it. No special
|
||||
reporting skill or goal_report tool is required in dev mode. Do not repeat unchanged
|
||||
reports unless asked or a scheduled check produces information that matters.
|
||||
|
||||
End normally only at verified satisfaction, explicit pause/cancellation, a genuine
|
||||
blocker, or a registered automatic/manual wait with no ready work. If execution is forced
|
||||
to end while work remains, checkpoint it as incomplete and name the actual resume path;
|
||||
never imply that writing `next` scheduled that action.
|
||||
|
||||
For bounded dev testing, use
|
||||
[ms-goal's execution checks](../ms-goal/references/execution-checks.md).
|
||||
@@ -0,0 +1,320 @@
|
||||
// Hermetic tests for the pi /goal extension pure logic (state, parse, settle, store).
|
||||
// Run: node --test test/goal.test.ts
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir, homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
checkLimitReached,
|
||||
clearGoal,
|
||||
DEFAULT_MAX_CHECKS,
|
||||
DEFAULT_MAX_NO_PROGRESS_REPORTS,
|
||||
envDefaultMaxChecks,
|
||||
envDefaultMaxNoProgressReports,
|
||||
initialState,
|
||||
pauseGoal,
|
||||
recordCheckInjected,
|
||||
recordReport,
|
||||
resumeGoal,
|
||||
setGoal,
|
||||
validateReport,
|
||||
} from "../lib/state.ts";
|
||||
import { parseGoalCommand } from "../lib/parse.ts";
|
||||
import { decideSettle } from "../lib/settle.ts";
|
||||
import { agentStateDir, loadState, saveState, stateFilePath } from "../lib/store.ts";
|
||||
|
||||
// ---------- state machine ----------
|
||||
|
||||
test("initialState is status none with default cap", () => {
|
||||
const s = initialState(25);
|
||||
assert.equal(s.status, "none");
|
||||
assert.equal(s.text, "");
|
||||
assert.equal(s.checks, 0);
|
||||
assert.equal(s.maxChecks, 25);
|
||||
assert.equal(s.noProgressReports, 0);
|
||||
assert.equal(s.maxNoProgressReports, 3);
|
||||
assert.equal(s.workEventSinceReport, false);
|
||||
assert.equal(DEFAULT_MAX_CHECKS, 25);
|
||||
assert.equal(DEFAULT_MAX_NO_PROGRESS_REPORTS, 3);
|
||||
});
|
||||
|
||||
test("setGoal activates with zeroed checks and optional cap", () => {
|
||||
const s = setGoal(initialState(25), "ship the thing");
|
||||
assert.equal(s.status, "active");
|
||||
assert.equal(s.text, "ship the thing");
|
||||
assert.equal(s.checks, 0);
|
||||
assert.equal(s.maxChecks, 25);
|
||||
|
||||
const capped = setGoal(initialState(25), "another", 7);
|
||||
assert.equal(capped.maxChecks, 7);
|
||||
});
|
||||
|
||||
test("pause only from active; keeps goal and reason", () => {
|
||||
const active = setGoal(initialState(25), "goal");
|
||||
const paused = pauseGoal(active, "stopped by operator");
|
||||
assert.equal(paused.status, "paused");
|
||||
assert.equal(paused.pausedReason, "stopped by operator");
|
||||
assert.equal(paused.text, "goal");
|
||||
assert.equal(pauseGoal(paused, "again"), paused); // idempotent when not active
|
||||
assert.equal(pauseGoal(initialState(25), "x").status, "none");
|
||||
});
|
||||
|
||||
test("resume only from paused; resets checks and clears reason", () => {
|
||||
const paused = pauseGoal(setGoal(initialState(25), "goal"), "cap");
|
||||
paused.checks = 9;
|
||||
paused.noProgressReports = 2;
|
||||
paused.workEventSinceReport = true;
|
||||
const resumed = resumeGoal(paused);
|
||||
assert.equal(resumed.status, "active");
|
||||
assert.equal(resumed.checks, 0);
|
||||
assert.equal(resumed.noProgressReports, 0);
|
||||
assert.equal(resumed.workEventSinceReport, false);
|
||||
assert.equal(resumed.pausedReason, undefined);
|
||||
assert.equal(resumeGoal(initialState(25)).status, "none");
|
||||
});
|
||||
|
||||
test("clearGoal returns to none", () => {
|
||||
const cleared = clearGoal(setGoal(initialState(25), "goal", 9));
|
||||
assert.equal(cleared.status, "none");
|
||||
assert.equal(cleared.text, "");
|
||||
assert.equal(cleared.maxChecks, 9);
|
||||
});
|
||||
|
||||
test("report resets consecutive checks; limit and injection counting interlock", () => {
|
||||
let s = setGoal(initialState(25), "goal", 3);
|
||||
assert.equal(checkLimitReached(s), false);
|
||||
s = recordCheckInjected(s);
|
||||
s = recordCheckInjected(s);
|
||||
assert.equal(checkLimitReached(s), false); // 2 of 3 injected, one slot left
|
||||
s = recordCheckInjected(s);
|
||||
assert.equal(checkLimitReached(s), true); // cap exhausted: auto-pause, no injection
|
||||
s = recordReport(s);
|
||||
assert.equal(checkLimitReached(s), false); // any report resets the counter
|
||||
});
|
||||
|
||||
// ---------- env default cap ----------
|
||||
|
||||
test("environment caps: valid overrides and invalid values fall back", () => {
|
||||
assert.equal(envDefaultMaxChecks({}), 25);
|
||||
assert.equal(envDefaultMaxChecks({ GOAL_MAX_CHECKS: "10" }), 10);
|
||||
assert.equal(envDefaultMaxChecks({ GOAL_MAX_CHECKS: "abc" }), 25);
|
||||
assert.equal(envDefaultMaxChecks({ GOAL_MAX_CHECKS: "0" }), 25);
|
||||
assert.equal(envDefaultMaxChecks({ GOAL_MAX_CHECKS: "" }), 25);
|
||||
assert.equal(envDefaultMaxNoProgressReports({}), 3);
|
||||
assert.equal(envDefaultMaxNoProgressReports({ GOAL_MAX_NO_PROGRESS_REPORTS: "5" }), 5);
|
||||
assert.equal(envDefaultMaxNoProgressReports({ GOAL_MAX_NO_PROGRESS_REPORTS: "0" }), 3);
|
||||
assert.equal(envDefaultMaxNoProgressReports({ GOAL_MAX_NO_PROGRESS_REPORTS: "bad" }), 3);
|
||||
});
|
||||
|
||||
// ---------- command parsing ----------
|
||||
|
||||
test("parse: verbs and status", () => {
|
||||
assert.deepEqual(parseGoalCommand(""), { kind: "status" });
|
||||
assert.deepEqual(parseGoalCommand(" "), { kind: "status" });
|
||||
assert.deepEqual(parseGoalCommand("stop"), { kind: "stop" });
|
||||
assert.deepEqual(parseGoalCommand("STOP"), { kind: "stop" });
|
||||
assert.deepEqual(parseGoalCommand("clear"), { kind: "clear" });
|
||||
assert.deepEqual(parseGoalCommand("resume"), { kind: "resume" });
|
||||
});
|
||||
|
||||
test("parse: set with plain text", () => {
|
||||
assert.deepEqual(parseGoalCommand("ship the thing"), { kind: "set", text: "ship the thing", max: undefined });
|
||||
assert.deepEqual(parseGoalCommand(" padded "), { kind: "set", text: "padded", max: undefined });
|
||||
});
|
||||
|
||||
test("parse: --max forms attach to set", () => {
|
||||
assert.deepEqual(parseGoalCommand("do it --max 5"), { kind: "set", text: "do it", max: 5 });
|
||||
assert.deepEqual(parseGoalCommand("--max 5 do it"), { kind: "set", text: "do it", max: 5 });
|
||||
assert.deepEqual(parseGoalCommand("do it --max=7"), { kind: "set", text: "do it", max: 7 });
|
||||
assert.deepEqual(parseGoalCommand("--max 40 write the spec"), {
|
||||
kind: "set",
|
||||
text: "write the spec",
|
||||
max: 40,
|
||||
});
|
||||
});
|
||||
|
||||
test("parse: --max errors", () => {
|
||||
assert.equal(parseGoalCommand("--max").kind, "error");
|
||||
assert.equal(parseGoalCommand("--max abc").kind, "error");
|
||||
assert.equal(parseGoalCommand("text --max 0").kind, "error");
|
||||
assert.equal(parseGoalCommand("text --max").kind, "error");
|
||||
assert.equal(parseGoalCommand("--max 5").kind, "error"); // no goal text
|
||||
assert.equal(parseGoalCommand("text --max 5.0").kind, "error");
|
||||
});
|
||||
|
||||
test("parse: text containing verbs is goal text, not a verb", () => {
|
||||
assert.deepEqual(parseGoalCommand("stop now"), { kind: "set", text: "stop now", max: undefined });
|
||||
});
|
||||
|
||||
test("setGoal without --max uses fresh default, not the previous goal's cap", () => {
|
||||
let s = setGoal(initialState(25), "first", 3);
|
||||
assert.equal(s.maxChecks, 3);
|
||||
s = clearGoal(s);
|
||||
s = setGoal(s, "second");
|
||||
assert.equal(s.maxChecks, 25); // per-goal --max: no leak from the capped goal
|
||||
s = setGoal(s, "third", 40);
|
||||
assert.equal(s.maxChecks, 40);
|
||||
s = setGoal(s, "fourth");
|
||||
assert.equal(s.maxChecks, 25);
|
||||
assert.equal(setGoal(initialState(25), "g", undefined).maxChecks, envDefaultMaxChecks({ GOAL_MAX_CHECKS: "" }));
|
||||
});
|
||||
|
||||
// ---------- settle decision (abort/error/cap pause paths, AC4 + AC5) ----------
|
||||
|
||||
test("settle: normal completion injects, abort and error pause", () => {
|
||||
const s = setGoal(initialState(25), "goal");
|
||||
assert.deepEqual(decideSettle(s, "toolUse"), { action: "inject" });
|
||||
assert.deepEqual(decideSettle(s, "stop"), { action: "inject" });
|
||||
assert.deepEqual(decideSettle(s, undefined), { action: "inject" });
|
||||
|
||||
const aborted = decideSettle(s, "aborted");
|
||||
assert.equal(aborted.action, "pause");
|
||||
assert.match((aborted as { reason: string }).reason, /aborted/);
|
||||
|
||||
const errored = decideSettle(s, "error");
|
||||
assert.equal(errored.action, "pause");
|
||||
assert.match((errored as { reason: string }).reason, /error/);
|
||||
});
|
||||
|
||||
test("settle: cap exhaustion pauses with cap reason", () => {
|
||||
const s = setGoal(initialState(25), "goal", 2);
|
||||
s.checks = 2;
|
||||
const d = decideSettle(s, "stop");
|
||||
assert.equal(d.action, "pause");
|
||||
assert.match((d as { reason: string }).reason, /cap of 2/);
|
||||
// an injected-check counter reset by a report makes the cap pass again
|
||||
assert.deepEqual(decideSettle(recordReport(s), "stop"), { action: "inject" });
|
||||
});
|
||||
|
||||
// ---------- report validation (FR3) ----------
|
||||
|
||||
test("validateReport: satisfied and blocked require evidence; in_progress does not", () => {
|
||||
assert.match(validateReport("satisfied", "")!, /evidence/);
|
||||
assert.match(validateReport("satisfied", " ")!, /evidence/);
|
||||
assert.match(validateReport("satisfied", undefined)!, /evidence/);
|
||||
assert.match(validateReport("blocked", "")!, /reason/);
|
||||
assert.match(validateReport("blocked", undefined)!, /reason/);
|
||||
assert.equal(validateReport("in_progress", ""), null);
|
||||
assert.equal(validateReport("satisfied", "counted to three"), null);
|
||||
assert.equal(validateReport("blocked", "missing credentials"), null);
|
||||
});
|
||||
|
||||
// ---------- store ----------
|
||||
|
||||
test("store: round-trip, missing file, corrupt file", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-test-"));
|
||||
try {
|
||||
const path = join(dir, "goal-state.json");
|
||||
assert.deepEqual(loadState(path), initialState()); // missing file -> initial
|
||||
|
||||
const s = setGoal(initialState(25), "durable goal", 11);
|
||||
s.checks = 4;
|
||||
s.noProgressReports = 2;
|
||||
s.workEventSinceReport = true;
|
||||
s.lastProgressFingerprint = "abc";
|
||||
s.lastNextAction = "measure gate";
|
||||
s.activeWait = { owner: "reviewer", watchId: "review-watch", nextCheck: "review arrives" };
|
||||
saveState(s, path);
|
||||
const loaded = loadState(path);
|
||||
assert.equal(loaded.text, "durable goal");
|
||||
assert.equal(loaded.status, "active");
|
||||
assert.equal(loaded.maxChecks, 11);
|
||||
assert.equal(loaded.checks, 4);
|
||||
assert.equal(loaded.noProgressReports, 2);
|
||||
assert.equal(loaded.workEventSinceReport, true);
|
||||
assert.equal(loaded.lastProgressFingerprint, "abc");
|
||||
assert.equal(loaded.lastNextAction, "measure gate");
|
||||
assert.equal(loaded.activeWait?.watchId, "review-watch");
|
||||
|
||||
writeFileSync(path, "{not json", "utf8");
|
||||
assert.deepEqual(loadState(path), initialState()); // corrupt -> initial
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("store: legacy version-1 active state gains NG8 defaults without losing focus", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-test-legacy-v1-"));
|
||||
try {
|
||||
const path = join(dir, "goal-state.legacy-v1.json");
|
||||
writeFileSync(
|
||||
path,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
text: "preserve this active goal",
|
||||
status: "active",
|
||||
checks: 7,
|
||||
maxChecks: 19,
|
||||
setAt: "2026-08-30T00:00:00.000Z",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const state = loadState(path);
|
||||
assert.equal(state.text, "preserve this active goal");
|
||||
assert.equal(state.status, "active");
|
||||
assert.equal(state.checks, 7);
|
||||
assert.equal(state.maxChecks, 19);
|
||||
assert.equal(state.noProgressReports, 0);
|
||||
assert.equal(state.maxNoProgressReports, DEFAULT_MAX_NO_PROGRESS_REPORTS);
|
||||
assert.equal(state.workEventSinceReport, false);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("store: normalize repairs bad counters", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-test-"));
|
||||
try {
|
||||
const path = join(dir, "goal-state.json");
|
||||
writeFileSync(
|
||||
path,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
text: "g",
|
||||
status: "active",
|
||||
checks: -3,
|
||||
maxChecks: 0,
|
||||
noProgressReports: -9,
|
||||
maxNoProgressReports: 0,
|
||||
workEventSinceReport: "yes",
|
||||
setAt: "t",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const s = loadState(path);
|
||||
assert.equal(s.checks, 0);
|
||||
assert.equal(s.maxChecks, DEFAULT_MAX_CHECKS);
|
||||
assert.equal(s.noProgressReports, 0);
|
||||
assert.equal(s.maxNoProgressReports, DEFAULT_MAX_NO_PROGRESS_REPORTS);
|
||||
assert.equal(s.workEventSinceReport, false);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("store: agent dir resolves from PI_CODING_AGENT_DIR with ~/.pi/agent fallback", () => {
|
||||
assert.equal(agentStateDir({ PI_CODING_AGENT_DIR: "/seat/.pi/agent" }), "/seat/.pi/agent");
|
||||
assert.equal(agentStateDir({}), join(homedir(), ".pi", "agent"));
|
||||
assert.equal(stateFilePath("/d"), join("/d", "goal-state.json"));
|
||||
});
|
||||
|
||||
test("store pauses invalid persisted bounded-wait configuration", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-wait-invalid-"));
|
||||
try {
|
||||
const path = join(dir, "state.json");
|
||||
for (const fields of [
|
||||
{ waitTimeoutSeconds: -1 },
|
||||
{ waitTimeoutSeconds: 60, activeWait: { owner: "operator", deadlineAt: 1e100 } },
|
||||
{ waitTimeoutSeconds: 60, waitWakeUsed: "false" },
|
||||
{ waitTimeoutSeconds: 60, activeWait: { owner: "operator" } },
|
||||
{ waitTimeoutSeconds: 60, activeWait: { owner: "operator", deadlineAt: 123456, wakeSent: true } },
|
||||
]) {
|
||||
writeFileSync(path, JSON.stringify({ ...setGoal(initialState(), "output"), ...fields }));
|
||||
assert.equal(loadState(path).status, "paused");
|
||||
assert.match(loadState(path).pausedReason!, /invalid persisted wait/);
|
||||
}
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
DEFAULT_MAX_NO_PROGRESS_REPORTS,
|
||||
initialState,
|
||||
recordCheckInjected,
|
||||
recordInProgressReport,
|
||||
recordWorkEvent,
|
||||
setGoal,
|
||||
type ProgressDetails,
|
||||
} from "../lib/state.ts";
|
||||
|
||||
const measurement: ProgressDetails = {
|
||||
kind: "measurement",
|
||||
gate: "PR review",
|
||||
owner: "rev-code-01",
|
||||
lastMeasurement: "PR head abc123 remains unreviewed at 2026-08-31T15:00:00Z",
|
||||
nextAction: "dispatch exact-head review",
|
||||
};
|
||||
|
||||
function reportOnlyCycle(state: ReturnType<typeof setGoal>) {
|
||||
const checked = recordCheckInjected(state);
|
||||
return recordInProgressReport(checked, { evidence: "", progress: undefined }).state;
|
||||
}
|
||||
|
||||
test("exact report-only loop pauses within the no-progress bound", () => {
|
||||
let state = setGoal(initialState(), "ship", 25);
|
||||
for (let n = 1; n <= DEFAULT_MAX_NO_PROGRESS_REPORTS; n++) {
|
||||
state = reportOnlyCycle(state);
|
||||
assert.equal(state.noProgressReports, n);
|
||||
}
|
||||
assert.equal(state.status, "paused");
|
||||
assert.match(state.pausedReason ?? "", /no substantive progress/);
|
||||
});
|
||||
|
||||
test("empty, malformed, and duplicate evidence never reset no-progress", () => {
|
||||
let state = setGoal(initialState(), "ship", 25);
|
||||
state = recordInProgressReport(recordCheckInjected(state), { evidence: "", progress: undefined }).state;
|
||||
assert.equal(state.noProgressReports, 1);
|
||||
|
||||
state = recordInProgressReport(recordCheckInjected(state), {
|
||||
evidence: "measured",
|
||||
progress: { ...measurement, owner: "" },
|
||||
}).state;
|
||||
assert.equal(state.noProgressReports, 2);
|
||||
|
||||
state = recordWorkEvent(state, "read", "2026-08-31T15:00:00Z");
|
||||
const accepted = recordInProgressReport(recordCheckInjected(state), {
|
||||
evidence: "measured live PR state",
|
||||
progress: measurement,
|
||||
});
|
||||
assert.equal(accepted.classification, "progress");
|
||||
state = accepted.state;
|
||||
assert.equal(state.noProgressReports, 0);
|
||||
|
||||
state = recordWorkEvent(state, "read", "2026-08-31T15:01:00Z");
|
||||
const duplicate = recordInProgressReport(recordCheckInjected(state), {
|
||||
evidence: "measured live PR state",
|
||||
progress: measurement,
|
||||
});
|
||||
assert.equal(duplicate.classification, "no_progress");
|
||||
assert.equal(duplicate.state.noProgressReports, 1);
|
||||
});
|
||||
|
||||
test("measurement, action, and delegation require an observed successful work event", () => {
|
||||
for (const kind of ["measurement", "action", "delegation"] as const) {
|
||||
const details = { ...measurement, kind };
|
||||
let state = setGoal(initialState(), kind, 25);
|
||||
const unsupported = recordInProgressReport(recordCheckInjected(state), {
|
||||
evidence: `${kind} claim`,
|
||||
progress: details,
|
||||
});
|
||||
assert.equal(unsupported.classification, "no_progress");
|
||||
|
||||
state = recordWorkEvent(unsupported.state, kind === "action" ? "edit" : "bash", "2026-08-31T15:00:00Z");
|
||||
const accepted = recordInProgressReport(recordCheckInjected(state), {
|
||||
evidence: `${kind} completed with a new result`,
|
||||
progress: { ...details, lastMeasurement: `${details.lastMeasurement} ${kind}` },
|
||||
});
|
||||
assert.equal(accepted.classification, "progress");
|
||||
assert.equal(accepted.state.noProgressReports, 0);
|
||||
assert.equal(accepted.state.checks, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test("explicit non-tool work requires a concrete artifact", () => {
|
||||
let state = setGoal(initialState(), "draft", 25);
|
||||
const missing = recordInProgressReport(recordCheckInjected(state), {
|
||||
evidence: "drafted reasoning",
|
||||
progress: { ...measurement, kind: "non_tool" },
|
||||
});
|
||||
assert.equal(missing.classification, "no_progress");
|
||||
|
||||
const accepted = recordInProgressReport(recordCheckInjected(missing.state), {
|
||||
evidence: "completed architecture decision",
|
||||
progress: { ...measurement, kind: "non_tool", artifact: "work/decision-D12.md" },
|
||||
});
|
||||
assert.equal(accepted.classification, "progress");
|
||||
assert.equal(accepted.state.noProgressReports, 0);
|
||||
});
|
||||
|
||||
test("a legitimate external wait remains active without false-positive pause", () => {
|
||||
let state = setGoal(initialState(), "await review", 25);
|
||||
state = recordInProgressReport(recordCheckInjected(state), { evidence: "", progress: undefined }).state;
|
||||
state = recordInProgressReport(recordCheckInjected(state), { evidence: "", progress: undefined }).state;
|
||||
assert.equal(state.noProgressReports, 2);
|
||||
|
||||
const waiting: ProgressDetails = {
|
||||
kind: "wait",
|
||||
gate: "independent review",
|
||||
owner: "rev-code-01",
|
||||
lastMeasurement: "review dispatched at 2026-08-31T15:00:00Z",
|
||||
nextAction: "merge only after PASS",
|
||||
watchId: "ng8-review",
|
||||
nextCheck: "review artifact exists",
|
||||
};
|
||||
for (let n = 0; n < 10; n++) {
|
||||
const outcome = recordInProgressReport(recordCheckInjected(state), {
|
||||
evidence: "approved watch remains active",
|
||||
progress: waiting,
|
||||
});
|
||||
assert.equal(outcome.classification, "waiting");
|
||||
state = outcome.state;
|
||||
assert.equal(state.status, "active");
|
||||
assert.equal(state.noProgressReports, 2);
|
||||
assert.equal(state.checks, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test("RED CONTROL: neutralizing no-progress counting restores the infinite loop", async () => {
|
||||
const source = readFileSync(new URL("../lib/state.ts", import.meta.url), "utf8");
|
||||
const sabotaged = source.replace(
|
||||
"const nextNoProgress = state.noProgressReports + 1; // NG8_COUNT_GATE",
|
||||
"const nextNoProgress = state.noProgressReports; // NG8_COUNT_GATE neutralized",
|
||||
);
|
||||
assert.notEqual(sabotaged, source, "count sabotage must change source");
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-ng8-count-red-"));
|
||||
try {
|
||||
const path = join(dir, "state.ts");
|
||||
writeFileSync(path, sabotaged);
|
||||
const red = await import(`${pathToFileURL(path).href}?red=${Date.now()}`);
|
||||
let state = red.setGoal(red.initialState(), "ship", 25);
|
||||
for (let n = 0; n < DEFAULT_MAX_NO_PROGRESS_REPORTS + 2; n++) {
|
||||
state = red.recordCheckInjected(state);
|
||||
state = red.recordInProgressReport(state, { evidence: "", progress: undefined }).state;
|
||||
}
|
||||
assert.equal(state.status, "active", "RED: report-only loop survives when counting is neutralized");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("RED CONTROL: neutralizing pause enforcement leaves the counted loop active", async () => {
|
||||
const source = readFileSync(new URL("../lib/state.ts", import.meta.url), "utf8");
|
||||
const sabotaged = source.replace(
|
||||
"if (nextNoProgress >= state.maxNoProgressReports) { // NG8_PAUSE_GATE",
|
||||
"if (false && nextNoProgress >= state.maxNoProgressReports) { // NG8_PAUSE_GATE neutralized",
|
||||
);
|
||||
assert.notEqual(sabotaged, source, "pause sabotage must change source");
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-ng8-pause-red-"));
|
||||
try {
|
||||
const path = join(dir, "state.ts");
|
||||
writeFileSync(path, sabotaged);
|
||||
const red = await import(`${pathToFileURL(path).href}?red=${Date.now()}`);
|
||||
let state = red.setGoal(red.initialState(), "ship", 25);
|
||||
for (let n = 0; n < DEFAULT_MAX_NO_PROGRESS_REPORTS; n++) {
|
||||
state = red.recordCheckInjected(state);
|
||||
state = red.recordInProgressReport(state, { evidence: "", progress: undefined }).state;
|
||||
}
|
||||
assert.equal(state.noProgressReports, DEFAULT_MAX_NO_PROGRESS_REPORTS);
|
||||
assert.equal(state.status, "active", "RED: counted loop survives when pause gate is neutralized");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { initialState, setGoal, recordInProgressReport } from "../lib/state.ts";
|
||||
import { decideSettle } from "../lib/settle.ts";
|
||||
import { parseGoalCommand } from "../lib/parse.ts";
|
||||
|
||||
const wait = { evidence: "Dependency pending", progress: { kind: "wait" as const, gate: "input", owner: "operator", lastMeasurement: "input absent", nextAction: "Read input when available", nextCheck: "input file arrives" } };
|
||||
|
||||
test("opted-in waits suspend settle injection", () => {
|
||||
const state = { ...setGoal(initialState(), "prepare output"), waitTimeoutSeconds: 60 };
|
||||
const waiting = recordInProgressReport(state, wait).state;
|
||||
assert.equal(decideSettle(waiting, "stop").action, "wait");
|
||||
});
|
||||
|
||||
test("operator can select bounded waits per goal", () => {
|
||||
assert.deepEqual(parseGoalCommand("--wait-timeout 60 --max 5 prepare output"), { kind: "set", text: "prepare output", max: 5, waitTimeoutSeconds: 60 });
|
||||
});
|
||||
|
||||
test("timeout input is bounded and opt-in never leaks to another goal", () => {
|
||||
for (const args of ["--wait-timeout", "--wait-timeout=", "--wait-timeout 0 x", "--wait-timeout 9 x", "--wait-timeout 86401 x", "--wait-timeout 60.0 x", "--wait-timeout 60", "--wait-timeout 60 --wait-timeout 30 x"]) {
|
||||
assert.equal(parseGoalCommand(args).kind, "error", args);
|
||||
}
|
||||
const opted = setGoal(initialState(), "one", undefined, 60);
|
||||
assert.equal(setGoal(opted, "two").waitTimeoutSeconds, undefined);
|
||||
const legacyWait = recordInProgressReport(setGoal(initialState(), "legacy"), wait).state;
|
||||
assert.equal(decideSettle(legacyWait, "stop").action, "inject");
|
||||
});
|
||||
@@ -0,0 +1,599 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { fencedStateFilePath } from "../lib/store.ts";
|
||||
import {
|
||||
publishGoalPolicy,
|
||||
registerGoalTrackedItemResolver,
|
||||
resetGoalPolicyRegistryForTest,
|
||||
} from "../../mosaic-core/lib/goal-policy.ts";
|
||||
|
||||
interface SentMessage {
|
||||
content: string;
|
||||
options?: { deliverAs?: "steer" | "followUp" };
|
||||
}
|
||||
|
||||
const GOAL_ROOT = dirname(fileURLToPath(new URL("../index.ts", import.meta.url)));
|
||||
const INCARNATION_SOURCE = join(GOAL_ROOT, "..", "mosaic-core", "lib", "incarnation.ts");
|
||||
const EXECUTIVE_UPDATE_FORMAT = "structured progress in the ms-executive-update format (Just Completed / Next Step / Blocked)";
|
||||
const PROACTIVE_LOOP_DIRECTIVE =
|
||||
"Before reporting, run the ms-proactive-agent loop: reconcile records, perform the next authorized ready task, and verify its result. If no task can proceed, report a concrete wait or blocker. This extension owns goal lifecycle; use skills for task tracking, not a second goal loop.";
|
||||
|
||||
async function loadGoalExtension(root: string): Promise<(pi: any) => void> {
|
||||
const extensionRoot = join(root, "extensions", "goal-runtime");
|
||||
cpSync(join(GOAL_ROOT, "lib"), join(extensionRoot, "lib"), { recursive: true });
|
||||
mkdirSync(join(root, "extensions", "mosaic-core", "lib"), { recursive: true });
|
||||
cpSync(INCARNATION_SOURCE, join(root, "extensions", "mosaic-core", "lib", "incarnation.ts"));
|
||||
cpSync(join(GOAL_ROOT, "..", "mosaic-core", "lib", "goal-policy.ts"), join(root, "extensions", "mosaic-core", "lib", "goal-policy.ts"));
|
||||
|
||||
const source = readFileSync(join(GOAL_ROOT, "index.ts"), "utf8");
|
||||
const withoutTypebox = source.replace(
|
||||
'import { Type } from "typebox";',
|
||||
'const Type = { Object: (properties: unknown) => ({ type: "object", properties }), Optional: (schema: unknown) => schema, String: (options: unknown = {}) => ({ type: "string", ...(options as object) }) };',
|
||||
);
|
||||
const selfContained = withoutTypebox.replace(
|
||||
'import { StringEnum } from "@earendil-works/pi-ai";',
|
||||
'const StringEnum = (values: readonly string[]) => ({ type: "string", enum: values });',
|
||||
);
|
||||
assert.notEqual(withoutTypebox, source, "headless harness must replace TypeBox runtime import");
|
||||
assert.notEqual(selfContained, withoutTypebox, "headless harness must replace pi-ai runtime import");
|
||||
writeFileSync(join(extensionRoot, "index.ts"), selfContained);
|
||||
return (await import(`${pathToFileURL(join(extensionRoot, "index.ts")).href}?runtime=${Date.now()}`)).default;
|
||||
}
|
||||
|
||||
async function createHeadlessHarness(root: string, incarnation: string) {
|
||||
const agentDir = join(root, "state", "goal");
|
||||
const handlers = new Map<string, Array<(event: any, ctx: any) => any>>();
|
||||
const commands = new Map<string, any>();
|
||||
const tools = new Map<string, any>();
|
||||
const sent: SentMessage[] = [];
|
||||
const notifications: Array<{ message: string; level: string }> = [];
|
||||
const widgets: unknown[] = [];
|
||||
const statuses: string[] = [];
|
||||
const shortcuts = new Map<string, any>();
|
||||
|
||||
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
const previousIncarnation = process.env.MOSAIC_LAUNCH_INCARNATION;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
process.env.MOSAIC_LAUNCH_INCARNATION = incarnation;
|
||||
|
||||
const pi: any = {
|
||||
on(name: string, handler: (event: any, ctx: any) => any) {
|
||||
handlers.set(name, [...(handlers.get(name) ?? []), handler]);
|
||||
},
|
||||
registerCommand(name: string, command: any) {
|
||||
commands.set(name, command);
|
||||
},
|
||||
registerShortcut(key: string, shortcut: any) { shortcuts.set(key, shortcut); },
|
||||
registerTool(tool: any) {
|
||||
tools.set(tool.name, tool);
|
||||
},
|
||||
sendUserMessage(content: string, options?: SentMessage["options"]) {
|
||||
sent.push({ content, options });
|
||||
},
|
||||
};
|
||||
|
||||
let idle = false;
|
||||
const ctx: any = {
|
||||
mode: "interactive",
|
||||
hasUI: true,
|
||||
isIdle: () => idle,
|
||||
ui: {
|
||||
theme: { fg: (color: string, text: string) => `[${color}]${text}` },
|
||||
setStatus(_id: string, text: string) { statuses.push(text); },
|
||||
notify(message: string, level: string) {
|
||||
notifications.push({ message, level });
|
||||
},
|
||||
setWidget(_id: string, value: unknown) {
|
||||
widgets.push(value);
|
||||
},
|
||||
},
|
||||
sessionManager: {
|
||||
getBranch: () => [{ type: "message", message: { role: "assistant", stopReason: "stop" } }],
|
||||
},
|
||||
};
|
||||
|
||||
const goalExtension = await loadGoalExtension(root);
|
||||
goalExtension(pi);
|
||||
|
||||
return {
|
||||
commands,
|
||||
tools,
|
||||
sent,
|
||||
notifications,
|
||||
widgets,
|
||||
statuses,
|
||||
shortcuts,
|
||||
ctx,
|
||||
setIdle(value: boolean) {
|
||||
idle = value;
|
||||
},
|
||||
async emit(name: string, event: any = { type: name }) {
|
||||
for (const handler of handlers.get(name) ?? []) await handler(event, ctx);
|
||||
},
|
||||
state() {
|
||||
return JSON.parse(readFileSync(fencedStateFilePath(agentDir, incarnation), "utf8"));
|
||||
},
|
||||
restoreEnv() {
|
||||
if (previousAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previousAgentDir;
|
||||
if (previousIncarnation === undefined) delete process.env.MOSAIC_LAUNCH_INCARNATION;
|
||||
else process.env.MOSAIC_LAUNCH_INCARNATION = previousIncarnation;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("goal reporting instructions name the executive-update format and proactive loop", () => {
|
||||
const source = readFileSync(join(GOAL_ROOT, "index.ts"), "utf8");
|
||||
const checkPrompt = source.slice(source.indexOf("function checkPrompt(): string"), source.indexOf("/** Inject one check prompt"));
|
||||
const goalReport = source.slice(source.indexOf('name: "goal_report"'), source.indexOf("promptSnippet:", source.indexOf('name: "goal_report"')));
|
||||
const beforeAgentStart = source.slice(source.indexOf('pi.on("before_agent_start"'), source.indexOf('pi.on("tool_result"'));
|
||||
|
||||
for (const [section, text] of Object.entries({ checkPrompt, goalReport, beforeAgentStart })) {
|
||||
assert.ok(text.includes(EXECUTIVE_UPDATE_FORMAT), `${section} must name the executive-update format`);
|
||||
assert.ok(text.includes(PROACTIVE_LOOP_DIRECTIVE), `${section} must name the proactive loop`);
|
||||
}
|
||||
});
|
||||
|
||||
test("headless runtime exercises goal_report, agent_settled, follow-up injection, and pause", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-ng8-runtime-"));
|
||||
const harness = await createHeadlessHarness(dir, "ng8-headless-runtime");
|
||||
try {
|
||||
const goal = harness.commands.get("goal");
|
||||
const report = harness.tools.get("goal_report");
|
||||
assert.ok(goal);
|
||||
assert.ok(report);
|
||||
|
||||
await goal.handler("ship safely", harness.ctx);
|
||||
assert.equal(harness.sent.length, 1);
|
||||
assert.equal(harness.sent[0].options?.deliverAs, "followUp");
|
||||
assert.match(harness.sent[0].content, /no-progress 0\/3/);
|
||||
|
||||
for (let cycle = 1; cycle <= 3; cycle++) {
|
||||
const result = await report.execute(`report-${cycle}`, { status: "in_progress" }, undefined, undefined, harness.ctx);
|
||||
await harness.emit("tool_result", {
|
||||
type: "tool_result",
|
||||
toolCallId: `report-${cycle}`,
|
||||
toolName: "goal_report",
|
||||
input: { status: "in_progress" },
|
||||
content: result.content,
|
||||
isError: false,
|
||||
});
|
||||
await harness.emit("agent_settled");
|
||||
if (cycle < 3) assert.equal(harness.sent.length, cycle + 1);
|
||||
}
|
||||
|
||||
assert.equal(harness.sent.length, 3, "paused loop injects no fourth check");
|
||||
assert.equal(harness.state().status, "paused");
|
||||
assert.equal(harness.state().noProgressReports, 3);
|
||||
assert.ok(harness.notifications.some((notice) => notice.level === "warning" && /no substantive progress/.test(notice.message)));
|
||||
} finally {
|
||||
harness.restoreEnv();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("C7/C8: attested parser and resolver reject before Goal state changes", async () => {
|
||||
resetGoalPolicyRegistryForTest();
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-v4-policy-"));
|
||||
const incarnation = "inc-goal-v4";
|
||||
const harness = await createHeadlessHarness(dir, incarnation);
|
||||
try {
|
||||
assert.equal(registerGoalTrackedItemResolver(incarnation, async () => Object.freeze({ outcome: "zero" as const })).ok, true);
|
||||
const journalReasons: string[] = [];
|
||||
const publication = publishGoalPolicy({
|
||||
schemaVersion: 1,
|
||||
role: "plan-ng",
|
||||
roleRevision: 4,
|
||||
manifestSha256: "a".repeat(64),
|
||||
format: "ms-executive-update/v1",
|
||||
contractPath: "skills-local/ms-executive-update/SKILL.md",
|
||||
contractSection: "Machine contract (for `goal_report` payloads and any parser)",
|
||||
contractBlob: "df30c6fbb54b4a65a298c9e51c07f742610d171c",
|
||||
contractSha256: "bbea48a46b1f8da7bc759f86856fb52830b7dde456b826317163c6dc6ccab319",
|
||||
enforcement: "pre-state-change-fail-closed",
|
||||
identifierResolution: "consumer-fail-closed",
|
||||
launchGeneration: 1,
|
||||
incarnationId: incarnation,
|
||||
}, (reason) => journalReasons.push(reason));
|
||||
assert.equal(publication.ok, true);
|
||||
|
||||
const goal = harness.commands.get("goal");
|
||||
const report = harness.tools.get("goal_report");
|
||||
await goal.handler("attested policy", harness.ctx);
|
||||
const before = harness.state();
|
||||
const update = [
|
||||
"Just Completed:", "", "* T165: contracts added", "", "Next Step:", "", "* T165: run tests", "", "Blocked:", "", "* T165: none \u2014 nothing from you", "",
|
||||
].join("\n");
|
||||
await assert.rejects(
|
||||
() => report.execute("attested-reject", { status: "satisfied", evidence: update }, undefined, undefined, harness.ctx),
|
||||
/resolver-zero/,
|
||||
);
|
||||
const after = harness.state();
|
||||
assert.equal(after.status, "active");
|
||||
assert.equal(after.checks, before.checks);
|
||||
assert.equal(after.noProgressReports, before.noProgressReports);
|
||||
|
||||
const noChange = "No change since T165; still waiting on T165.";
|
||||
for (const status of ["satisfied", "blocked"] as const) {
|
||||
await assert.rejects(
|
||||
() => report.execute(`no-change-${status}`, { status, evidence: noChange }, undefined, undefined, harness.ctx),
|
||||
/no-change-status/,
|
||||
);
|
||||
}
|
||||
const terminalAfter = harness.state();
|
||||
assert.equal(terminalAfter.status, "active", "unchanged form cannot settle or pause the goal");
|
||||
assert.equal(terminalAfter.checks, before.checks);
|
||||
assert.equal(terminalAfter.noProgressReports, before.noProgressReports);
|
||||
assert.deepEqual(
|
||||
journalReasons,
|
||||
["resolver-zero", "no-change-status", "no-change-status"],
|
||||
"all denials journal stable codes without report payload text",
|
||||
);
|
||||
} finally {
|
||||
resetGoalPolicyRegistryForTest();
|
||||
harness.restoreEnv();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("headless runtime accepts a tool-backed measurement and preserves terminal behavior", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-ng8-runtime-work-"));
|
||||
const harness = await createHeadlessHarness(dir, "ng8-headless-work");
|
||||
try {
|
||||
const goal = harness.commands.get("goal");
|
||||
const report = harness.tools.get("goal_report");
|
||||
await goal.handler("measure", harness.ctx);
|
||||
|
||||
await harness.emit("tool_result", {
|
||||
type: "tool_result",
|
||||
toolCallId: "read-error",
|
||||
toolName: "read",
|
||||
input: { path: "/tmp/missing" },
|
||||
content: [{ type: "text", text: "missing" }],
|
||||
isError: true,
|
||||
});
|
||||
assert.equal(harness.state().workEventSinceReport, false, "failed tools are not progress events");
|
||||
|
||||
await harness.emit("tool_result", {
|
||||
type: "tool_result",
|
||||
toolCallId: "read-1",
|
||||
toolName: "read",
|
||||
input: { path: "/tmp/status" },
|
||||
content: [{ type: "text", text: "measured" }],
|
||||
isError: false,
|
||||
});
|
||||
assert.equal(harness.state().workEventSinceReport, true);
|
||||
|
||||
const progress = {
|
||||
kind: "measurement",
|
||||
gate: "review",
|
||||
owner: "reviewer",
|
||||
lastMeasurement: "head abc remains pending at 2026-08-31T15:00:00Z",
|
||||
nextAction: "dispatch reviewer",
|
||||
};
|
||||
const accepted = await report.execute(
|
||||
"progress-1",
|
||||
{ status: "in_progress", evidence: "measured exact head", progress },
|
||||
undefined,
|
||||
undefined,
|
||||
harness.ctx,
|
||||
);
|
||||
assert.match(accepted.content[0].text, /Recorded substantive progress/);
|
||||
assert.equal(harness.state().noProgressReports, 0);
|
||||
assert.equal(harness.state().workEventSinceReport, false);
|
||||
|
||||
await goal.handler("done goal", harness.ctx);
|
||||
await assert.rejects(
|
||||
() => report.execute("satisfied-empty", { status: "satisfied" }, undefined, undefined, harness.ctx),
|
||||
/requires non-empty evidence/,
|
||||
);
|
||||
const satisfied = await report.execute(
|
||||
"satisfied",
|
||||
{ status: "satisfied", evidence: "acceptance evidence" },
|
||||
undefined,
|
||||
undefined,
|
||||
harness.ctx,
|
||||
);
|
||||
assert.match(satisfied.content[0].text, /SATISFIED/);
|
||||
assert.equal(harness.state().status, "none");
|
||||
|
||||
await goal.handler("blocked goal", harness.ctx);
|
||||
const blocked = await report.execute(
|
||||
"blocked",
|
||||
{ status: "blocked", evidence: "missing credential" },
|
||||
undefined,
|
||||
undefined,
|
||||
harness.ctx,
|
||||
);
|
||||
assert.match(blocked.content[0].text, /BLOCKED/);
|
||||
assert.equal(harness.state().status, "blocked");
|
||||
} finally {
|
||||
harness.restoreEnv();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
const waitReport = {
|
||||
status: "in_progress", evidence: "Input is pending",
|
||||
progress: { kind: "wait", gate: "input", owner: "operator", lastMeasurement: "input absent",
|
||||
nextAction: "Read input and finish output", nextCheck: "input file available" },
|
||||
};
|
||||
|
||||
test("quiet wait: no heartbeats, one deadline wake, unresolved dependency pauses", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 100000 });
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-quiet-runtime-"));
|
||||
const harness = await createHeadlessHarness(dir, "quiet-runtime");
|
||||
try {
|
||||
harness.setIdle(true);
|
||||
const goal = harness.commands.get("goal");
|
||||
const report = harness.tools.get("goal_report");
|
||||
await goal.handler("--wait-timeout 60 write output", harness.ctx);
|
||||
await report.execute("wait-1", waitReport, undefined, undefined, harness.ctx);
|
||||
const deadline = harness.state().activeWait.deadlineAt;
|
||||
for (let i = 0; i < 100; i++) await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, 1, "settle events cannot generate idle heartbeats");
|
||||
t.mock.timers.tick(30000);
|
||||
await report.execute("wait-2", { ...waitReport, evidence: "Still pending, differently worded" }, undefined, undefined, harness.ctx);
|
||||
assert.equal(harness.state().activeWait.deadlineAt, deadline, "unchanged waits never extend deadline");
|
||||
t.mock.timers.tick(29999);
|
||||
assert.equal(harness.sent.length, 1);
|
||||
t.mock.timers.tick(1);
|
||||
assert.equal(harness.sent.length, 2);
|
||||
assert.match(harness.sent[1].content, /Wait deadline reached/);
|
||||
assert.equal(harness.state().waitWakeUsed, true);
|
||||
const result = await report.execute("wait-3", waitReport, undefined, undefined, harness.ctx);
|
||||
assert.equal(result.details.goalStatus, "paused");
|
||||
t.mock.timers.tick(86400000);
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, 2, "unresolved deadline never re-arms itself");
|
||||
await assert.rejects(() => report.execute("late-satisfied", { status: "satisfied", evidence: "late result" }, undefined, undefined, harness.ctx), /paused/);
|
||||
} finally {
|
||||
await harness.emit("session_shutdown");
|
||||
harness.restoreEnv();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("quiet wait: persisted deadline survives reload without an immediate check", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 100000 });
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-quiet-reload-"));
|
||||
const harness = await createHeadlessHarness(dir, "quiet-reload");
|
||||
try {
|
||||
harness.setIdle(true);
|
||||
await harness.commands.get("goal").handler("--wait-timeout 60 write output", harness.ctx);
|
||||
await harness.tools.get("goal_report").execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
await harness.emit("session_shutdown");
|
||||
t.mock.timers.tick(30000);
|
||||
await harness.emit("session_start");
|
||||
assert.equal(harness.sent.length, 1);
|
||||
t.mock.timers.tick(30000);
|
||||
assert.equal(harness.sent.length, 2);
|
||||
assert.match(harness.sent[1].content, /Wait deadline reached/);
|
||||
} finally {
|
||||
await harness.emit("session_shutdown");
|
||||
harness.restoreEnv();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("quiet wait: stop, clear, replacement, and completion cancel the timer", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 100000 });
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-quiet-cancel-"));
|
||||
const harness = await createHeadlessHarness(dir, "quiet-cancel");
|
||||
try {
|
||||
harness.setIdle(true);
|
||||
const goal = harness.commands.get("goal");
|
||||
const report = harness.tools.get("goal_report");
|
||||
for (const operation of ["stop", "clear", "replacement", "satisfied"]) {
|
||||
await goal.handler("--wait-timeout 60 write output", harness.ctx);
|
||||
await report.execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
if (operation === "satisfied") {
|
||||
await report.execute("done", { status: "satisfied", evidence: "Output verified" }, undefined, undefined, harness.ctx);
|
||||
} else {
|
||||
await goal.handler(operation, harness.ctx);
|
||||
}
|
||||
const before = harness.sent.length;
|
||||
t.mock.timers.tick(61000);
|
||||
assert.equal(harness.sent.length, before, `${operation} leaves no deadline wake`);
|
||||
}
|
||||
} finally {
|
||||
await harness.emit("session_shutdown");
|
||||
harness.restoreEnv();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("quiet wait: a busy turn is allowed to settle before deadline injection", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 100000 });
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-quiet-busy-"));
|
||||
const harness = await createHeadlessHarness(dir, "quiet-busy");
|
||||
try {
|
||||
await harness.commands.get("goal").handler("--wait-timeout 60 write output", harness.ctx);
|
||||
await harness.tools.get("goal_report").execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
t.mock.timers.tick(60000);
|
||||
assert.equal(harness.sent.length, 1, "no deadline queued during a running turn");
|
||||
await harness.commands.get("goal").handler("stop", harness.ctx);
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, 1, "stop cancels deferred deadline");
|
||||
await harness.commands.get("goal").handler("resume", harness.ctx);
|
||||
assert.equal(harness.state().activeWait, undefined);
|
||||
await harness.tools.get("goal_report").execute("wait-again", waitReport, undefined, undefined, harness.ctx);
|
||||
t.mock.timers.tick(60000);
|
||||
const before = harness.sent.length;
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, before + 1);
|
||||
} finally {
|
||||
await harness.emit("session_shutdown");
|
||||
harness.restoreEnv();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("quiet wait: relevant progress cancels the wait; print mode has no timer wake", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 100000 });
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-quiet-progress-"));
|
||||
const harness = await createHeadlessHarness(dir, "quiet-progress");
|
||||
try {
|
||||
harness.setIdle(true);
|
||||
const goal = harness.commands.get("goal");
|
||||
const report = harness.tools.get("goal_report");
|
||||
await goal.handler("--wait-timeout 60 write output", harness.ctx);
|
||||
await report.execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
await harness.emit("tool_result", { toolName: "read", isError: false });
|
||||
await report.execute("progress", { status: "in_progress", evidence: "Input arrived and was verified", progress: { ...waitReport.progress, kind: "measurement", lastMeasurement: "Input present" } }, undefined, undefined, harness.ctx);
|
||||
assert.equal(harness.state().activeWait, undefined);
|
||||
t.mock.timers.tick(60000);
|
||||
assert.equal(harness.sent.length, 1);
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, 2, "ready work continues after wait is resolved");
|
||||
harness.ctx.mode = "print";
|
||||
await goal.handler("--wait-timeout 60 write output", harness.ctx);
|
||||
await report.execute("wait-print", waitReport, undefined, undefined, harness.ctx);
|
||||
t.mock.timers.tick(60000);
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, 2, "print mode never schedules a model wake");
|
||||
} finally {
|
||||
await harness.emit("session_shutdown");
|
||||
harness.restoreEnv();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("deadline readiness recovers after compaction without agent_settled", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 100000 });
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-compaction-"));
|
||||
const harness = await createHeadlessHarness(dir, "quiet-compaction");
|
||||
try {
|
||||
await harness.commands.get("goal").handler("--wait-timeout 60 output", harness.ctx);
|
||||
await harness.tools.get("goal_report").execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
t.mock.timers.tick(60000);
|
||||
assert.equal(harness.sent.length, 1);
|
||||
harness.setIdle(true);
|
||||
t.mock.timers.tick(1000);
|
||||
assert.equal(harness.sent.length, 2, "runtime readiness alone releases the deferred wake");
|
||||
} finally {
|
||||
await harness.emit("session_shutdown"); harness.restoreEnv(); rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("unobserved deadline dispatch pauses once; observed start cancels watchdog", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 100000 });
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-dispatch-"));
|
||||
const harness = await createHeadlessHarness(dir, "quiet-dispatch");
|
||||
try {
|
||||
harness.setIdle(true);
|
||||
const goal = harness.commands.get("goal");
|
||||
const report = harness.tools.get("goal_report");
|
||||
await goal.handler("--wait-timeout 60 output", harness.ctx);
|
||||
await report.execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
t.mock.timers.tick(60000);
|
||||
const sent = harness.sent.length;
|
||||
// Ordinary Pi sendUserMessage returns void; a rejected async delivery may never start.
|
||||
t.mock.timers.tick(30000);
|
||||
assert.equal(harness.state().status, "paused");
|
||||
assert.match(harness.state().pausedReason, /not observed/);
|
||||
assert.equal(harness.sent.length, sent, "watchdog never retries a potentially delivered request");
|
||||
await goal.handler("resume", harness.ctx);
|
||||
await report.execute("wait-again", waitReport, undefined, undefined, harness.ctx);
|
||||
t.mock.timers.tick(60000);
|
||||
await harness.emit("before_agent_start", { prompt: harness.sent.at(-1)!.content, systemPrompt: "base" });
|
||||
assert.equal(harness.state().activeWait.wakeObserved, true);
|
||||
t.mock.timers.tick(30000);
|
||||
assert.equal(harness.state().status, "active", "observed model start cancels the acknowledgment timer");
|
||||
} finally {
|
||||
await harness.emit("session_shutdown"); harness.restoreEnv(); rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("continuously busy runtime pauses after bounded readiness checks", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 100000 });
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-busy-limit-"));
|
||||
const harness = await createHeadlessHarness(dir, "quiet-busy-limit");
|
||||
try {
|
||||
await harness.commands.get("goal").handler("--wait-timeout 60 output", harness.ctx);
|
||||
await harness.tools.get("goal_report").execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
t.mock.timers.tick(60000);
|
||||
for (let i = 0; i < 60; i++) t.mock.timers.tick(1000);
|
||||
assert.equal(harness.state().status, "paused");
|
||||
assert.match(harness.state().pausedReason, /remained busy/);
|
||||
assert.equal(harness.sent.length, 1);
|
||||
} finally {
|
||||
await harness.emit("session_shutdown"); harness.restoreEnv(); rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("footer and both recall paths cover lifecycle without adding widget content", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-footer-"));
|
||||
const harness = await createHeadlessHarness(dir, "footer");
|
||||
const previousNoColor = process.env.NO_COLOR;
|
||||
delete process.env.NO_COLOR;
|
||||
try {
|
||||
const goal = harness.commands.get("goal");
|
||||
const report = harness.tools.get("goal_report");
|
||||
const text = "Full goal " + "acceptance criteria ".repeat(100) + " END-OF-GOAL";
|
||||
await harness.emit("session_start");
|
||||
assert.match(harness.statuses.at(-1)!, /\[muted\]Goal: None/);
|
||||
await goal.handler(text, harness.ctx);
|
||||
assert.match(harness.statuses.at(-1)!, /\[accent\]Goal: Active/);
|
||||
const sent = harness.sent.length;
|
||||
await goal.handler("", harness.ctx);
|
||||
const recall = harness.notifications.at(-1)!.message;
|
||||
assert.ok(recall.includes(text));
|
||||
await harness.shortcuts.get("alt+g").handler(harness.ctx);
|
||||
assert.equal(harness.notifications.at(-1)!.message, recall);
|
||||
assert.equal(harness.sent.length, sent, "recall never starts a turn");
|
||||
assert.equal(harness.shortcuts.has("tab"), false);
|
||||
await report.execute("wait", waitReport, undefined, undefined, harness.ctx);
|
||||
assert.match(harness.statuses.at(-1)!, /\[warning\]Goal: Waiting/);
|
||||
await goal.handler("stop", harness.ctx);
|
||||
assert.match(harness.statuses.at(-1)!, /\[warning\]Goal: Paused/);
|
||||
await goal.handler("resume", harness.ctx);
|
||||
await report.execute("blocked", { status: "blocked", evidence: "permission needed" }, undefined, undefined, harness.ctx);
|
||||
assert.match(harness.statuses.at(-1)!, /\[error\]Goal: Blocked/);
|
||||
await goal.handler("resume", harness.ctx);
|
||||
await report.execute("done", { status: "satisfied", evidence: "verified" }, undefined, undefined, harness.ctx);
|
||||
assert.match(harness.statuses.at(-1)!, /\[success\]Goal: Complete/);
|
||||
assert.equal(harness.state().lastOutcome.text, text);
|
||||
const finishedSent = harness.sent.length;
|
||||
await harness.emit("agent_settled");
|
||||
await harness.emit("session_start");
|
||||
await goal.handler("resume", harness.ctx);
|
||||
assert.equal(harness.sent.length, finishedSent);
|
||||
await goal.handler("", harness.ctx);
|
||||
assert.ok(harness.notifications.at(-1)!.message.includes(text));
|
||||
process.env.NO_COLOR = "1";
|
||||
await harness.emit("session_start");
|
||||
assert.equal(harness.statuses.at(-1), "Goal: Complete (/goal, Alt+G)");
|
||||
await goal.handler("clear", harness.ctx);
|
||||
assert.equal(harness.state().lastOutcome, undefined);
|
||||
assert.equal(harness.statuses.at(-1), "Goal: None (/goal, Alt+G)");
|
||||
assert.ok(harness.widgets.every(value => value === undefined));
|
||||
} finally {
|
||||
if (previousNoColor === undefined) delete process.env.NO_COLOR;
|
||||
else process.env.NO_COLOR = previousNoColor;
|
||||
await harness.emit("session_shutdown"); harness.restoreEnv(); rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("state-write failure cannot report goal completion or inject another check", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "goal-save-failure-"));
|
||||
const incarnation = "quiet-save-failure";
|
||||
const harness = await createHeadlessHarness(dir, incarnation);
|
||||
try {
|
||||
await harness.commands.get("goal").handler("--wait-timeout 60 output", harness.ctx);
|
||||
const statePath = fencedStateFilePath(join(dir, "state", "goal"), incarnation);
|
||||
rmSync(statePath);
|
||||
mkdirSync(statePath); // force atomic replacement failure without mocking away filesystem behavior
|
||||
await assert.rejects(() => harness.tools.get("goal_report").execute("done", { status: "satisfied", evidence: "done" }, undefined, undefined, harness.ctx), /could not be saved/);
|
||||
await harness.emit("agent_settled");
|
||||
assert.equal(harness.sent.length, 1);
|
||||
assert.equal(harness.notifications.some((n) => n.message.includes("satisfied and cleared")), false);
|
||||
} finally {
|
||||
await harness.emit("session_shutdown"); harness.restoreEnv(); rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user