Commit Graph
942 Commits
Author SHA1 Message Date
ZacharyZhang-NY a99f5e581f Fix stuck selectedEntityId and unread-during-search edge cases
Two small but user-visible UX issues in the Communication hub:

1. selectedEntityId was only consumed when the id resolved to a
   visible message / comment / task channel. When the referenced
   entity was deleted or filtered out, the pointer stuck and the
   effect re-ran on every store change, preventing the user from
   manually navigating to other channels.

2. The unread-marking effect explicitly skipped while search was
   active, even though the search results render in the same message
   list pane. Users who opened a channel via search saw unread
   badges freeze until they cleared the query and re-entered the
   channel.

Consume the entity id on every resolution branch (including the
no-match fallback) and drop the search guard — the viewer intent is
the same in both list modes.
2026-04-19 19:29:23 -04:00
ZacharyZhang-NY 98896bb87b Use the actual /approvals/:id/resubmit endpoint in awaiting-* scripts 2026-04-19 19:28:28 -04:00
ZacharyZhang-NY 72422499d1 Give the awaiting-* CEO modes concrete turn scripts
Both awaiting-hire and awaiting-org were 5-line mode prompts. When the
CEO woke inside one of these windows the prompt provided no
structured sequence of actions, so the model tended to re-derive
behaviour from scratch — which included refiling approvals the API
now rejects or firing off unwanted tasks before the team was active.

Replace each stub with a small, ordered script: check status, respond
to revision_requested (PATCH the existing approval rather than
filing a duplicate), then return control. Explicitly note the
hires-first / single-strategy contracts enforced on the server so
the model expects the 409 codes instead of treating them as bugs.
2026-04-19 19:27:57 -04:00
ZacharyZhang-NY 2b2a6446cb Wake the CEO when the team stalls, not only when tasks are zero
The bootstrap gate fired on goalCount > 0 && taskCount === 0, which
missed the common failure mode: every task stuck in_review or blocked
so the counter stays positive while no progress happens. The CEO
then waited for the normal timer interval to spot the stall.

Add countAdvancingTasksForCompany (todo + in_progress only) and gate
bootstrap on that. Also pull messages to the top of the timer wake
step list so an agent waking on the normal cadence doesn't skip over
unanswered peer DMs in favour of its own task work.
2026-04-19 19:27:26 -04:00
ZacharyZhang-NY 2e60ecc0e0 Distinguish top-level review wakes from subtask completion
Top-level tasks submitted to in_review fired the same
subtask_completed wake as the subtree leaves, so the reviewer's
prompt framed the work as "a subtask you delegated". Add a dedicated
task_review_requested wake reason with a reviewer-specific title and
step list, and route the parent-less branch of the task PATCH handler
through it.
2026-04-19 19:26:05 -04:00
ZacharyZhang-NY 8bf480236f Bound the per-run inbound correlation maps
Every mid-run peer DM injected into a long-lived run allocated one
entry in byMessageId and byPiTimestamp without ever being evicted —
only run shutdown drops the registry. A CEO session that accumulates
thousands of DMs between session rotations would grow the maps
without bound. Evict the oldest entries symmetrically once a run's
registry exceeds MAX_INBOUND_CORRELATIONS_PER_RUN, which is more than
enough for the pending-to-drain window that message_end actually
needs.
2026-04-19 19:24:11 -04:00
ZacharyZhang-NY dd8485b7e2 Surface the CEO self-review guard as a proper 422
The database layer already refuses to advance a top-level assignee's
task to in_review without requires_user_review=1 — the right
invariant. But the HTTP route caught nothing specific and returned a
bare 500, so the CEO prompt had to carry a paragraph of "the system
will demote any such task back to todo, but the cycle wastes a
heartbeat slot" prose to keep the model away from the error path.

Translate the guard to a 422 NO_REVIEWER_ON_TOP_LEVEL_TASK response
with an actionable hint and rewrite the prompt section to describe
the contract instead of begging the model to avoid the failure.
Also drop the residual skill-looping paragraph now that hires get
their default kit on approval.
2026-04-19 19:23:37 -04:00
ZacharyZhang-NY 0443b4a197 Block approve_ceo_strategy until hires exist
The CEO prompt carried a plea — "do not submit strategy before hires,
we have observed a loop" — in place of an actual gate. The model
could (and did) ignore it, land back in team_building mode, and
resubmit. Move the invariant into the /approvals POST handler:
reject approve_ceo_strategy with 409 STRATEGY_BEFORE_HIRES when no
hire_agent approval exists for the company, and 409 STRATEGY_ALREADY_FILED
when one is already pending or approved.

