Q165: the company's profile and how it runs can be changed after onboarding

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 03:20:39 -04:00
1 parent 93c5eb2d71
commit 20c37362f3
15 files changed
+208 -37

No files matched your search

@@ -56,6 +56,10 @@ export const organizationContracts = {
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, 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>,
updateOrganization: z.strictObject({
orgId: id, name: z.string(), description: z.string(), mission: z.string(), goal: z.string(),
executionMode: z.enum(["ai_ceo", "ai_cofounder"]), autoApproveHires: z.boolean(), reviewDeliverables: z.boolean(),
}) satisfies z.ZodType<C.UpdateOrganizationInput>,
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
@@ -184,6 +184,7 @@ export function registerIpcHandlers(deps: IpcDeps): void {
});
handle("createGoal", (input) => mutate(() => organization.createGoal(input)));
handle("setGoalDueDate", (input) => mutate(() => organization.setGoalDueDate(input)));
handle("updateOrganization", (input) => mutate(() => organization.updateOrganization(input)));
handle("createProject", (input) => mutate(() => organization.createProject(input)));
handle("createWorkspace", (input) => mutate(() => organization.createWorkspace(input)));
handle("createDocument", (input) => mutate(() => documents.create(input)));
+23
View File
@@ -0,0 +1,23 @@
import type { DatabaseSync } from "node:sqlite";
/** The global handbrake: while paused, no run is queued or claimed (one in flight finishes); it survives restarts. */
export function setAutonomousPaused(db: DatabaseSync, paused: boolean): void {
db.prepare(`
insert into app_state(id, active_org_id, autorun_paused, updated_at)
values (1, null, ?, ?)
on conflict(id) do update set autorun_paused = excluded.autorun_paused, updated_at = excluded.updated_at
`).run(paused ? 1 : 0, new Date().toISOString());
}
/** The app-wide theme and language, kept for every company. */
export function updateUiPreferences(db: DatabaseSync, input: { theme?: "system" | "light" | "dark"; language?: "en" | "zh" }): void {
const current = db.prepare("select theme, language from app_state where id = 1")
.get() as { theme: string; language: string } | undefined;
const theme = input.theme ?? current?.theme ?? "system";
const language = input.language ?? current?.language ?? "en";
db.prepare(`
insert into app_state(id, active_org_id, theme, language, updated_at)
values (1, null, ?, ?, ?)
on conflict(id) do update set theme = excluded.theme, language = excluded.language, updated_at = excluded.updated_at
`).run(theme, language, new Date().toISOString());
}
+21
View File
@@ -0,0 +1,21 @@
import type { DatabaseSync } from "node:sqlite";
import type { SetGoalDueDateInput } from "../../shared/commands";
import { AuditService } from "./audit-service";
/** Sets or clears when a goal is planned to be reached. */
export function setGoalDueDate(db: DatabaseSync, input: SetGoalDueDateInput): void {
const result = 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.");
new AuditService(db).record({ orgId: input.orgId, entityType: "goal", entityId: input.goalId, actorType: "employee", actorId: null, action: "due_date_set", payload: { dueAt: input.dueAt } });
}
/** A target date is an exact ISO UTC time on a real calendar day; anything else is refused rather than stored shifted. */
export 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;
}
+28
View File
@@ -0,0 +1,28 @@
import type { DatabaseSync } from "node:sqlite";
import type { UpdateOrganizationInput } from "../../shared/commands";
import { AuditService } from "./audit-service";
/** Changes the profile and how the company runs; requests already waiting keep their state, new work follows the new settings. */
export function updateOrganizationProfile(db: DatabaseSync, input: UpdateOrganizationInput): void {
const name = input.name.trim();
if (!name) throw new Error("The company needs a name.");
const before = db.prepare(`
select name, description, mission, goal, execution_mode, auto_approve_hires, review_deliverables from organizations where id = ?
`).get(input.orgId) as { name: string; description: string; mission: string; goal: string; execution_mode: string; auto_approve_hires: number; review_deliverables: number } | undefined;
if (!before) throw new Error("Organization was not found.");
const next = {
name, description: input.description.trim(), mission: input.mission.trim(), goal: input.goal.trim(),
executionMode: input.executionMode, autoApproveHires: input.autoApproveHires, reviewDeliverables: input.reviewDeliverables,
};
const previous = {
name: before.name, description: before.description, mission: before.mission, goal: before.goal,
executionMode: before.execution_mode, autoApproveHires: before.auto_approve_hires === 1, reviewDeliverables: before.review_deliverables === 1,
};
const changed = (Object.keys(next) as Array<keyof typeof next>).filter((key) => next[key] !== previous[key]);
db.prepare(`
update organizations set name = ?, description = ?, mission = ?, goal = ?, execution_mode = ?, auto_approve_hires = ?, review_deliverables = ?
where id = ?
`).run(next.name, next.description, next.mission, next.goal, next.executionMode, next.autoApproveHires ? 1 : 0, next.reviewDeliverables ? 1 : 0, input.orgId);
console.info(`[organization] ${input.orgId} updated: ${changed.join(", ") || "nothing changed"}`);
new AuditService(db).record({ orgId: input.orgId, entityType: "organization", entityId: input.orgId, actorType: "employee", actorId: null, action: "updated", payload: { changed, ...Object.fromEntries(changed.map((key) => [key, next[key]])) } });
}
+15 -36
View File
@@ -1,9 +1,12 @@
import { setAutonomousPaused, updateUiPreferences } from "./app-state";
import { updateOrganizationProfile } from "./organization-profile";
import { setGoalDueDate, validDueAt } from "./goal-due-date";
import type { StatusCause } from "./task-mutations";
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, SetGoalDueDateInput, SetTaskBlockedInput, UpdateAgentInput, UpdateEmployeeProfileInput, UpdateTaskInput } from "../../shared/commands";
import type { CreateAgentInput, CreateGoalInput, CreateOrganizationInput, CreateProjectInput, CreateTaskInput, CreateWorkspaceInput, DeleteTaskInput, SetGoalDueDateInput, SetTaskBlockedInput, UpdateAgentInput, UpdateOrganizationInput, UpdateEmployeeProfileInput, UpdateTaskInput } from "../../shared/commands";
import { AuditService } from "./audit-service";
import { ensureAgentHeartbeat, resolveGoalKickoff, type GoalKickoffPlan } from "./goal-kickoff";
import { PolicyService } from "./policy-service";
@@ -33,6 +36,12 @@ export class OrganizationService {
return seedOrganization(this.db, input);
}
updateOrganization(input: UpdateOrganizationInput): void {
updateOrganizationProfile(this.db, input);
}
setActiveOrganization(orgId: string): void {
const row = this.db.prepare("select id from organizations where id = ?").get(orgId) as { id: string } | undefined;
if (!row) throw new Error("Organization was not found.");
@@ -53,30 +62,12 @@ export class OrganizationService {
});
}
/**
* The global handbrake: while paused, the autonomous queue stops enqueuing
* and claiming runs (the run already in flight finishes). Survives
* restarts, so a runaway conversation stays stopped until the operator
* resumes it.
*/
setAutonomousPaused(paused: boolean): void {
this.db.prepare(`
insert into app_state(id, active_org_id, autorun_paused, updated_at)
values (1, null, ?, ?)
on conflict(id) do update set autorun_paused = excluded.autorun_paused, updated_at = excluded.updated_at
`).run(paused ? 1 : 0, new Date().toISOString());
setAutonomousPaused(this.db, paused);
}
updateUiPreferences(input: { theme?: "system" | "light" | "dark"; language?: "en" | "zh" }): void {
const current = this.db.prepare("select theme, language from app_state where id = 1")
.get() as { theme: string; language: string } | undefined;
const theme = input.theme ?? current?.theme ?? "system";
const language = input.language ?? current?.language ?? "en";
this.db.prepare(`
insert into app_state(id, active_org_id, theme, language, updated_at)
values (1, null, ?, ?, ?)
on conflict(id) do update set theme = excluded.theme, language = excluded.language, updated_at = excluded.updated_at
`).run(theme, language, new Date().toISOString());
updateUiPreferences(this.db, input);
}
deleteOrganization(orgId: string): void {
@@ -478,12 +469,10 @@ 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 } });
setGoalDueDate(this.db, input);
}
setTaskBlocked(input: SetTaskBlockedInput): void {
@@ -502,13 +491,3 @@ 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;
}