Goal completion is a claim, not a decision: the independent audit
Adopted from studying Claude Code's /goal (completion judged by a fresh model, never the one doing the work) and pi-goal-x's auditor (isolated session, read-only tools, explicit approval marker, rejects scaffold-only and proxy-metric completions). complete_goal no longer closes a goal on the caller's word: - It now takes a required `evidence` claim, and permission/argument gates run before any audit spend. - The audit is an isolated tool-turn loop: fresh conversation (the auditor never sees the executor's transcript), read-only workspace tools (no run_command), strict auditor instructions, at most 6 iterations. - Only an explicit <approved/> closes the goal (recorded with the audit verdict). <disapproved/>, no marker, or an exhausted budget all leave the goal active and hand the decisive gap back to the caller as the tool result. - The handler moved from the sync dispatcher into the run loop's async routing because it needs provider access; the goal-review prompt warns that substanceless claims will bounce. AGENTS.md documents both this and the web-access contract.
This commit is contained in:
1 parent
80e70649c3
commit
34ea33cb8d
5 files changed
+273
-67
No files matched your search
@@ -1,36 +1,134 @@
|
||||
import { AuditService } from "./audit-service";
|
||||
import { executeWorkspaceTool, WORKSPACE_TOOLS } from "./agent-workspace-tools";
|
||||
import { requireString } from "./agent-tool-args";
|
||||
import type { AgentToolContext, AgentToolDeps } from "./agent-tools";
|
||||
import type { ToolDefinition } from "./provider-tool-turn";
|
||||
import type { ProviderService } from "./provider-service";
|
||||
import type { AgentTurnMessage, ToolCall, ToolDefinition } from "./provider-tool-turn";
|
||||
|
||||
const AUDIT_MAX_ITERATIONS = 6;
|
||||
|
||||
export const GOAL_TOOLS: ToolDefinition[] = [
|
||||
{
|
||||
name: "complete_goal",
|
||||
description: "Mark the goal your current task belongs to as achieved. CEO or the goal's owner only. Call it only when the evidence shows the goal is genuinely delivered — for software, that means code in the workspace that builds and passes its tests, not documents about it.",
|
||||
parameters: { type: "object", additionalProperties: false, properties: {}, required: [] },
|
||||
description: "Ask to close the goal your current task belongs to as achieved. CEO or the goal's owner only. An independent auditor inspects the workspace before the goal actually closes — scaffolds, plans, and documents about the work do not pass; working results do.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
evidence: {
|
||||
type: "string",
|
||||
description: "The concrete evidence that the goal is achieved: what exists, where it lives in the workspace, and what verification passed.",
|
||||
},
|
||||
},
|
||||
required: ["evidence"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function completeGoalTool(deps: AgentToolDeps, ctx: AgentToolContext): string {
|
||||
if (!ctx.taskGoalId) return "ERROR: this task is not attached to a goal, so there is nothing to complete.";
|
||||
const goal = deps.db.prepare("select id, title, status, owner_agent_id from goals where id = ? and org_id = ?")
|
||||
.get(ctx.taskGoalId, ctx.orgId) as { id: string; title: string; status: string; owner_agent_id: string | null } | undefined;
|
||||
if (!goal) throw new Error("Goal was not found.");
|
||||
const acting = deps.db.prepare("select role_level from agents where id = ? and org_id = ?")
|
||||
.get(ctx.agent.id, ctx.orgId) as { role_level: string } | undefined;
|
||||
if (acting?.role_level !== "c_suite" && ctx.agent.id !== goal.owner_agent_id) {
|
||||
return "ERROR: Only the CEO or the goal's owner can complete a goal. Report your evidence to them instead.";
|
||||
export interface GoalCompletionArgs {
|
||||
deps: AgentToolDeps;
|
||||
ctx: AgentToolContext;
|
||||
providers: Pick<ProviderService, "runToolTurn">;
|
||||
providerId: string;
|
||||
workspaceId: string | null;
|
||||
call: ToolCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion is a claim, not a decision (the pi-goal-x auditor pattern): the
|
||||
* executor states evidence, and a fresh model with read-only workspace tools
|
||||
* verifies it. The auditor never shares the executor's conversation, so it
|
||||
* judges what actually exists rather than what was said to exist. Any outcome
|
||||
* other than an explicit approval leaves the goal open.
|
||||
*/
|
||||
export async function completeGoalTool(args: GoalCompletionArgs): Promise<string> {
|
||||
const { deps, ctx, call } = args;
|
||||
try {
|
||||
if (!ctx.taskGoalId) return "ERROR: this task is not attached to a goal, so there is nothing to complete.";
|
||||
const goal = deps.db.prepare("select id, title, description, status, owner_agent_id from goals where id = ? and org_id = ?")
|
||||
.get(ctx.taskGoalId, ctx.orgId) as { id: string; title: string; description: string; status: string; owner_agent_id: string | null } | undefined;
|
||||
if (!goal) throw new Error("Goal was not found.");
|
||||
const acting = deps.db.prepare("select role_level from agents where id = ? and org_id = ?")
|
||||
.get(ctx.agent.id, ctx.orgId) as { role_level: string } | undefined;
|
||||
if (acting?.role_level !== "c_suite" && ctx.agent.id !== goal.owner_agent_id) {
|
||||
return "ERROR: Only the CEO or the goal's owner can complete a goal. Report your evidence to them instead.";
|
||||
}
|
||||
if (goal.status !== "active") return `Goal "${goal.title}" is already ${goal.status}.`;
|
||||
const evidence = requireString(call.arguments.evidence, "evidence");
|
||||
|
||||
const verdict = await auditGoalCompletion(args, goal, evidence);
|
||||
if (!verdict.approved) {
|
||||
return `The independent audit rejected closing the goal: ${verdict.reason} The goal stays active — close that gap or create the tasks that will.`;
|
||||
}
|
||||
deps.db.prepare("update goals set status = 'achieved', updated_at = ? where id = ? and org_id = ?")
|
||||
.run(new Date().toISOString(), goal.id, ctx.orgId);
|
||||
new AuditService(deps.db).record({
|
||||
orgId: ctx.orgId,
|
||||
entityType: "goal",
|
||||
entityId: goal.id,
|
||||
actorType: "agent",
|
||||
actorId: ctx.agent.id,
|
||||
action: "status_changed",
|
||||
payload: { status: "achieved", auditVerdict: verdict.reason },
|
||||
});
|
||||
return `Goal "${goal.title}" is marked achieved — the independent audit confirmed it (${verdict.reason}). Tell the team.`;
|
||||
} catch (error) {
|
||||
return `ERROR: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
if (goal.status !== "active") return `Goal "${goal.title}" is already ${goal.status}.`;
|
||||
deps.db.prepare("update goals set status = 'achieved', updated_at = ? where id = ? and org_id = ?")
|
||||
.run(new Date().toISOString(), goal.id, ctx.orgId);
|
||||
new AuditService(deps.db).record({
|
||||
orgId: ctx.orgId,
|
||||
entityType: "goal",
|
||||
entityId: goal.id,
|
||||
actorType: "agent",
|
||||
actorId: ctx.agent.id,
|
||||
action: "status_changed",
|
||||
payload: { status: "achieved" },
|
||||
});
|
||||
return `Goal "${goal.title}" is marked achieved. Tell the team.`;
|
||||
}
|
||||
|
||||
interface AuditVerdict {
|
||||
approved: boolean;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
async function auditGoalCompletion(
|
||||
args: GoalCompletionArgs,
|
||||
goal: { title: string; description: string },
|
||||
evidence: string,
|
||||
): Promise<AuditVerdict> {
|
||||
// Read-only inspection: the auditor may look at anything but change nothing.
|
||||
const auditTools = WORKSPACE_TOOLS.filter((tool) => tool.name !== "run_command");
|
||||
const auditCtx: AgentToolContext = {
|
||||
...args.ctx,
|
||||
workspace: args.ctx.workspace ? { ...args.ctx.workspace, writeAccess: false } : null,
|
||||
};
|
||||
const messages: AgentTurnMessage[] = [{
|
||||
role: "user",
|
||||
text: [
|
||||
"A goal is claimed complete. Decide whether the claim survives the evidence.",
|
||||
`Goal: ${goal.title}`,
|
||||
`Goal detail: ${goal.description}`,
|
||||
"The executor's claim:",
|
||||
evidence,
|
||||
"Inspect the workspace with list_files and read_file before deciding. Judge outcomes, not artifacts about outcomes: plans, specs, status reports, and skeletons do not make a goal achieved; working deliverables do. If the goal promises software, look for the actual implementation, not documents describing it.",
|
||||
"End your final message with exactly one marker: <approved/> if the goal is genuinely achieved, or <disapproved/> followed by one short sentence naming the decisive gap.",
|
||||
].join("\n\n"),
|
||||
}];
|
||||
const instructions = "You are the independent completion auditor. You did not do the work and owe its author nothing; your only loyalty is to what actually exists. Be strict: an unverifiable claim is a false claim.";
|
||||
|
||||
for (let iteration = 1; iteration <= AUDIT_MAX_ITERATIONS; iteration += 1) {
|
||||
const turn = await args.providers.runToolTurn({
|
||||
orgId: args.ctx.orgId,
|
||||
providerId: args.providerId,
|
||||
workspaceId: args.workspaceId,
|
||||
instructions,
|
||||
messages,
|
||||
tools: auditTools,
|
||||
});
|
||||
if (turn.toolCalls.length === 0) return parseVerdict(turn.text);
|
||||
messages.push({ role: "assistant", text: turn.text, toolCalls: turn.toolCalls });
|
||||
const results: Array<{ toolCallId: string; toolName: string; output: string }> = [];
|
||||
for (const toolCall of turn.toolCalls) {
|
||||
results.push({ toolCallId: toolCall.id, toolName: toolCall.name, output: await executeWorkspaceTool(auditCtx, toolCall) });
|
||||
}
|
||||
messages.push({ role: "tool", results });
|
||||
}
|
||||
return { approved: false, reason: "the audit did not reach a verdict within its budget, which counts as not achieved" };
|
||||
}
|
||||
|
||||
function parseVerdict(text: string): AuditVerdict {
|
||||
const reason = text.replace(/<\/?(approved|disapproved)\s*\/?>/gi, " ").replace(/\s+/g, " ").trim();
|
||||
if (/<approved\s*\/>/i.test(text)) return { approved: true, reason: reason || "no gaps found" };
|
||||
return { approved: false, reason: reason || "the auditor did not state an explicit approval" };
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ProviderService } from "./provider-service";
|
||||
import { AGENT_TOOLS, dispatchAgentTool, type AgentToolContext, type AgentToolDeps } from "./agent-tools";
|
||||
import { completeGoalTool } from "./agent-goal-tools";
|
||||
import { executeWebTool, isWebTool, WEB_TOOLS } from "./agent-web-tools";
|
||||
import { executeWorkspaceTool, isWorkspaceTool, WORKSPACE_TOOLS } from "./agent-workspace-tools";
|
||||
import { canonicalMentionHandle } from "../../shared/mention-handles";
|
||||
@@ -76,6 +77,17 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn
|
||||
output = await executeWorkspaceTool(input.toolContext, call);
|
||||
} else if (isWebTool(call.name)) {
|
||||
output = await executeWebTool(call);
|
||||
} else if (call.name === "complete_goal") {
|
||||
// Runs an independent audit turn against the provider, so it needs
|
||||
// the loop's provider access rather than the sync dispatcher.
|
||||
output = await completeGoalTool({
|
||||
deps: deps.tools,
|
||||
ctx: input.toolContext,
|
||||
providers: deps.providers,
|
||||
providerId: input.providerId,
|
||||
workspaceId: input.workspaceId,
|
||||
call,
|
||||
});
|
||||
} else {
|
||||
output = dispatchAgentTool(deps.tools, input.toolContext, call, {
|
||||
announceTaskAssignments: shouldAnnounceTaskAssignment(call, turn.toolCalls),
|
||||
|
||||
@@ -8,7 +8,7 @@ import { readMessagesTool } from "./agent-read-messages";
|
||||
import { grantConnectorTool, listConnectorsTool } from "./agent-connector-tools";
|
||||
import { bindSkillSlugs, grantSkillTool, listSkillsTool } from "./agent-skill-tools";
|
||||
import { createDocumentTool, writeDeliverableTool } from "./agent-deliverable-tools";
|
||||
import { completeGoalTool, GOAL_TOOLS } from "./agent-goal-tools";
|
||||
import { GOAL_TOOLS } from "./agent-goal-tools";
|
||||
import { describeSend } from "./agent-message-presence";
|
||||
import { optionalString, optionalStringArray, requireString } from "./agent-tool-args";
|
||||
import type { JsonRecord } from "./provider-http";
|
||||
@@ -234,8 +234,6 @@ export function dispatchAgentTool(
|
||||
return createTask(deps, ctx, call.arguments, options);
|
||||
case "update_task_status":
|
||||
return updateTaskStatus(deps, ctx, call.arguments);
|
||||
case "complete_goal":
|
||||
return completeGoalTool(deps, ctx);
|
||||
case "write_deliverable":
|
||||
return writeDeliverableTool(deps, ctx, call.arguments);
|
||||
case "send_message":
|
||||
|
||||
@@ -88,7 +88,7 @@ function goalReviewDescription(goalTitle: string): string {
|
||||
`Every task under the goal "${goalTitle.trim()}" is finished, but the goal itself is still open. Decide what happens next — this is a checkpoint, not a formality.`,
|
||||
"First gather evidence: list_files and read_file the workspace, and read what the team reported. Plans, specs, and status documents are not the product. If the goal promises working software, the proof is code in the workspace that builds and passes its tests (run_command).",
|
||||
"Then take exactly one of these actions before closing this task:",
|
||||
"1. The goal is genuinely achieved -> call complete_goal.",
|
||||
"1. The goal is genuinely achieved -> call complete_goal with your evidence. An independent auditor inspects the workspace before the goal actually closes, so claims without substance will bounce.",
|
||||
"2. It is not achieved -> create the next wave of tasks (create_task, assigned by name). Implementation work must set requires_filesystem: true so the assignee can change code and run commands. Documents alone must not close the next wave either.",
|
||||
].join("\n\n");
|
||||
}
|
||||
@@ -1,56 +1,154 @@
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { afterAll, describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { ChatService } from "../../src/main/services/chat-service";
|
||||
import { dispatchAgentTool, type AgentToolContext, type AgentToolDeps } from "../../src/main/services/agent-tools";
|
||||
import { completeGoalTool, type GoalCompletionArgs } from "../../src/main/services/agent-goal-tools";
|
||||
import type { AgentToolContext, AgentToolDeps } from "../../src/main/services/agent-tools";
|
||||
import type { ProviderService } from "../../src/main/services/provider-service";
|
||||
import type { ToolCall } from "../../src/main/services/provider-tool-turn";
|
||||
|
||||
function call(name: string): ToolCall {
|
||||
return { id: `call_${randomUUID()}`, name, arguments: {} };
|
||||
type Auditor = ProviderService["runToolTurn"];
|
||||
type AuditorInput = Parameters<Auditor>[0];
|
||||
type AuditorTurn = Awaited<ReturnType<Auditor>>;
|
||||
|
||||
function turnOf(text: string, toolCalls: ToolCall[] = []): AuditorTurn {
|
||||
return { text, toolCalls, rawResponseJson: "{}", requestId: `req_${randomUUID()}` };
|
||||
}
|
||||
|
||||
describe("complete_goal tool", () => {
|
||||
test("the CEO can close their goal; a staff engineer cannot", () => {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-goal-tool-${randomUUID()}.db`));
|
||||
const organization = new OrganizationService(connection.db);
|
||||
let orgId = "";
|
||||
let ceoId = "";
|
||||
let staffId = "";
|
||||
let goalId = "";
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({ organizationName: "GoalCorp", employeeName: "Yang", mission: "m", goal: "g" });
|
||||
const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
|
||||
ceoId = organization.createAgent({
|
||||
orgId, name: "CEO", role: "CEO", department: "Executive",
|
||||
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 25000, managerAgentId: null, roleLevel: "c_suite",
|
||||
});
|
||||
staffId = organization.createAgent({
|
||||
orgId, name: "Dev", role: "Engineer", department: "Engineering",
|
||||
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 10000, managerAgentId: ceoId, roleLevel: "staff",
|
||||
});
|
||||
goalId = organization.createGoal({ orgId, title: "Ship it", description: "For real.", ownerAgentId: ceoId });
|
||||
const workspaceRoot = join(tmpdir(), `ac-goal-audit-${randomUUID()}`);
|
||||
mkdirSync(join(workspaceRoot, "src"), { recursive: true });
|
||||
writeFileSync(join(workspaceRoot, "src", "index.ts"), "export const app = () => 'running';\n");
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(workspaceRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
interface Harness {
|
||||
connection: DatabaseConnection;
|
||||
deps: AgentToolDeps;
|
||||
orgId: string;
|
||||
ceoId: string;
|
||||
staffId: string;
|
||||
goalId: string;
|
||||
providerId: string;
|
||||
}
|
||||
|
||||
function createHarness(): Harness {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-goal-tool-${randomUUID()}.db`));
|
||||
const organization = new OrganizationService(connection.db);
|
||||
let orgId = "";
|
||||
let ceoId = "";
|
||||
let staffId = "";
|
||||
let goalId = "";
|
||||
let providerId = "";
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({ organizationName: "GoalCorp", employeeName: "Yang", mission: "m", goal: "g" });
|
||||
providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
|
||||
ceoId = organization.createAgent({
|
||||
orgId, name: "CEO", role: "CEO", department: "Executive",
|
||||
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 25000, managerAgentId: null, roleLevel: "c_suite",
|
||||
});
|
||||
const deps: AgentToolDeps = { db: connection.db, organization, chat: new ChatService(connection.db) };
|
||||
const ctxFor = (agentId: string): AgentToolContext => ({
|
||||
orgId, agent: { id: agentId, modelProviderId: null, modelId: null },
|
||||
taskProjectId: null, taskGoalId: goalId, workspace: null,
|
||||
staffId = organization.createAgent({
|
||||
orgId, name: "Dev", role: "Engineer", department: "Engineering",
|
||||
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 10000, managerAgentId: ceoId, roleLevel: "staff",
|
||||
});
|
||||
goalId = organization.createGoal({ orgId, title: "Ship it", description: "For real.", ownerAgentId: ceoId });
|
||||
});
|
||||
return { connection, deps: { db: connection.db, organization, chat: new ChatService(connection.db) }, orgId, ceoId, staffId, goalId, providerId };
|
||||
}
|
||||
|
||||
expect(dispatchAgentTool(deps, ctxFor(staffId), call("complete_goal"))).toContain("ERROR");
|
||||
expect(connection.db.prepare("select status from goals where id = ?").get(goalId)).toEqual({ status: "active" });
|
||||
function call(args: Record<string, unknown>): ToolCall {
|
||||
return { id: `call_${randomUUID()}`, name: "complete_goal", arguments: args };
|
||||
}
|
||||
|
||||
expect(dispatchAgentTool(deps, ctxFor(ceoId), call("complete_goal"))).toContain("achieved");
|
||||
expect(connection.db.prepare("select status from goals where id = ?").get(goalId)).toEqual({ status: "achieved" });
|
||||
function argsFor(h: Harness, agentId: string, auditor: Auditor, toolCall: ToolCall): GoalCompletionArgs {
|
||||
const ctx: AgentToolContext = {
|
||||
orgId: h.orgId,
|
||||
agent: { id: agentId, modelProviderId: h.providerId, modelId: "gpt-5.4" },
|
||||
taskProjectId: null,
|
||||
taskGoalId: h.goalId,
|
||||
workspace: { id: "wsp_audit", localPath: workspaceRoot, writeAccess: true },
|
||||
};
|
||||
return { deps: h.deps, ctx, providers: { runToolTurn: auditor }, providerId: h.providerId, workspaceId: null, call: toolCall };
|
||||
}
|
||||
|
||||
// Idempotent, and honest about it.
|
||||
expect(dispatchAgentTool(deps, ctxFor(ceoId), call("complete_goal"))).toContain("already achieved");
|
||||
function goalStatus(h: Harness): string {
|
||||
return (h.connection.db.prepare("select status from goals where id = ?").get(h.goalId) as { status: string }).status;
|
||||
}
|
||||
|
||||
// A task with no goal attached cannot complete anything.
|
||||
const noGoalCtx = { ...ctxFor(ceoId), taskGoalId: null };
|
||||
expect(dispatchAgentTool(deps, noGoalCtx, call("complete_goal"))).toContain("ERROR");
|
||||
connection.close();
|
||||
describe("complete_goal with independent audit", () => {
|
||||
test("an approving auditor closes the goal; the auditor gets read-only workspace access", async () => {
|
||||
const h = createHarness();
|
||||
const seenTools: string[][] = [];
|
||||
let turns = 0;
|
||||
const auditor = async (input: AuditorInput): Promise<AuditorTurn> => {
|
||||
seenTools.push(input.tools.map((tool) => tool.name));
|
||||
turns += 1;
|
||||
if (turns === 1) {
|
||||
return turnOf("Inspecting.", [{ id: "c1", name: "read_file", arguments: { path: "src/index.ts" } }]);
|
||||
}
|
||||
const readResult = input.messages.at(-1);
|
||||
expect(readResult?.role).toBe("tool");
|
||||
return turnOf("The implementation exists and runs. <approved/>");
|
||||
};
|
||||
|
||||
const output = await completeGoalTool(argsFor(h, h.ceoId, auditor, call({ evidence: "src/index.ts implements the app and runs." })));
|
||||
expect(output).toContain("achieved");
|
||||
expect(goalStatus(h)).toBe("achieved");
|
||||
// The auditor never receives run_command — inspection is read-only.
|
||||
expect(seenTools.every((tools) => !tools.includes("run_command"))).toBe(true);
|
||||
expect(seenTools[0]).toContain("read_file");
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("a disapproving auditor keeps the goal open and returns the gap to the caller", async () => {
|
||||
const h = createHarness();
|
||||
const auditor = async (): Promise<AuditorTurn> => turnOf("<disapproved/> Only planning documents exist; there is no implementation.");
|
||||
const output = await completeGoalTool(argsFor(h, h.ceoId, auditor, call({ evidence: "We wrote a very thorough plan." })));
|
||||
expect(output).toContain("audit rejected");
|
||||
expect(output).toContain("no implementation");
|
||||
expect(goalStatus(h)).toBe("active");
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("no explicit approval means not achieved", async () => {
|
||||
const h = createHarness();
|
||||
const auditor = async (): Promise<AuditorTurn> => turnOf("Looks plausible I guess.");
|
||||
const output = await completeGoalTool(argsFor(h, h.ceoId, auditor, call({ evidence: "Trust me." })));
|
||||
expect(output).toContain("audit rejected");
|
||||
expect(goalStatus(h)).toBe("active");
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("permission and argument gates run before any audit spend", async () => {
|
||||
const h = createHarness();
|
||||
let auditorCalls = 0;
|
||||
const auditor = async (): Promise<AuditorTurn> => {
|
||||
auditorCalls += 1;
|
||||
return turnOf("<approved/>");
|
||||
};
|
||||
|
||||
// Staff cannot complete a goal they do not own.
|
||||
expect(await completeGoalTool(argsFor(h, h.staffId, auditor, call({ evidence: "done" })))).toContain("ERROR");
|
||||
// Evidence is mandatory.
|
||||
expect(await completeGoalTool(argsFor(h, h.ceoId, auditor, call({})))).toContain("ERROR");
|
||||
// A task without a goal has nothing to complete.
|
||||
const detached = argsFor(h, h.ceoId, auditor, call({ evidence: "done" }));
|
||||
detached.ctx.taskGoalId = null;
|
||||
expect(await completeGoalTool(detached)).toContain("ERROR");
|
||||
|
||||
expect(auditorCalls).toBe(0);
|
||||
expect(goalStatus(h)).toBe("active");
|
||||
|
||||
// Once achieved, a second attempt reports the state instead of re-auditing.
|
||||
await completeGoalTool(argsFor(h, h.ceoId, auditor, call({ evidence: "src/index.ts works" })));
|
||||
expect(goalStatus(h)).toBe("achieved");
|
||||
expect(await completeGoalTool(argsFor(h, h.ceoId, auditor, call({ evidence: "again" })))).toContain("already achieved");
|
||||
expect(auditorCalls).toBe(1);
|
||||
h.connection.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user