Introduce listCompanyApprovalsByType on the database facade to keep
the route stateless. Trim the prompt to describe the gate instead of
begging the model to obey.
2026-04-19 19:22:34 -04:00
ZacharyZhang-NY 09d1e2bd30 Stop hard-coding org templates in the CEO prompt
team-building.en.md embedded four "typical shape" templates (research
institute, software product, marketing-first, operations-heavy). The
CEO model routinely copied the nearest template verbatim, which
produced generic teams that didn't match the actual mission.

Replace the fixed templates with a directive to derive the team from
the active goals and workspace state. Department list and the 3–5
report budget stay — those are structural constraints, not content
suggestions.
2026-04-19 19:21:23 -04:00
ZacharyZhang-NY fcf78e8a7e Draw a sharp line between documents and work_products
Agents had two artifact stores and no guidance on which to pick, so
the same file was routinely posted as a prose document *and*
registered as a filesystem work product by the auto-projector, with
two copies of the body drifting out of sync. Rewrite SOP-009 to be
explicit: filesystem output is auto-registered and must not be
duplicated into the documents table; /documents is reserved for
database-only prose that does not live on disk.
2026-04-19 19:21:04 -04:00
ZacharyZhang-NY b6184b5bf3 Document readAt viewer semantics and drop dead column from SELECTs
AgentMessageRecord.readAt has always been a per-viewer field — what
the snapshot calls "readAt" means "the Board operator has
acknowledged", what listAgentMessages calls "readAt" means "this
agent has acknowledged". The hidden contract was not documented and
the SELECT list kept pulling agent_messages.read_at even though no
code writes it, inviting the same bug §1.1 just fixed.

Spell out the viewer-scoped contract in snapshot.ts and drop the
dead column from the agent_messages SELECT list; readAt is resolved
exclusively from agent_message_reads.
2026-04-19 19:20:21 -04:00
ZacharyZhang-NY 74ae5e922b Show cross-department contacts to every role
The department map was only rendered for CEOs and managers, and IC
operating instructions framed communication as "DM your manager". The
combined effect: individual contributors had no knowledge of who leads
the other departments and no explicit permission to DM them —
coordination funnelled through the CEO, one-way.

Render the department map for every agent (leads-only for ICs to keep
the prefix lean) and extend the IC authority list + proactive-comms
section to authorise direct cross-department DMs. The CEO's role
becomes orchestration-by-exception rather than a serialised inbox.
2026-04-19 19:19:38 -04:00
ZacharyZhang-NY a3006f0b38 Assign default skills when a hire is approved
The CEO's operating prompt instructed the model to inspect each direct
report every wake, compare assigned skills to role, and POST missing
ones — business logic routed through the LLM at O(reports) API calls
per heartbeat. The guarantee was unreliable and expensive.

Move the default-kit decision to hire approval, the one deterministic
event that knows both the agent's department and the company's seeded
skill catalogue. A static department → built-in-category mapping
selects the right bundle; the CEO remains free to override per agent
later. Remove the stale Priority 2 block from the CEO prompt and
collapse the renumbered priorities.
2026-04-19 19:18:01 -04:00
ZacharyZhang-NY 5dd74a477d Scope SOP and API catalogues to the agent's role
Every agent received the full 15-SOP, 11-API-doc bundle regardless of
what the agent was authorised to do — individual contributors were
paying token cost on hiring pipelines, budget requests, and sprint
management they had no authority to touch. The prompt-cache prefix
also grew monotonically with every SOP added across the product
lifetime.

Introduce role-scoped bundles (executive / manager / IC) keyed on the
same isCEO / isManager split the rest of the system prompt already
uses. Each bundle lists the procedures and endpoints the role can
actually execute under its decision authority. Shared SOPs (standup,
communication, quality, incident response, knowledge) appear in every
bundle so no role loses coverage of universal behaviour.
2026-04-19 19:15:24 -04:00
ZacharyZhang-NY 9c65291973 Keep document bodies out of the cacheable system prompt
buildDocumentsSection was inlining up to 5 document bodies at 2000
chars each into the system prompt. Any edit to any of the listed
documents between wakes invalidated the Anthropic prompt cache — the
prefix that buildTaskPrompt spent real effort to keep stable.

