feat(extensions): establish canonical goal source (#54, #55)

This commit is contained in:
2026-09-06 02:32:32 -05:00
parent 44f257cb06
commit d4696d09eb
43 changed files with 6845 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
# goal — pi extension
Operator-set goal with a forced check<->proceed loop. `/goal <text>` activates a goal; the
extension keeps the agent working and reporting until it declares the goal `satisfied` via the
`goal_report` tool, then retains a recallable completed outcome and returns the session to normal operation.
Requirements record: `docs/PRD.md` in mosaic-brain (issue #52). Design locked with Jason
2026-08-28 (Q1-Q8).
## Usage
```
/goal full current or completed goal, state, and counters
/goal <text> set (or replace, with a notify) the active goal
/goal --max N <text> set with a non-default run limit (default 25)
/goal stop pause the loop, goal retained
/goal resume continue a paused goal (kicks a check turn)
/goal clear remove the goal entirely
```
`GOAL_MAX_CHECKS` env overrides the default cap for new goals (invalid values fall back to 25).
## Loop mechanics
1. `before_agent_start` appends the active goal and reporting instructions to the system
prompt every turn, so the goal survives context growth and compaction.
2. When the agent settles without the goal being satisfied, the extension injects a check
prompt (`sendUserMessage`), forcing the next turn: the check<->proceed loop. Before reporting,
follow the continuation loop in `skills-local/ms-proactive-agent/SKILL.md`.
3. The agent reports via `goal_report`, the loop's only exit:
- `satisfied` + evidence: completed outcome retained for recall, normal operation
(`evidence`, `reason`, and `in_progress` structured progress use the `ms-executive-update`
format — `skills-local/ms-executive-update/SKILL.md` — so the operator reads one shape everywhere)
- `blocked` + reason: loop paused, reason surfaced to the operator
- `in_progress`: evidence plus a structured progress record naming the nearest gate,
owner, last live measurement, and next action
4. Successful non-`goal_report` tool results create one observable work event. Measurement,
action, and delegation reports require that event. Non-tool work requires a concrete artifact.
Waiting requires an approved watch id or concrete next-check condition.
5. Empty, malformed, unsupported, or duplicate `in_progress` reports do not reset the check
counter. They increment a persisted no-progress counter and pause after three reports by
default (`GOAL_MAX_NO_PROGRESS_REPORTS`). Repeated valid wait reports remain active without
claiming new progress or triggering a false-positive pause.
6. Esc (run abort) and run errors pause the loop instead of re-injecting; `/goal resume`
continues. Operator-typed messages always flow through unchanged.
7. No-report cap: N consecutive check turns without an accepted report auto-pause the loop and
notify. Default 25 (`GOAL_MAX_CHECKS`), per-goal `--max N`.
The loop needs a persistent session (TUI or RPC). In print mode (`pi -p`) the goal directive
and seat-durable state still apply, but check turns are not forced — print mode exits when the
prompt pipeline completes, and a forced turn there races session teardown (measured).
## State
Incarnation-durable state survives `/new`, `/resume`, and reloads within one process. This
repository's native development installation stores it under `.pi/state/goal/`. The identity is
the process-launch incarnation, never `PI_SESSION_ID`. Unknown-owner legacy `goal-state.json`
files are quarantined rather than inherited. One active goal exists per incarnation; setting a
new goal replaces that incarnation's goal.
State stores counters, bounded routing fields, and a SHA-256 evidence fingerprint. It does not
persist free-form progress evidence.
## Development installation
This directory is canonical source. Do not edit `.pi/extensions/`; generate that native-test
installation with `scripts/sync-dev-extensions.sh`. The sync copies `goal/` and its
`mosaic-core/lib/` dependency as ordinary files, verifies content hashes, and refuses to
overwrite installation drift. It also enforces that `goal/index.ts` is the only extension
entrypoint in this bounded package.
Start the isolated native test with `bash scripts/goal-dev.sh`. It runs the sync and passes the
generated entrypoint explicitly to Pi while disabling ambient extension discovery. This does
not install or reload the live fleet extension and does not add the extension to Docker.
Directory layout: `index.ts` for wiring, `lib/` for logic, and `test/` for hermetic checks.
Pi supplies the extension API, TypeBox, and pi-ai imports at runtime.
## Tests
```
node --test extensions/goal/test/*.test.ts
bash scripts/test-extension-package.sh
```
The suite includes pure state, persistence, incarnation fencing, exact report-only loop,
duplicate evidence, legitimate wait, headless extension-runtime, and enforcement-neutralization
red controls.
## Bounded waits (operator opt-in)
Use `/goal --wait-timeout 60 <goal text>` to suspend automatic checks during a
reported wait. The flag accepts 1086400 seconds and combines with `--max N`.
Omitting it preserves the previous wait-loop behavior. Existing running sessions
must `/reload` to load this patch; do not reload unrelated seats for a trial.
A valid `goal_report` with `status: "in_progress"`, `progress.kind: "wait"`, an
owner, and a watch id or concrete `nextCheck` persists the deadline and yields.
There are no model heartbeats during that wait. Repeated wait reports preserve
its original deadline. A substantive progress report clears the wait. The
extension neither evaluates arbitrary shell conditions nor sends messages to
other agents: a relevant incoming message can start an ordinary turn, or the
one-shot deadline asks the agent to reconcile the dependency once.
There is **one automatic deadline wake per goal or explicit `/goal resume`**.
Reporting another wait after that wake pauses the goal, including when its
wording changes. This intentionally limits the initial rollout; multiple
successive timed dependencies require explicit resumption. Deadline expiry
never supplies approval or permits the agent to invent missing input.
The timer belongs to this Pi process. Reload within the same incarnation
restores the original deadline; shutdown cancels timers. A new process has a
new fenced goal identity and does not inherit another incarnation's goal.
This patch is not a process supervisor. Print mode records a manual wait and
never schedules a wake. No external systemd timer is installed.
If the runtime is busy at expiry, readiness is checked in code every second
for at most 60 seconds; this also covers manual compaction with no settle event.
If still busy, the goal pauses with a UI notice. A dispatched deadline wake
must appear in `before_agent_start` within 30 seconds or the goal pauses without
resending. The matching event confirms turn start, not successful task execution.
Stop, clear, replacement, satisfaction, and blocking cancel owned timers.
Late reports cannot settle a paused goal. Reports also refuse if the goal changes
while an asynchronous policy check is running. State snapshots use atomic file
replacement; failed saves stop continuation instead of claiming a saved result.
The tests exercise unchanged waits, deadline dispatch and observation, unresolved
pause, compaction, reload, cancellation, print mode, malformed persisted waits,
and actual filesystem write failure. This is separate from live seat acceptance.
+504
View File
@@ -0,0 +1,504 @@
// pi /goal extension — operator-set goal with a forced check<->proceed loop.
//
// Source of truth for requirements: docs/PRD.md (mosaic-brain, issue #52).
// Design locked with Jason 2026-08-28 (Q1-Q8): verb set, agent-self-declared
// satisfaction via goal_report, abort pauses, seat-durable state, runaway cap.
import { randomUUID } from "node:crypto";
import { fileURLToPath } from "node:url";
import { goalDisplay, goalDetails } from "./lib/display.ts";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
import {
clearGoal,
completeGoal,
blockGoal,
pauseGoal,
recordCheckInjected,
recordInProgressReport,
recordReport,
recordWorkEvent,
resumeGoal,
setGoal,
validateReport,
type GoalState,
type ProgressDetails,
} from "./lib/state.ts";
import { decideSettle } from "./lib/settle.ts";
import { parseGoalCommand } from "./lib/parse.ts";
import { loadState, saveState, resolveStatePath } from "./lib/store.ts";
import { goalPolicyDenialCode, validateAttestedGoalReport } from "./lib/executive-update.ts";
import { incarnationIdentity } from "../mosaic-core/lib/incarnation.ts";
import { goalPolicyPublication, recordGoalPolicyDenial, registerGoalPolicyConsumer } from "../mosaic-core/lib/goal-policy.ts";
const WIDGET_ID = "goal";
export default function goalExtension(pi: ExtensionAPI) {
// Register before role-specific adapters and Mosaic Core publish the one
// trusted version-4 policy attestation for this process incarnation.
const incarnationId = incarnationIdentity();
registerGoalPolicyConsumer(incarnationId);
// NG-7 incarnation fencing (Mercer F1): state is scoped to THIS process
// launch incarnation — a sibling incarnation of the same seat (marcie-2)
// resolves its own fenced file and can never inherit, replace, pause, or
// settle this incarnation's active focus. Legacy unfenced state is
// quarantined without adoption (store.ts).
// NG development state belongs to this project copy, never the live fleet agent home.
const statePath = resolveStatePath(fileURLToPath(new URL("../../state/goal/", import.meta.url)));
let state: GoalState = loadState(statePath);
// Set on session_shutdown: after teardown, any ctx/runner use throws.
let dead = false;
let waitTimer: ReturnType<typeof setTimeout> | undefined;
let armedDeadline: string | undefined;
let deadlinePending = false;
function cancelWaitTimer(): void {
if (waitTimer !== undefined) clearTimeout(waitTimer);
waitTimer = undefined;
armedDeadline = undefined;
deadlinePending = false;
}
// A deadline is a single reconciliation wake, never an approval or a heartbeat.
function wakeDeadline(ctx: ExtensionContext): void {
if (dead || state.status !== "active" || !state.waitTimeoutSeconds ||
!state.activeWait || state.activeWait.wakeSent) return;
state = { ...state, waitWakeUsed: true,
activeWait: { ...state.activeWait, wakeSent: true, wakeRequestId: randomUUID(), wakeDispatchedAt: Date.now(), wakeObserved: false } };
if (!persist(ctx)) return;
beginCheck(ctx);
}
function syncWaitTimer(ctx: ExtensionContext): void {
const wait = state.activeWait;
if (dead || ctx.mode === "print" || state.status !== "active" || !state.waitTimeoutSeconds ||
!wait || !Number.isFinite(wait.deadlineAt) || (wait.wakeSent && wait.wakeObserved)) {
cancelWaitTimer();
return;
}
const deadline = wait.wakeSent ? wait.wakeDispatchedAt! + 30000 : wait.deadlineAt!;
const key = `${wait.wakeSent ? wait.wakeRequestId : "wait"}:${deadline}`;
if (armedDeadline === key) return;
cancelWaitTimer();
armedDeadline = key;
const tick = () => {
waitTimer = undefined;
if (dead || state.status !== "active" || armedDeadline !== key) return;
if (wait.wakeSent) {
pauseWith(ctx, "deadline wake was not observed within 30s", "wake delivery unconfirmed; paused — /goal resume after checking the session");
return;
}
if (!ctx.isIdle()) {
// Compaction can make a session busy without ever emitting agent_settled.
// Poll runtime readiness in code only, for at most 60s; never call the model.
if (Date.now() >= deadline + 60000) {
pauseWith(ctx, "session remained busy after wait deadline", "deadline could not start work; paused — /goal resume when ready");
return;
}
deadlinePending = true;
waitTimer = setTimeout(tick, 1000);
waitTimer.unref?.();
return;
}
wakeDeadline(ctx);
};
waitTimer = setTimeout(tick, Math.max(1, deadline - Date.now()));
waitTimer.unref?.();
}
// ---------- helpers ----------
function safeNotify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error"): void {
try {
ctx.ui.notify(message, level);
} catch {
// stale ctx or no UI — never break a turn over a notification
}
}
function persist(ctx: ExtensionContext): boolean {
try {
saveState(state, statePath);
syncWaitTimer(ctx);
return true;
} catch (err) {
state = pauseGoal(state, "state save failed; continuation stopped");
cancelWaitTimer();
try {
safeNotify(ctx, `goal: state save failed: ${err instanceof Error ? err.message : String(err)}`, "error");
} catch {
// no UI — nothing else to do
}
return false;
}
}
function renderWidget(ctx: ExtensionContext): void {
if (!ctx.hasUI) return;
try {
// Clear a stale widget after upgrading/reloading; never register widget content.
ctx.ui.setWidget(WIDGET_ID, undefined);
const display = goalDisplay(state);
const label = `Goal: ${display.label} (/goal, Alt+G)`;
ctx.ui.setStatus("goal", process.env.NO_COLOR !== undefined
? label : ctx.ui.theme.fg(display.color, label));
} catch {
// Rendering is best-effort; never break the session over it.
}
}
function notifyStatus(ctx: ExtensionContext): void {
safeNotify(ctx, goalDetails(state), "info");
}
function checkPrompt(): string {
const deadline = state.waitTimeoutSeconds && state.activeWait?.wakeSent
? " Wait deadline reached. Reconcile the dependency once. A deadline is not approval. If still unresolved and no independent work remains, report blocked; do not re-arm or poll."
: "";
const next = state.lastNextAction ? ` Execute the recorded next action first: ${state.lastNextAction}.` : "";
return (
`[goal check ${state.checks}/${state.maxChecks}; no-progress ${state.noProgressReports}/${state.maxNoProgressReports}]` +
`${state.activeWait?.wakeRequestId ? `[goal wake ${state.activeWait.wakeRequestId}] ` : ""}${deadline}${next} 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. ` +
`Report with goal_report exactly once. Use "satisfied" with evidence when fully met or "blocked" with the reason when no meaningful next step exists. For "in_progress", do not submit ` +
`status alone: provide structured progress in the ms-executive-update format (Just Completed / Next Step / Blocked) ` +
`with evidence plus progress.kind, nearest gate, owner, last live measurement, and next action. ` +
`Measurement/action/delegation requires a successful work tool event; non-tool work ` +
`requires an artifact; waiting requires an approved watch id or concrete next-check condition. ` +
`Empty, malformed, or duplicate reports increment the no-progress pause counter.`
);
}
/** Inject one check prompt: immediately when idle, queued behind a running agent otherwise. */
function beginCheck(ctx: ExtensionContext): void {
if (dead || state.status !== "active") return;
// The loop needs a persistent session. Print mode exits after the prompt
// pipeline completes; a forced turn there races teardown (measured). The
// goal directive (before_agent_start) still applies in print mode.
if (ctx.mode === "print") return;
state = recordCheckInjected(state);
if (!persist(ctx)) return;
renderWidget(ctx);
const msg = checkPrompt();
if (ctx.isIdle()) {
try {
pi.sendUserMessage(msg);
return;
} catch {
// fall through to the queued form
}
}
try {
pi.sendUserMessage(msg, { deliverAs: "followUp" });
} catch (err) {
pauseWith(ctx, "check injection failed", "check injection failed — /goal resume after resolving delivery");
safeNotify(ctx, `goal: check injection failed: ${err instanceof Error ? err.message : String(err)}`, "error");
}
}
/** Stop reason of the most recent assistant message, or undefined when none. */
function lastAssistantStopReason(ctx: ExtensionContext): string | undefined {
try {
const branch = ctx.sessionManager.getBranch();
for (let i = branch.length - 1; i >= 0; i--) {
const entry = branch[i];
if (entry?.type === "message") {
const msg = entry.message;
if (msg?.role === "assistant") return msg.stopReason;
}
}
} catch {
// session introspection is best-effort
}
return undefined;
}
function pauseWith(ctx: ExtensionContext, reason: string, notice: string): void {
state = pauseGoal(state, reason);
persist(ctx);
renderWidget(ctx);
safeNotify(ctx, `goal: ${notice}`, "warning");
}
// ---------- /goal command ----------
pi.registerCommand("goal", {
description: "Set or manage the session goal: <text> | stop | clear | resume | --max N | --wait-timeout S",
handler: async (args, ctx) => {
const cmd = parseGoalCommand(args ?? "");
switch (cmd.kind) {
case "error":
safeNotify(ctx, `goal: ${cmd.message}`, "error");
return;
case "status":
notifyStatus(ctx);
return;
case "stop":
if (state.status !== "active") {
safeNotify(ctx, "goal: nothing active to stop", "warning");
return;
}
pauseWith(ctx, "stopped by operator", "paused — /goal resume to continue");
return;
case "clear":
if (state.status === "none" && !state.lastOutcome) {
safeNotify(ctx, "goal: no goal set", "warning");
return;
}
state = clearGoal(state);
if (!persist(ctx)) return;
renderWidget(ctx);
safeNotify(ctx, "goal: cleared — normal operation", "info");
return;
case "resume":
if (state.status !== "paused" && state.status !== "blocked") {
safeNotify(ctx, "goal: nothing paused to resume", "warning");
return;
}
state = resumeGoal(state);
if (!persist(ctx)) return;
renderWidget(ctx);
safeNotify(ctx, "goal: resumed", "info");
beginCheck(ctx);
return;
case "set": {
const replacing = state.status !== "none";
state = setGoal(state, cmd.text, cmd.max, cmd.waitTimeoutSeconds);
if (!persist(ctx)) return;
renderWidget(ctx);
safeNotify(
ctx,
`${replacing ? "goal: replaced" : "goal: set"}${truncate(cmd.text, 60)} (cap ${state.maxChecks})`,
"info",
);
beginCheck(ctx);
return;
}
}
},
});
pi.registerShortcut("alt+g", {
description: "Show the full NG goal text and status",
handler: async (ctx) => { notifyStatus(ctx); },
});
// ---------- goal_report tool (the loop's only exit) ----------
pi.registerTool({
name: "goal_report",
label: "Goal Report",
description:
"Report progress on the active operator goal. Required at every goal check-in. " +
"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. " +
'Use "satisfied" with evidence, "blocked" with the reason, or "in_progress" with ' +
"mechanically backed structured progress in the ms-executive-update format (Just Completed / Next Step / Blocked) or an explicit waiting state. Status-only and duplicate " +
"in_progress reports count toward an automatic no-progress pause.",
promptSnippet: "Report goal status with evidence and mechanically backed progress",
parameters: Type.Object({
status: StringEnum(["satisfied", "blocked", "in_progress"] as const),
evidence: Type.Optional(
Type.String({
description: "Completion evidence, blocker reason, or concise evidence for this work cycle",
}),
),
progress: Type.Optional(
Type.Object({
kind: StringEnum(["measurement", "action", "delegation", "wait", "non_tool"] as const),
gate: Type.String({ description: "Nearest incomplete gate" }),
owner: Type.String({ description: "Current gate or external-wait owner" }),
lastMeasurement: Type.String({ description: "Most recent live measurement, including what and when" }),
nextAction: Type.String({ description: "Next concrete action or check" }),
artifact: Type.Optional(Type.String({ description: "Required artifact locator for non_tool work" })),
watchId: Type.Optional(Type.String({ description: "Approved active watch identifier" })),
nextCheck: Type.Optional(Type.String({ description: "Concrete condition that ends or rechecks a wait" })),
}),
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
if (state.status === "none") {
return {
content: [{ type: "text", text: "No goal is active. Nothing to report." }],
details: { goalStatus: "none" },
};
}
if (state.status !== "active") {
throw new Error("Goal is paused; only /goal resume or a new operator goal permits reports");
}
const reportingState = state;
// A v4 policy parses and resolves the untrimmed payload before any
// existing validation, state transition, persistence, or UI operation.
const policy = goalPolicyPublication(incarnationId);
if (policy) {
const attested = await validateAttestedGoalReport(params.evidence, policy, params.status);
if (!attested.ok) {
const denial = goalPolicyDenialCode(attested.reason);
recordGoalPolicyDenial(incarnationId, denial);
throw new Error(`goal_report rejected by attested policy: ${denial}`);
}
}
if (state !== reportingState || state.status !== "active") {
throw new Error("Goal changed while validating report; report was not applied");
}
const evidence = (params.evidence ?? "").trim();
const reportError = validateReport(params.status, evidence);
if (reportError) {
throw new Error(reportError);
}
if (params.status === "satisfied") {
state = completeGoal(state, evidence);
if (!persist(ctx)) {
state = pauseGoal(reportingState, "state save failed; completion not recorded");
throw new Error("Goal completion could not be saved; continuation stopped");
}
renderWidget(ctx);
safeNotify(ctx,
`goal: satisfied and cleared${evidence ? `${truncate(evidence, 80)}` : ""}`,
"info",
);
return {
content: [
{
type: "text",
text: "Goal recorded as SATISFIED. The goal is cleared; normal operation resumes. Do not call goal_report again unless the operator sets a new goal.",
},
],
details: { goalStatus: "none" },
};
}
if (params.status === "blocked") {
const reason = evidence || "no reason given";
state = blockGoal(recordReport(state), truncate(reason, 120));
if (!persist(ctx)) throw new Error("Goal blocker could not be saved; continuation stopped");
renderWidget(ctx);
safeNotify(ctx, `goal: BLOCKED — ${truncate(reason, 100)} — /goal resume after resolving`, "warning");
return {
content: [
{
type: "text",
text: "Goal recorded as BLOCKED. The loop is paused and the operator has been notified. Do not call goal_report again until the operator resumes or sets a new goal.",
},
],
details: { goalStatus: "blocked" },
};
}
const outcome = recordInProgressReport(state, {
evidence,
progress: params.progress as ProgressDetails | undefined,
});
state = outcome.state;
if (!persist(ctx)) throw new Error("Goal state could not be saved; continuation stopped");
renderWidget(ctx);
if (state.status === "paused") {
safeNotify(ctx, `${state.pausedReason} — /goal resume after resolving`, "warning");
return {
content: [
{
type: "text",
text: `Goal paused: ${state.pausedReason} (${outcome.reason}). The operator has been notified. Do not continue this goal until the operator resumes it.`,
},
],
details: { goalStatus: "paused", classification: outcome.classification },
};
}
const text =
outcome.classification === "progress"
? `Recorded substantive progress (${outcome.reason}). Continue with the recorded next action.`
: outcome.classification === "waiting"
? (state.waitTimeoutSeconds && ctx.mode !== "print"
? `Waiting recorded. Automatic goal checks are suspended until a relevant incoming message or the deadline at ${new Date(state.activeWait!.deadlineAt!).toISOString()}. The extension owns this single deadline wake; do not add timers or report unchanged status. Checkpoint and yield now.`
: (ctx.mode === "print" ? "Waiting recorded. Print mode cannot schedule a wake; resume this goal in a persistent session." : `Recorded explicit waiting state (${outcome.reason}). Keep the approved watch or next-check condition active.`))
: `No new progress recorded (${outcome.reason}). No-progress count is ${state.noProgressReports}/${state.maxNoProgressReports}; take the next safe action before reporting again.`;
return {
content: [{ type: "text", text }],
details: { goalStatus: "active", classification: outcome.classification },
};
},
});
// ---------- loop wiring ----------
pi.on("before_agent_start", async (event, ctx) => {
if (state.status !== "active") {
if (event.prompt.startsWith("[goal check ")) {
return { systemPrompt: `${event.systemPrompt}\nThis queued goal check is obsolete: the goal is paused or cleared. Do not execute it or call goal_report.` };
}
return;
}
if (state.activeWait?.wakeRequestId && event.prompt.includes(`[goal wake ${state.activeWait.wakeRequestId}]`)) {
state = { ...state, activeWait: { ...state.activeWait, wakeObserved: true } };
if (!persist(ctx)) return { systemPrompt: `${event.systemPrompt}\nGoal state save failed. Stop this goal and report the recording failure.` };
}
const waitingDirective = state.waitTimeoutSeconds && ctx.mode !== "print"
? " Bounded waits enabled: report in_progress with progress.kind=wait and a concrete nextCheck when no ready work remains. The extension suspends checks and supplies one deadline wake per goal/resume. Do not create a duplicate timer, sleep loop, or heartbeat. On unrelated incoming messages, keep the wait; on a relevant event verify the dependency. After the deadline wake, an unresolved wait pauses."
: "";
const directive =
`[goal] ACTIVE OPERATOR GOAL: ${state.text}\n` +
`Work toward this goal. 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. ` +
`At every check-in you MUST call goal_report exactly once. Use ` +
`"satisfied" with evidence when fully met or "blocked" with the reason when no meaningful next step ` +
`exists. An "in_progress" report MUST follow a concrete action and use structured progress in the ms-executive-update format (Just Completed / Next Step / Blocked), or ` +
`declare an explicit approved wait/non-tool artifact. Status-only and duplicate reports are no progress. ` +
`Do not end a turn without reporting.${waitingDirective}`;
return { systemPrompt: `${event.systemPrompt}\n\n${directive}` };
});
pi.on("tool_result", async (event, ctx) => {
if (state.status !== "active" || event.isError || event.toolName === "goal_report") return;
state = recordWorkEvent(state, event.toolName);
persist(ctx);
renderWidget(ctx);
});
pi.on("agent_settled", async (_event, ctx) => {
if (state.status !== "active") return;
// Abort/error pause (Q3i) and runaway cap (Q4) live in lib/settle.ts (pure, tested).
const decision = decideSettle(state, lastAssistantStopReason(ctx));
if (decision.action === "pause") {
pauseWith(ctx, decision.reason, `${decision.reason} — /goal resume to continue`);
return;
}
if (decision.action === "wait") {
if (deadlinePending) wakeDeadline(ctx);
else syncWaitTimer(ctx);
return;
}
beginCheck(ctx);
});
// Teardown guard: after session replacement/reload/quit, stop touching ctx.
pi.on("session_shutdown", async () => {
dead = true;
cancelWaitTimer();
});
// Seat-durable state (Q7b): reload from disk on every session start/reload/resume/new
// so a fresh session in the same seat picks up the active goal.
pi.on("session_start", async (_event, ctx) => {
dead = false;
state = loadState(statePath);
syncWaitTimer(ctx);
renderWidget(ctx);
if (state.status === "active") {
safeNotify(ctx, `goal: active — ${truncate(state.text, 60)}`, "info");
// Durability (Q7b): a fresh session with an active goal re-engages the
// loop immediately — no prior turn exists to emit agent_settled.
if (!(state.waitTimeoutSeconds && state.activeWait && !state.activeWait.wakeSent)) beginCheck(ctx);
} else if (state.status === "paused" || state.status === "blocked") {
safeNotify(ctx, "goal: paused — /goal resume to continue", "info");
}
});
}
function truncate(s: string, n: number): string {
return s.length <= n ? s : s.slice(0, n - 1) + "…";
}
+44
View File
@@ -0,0 +1,44 @@
import type { GoalState } from "./state.ts";
export type GoalDisplay = {
label: "Active" | "Waiting" | "Paused" | "Blocked" | "Complete" | "None";
color: "accent" | "warning" | "error" | "success" | "muted";
};
export function goalDisplay(state: GoalState): GoalDisplay {
if (state.status === "none") return state.lastOutcome
? { label: "Complete", color: "success" }
: { label: "None", color: "muted" };
if (state.status === "blocked" ||
(state.status === "paused" && state.pausedReason?.startsWith("blocked:"))) {
return { label: "Blocked", color: "error" };
}
if (state.status === "paused") return { label: "Paused", color: "warning" };
if (state.activeWait && !state.activeWait.wakeSent) return { label: "Waiting", color: "warning" };
return { label: "Active", color: "accent" };
}
/** Full recall is deliberately separate from short transient notifications. */
export function goalDetails(state: GoalState): string {
const display = goalDisplay(state);
if (display.label === "None") return "Goal: None\nUse /goal <text> to set a goal.";
const completed = state.status === "none" ? state.lastOutcome : undefined;
const lines = [`Goal: ${display.label}`, "", completed?.text ?? state.text, ""];
if (completed) {
lines.push(`Completed: ${completed.at}`, `Evidence: ${completed.evidence}`);
} else {
if (state.pausedReason) lines.push(`Reason: ${state.pausedReason}`);
if (state.status === "active" && state.activeWait) {
lines.push(`Wait owner: ${state.activeWait.owner}`);
if (state.activeWait.nextCheck) lines.push(`Next check: ${state.activeWait.nextCheck}`);
if (state.activeWait.watchId) lines.push(`Watch: ${state.activeWait.watchId}`);
if (state.activeWait.deadlineAt !== undefined) {
const deadline = new Date(state.activeWait.deadlineAt);
lines.push(`Deadline: ${Number.isFinite(deadline.getTime()) ? deadline.toISOString() : "invalid"}`);
}
}
lines.push(`Checks: ${state.checks}/${state.maxChecks}`, `No-progress reports: ${state.noProgressReports}/${state.maxNoProgressReports}`);
}
lines.push("", "/goal stop | /goal resume | /goal clear");
return lines.join("\n");
}
+185
View File
@@ -0,0 +1,185 @@
// lib/executive-update.ts — parser for the cited ms-executive-update Machine contract.
//
// The contract bytes are pinned and verified by Mosaic Core before this parser
// is reachable for a version-4 role. This module accepts no alternate format,
// heading, whitespace form, or identifier spelling.
import {
GOAL_REPORT_CONTRACT_BLOB,
GOAL_REPORT_CONTRACT_PATH,
GOAL_REPORT_CONTRACT_SECTION,
GOAL_REPORT_CONTRACT_SHA256,
GOAL_REPORT_FORMAT,
type GoalItemResolutionV1,
type GoalPolicyPublication,
type GoalTrackedItem,
validateGoalItemResolution,
} from "../../mosaic-core/lib/goal-policy.ts";
export {
GOAL_REPORT_FORMAT,
GOAL_REPORT_CONTRACT_PATH,
GOAL_REPORT_CONTRACT_SECTION,
GOAL_REPORT_CONTRACT_BLOB,
GOAL_REPORT_CONTRACT_SHA256,
};
export type ExecutiveUpdateSection = GoalTrackedItem["section"];
export type ExecutiveUpdateParseResult =
| Readonly<{ ok: true; kind: "update" | "no-change"; items: readonly GoalTrackedItem[] }>
| Readonly<{ ok: false; reason: string }>;
const SECTIONS: readonly ExecutiveUpdateSection[] = ["Just Completed", "Next Step", "Blocked"];
const CONTROL_RE = /[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/;
const TASK_RE = /^T[0-9]+$/;
const PR_RE = /^#[0-9]+$/;
const ROW_RE = /^[A-Z]{1,4}\.?[0-9]+(?:\.[0-9]+)*$/;
const MARKER_RE = /^[A-Z0-9]+(?:-[A-Z0-9]+)*-Q[0-9]+$/;
const SHA_RE = /^[0-9a-f]{40}$/;
const PATH_RE = /^`[^`\s]+`$/;
function invalid(reason: string): ExecutiveUpdateParseResult {
return Object.freeze({ ok: false, reason });
}
function valid(items: GoalTrackedItem[], kind: "update" | "no-change" = "update"): ExecutiveUpdateParseResult {
return Object.freeze({ ok: true, kind, items: Object.freeze(items) });
}
function isContractItemToken(token: string): boolean {
if (TASK_RE.test(token) || PR_RE.test(token) || ROW_RE.test(token) || MARKER_RE.test(token) || SHA_RE.test(token)) return true;
if (!PATH_RE.test(token)) return false;
const path = token.slice(1, -1);
return !path.startsWith("/") && !path.startsWith("./") && !path.startsWith("../") && !/(?:^|\/)\.\.(?:\/|$)/.test(path);
}
function hasValidBlockedSuffix(text: string): boolean {
return text.endsWith("\u2014 nothing from you")
|| /\u2014 Decision needed: \(1\) .+ \(2\) .+; recommend [1-9][0-9]*$/.test(text);
}
/** The contract's distinct single-line update form. */
function parseNoChange(raw: string): ExecutiveUpdateParseResult | undefined {
const match = /^No change since ([^;\n]+); still waiting on (.+)\.$/.exec(raw);
if (!match) return undefined;
const [, since, waiting] = match;
if (!isContractItemToken(since) || !isContractItemToken(waiting)) return invalid("No change: invalid item identifier");
// The trusted resolver's public input is deliberately the existing closed
// GoalTrackedItem shape. Both references are waiting-state identifiers; the
// no-change kind below supplies the stricter unchanged-state rule.
return valid([
Object.freeze({ token: since, section: "Next Step" }),
Object.freeze({ token: waiting, section: "Next Step" }),
], "no-change");
}
function parseSection(section: ExecutiveUpdateSection, body: string): ExecutiveUpdateParseResult | GoalTrackedItem[] {
if (body === "* none\n") return [];
if (!body.endsWith("\n")) return invalid(`${section}: final bullet must end with LF`);
const lines = body.slice(0, -1).split("\n");
if (lines.length < 1 || lines.length > 5) return invalid(`${section}: requires one to five bullets or * none`);
const items: GoalTrackedItem[] = [];
for (const line of lines) {
if (line === "" || /[ \t]$/.test(line)) return invalid(`${section}: blank or trailing-whitespace line`);
const match = /^\* ([^:\n]+): ([^\n]+)$/.exec(line);
if (!match) return invalid(`${section}: invalid bullet grammar`);
const [, token, text] = match;
if (!isContractItemToken(token)) return invalid(`${section}: invalid item identifier`);
if (text.startsWith("-")) return invalid(`${section}: bullet text may not start with -`);
if (section === "Blocked" && !hasValidBlockedSuffix(text)) return invalid("Blocked: required terminal disposition missing");
items.push(Object.freeze({ token, section }));
}
return items;
}
/** Parse the exact three-section Machine contract before any Goal state mutation. */
export function parseExecutiveUpdate(raw: unknown): ExecutiveUpdateParseResult {
if (typeof raw !== "string") return invalid("payload must be a string");
if (raw.length === 0) return invalid("payload is empty");
if (raw !== raw.normalize("NFC")) return invalid("payload is not NFC-normalized");
if (CONTROL_RE.test(raw)) return invalid("payload contains control characters or non-LF line endings");
if (/(^|\n)[^\n]*[ \t](?=\n|$)/.test(raw)) return invalid("payload contains trailing whitespace");
const noChange = parseNoChange(raw);
if (noChange) return noChange;
const first = "Just Completed:\n\n";
const second = "\nNext Step:\n\n";
const third = "\nBlocked:\n\n";
if (!raw.startsWith(first)) return invalid("first heading must be Just Completed");
const nextIndex = raw.indexOf(second, first.length);
if (nextIndex < 0 || raw.indexOf(second, nextIndex + 1) !== -1) return invalid("Next Step heading must appear exactly once after Just Completed");
const blockedIndex = raw.indexOf(third, nextIndex + second.length);
if (blockedIndex < 0 || raw.indexOf(third, blockedIndex + 1) !== -1) return invalid("Blocked heading must appear exactly once after Next Step");
if (raw.includes("\nJust Completed:\n", first.length) || raw.includes("\nNext Step:\n", 0) && raw.indexOf(second) !== nextIndex) return invalid("heading duplication or order violation");
const bodies = [
raw.slice(first.length, nextIndex),
raw.slice(nextIndex + second.length, blockedIndex),
raw.slice(blockedIndex + third.length),
];
const parsed: GoalTrackedItem[] = [];
for (let index = 0; index < SECTIONS.length; index++) {
const sectionItems = parseSection(SECTIONS[index], bodies[index]);
if (!Array.isArray(sectionItems)) return sectionItems;
parsed.push(...sectionItems);
}
return valid(parsed);
}
export type AttestedGoalReportResult =
| Readonly<{ ok: true; items: readonly GoalTrackedItem[] }>
| Readonly<{ ok: false; reason: string }>;
function resolutionError(item: GoalTrackedItem, result: GoalItemResolutionV1): AttestedGoalReportResult | undefined {
if (result.outcome !== "resolved") return Object.freeze({ ok: false, reason: `resolver-${result.outcome}` });
if (item.section === "Just Completed" && !result.changedSincePreviousAcceptedReport) {
return Object.freeze({ ok: false, reason: "unchanged-completion" });
}
if (item.section === "Just Completed" && result.completionEvidenceId === null) {
return Object.freeze({ ok: false, reason: "completion-evidence-missing" });
}
return undefined;
}
export type AttestedGoalReportStatus = "satisfied" | "blocked" | "in_progress";
/** Resolve each parser-produced item through the attested, read-only resolver. */
export async function validateAttestedGoalReport(
raw: unknown,
publication: GoalPolicyPublication,
status: AttestedGoalReportStatus,
): Promise<AttestedGoalReportResult> {
const parsed = parseExecutiveUpdate(raw);
if (!parsed.ok) return parsed;
if (parsed.kind === "no-change" && status !== "in_progress") {
return Object.freeze({ ok: false, reason: "no-change-status" });
}
for (const item of parsed.items) {
let resolution: unknown;
try {
resolution = await publication.resolver(item);
} catch {
return Object.freeze({ ok: false, reason: "resolver-unavailable" });
}
if (!validateGoalItemResolution(resolution)) {
return Object.freeze({ ok: false, reason: "resolver-malformed" });
}
if (parsed.kind === "no-change") {
if (resolution.outcome !== "resolved") return Object.freeze({ ok: false, reason: `resolver-${resolution.outcome}` });
if (resolution.changedSincePreviousAcceptedReport) return Object.freeze({ ok: false, reason: "no-change-state-changed" });
continue;
}
const error = resolutionError(item, resolution);
if (error) return error;
}
return Object.freeze({ ok: true, items: parsed.items });
}
/** Stable code for Core's payload-free denial journal. */
export function goalPolicyDenialCode(reason: string): string {
return ["resolver-zero", "resolver-multiple", "resolver-unavailable", "resolver-stale", "resolver-malformed", "unchanged-completion", "completion-evidence-missing", "no-change-status", "no-change-state-changed"].includes(reason)
? reason
: "parser-rejected";
}
+78
View File
@@ -0,0 +1,78 @@
// Pure /goal command parsing. No I/O, no pi imports.
//
// Verbs (Q1, locked with Jason 2026-08-28):
// bare -> status
// stop -> pause the loop, goal retained
// clear -> remove the goal entirely
// resume -> continue a paused goal
// <text> -> set (or replace) the active goal
// --max N / --max=N run-limit option on set (Q4); remainder is the goal text
//
// Only the exact words stop/clear/resume are verbs; anything else is goal text.
export type GoalCommand =
| { kind: "status" }
| { kind: "stop" }
| { kind: "clear" }
| { kind: "resume" }
| { kind: "set"; text: string; max?: number; waitTimeoutSeconds?: number }
| { kind: "error"; message: string };
export function parseGoalCommand(raw: string): GoalCommand {
const input = (raw ?? "").trim();
if (input === "") return { kind: "status" };
const lower = input.toLowerCase();
if (lower === "stop") return { kind: "stop" };
if (lower === "clear") return { kind: "clear" };
if (lower === "resume") return { kind: "resume" };
let max: number | undefined;
let text = input;
let waitTimeoutSeconds: number | undefined;
const waitOption = text.match(/(?:^|\s)--wait-timeout(?:=|\s+)([^\s]+)(?=\s|$)/);
if (waitOption) {
const n = Number(waitOption[1]);
if (!Number.isInteger(n) || n < 10 || n > 86400 || String(n) !== waitOption[1]) {
return { kind: "error", message: "--wait-timeout must be an integer from 10 to 86400 seconds" };
}
waitTimeoutSeconds = n;
text = text.replace(waitOption[0], " ").trim();
}
if (/(?:^|\s)--wait-timeout(?:=|\s|$)/.test(text)) {
return { kind: "error", message: "--wait-timeout requires one value from 10 to 86400 seconds" };
}
const inline = text.match(/(?:^|\s)--max=([^\s]+)(?:\s|$)/);
if (inline) {
const parsed = parseMax(inline[1]);
if (typeof parsed === "string") return { kind: "error", message: parsed };
max = parsed;
text = text.replace(inline[0], " ").trim();
} else {
const spaced = text.match(/(?:^|\s)--max\s+([^\s]+)(?:\s|$)/);
if (spaced) {
const parsed = parseMax(spaced[1]);
if (typeof parsed === "string") return { kind: "error", message: parsed };
max = parsed;
text = text.replace(spaced[0], " ").trim();
} else if (/(?:^|\s)--max(?:\s|$)/.test(text)) {
return { kind: "error", message: '--max requires a positive integer (e.g. "--max 40")' };
}
}
text = text.trim();
if (text === "") {
return { kind: "error", message: "goal text required (verbs: stop, clear, resume)" };
}
return { kind: "set", text, max, ...(waitTimeoutSeconds === undefined ? {} : { waitTimeoutSeconds }) };
}
function parseMax(raw: string): number | string {
const n = Number.parseInt(raw, 10);
if (!Number.isInteger(n) || n < 1 || String(n) !== raw) {
return `--max must be a positive integer, got "${raw}"`;
}
return n;
}
+30
View File
@@ -0,0 +1,30 @@
// Pure settle decision for the goal loop — extracted so the abort/error/cap
// paths (AC4, AC5) are hermetically testable without a live agent run.
import { checkLimitReached, type GoalState } from "./state.ts";
export type SettleDecision = { action: "inject" } | { action: "wait" } | { action: "pause"; reason: string };
/**
* Decide what the loop does when the agent settles while a goal is active.
* stopReason comes from the last assistant message ("stop", "toolUse",
* "aborted", "error", ...).
*/
export function decideSettle(state: GoalState, stopReason: string | undefined): SettleDecision {
if (stopReason === "aborted") {
return { action: "pause", reason: "paused: run aborted (Esc)" };
}
if (stopReason === "error") {
return { action: "pause", reason: "paused: run error" };
}
if (state.waitTimeoutSeconds && state.activeWait && !state.activeWait.wakeSent) {
return { action: "wait" };
}
if (checkLimitReached(state)) {
return {
action: "pause",
reason: `paused: cap of ${state.maxChecks} checks reached without a report`,
};
}
return { action: "inject" };
}
+348
View File
@@ -0,0 +1,348 @@
// Pure goal state machine for the pi /goal extension.
// No I/O, no pi imports — hermetically testable.
import { createHash } from "node:crypto";
export type GoalStatus = "active" | "paused" | "blocked" | "none";
export type GoalTerminalStatus = "complete";
export type ProgressKind = "measurement" | "action" | "delegation" | "wait" | "non_tool";
export interface GoalTerminalOutcome {
text: string;
status: GoalTerminalStatus;
evidence: string;
at: string;
}
export interface ProgressDetails {
kind: ProgressKind;
/** Nearest incomplete acceptance or delivery gate. */
gate: string;
/** Current owner of that gate or external wait. */
owner: string;
/** Most recent live measurement, including what and when. */
lastMeasurement: string;
/** Next concrete action or check. */
nextAction: string;
/** Concrete artifact produced by legitimate non-tool work. */
artifact?: string;
/** Approved agent-watch identifier for a waiting cycle. */
watchId?: string;
/** Concrete condition that ends or rechecks a waiting cycle. */
nextCheck?: string;
}
export interface InProgressReport {
evidence: string;
progress?: ProgressDetails;
}
export type InProgressClassification = "progress" | "waiting" | "no_progress";
export interface InProgressOutcome {
state: GoalState;
classification: InProgressClassification;
reason: string;
}
export interface GoalState {
version: 1;
text: string;
status: GoalStatus;
/** Check prompts injected since the last accepted progress or waiting report. */
checks: number;
/** Cap on consecutive checks without an accepted report (Q4: default 25, per-goal --max). */
maxChecks: number;
/** Consecutive empty, malformed, unsupported, or duplicate in_progress reports. */
noProgressReports: number;
/** Bounded report-only loop threshold. */
maxNoProgressReports: number;
/** A successful non-goal_report tool result is available to back one progress report. */
workEventSinceReport: boolean;
lastActivityTool?: string;
lastActivityAt?: string;
/** Digest only: do not persist free-form evidence in the state file. */
lastProgressFingerprint?: string;
/** Bounded next action shown in the next forced-check prompt. */
lastNextAction?: string;
/** Explicit wait state. Repeated reports keep it active but do not claim new progress. */
activeWait?: {
owner: string;
watchId?: string;
nextCheck?: string;
deadlineAt?: number;
/** Dispatch receipt; wakeObserved confirms before_agent_start, not task completion. */
wakeSent?: boolean;
wakeRequestId?: string;
wakeDispatchedAt?: number;
wakeObserved?: boolean;
};
/** Operator opt-in: suspend waits, with one deadline wake per goal/resume. */
waitTimeoutSeconds?: number;
waitWakeUsed?: boolean;
setAt: string;
/** Why the goal is paused or blocked: operator stop, abort, error, cap, no-progress, or blocker reason. */
pausedReason?: string;
/** Most recent completed goal, retained for status recall without keeping its loop active. */
lastOutcome?: GoalTerminalOutcome;
}
export const DEFAULT_MAX_CHECKS = 25;
export const DEFAULT_MAX_NO_PROGRESS_REPORTS = 3;
/** Default cap for new goals; GOAL_MAX_CHECKS env overrides (invalid values fall back). */
export function envDefaultMaxChecks(env: NodeJS.ProcessEnv = process.env): number {
return positiveInteger(env.GOAL_MAX_CHECKS, DEFAULT_MAX_CHECKS);
}
/** Report-only loop cap; intentionally separate from the no-report check cap. */
export function envDefaultMaxNoProgressReports(env: NodeJS.ProcessEnv = process.env): number {
return positiveInteger(env.GOAL_MAX_NO_PROGRESS_REPORTS, DEFAULT_MAX_NO_PROGRESS_REPORTS);
}
function positiveInteger(raw: string | undefined, fallback: number): number {
if (!raw) return fallback;
const n = Number.parseInt(raw, 10);
return Number.isInteger(n) && n >= 1 ? n : fallback;
}
export function initialState(
maxChecks: number = envDefaultMaxChecks(),
maxNoProgressReports: number = envDefaultMaxNoProgressReports(),
): GoalState {
return {
version: 1,
text: "",
status: "none",
checks: 0,
maxChecks,
noProgressReports: 0,
maxNoProgressReports,
workEventSinceReport: false,
setAt: "",
};
}
export function setGoal(state: GoalState, text: string, maxChecks?: number, waitTimeoutSeconds?: number): GoalState {
return {
...initialState(maxChecks ?? envDefaultMaxChecks(), envDefaultMaxNoProgressReports()),
text,
...(waitTimeoutSeconds === undefined ? {} : { waitTimeoutSeconds, waitWakeUsed: false }),
status: "active",
setAt: new Date().toISOString(),
};
}
export function pauseGoal(state: GoalState, reason: string): GoalState {
if (state.status !== "active") return state;
return { ...state, status: "paused", pausedReason: reason };
}
export function blockGoal(state: GoalState, reason: string): GoalState {
if (state.status !== "active") return state;
return { ...state, status: "blocked", pausedReason: reason };
}
export function resumeGoal(state: GoalState): GoalState {
if (state.status !== "paused" && state.status !== "blocked") return state;
return {
...state,
status: "active",
checks: 0,
noProgressReports: 0,
workEventSinceReport: false,
pausedReason: undefined,
activeWait: state.waitTimeoutSeconds ? undefined : state.activeWait,
waitWakeUsed: state.waitTimeoutSeconds ? false : state.waitWakeUsed,
};
}
export function clearGoal(state: GoalState): GoalState {
return { ...initialState(state.maxChecks, state.maxNoProgressReports) };
}
export function completeGoal(state: GoalState, evidence: string, at: string = new Date().toISOString()): GoalState {
const cleared = initialState(state.maxChecks, state.maxNoProgressReports);
return {
...cleared,
lastOutcome: {
text: state.text,
status: "complete",
evidence: bounded(evidence, 1000),
at,
},
};
}
/** Terminal reports retain the original reset behavior. */
export function recordReport(state: GoalState): GoalState {
return {
...state,
checks: 0,
noProgressReports: 0,
workEventSinceReport: false,
};
}
/** Record one successful tool result that may back exactly one progress report. */
export function recordWorkEvent(state: GoalState, toolName: string, at: string = new Date().toISOString()): GoalState {
if (state.status !== "active") return state;
return {
...state,
workEventSinceReport: true,
lastActivityTool: bounded(toolName, 80),
lastActivityAt: at,
};
}
/**
* Classify an in_progress report mechanically.
*
* Tool-backed progress requires a successful work event. Non-tool work requires
* a concrete artifact. Waits require an owner plus a watch id or next-check
* condition. Duplicate evidence is never progress. Empty/malformed/unsupported
* reports increment a persisted counter and pause at the bound.
*/
export function recordInProgressReport(state: GoalState, report: InProgressReport, now: number = Date.now()): InProgressOutcome {
if (state.status !== "active") {
return { state, classification: "no_progress", reason: "goal is not active" };
}
const evidence = report.evidence.trim();
const progress = normalizeProgress(report.progress);
if (evidence === "" || !progress) {
return recordNoProgress(state, evidence === "" ? "empty evidence" : "missing structured progress");
}
const missing = requiredProgressField(progress);
if (missing) return recordNoProgress(state, `missing ${missing}`);
const fingerprint = progressFingerprint(evidence, progress);
if (progress.kind === "wait") {
if (!progress.watchId && !progress.nextCheck) {
return recordNoProgress(state, "wait requires an approved watch id or concrete next-check condition");
}
if (state.waitTimeoutSeconds && state.waitWakeUsed) {
return {
state: pauseGoal(state, "wait deadline wake exhausted; resolve the dependency before /goal resume"),
classification: "no_progress",
reason: "one automatic deadline wake per goal/resume has already been used",
};
}
const sameWait = state.lastProgressFingerprint === fingerprint;
return {
state: {
...state,
checks: 0,
workEventSinceReport: false,
lastProgressFingerprint: fingerprint,
lastNextAction: bounded(progress.nextAction, 240),
activeWait: {
...(state.waitTimeoutSeconds ? {
deadlineAt: state.activeWait?.deadlineAt ?? now + state.waitTimeoutSeconds * 1000,
wakeSent: false,
} : {}),
owner: bounded(progress.owner, 120),
watchId: progress.watchId ? bounded(progress.watchId, 120) : undefined,
nextCheck: progress.nextCheck ? bounded(progress.nextCheck, 240) : undefined,
},
},
classification: "waiting",
reason: sameWait ? "approved wait remains active" : "approved wait recorded",
};
}
if (progress.kind === "non_tool") {
if (!progress.artifact) return recordNoProgress(state, "non-tool work requires a concrete artifact");
} else if (!state.workEventSinceReport) {
return recordNoProgress(state, `${progress.kind} lacks a successful work event`);
}
if (state.lastProgressFingerprint === fingerprint) {
return recordNoProgress(state, "duplicate evidence");
}
return {
state: {
...state,
checks: 0,
noProgressReports: 0,
workEventSinceReport: false,
lastProgressFingerprint: fingerprint,
lastNextAction: bounded(progress.nextAction, 240),
activeWait: undefined,
},
classification: "progress",
reason: `${progress.kind} recorded`,
};
}
function recordNoProgress(state: GoalState, reason: string): InProgressOutcome {
const nextNoProgress = state.noProgressReports + 1; // NG8_COUNT_GATE
let next: GoalState = {
...state,
noProgressReports: nextNoProgress,
workEventSinceReport: false,
};
if (nextNoProgress >= state.maxNoProgressReports) { // NG8_PAUSE_GATE
next = pauseGoal(next, `paused: no substantive progress after ${nextNoProgress} in_progress reports`);
}
return { state: next, classification: "no_progress", reason };
}
function normalizeProgress(progress: ProgressDetails | undefined): ProgressDetails | undefined {
if (!progress) return undefined;
return {
kind: progress.kind,
gate: progress.gate?.trim() ?? "",
owner: progress.owner?.trim() ?? "",
lastMeasurement: progress.lastMeasurement?.trim() ?? "",
nextAction: progress.nextAction?.trim() ?? "",
artifact: progress.artifact?.trim() || undefined,
watchId: progress.watchId?.trim() || undefined,
nextCheck: progress.nextCheck?.trim() || undefined,
};
}
function requiredProgressField(progress: ProgressDetails): string | undefined {
if (!progress.gate) return "nearest gate";
if (!progress.owner) return "gate owner";
if (!progress.lastMeasurement) return "last live measurement";
if (!progress.nextAction) return "next action";
return undefined;
}
function progressFingerprint(evidence: string, progress: ProgressDetails): string {
return createHash("sha256")
.update(JSON.stringify({ evidence: evidence.trim(), progress }))
.digest("hex");
}
function bounded(value: string, max: number): string {
return value.length <= max ? value : value.slice(0, max);
}
/** True when the check cap is exhausted and the loop must auto-pause instead of injecting. */
export function checkLimitReached(state: GoalState): boolean {
return state.checks >= state.maxChecks;
}
/** Count one injected check prompt. Callers guard with checkLimitReached first. */
export function recordCheckInjected(state: GoalState): GoalState {
return { ...state, checks: state.checks + 1 };
}
/**
* FR3: satisfied requires evidence, blocked requires a reason.
* in_progress is intentionally accepted here so empty/malformed reports reach
* the no-progress counter instead of being rejected before enforcement.
*/
export function validateReport(status: string, evidence: string | undefined): string | null {
const e = (evidence ?? "").trim();
if (status === "satisfied" && e === "") {
return 'goal_report status "satisfied" requires non-empty evidence describing how the goal is met';
}
if (status === "blocked" && e === "") {
return 'goal_report status "blocked" requires a non-empty reason';
}
return null;
}
+167
View File
@@ -0,0 +1,167 @@
// Seat-durable goal state persistence (Q7b: survives /new, /resume, restarts).
// The state file lives beside the pi agent home: $PI_CODING_AGENT_DIR/goal-state.json
// for fleet seats, ~/.pi/agent/goal-state.json for operator sessions.
//
// NG-7 INCARNATION FENCING (Mercer F1, board T129): the seat-level file was
// ONE shared mutable object — two incarnations of a seat (marcie + marcie-2)
// inherited and rewrote each other's goal state on session_start. State is
// now FENCED per process-launch incarnation identity (mosaic-core
// lib/incarnation.ts — the same identity the R6 journal keys on; NOT
// PI_SESSION_ID, which is session identity):
//
// <agentDir>/goal-state.<incarnationId>.json
//
// Migration (NG-7 remediation F4): an existing unfenced goal-state.json has
// an UNKNOWN OWNER — for the live canary case it is legacy AND ACTIVE (a
// previous incarnation's focus), and velma B1 forbids a fresh incarnation
// inheriting another incarnation's active focus. So the legacy file is
// QUARANTINED, never claimed: atomically renamed aside with its bytes
// preserved (goal-state.legacy.json, then .legacy.1.json, .legacy.2.json…),
// and the resolving incarnation starts FRESH. Nobody inherits an active
// focus; nobody loses the evidence.
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, unlinkSync } from "node:fs";
import { randomUUID } from "node:crypto";
import { dirname, join } from "node:path";
import { homedir } from "node:os";
import { initialState, type GoalState } from "./state.ts";
import { incarnationIdentity } from "../../mosaic-core/lib/incarnation.ts";
export const LEGACY_STATE_FILENAME = "goal-state.json";
/** Pi agent home for the current process (seat dir for fleet seats, ~/.pi/agent otherwise). */
export function agentStateDir(env: NodeJS.ProcessEnv = process.env): string {
const dir = env.PI_CODING_AGENT_DIR;
if (dir && dir.trim() !== "") return dir;
return join(homedir(), ".pi", "agent");
}
export function stateFilePath(dir: string = agentStateDir()): string {
return join(dir, LEGACY_STATE_FILENAME);
}
/** The incarnation-fenced state file for a given agent dir + incarnation. */
export function fencedStateFilePath(dir: string, incarnationId: string): string {
return join(dir, `goal-state.${incarnationId}.json`);
}
export interface ResolveIO {
existsSync(path: string): boolean;
renameSync(from: string, to: string): void;
}
function defaultResolveIO(): ResolveIO {
return { existsSync, renameSync };
}
/** First free quarantine slot for the unknown-owner legacy file. */
export function quarantinePath(dir: string, io: ResolveIO): string {
const first = join(dir, "goal-state.legacy.json");
if (!io.existsSync(first)) return first;
for (let n = 1; ; n++) {
const candidate = join(dir, `goal-state.legacy.${n}.json`);
if (!io.existsSync(candidate)) return candidate;
}
}
/**
* Resolve the state path for THIS process incarnation, quarantining any
* unknown-owner legacy state (bytes preserved; the resolver starts fresh).
* Inject `incarnationId` and `io` for tests; production callers take the
* defaults.
*/
export function resolveStatePath(
dir: string = agentStateDir(),
opts: { incarnationId?: string; io?: ResolveIO } = {},
): string {
const io = opts.io ?? defaultResolveIO();
const incarnationId = opts.incarnationId ?? incarnationIdentity();
const fenced = fencedStateFilePath(dir, incarnationId);
if (io.existsSync(fenced)) return fenced;
const legacy = join(dir, LEGACY_STATE_FILENAME);
if (io.existsSync(legacy)) {
try {
io.renameSync(legacy, quarantinePath(dir, io)); // atomic, bytes preserved
} catch {
// raced by a sibling incarnation or fs trouble: start fresh rather
// than ever reading a file another incarnation may still rewrite
return fenced;
}
}
return fenced;
}
export function loadState(path: string = stateFilePath()): GoalState {
try {
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<GoalState>;
if (parsed?.version !== 1 || typeof parsed.text !== "string" || !parsed.status) {
return initialState();
}
return normalize(parsed as GoalState);
} catch {
return initialState();
}
}
export function saveState(state: GoalState, path: string = stateFilePath()): void {
mkdirSync(dirname(path), { recursive: true });
const temporary = `${path}.${randomUUID()}.tmp`;
try {
writeFileSync(temporary, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", flag: "wx", mode: 0o600 });
renameSync(temporary, path);
} finally {
if (existsSync(temporary)) unlinkSync(temporary);
}
}
function normalize(state: GoalState): GoalState {
const defaults = initialState();
const maxChecks =
Number.isInteger(state.maxChecks) && state.maxChecks >= 1
? state.maxChecks
: defaults.maxChecks;
const checks = Number.isInteger(state.checks) && state.checks >= 0 ? state.checks : 0;
const maxNoProgressReports =
Number.isInteger(state.maxNoProgressReports) && state.maxNoProgressReports >= 1
? state.maxNoProgressReports
: defaults.maxNoProgressReports;
const noProgressReports =
Number.isInteger(state.noProgressReports) && state.noProgressReports >= 0
? state.noProgressReports
: 0;
const normalized: GoalState = {
...defaults,
...state,
maxChecks,
checks,
maxNoProgressReports,
noProgressReports,
workEventSinceReport: state.workEventSinceReport === true,
};
const outcome = state.lastOutcome;
delete normalized.lastOutcome;
if (state.status === "none" && outcome?.status === "complete" &&
typeof outcome.text === "string" && outcome.text.length > 0 &&
typeof outcome.evidence === "string" && outcome.evidence.length > 0 &&
typeof outcome.at === "string" && Number.isFinite(Date.parse(outcome.at))) {
normalized.lastOutcome = { text: outcome.text, status: "complete", evidence: outcome.evidence.slice(0, 1000), at: outcome.at };
}
if (!["active", "paused", "blocked", "none"].includes(state.status)) {
normalized.status = "paused";
normalized.pausedReason = "invalid persisted status; inspect before /goal resume";
}
if (typeof normalized.pausedReason !== "string") delete normalized.pausedReason;
if (state.waitTimeoutSeconds !== undefined) {
const timeoutValid = Number.isInteger(state.waitTimeoutSeconds) && state.waitTimeoutSeconds >= 10 && state.waitTimeoutSeconds <= 86400;
const wait = state.activeWait;
const validTime = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 8640000000000000;
const deadlineValid = !wait || (validTime(wait.deadlineAt) && wait.deadlineAt <= Date.now() + 86400000);
const flagsValid = [state.waitWakeUsed, wait?.wakeSent, wait?.wakeObserved].every(value => value === undefined || typeof value === "boolean");
const dispatchValid = !wait?.wakeSent || (validTime(wait.wakeDispatchedAt) &&
typeof wait.wakeRequestId === "string" && wait.wakeRequestId.length > 0 && state.waitWakeUsed === true);
if (!timeoutValid || !deadlineValid || !dispatchValid || !flagsValid) {
return { ...normalized, status: state.status === "none" ? "none" : "paused", pausedReason: "invalid persisted wait state; inspect before /goal resume" };
}
}
return normalized;
}
@@ -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\./);
});
+65
View File
@@ -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);
});
+253
View File
@@ -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).
+320
View File
@@ -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 }); }
});
+180
View File
@@ -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 });
}
});
+27
View File
@@ -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");
});
+599
View File
@@ -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 });
}
});