Goals end at achieved, not at an empty board: the goal-review loop
The board draining to 280/280 done left the company idle under a still-active goal, with 24 markdown files and zero code as its life's work. Two structural gaps caused it: goals had no lifecycle (status was written once as 'active' and never read again — nothing re-engages anyone when the last task closes), and nothing anywhere distinguished plans from product, so every "engineering" objective closed on documents. The loop, closed: - tasks.origin (migration 055) records provenance: chat_wake for mention vehicles (backfilled from their runs), goal_review for checkpoints, null for real work. Chatter and checkpoints can therefore never count as progress. - spawnGoalReviews (each worker tick): an active goal with zero open tasks and real progress since the last review gets ONE review task for the goal owner. The review demands evidence — list the workspace, read the code, run the tests; documents are how work starts, not proof it happened — and must end in exactly one of: complete_goal, or the next wave of tasks with requires_filesystem engineering. A review closed with neither spawns nothing: termination is structural, not a counter. - complete_goal (CEO or goal owner) finally lets someone say a goal is achieved, audited. - CEO and department-head voices now state the doctrine: a goal that promises software closes on code that builds and passes tests in the workspace, and implementation is delegated with requires_filesystem: true. On the live profile this arms one review: the active platform goal has 0 open tasks and 21 real completed ones, so the CEO gets the checkpoint on next launch and the company starts building instead of stopping.
This commit is contained in:
1 parent
291816aa06
commit
26de3129b1
14 files changed
+365
-4
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { 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 type { ToolCall } from "../../src/main/services/provider-tool-turn";
|
||||
|
||||
function call(name: string): ToolCall {
|
||||
return { id: `call_${randomUUID()}`, name, arguments: {} };
|
||||
}
|
||||
|
||||
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 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,
|
||||
});
|
||||
|
||||
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" });
|
||||
|
||||
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" });
|
||||
|
||||
// Idempotent, and honest about it.
|
||||
expect(dispatchAgentTool(deps, ctxFor(ceoId), call("complete_goal"))).toContain("already achieved");
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { spawnGoalReviews } from "../../src/main/services/goal-review-service";
|
||||
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../src/shared/domain";
|
||||
|
||||
interface Harness {
|
||||
connection: DatabaseConnection;
|
||||
organization: OrganizationService;
|
||||
orgId: string;
|
||||
ceoId: string;
|
||||
goalId: string;
|
||||
kickoffId: string;
|
||||
}
|
||||
|
||||
function createHarness(): Harness {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-goal-review-${randomUUID()}.db`));
|
||||
const organization = new OrganizationService(connection.db);
|
||||
let orgId = "";
|
||||
let ceoId = "";
|
||||
let goalId = "";
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({ organizationName: "LoopCorp", 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",
|
||||
});
|
||||
goalId = organization.createGoal({ orgId, title: "Ship the platform", description: "Deliver it for real.", ownerAgentId: ceoId });
|
||||
});
|
||||
const kickoffId = (connection.db.prepare("select id from tasks where goal_id = ?").get(goalId) as { id: string }).id;
|
||||
return { connection, organization, orgId, ceoId, goalId, kickoffId };
|
||||
}
|
||||
|
||||
function setTaskStatus(h: Harness, taskId: string, status: string, updatedAt?: string): void {
|
||||
h.connection.db.prepare("update tasks set status = ?, updated_at = ? where id = ?")
|
||||
.run(status, updatedAt ?? new Date().toISOString(), taskId);
|
||||
}
|
||||
|
||||
function addDoneTask(h: Harness, title: string, origin: string | null, updatedAt: string): string {
|
||||
let taskId = "";
|
||||
h.connection.transaction(() => {
|
||||
taskId = h.organization.createTask({
|
||||
orgId: h.orgId, title, description: "d", status: TASK_STATUS.DONE, assigneeAgentId: h.ceoId,
|
||||
risk: RISK_LEVEL.LOW, projectId: null, goalId: h.goalId, workspaceId: null,
|
||||
priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
|
||||
...(origin ? { origin: origin as "chat_wake" | "goal_review" } : {}),
|
||||
});
|
||||
});
|
||||
h.connection.db.prepare("update tasks set updated_at = ? where id = ?").run(updatedAt, taskId);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
function reviews(h: Harness): Array<{ id: string; status: string }> {
|
||||
return h.connection.db.prepare("select id, status from tasks where goal_id = ? and origin = 'goal_review' order by created_at")
|
||||
.all(h.goalId) as Array<{ id: string; status: string }>;
|
||||
}
|
||||
|
||||
describe("goal review service", () => {
|
||||
test("spawns one review when the board drains, and never duplicates while it is open", () => {
|
||||
const h = createHarness();
|
||||
// Open kickoff blocks the checkpoint.
|
||||
expect(h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization))).toEqual([]);
|
||||
|
||||
setTaskStatus(h, h.kickoffId, TASK_STATUS.DONE);
|
||||
const first = h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization));
|
||||
expect(first).toHaveLength(1);
|
||||
const review = h.connection.db.prepare("select title, origin, status, assignee_agent_id from tasks where id = ?")
|
||||
.get(first[0]) as { title: string; origin: string; status: string; assignee_agent_id: string };
|
||||
expect(review).toMatchObject({ origin: "goal_review", status: "backlog", assignee_agent_id: h.ceoId });
|
||||
expect(review.title).toContain("Goal review:");
|
||||
|
||||
// The open review itself blocks a duplicate.
|
||||
expect(h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization))).toEqual([]);
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("a review that ends without new work does not respawn; real progress does", () => {
|
||||
const h = createHarness();
|
||||
setTaskStatus(h, h.kickoffId, TASK_STATUS.DONE);
|
||||
const [reviewId] = h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization));
|
||||
|
||||
// CEO closes the review without creating tasks or completing the goal: silence, not a loop.
|
||||
setTaskStatus(h, reviewId, TASK_STATUS.DONE, "2026-08-01T10:00:00.000Z");
|
||||
expect(h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization))).toEqual([]);
|
||||
|
||||
// Chatter never counts as progress.
|
||||
addDoneTask(h, "Reply to CEO", "chat_wake", "2026-08-01T12:00:00.000Z");
|
||||
expect(h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization))).toEqual([]);
|
||||
|
||||
// A real completed task after the last review re-arms the checkpoint.
|
||||
addDoneTask(h, "Implement tenant module", null, "2026-08-01T13:00:00.000Z");
|
||||
expect(h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization))).toHaveLength(1);
|
||||
expect(reviews(h)).toHaveLength(2);
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("achieved goals never spawn reviews", () => {
|
||||
const h = createHarness();
|
||||
setTaskStatus(h, h.kickoffId, TASK_STATUS.DONE);
|
||||
h.connection.db.prepare("update goals set status = 'achieved' where id = ?").run(h.goalId);
|
||||
expect(h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization))).toEqual([]);
|
||||
h.connection.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { taskOriginSchema } from "../../src/main/database/migrations/055-task-origin";
|
||||
|
||||
describe("task origin migration", () => {
|
||||
test("adds the column and backfills historical wake vehicles from their runs", () => {
|
||||
const db = new DatabaseSync(join(tmpdir(), `agentcompany-origin-migration-${randomUUID()}.db`));
|
||||
db.exec(`
|
||||
create table schema_migrations(version integer primary key, name text not null, applied_at text not null);
|
||||
create table tasks(id text primary key, status text not null, updated_at text not null);
|
||||
create table task_runs(id text primary key, task_id text not null, trigger text not null, state text not null);
|
||||
insert into schema_migrations(version, name, applied_at) values (54, 'close_stale_wake_tasks', datetime('now'));
|
||||
insert into tasks(id, status, updated_at) values ('tsk_wake', 'done', datetime('now'));
|
||||
insert into tasks(id, status, updated_at) values ('tsk_real', 'done', datetime('now'));
|
||||
insert into task_runs(id, task_id, trigger, state) values ('run_1', 'tsk_wake', 'chat_mention', 'completed');
|
||||
insert into task_runs(id, task_id, trigger, state) values ('run_2', 'tsk_real', 'heartbeat', 'completed');
|
||||
`);
|
||||
|
||||
db.exec(taskOriginSchema);
|
||||
|
||||
const origins = Object.fromEntries(
|
||||
(db.prepare("select id, origin from tasks").all() as Array<{ id: string; origin: string | null }>).map((row) => [row.id, row.origin]),
|
||||
);
|
||||
expect(origins).toEqual({ tsk_wake: "chat_wake", tsk_real: null });
|
||||
expect(db.prepare("select version from schema_migrations order by version desc limit 1").get()).toEqual({ version: 55 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user