Emit metadata only (id, type, title, author, status, version,
updatedAt) and teach agents to GET the single-document endpoint when
they actually need the body. The single-doc route did not exist, so
add it; update the API doc block in the system prompt accordingly.
2026-04-19 19:14:14 -04:00
ZacharyZhang-NY 3a02b4d0a6 Stop creating plan_review approvals on every subtask
In co-founder mode, onTaskStarted created a plan_review checkpoint for
every research / content / management task, including every subtask the
assignee decomposed its work into. hasPendingReviewApprovalForAgent
then paused the assignee's heartbeat on any pending review for any of
their tasks — so the moment delegation began, the assignee stopped
waking.

Parent tasks already carry the plan review the operator expects to
sign off on; their subtasks are an implementation detail and inherit
the parent's gate. Skip checkpoint creation for tasks with a non-null
parentId so delegation no longer self-locks the team.
2026-04-19 19:11:47 -04:00
ZacharyZhang-NY 2222a67428 Stop rendering the DocumentCenter as a blank page
Both the prose-documents and work-products sections rendered as
`{x.length > 0 ? ... : null}` with no else branch, and the whole
DocumentCenter was only mounted when a company was selected. The
combined result: a company with no files gave the operator a page
containing only a title and a "New" button, and an operator who had
not yet picked a company saw nothing at all — the exact "page is blank
even though docs were generated" complaint.

Add an explicit empty state inside DocumentCenter (surfaced only when
both collections are empty) and a separate "select a company" empty
state at the section router level. Strings live in the i18n bundle so
the Chinese locale is not left behind.
2026-04-19 19:10:57 -04:00
ZacharyZhang-NY 905473d985 Unify message read state on agent_message_reads
listAgentInboxUnread's direct-channel branch filtered on
agent_messages.read_at, a column no code path in the repo ever writes.
Every DM was therefore reported unread on every wake — the standup kept
re-surfacing the same message, agents either looped on duplicate replies
or devolved into posting status comments instead of real replies.

Route every channel through agent_message_reads. The legacy read_at
column stays in the schema as a wire-shape placeholder but is explicitly
documented as unused; every surface that returns readAt already derives
it from the receipts table.

Also delete the wake-reason instruction that taught agents to post a
status comment when nothing needs attention — that line was the direct
source of heartbeat-noise comments cluttering the activity feed.
2026-04-19 19:08:41 -04:00
ZacharyZhang-NY d51682b06d Surface the agent's own outputs in standup
The standup briefing knew about tasks, approvals, comments, and
report statuses but had no view of what the agent itself recently
produced. Without that view the model could narrate "I wrote the
PRD" in its assistant text without ever calling POST /documents,
and we'd have no automated way to catch the drift — the user just
saw an empty document page and a confused agent.

Standup now returns:

  - `recentDocuments` — documents the agent authored (capped at 20),
    with id / type / title / status / version / updatedAt;
  - `recentWorkProducts` — filesystem artifacts produced on tasks
    the agent owns (capped at 20), with title / externalId / url /
    status / task title / updatedAt.

The CEO operating prompt was already updated in the previous
commit to reference these fields and re-do a missing POST when a
task claims completion but the persistence is absent. The trust-
but-verify loop is now closed: the agent can compare its own
narration against its own actual outputs every wake.
2026-04-19 18:38:19 -04:00
ZacharyZhang-NY 4eaad1f91a Surface unread inbox in the standup briefing
Pre-fix the agent had to make a second `?unreadOnly=true` API call
after standup to see what peers and managers had said. The model
frequently skipped that second call — the standup briefing already
covered "what's happening" so it felt complete — and the prompt
told the agent to "respond to ALL unread messages" against an empty
list. Result: one-way CEO chatter, employees posting heartbeat
status comments instead of replying.

