Q53: stalled goal reviews leave a visible state and a resume path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
69e991ff93
commit
ec2698575d
15 files changed
+149
-45
No files matched your search
@@ -83,7 +83,7 @@ export async function completeGoalTool(args: GoalCompletionArgs): Promise<string
|
||||
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 = ?")
|
||||
deps.db.prepare("update goals set status = 'achieved', stalled_reason = null, updated_at = ? where id = ? and org_id = ?")
|
||||
.run(new Date().toISOString(), goal.id, ctx.orgId);
|
||||
new AuditService(deps.db).record({
|
||||
orgId: ctx.orgId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../shared/domain";
|
||||
import { ensureAgentHeartbeat, resolveGoalKickoff } from "./goal-kickoff";
|
||||
import { AuditService } from "./audit-service";
|
||||
import type { OrganizationService } from "./organization-service";
|
||||
|
||||
interface ReviewableGoalRow {
|
||||
@@ -11,20 +12,7 @@ interface ReviewableGoalRow {
|
||||
owner_agent_id: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the loop the task pipeline leaves open: tasks end at `done`, but a
|
||||
* company is finished when its GOAL is achieved. Whenever an active goal has
|
||||
* no open tasks left and real work landed since the last checkpoint, the goal
|
||||
* owner (usually the CEO) gets one review task: verify the goal against the
|
||||
* actual state of the workspace, then either mark it achieved (complete_goal)
|
||||
* or plan the next wave of tasks. Without this, the company drains its board,
|
||||
* declares nothing, and idles forever under a still-active goal.
|
||||
*
|
||||
* Termination is structural: chatter (`chat_wake`) and reviews themselves
|
||||
* never count as progress, so a review that ends with no new work and no
|
||||
* completed goal does NOT respawn — the loop only continues while real tasks
|
||||
* keep completing or the goal is closed.
|
||||
*/
|
||||
/** When an active goal's board drains after real progress, hands its owner a review: close the goal or plan the next wave. */
|
||||
export function spawnGoalReviews(db: DatabaseSync, organization: OrganizationService): string[] {
|
||||
const goals = db.prepare(`
|
||||
select g.id, g.org_id, g.title, g.description, g.owner_agent_id
|
||||
@@ -55,34 +43,88 @@ export function spawnGoalReviews(db: DatabaseSync, organization: OrganizationSer
|
||||
|
||||
const created: string[] = [];
|
||||
for (const goal of goals) {
|
||||
const plan = resolveGoalKickoff(db, {
|
||||
orgId: goal.org_id,
|
||||
goalTitle: goal.title,
|
||||
goalDescription: goal.description,
|
||||
ownerAgentId: goal.owner_agent_id,
|
||||
});
|
||||
if (!plan) continue;
|
||||
ensureAgentHeartbeat(db, goal.org_id, plan.assigneeAgentId);
|
||||
const taskId = organization.createTask({
|
||||
orgId: goal.org_id,
|
||||
title: `Goal review: ${goal.title.trim()}`,
|
||||
description: goalReviewDescription(goal.title),
|
||||
status: TASK_STATUS.BACKLOG,
|
||||
assigneeAgentId: plan.assigneeAgentId,
|
||||
risk: RISK_LEVEL.MEDIUM,
|
||||
projectId: null,
|
||||
goalId: goal.id,
|
||||
workspaceId: plan.workspaceId,
|
||||
priority: TASK_PRIORITY.HIGH,
|
||||
requiresFilesystem: plan.workspaceId !== null,
|
||||
origin: "goal_review",
|
||||
});
|
||||
created.push(taskId);
|
||||
console.info(`[goal-review] all tasks for goal "${goal.title}" are finished; created a review checkpoint for the goal owner.`);
|
||||
const taskId = createReviewTask(db, organization, goal);
|
||||
if (taskId) created.push(taskId);
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
/** Closes a stalled goal's stuck review and starts a fresh one; returns the new review task id. */
|
||||
export function resumeGoal(db: DatabaseSync, organization: OrganizationService, orgId: string, goalId: string): string {
|
||||
const goal = db.prepare("select id, org_id, title, description, owner_agent_id, stalled_reason from goals where id = ? and org_id = ? and status = 'active'")
|
||||
.get(goalId, orgId) as (ReviewableGoalRow & { stalled_reason: string | null }) | undefined;
|
||||
if (!goal) throw new Error("Active goal was not found.");
|
||||
if (!goal.stalled_reason) throw new Error(`Goal "${goal.title}" is not stalled.`);
|
||||
db.prepare("update tasks set status = 'done', updated_at = ? where goal_id = ? and origin = 'goal_review' and status <> 'done'")
|
||||
.run(new Date().toISOString(), goalId);
|
||||
const taskId = createReviewTask(db, organization, goal);
|
||||
if (!taskId) throw new Error(`Goal "${goal.title}" has no active agent to review it.`);
|
||||
new AuditService(db).record({ orgId, entityType: "goal", entityId: goalId, actorType: "employee", actorId: null, action: "resumed", payload: { reviewTaskId: taskId } });
|
||||
return taskId;
|
||||
}
|
||||
|
||||
function createReviewTask(db: DatabaseSync, organization: OrganizationService, goal: ReviewableGoalRow): string | null {
|
||||
const plan = resolveGoalKickoff(db, {
|
||||
orgId: goal.org_id,
|
||||
goalTitle: goal.title,
|
||||
goalDescription: goal.description,
|
||||
ownerAgentId: goal.owner_agent_id,
|
||||
});
|
||||
if (!plan) return null;
|
||||
ensureAgentHeartbeat(db, goal.org_id, plan.assigneeAgentId);
|
||||
const taskId = organization.createTask({
|
||||
orgId: goal.org_id,
|
||||
title: `Goal review: ${goal.title.trim()}`,
|
||||
description: goalReviewDescription(goal.title),
|
||||
status: TASK_STATUS.BACKLOG,
|
||||
assigneeAgentId: plan.assigneeAgentId,
|
||||
risk: RISK_LEVEL.MEDIUM,
|
||||
projectId: null,
|
||||
goalId: goal.id,
|
||||
workspaceId: plan.workspaceId,
|
||||
priority: TASK_PRIORITY.HIGH,
|
||||
requiresFilesystem: plan.workspaceId !== null,
|
||||
origin: "goal_review",
|
||||
});
|
||||
db.prepare("update goals set stalled_reason = null where id = ?").run(goal.id);
|
||||
console.info(`[goal-review] created a review checkpoint for goal "${goal.title}".`);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/** Gives an active goal a visible stalled_reason when its review loop can no longer move on its own; returns how many stalled now. */
|
||||
export function markStalledGoals(db: DatabaseSync): number {
|
||||
const stalls = db.prepare(`
|
||||
select g.id, g.org_id, g.title,
|
||||
case when exists (
|
||||
select 1 from tasks r where r.goal_id = g.id and r.origin = 'goal_review' and r.status <> 'done' and r.stalled_reason is not null
|
||||
) then 'budget' else 'silent' end as kind
|
||||
from goals g
|
||||
where g.status = 'active' and g.stalled_reason is null
|
||||
and (
|
||||
exists (select 1 from tasks r where r.goal_id = g.id and r.origin = 'goal_review' and r.status <> 'done' and r.stalled_reason is not null)
|
||||
or (
|
||||
not exists (select 1 from tasks t where t.goal_id = g.id and t.status in ('backlog', 'in_progress', 'review'))
|
||||
and exists (select 1 from tasks r where r.goal_id = g.id and r.origin = 'goal_review')
|
||||
and not exists (
|
||||
select 1 from tasks t
|
||||
where t.goal_id = g.id and t.status = 'done' and t.origin is null
|
||||
and t.updated_at > (select max(r.created_at) from tasks r where r.goal_id = g.id and r.origin = 'goal_review')
|
||||
)
|
||||
)
|
||||
)
|
||||
`).all() as Array<{ id: string; org_id: string; title: string; kind: "budget" | "silent" }>;
|
||||
const audit = new AuditService(db);
|
||||
for (const goal of stalls) {
|
||||
const reason = goal.kind === "budget"
|
||||
? "Stalled: the goal review spent its run budget without a decision. Resume it to run a fresh review."
|
||||
: "Stalled: the last goal review neither closed the goal nor planned new work. Resume it to run a fresh review.";
|
||||
db.prepare("update goals set stalled_reason = ? where id = ?").run(reason, goal.id);
|
||||
console.warn(`[goal-review] goal "${goal.title}" ${reason}`);
|
||||
audit.record({ orgId: goal.org_id, entityType: "goal", entityId: goal.id, actorType: "system", actorId: null, action: "stalled", payload: { reason } });
|
||||
}
|
||||
return stalls.length;
|
||||
}
|
||||
|
||||
function goalReviewDescription(goalTitle: string): string {
|
||||
return [
|
||||
`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.`,
|
||||
|
||||
Reference in new issue
Block a user