Q160: a goal's pace is measured against its target date, and claimed only when it has one

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 02:44:20 -04:00
1 parent 459dcf0ea2
commit c9117fe46d
18 files changed
+172 -24

No files matched your search

+2 -1
View File
@@ -54,7 +54,8 @@ export const organizationContracts = {
]),
updateTaskStatus: z.strictObject({ orgId: id, taskId: id, status: z.enum(TASK_STATUS) }) satisfies z.ZodType<C.UpdateTaskStatusInput>,
runTask: z.strictObject({ orgId: id, taskId: id }) satisfies z.ZodType<C.RunTaskInput>,
createGoal: z.strictObject({ orgId: id, title: z.string(), description: z.string(), ownerAgentId: nullableId }) satisfies z.ZodType<C.CreateGoalInput>,
createGoal: z.strictObject({ orgId: id, title: z.string(), description: z.string(), ownerAgentId: nullableId, dueAt: z.string().nullable().optional() }) satisfies z.ZodType<C.CreateGoalInput>,
setGoalDueDate: z.strictObject({ orgId: id, goalId: id, dueAt: z.string().nullable() }) satisfies z.ZodType<C.SetGoalDueDateInput>,
resumeGoal: z.strictObject({ orgId: id, goalId: id }) satisfies z.ZodType<C.ResumeGoalInput>,
createProject: z.strictObject({
orgId: id, goalId: nullableId, name: z.string(), description: z.string(), leadAgentId: nullableId,
+1
View File
@@ -183,6 +183,7 @@ export function registerIpcHandlers(deps: IpcDeps): void {
}
});
handle("createGoal", (input) => mutate(() => organization.createGoal(input)));
handle("setGoalDueDate", (input) => mutate(() => organization.setGoalDueDate(input)));
handle("createProject", (input) => mutate(() => organization.createProject(input)));
handle("createWorkspace", (input) => mutate(() => organization.createWorkspace(input)));
handle("createDocument", (input) => mutate(() => documents.create(input)));
@@ -0,0 +1,7 @@
// When a goal is planned to be reached; without it, progress is reported but no pace is claimed.
export const goalDueDateSchema = `
alter table goals add column due_at text;
insert into schema_migrations(version, name, applied_at)
values (102, 'goal_due_date', datetime('now'));
`;
+2
View File
@@ -100,6 +100,7 @@ import { dropAuditReplaysSchema } from "./migrations/098-drop-audit-replays";
import { dropRollbackRecordsSchema } from "./migrations/099-drop-rollback-records";
import { chatReplyRequestsSchema } from "./migrations/100-chat-reply-requests";
import { taskBlockedSchema } from "./migrations/101-task-blocked";
import { goalDueDateSchema } from "./migrations/102-goal-due-date";
interface Migration {
version: number;
@@ -209,6 +210,7 @@ const MIGRATIONS: Migration[] = [
{ version: 99, name: "drop_rollback_records", sql: dropRollbackRecordsSchema },
{ version: 100, name: "chat_reply_requests", sql: chatReplyRequestsSchema },
{ version: 101, name: "task_blocked", sql: taskBlockedSchema },
{ version: 102, name: "goal_due_date", sql: goalDueDateSchema },
];
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
+22 -4
View File
@@ -2,7 +2,7 @@ import type { TaskDeleteImpact } from "../../shared/domain";
import type { DatabaseSync } from "node:sqlite";
import { APPROVAL_STATE, RISK_LEVEL, TASK_STATUS, WRITING_MODE, type RiskLevel } from "../../shared/domain";
import { createId } from "../../shared/ids";
import type { CreateAgentInput, CreateGoalInput, CreateOrganizationInput, CreateProjectInput, CreateTaskInput, CreateWorkspaceInput, DeleteTaskInput, SetTaskBlockedInput, UpdateAgentInput, UpdateEmployeeProfileInput, UpdateTaskInput } from "../../shared/commands";
import type { CreateAgentInput, CreateGoalInput, CreateOrganizationInput, CreateProjectInput, CreateTaskInput, CreateWorkspaceInput, DeleteTaskInput, SetGoalDueDateInput, SetTaskBlockedInput, UpdateAgentInput, UpdateEmployeeProfileInput, UpdateTaskInput } from "../../shared/commands";
import { AuditService } from "./audit-service";
import { ensureAgentHeartbeat, resolveGoalKickoff, type GoalKickoffPlan } from "./goal-kickoff";
import { PolicyService } from "./policy-service";
@@ -359,9 +359,9 @@ export class OrganizationService {
const now = new Date().toISOString();
if (input.ownerAgentId) this.verifyOptionalOrgEntity("agents", input.ownerAgentId, input.orgId, "Owner agent");
this.db.prepare(`
insert into goals(id, org_id, title, description, status, owner_agent_id, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, input.orgId, input.title.trim(), input.description.trim(), "active", input.ownerAgentId, now, now);
insert into goals(id, org_id, title, description, status, owner_agent_id, due_at, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, input.orgId, input.title.trim(), input.description.trim(), "active", input.ownerAgentId, validDueAt(input.dueAt ?? null), now, now);
this.audit.record({
orgId: input.orgId,
entityType: "goal",
@@ -477,6 +477,14 @@ export class OrganizationService {
return this.tasks.deleteImpact(orgId, taskId);
}
/** Sets or clears when a goal is planned to be reached. */
setGoalDueDate(input: SetGoalDueDateInput): void {
const result = this.db.prepare("update goals set due_at = ?, updated_at = ? where id = ? and org_id = ?")
.run(validDueAt(input.dueAt), new Date().toISOString(), input.goalId, input.orgId);
if (result.changes === 0) throw new Error("Goal was not found.");
this.audit.record({ orgId: input.orgId, entityType: "goal", entityId: input.goalId, actorType: "employee", actorId: null, action: "due_date_set", payload: { dueAt: input.dueAt } });
}
setTaskBlocked(input: SetTaskBlockedInput): void {
this.tasks.setBlocked(input);
}
@@ -493,3 +501,13 @@ export class OrganizationService {
return this.tasks.agentSetStatus(input);
}
}
/** A target date is an exact ISO UTC time on a real calendar day; anything else is refused rather than stored shifted. */
function validDueAt(dueAt: string | null): string | null {
if (dueAt === null) return null;
const exact = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(dueAt);
const time = new Date(dueAt);
const normalized = dueAt.length === 20 ? dueAt.replace("Z", ".000Z") : dueAt;
if (!exact || Number.isNaN(time.getTime()) || time.toISOString() !== normalized) throw new Error(`"${dueAt}" is not a date.`);
return normalized;
}