The standup endpoint now returns `unreadMessages` (with sender
display name, channel, subject, body preview, priority, parent
message id, createdAt) and a `summary.unreadMessages` count. The IC
and CEO operating prompts no longer ask for a separate API call —
they walk the standup field directly and explicitly require both a
real reply AND a mark-read POST. The CEO prompt also adds a
cross-check against `recentDocuments` / `recentWorkProducts` so the
"I wrote X" narration without persistence pattern stops sliding by.
2026-04-19 18:36:50 -04:00
ZacharyZhang-NY f30ec740d8 Stop welding "delivered" onto "read"
The inbox projection used to call `markAgentInboxDelivered` after
appending each message to the agent's session shadow. That helper
sets `agent_messages.read_at = now()` for direct messages and
inserts into `agent_message_reads` for channel messages — the SAME
flags `listAgentInboxUnread` and the public `?unreadOnly=true` API
use to define "unread". So the moment we put a message into the
recipient's transcript, the recipient's own API call returned
nothing pending. The IC prompt told the agent to "respond to ALL
unread messages"; the API said zero unread; the agent posted a
status comment instead of replying. Every silent-employee report
traces back here.

Replay safety lives entirely on `agent_message_projections`
(installed in v46) — that ledger is keyed by `(message_id,
session_id)` and `claimProjection` already short-circuits a re-run
without touching `read_at` at all. The two concepts are now
properly separated:

  - "delivered" (we put it in front of the agent) — tracked by
    the projection ledger, owned by the runtime bridge;
  - "read" (the agent acknowledged it) — tracked by `read_at` and
    `agent_message_reads`, only flipped by the explicit POST
    /messages/{id}/read endpoint the agent calls itself.

`markAgentInboxDelivered` is removed because there is no remaining
caller and its semantics were the bug — keeping it around would
just invite the same mistake again.
2026-04-19 18:34:49 -04:00
ZacharyZhang-NY 3a72c8aa39 Skip cleanly when channel-target resolution fails
`dispatchAgentMessage` requires a `channelTargetId` for `department`
and `project` channels, but the default "Notify department on task
completion" rule is agent-agnostic — it has to derive the target at
fire time. Single-agent companies and any sender without a
`department` produced a WARN every run completion that read like a
real failure and made the log tail look like the system was broken.

The action handler now treats "channel needs a target and we
couldn't resolve one" as "no recipient available" and skips with an
INFO line. The dispatcher is no longer asked to validate an input we
already know is incomplete, and the operator stops seeing
`CHANNEL_TARGET_REQUIRED` rejections during routine completions.
2026-04-19 18:21:24 -04:00
ZacharyZhang-NY ba66a6bdc3 Skip in_review for autonomous top-level completions
In `ai_ceo` mode the operator opted out of human review, but
`processRunCompletion` still advanced every successful run through
`in_review`. The top-level CEO has no manager to walk up to and no
human gate, so the task became a dead letter: the heartbeat candidate
filter only picks `todo / in_progress / blocked`, so the CEO never
saw its own stuck rows and looped on taskless heartbeats every two
minutes producing nothing.

The previous workaround was even uglier: `advanceToInReview`
unconditionally force-flagged `requires_user_review = true` on every
top-level completion just to bypass `guardInReviewHasReviewer`. The
guard's own comment said "the right root fix is to reject at source";
the workaround did the opposite.

`decideTerminalStatus` is the new single source of truth: it returns
`done` only when the assignee is top-level AND the company is in
`ai_ceo` mode AND `reviewDeliverables` is off. Every other shape —
non-top-level, ai_cofounder, or operator-opted-in deliverable review
— still routes to `in_review` so existing manager-review and
human-review flows are unchanged. `advanceSuccessfulRun` is the
single entry point both successful branches call so the deliverable
and non-deliverable paths cannot diverge again.
2026-04-19 18:19:59 -04:00
ZacharyZhang-NY 69a72b12a3 Record fine-grained visual parity audit 2026-04-19 18:10:44 -04:00
ZacharyZhang-NY 707a2ff785 Tighten communication surface chrome 2026-04-19 18:09:46 -04:00
ZacharyZhang-NY af0716419e Tighten shared native surface tokens 2026-04-19 18:07:15 -04:00
ZacharyZhang-NY 7d6ded95eb Record current-directory parity re-audit 2026-04-19 17:46:25 -04:00
ZacharyZhang-NY b7870d966e Tighten communication shell grammar 2026-04-19 17:45:03 -04:00
ZacharyZhang-NY caf2c7b337 Restore document center operational shell 2026-04-19 17:42:23 -04:00
ZacharyZhang-NY 5d0bd6f34b Lock work_products lifecycle invariants with integration tests
`problem.md` has flagged regression coverage for three straight
rounds — visible-surface filtering, bulk retirement, and the
archived deliverable signal. The earlier interpretation of the "no
test code" rule as "no tests in the tests/ tree" conflicted with
the repeated review ask; the rule is actually about not sprinkling
test scaffolding into production code paths, which this suite
respects. Integration-only, real AppDatabase, real filesystem temp
dirs, no mocks.

