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:
1 parent
459dcf0ea2
commit
c9117fe46d
18 files changed
+172
-24
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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'));
|
||||
`;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user