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.
Replace options IIFE with memoised runSelectOptions; extract magic-string
run-summary filters to SHUTDOWN_SUMMARY/WORKER_CRASH_SUMMARY constants;
localise Close console aria-label via console.close key.
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.
P1-21: worker-crash-recovery reinvented workspace resolution inline and
silently dropped the exclusive/shared distinction — a recovered run
could claim shared mode that the original run held exclusively.
Introduce resolve-workspace.ts as the single source of truth for the
task→agent→project→company fallback chain and its lockMode, used by
both run-dispatch and crash recovery.
P1-13: 100-line call to buildTaskPrompt inlined 25 snapshot filters
(meetings, knowledge, documents, approvals, connectors). Extract into
named helpers (filterUpcomingMeetings, filterRelevantKnowledge,
filterRecentDocuments, filterPendingApprovals, buildConnectorCatalog)
so each filter is individually testable and buildRunPayload reads as
a composition.
Add currentCompanyId to effect deps so switching company aborts the
previous company's pending inbox/metrics/standup fetches before stale
results can overwrite new state.
P2-49: CEO Priority 1 standup list had two '4.' entries followed by
'5.' and '6.'. Renumber to 3/4/5/6/7.
P2-45: Remove the tail 'Language Requirement Reminder' section. The
body of the prompt is English regardless of locale, so appending
'remember Chinese' after 8KB of English made the locale signal less
coherent rather than more. Keep the single up-front locale instruction.
Remove dual-source: option no longer takes selectedPaletteIndex; all reads
go through useAppStore.getState().selectedPaletteIndex inside the keydown
handler.
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.
P2-24: now.getDay() returns local time weekday but the stamp reads
'UTC', producing off-by-one-day errors near UTC midnight. Switch to
now.getUTCDay().
P2-35: The policy row is the gate that decides whether the agent runs;
pretending cooldown is 10s when the read fails hides data corruption.
db.getHeartbeatPolicy already supplies field-level defaults from policy
JSON — and the only reason to throw here is 'agent not found', which
is already impossible because the caller just loaded db.getAgent.
Five near-identical 25-line entity result blocks collapse into one
PaletteResultSection. Extract closeAndReset() callback used in 5 places.
Drop dead fallback: runs.find(...)?.id.slice(0,8) === run.id.slice(0,8).