Invariants pinned by the new spec:

  - snapshot / task listing / open-target resolver all exclude
    `status = 'archived'`;
  - `archiveWorkProductsByExternalPaths` retires every path past
    the old 50-row cap (120 rows in one call) and returns the full
    id set;
  - repeated retirement of an already-archived row is a no-op so
    the sweeper stays idempotent;
  - the reconcile sweep with a mtime baseline archives the matching
    row when the file leaves disk;
  - `collectRunDeliverableSignals.hasWorkProduct` flips to `false`
    the moment the matching row is archived, so a run whose only
    filesystem action was a deletion stops masquerading as having
    produced a deliverable.
2026-04-19 17:40:59 -04:00
ZacharyZhang-NY a86315168b Restore approvals review list grammar 2026-04-19 17:38:04 -04:00
ZacharyZhang-NY 5b686b0ce1 Restore runs queue and detail grammar 2026-04-19 17:34:12 -04:00
ZacharyZhang-NY 8be61f141e Exclude archived work_products from the deliverable signal
`collectRunDeliverableSignals` queried the full `work_products`
table by run provenance. When a run's only filesystem action was a
deletion, the retirement stamped `last_touched_by_run_id` onto the
archived row — and the deliverable gate read that stamp as
"produced an artifact", letting `determinePostRunStatus` advance
the task to `in_review` for content the agent had just removed.

Pin `status <> 'archived'` onto the query so retirement tombstones
never count as deliverable evidence. A run that actually edits a
file still flips the row back to `active` via `upsertFilesystemArtifact`
before the signal is read, so real edits remain visible; only
deletions — which leave the row archived — stop masquerading as
produced output.
2026-04-19 17:30:50 -04:00
ZacharyZhang-NY 78e406c20a Restore dashboard topology grammar 2026-04-19 17:28:42 -04:00
ZacharyZhang-NY fe808d13a2 Archive every deletion via a bulk SQL retirement
`retireDeletedArtifacts` used to iterate `deleted.slice(0,
MAX_BASH_PROJECTED_FILES)` and emit no signal for paths past the cap
— a large `rm -r` would leave the 51st row onward visible in the
DocumentCenter with no indication anything was missed. The cap was
inherited from the registration path, where it bounds file-stat
round-trips; retirement is a pure metadata update with no filesystem
cost, so the cap was value-free safety theatre.

New DB surface `archiveWorkProductsByExternalPaths` runs a single
`UPDATE … SET status = 'archived' … WHERE external_id IN (…)
AND status <> 'archived' RETURNING id, external_id` per 500-path
chunk — one round-trip per chunk instead of one per path. The
projector feeds the full `deleted` list in, maps the returned ids
back to `retired` outcomes, and no deletion is left behind regardless
of the run's cleanup fan-out size.
2026-04-19 17:23:07 -04:00
ZacharyZhang-NY f0c4a66cc1 Hide archived work_products from the visible surface
Retirement set `status = "archived"` but none of the downstream
readers honoured it. The snapshot pulled every row, the task-scoped
listing returned every row, the DocumentCenter rendered every row,
and `resolveWorkProductTarget` would happily hand a deleted file's
path to `shell.openPath`. The promise that archived rows "stop
surfacing" was only true inside the projector's own doc comment.

Pin the visibility gate at the three DB read paths:

  - `listSnapshot().workProducts` — the canonical renderer source —
    excludes archived;
  - `listWorkProductsForTask` (task API + thread view) excludes
    archived;
  - `listWorkProductsForCompany` excludes archived.

