Commit Graph
38 Commits
Author SHA1 Message Date
ZacharyZhang-NY c26e7420fb fix(browser): queue browser action dispatch failures 2026-04-23 19:39:29 -04:00
ZacharyZhang-NY a4af4a866a Prevent self-directed agent messages 2026-04-22 20:28:49 -04:00
ZacharyZhang-NY 6da0a58769 refactor(db): group database domain modules 2026-04-22 19:24:25 -04:00
ZacharyZhang-NY 0d9f4f15eb refactor(automation): register workflow actions 2026-04-22 18:54:31 -04:00
ZacharyZhang-NY 19f8494d09 fix(db): make filesystem lock intent explicit 2026-04-22 13:51:41 -04:00
ZacharyZhang-NY 827f06f358 Migrate 26 high-value catch sites to AppError taxonomy (C6 wave 1)
Classify scheduler, orchestration, IPC read, and automation catch
blocks via logAppError + classifyUnknownError so each failure surfaces
a stable `module.subdomain.condition` code plus a kind. No behaviour
change — returns, throws, and fall-through are preserved; only log
prefixes and the taxonomy metadata are new.

Wave 1 coverage (26 sites, all non-database):

  - orchestration/scheduler-lock-recovery (3)
  - orchestration/scheduler-housekeeping (5)
  - orchestration/scheduler-idle-automation (2)
  - orchestration/scheduler-heartbeat (4)
  - orchestration/turn-context (2)
  - ipc-core-read-handlers (6)
  - automation/automation-handlers (4)

Relax the Logger interface so `error` is optional, letting callers
whose deps expose only `{info, warn}` (scheduler contexts) pass in
directly without wrapping. The helper falls back to warn when error
is absent.
2026-04-20 13:37:40 -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
ZacharyZhang-NY a69fbb053b Codemod db pass-through calls: messages + runs + approvals + company (C1 part 2/3)
Rewrite ~376 call sites for messages, runs, approvalsMod, and company
domains and delete the corresponding pass-through methods on
AppDatabase / AppDatabaseBase. Structural type references
(ReturnType<HandlerDeps["db"]["getRun"]>) and SweepStaleReviewsDeps-style
mocks are updated to route through the owning sub-module.
2026-04-20 13:20:15 -04:00
ZacharyZhang-NY 5e9b7acbc6 Codemod db pass-through calls to domain modules: tasks + agents (C1 part 1/3)
Rewrite ~525 call sites from `db.<method>()` to `db.<module>.<method>()`
for tasks and agents domains (including `getAgent`/`findAgent`/`getTask`
which live on the `entityCrud` field). Delete the corresponding
pass-through methods on AppDatabaseBase. Structural types that narrowed
`db` to a bag of flat methods (CapabilityMatchDb, EscalationDeps,
SweepStaleReviewsDeps) are reshaped to match the new namespace. Internal
callback references inside database.ts/database-base.ts now invoke the
owning module directly (e.g. `this.agents.saveAgent`) rather than the
deleted pass-through.

The codemod lives at scripts/codemod/remove-appdatabase-passthrough.mjs
and filters by owning module + optional explicit method list. It is
idempotent and does not re-rewrite already-namespaced calls.
2026-04-20 13:17:01 -04:00
ZacharyZhang-NY 557090a6a8 Move message-*.ts into messaging/ subdir (N10)
Consolidate the two runtime messaging modules under src/main/messaging/. Storage stays in database/messages.ts — that is the SQL layer and belongs with the other DB modules; messaging/ covers the dispatch/normalize runtime around it.

- message-dispatch.ts -> messaging/dispatch.ts
- message-normalize.ts -> messaging/normalize.ts
- add messaging/index.ts barrel for future consumers (existing call sites import the concrete module path directly)
- 10 call sites updated across src/main and tests
2026-04-20 12:29:15 -04:00
ZacharyZhang-NY 6b22fa9f0e Free tasks from the checkout_race_lost deadlock
Two bugs combined to park successful-but-deliverable-less tasks in an
unrecoverable state. Operator saw Alex Chen loop on
`checkout_race_lost` every two minutes for 16+ minutes in the field
log after completing its first task.

1. `processRunCompletion` in automation-run-completion.ts fired a no-op
   status transition (`in_progress` → `in_progress`) when a run
   succeeded without producing the expected deliverable. advanceTaskStatus
   no-ops on identical prev/next, and handleTaskStatusChange logged its
   `caller should gate this` warning and returned. finishRun had already
   released active_run_id and checkout_run_id, so the task sat at
   `in_progress` with no lock, no run — the exact orphan pattern that
   checkoutTask's `status IN ('todo','backlog')` clause refuses.
   Transition the row to `blocked` with the diagnostic comment attached
   so the manager's blocker queue surfaces it, the assignee stops
   spinning, and effectiveStatus carries the real state downstream.

