Commit Graph
140 Commits
Author SHA1 Message Date
ZacharyZhang-NY 2bb2acd635 Grow a real OKR company: officers, departments, ranks - and a stable chart
Researched how OKR-run companies cascade (company objective -> C-suite
officers per function -> department objectives -> individual key
results) and encoded that shape into how the CEO builds the org.

- hire_agent gains seniority (director/lead/staff; c_suite stays
  reserved for the CEO's privileges) and a business-card title, so the
  CEO hires a CTO/CFO/CMO as directors instead of a flat pool of ICs
- CEO persona + goal kickoff now direct the OKR cascade: hire 2-4
  officers, hand each a department objective, let them build teams;
  department heads get their own doctrine (hire leads/staff, cascade
  key results as tasks, report up)
- Migration 050 adds Finance and Sales to every org's canonical
  departments (backfilled for existing DBs) so a real C-suite has
  somewhere to hire
- Org chart viewport is a fixed-height scroll region: zooming rescales
  the diagram inside it instead of resizing the card
- Unit test drives a live three-level chain: CEO -> CTO (director) ->
  Engineering Manager (lead) -> staff, with rank/title persisted
2026-07-08 21:47:07 -04:00
ZacharyZhang-NY e55b8b85b0 Make every identity editable: agent and operator names plus avatars
- Migration 049: avatar_emoji/avatar_color on agents and employees
- updateAgent accepts a new name (mention-name validated) and avatar;
  new updateEmployeeProfile renames the operator and sets their avatar
- Avatars are emoji-on-color tiles (renderer CSP blocks remote images);
  an AvatarDirectoryContext built from the snapshot lets the shared
  Avatar component honor custom looks at every call site - chat rows,
  conversation list, org chart, office card, sidebar - without
  threading props through pages
- Team edit dialog gains Name + avatar editor (CEO included); the
  sidebar foot opens an Edit-profile dialog for the operator
- e2e: rename an agent to a new mention handle with an emoji avatar,
  edit the operator profile, both verified in the live shell
2026-07-08 21:33:23 -04:00
ZacharyZhang-NY d404e1e72a Pin the UI locale to the language setting and add theme + language selectors with a dark palette 2026-07-07 12:28:06 -04:00
ZacharyZhang-NY 00e05054ca Mirror the Composio project's auth configs as the connector catalog with logo.dev icons 2026-07-07 11:06:07 -04:00
ZacharyZhang-NY a2de132083 Embed the Composio key in the app bundle instead of shipping cred.txt
Root cause of the recurring 401: a revoked key was hardcoded in
startup-recovery.ts and onboarding-service.ts and reseeded into
composio_settings on every launch, and the interim fix shipped cred.txt
as a visible file under resources/.

- electron.vite.config.ts reads the gitignored cred.txt at build time
  and injects the key into the main-process bundle as
  __COMPOSIO_API_KEY__; no credential file is packaged and the key is
  not user-visible or configurable. The renderer bundle never sees it.
- Delete both hardcoded-key seeders; migration 046 purges stored
  api_key and never-read toolkits rows while keeping entity_id and
  auth_config state.
- Stop shipping composio_settings in the renderer snapshot.
2026-07-07 01:40:19 -04:00
ZacharyZhang-NY 07d0b4587d Promote AgentCompany-New to the repository root and retire the old app
The legacy application - its src, tests, landing site, promo assets, docs,
release tooling, CI workflows, and root configs - leaves the tree; it stays
reachable in history. Everything under AgentCompany-New moves up unchanged:
same package name, so the production userData/database path is untouched,
and e2e/build scripts already resolved paths relative to the project. The
developer cred.txt stays gitignored at its new location. Full verify:ci
passes from the new root.
2026-07-07 00:04:13 -04:00
ZacharyZhang-NY 810e2d5967 update 2026-05-15 19:40:20 -04:00
ZacharyZhang-NY b92cf62c4c fix(data): canonicalize operation json fields 2026-05-15 02:36:19 -04:00
ZacharyZhang-NY 66e24b1060 fix(security): scope managed path opening 2026-05-15 00:26:20 -04:00
ZacharyZhang-NY bde35c3518 fix(security): scope social account references 2026-05-15 00:15:37 -04:00
ZacharyZhang-NY 51d468e4c3 fix(security): scope work product creation 2026-05-15 00:00:04 -04:00
ZacharyZhang-NY d1a3d23c70 fix(security): scope skill assignments by company 2026-05-14 23:37:58 -04:00
ZacharyZhang-NY 41e1dec647 fix(security): enforce company scoped updates 2026-05-14 23:28:29 -04:00
ZacharyZhang-NY a532120649 feat(connectors): add Ollama and LM Studio as a local auth kind
Both servers expose OpenAI-compatible /v1/chat/completions and /v1/models,
so we route them through pi-ai's `openai-completions` API the same way
Fireworks does — but with a per-installation base URL the user enters
during onboarding instead of a hardcoded one. The shared schemas grow a
`local` credential payload carrying just the baseUrl; the credential
resolver surfaces it on `ResolvedCredential.baseUrl`, the runtime bridge
forwards it through `prepareRun`, and the worker plumbs it into
`resolveModel` so the streamed Model<openai-completions> targets the
user's server. The local-cost path short-circuits to zero in cost-writer
since users bill their own hardware, and the live model list is fetched
from the saved baseUrl by `fetchLocalConnectorModels` (Ollama and LM
Studio both speak the standard OpenAI catalog shape). Onboarding ships
a new LocalProviderCard and the Settings panel a LocalForm; both default
to the catalog's per-connector localhost URL and accept any custom URL.
2026-05-14 14:55:38 -04:00
ZacharyZhang-NY 0e1710065f fix(messaging): self-read receipt at send time so authors don't inflate unread counts
agent_message_reads is the per-viewer source of truth for readAt, but
the send path never recorded the author's own receipt. Every message
the board operator or an agent sent therefore counted as unread to
themselves until they happened to re-open the channel — sidebar badge,
inbox panel, and the channel sidebar all derive from the same join
table. Insert a (message_id, from_agent_id, created_at) row in the
same transaction as the message itself; the receipt becomes an
atomic part of the write rather than a deferred maintenance task.
2026-05-14 14:36:10 -04:00
ZacharyZhang-NY 1e36259c5d fix(orchestration): enforce approval gates on heartbeat and recovery paths
The heartbeat path (canRunAgent → startAgentHeartbeatRun) only checked
isAgentPendingApproval (hire gate), completely bypassing plan_review,
draft_review, strategy_review and other blocking approval types. Agents
in Co-Founder mode continued running despite pending approvals.

Root cause: task-runner.prepareTaskRun had the full getBlockingApproval
check, but heartbeat runs never went through prepareTaskRun — they
created runs directly, skipping the approval gate entirely.

Three fixes:
- heartbeat.canRunAgent: add getBlockingApproval(companyId, agentId, null)
  for agent-level blocking approvals
- heartbeat.startAgentHeartbeatRun: add getBlockingApproval with task id
  after task resolution, before run creation
- task-runner.resumeQueuedRuns: add getBlockingApproval check so app
  restart does not dispatch runs past pending approvals

Also replaced the hardcoded type list in hasPendingReviewApprovalForAgent
with a dynamic join on approval_policies.blocks_execution, eliminating
the sync risk when new blocking approval types are added.
2026-04-29 20:17:33 -04:00
ZacharyZhang-NY c9deca5454 fix(database): project only live active runs 2026-04-29 00:10:23 -04:00
ZacharyZhang-NY 7dbaaf0022 fix(worker): recover tasks after worker crash 2026-04-28 23:37:24 -04:00
ZacharyZhang-NY 49ea508ee1 fix(scheduler): drain pending wakeup requests 2026-04-28 20:26:29 -04:00
ZacharyZhang-NY 749ad3a231 fix(scheduler): wake agents with assigned todo work 2026-04-28 20:16:08 -04:00
ZacharyZhang-NY 31aac7c0de fix(messages): normalize escaped markdown newlines 2026-04-24 10:35:11 -04:00
ZacharyZhang-NY eabd304a45 fix(database): normalize baseline schema ddl 2026-04-24 09:39:44 -04:00
ZacharyZhang-NY 9ee1425a71 refactor(approvals): move execution policy into database 2026-04-24 00:33:56 -04:00
ZacharyZhang-NY f8eb57ad8c refactor(database): move task runtime ownership to leases 2026-04-24 00:18:51 -04:00
ZacharyZhang-NY 1f4af89be0 refactor(database): squash schema migrations to v56 baseline 2026-04-24 00:00:13 -04:00
ZacharyZhang-NY 67ccfc1160 fix: enforce work product taxonomy 2026-04-23 22:44:41 -04:00
ZacharyZhang-NY d2e94959e0 fix: make run log creation atomic 2026-04-23 22:14:01 -04:00
ZacharyZhang-NY 88c905c381 fix: make artifact projection durable 2026-04-23 21:50:49 -04:00
ZacharyZhang-NY 84c00e0011 fix: validate and compact session entries 2026-04-23 21:21:59 -04:00
ZacharyZhang-NY c850d70bfc fix: enforce work product primary invariant 2026-04-23 21:08:07 -04:00
ZacharyZhang-NY a4af4a866a Prevent self-directed agent messages 2026-04-22 20:28:49 -04:00
ZacharyZhang-NY d13c9d83e8 Resolve filesystem work product targets by existing file 2026-04-22 20:06:01 -04:00
ZacharyZhang-NY fa006524c2 refactor(runtime): centralize lifecycle constants 2026-04-22 16:58:36 -04:00
ZacharyZhang-NY 775598ef4c fix(db): make task row state authoritative 2026-04-22 16:24:57 -04:00
ZacharyZhang-NY 3aaa64ddfd refactor(runtime): enforce source file size boundaries 2026-04-22 15:56:09 -04:00
ZacharyZhang-NY eefb515aa2 fix(runtime): classify run usage status 2026-04-22 15:41:19 -04:00
ZacharyZhang-NY 43583ef30b fix(runtime): gate cofounder checkpoints 2026-04-22 14:05:26 -04:00
ZacharyZhang-NY 7a1dd647aa fix(api): make task updates transactional 2026-04-22 14:00:24 -04:00
ZacharyZhang-NY 19f8494d09 fix(db): make filesystem lock intent explicit 2026-04-22 13:51:41 -04:00
ZacharyZhang-NY 0785c9ba47 fix(db): make task checkout atomic 2026-04-22 13:38:54 -04:00
ZacharyZhang-NY 9fba8dd6c3 fix(projector): stop writing null workspace_id on work_products
Two sites in upsertFilesystemArtifact leaked null workspace_ids
into the work_products table and produced the orphan rows the read
path now has to heal:

1. Create path unconditionally forwarded ctx.workspaceId, which was
   null whenever a run was created without a workspace binding
   (historically cognitive runs, or runs that lost their link through
   the observationPath regression fixed in 1f2f977).

2. Update path passed {workspaceId: ctx.workspaceId} as part of the
   patch. updateWorkProduct treats any key present in patch as "write
   this value", so a run touching a previously-well-linked row with
   ctx.workspaceId=null would clobber the existing workspace_id back
   to null — actively destroying data.

Resolve both at the projector layer:

  - Add workspacesMod.findWorkspaceIdByLocalPath(companyId, localPath)
    as a reverse-lookup. workspacePath is the argument the projector
    is already walking; it came from some workspace row's local_path,
    so finding that row by path is deterministic.
  - Compute resolvedWorkspaceId = ctx.workspaceId ?? reverseLookup(...)
    once, and use it for both the identity lookup
    (findWorkProductByExternalId) and the subsequent write.
  - On the update path, only include workspaceId in the patch when
    resolvedWorkspaceId is non-null. Null now means "no info — leave
    the existing column alone" instead of "clobber to null".

Together with the read-path heal in cfb8ba5 this closes the loop:
new files never enter the table with a null workspace_id if we can
possibly resolve it, old files keep their links, and truly unlinkable
rows still fall back to the observation workspace on open.

Typecheck and build:verify clean.
2026-04-21 14:52:38 -04:00
ZacharyZhang-NY cfb8ba5dc1 fix(docs): heal orphan work_products by falling back to observation workspace
Work-product rows written before the artifact projector tracked
workspace_id end-to-end can lose the link through all three joins:
workspace_id is NULL, task_id is NULL, and the row's run exists but
its runs.workspace_id is also NULL. Those orphans were readable in
the DocumentCenter (the listing doesn't need a workspace root) but
could not be opened — resolveWorkProductTarget returned null and the
toast now reports workspace_unresolved.

Add a fourth resolution tier in resolveWorkProductTarget: if the
three-step chain yields nothing, look up the company's observation
workspace (primary first, then oldest) and check whether the
external_id resolves to a real file inside that root. If it does,
return that absolute path and the row opens. If the file isn't
there, the error falls through with an extended detail line naming
the observation root that was tried, so the operator sees exactly
why the heal also failed.

This is deterministic — exactly one fallback workspace — and
side-effect-free on the read path: the row itself is left alone,
no stealth write-back. A follow-up commit will stop the projector
from writing NULL workspace_id in the first place.

Typecheck and build:verify clean.
2026-04-21 14:51:06 -04:00
ZacharyZhang-NY 390f76fa76 fix(docs): split work-product open errors into actionable reasons
The previous handler folded every null target into one error code
("WORK_PRODUCT_NOT_FOUND: Work product not found or its workspace
is unresolved"). When a user couldn't open a file we had no way to
tell whether the row was archived, the provider/external_id was
empty, the workspace had no local_path, or the external_id escaped
the root.

Widen resolveWorkProductTarget's return from (target | null) to
(target | error) where error carries one of five reasons:

  not_found             row missing for id+company
  archived              status == "archived" tombstone
  no_target             provider != filesystem and no url
  workspace_unresolved  all three workspace_id / task_id / run_id
                         lookups returned no local_path
  path_escape           external_id is absolute or steps outside root

Each reason includes the concrete detail (ids checked, paths tried)
so the toast on the renderer side tells the user exactly which slot
is null without having to open the DB.

The IPC handler maps each reason to its own failure code and passes
the detail through, so WorkProductRow's toast now reads e.g.
"workspace_id=null task_id=null run_id=null — none led to a
workspace with a local_path" instead of a generic unresolved.

Typecheck and build:verify clean.
2026-04-21 14:45:59 -04:00
ZacharyZhang-NY 1f2f9770a6 Hotfix: separate workspace observation from lock claim — restore artifact projector for cognitive runs
Production regression of T1 (7829acc..15e3863). User reports "明明生成
了很多文件,但只有一个" — the filesystem shows 19 markdown deliverables
in ~/Documents/Github/test/{business-plan,operations,ceo-coordination}/
but the DocumentCenter surfaces only one row.

Root cause is structural, predicted in spirit by Karpathy's Round 1
take ("把'认知'和'执行'两件事一起锁是数据结构错"). The T1 rewrite
collapsed two concerns into one nullable field on the run context:

  workspacePath = workspace?.localPath ?? null
                  ^^^^^^^^^                ^^^^
                  lock target              observation root

For lockMode='none' (every cognitive C-suite agent post-T1) the
resolver returns workspace=null, so workspacePath was null, so
artifact-projector.ts:242 short-circuited every projection with
`{ kind: "skipped", reason: "no_workspace" }`. The agents kept
writing files via bash (cwd falls back to the company default
workspace at worker/runtime/index.ts:142), but nothing observed
them — the DocumentCenter only saw the one file that was projected
back when the agent still held an exclusive workspace.

Fix: split the two concerns. WorkspaceResolution gains a non-lock
`observationPath` field; the resolver populates it from the company
primary workspace (new `getCompanyObservationWorkspace` query —
primary first, oldest second, null only when company has zero
workspace rows) for lockMode='none' runs, and from `workspace.localPath`
itself for lockMode='exclusive' runs. RunDispatchParams threads the
field through; turn-context's `workspacePath` reads it first.
artifact-projector.ts is unchanged — it just sees a non-null path
again and resumes baselining, diffing, and upserting work_products
rows the way it did pre-T1.

Three new unit tests pin the regression: cognitive role with
workspace ⇒ observationPath set; cognitive role with no workspace ⇒
observationPath null; exclusive ⇒ observationPath equals
workspace.localPath. typecheck green, 543/543 unit tests, 10/10
eval. No fallback, no mock, no silent degradation.
2026-04-20 16:22:40 -04:00
ZacharyZhang-NY 15e3863cba saveTask: accept requires_filesystem; opt in for the onboarding kickoff task
- saveTask now takes an optional requiresFilesystem boolean. Omitted
  → column keeps its current value on updates, defaults to 0 on
  inserts. Explicit true/false → written through.
- Onboarding kickoff (the CEO's very first task) sets
  requiresFilesystem=true. The task description already tells the
  agent to list directories and read README/package.json — that is
  filesystem work, so the task must hold the workspace exclusive.
- Other saveTask call sites (automation reassign, routes-tasks patch,
  create_task, meetings) stay implicit — update paths preserve the
  existing column value, new user-created tasks default to 0 (safe
  cognitive default) and can be opted in later by the creator.
2026-04-20 15:36:08 -04:00
ZacharyZhang-NY 28cdf7c56f Rewrite resolveWorkspace to per-task lockMode inference; drop company_fallback silent share (B4-adjacent)
Old resolver: task → agent → project → any company workspace, all
marked exclusive. That fourth step folded every runnable agent in a
multi-agent company onto a single workspace row and serialised the
C-suite (four agents, one exclusive lock, three starved).

New resolver: lockMode='exclusive' is now an opt-in decision inferred
per task. Three rules:

  1. task.workspaceId set → exclusive on that workspace (explicit).
  2. filesystem signal (task.requiresFilesystem OR agent in a
     coding role/engineering department) → exclusive on
     agent.workspaceId / project primary; throws if neither exists.
  3. otherwise → none / null. Pure cognitive work runs with no
     workspace lock and fully in parallel.

Removed:
  - company_fallback silent-share path. Filesystem-touching runs
    with no legitimate binding now throw WorkspaceConfigurationError
    at resolve time so the caller decides recovery rather than the
    resolver picking a random shared workspace.
  - workspacesMod.getAnyCompanyWorkspace (now dead code).

Invariants now enforced by types:
  - lockMode='exclusive' ⇒ workspace is a live WorkspaceRecord.
  - lockMode='none'      ⇒ workspace is null.

Tests updated to cover the new decision table and the four-non-
coding-agents parallel case that was the original bug.
2026-04-20 15:32:01 -04:00
ZacharyZhang-NY 7829acc7f0 Add tasks.requires_filesystem column (v52) — per-task filesystem lock policy
Before v52, resolveWorkspaceWithLockMode walked a four-step fallback
chain (task → agent → project → any company workspace) and marked
every resolved row exclusive. In a multi-agent company every heartbeat
collapsed onto the same company workspace row, every run claimed the
same exclusive lock, and only one agent ever executed at a time.

Root cause is a missing column: per-task 'does this task touch the
filesystem?' had no place to live, so the orchestrator defaulted to
yes on everything. v52 gives that question its own column.

Backfill is conservative:
  - in-flight tasks (in_progress + active_run_id) → 1 (preserve
    the lease the dispatcher already holds)
  - todo/backlog tasks whose assignee is an engineering-flavoured
    role (CTO/engineer/developer/devops/sre or department =
    engineering) → 1
  - everything else → 0

TaskRecord now exposes requiresFilesystem on both entityCrud.getTask
and the snapshot read path.
2026-04-20 15:25:36 -04:00
ZacharyZhang-NY b628b49016 Hotfix B2 Phase 1: drop task_leases.run_id FK (v51 migration)
Production bug on launch — user reports UI 'FOREIGN KEY constraint
failed' on 启动公司 and the main-process log loops forever with
'[heartbeat] CEO not ready: checkout_race_lost'. Both trace to the
same root cause introduced by v50 (e2c839b..2656684 B2 Phase 1):

  task_leases.run_id TEXT NOT NULL REFERENCES runs(id)

The orchestration sequence contradicts this FK. In both heartbeat.ts
(line 165) and task-runner.ts (line 91), the caller generates a fresh
runId = randomUUID() and immediately calls db.tasks.checkoutTask
BEFORE the corresponding row is inserted into the runs table. The
legacy tasks.checkout_run_id column has always been 'dangling
allowed' — the run row saves later when task-runner.ts opens the
run record. v50's FK tightened the shadow lease contract without
coordinating with that insertion sequence, so every checkoutTask
attempt now rolls back with a FK violation, takes the whole CAS
transaction with it, returns changes=0, and the heartbeat reports
checkout_race_lost. The task never advances — the exact orphan
symptom B2 was supposed to eliminate.

Fix: migration v51 recreates task_leases WITHOUT the run_id FK.

- task_id FK + ON DELETE CASCADE stays (a lease for a non-existent
  task is still meaningless)
- UNIQUE(task_id, run_id) stays (still a correctness constraint)
- run_id loses the FK — it mirrors the legacy checkout_run_id's
  loose semantics, which is exactly what Phase 1 dual-write is
  supposed to mirror
- Existing data preserved verbatim via INSERT SELECT
- Uses PRAGMA defer_foreign_keys=ON inside the transaction (the
  pragma is valid inside a txn; regular foreign_keys=OFF would not
  be since runMigrations already opened BEGIN IMMEDIATE)

Post-v51 the run_id column is still NOT NULL and the shadow probe
(ab4afba) will still surface missing runs as divergence — the FK
tightening is a Phase 2 concern coordinated with the saveRun
insertion order, not a Phase 1 shadow-write concern.

typecheck green, 529 unit tests green, 8/8 eval green.
2026-04-20 14:44:59 -04:00
ZacharyZhang-NY 54a97f67ac B2 Phase 1 follow-up: plug lease orphan gap in advanceTaskStatus + scope interruptStaleRuns cleanup
Linus review T1 surfaced three real bugs in the B2 Phase 1 shadow
dual-write surface. Fix them in one pass.

1. advanceTaskStatus left task_leases behind on terminal (done /
   cancelled / blocked) and "return to queue" (todo / backlog)
   transitions. The status->phase trigger advanced tasks.phase but the
   lease row lingered as an orphan. dev-mode verifyPhaseConsistency
   could see the divergence; production silently drifted. Wrap the
   status UPDATE and an explicit DELETE FROM task_leases in a single
   transaction so no caller observes the intermediate state.

2. The no-restricted-syntax guard in eslint.config.mjs only matched
   literals shaped "UPDATE tasks SET status" -- advanceTaskStatus's
   dynamic `UPDATE tasks SET ${timestampSets.join(", ")}` slipped past,
   and so did saveTask / recoverStaleLocks (status not first column).
   Tighten the selectors to (a) match any status-write within UPDATE
   tasks regardless of column order, and (b) flag template literals
   whose head ends with `SET ` so dynamic helpers must carry an
   explicit eslint-disable with rationale. Five grandfathered sites
   now carry matching disables.

3. interruptStaleRuns scoped its task+lease cleanup with
   `WHERE finished_at = ?` against the freshly stamped timestamp --
   safe in isolation, vulnerable to ISO-millisecond ties with other
   already-interrupted runs. Capture the stale run ids before
   mutating anything and drive every downstream statement from that
   explicit id list. Original semantics preserved; scoping is exact.

Unit tests added in tests/unit/tasks-phase.test.ts cover the
advanceTaskStatus lease cleanup across done / cancelled / blocked /
todo, and pin the in_review case as "lease stays".
2026-04-20 13:51:53 -04:00
ZacharyZhang-NY 118e3d8795 Remove final AppDatabase pass-through methods with ESLint guard (C1 part 3/3)
Migrate all remaining pass-through call sites (social, skills, budget,
entityCrud, documents, knowledge, automation, wakeup, backup, threads,
settings, snapshot, connectors, workspaces, activity) to their owning
domain modules. `AppDatabase` now carries only three methods:

  - createHireRequest (multi-module composite: agents + approvalsMod)
  - rollbackConfigRevision (multi-module composite: documents + entityCrud)
  - queryAll (raw-SQL escape hatch, @internal)

The vault/env-binding infrastructure previously passed argument-by-
argument into `entityCrud.saveSecret`/`resolveSecretEnv` now binds at
module construction, letting callers invoke `db.entityCrud.saveSecret({...})`
without carrying vault plumbing.

Structural types that narrowed AppDatabase to a bag of flat methods
(`ConnectorModelPolicyDeps` callers, `CapabilityMatchDb`, test mocks in
budget-enforcement / budget-incidents / task-quality-gates /
failure-escalation / task-quality-gates-sweep) are reshaped to the new
namespace. `connectorPolicyDepsFromDb` replaces the old pattern of
handing the whole AppDatabase to `validateConnectorModelSelection`.

A new ESLint rule in eslint.config.mjs blocks new top-level methods on
AppDatabase so the pass-through layer cannot silently grow back.
`AppDatabase.ts` dropped from 193 lines to 58; `AppDatabaseBase` lost
the 270-line trailing pass-through block.
2026-04-20 13:31:39 -04:00