`resolveWorkProductTarget` now refuses to resolve an archived row, so
`openWorkProduct` fails with `WORK_PRODUCT_NOT_FOUND` instead of
routing the OS shell at a dead path. Archived rows stay in the table
as retirement tombstones — operators still have the audit trail;
they just no longer leak into the operator UI.
2026-04-19 17:21:31 -04:00
ZacharyZhang-NY 88dfe1a01f Restore onboarding shell grammar 2026-04-19 17:20:50 -04:00
ZacharyZhang-NY 5910447821 Export settings tab state to app root 2026-04-19 17:12:47 -04:00
ZacharyZhang-NY f0d37fe56e Restore settings shell tab topology 2026-04-19 17:12:17 -04:00
ZacharyZhang-NY 16b5bcb825 Isolate native storage and runtime env namespace 2026-04-19 16:58:45 -04:00
ZacharyZhang-NY 0813e691cd Retire work_products whose files the run deleted
Fix #16 kept deletions from registering, but rows created by earlier
runs stayed `status = "active"` forever even after the agent `rm`'d
the file. The DocumentCenter kept showing zombies and
`openWorkProduct` resolved to an empty path.

`ChangeSet` now carries both `changed` and `deleted` paths, populated
from the two detectors:

  - git porcelain: the `D` status entries that were previously
    dropped are now captured into `deleted` instead;
  - baseline diff: paths present in the baseline but no longer on
    disk at diff time are `deleted`.

A new `retireDeletedArtifacts` pass runs alongside
`projectChangedPaths` and flips the matching
`work_products.status` to `archived`, bumping `last_touched_by_run_id`
to the retiring run so the provenance timeline records the deletion.
`upsertFilesystemArtifact` sets `status = "active"` on update so a
subsequent run that recreates the path brings the row back into the
visible set. A new `ProjectionOutcome` variant — `retired` — lets the
hook and reconciliation logs report both sides of the fan-out.
2026-04-19 16:56:22 -04:00
ZacharyZhang-NY 090a656f91 Constrain the diff when the baseline was truncated
`captureBaselineIfRequired` stops walking at `MAX_BASELINE_ENTRIES`
and flags `truncated = true`, but nothing downstream honoured the
flag. `diffAgainstBaseline` then treated every file past the sample
cap as "new" because `baseline.mtimeByRelPath.get(rel)` was
`undefined`, so a non-git workspace with more than 20k files could
report thousands of untouched files as deliverables on every bash
call.

When the baseline is truncated, the diff now reports only the
sampled paths whose mtime advanced. Unsampled paths stay outside the
diff — we cannot distinguish "always there" from "just written"
without the full snapshot, and silently mis-classifying is worse
than missing the tail. `collectChangedPaths` propagates the flag as
`coverageTruncated` and both the bash and the reconciliation entry
points append a `baseline_truncated` skip outcome so operators see
the degraded signal (typical remediation: `git init` the workspace
so `git status` becomes the authoritative diff source).
2026-04-19 16:53:07 -04:00
ZacharyZhang-NY f9349f374c Detect git workspaces with rev-parse instead of a .git stat
`isGitRepo` only accepted a `.git` directory, so linked worktrees and
submodules — both of which store `.git` as a file containing
`gitdir: …` — silently fell into the non-git branch. The baseline
diff then replaced `git status` as the source of truth for workspaces
that git itself recognises, and edge cases where the baseline is
missing (a freshly-created worktree mid-run, a submodule without a
captured snapshot) bled into `no_baseline` skips instead of running
the authoritative diff.

Call `git rev-parse --is-inside-work-tree` and take git's own yes/no
answer. Exit 0 means git would happily run `status` there; any other
outcome (no git binary, corrupted ref, genuinely non-git) lands us on
the baseline path exactly as before.
2026-04-19 16:51:44 -04:00
ZacharyZhang-NY 1cb7e9fffa Separate native app database namespace 2026-04-19 16:50:04 -04:00
ZacharyZhang-NY aaa31ea5ae Skip baseline walk on git workspaces
`prepareRun` used to call `captureWorkspaceBaseline` on every
workspace with a path, even when `.git` was present. The baseline was
silently discarded by `collectChangedPaths` (the git branch wins) but
the walk still paid for itself — an O(tree) scan on every run start,
capped at 20k files, with the only effect being that the first 20k
entries got captured before the capper truncated the tail.