2. `checkoutTask` itself refused to reclaim any orphan `in_progress`
   row, even when active_run_id was already null. That refusal was the
   right invariant against concurrent re-entry but wrong for the orphan
   case a crashed or status-unchanged run leaves behind. Extend the CAS
   predicate to accept `status = 'in_progress' AND active_run_id IS NULL`
   — heartbeat candidate selection already filters out tasks whose
   active_run_id points at a live run, so the guard preserves the
   original mutual-exclusion property for the busy case while allowing
   the orphan reclaim the scheduler actually needs.
2026-04-19 21:09:16 -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 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 efb4bddab6 Count work_products toward a run's deliverable judgment
The deliverable check used to compare a run against knowledge entries,
documents, and substantial task comments only, so a research or content
task that produced its output as a markdown file on disk (via
write/edit/bash) stayed pinned in_progress even though the deliverable
was already sitting in `work_products`.

Split the DB query into `collectRunDeliverableSignals`, which returns
the four surfaces (knowledge / document / work product / substantial
comment) explicitly, and push the per-task-type acceptance logic into
`automation-run-completion`. Each task type declares which signal set
counts as a valid deliverable: content tasks now require a document or
an on-disk artifact and will not advance on a knowledge entry alone;
research and general accept any of the four; code and management
preserve their broader surface. Tests updated to cover the new signal
and the content/knowledge rejection case.
2026-04-19 14:59:05 -04:00
ZacharyZhang-NY 7f23f40e87 fix(db): strip implicit data mutations from startup
`init()` ran three implicit data-repair queries on every boot:
`reconcileOrphanedTasks`, `reconcileUnreviewableTasks`, and a
secrets-table migration whose fallback glued legacy rows onto a
synthetic zero UUID when no current company existed. The first
two papered over state-machine leaks at init time — hiding the
real bug and making startup observably mutate production data;
the third silently connected ciphertext to a non-existent company
with no UI visibility and no deletion path.

Root fix: the startup path is now allowed exactly one mutation —
`interruptStaleRuns`, which is crash recovery (running-at-boot =
worker died mid-turn) and cannot be expressed anywhere else.

The dead-letter `in_review` case moves to the transition point:
`tasks-status.guardInReviewHasReviewer` refuses to advance a task
into `in_review` when its assignee is a top-level agent
(`reports_to IS NULL`) and the task is not flagged for user
review. Callers either pre-flag the task (the CEO-completion path
in `automation-run-completion.advanceToInReview` now does this)
or the transition is rejected. No more silent resurrection at
boot; the failure is visible and actionable at source.

The orphan-task reconcile disappears entirely — the `finishRun`
guard already enforces the invariant at the one place a run can
leave the in-progress state, and `recoverStaleLocks` handles
stuck executions at runtime.

The secrets migration now throws when neither
`settings.current_company_id` nor any company row exists, rather
than binding legacy ciphertext to `00000000-0000-0000-0000-000000000000`.
Legacy rows stay on the old table until the operator creates a
real company, at which point the upgrade succeeds deterministically.

Tests rewritten: the reconciliation regression suite now asserts
the transition-time guard and the lone `interruptStaleRuns`
behavior. `resolve-workspace.test.ts` catches up with the
exclusive-by-default lock policy committed earlier.
2026-04-19 01:12:38 -04:00
ZacharyZhang-NY 36bed1b0f1 feat(team): add TeamHeaderSummary strip + delete unreachable orgchart section
Two S-level audit items landed together because they are the same
"first-second orientation" concern on the team page:

S1 — Empty OrgChart was the first thing the board saw; the canvas
     carried no summary data. Now a compact summary strip sits above
     the chart showing Members / Running / Attention / Pending hires /
     This-month budget (spent/cap/%). All five numbers computed from
     the existing snapshot — no extra IPC.
S9 — Activity and budget context previously required visiting three
     separate pages. The summary strip collapses the minimum-useful
     subset into a single glance; the full drill-down paths (Work,
     Approvals, Costs) remain one click away.

S10 — The `orgchart` SectionId was a dead rendering branch:
       - Not in the sidebar (useUIController sidebarGroups)
       - Not in the command palette (paletteOnlyNavDefs)
       - Not reachable from any navigateTo() call in renderer code
     The only callers were an automation post-hire notification and a
     unit test — both pointed at "orgchart" as a navigation target.
     Routed both to "agents" instead (which renders the OrgChart at
     the top anyway, plus the new summary strip and the roster), so
     clicking the "Agent Hired" notification now lands on the team
     page with the new hire highlighted.