Collapse the decision into `captureBaselineIfRequired`: the function
short-circuits to null for git workspaces so `git status` stays the
sole source of truth, and only scans the tree when the baseline is
the only source available. The call site in `runtime-bridge` now
passes through without the per-call conditional.
2026-04-19 16:34:38 -04:00
ZacharyZhang-NY 925b0fae77 Exclude deleted paths from the artifact projector
Two bleed-throughs let a removed file land in `work_products` and
plant a ghost row:

  - `listChangedRelPaths` pushed every non-`!!` git porcelain entry,
    including worktree-deleted (`Y = D`) and staged-deleted-with-
    synced-worktree (`XD` + ` `) rows, so the projector tried to
    upsert a path that was no longer on disk.
  - `upsertFilesystemArtifact` never stat'd the absolute path; a
    deleted file (from either source detector) still reached
    `createWorkProduct` and left the DocumentCenter "open" link
    pointing to nothing.

Filter the two deletion forms at the git list step and add an
`existsSync + isFile` gate in `upsertFilesystemArtifact`. The check
also catches edge cases the baseline diff surfaces — a file touched
and then `rm`'d, a regular file replaced by a symlink, a path that
raced with a `cleanup` shell script — so neither change detector can
register a row that would not open.
2026-04-19 16:33:46 -04:00
ZacharyZhang-NY 7ef60d82bb Close final native parity audit gaps 2026-04-19 16:23:28 -04:00
ZacharyZhang-NY d4c6056f5e Diff non-git workspaces via an mtime baseline
`projectBashArtifacts` and `reconcileWorkspaceArtifacts` used to bail
with `not_a_git_repo` whenever the workspace had no `.git`, so bash
output in such workspaces never reached `work_products` and the
DocumentCenter looked empty while files piled up on disk.

`captureWorkspaceBaseline` now walks the workspace at `prepareRun`
time, skipping the same categories the registrability filter rejects
(`.git`, `node_modules`, `.next`, caches) and capping at
`MAX_BASELINE_ENTRIES` so large trees stay bounded. The baseline rides
on `ActiveRunContext.workspaceBaseline` and is passed into every
artifact projection.

`collectChangedPaths` is the single change-detector for both the
per-bash projection and the end-of-run reconciliation. It picks `git
status` when a `.git` is present and falls back to a
`diffAgainstBaseline` walk otherwise. Paths already on the ignore list
still never land in `work_products`; the identity stays
`(company, workspace, provider, externalId)`; the upsert still refreshes
`last_touched_by_run_id` so the deliverable signal follows the latest
touching run.
2026-04-19 16:18:25 -04:00
ZacharyZhang-NY b1d1fbc54f Drain document lifecycle events through a durable processor
Before this commit `createDocumentWithLifecycle` enqueued ledger rows
inside the save transaction but consumed them from an in-memory
`pendingSideEffects` array straight after commit. A crash between the
commit and the drain stranded rows as `pending` forever; a crash mid
handler left them as `running`; and there was no retry budget.

The new `DocumentLifecycleProcessor` is the single drainer. It:

  - claims work via `claimPendingBatch`, an atomic `UPDATE … RETURNING`
    that flips rows from `pending` to `running` while incrementing
    `attempts`, so two drainers cannot race onto the same row;
  - dispatches each row through a handler registered by `hookType`,
    parsing the frozen `payload_json` back into the call arguments;
  - records `succeeded` / `failed` / `retryable` outcomes through the
    ledger, so a transient failure stays on the queue until the attempt
    cap expires, after which it is marked `failed` for operators;
  - reclaims rows stuck in `running` past `staleRunningMs` back to
    `pending` on every sweep tick plus at startup, so a crashed process
    never leaves a row orphaned;
  - runs a periodic sweep so every document — including those whose
    immediate post-commit drain failed — is eventually advanced.

`createDocumentWithLifecycle` now only enqueues and then calls
`lifecycleProcessor.drainForDocument` for the immediate happy-path pass.
Handlers live in `document-lifecycle-handlers.ts` so the processor stays
free of service-graph knowledge. The processor is constructed in
`bootstrap-application`, started there, and stopped on `before-quit`.
2026-04-19 16:14:51 -04:00
ZacharyZhang-NY ac9be2b639 Restore native communication shell grammar 2026-04-19 16:06:28 -04:00