Removed:
- `orgchart` from SectionId enum (src/shared/types/enums.ts)
- `section === "orgchart"` render branch (OrganizationSections.tsx)
- `nav.orgchart` i18n key (zh + en)

Added:
- src/renderer/components/organization/TeamHeaderSummary.tsx
- team.summary.* i18n keys (zh + en)

Covers S1 / S9 / S10.
2026-04-17 19:57:58 -04:00
ZacharyZhang-NY 7e1d4199ee refactor(schema): make runs.task_id nullable, delete heartbeat workaround
The NOT NULL constraint forced heartbeat runs to synthesize a fake task
row (`[Heartbeat] X — Check for work`) so the FK could resolve. Three
layers of code — acquireHeartbeatTask, findReusableHeartbeatTask,
cleanupDuplicateHeartbeatTasks, plus the is_heartbeat flag column + UI
filter — existed to paper over one bad constraint. Remove the
constraint and the whole workaround vanishes.

SQLite table rebuild with data preservation. Live production DB, no
backup — migration is atomic, idempotent, and verified before DROP.

## Migration v35 (rebuild runs with nullable task_id)

Uses `fn:` form so it can:
- Snapshot `COUNT(*) FROM runs` before copy
- Walk `PRAGMA table_info(runs)` to detect drift columns (v34 declared
  a column set; any additive ALTER since v14 shows up here)
- Assert `COUNT(*) FROM runs_new == beforeCount` before `DROP TABLE runs`
- `PRAGMA defer_foreign_keys = ON` so run_events(run_id) FK defers to
  commit time, by which runs has the same id set under the new name
- Recreate all 6 indexes on the new table

If the assertion fails, the migration throws inside the existing
`transaction()` wrapper. SQLite rolls back the whole migration, old
`runs` table is untouched, `schema_migrations` never records v35. Next
app start retries cleanly.

## Migration v36 (drop tasks.is_heartbeat + synthetic rows)

Runs now carry `task_id = NULL` for heartbeat dispatches. The
`is_heartbeat` flag is dead data going forward.

1. `UPDATE runs SET task_id = NULL WHERE task_id IN (SELECT id FROM tasks WHERE is_heartbeat = 1)`
2. `DELETE FROM tasks WHERE is_heartbeat = 1` (synthetic rows, not real work)
3. Rebuild `tasks` without the `is_heartbeat` column (same defer-FK +
   row-count-assert pattern as v35)

## Code cleanup

- `RunRecord.taskId: string | null`; `createRun` / `cancelRunForDeletion`
  / `finishRun` guard every `UPDATE tasks` path with a null-check.
- `TaskRecord.isHeartbeat` removed; every SELECT / filter / UI reference
  dropped.
- Delete `acquireHeartbeatTask` / `findReusableHeartbeatTask` /
  `cleanupDuplicateHeartbeatTasks` — dead after the constraint lifts.
- Delete `src/main/orchestration/heartbeat-task.ts` (pure workaround
  constant file).
- `heartbeat.ts` creates runs with `taskId: null` when the agent has no
  assigned work. Turn context already supports nullable task
  (heartbeatFallbackSection); prompt surface unchanged.
- `validateTaskCreation` drops the `isHeartbeat` parameter — user-
  authored + automation-authored tasks are never heartbeat.
- Renderer: `run.taskId ? taskMap.get(...) : undefined` in Dashboard /
  RunCenter / RunDetail / RunQueue.
- Delete `tests/unit/heartbeat-coalescing.test.ts` (12 tests that
  exercised the deleted DB methods).
- Drop 1 isHeartbeat-bypass test from `task-quality-gates.test.ts`.

## Grep verification

- `isHeartbeatTaskTitle src/` → 0
- `acquireHeartbeatTask src/` → migration comment only
- `is_heartbeat src/` → schema.ts migration history only
- `isHeartbeat src/` → 0

Tests: 578/578 (was 591 — delta is 13 tests that tested deleted code).
2026-04-17 16:16:08 -04:00
ZacharyZhang-NY 40040b3dd2 refactor(errors): surface 57 deferred catch-and-swallow sites via logger
The earlier sweep cleaned the hot path (worker/runtime/automation/api-
server/ipc core — 59 sites). This pass finishes the deferred long tail:
main/* services, database/* CRUD modules, api-server/* routes, ipc-*
handlers, thread-integration, automation helpers, browser, skills.

Default strategy: catch (error) { logger.warn("[module] context:", ...) }
— preserve current behavior, make the failure visible instead of
silently hidden. Widened 3 logger interface signatures to include warn
where callers only passed info/error previously (thread-helpers,
host-environment-service, ipc-domain-common).

Legitimate boundaries left intact: JSON.parse on untrusted input, DNS
resolver no-records paths, explicit safe* helpers (safeJson, parseJson,
parseTriggerDetail), URL-parse predicates, and `<unserialisable>`
fallbacks inside the prompt-builder summarisers. Each kept catch now
either has a reason in context or is a documented safe* helper.

Tests adjusted only where test-constructed no-op loggers needed a warn
stub (message-dispatch, company-mode-switch, ipc-agent-graph-handlers).

With the earlier pass, AgentCompany no longer has silently-swallowing
catch blocks in main-process or renderer business code.
2026-04-17 15:25:13 -04:00
ZacharyZhang-NY 14dc9a4897 refactor(cost): single writer for cost_entries, finishRun reads SUM
Cost was written through three independent paths: worker-runner shipped
`finalUsage.costUsd` in run-finished -> finishRun wrote `runs.cost_usd`,
`tasks.cost_usd += costUsd`, `agents.spent_monthly_usd += costUsd`, and
inserted a per-run aggregate row in cost_entries. CostWriter
additionally inserted per-message rows in cost_entries from main. Any
CostWriter failure created silent drift between `SUM(cost_entries)`
(read by budget enforcement) and `runs.cost_usd` (read by the UI).

Make CostWriter the single writer, finishRun the single reader.

- Add `db.recordCostEntry(...)` as the first-class insert path for
  per-message cost rows. CostWriter calls it; tests call it directly.
- Remove the aggregate INSERT from finishRun. finishRun now reads
  `SELECT COALESCE(SUM(amount_usd), 0) FROM cost_entries WHERE run_id=?
  AND unattributed=0` and writes that as the authoritative run total to
  runs.cost_usd, tasks.cost_usd, and agents.spent_monthly_usd.
- Drop `costUsd` from `finishRun` input, the worker `run-finished`
  ChildMessage shape, the automation `RunFinishedEvent` type, and every
  caller + test fixture. Cost is owned by the per-message path.
- worker-runner stops extracting `usage.cost.total` — token counts stay
  for session accumulation, cost comes from pi-ai via CostWriter.
- run-finished-helpers now gates budget incident checks on
  `event.status === "succeeded"` instead of a cost field that no longer
  exists on the event.
- automation-run-completion fires `budget_threshold` on every succeeded
  run (post-run spent_monthly_usd already reflects the SUM path).
- Update budget tests to write a `recordCostEntry` before `finishRun`
  to simulate the per-message cost path; drop `costUsd` from cross-
  subsystem, edge-case-budget, database, and related fixtures.

One writer, one reader, one source of truth. No drift.
2026-04-17 15:04:22 -04:00
ZacharyZhang-NY 5547c29491 update 2026-04-17 14:03:56 -04:00
ZacharyZhang-NY a78bdb9b02 refactor(automation): type DEPARTMENT_SKILL_KEYWORDS with DepartmentId enum
Replaces Record<string, string[]> with Record<DepartmentId, string[]>
so future edits to the department taxonomy trigger TS errors for any
keyword map that drifts. Cast at the lookup site reflects the fact
that agent.department is a free-form string column that may hold
legacy or custom values.
2026-04-17 01:10:49 -04:00
ZacharyZhang-NY e892bf3366 refactor(main): simplify escalation trigger map + unify api-section curl guidance
- P3-52: replace 5-deep if-chain in deriveEscalationWakeTrigger with two
  lookup maps (status takes precedence over rule trigger), which removes
  the cognitive overhead of reading the chain and makes the two inputs'
  roles explicit.
- P3-53: rewrite the Windows-only curl warning as platform-neutral
  guidance that matches the surrounding examples. The old text said
  'prefer fetch on Windows' while the doc showed curl POSTs everywhere;
  the new text recommends fetch universally and explains when the curl
  temp-file pattern is needed.
2026-04-16 23:27:29 -04:00
ZacharyZhang-NY 32e676aec9 fix(automation): allow explicit scheduledAt / scheduleOffsetMinutes
P3-58: Automation rule's schedule_meeting action hardcoded +1hr from
now, producing meetings that could land in the past or outside working
hours. Accept scheduledAt (ISO-8601) first, fall back to
scheduleOffsetMinutes, and only default to +60min when the rule
specifies no time at all.
2026-04-16 23:06:06 -04:00
ZacharyZhang-NY d4c0d0a6bc refactor(db): move run-deliverable detection out of raw db.queryAll
P2-37: automation-run-completion.checkRunProducedDeliverable issued 5
raw SQLs via db.queryAll. Consolidate into db.runProducedDeliverable
so schema changes to knowledge_base / documents / task_comments
surface in the knowledge domain module rather than silently breaking
at the caller.
2026-04-16 23:02:43 -04:00
ZacharyZhang-NY 5403ee9aa0 fix(approval-handler): only parse workflow payload for dangerous_command type
P1-22: The opportunistic JSON.parse of payloadJson ran for every
approval, hoping to find workflowId/stepId. Gate on the actual
approval type that carries workflow metadata (dangerous_command, as
emitted by runWorkflowPipeline), and validate payload shape via zod.
2026-04-16 23:01:23 -04:00
ZacharyZhang-NY 7df0d564e5 refactor(workflow): validate workflow steps via zod schema
P1-20: normalizeWorkflowSteps used ad-hoc typeof checks that silently
accepted malformed entries (e.g. number where string was expected).
Replace with workflowStepsInputSchema zod validation; semantics for
defaults/action whitelist are preserved.
2026-04-16 23:00:45 -04:00
ZacharyZhang-NY 2df921ca88 refactor(agents): share leadership role detector, use word-boundary match
P2-23: 'lead' in system-prompt.ts matched 'cleaner' / 'leaderboard' via
substring .includes(). Hoist LEADERSHIP_ROLE_KEYWORDS into a shared
agent-role-helpers.ts module; tokenize input on non-alphanum to require
whole-word match.

P3-54: Same keyword list was duplicated in automation-skills with drift
(missing 'ceo', different members). Both sites now call through the
shared helper.
2026-04-16 22:57:35 -04:00
ZacharyZhang-NY 2deaa0d8b8 feat(automation): add per-action zod schemas for config validation
P2-29: JSON.parse(actionConfigJson) with blanket '(config.x as string)'
coerced malformed config to placeholder values silently (e.g. numeric
priority landed as 'medium'). Introduce parseActionConfig(action, json)
backed by per-action zod schemas in automation-action-schemas.ts.
Validation failures throw, surfaced by evaluateAutomationRules' logger
+ execution recorder. Individual case branches keep existing default
semantics for optional fields.
2026-04-16 22:56:18 -04:00
ZacharyZhang-NY bb397dff02 refactor(heartbeat): centralize [Heartbeat] prefix as a shared constant
P2-36: Multiple subsystems were string-coupled via the magic
'[Heartbeat]' prefix. Introduce src/main/orchestration/heartbeat-task.ts
as the single source of truth and replace scattered literals in
heartbeat dispatch, automation actions, and run completion.

P3-52: Collapse 5-deep ternary in automation-actions' wake-trigger
selection into lookup helpers (deriveHeartbeatWakeTrigger,
deriveEscalationWakeTrigger).
2026-04-16 22:53:19 -04:00
ZacharyZhang-NY 66f5e0923d fix(automation): log no-op status-change calls instead of silently returning
P2-34: Silent no-op guard hid caller bugs. Warn-log the violation so
it shows up in traces; callers should still gate.
2026-04-16 22:51:12 -04:00
ZacharyZhang-NY 4ebd98bb99 fix(automation): surface malformed condition JSON instead of silently returning false
P2-46: Previous bare try/catch hid parse errors, making rules silently
non-firing. Let JSON.parse throw; the caller (evaluateAutomationRules)
already logs and records the failure.
2026-04-16 22:51:00 -04:00
ZacharyZhang-NY 62050c9c05 refactor(automation): use JSON-stringified execution keys
P1-19: Previous string-join with ':' separator could collide when
field values contain colons (e.g. runId='a:b' vs runId='a' + previousStatus='b').
Use JSON.stringify for unambiguous serialization.
2026-04-16 22:50:38 -04:00
ZacharyZhang-NY 1a57622849 fix(automation): remove silent JSON.parse fallback in executeAutomationAction 2026-04-16 21:36:41 -04:00
ZacharyZhang-NY 9779c4d1ca update 2026-04-07 20:18:47 -04:00
ZacharyZhang-NY f5d4f8b705 update 2026-04-06 00:35:27 -04:00
ZacharyZhang-NY 9145572422 update 2026-04-05 20:14:48 -04:00
ZacharyZhang-NY 67c757f481 update 2026-04-04 12:03:50 -04:00