Skip to content

Release v2026.7.7 — promote to main#471

Merged
webdevtodayjason merged 100 commits into
mainfrom
release/v2026.7.7
Jul 9, 2026
Merged

Release v2026.7.7 — promote to main#471
webdevtodayjason merged 100 commits into
mainfrom
release/v2026.7.7

Conversation

@webdevtodayjason

@webdevtodayjason webdevtodayjason commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Promotes the v2026.7.7 release commit (b4cdc1ed, tag v2026.7.7) to main, following the release-core runbook and the v2026.5.6.6 (#430) / v2026.5.6.7 (#433) promotion pattern.

Release: https://github.com/ArgentAIOS/argentos-core/releases/tag/v2026.7.7
Bundles 33 PRs since v2026.5.6.7 — Worker Runtime v2 end-to-end, approval integrity, fail-closed Rust daemon auth, unattended-installer fix, 11 critical Dependabot alerts cleared.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TCTvhCHX79hvV5DWQWqqFq

Summary by CodeRabbit

  • New Features

    • Added a new worker setup wizard with step-by-step identity, department, boundaries, simulation, review, and deploy flows.
    • Introduced support for job grades, scorecards, worker runtime leases, and canary receipt/status views.
    • Added new tools for intent management, workforce setup, onboarding packs, job reporting, and copilot system insights.
  • Bug Fixes

    • Improved dashboard/server reliability, token handling, calendar cooldowns, and Telegram payload delivery.
    • Hardened auth, startup, and release flows for more consistent behavior.
  • Documentation

    • Updated release notes, handoff, orchestration, and boundary guidance to reflect the latest platform model.

webdevtodayjason and others added 30 commits May 26, 2026 13:28
…dashboard @noble build

Two fixes for the public `curl | bash` install, both surfaced on a clean Mac
that has no compatible system Node (so the installer provisions its private
Node runtime):

- activate_pinned_pnpm invoked corepack/npm (`#!/usr/bin/env node` scripts)
  without that Node on PATH, so they died with "env: node: No such file or
  directory"; the pinned pnpm@10.23.0 was never cached and the install fell
  back to a mismatched global pnpm and aborted. Put node on PATH, set
  COREPACK_ENABLE_DOWNLOAD_PROMPT=0 (no TTY under curl|bash), and surface the
  corepack error instead of swallowing it.
- dashboard `vite build` imports shared ui/ source that needs @noble/ed25519,
  which the installer's --ignore-workspace install never provided. Declare it
  at the repo root so it resolves for the dashboard bundle. (lockfile fast-path
  falls back to a non-frozen install until a clean workspace regen lands.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror of the fix promoted to main via #433. dev still carried the drift from
v2026.5.6.7 (@noble/ed25519 added to root package.json without regenerating the
root importer in pnpm-lock.yaml), so a release cut from dev would re-break
frozen-lockfile CI. Surgical importer entry; validated with
`pnpm install --frozen-lockfile` (pnpm 10.23.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mpt (#434)

The "Type YES once you have copied this key" acknowledgment could hang
non-interactive installs two ways:

1. The skip guard checked NO_PROMPT but not NO_ONBOARD — so operators and
   the install-smoke harness setting ARGENT_NO_ONBOARD=1 (the documented
   flag) still hit the prompt.
2. The read loop swallowed EOF (`read … || ack=""`) and looped forever, so
   a readable-but-empty tty (Docker -t in CI, the smoke harness) spun
   indefinitely instead of continuing.

Honor NO_ONBOARD in the skip condition, and break the loop on EOF. Real
interactive `curl | bash` operators still get the YES gate (their
/dev/tty is readable and returns input); only unattended runs auto-skip.
The key is already persisted to ~/.argentos/.master-key, so the prompt is
awareness-only — safe to skip when there's no human.

Closes #429
…istillation merge) (#435)

* wip(business-merge): land distillation slices (interrupted mid-flight by usage ceiling)

Recon + landing map complete. Slices A/B/D/E largely on disk; slice C
(execution-worker-runner union-merge, jobs.test.ts deletion) incomplete.
NOT verified — tsgo/tests/lint not yet run on this state. See HANDOFF.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: 2026-06-10 session handoff (merge state, decisions, resume order)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(business-merge): green-loop pass — adapt landed tools to core interfaces, unify runner types

- tools: canonical (toolCallId, params) execute, label not type/strict, textResult details
- copilot run-story: storage.tasks.get() replaces dropped JobTaskContext.task (tool + server-method; clears known-failing copilot.ts:225)
- server-methods/types: real ExecutionWorkerRunner replaces local Like clone (clears 4 known-failing TS18046)
- lint: no-shadow renames, sync loadConfig, String(err); oxfmt over changed src files
tsgo: 206 errors, all baseline (dev=216, −10, +0 new). Targeted: 49/49 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(business-merge): retire overlay machinery + update sentinels to one-repo reality

- delete optional-module/optional-tool-factory loaders (all seams now static; loaders never resolved in bundled runtimes)
- delete public-core export pipeline (export-public-core, public-core-export, denylist test — repo is one open codebase)
- AGENTS.md/.argent-repo.json/README/persona-prompt/core-business-boundary.md: business layer is first-class core code; open-source + Titanium services model (2026-06-10)
- dashboard tsconfig lib ES2022→ES2023 (toSorted/toReversed; clears 69 project errors incl. all 6 merge-introduced)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(release): bump to 2026.6.10-dev.0 + session ledger/handoff for business-merge verification

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: handoff — #405 lane complete (PR #436)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(types): clear 8 baseline TS errors in merge-touched files (tsc-since line-shift)

tsc-since keys on file:line, so the merge's line shifts surfaced these
pre-existing errors as net-new. Fixed for real instead of re-baselining:
- personal.ts: annotate matcher map return (SkillMatchCandidate | null) —
  clears the type-predicate direction error + 4 null cascades + TS2322
- sis-runner.ts: contract cast via unknown
- server.impl.ts: drop bogus { config } option ensureAuthProfileStore
  never accepted

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…(writes + reads) (#436)

* perf(#405): take personal-skill bookkeeping writes off the turn critical path

The personal-skill block (added 2026-05-03, attempt.ts ~525-609) runs ~5
sequential memory-backend awaits on every turn, eagerly and serially BEFORE
the QW-1 parallel-I/O batch — so it blocks while nothing overlaps. Phase
decomposition of 486 real May operator turns shows this pre-tool window is
~81% of slow-turn wall-clock (mean ~3.6s on slow turns); slow-turn rate
doubled April->May (12%->25%), matching this block's introduction date.

This commit lands the safe, certain part:
- createPersonalSkillReviewEvent (review-event audit write) and the per-skill
  lastUsedAt updates are bookkeeping whose results are NOT consumed in the
  turn. Make them fire-and-forget (void + .catch) so they don't block the
  critical path. Safe in the persistent gateway process, which flushes them.
- Add markPhase("personal_skills") so the remaining READ-side cost
  (getMemoryAdapter + reviewPersonalSkillCandidates + listPersonalSkillCandidates)
  is measurable separately from async_io_started, which currently lumps it in.

Read-side fix (cache candidates per agent / parallelize into QW-1 / gate when
empty) is the next slice, sized by the new phase marker.

Refs #405

* perf(#405): cache + parallelize the personal-skill read path off the turn critical path

The block's reads (review O(N) serial chain + list) now go through a
per-agent 60s TTL cache (src/agents/skills/personal-skill-read-cache.ts)
and the whole block joins the QW-1 parallel I/O batch, with its outputs
consumed after the batch await (dependency-verified). Mutation paths
invalidate: personal-skill tool author/patch, gateway skills update/
resolve-conflict/delete, live-inbox candidate create/reinforce, SIS
candidate creation, and usage state TRANSITIONS (counter bumps stay
cached — invalidating per-run would defeat the cache).

Measured (isolated gateway, 24 candidates + 30k review rows, PG 5433):
- critical-path personal-skill backend reads: 2/turn -> 0/turn warm
  (pg_stat_user_tables deltas; cold fill = 2 reads, overlapped in-batch)
- warm-turn batch envelope 16-18ms incl. personal skills (was serial
  pre-async_io); query elimination is contention-immune — the 1482ms
  cold spikes measured on the operator gateway cannot recur warm.

Cache contract pinned by 7 unit tests (TTL, isolation, invalidation,
failure-no-cache, concurrent dedupe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(types): clear shifted-baseline TS errors flagged by tsc-since

Same fixes as the merge branch: matcher map return annotation
(SkillMatchCandidate | null), SIS contract cast via unknown, drop the
bogus ensureAuthProfileStore { config } option.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(types): keep server.impl.ts at dev state on this branch — its baseline errors are properly fixed in the merge-back PR (#435); editing it here only shifts tsc-since line keys

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump to 2026.6.10-dev.1 per dev version contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#437)

Ports the stranded live-install hot-patch (preserved in the install
stash 'hot-patch-preserved-2026-06-10') upstream: canonicalize
favorite-slot keys by first token for stable singleton properties
(color/number/voice) so exact stored facts outrank adjacent noise in
reranking, with a gold-set regression test (the favorite-color case).
Restores recall behavior the operator's install had before tonight's
update. Bumps to 2026.6.10-dev.2.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The May-24 snapshot drifted: tonight's merges fixed 18 baseline errors
but tsc-since keys on file:line, so edits in files with remaining
baseline errors (memu-tools.ts) shifted lines and tripped CI as
net-new. Snapshot now matches dev reality; the memu-tools type-model
drift (query-intent union, recall item nesting) stays tracked in the
baseline and the Follow-Ups closet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…at init (#439)

A brand-new install's Postgres had no path to a working schema: PgAdapter
init assumed core tables existed (created historically by hand-run
drizzle-kit push + migrations + ensure-pg-tables.sh) and died on
'relation tasks does not exist'; even with schema, the first orchestrator
task insert died on the tasks->agents FK because nothing seeded the
agent row. Found during the 2026-06-10 governed-spine smoke.

- src/data/pg/bootstrap-schema.ts (generated): full schema dump (39
  tables, extensions, RLS policies), applied by init ONLY when both
  tasks and agents relations are absent — existing databases skip it
  (verified: no bootstrap attempt against a populated DB).
- seedAgentRows at init (configured agent + main) and an idempotent
  agent upsert in PgTaskAdapter.create for late-created family workers.
- scripts/generate-pg-bootstrap.ts documents + automates regeneration.
- Declares PgAdapter.agentId (clears 3 baseline TS errors; 198 -> 195).

Verified end-to-end: zero-table database -> gateway boot -> 39 tables +
agents seeded in ~85ms -> jobs.templates.create -> assignments.create ->
runNow creates the task with no FK error.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(dashboard): mount the worker wizard from the Workforce Board roster

The guided worker-wizard (landed in #435 but unreachable) gets its entry
point: a 'Guided Worker Wizard' button beside the manual create-worker
form, modal-rendered with the board's gatewayRequest and a roster reload
on close. Dashboard tsc stays at the 77-error baseline; vite build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump to 2026.6.10-dev.5 per dev version contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…e markers (#441)

- scripts/demo/conductor-demo.mjs: seeds six MSP tickets ([TICKET] board
  tasks) + the 'Ticket Triage Conductor' simulate-stage job template
  (toolsAllow: tasks, memory_recall); optional --assign/--run-now.
- model-auth: LM Studio/Ollama get the same keyless carve-out at runtime
  that automatic routing already had — a configured local model now runs
  without an auth profile (worker cycles died on 'No API key' before).
- api-server: resolveGatewayConfigToken honors ARGENT_STATE_DIR instead
  of hardcoding ~/.argentos (state-dir isolation leak; sandboxed
  gateways 401'd against the operator's real token).
- attempt.ts: three bootstrap phase markers (skill_entries,
  skills_prompt, session_store) bracketing the measured ~1.3s cold-turn
  window so the cost is attributable from [tony-stark] lines.

Demo rehearsal findings (full log in ops/ledger): worker executed live
against lmstudio/google/gemma-4-12b; a 2-tool worker turn still ships a
~43k-token system prompt (n_keep 43019 vs n_ctx 4096) — concrete #407
evidence.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…nly (#443)

Rehearsal showed the worker's progress contract requires the agent to
update ITS OWN job task (boardChanged); the original SOP's blanket
'do not modify tasks' blocked every run. Tickets stay read-only; step 5
now instructs completing the assigned work task. Bumps 2026.6.11-dev.0.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…finding)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ection (#442) (#444)

Local models under the worker job prompt sometimes WRITE tool
invocations as message text (gemma: '<|tool_call>call:tasks…',
qwen: '[ tasks: list… ]') — nothing executes, runs silently burn
attempts. The worker now: (1) states the native-call-only rule in the
execution prompt, (2) detects the observed pseudo-call dialects in
no-tool attempts (looksLikeTextualToolCall, pinned by tests),
(3) re-prompts the next attempt with an explicit FORMAT CORRECTION,
and (4) logs the detection for diagnosis before honest auto-block.

Measured tonight (isolated gateway, 6-ticket conductor template):
- gemma-4-12b-QAT fires native tool calls on plain turns (114s round
  trip) but cannot hold format under the ~40k-token worker prompt even
  with correction; qwen3.6-35b-a3b likewise → local conductors need the
  #407 prompt shrink; guard makes the failure visible + bounded.
- gemma-4-12b Q4_K_M emits the textual dialect even on PLAIN turns —
  QAT variant is strictly better for tool work.

Cloud providers unaffected (directive is universal-safe; detection only
inspects failed no-tool attempts). Bumps 2026.6.11-dev.2.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…orkers (#442, #407 worker lane) (#447)

P1 of Worker Runtime v2 (ops/WORKER_RUNTIME_V2_DESIGN_2026-06-11.md), measured
live on a sandboxed gateway against lmstudio/google/gemma-4-12b-qat:

- Worker lane runs with promptMode:"minimal": no context files, personal
  skills, cross-channel context, or heartbeat prompt — the task prompt
  carries the role contract. Measured: worker system prompt ~40k -> 2,056
  tokens (8,222 chars); LM Studio prompt n_tokens 43,019 -> ~2.7k; turn
  round-trip 114s -> ~10s.
- lmstudio/ollama route through the Argent OpenAI provider even in pi_only
  runtime mode. Root cause of #442 confirmed on the wire: pi's
  openai-completions driver ships tool specs as prompt text and the POST
  body carries NO native "tools" field, so small local models can only
  answer with textual pseudo-calls. Keyless carve-out for local runtimes
  (client-site boxes run without cloud keys).
- pi's agent loop passes context.tools EMPTY to the stream layer (all tools
  ride as customTools) — merge the policy-filtered toolset into the stream
  context on the argent-provider path. Measured: gemma-4-12b-QAT answers
  finish_reason:"tool_calls" with a correct native `tasks list [TICKET]`
  call, runs a multi-turn tool loop, zero pseudo-call detector hits.
- P0 rider (#445): jobs.assignments.runNow names the gating open/blocked
  task (blockedBy + hint) instead of returning a silent OK. Resolved via
  job runs because tasks.block replaces task metadata.
- Fix session tool-grant read side: cfg.session.store, not cfg.sessionStore
  (field never existed) — a configured custom store path silently bypassed
  session tool grants.
- New regression test pins worker grant filtering end-to-end (write path
  identical to withSessionToolPolicyOverride, custom store path covered).

Known residual, by design: gemma triages via native tool loop but does not
end by mutating its own task row, so v1's boardChanged contract still
auto-blocks the run — that is the work_report gap (v2 D5, lands in P4).

Verification: full suite at dev baseline parity (42 failed files / 134
failed tests, identical list; the +1 seen in some runs is model-auth.test's
pre-existing env-mutation flake, closet-logged). Touched-file lint at
baseline. check-invariants 3/3. tsgo clean on touched files.

Refs #442 #407 #445 #444

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…TES on gemma (v2 D5) (#450)

* feat(worker): work_report completion contract — conductor demo COMPLETES on gemma (v2 D5 / P4-lite)

Workers now prove progress by FILING A REPORT; the runner updates the board.
Measured live on a sandboxed gateway, lmstudio/google/gemma-4-12b-qat: run
status COMPLETED (review pending) with a 2,934-char triage log stored on the
run record, all six demo tickets untouched, zero worker board mutations.

- work_report tool (done|blocked|need_input + summary + evidence), registered
  unconditionally, auto-granted on every worker turn (runner appends it to the
  session toolsAllow override). Reports land in a globalThis-anchored registry
  (tsdown duplicates the module across bundle entries) keyed by runId AND
  sessionKey (cached sessions can hold a stale runId closure).
- Runner consumes the report after each turn: done -> tasks.complete + run
  completed with report.summary as the run summary and the report stored in
  run metadata; blocked/need_input -> tasks.block with the report reason; no
  report -> existing no-progress path with reason "no_report".
- Job-task execution prompt now states the completion contract; the v1 ending
  block ("mark it completed / update task state / return a summary") competed
  directly with read-only SOPs and is now non-job-only. Conductor demo SOP
  step 5 de-hacked (#443): file work_report, zero board mutations.
- ROOT-CAUSE FIND while landing this: createAgentSession dropped
  options.customTools entirely — _tools was EMPTY on the embedded pi_only
  path, so EVERY model tool call returned "Tool X not found" while requests
  still declared the tools natively. v1's boardChanged acceptance masked the
  whole class (#423 silent-tool-skip, biggest instance). Fixed by registering
  customTools in the executable registry (ToolDefinition is structurally an
  AgentTool); a permanent "tool lookup miss" diagnostic now logs the registry
  on every miss. With execution real, gemma's triage cites actual board ids.
- Boy-scout: run-record blockers now read the metadata keys tasks.block/fail
  actually write (blockReason/failReason, not blockedReason/failureReason).

Correction to the #447 measurement note: the "multi-turn tool loop" observed
there was the model retrying after "Tool not found" errors — the native
tool-call CHANNEL was fixed, but execution was broken until this commit.

Verification: full suite at dev baseline parity (42 failed files / 134 failed
tests, identical list; +7 new passing). Touched-file lint at baseline (-1).
tsgo clean on touched files.

Refs #443 #423 #442 #407 · builds on #447

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump dev version 2026.6.11-dev.3

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ernel inference (LIMBIC law 3) (#451)

* perf(kernel): deterministic salience gate — an idle gateway burns zero kernel inference (LIMBIC law 3)

The consciousness kernel asked a model "anything on your mind?" every tick
(default 120s = 720 inferences/day) and ran the executive cycle (memory
research = embedding/reranker inference, web evidence) per tick behind a
budget gate whose default (0) never engages. The idle-activity gate only
suppressed the never-any-activity case — any operator activity within 30
minutes opened ungated cognition every 2 minutes all day. Net effect:
Jason's box runs hot whenever he's awake (the EVY-KERNEL-DESIGN.md autopsy's
"720 dense inferences at 157F", live in our logs).

The tick now computes deterministic salience FIRST — cognition (inner
reflection + executive cycle) runs only when:
  (a) operator activity happened since the last cognition,
  (b) the task board changed (max updatedAt / count via a non-blocking
      cached probe — the tick never awaits storage), or
  (c) the salience anchor is due (new agents.defaults.kernel
      .salienceAnchorHours, default 24h; 0 disables).
Otherwise the tick is pure arithmetic and journals a first-class
"salience-skip" decision (skip(no-salience)) — why-didn't-you-reflect is
answerable from the decision ledger.

Self-stimulation guard (LIMBIC ruling 7): every embedded run — including
cron workers and heartbeats — recorded a kernel conversation turn whose
prompt counted as operator activity, so background chatter held the gate
open forever. Runs now carry origin: operator|background; background lanes
sync conversation continuity but are zero-weighted for salience
(conversation.lastOperatorActivityAt is operator-only).

Config semantics unchanged: enabled/mode/tickMs/idleActivityGateMinutes/
budgets all mean what they meant; the inner idle gate still applies after
the salience gate. State fields are additive and hydrate tolerantly from
old self-state files.

Tests: 10 new salience-gate cases incl. a simulated quiet week (5,040 ticks
-> exactly 7 anchor cognitions, 100% first-class skips otherwise); kernel
suite updated where it encoded cognize-every-tick (38/38 green).

Refs EVY-KERNEL-DESIGN.md (LIMBIC/1) sections 5-6 and 10 · #438 groundwork

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: kernel interim note in v2 design (LIMBIC adoption, idle-inference inventory)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…rify ABI (#416) (#448)

prebuild-install fetches binaries keyed to the ABI of the Node RUNNING the
package manager, not the Node the gateway loads them with. On a machine with
shell nvm Node 22, gateway plist node@22, and Homebrew node 26, `argent
update` (spawned with launchd/Homebrew PATH) landed an ABI-147 better-sqlite3
that the ABI-127 gateway cannot load — aos-lcm failed to register on every
boot after update (hit live again 2026-06-11 21:12).

Three layers, per the issue's proposed design:

1. Resolve the gateway service's pinned Node from the service definition
   (LaunchAgent plist / systemd unit argv[0], via resolveGatewayService) with
   fallback to the updater's own execPath.
2. Prepend its bin dir to PATH for every native-affecting step: git-mode
   "deps install" + "rebuild native modules", and npm/pnpm-mode
   "global update" — so install scripts run under the gateway's Node and
   prebuild-install fetches the matching ABI.
3. Post-update failsafe: load better-sqlite3 under the gateway's Node
   ("verify native ABI (gateway node)" step). On failure, force a
   from-source rebuild (npm_config_build_from_source) under the gateway's
   Node and re-verify; the retry-step naming reuses the existing
   isRecoveredStepFailure convention so a healed verify doesn't fail the
   update, while an unhealed one names the failed stage.

Tests: 4 new cases (gateway-node PATH on global update + verify step, heal
path with from-source rebuild under gateway node, unhealed mismatch reports
the failed stage, non-node service argv falls back to process.execPath).
All 16 existing update-runner tests unchanged and green.

Closes #416

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…updater ABI fix #448)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two field findings from Jason's box being stuck on 2026.6.10-dev.1:

1. The #413 regenerated tracked artifacts (dist/control-ui, dashboard
   provider-catalog, dashboard _generated) are excluded from the dirty CHECK
   but still block `git rebase` — so the first successful build wedged every
   subsequent update at "rebase-failed". Restore them before rebasing, same
   as pnpm-lock.yaml (committed versions are the sanctioned fallbacks; the
   build step rewrites them immediately after).

2. pi-tools passed disableImageTool/disableImageGenerationTool to
   createArgentTools, which neither declared nor honored them — a silent
   no-op AND the tsc error that update preflight counted as net-new against
   older baselines, failing preflight builds on every new commit. The flags
   now exist and actually omit the image tools.

Bumps 2026.6.11-dev.5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-drafted, ground rules, live-system facts)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… compiled role profiles (#407/#442)

The blank-slate law becomes structural. Job runs now execute in ephemeral
per-run sessions (agent:<id>:worker:<assignmentId>:<runId>) born with the
role profile's tool grants on the session entry and discarded after the
run — withSessionToolPolicyOverride (the mutate/restore hack on the shared
session store) is deleted.

- src/agents/worker-role-profile.ts: templates compile to role profiles —
  system prompt = company alignment (intent global+department) + role
  contract (rolePrompt/SOP/successDefinition/relationship) + execution-mode
  directives + tool rules; toolsAllow = grants + work_report (default-deny:
  empty grant = report-only worker). Cached per (templateId, updatedAt,
  mode, stage), invalidated on template edit. D8: template.metadata.model
  rides the profile (template → agent worker model → default).
- systemPromptOverride threaded agentCommand → runEmbeddedPiAgent →
  runEmbeddedAttempt: a profile run ships EXACTLY the compiled prompt — the
  embedded builder is skipped and the session bootstrap hint (operator
  session state) is not appended. Skill-snapshot build skipped for override
  runs (ephemeral sessions would rebuild it every run for nothing).
- Worker runner: job-task messages slim to task + assignment facts (role
  contract no longer repeated per task); agent-level intent hint stays on
  the non-job (own-queue) lane only; stage-driven denies remain
  runtime-resolved per assignment.
- Tests: profile compile inspection (forbidden operator markers), grants
  structure, mode directives, cache hit/invalidation; ephemeral session
  create/grant-filter/dispose (+ transcript removal,
  ARGENT_WORKER_KEEP_SESSIONS=1 escape hatch for law audits).

Suite: 42 failed files / 134 failed tests — exact dev baseline parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(worker): Worker Runtime v2 P2 — blank-slate ephemeral sessions + compiled role profiles
…tbeat, halt, boot sweep, full runNow semantics (#445, #425)

D4+D6 from ops/WORKER_RUNTIME_V2_DESIGN_2026-06-11.md, all measured live on
the sandbox gateway (gemma-4-12b-QAT, scratch PG).

Lease protocol (TaskAdapter, pg + sqlite + dual):
- claim() — atomic CAS: pending or lease-lapsed in_progress → in_progress
  with claimedBy/claimTtl/claimAcquiredAt; 0 rows = lost, pick next
- heartbeatClaim() — runner refreshes TTL every 60s while the turn is in
  flight; heartbeats stop at maxRunMinutes so a wedged call lets the lease
  lapse instead of holding it forever
- releaseClaim(requeue) — pending + attempt++, reason in metadata
- sweepExpiredClaims({orphanAll}) — TTL expiry each cycle; boot-time orphan
  sweep requeues every held lease (crash recovery = restart time, not TTL)
- terminal statuses clear the lease in both stores; new additive columns
  claimed_by/claim_ttl/claim_acquired_at/attempt via the existing
  ADD-COLUMN-IF-NOT-EXISTS path (+ sqlite try/catch ALTERs)

Halt (#423 item 5): execution.worker.halt gateway method — pause scope +
cooperative abort at T=0 + unconditional abortEmbeddedPiRun at T=30s grace,
no worker ack. Aborts surface as meta.aborted normal returns (drill 1
caught the catch-only handling re-claiming the task 85ms after halt) — the
runner now handles both flavors and checks pause per loop iteration.
Measured: halt → task blocked + cycle stopped in 66ms.

runNow full semantics (#445): blocked gating task re-queues as a fresh
attempt by default (attempt history carried, block reason cleared);
{fresh:true} supersedes (cancelled + metadata.superseded) and cuts a new
task (re-arming nextRunAt — first ensureDueTasks pass consumes due-ness);
pending → dispatch now; in_progress → named, left alone. The response
always names what it did.

Persistent attempt counter feeds auto-block: a task at maxNoProgressAttempts
requeues (crash loop, lease expiry) is blocked at pick time.

Boy-scout: fixed 2 pre-existing tsc errors in sqlite-to-pg.ts (heterogeneous
table list typing; AgentRole union mismatch).

Tests: 7-case lease protocol suite (sqlite module); full suite at baseline
parity (43/135 incl. catalog.test.ts concurrency flake — passes in
isolation, same class as documented model-auth flake).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(worker): Worker Runtime v2 P3 — lease lifecycle (claim/heartbeat/halt) + full runNow semantics
…e remaining fan-spinners (LIMBIC law 3, #451 follow-through)

An idle default-config box burned ~96 full agent turns/day between the
30m heartbeat and 30m contemplation loops. Both now pre-gate with the
kernel's deterministic salience arithmetic (decideTickSalience reused):
an interval beat/cycle runs only on operator activity since the last
salient run, a task-board delta, or a due anchor — otherwise it skips
with first-class reason "no-salience".

- src/infra/idle-salience-gate.ts: shared gate — kernel's non-blocking
  cached board probe + session-store __lastUserMessage as the operator
  signal; in-memory per subsystem+agent (first due run after restart is
  the designed baseline admission); salient verdicts consume salience.
- heartbeat: gate applies ONLY to reason=="interval" — exec events,
  cron wakes, and manual triggers carry their own salience and bypass.
- contemplation: gate is the last check before dispatch (after
  agent-busy) so consumed salience is never wasted by a downstream skip.
  Bare idleness is no longer salience — contemplate once after activity
  settles, then quiet until something happens or the anchor.
- Config (additive, registered in types + zod + schema labels):
  {heartbeat,contemplation}.salienceGate (default true) and
  .salienceAnchorHours (default 24, 0 disables the anchor).
- ops/known-failing.json refreshed 198 → 191: my line-shifts re-keyed,
  7 already-fixed errors dropped, zero new entries (verified by
  line-insensitive diff).

Suite: 42 failed files / 134 failed tests — exact dev baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(isolation leak #5)

The dashboard api child read storage config from the hardcoded
~/.argentos/argent.json and preferred it over the ARGENT_PG_URL env —
so a sandboxed gateway's api child silently connected to the operator's
live Postgres (the 2026-06-09 live-board incident).

- STORAGE_CONFIG_PATH: ARGENT_CONFIG_PATH → ARGENT_STATE_DIR/argent.json
  → $HOME/.argentos/argent.json (same chain as the gateway auth path)
- resolvePgConnectionString: explicit env (ARGENT_PG_URL/DATABASE_URL)
  now wins over config — an exported env var is operator intent and is
  exactly how sandbox isolation passes the scratch DB
- readStorageConfigSummary reuses the same resolution

Remaining bare $HOME config reads in this file are closet-owned (one
shared resolver sweep).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… gateway.log incident)

launchd appends to <stateDir>/logs/gateway.log forever with no system
rotation. New src/infra/gateway-log-rotation.ts: every 10 min, files over
64MB get their last 8MB preserved to <file>.1 and are truncated IN PLACE —
launchd holds an O_APPEND fd, so truncate keeps working where a rename
would silently rotate nothing. Wired into gateway startup next to the
health checks; stopped on close. Tests pin the same-inode + tail-preserved
contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…191 errors, zero new)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Folks, this PR is a BEAUTIFUL, TREMENDOUS merge — we're bringing all the Business stuff HOME into core, no more licensed overlay nonsense, believe me. We built Worker Runtime v2 with lease claiming, grading, role profiles — the best runtime, nobody's seen anything like it. We hardened the Rust daemons with real bearer-token auth and canary receipts, built a GORGEOUS new Worker Wizard dashboard, moved service-keys to async (tremendous efficiency), and bumped every extension version. HUGE.

Changes

Business-to-Core Merge & Runtime

Layer / File(s) Summary
Docs, ops runbooks, changelog
AGENTS.md, README.md, CHANGELOG.md, ORCHESTRATION.md, HANDOFF.md, docs/concepts/core-business-boundary.md, ops/*
Rules and history rewritten to say one repo, one truth — Business lives here now, license server stays outside, folks.
Worker Runtime v2 backend
src/data/tasks.ts, src/data/pg-adapter.ts, src/data/pg/schema.ts, src/gateway/server-methods/jobs.ts, src/agents/worker-role-profile.ts, src/agents/tools/work-report-tool.ts
Lease claiming, grade events, cached role profiles, and a work_report contract — a truly disciplined worker lifecycle.
Business tools merged in
src/agents/tools/intent-tool.ts, jobs-tool.ts, workforce-setup-tool.ts, copilot-system-tool.ts, onboarding-pack-tool.ts, src/agents/argent-tools.ts
All those great tools statically wired in — no more optional loading, they're PERMANENT now.
Service-key async migration
src/agents/tools/*-deploy-tool.ts, email-delivery-tool.ts, audio-*-tool.ts, namecheap-dns-tool.ts
Every service-key lookup upgraded to async, cleaner, faster, WINNING.
Rust auth & canary receipts
rust/argent-execd/src/*.rs, rust/argentd/src/*.rs
Bearer-token auth, fail-closed by default, plus redacted canary receipt generation — SECURE, tremendously secure.
Worker Wizard dashboard
dashboard/src/components/worker-wizard/*
A brand new, beautiful multi-step wizard for standing up workers — the best UI, nobody builds it like this.
Doctor-auth, workflows, dashboard fixes
src/commands/doctor-auth.ts, src/gateway/server-methods/workflows.ts, dashboard/api-server.cjs
Cleaned up deprecated auth profiles across every agent store, fixed workflow tool-grant validation, and fixed calendar/token resolution.
Extension version bumps
extensions/*/package.json, extensions/*/CHANGELOG.md
Every single extension bumped to 2026.7.7 — total alignment, total victory.

Estimated code review effort: 5 (Critical) | ~180 minutes

Possibly related PRs

Suggested labels: gateway, docs, cli, commands, scripts, agents

Poem

A rabbit hopped through core and business lanes,
Merged the tools, no overlays, no chains.
Rust got tokens, receipts got redacted,
A Wizard was born, beautifully crafted.
🐇 Believe me — this PR is TREMENDOUS, the best release! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is far too thin and misses the required template sections for scope, validation, runtime impact, risk, and logs. Rewrite the PR description to follow the template: add Summary, Scope, Validation, Install/Runtime impact, Risk, and Screenshots/Logs sections.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, specific, and accurately summarizes the release promotion to main.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/v2026.7.7

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
ORCHESTRATION.md (1)

224-300: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Huge, tremendous log, folks — but maybe not for a public repo.

This whole file reads like a raw internal orchestrator transcript: the operator's real first name, granular pricing/revenue strategy ("Revenue model = Titanium services... People are still going to pay us"), and internal PR/issue play-by-play. The learnings on file say this repo is being treated as the single source of truth and — per the log itself — open-sourcing was approved. Committing an unredacted internal strategy/decision log with a named individual into permanent git history of a repo headed public is a privacy/business-sensitivity risk that's very, very big league, not good.

Recommend moving this kind of session log to a private ops channel/vault (the file itself even mentions an Obsidian vault as the "durable record") rather than tracked git history, or scrub names/strategy specifics before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ORCHESTRATION.md` around lines 224 - 300, This session log contains sensitive
internal transcript details, including a real person’s name and business
strategy/revenue discussion, which should not be committed as-is. Move the raw
orchestrator record out of tracked git history into a private ops/vault
location, or redact names and strategy-specific details before merging; update
the relevant documentation section in ORCHESTRATION.md to point to the
sanitized/private record. Use the existing “Session — 2026-06-10 evening” and
“Decision Log” sections as the cleanup targets.

Source: Learnings

rust/argentd/src/http.rs (1)

1377-1394: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use json_string for dynamic hello-response strings too.

request.id and subscription entries are still manually interpolated/escaped, so control characters can produce invalid JSON even though the new helper handles them correctly.

Proposed direction
-        request.id,
+        json_string(&request.id),
...
-            .map(|entry| format!("\"{}\"", entry.replace('\\', "\\\\").replace('"', "\\\"")))
+            .map(|entry| json_string(entry))

This also requires changing the format string’s "id":"{}" slot to "id":{}.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/argentd/src/http.rs` around lines 1377 - 1394, The hello-response JSON
in the `build_hello_response` path still manually interpolates dynamic strings,
so `request.id` and the subscription list can break escaping; switch these
fields to use `json_string` like the other response fields. Update the format
template in `http.rs` so the `"id"` slot uses the already-escaped JSON value
(`"id":{}` instead of a quoted placeholder), and apply the same helper when
serializing each subscription entry in the `request.subscriptions` mapping.
src/agents/pi-embedded-runner/run/attempt.ts (1)

546-641: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Skip personal-skill reads and writes for minimal/override runs.

Minimal runs omit the prompt blocks later, but the personal-skill promise has already reviewed candidates, emitted lifecycle events, and written review/lastUsedAt bookkeeping. That violates the blank-slate behavior described on Lines 695-697.

Proposed direction
+    const promptMode: PromptMode =
+      params.promptMode ?? (isSubagentSessionKey(params.sessionKey) ? "subagent" : "full");
+    const isMinimalPromptRun = promptMode === "minimal" || promptMode === "none";
+
     const personalSkillsStartedAt = Date.now();
-    const personalSkillsPromise = (async () => {
+    const personalSkillsPromise =
+      isMinimalPromptRun || params.systemPromptOverride
+        ? Promise.resolve(null)
+        : (async () => {
           const memory = await getMemoryAdapter();
           ...
-    })().catch((err) => {
-      log.debug(`personal skill review unavailable: ${String(err)}`);
-      return null;
-    });
+        })().catch((err) => {
+          log.debug(`personal skill review unavailable: ${String(err)}`);
+          return null;
+        });

Then remove the later duplicate promptMode / isMinimalPromptRun declaration.

Also applies to: 693-706, 921-938

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agents/pi-embedded-runner/run/attempt.ts` around lines 546 - 641, The
personal-skill work in attempt.ts is still running for minimal/override turns,
causing candidate reads, lifecycle events, and bookkeeping writes before the
prompt blocks are skipped. Gate the entire personal-skill promise path in the
run flow so it does nothing when promptMode indicates a minimal or override run,
and make sure the later prompt-mode check is reused instead of redeclared.
Update the logic around personalSkillsPromise, params.onAgentEvent,
createPersonalSkillReviewEvent, and updatePersonalSkillCandidate so they are
only executed for non-minimal runs.
🧹 Nitpick comments (28)
ops/runbooks/release-core.md (1)

172-192: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Big, beautiful trap warning — but the actual command doesn't protect you from it, folks!

The comment block correctly warns that ARGENT_INSTALL_URL defaults to the LIVE, previous-release installer. But the runnable command on Line 184 is just the bare ARGENT_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke — none of the local-export workaround from Lines 179-183 is baked into it. Anyone who just copies the "real" command (not the comment lines) will smoke-test the wrong installer and still get a green gate. Not good, not good at all for a step marked irreversible after Step 5.

Consider making the safe path the default runnable command, or adding an explicit assertion/echo of which ARGENT_INSTALL_URL is in effect before the gate proceeds.

📝 Suggested tweak to make the safe path the default
-# 4g. Docker installer smoke (required before EVERY release)
+# 4g. Docker installer smoke (required before EVERY release)
+#     Default to gating the NEW installer explicitly rather than relying on
+#     operators to remember to override the trap manually:
+pnpm export:hosted-installers
+python3 -m http.server 8765 --directory dist/hosted-installers &
+ARGENT_INSTALL_URL=http://host.docker.internal:8765/install.sh \
+ARGENT_INSTALL_CLI_URL=http://host.docker.internal:8765/install-cli.sh \
 ARGENT_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke

Since this depends on the behavior of scripts/test-install-sh-docker.sh (not in the reviewed file set), please confirm the harness doesn't already guard against this before treating it as required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ops/runbooks/release-core.md` around lines 172 - 192, The runnable smoke-test
command in release-core.md currently omits the local installer export/serve
steps, so it can still gate against the live previous-release installer by
default. Update the documented command near the Step 5 smoke test so the safe
path is the default, using the existing release/export workflow shown in the
same section and the smoke harness entrypoint pnpm test:install:smoke;
alternatively, add a clear pre-flight check or echo of the effective
ARGENT_INSTALL_URL before running the gate so copy-paste users cannot miss the
trap.
src/data/tasks.ts (2)

605-611: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

String-interpolated ${Number(now)} in the sweep query — swap it for a bound parameter, no reason to live dangerously.

Number() coercion mitigates real injection risk since now is a number, but binding it as a ? parameter (like everywhere else in this file) is cheap, consistent, and future-proof against someone loosening the type later.

🧹 Proposed fix
-    const where = opts?.orphanAll
-      ? `status = 'in_progress' AND claimed_by IS NOT NULL`
-      : `status = 'in_progress' AND claimed_by IS NOT NULL AND COALESCE(claim_ttl, 0) <= ${Number(now)}`;
-    const rows = db.prepare(`SELECT * FROM tasks WHERE ${where}`).all() as TaskRow[];
+    const where = opts?.orphanAll
+      ? `status = 'in_progress' AND claimed_by IS NOT NULL`
+      : `status = 'in_progress' AND claimed_by IS NOT NULL AND COALESCE(claim_ttl, 0) <= ?`;
+    const rows = opts?.orphanAll
+      ? (db.prepare(`SELECT * FROM tasks WHERE ${where}`).all() as TaskRow[])
+      : (db.prepare(`SELECT * FROM tasks WHERE ${where}`).all(now) as TaskRow[]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/data/tasks.ts` around lines 605 - 611, The sweepExpiredClaims query in
tasks.ts is still interpolating now directly into the SQL string via
Number(now); update this method to use a bound parameter for the claim_ttl
comparison instead. Keep the existing sweepExpiredClaims signature and where
logic, but pass now as a prepared statement parameter in
db.prepare(...).all(...) the same way other queries in this file do, so the SQL
stays consistent and future-proof.

118-131: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No index for the new lease-query hot path — could get slow at scale, and nobody likes slow, believe me.

claim()'s WHERE and sweepExpiredClaims()'s candidate SELECT both filter on status/claimed_by/claim_ttl, but the migration only adds columns, no supporting index. On a big tasks table this becomes a full scan on every claim attempt and every sweep cycle.

⚡ Proposed addition to the migration loop
     for (const ddl of [
       "ALTER TABLE tasks ADD COLUMN claimed_by TEXT",
       "ALTER TABLE tasks ADD COLUMN claim_ttl INTEGER",
       "ALTER TABLE tasks ADD COLUMN claim_acquired_at INTEGER",
       "ALTER TABLE tasks ADD COLUMN attempt INTEGER DEFAULT 0",
+      "CREATE INDEX IF NOT EXISTS idx_tasks_lease ON tasks(status, claim_ttl)",
     ]) {

Also applies to: 605-611

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/data/tasks.ts` around lines 118 - 131, The lease-column migration in
tasks.ts adds the new fields but no index for the hot query path, so add a
supporting index in the same migration loop for the columns used by claim() and
sweepExpiredClaims() (status, claimed_by, claim_ttl, and related lease filters).
Update the migration block that currently runs the ALTER TABLE statements so it
also creates the appropriate index, and make sure it is safe to re-run if it
already exists.
src/agents/tools/onboarding-pack-tool.ts (2)

266-286: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

A little duplication here, folks — same pattern three times over.

The Array.isArray(...) ? ... : [] + .flatMap((entry) => typeof entry === "string" ? [entry] : []) shape repeats for integrations, neverDo, and requiresApprovalFor. A small toStringArray(value: unknown): string[] helper would tidy this up nicely — not a big deal, but tremendous for readability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agents/tools/onboarding-pack-tool.ts` around lines 266 - 286, The array
normalization logic is duplicated in the onboarding pack parsing flow for
integrations, neverDo, and requiresApprovalFor. Add a small reusable helper such
as toStringArray(value: unknown): string[] near the existing helpers in
onboarding-pack-tool.ts, then use it in the code path that builds integrations,
neverDo, and requiresApprovalFor so each field uses the same single conversion
step instead of repeating the Array.isArray/flatMap pattern.

708-722: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

These saves run one-by-one — parallelize 'em, it'll be huge for speed.

The for loop awaits each saveDocPanelArtifact call sequentially. Since artifacts are independent, running them via Promise.allSettled would cut latency for multi-artifact intake payloads, no downside.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agents/tools/onboarding-pack-tool.ts` around lines 708 - 722, The
artifact saves in `onboarding-pack-tool.ts` are running sequentially inside the
`saveToDocPanel` block, which slows multi-artifact intake. Update the `for` loop
around `saveDocPanelArtifact` to run all artifact saves in parallel using
`Promise.allSettled`, then aggregate the settled results into the existing
`saved` and `failed` arrays based on each artifact’s `key`, `title`, and
returned `id`/`error` so the rest of the logic stays the same.
dashboard/src/components/worker-wizard/WorkerWizard.tsx (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Deep ../../../../ import — a little messy, could be tremendous with an alias.

Four levels of relative traversal into src/config is brittle across a dashboard/core boundary. If a path alias (e.g. @core/config/*) is already configured elsewhere in the dashboard tsconfig, prefer that instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/WorkerWizard.tsx` at line 4, The
import in WorkerWizard is using a deep relative path into src/config, which is
brittle across the dashboard/core boundary. Update the IntentConfig and
IntentDepartmentConfig import in WorkerWizard.tsx to use the existing path alias
for the core config module if one is already configured in tsconfig, and keep
the symbol names unchanged so the component still resolves the same types.
src/infra/consciousness-kernel-state.ts (1)

1263-1269: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

A small, very avoidable double-computation — nothing tremendous, but let's tidy it up.

asNumber(shadow.lastBoardMaxUpdatedAt, ...) and the same for lastBoardTaskCount are each called twice — once to check finiteness, once to get the value. Works fine, just redundant.

♻️ Proposed cleanup
-        lastSalientCognitionAt: asString(shadow.lastSalientCognitionAt),
-        lastBoardMaxUpdatedAt: Number.isFinite(asNumber(shadow.lastBoardMaxUpdatedAt, Number.NaN))
-          ? asNumber(shadow.lastBoardMaxUpdatedAt, 0)
-          : null,
-        lastBoardTaskCount: Number.isFinite(asNumber(shadow.lastBoardTaskCount, Number.NaN))
-          ? Math.max(0, Math.floor(asNumber(shadow.lastBoardTaskCount, 0)))
-          : null,
+        lastSalientCognitionAt: asString(shadow.lastSalientCognitionAt),
+        lastBoardMaxUpdatedAt: (() => {
+          const n = asNumber(shadow.lastBoardMaxUpdatedAt, Number.NaN);
+          return Number.isFinite(n) ? n : null;
+        })(),
+        lastBoardTaskCount: (() => {
+          const n = asNumber(shadow.lastBoardTaskCount, Number.NaN);
+          return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : null;
+        })(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/infra/consciousness-kernel-state.ts` around lines 1263 - 1269, The
`lastBoardMaxUpdatedAt` and `lastBoardTaskCount` mappings in
`consciousness-kernel-state.ts` are doing the same `asNumber(...)` conversion
twice, once for the finite check and again for the returned value. Refactor the
surrounding object-building logic to compute each converted value once in
`asKernelState` (or the relevant state-mapping function) and reuse that result
for both the `Number.isFinite` guard and the final assignment, while preserving
the existing null/Math.max/Math.floor behavior.
scripts/demo/conductor-demo.mjs (1)

131-141: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fixed 1500ms sleep instead of waiting on the actual connect ack — folks, that's a real race, not fake news.

main() starts its gateway RPC calls (jobs.templates.list, etc.) after a flat setTimeout(..., 1500), hoping the WebSocket is open and the connect handshake (fired inside ws.on("open", ...)) has completed by then. If the connection is slower than 1.5s, ws.send() throws synchronously and the demo dies with a generic "demo setup failed" message instead of a clear connection error.

🔧 Suggested fix — wait on the actual open/connect ack instead of a fixed timer
-ws.on("open", () => {
-  send("connect", {
+let connected;
+ws.on("open", () => {
+  connected = send("connect", {
     auth: { token: TOKEN },
     client: { id: "test", mode: "test", version: "demo", platform: "node" },
     caps: [],
     role: "operator",
     scopes: ["operator.admin"],
     minProtocol: 3,
     maxProtocol: 3,
-  }).catch(() => {});
+  });
 });

 async function main() {
-  await new Promise((r) => setTimeout(r, 1500));
+  await new Promise((resolve) => ws.once("open", resolve));
+  await connected;

   await seedTickets();

Also applies to: 165-170, 229-232

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/demo/conductor-demo.mjs` around lines 131 - 141, The demo startup in
conductor-demo.mjs currently relies on a fixed delay before running gateway RPC
calls, which can race ahead of the WebSocket connect handshake. Update main() to
wait on the actual ws.on("open") and the send("connect", ...) acknowledgment
from the connect flow before issuing jobs.templates.list and the other demo
RPCs, and handle failures from that handshake explicitly instead of swallowing
them. Use the existing send() call inside ws.on("open") and the later setup path
as the integration points so the demo only proceeds once the connection is truly
ready.
src/agents/skills/workspace.ts (1)

139-157: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Big, tremendous fallback — but silent, folks.

When loadSkillsFromDir returns neither an array nor { skills: [...] }, this quietly returns [] for that directory. No warning, nobody knows skills just vanished for that source. A small console.warn would save someone a very confusing debugging afternoon later.

🔔 Suggested nudge
     } else {
+      console.warn(`[skills] unexpected loadSkillsFromDir shape for source=${params.source}`);
       return [];
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agents/skills/workspace.ts` around lines 139 - 157, The fallback in the
skills loading path is too silent: when the result from loadSkillsFromDir is
neither an array nor an object with a skills array, the function currently
returns an empty list without any signal. Update the logic in the workspace
skills loader to emit a warning before returning [] so it’s clear which
source/directory failed to produce usable skills; use the existing
loadSkillsFromDir and the surrounding params.source context to make the warning
informative.
rust/argentd/src/ws.rs (1)

1714-1779: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Manual JSON param scraping strikes again — but hey, it's consistent with the neighborhood.

find_param_bool/find_param_json_string/find_param_number are hand-rolled scanners rather than using a real JSON parser, but that matches the existing find_optional_param_string pattern already in this file. Not asking for a rewrite of the whole file's parsing strategy here — just flagging that find_param_number's digit/sign/exponent take_while can build malformed tokens (e.g. "1.2.3", "--1") that fail .parse() and fall back to None silently. Safe today given the unwrap_or(20)/unwrap_or(false) fallbacks, so no action needed unless callers start depending on precise error surfacing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/argentd/src/ws.rs` around lines 1714 - 1779, `find_param_number` can
assemble invalid numeric tokens because it only scans a loose character set and
then silently returns None on parse failure. Tighten the token extraction in
`find_param_number` so it only accepts a valid JSON number shape before parsing,
and keep the existing fallback behavior used by the callers in `ws.rs` unless
you plan to surface parse errors explicitly.
dashboard/src/components/worker-wizard/steps/StepSimulation.tsx (2)

49-58: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Give this icon-only toggle an accessible name — huge for screen reader folks.

The toggle button only renders an icon with no aria-label/aria-pressed, so assistive tech has no idea what it does.

♿ Proposed fix
         <button
           onClick={() => update({ enabled: !sim.enabled })}
           className="text-purple-400 hover:text-purple-300 transition-colors"
+          aria-pressed={sim.enabled}
+          aria-label={sim.enabled ? "Disable simulation gate" : "Enable simulation gate"}
         >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/steps/StepSimulation.tsx` around lines
49 - 58, The icon-only toggle in StepSimulation should expose an accessible name
and state so assistive tech can identify it. Update the button in
StepSimulation’s toggle handler to include an appropriate aria-label that
reflects the simulation setting and add aria-pressed tied to sim.enabled,
keeping the existing update({ enabled: !sim.enabled }) behavior unchanged.

69-89: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Mark active mode with aria-pressed too.

Same pattern — "Warn"/"Enforce" active state is purely visual (color/border), no programmatic indication for assistive tech.

♿ Proposed fix
             <button
               onClick={() => update({ mode: "warn" })}
+              aria-pressed={sim.mode === "warn"}
               className={`flex-1 py-2 rounded-lg border text-sm font-medium transition-all ${
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/steps/StepSimulation.tsx` around lines
69 - 89, The Warn/Enforce mode buttons in StepSimulation use only visual styling
to indicate the selected state, so add a programmatic active-state signal on the
button elements. Update the toggle buttons in StepSimulation.tsx to set
aria-pressed based on sim.mode while keeping the existing update({ mode: ... })
behavior, so assistive tech can identify which mode is active.
src/gateway/server-methods/intent.ts (1)

60-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Cosmetic-only tweak, not a real fix.

Wrapping in String(err) doesn't change behavior for non-Error throwables — a plain object still stringifies to [object Object] same as before. Harmless, very low stakes, just flagging it's not actually improving the error message for non-Error throws.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gateway/server-methods/intent.ts` at line 60, The error logging in intent
simulation still doesn’t provide useful details for non-Error throwables because
`String(err)` can collapse plain objects to unhelpful output. Update `intent.ts`
in the `context.logGateway.error` path to detect the thrown value in `intent`
simulation handling and format it with meaningful detail (for example,
extracting message/name/stack for `Error` instances and serializing other
objects) so the logged failure is actually informative.
dashboard/src/components/worker-wizard/steps/StepBoundaries.tsx (1)

78-98: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Unvalidated JSON.parse of LLM output — low risk here, but flag for the record.

JSON.parse(jsonMatch[0]) on a regex-extracted blob, with no schema validation on parsed.escalation.maxAttempts/sentimentThreshold types before assigning into numeric state fields. Wrapped in try/catch so it fails safely, and this is an internal wizard-assist feature, not attacker-facing — so this is a nice-to-have, not urgent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/steps/StepBoundaries.tsx` around lines
78 - 98, The JSON parsing in StepBoundaries is taking regex-extracted LLM output
without validating the parsed shape or field types. Update the parsing path
around jsonMatch/JSON.parse and the update call to validate the object
(especially parsed.escalation.sentimentThreshold and
parsed.escalation.maxAttempts) before assigning into state, and keep the whole
parse/transform flow wrapped in try/catch so malformed output falls back safely.
src/argent-agent/create-agent-session.ts (1)

385-393: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nice catch on the silent tool miss — logging before throw is a great, great move.

Minor nit: console.warn is used directly here instead of whatever the project's shared logger is (if one exists elsewhere in this module). Not blocking, just flagging for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/argent-agent/create-agent-session.ts` around lines 385 - 393, Replace the
direct `console.warn` in the tool lookup miss path with the module’s shared
logger, if one is already used elsewhere in `create-agent-session.ts`, so
logging stays consistent. Update the `toolCall.name`/registry message to be
emitted through that logger before the `Tool ${toolCall.name as string} not
found` throw. Keep the existing warning detail and behavior unchanged, only swap
the logging mechanism to the shared one.
dashboard/src/components/worker-wizard/AIChatSidebar.tsx (1)

25-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

That regex is grabbing everything between the FIRST and LAST curly brace — tremendous risk, folks.

/\{[\s\S]*\}/ is greedy across the whole reply. If the assistant's prose has more than one brace pair (code examples, nested objects in prose, etc.), you'll parse the wrong span or throw (silently swallowed by the catch, so no suggestion — degraded UX rather than a crash, but still worth tightening).

Consider a non-greedy match or a small brace-balance scanner instead of the greedy regex.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/AIChatSidebar.tsx` at line 25, Tighten
the JSON extraction in AIChatSidebar’s parsing logic: the current jsonMatch
regex is greedy and can capture too much when the assistant reply contains
multiple brace pairs. Update the matching approach in the text parsing path to
use a non-greedy pattern or, preferably, a small brace-balancing scan so the
extracted JSON span is the intended object only. Keep the change localized
around the jsonMatch handling and the subsequent parse flow so the existing
fallback behavior remains intact.
dashboard/src/components/worker-wizard/steps/StepDepartment.tsx (1)

39-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Folks, these toggle buttons need to tell screen readers who's WINNING.

Big, beautiful mode-toggle, tremendous styling — but nobody using a screen reader knows which mode is currently selected, believe me. Add aria-pressed={dept.mode === "existing"} (and the "new" equivalent) so assistive tech knows the score.

♿ Proposed fix
         <button
           onClick={() => update({ mode: "existing" })}
+          aria-pressed={dept.mode === "existing"}
           className={`flex-1 flex items-center justify-center gap-2 py-2.5 rounded-lg border text-sm font-medium transition-all ${
         <button
           onClick={() => update({ mode: "new" })}
+          aria-pressed={dept.mode === "new"}
           className={`flex-1 flex items-center justify-center gap-2 py-2.5 rounded-lg border text-sm font-medium transition-all ${
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/steps/StepDepartment.tsx` around lines
39 - 61, The mode toggle buttons in StepDepartment need to expose their selected
state to assistive technology. Update the two button elements that call update({
mode: "existing" }) and update({ mode: "new" }) in the StepDepartment component
to include aria-pressed based on dept.mode so screen readers can tell which
option is currently active.
dashboard/src/components/worker-wizard/steps/StepDeploy.tsx (1)

83-88: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Regex-matching an error message to detect "already exists" — a little shaky, tell you what.

Works today, but it's coupled to exact backend wording. If that message ever changes, family.register retries start throwing on a perfectly normal "already exists" case. A structured error code/type from the gateway would be sturdier, but low priority given it's contained to one step.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/steps/StepDeploy.tsx` around lines 83
- 88, The error handling in StepDeploy’s agent registration catch block relies
on regex-matching Error.message for “already exists,” which is brittle. Update
the logic around the family.register call to use a structured error code/type
from the gateway if available, and keep the current fallback only if no typed
signal exists. Make the check in the same catch path so deployment success is
decided by a stable error identifier rather than backend wording.
src/infra/consciousness-kernel.ts (1)

2118-2153: 🚀 Performance & Scalability | 🔵 Trivial

Board cache refresh fires an unfiltered tasks.list() on every tick.

refreshBoardCache() kicks off on every scheduled tick regardless of tickMs or task volume — tremendous diligence keeping it non-blocking and error-swallowing, real smart engineering there. Just flagging that on short tick intervals or large boards this is a full-table read every cycle; might be worth throttling the refresh independent of tick cadence if boards get big.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/infra/consciousness-kernel.ts` around lines 2118 - 2153, The board cache
refresh in refreshBoardCache() is doing a full tasks.list() on every tick, which
can become expensive on small tick intervals or large boards. Add throttling or
a freshness check inside refreshBoardCache (or around the tick caller) so the
cache refresh runs at a lower cadence independent of the scheduler, while
keeping the existing non-blocking and error-swallowing behavior intact.
src/config/validation.ts (1)

87-97: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

OptionalIntentApi | null is now always non-null — the null path is dead weight.

Since getOptionalIntentApi() unconditionally returns the object now, the | null in the return type and the downstream if (intentApi) guard (line 123) never fire. Not urgent, just a small tidy-up opportunity down the line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config/validation.ts` around lines 87 - 97, The optional intent API is
now always returned, so the null branch is dead code. Update
getOptionalIntentApi() to return OptionalIntentApi directly, remove the
unnecessary null from the return type, and simplify the downstream intentApi
guard in validation logic so the code path reflects the always-available
resolveIntentValidationMode and validateIntentHierarchy helpers.
src/agents/model-auth.ts (1)

387-393: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tremendous fallback, folks — but let's not write "lmstudio/ollama" twice.

The local-keyless carve-out here duplicates the identical normalized === "lmstudio" || normalized === "ollama" check already living in isModelProviderAvailableForAutomaticRouting (Lines 217-219). Two lists of the same providers means two places to update the next time a keyless local provider shows up — a beautiful setup for drift, believe me.

♻️ Proposed fix: single source of truth
+const LOCAL_KEYLESS_PROVIDERS = new Set(["lmstudio", "ollama"]);
+
 export function isModelProviderAvailableForAutomaticRouting(params: {
   ...
-  if (normalizedProvider === "lmstudio" || normalizedProvider === "ollama") {
+  if (LOCAL_KEYLESS_PROVIDERS.has(normalizedProvider)) {
     return true;
   }
   ...
-  if (normalized === "lmstudio" || normalized === "ollama") {
+  if (LOCAL_KEYLESS_PROVIDERS.has(normalized)) {
     return { apiKey: normalized, source: "local-provider", mode: "api-key" };
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agents/model-auth.ts` around lines 387 - 393, The local-keyless provider
check in model-auth.ts is duplicated between the carve-out here and
isModelProviderAvailableForAutomaticRouting, so consolidate the lmstudio/ollama
list into a single shared helper or constant and use it in both places. Update
the logic around the normalized provider checks to reference that one source of
truth, keeping the existing behavior for local providers while avoiding drift
when new keyless providers are added.
rust/argentd/src/hub.rs (1)

84-91: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Minor idiom nit: Vec::remove(0) is O(n) where a queue op would be O(1).

At a 1000-item cap this is bounded and cheap, but VecDeque::push_back/pop_front is the idiomatic O(1) choice for this eviction pattern instead of shifting the whole vector on every overflow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/argentd/src/hub.rs` around lines 84 - 91, The canary receipt eviction in
append_canary_receipt currently uses Vec::remove(0), which shifts elements on
every overflow; switch the canary_receipts storage in Hub to a queue-friendly
type and update append_canary_receipt to use push_back/pop_front so the capped
buffer stays O(1) on eviction while preserving the existing 1000-item limit.
dashboard/src/components/worker-wizard/steps/StepIdentity.tsx (1)

70-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Labels aren't wired to their inputs — not great for accessibility, believe me.

None of the <label> elements in this form use htmlFor paired with an input id (Display Name, Agent ID, Emoji, Role, Team, Custom Role). Screen readers lose the label association even though it looks fine visually.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/steps/StepIdentity.tsx` around lines
70 - 145, The StepIdentity form has unlabeled control associations because the
`<label>` elements are not linked to their inputs. Update the inputs in
StepIdentity so each field (Display Name, Agent ID, Emoji, Role, Team, Custom
Role) has a unique `id`, and set the matching `htmlFor` on the corresponding
label. Keep the existing `update`, `onChange`, and `ROLE_OPTIONS` logic
unchanged; only wire the labels to the inputs for accessibility.
extensions/telegram/src/channel.ts (1)

350-361: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Small nit: that fallback object at the end is basically dead code.

Since the loop only runs when mediaUrls.length > 0, finalResult will always be set by the time you return — the ?? { messageId: "unknown", chatId: to } fallback can never actually trigger. Not a big deal, just a tiny bit of unreachable defensive code, believe me it's minor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/telegram/src/channel.ts` around lines 350 - 361, The fallback
object in the Telegram media send path is unreachable because the loop in the
send flow always assigns finalResult when mediaUrls has entries. Update the
return in channel.ts near the media sending logic to return the actual
finalResult from send, and remove the dead fallback branch so the code matches
the guaranteed control flow.
src/data/pg/schema.ts (1)

702-707: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add an index for lease-expiry sweeps — big, fast wins here, folks.

claimedBy/claimTtl will get scanned constantly by the worker lease-claim/expiry logic (Worker Runtime v2 D4), but there's no index backing those lookups — that's a tremendous performance risk once the tasks table gets some real volume, believe me.

⚡ Proposed index addition
   (t) => [
     index("idx_tasks_status").on(t.status),
     index("idx_tasks_priority").on(t.priority),
     index("idx_tasks_agent").on(t.agentId),
     index("idx_tasks_due").on(t.dueAt),
     index("idx_tasks_team").on(t.teamId),
     index("idx_tasks_job_assignment").on(t.jobAssignmentId),
     index("idx_tasks_job_template").on(t.jobTemplateId),
+    index("idx_tasks_claim").on(t.claimedBy, t.claimTtl),
     // FTS index — created via raw SQL in migration:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/data/pg/schema.ts` around lines 702 - 707, The lease fields on the tasks
schema are being queried repeatedly by the worker claim/expiry flow, but
`claimedBy` and `claimTtl` currently have no index support. Add a dedicated
index in `schema.ts` alongside the existing task/lease columns, using the
lease-related columns (`claimedBy`, `claimTtl`) so the worker runtime sweep and
claim lookups can use it efficiently. If there is a schema index list or table
definition helper near `PgAdapter.ensureCoreSchema`, update that same path so
the new index is created for both fresh and existing databases.
dashboard/src/components/worker-wizard/shared/TagInput.tsx (2)

91-93: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Give that little "X" button a name — tremendous accessibility win, very cheap to do!

The remove-tag button only renders an icon with no aria-label, so screen reader users get nothing useful announced.

♿ Proposed fix
-            <button onClick={() => removeTag(tag)} className="hover:text-white transition-colors">
+            <button
+              onClick={() => removeTag(tag)}
+              aria-label={`Remove ${tag}`}
+              className="hover:text-white transition-colors"
+            >
               <X className="w-3 h-3" />
             </button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/shared/TagInput.tsx` around lines 91 -
93, The remove-tag control in TagInput renders only the X icon, so add an
accessible label to the button used with removeTag(tag) and the X icon. Update
the button in TagInput so screen readers announce a clear action name (for
example, remove that tag) while keeping the visual icon unchanged.

24-49: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Move colorMap out of the component, folks — nobody wants to rebuild this beautiful object every single render!

colorMap is a static lookup with no dependency on props/state, but it's re-created on every render inside the component body. Hoist it to module scope.

♻️ Proposed fix
+const TAG_COLOR_MAP: Record<string, { bg: string; border: string; text: string; pill: string }> = {
+  purple: {
+    bg: "bg-purple-600/10",
+    border: "border-purple-500/30",
+    text: "text-purple-300",
+    pill: "bg-purple-600/20 text-purple-300",
+  },
+  red: { bg: "bg-red-600/10", border: "border-red-500/30", text: "text-red-300", pill: "bg-red-600/20 text-red-300" },
+  cyan: { bg: "bg-cyan-600/10", border: "border-cyan-500/30", text: "text-cyan-300", pill: "bg-cyan-600/20 text-cyan-300" },
+  amber: { bg: "bg-amber-600/10", border: "border-amber-500/30", text: "text-amber-300", pill: "bg-amber-600/20 text-amber-300" },
+};
+
 export function TagInput({ ... }: TagInputProps) {
   const [input, setInput] = useState("");
   const inputRef = useRef<HTMLInputElement>(null);
-  const colorMap: Record<string, ...> = { ... };
-  const colors = colorMap[accentColor] || colorMap.purple;
+  const colors = TAG_COLOR_MAP[accentColor] || TAG_COLOR_MAP.purple;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/shared/TagInput.tsx` around lines 24 -
49, The static colorMap in TagInput is being recreated on every render even
though it has no props or state dependencies. Hoist colorMap out of the TagInput
component body to module scope so the lookup is defined once and reused across
renders, keeping the existing palette entries and references intact.
src/agents/pi-embedded-runner/run/attempt.ts (1)

1153-1161: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use effectiveTools for native-provider tool context.

When simulateWrites is enabled, execution uses wrapped tools, but the native Argent provider context still advertises the original tools. Feed the same effectiveTools into the stream context so model-visible tool metadata matches the simulated execution contract.

Proposed fix
-            const nativeStreamTools = tools;
+            const nativeStreamTools = effectiveTools;

Also applies to: 1322-1347

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agents/pi-embedded-runner/run/attempt.ts` around lines 1153 - 1161, The
native provider context is still using the original tools list even when
simulateWrites wraps them, so model-visible metadata can diverge from actual
execution. Update the tool context in the run flow that builds the Argent stream
options to use effectiveTools instead of tools, and apply the same change
anywhere else in the attempt path that forwards tool metadata (including the
other affected block in attempt.ts). Keep splitSdkTools and wrapToolForSimulate
as-is; only ensure the stream/context layer receives the simulated tool set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6f99ed08-01f6-44a9-9647-5dab82858e80

📥 Commits

Reviewing files that changed from the base of the PR and between 14c7663 and b4cdc1e.

⛔ Files ignored due to path filters (2)
  • dashboard/package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (279)
  • .argent-repo.json
  • .github/workflows/ci.yml
  • .gitignore
  • AGENTS.md
  • CHANGELOG.md
  • HANDOFF.md
  • ORCHESTRATION.md
  • README.md
  • dashboard/api-server.cjs
  • dashboard/package.json
  • dashboard/src/App.tsx
  • dashboard/src/components/WorkerFlowModal.tsx
  • dashboard/src/components/WorkerFlowModalBridge.ts
  • dashboard/src/components/WorkforceBoard.tsx
  • dashboard/src/components/WorkforceBoardBridge.ts
  • dashboard/src/components/widgets/OrgChartWidget.tsx
  • dashboard/src/components/widgets/WorkflowsWidget.tsx
  • dashboard/src/components/worker-wizard/AIChatSidebar.tsx
  • dashboard/src/components/worker-wizard/WorkerWizard.tsx
  • dashboard/src/components/worker-wizard/index.ts
  • dashboard/src/components/worker-wizard/shared/HierarchyDiagram.tsx
  • dashboard/src/components/worker-wizard/shared/StepIndicator.tsx
  • dashboard/src/components/worker-wizard/shared/TagInput.tsx
  • dashboard/src/components/worker-wizard/steps/StepBoundaries.tsx
  • dashboard/src/components/worker-wizard/steps/StepDepartment.tsx
  • dashboard/src/components/worker-wizard/steps/StepDeploy.tsx
  • dashboard/src/components/worker-wizard/steps/StepIdentity.tsx
  • dashboard/src/components/worker-wizard/steps/StepReview.tsx
  • dashboard/src/components/worker-wizard/steps/StepSimulation.tsx
  • dashboard/src/components/worker-wizard/types.ts
  • dashboard/src/lib/configSurfaceProfile.ts
  • dashboard/tsconfig.app.json
  • docs/concepts/core-business-boundary.md
  • docs/tools/slash-commands.md
  • extensions/bluebubbles/package.json
  • extensions/canvas-docs-enforcer/package.json
  • extensions/copilot-proxy/package.json
  • extensions/dashboard-canvas-bridge/package.json
  • extensions/diagnostics-otel/package.json
  • extensions/discord/package.json
  • extensions/feishu/package.json
  • extensions/google-gemini-cli-auth/package.json
  • extensions/google-meet/package.json
  • extensions/google-workspace/package.json
  • extensions/googlechat/package.json
  • extensions/imessage/package.json
  • extensions/line/package.json
  • extensions/llm-task/package.json
  • extensions/lobster/package.json
  • extensions/matrix/CHANGELOG.md
  • extensions/matrix/package.json
  • extensions/mattermost/package.json
  • extensions/memory-lancedb/package.json
  • extensions/minimax-portal-auth/package.json
  • extensions/msteams/CHANGELOG.md
  • extensions/msteams/package.json
  • extensions/nextcloud-talk/package.json
  • extensions/nostr/CHANGELOG.md
  • extensions/nostr/package.json
  • extensions/open-prose/package.json
  • extensions/response-validator/package.json
  • extensions/signal/package.json
  • extensions/slack/package.json
  • extensions/summarize-tts-enforcer/package.json
  • extensions/tasks-projects-enforcer/package.json
  • extensions/telegram/package.json
  • extensions/telegram/src/channel.sendpayload.test.ts
  • extensions/telegram/src/channel.ts
  • extensions/tlon/package.json
  • extensions/twitch/CHANGELOG.md
  • extensions/twitch/package.json
  • extensions/voice-call/CHANGELOG.md
  • extensions/voice-call/package.json
  • extensions/whatsapp/package.json
  • extensions/zalo/CHANGELOG.md
  • extensions/zalo/package.json
  • extensions/zalouser/CHANGELOG.md
  • extensions/zalouser/package.json
  • ops/AGENT_PERSONA_ONBOARDING_PROMPT.md
  • ops/BUSINESS_MERGE_LANDING_MAP_2026-06-10.md
  • ops/WORKER_RUNTIME_V2_DESIGN_2026-06-11.md
  • ops/known-failing.json
  • ops/runbooks/release-core.md
  • package.json
  • rust/AUTHORITY_DELEGATION_SEAM.md
  • rust/argent-execd/PROMOTION_CHECKLIST.md
  • rust/argent-execd/src/main.rs
  • rust/argent-execd/src/server.rs
  • rust/argent-execd/tests/http_control.rs
  • rust/argentd/src/http.rs
  • rust/argentd/src/hub.rs
  • rust/argentd/src/main.rs
  • rust/argentd/src/ws.rs
  • rust/argentd/tests/ws_parity.rs
  • scripts/demo/conductor-demo.mjs
  • scripts/export-public-core.ts
  • scripts/generate-pg-bootstrap.ts
  • scripts/install-hosted.sh
  • scripts/ui.js
  • src/agents/argent-tools-core.ts
  • src/agents/argent-tools.public-core.test.ts
  • src/agents/argent-tools.ts
  • src/agents/auth-profiles.auth-profile-cooldowns.test.ts
  • src/agents/auth-profiles/store.ts
  • src/agents/intent-runtime-gate-core.ts
  • src/agents/intent-runtime-gate.ts
  • src/agents/intent-simulation.ts
  • src/agents/model-auth.test.ts
  • src/agents/model-auth.ts
  • src/agents/optional-intent.test.ts
  • src/agents/optional-intent.ts
  • src/agents/optional-tool-factory.test.ts
  • src/agents/optional-tool-factory.ts
  • src/agents/pi-embedded-runner.test.ts
  • src/agents/pi-embedded-runner/google.ts
  • src/agents/pi-embedded-runner/run.overflow-compaction.test.ts
  • src/agents/pi-embedded-runner/run.ts
  • src/agents/pi-embedded-runner/run/attempt.ts
  • src/agents/pi-embedded-runner/run/params.ts
  • src/agents/pi-embedded-runner/run/types.ts
  • src/agents/pi-embedded-runner/simulate-tool-stub.test.ts
  • src/agents/pi-embedded-runner/simulate-tool-stub.ts
  • src/agents/pi-embedded-runner/types.ts
  • src/agents/pi-tools.policy.ts
  • src/agents/pi-tools.ts
  • src/agents/pi-tools.worker-session-policy.test.ts
  • src/agents/public-core-tools.ts
  • src/agents/skills.ts
  • src/agents/skills/personal-skill-read-cache.test.ts
  • src/agents/skills/personal-skill-read-cache.ts
  • src/agents/skills/personal.ts
  • src/agents/skills/workspace.ts
  • src/agents/test-helpers/fast-coding-tools.ts
  • src/agents/test-helpers/fast-core-tools.ts
  • src/agents/tool-claim-validation.ts
  • src/agents/tools/async-service-key-tools.test.ts
  • src/agents/tools/audio-alert-tool.ts
  • src/agents/tools/audio-generation-tool.ts
  • src/agents/tools/channel-config-tool.ts
  • src/agents/tools/coolify-deploy-tool.ts
  • src/agents/tools/copilot-system-tool.test.ts
  • src/agents/tools/copilot-system-tool.ts
  • src/agents/tools/email-delivery-tool.test.ts
  • src/agents/tools/email-delivery-tool.ts
  • src/agents/tools/intent-tool.test.ts
  • src/agents/tools/intent-tool.ts
  • src/agents/tools/jobs-tool.ts
  • src/agents/tools/memu-recall-quality.test.ts
  • src/agents/tools/memu-tools.ts
  • src/agents/tools/namecheap-dns-tool.ts
  • src/agents/tools/onboarding-pack-tool.ts
  • src/agents/tools/personal-skill-tool.ts
  • src/agents/tools/railway-deploy-tool.ts
  • src/agents/tools/tts-generate-tool.ts
  • src/agents/tools/vercel-deploy-tool.ts
  • src/agents/tools/web-tools.enabled-defaults.test.ts
  • src/agents/tools/work-report-tool.test.ts
  • src/agents/tools/work-report-tool.ts
  • src/agents/tools/workforce-setup-tool.ts
  • src/agents/worker-role-profile.test.ts
  • src/agents/worker-role-profile.ts
  • src/argent-agent/create-agent-session.ts
  • src/argent-ai/providers/zai.ts
  • src/auto-reply/reply/commands-compact.ts
  • src/auto-reply/reply/session.ts
  • src/cli/intent-cli-core.ts
  • src/commands/agent.delivery.test.ts
  • src/commands/agent.test.ts
  • src/commands/agent.ts
  • src/commands/agent/types.ts
  • src/commands/doctor-auth.deprecated-cli-profiles.test.ts
  • src/commands/doctor-auth.ts
  • src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.test.ts
  • src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.test.ts
  • src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.test.ts
  • src/commands/doctor.warns-state-directory-is-missing.test.ts
  • src/commands/status.executive-shadow.ts
  • src/config/load-profile.test.ts
  • src/config/schema.ts
  • src/config/types.agent-defaults.ts
  • src/config/validation.ts
  • src/config/zod-schema.agent-runtime.ts
  • src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts
  • src/dashboard-surface-profile.test.ts
  • src/data/adapter.ts
  • src/data/dual-adapter.ts
  • src/data/jobs.test.ts
  • src/data/migrate/sqlite-to-pg.ts
  • src/data/pg-adapter.ts
  • src/data/pg/bootstrap-schema.ts
  • src/data/pg/schema.ts
  • src/data/sqlite-adapter.ts
  • src/data/tasks.lease.test.ts
  • src/data/tasks.ts
  • src/data/types.ts
  • src/discord/monitor/provider.ts
  • src/gateway/job-orchestrator-bridge.ts
  • src/gateway/server-methods-list.ts
  • src/gateway/server-methods.ts
  • src/gateway/server-methods/commands.compact-agent-resolution.test.ts
  • src/gateway/server-methods/commands.ts
  • src/gateway/server-methods/copilot.ts
  • src/gateway/server-methods/execution-worker.ts
  • src/gateway/server-methods/intent-core.ts
  • src/gateway/server-methods/intent.ts
  • src/gateway/server-methods/jobs-grades.test.ts
  • src/gateway/server-methods/jobs.ts
  • src/gateway/server-methods/skills.ts
  • src/gateway/server-methods/types.ts
  • src/gateway/server-methods/workflows.output-channels.test.ts
  • src/gateway/server-methods/workflows.ts
  • src/gateway/server-startup.ts
  • src/gateway/server.impl.ts
  • src/infra/app-forge-model.test.ts
  • src/infra/app-forge-views.test.ts
  • src/infra/consciousness-kernel-salience.test.ts
  • src/infra/consciousness-kernel-state.ts
  • src/infra/consciousness-kernel.test.ts
  • src/infra/consciousness-kernel.ts
  • src/infra/contemplation-runner.ts
  • src/infra/execution-worker-runner-core.ts
  • src/infra/execution-worker-runner-impl.ts
  • src/infra/execution-worker-runner.test.ts
  • src/infra/execution-worker-runner.ts
  • src/infra/executive-shadow-client.integration.test.ts
  • src/infra/executive-shadow-client.ts
  • src/infra/gateway-log-rotation.test.ts
  • src/infra/gateway-log-rotation.ts
  • src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts
  • src/infra/heartbeat-runner.returns-default-unset.test.ts
  • src/infra/heartbeat-runner.ts
  • src/infra/idle-salience-gate.test.ts
  • src/infra/idle-salience-gate.ts
  • src/infra/intent-simulation-runner.ts
  • src/infra/intent-simulation-scenarios-t1.ts
  • src/infra/job-orchestrator-runner.ts
  • src/infra/keychain.ts
  • src/infra/license-core.ts
  • src/infra/public-core-denylist.test.ts
  • src/infra/public-core-export.test.ts
  • src/infra/public-core-export.ts
  • src/infra/run-event-log.test.ts
  • src/infra/run-event-log.ts
  • src/infra/sis-runner.ts
  • src/infra/update-runner.test.ts
  • src/infra/update-runner.ts
  • src/infra/work-report-crosscheck.test.ts
  • src/infra/work-report-crosscheck.ts
  • src/infra/worker-ephemeral-session.test.ts
  • src/infra/worker-ephemeral-session.ts
  • src/infra/worker-grading.test.ts
  • src/infra/worker-grading.ts
  • src/infra/workflow-approvals.text-command.test.ts
  • src/infra/workflow-approvals.ts
  • src/infra/workflow-execution-service.approval-drain.test.ts
  • src/infra/workflow-execution-service.approval-timeout.test.ts
  • src/infra/workflow-execution-service.ts
  • src/infra/workflow-runner.session-policy.test.ts
  • src/infra/workflow-runner.ts
  • src/licensing/client.ts
  • src/licensing/crypto.ts
  • src/licensing/index.ts
  • src/licensing/license.ts
  • src/licensing/manager.ts
  • src/licensing/secret-sync.ts
  • src/licensing/storage.ts
  • src/licensing/types.ts
  • src/memory/live-inbox/capture.ts
  • src/plugins/org-scope.ts
  • src/telegram/bot-handlers.ts
  • src/tui/components/filterable-select-list.ts
  • src/tui/components/searchable-select-list.ts
  • src/utils/dashboard-api.test.ts
  • src/utils/dashboard-api.ts
  • src/utils/optional-module.ts
  • src/wizard/onboarding.test.ts
  • tsdown.config.ts
  • ui/package.json
  • vitest.unit.config.ts
💤 Files with no reviewable changes (8)
  • src/agents/optional-tool-factory.test.ts
  • src/agents/intent-runtime-gate-core.ts
  • src/gateway/server-methods/intent-core.ts
  • src/data/jobs.test.ts
  • src/agents/optional-tool-factory.ts
  • src/agents/argent-tools-core.ts
  • src/cli/intent-cli-core.ts
  • scripts/export-public-core.ts

Comment on lines +32 to +58
if (step === "identity" && (parsed.displayName || parsed.role || parsed.team)) {
return {
identity: {
displayName: parsed.displayName || "",
agentId: parsed.agentId || "",
role: parsed.role || "custom",
customRole: parsed.customRole || "",
team: parsed.team || "",
emoji: parsed.emoji || "",
},
};
}
if (step === "boundaries" && (parsed.objective || parsed.neverDo || parsed.allowedActions)) {
return {
boundaries: {
objective: parsed.objective || "",
neverDo: parsed.neverDo || [],
allowedActions: parsed.allowedActions || [],
requiresHumanApproval: parsed.requiresHumanApproval || [],
escalation: parsed.escalation || {
sentimentThreshold: 0.3,
maxAttempts: 3,
alwaysEscalate: [],
},
},
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Big problem, folks — "Apply Suggestion" WIPES OUT stuff the user already typed.

tryParseSuggestion builds identity/boundaries from scratch using only what the AI's JSON mentions, defaulting everything else to ""/[]. Click "Apply" and any field the assistant didn't mention — emoji, team, neverDo rules, whatever — gets nuked. Not good, not good at all. Merge with the existing state instead of replacing it wholesale.

🛠️ Proposed fix — merge instead of replace
-function tryParseSuggestion(text: string, step: WizardStep): Partial<WizardState> | undefined {
+function tryParseSuggestion(
+  text: string,
+  step: WizardStep,
+  current: WizardState,
+): Partial<WizardState> | undefined {
   try {
     const jsonMatch = text.match(/\{[\s\S]*\}/);
     if (!jsonMatch) {
       return undefined;
     }
     const parsed = JSON.parse(jsonMatch[0]);

     // Map parsed fields to wizard state patches based on current step
     if (step === "identity" && (parsed.displayName || parsed.role || parsed.team)) {
       return {
         identity: {
-          displayName: parsed.displayName || "",
-          agentId: parsed.agentId || "",
-          role: parsed.role || "custom",
-          customRole: parsed.customRole || "",
-          team: parsed.team || "",
-          emoji: parsed.emoji || "",
+          ...current.identity,
+          displayName: parsed.displayName || current.identity.displayName,
+          agentId: parsed.agentId || current.identity.agentId,
+          role: parsed.role || current.identity.role,
+          customRole: parsed.customRole || current.identity.customRole,
+          team: parsed.team || current.identity.team,
+          emoji: parsed.emoji || current.identity.emoji,
         },
       };
     }
     if (step === "boundaries" && (parsed.objective || parsed.neverDo || parsed.allowedActions)) {
       return {
         boundaries: {
-          objective: parsed.objective || "",
-          neverDo: parsed.neverDo || [],
-          allowedActions: parsed.allowedActions || [],
-          requiresHumanApproval: parsed.requiresHumanApproval || [],
-          escalation: parsed.escalation || {
-            sentimentThreshold: 0.3,
-            maxAttempts: 3,
-            alwaysEscalate: [],
-          },
+          ...current.boundaries,
+          objective: parsed.objective || current.boundaries.objective,
+          neverDo: parsed.neverDo || current.boundaries.neverDo,
+          allowedActions: parsed.allowedActions || current.boundaries.allowedActions,
+          requiresHumanApproval:
+            parsed.requiresHumanApproval || current.boundaries.requiresHumanApproval,
+          escalation: parsed.escalation || current.boundaries.escalation,
         },
       };
     }
     return undefined;
   } catch {
     return undefined;
   }
 }

And update the call site (line 128) to pass state:

-      const suggestion = tryParseSuggestion(reply, currentStep);
+      const suggestion = tryParseSuggestion(reply, currentStep, state);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/AIChatSidebar.tsx` around lines 32 -
58, The suggestion parser in AIChatSidebar’s tryParseSuggestion is replacing
identity and boundaries with partial AI output, which wipes existing
user-entered fields. Update tryParseSuggestion to merge parsed values into the
current state instead of constructing full objects from scratch, preserving
untouched properties like emoji, team, neverDo, and escalation defaults. Also
update the Apply Suggestion call site in AIChatSidebar to pass the existing
state through so the merge can use it.

Comment on lines +99 to +106
<input
type="text"
value={id.emoji}
onChange={(e) => update({ emoji: e.target.value.slice(0, 2) })}
placeholder="🤖"
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm text-center placeholder-white/20 focus:border-purple-500/50 focus:outline-none"
/>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does JavaScript Intl.Segmenter reliably split grapheme clusters for compound emoji across major browsers in 2026?

💡 Result:

Yes, as of July 2026, the JavaScript Intl.Segmenter API is the standard and reliable way to split strings into grapheme clusters, including complex compound emoji, across all major browser engines [1][2][3]. Intl.Segmenter has achieved "Baseline: Newly Available" status [1][2], meaning it is supported in all modern versions of Chrome, Edge, Firefox, and Safari [4][5]. When used with the { granularity: "grapheme" } option (which is the default), the API correctly identifies and preserves complex extended grapheme clusters, such as emoji sequences connected by Zero-Width Joiners (ZWJ) or those containing skin tone modifiers, as single user-perceived characters [6][3][7]. This approach is widely recommended over traditional string manipulation methods (like.split('') or standard iteration), which often break these sequences into their individual code points [6][8][3]. While the underlying segmentation rules adhere to the Unicode Standard Annex #29 (Unicode Text Segmentation) [6][9], it remains the most robust native solution available for handling these sequences in JavaScript environments [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="dashboard/src/components/worker-wizard/steps/StepIdentity.tsx"

echo "== Line count =="
wc -l "$FILE"

echo
echo "== Relevant excerpt =="
sed -n '1,220p' "$FILE" | cat -n | sed -n '60,160p'

Repository: ArgentAIOS/argentos-core

Length of output: 2780


Use grapheme-aware slicing for this field

slice(0, 2) still breaks compound emoji, and Array.from(...).slice(0, 1) only counts code points. Use Intl.Segmenter with granularity: "grapheme" so ZWJ sequences, flags, and skin-tone emoji stay intact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/steps/StepIdentity.tsx` around lines
99 - 106, The emoji input in StepIdentity currently truncates with code-point
slicing, which can break compound emoji. Update the onChange handler in
StepIdentity to use grapheme-aware truncation via Intl.Segmenter with
granularity "grapheme" so update({ emoji: ... }) preserves ZWJ sequences, flags,
and skin-tone variants intact.

Comment on lines +306 to +337
// Server-side validation via preview
try {
const intentCopy = JSON.parse(JSON.stringify(intentConfig));
if (!intentCopy.agents) {
intentCopy.agents = {};
}
const { policy, simulation } = buildIntentAgentConfig(state);
intentCopy.agents[state.identity.agentId] = { ...policy, simulationGate: simulation };

if (state.department.mode === "new" && deptId) {
if (!intentCopy.departments) {
intentCopy.departments = {};
}
intentCopy.departments[deptId] = { objective: state.department.newObjective };
}

const res = await fetchLocalApi("/api/settings/intent/preview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ intent: intentCopy }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
if (data?.errors) {
for (const err of data.errors) {
issues.push({ field: "intent", message: String(err), severity: "error" });
}
}
}
} catch {
// Preview endpoint may not exist — skip
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Swallowing every preview-validation failure the same way — that's not a great, that's a bad deal.

Any error from the /api/settings/intent/preview call — network blip, malformed JSON, real server error — is caught and silently ignored with just a comment. If the preview endpoint is genuinely failing (not just "doesn't exist"), the wizard still reports the config as valid and lets the user deploy. Consider pushing a warning/error issue on unexpected failures instead of a blanket skip.

🛡️ Proposed fix to distinguish missing endpoint from real failures
-    } catch {
-      // Preview endpoint may not exist — skip
-    }
+    } catch (err) {
+      issues.push({
+        field: "intent",
+        message: `Server-side validation could not run: ${String(err)}`,
+        severity: "warning",
+      });
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Server-side validation via preview
try {
const intentCopy = JSON.parse(JSON.stringify(intentConfig));
if (!intentCopy.agents) {
intentCopy.agents = {};
}
const { policy, simulation } = buildIntentAgentConfig(state);
intentCopy.agents[state.identity.agentId] = { ...policy, simulationGate: simulation };
if (state.department.mode === "new" && deptId) {
if (!intentCopy.departments) {
intentCopy.departments = {};
}
intentCopy.departments[deptId] = { objective: state.department.newObjective };
}
const res = await fetchLocalApi("/api/settings/intent/preview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ intent: intentCopy }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
if (data?.errors) {
for (const err of data.errors) {
issues.push({ field: "intent", message: String(err), severity: "error" });
}
}
}
} catch {
// Preview endpoint may not exist — skip
}
// Server-side validation via preview
try {
const intentCopy = JSON.parse(JSON.stringify(intentConfig));
if (!intentCopy.agents) {
intentCopy.agents = {};
}
const { policy, simulation } = buildIntentAgentConfig(state);
intentCopy.agents[state.identity.agentId] = { ...policy, simulationGate: simulation };
if (state.department.mode === "new" && deptId) {
if (!intentCopy.departments) {
intentCopy.departments = {};
}
intentCopy.departments[deptId] = { objective: state.department.newObjective };
}
const res = await fetchLocalApi("/api/settings/intent/preview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ intent: intentCopy }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
if (data?.errors) {
for (const err of data.errors) {
issues.push({ field: "intent", message: String(err), severity: "error" });
}
}
}
} catch (err) {
issues.push({
field: "intent",
message: `Server-side validation could not run: ${String(err)}`,
severity: "warning",
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/WorkerWizard.tsx` around lines 306 -
337, The preview validation in WorkerWizard silently ignores every failure from
fetchLocalApi("/api/settings/intent/preview"), which masks real
network/server/parse errors. Update the try/catch around the preview request in
WorkerWizard to distinguish an unavailable endpoint from unexpected failures:
keep the current skip only for the known “endpoint missing” case, and for other
exceptions or non-OK responses without usable errors, add an issues entry with
warning/error severity so the user sees the validation problem. Use the existing
intent preview flow, including buildIntentAgentConfig(state), intentCopy, and
issues.push, as the place to surface these failures.

Comment on lines +436 to +448
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm"
onClick={(e) => {
if (e.target === e.currentTarget && step !== "deploy") {
onClose();
}
}}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No keyboard escape or dialog semantics on this modal — big, big accessibility problem, folks.

This modal only closes on a mouse click (backdrop or X button). There's no Escape key handler, no role="dialog"/aria-modal="true", and no visible focus trap. Keyboard-only users can't dismiss it or reliably navigate it — that's not a good deal for anybody.

♿ Proposed fix to add keyboard escape + dialog semantics
         <motion.div
           initial={{ opacity: 0 }}
           animate={{ opacity: 1 }}
           exit={{ opacity: 0 }}
           className="fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm"
+          role="dialog"
+          aria-modal="true"
           onClick={(e) => {
             if (e.target === e.currentTarget && step !== "deploy") {
               onClose();
             }
           }}
+          onKeyDown={(e) => {
+            if (e.key === "Escape" && step !== "deploy") {
+              onClose();
+            }
+          }}
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm"
onClick={(e) => {
if (e.target === e.currentTarget && step !== "deploy") {
onClose();
}
}}
>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm"
role="dialog"
aria-modal="true"
onClick={(e) => {
if (e.target === e.currentTarget && step !== "deploy") {
onClose();
}
}}
onKeyDown={(e) => {
if (e.key === "Escape" && step !== "deploy") {
onClose();
}
}}
>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/components/worker-wizard/WorkerWizard.tsx` around lines 436 -
448, The modal in WorkerWizard is missing keyboard and dialog accessibility
support. Update the modal container inside WorkerWizard to behave like a proper
dialog by adding dialog semantics (role and aria-modal), wiring an Escape key
handler to call onClose when the modal is open, and ensuring focus is
managed/trapped within the modal while it is displayed. Keep the existing
backdrop click behavior intact and make sure the step === "deploy" guard still
applies where appropriate.

Comment thread ops/known-failing.json
Comment on lines +2 to +3
"snapshotAt": "2026-06-12T04:06:58.501Z",
"totalErrors": 189,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Refresh the known-failing snapshot for this release commit.

This file was edited for the v2026.7.7 promotion, but snapshotAt still points to June 12, 2026. A stale baseline can hide newly introduced TypeScript errors or keep removed failures waived.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ops/known-failing.json` around lines 2 - 3, Refresh the known-failing
snapshot used by the release by updating the metadata in known-failing.json so
it reflects the current promotion commit instead of the stale June 12, 2026
baseline. Keep the existing structure but regenerate or update the snapshotAt
value and any related totals from the latest TypeScript failure set, using the
same known-failing snapshot format so the baseline matches the current release
state.

Comment thread src/data/pg-adapter.ts
Comment on lines +2141 to +2147
const agentId = input.agentId ?? this.agentId;
// tasks.agent_id has an FK to agents(id); a family worker created after
// init (or a fresh database) may not have its row yet.
await this.db
.insert(schema.agents)
.values({ id: agentId, name: agentId, status: "active", createdAt: now, updatedAt: now })
.onConflictDoNothing();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Seed missing agent rows in the job task insert paths too.

This fixes TaskAdapter.create, but job-generated tasks still insert directly with assignment.agentId; those paths can hit the same tasks.agent_id FK failure for family workers created after init. Please centralize the seed helper or call it before all task inserts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/data/pg-adapter.ts` around lines 2141 - 2147, The missing-agent seeding
fix in the task insert flow is only applied in TaskAdapter.create, but
job-generated task inserts that use assignment.agentId can still hit the same
tasks.agent_id foreign key failure. Update the shared insert path in
src/data/pg-adapter.ts so the agent row is seeded before every task insert,
either by centralizing the helper already used around this
db.insert(schema.agents) block or by invoking it from all job/task creation
methods that write tasks. Make sure the logic is reused consistently for any
path that creates tasks, not just TaskAdapter.create.

Comment thread src/data/pg-adapter.ts
Comment on lines +2277 to +2301
async releaseClaim(
id: string,
opts?: { claimedBy?: string; requeue?: boolean; reason?: string },
): Promise<Task | null> {
const existing = await this.get(id);
if (!existing) return null;
if (opts?.claimedBy && existing.claimedBy !== opts.claimedBy) return null;
const now = new Date();
const updates: Record<string, any> = {
claimedBy: null,
claimTtl: null,
updatedAt: now,
};
if (opts?.requeue && existing.status === "in_progress") {
updates.status = "pending";
updates.attempt = sql`COALESCE(${schema.tasks.attempt}, 0) + 1`;
if (opts.reason) {
updates.metadata = { ...(existing.metadata ?? {}), leaseReleaseReason: opts.reason };
}
}
const [row] = await this.db
.update(schema.tasks)
.set(updates)
.where(eq(schema.tasks.id, existing.id))
.returning();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Keep the claimant check in the releaseClaim update predicate.

releaseClaim() checks existing.claimedBy before updating, but then updates by task id only. If the lease expires and another worker claims between the read and update, the old claimant can clear the new claim.

Proposed fix
-    const [row] = await this.db
+    const releaseWhere =
+      opts?.claimedBy
+        ? and(eq(schema.tasks.id, existing.id), eq(schema.tasks.claimedBy, opts.claimedBy))
+        : eq(schema.tasks.id, existing.id);
+    const [row] = await this.db
       .update(schema.tasks)
       .set(updates)
-      .where(eq(schema.tasks.id, existing.id))
+      .where(releaseWhere)
       .returning();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async releaseClaim(
id: string,
opts?: { claimedBy?: string; requeue?: boolean; reason?: string },
): Promise<Task | null> {
const existing = await this.get(id);
if (!existing) return null;
if (opts?.claimedBy && existing.claimedBy !== opts.claimedBy) return null;
const now = new Date();
const updates: Record<string, any> = {
claimedBy: null,
claimTtl: null,
updatedAt: now,
};
if (opts?.requeue && existing.status === "in_progress") {
updates.status = "pending";
updates.attempt = sql`COALESCE(${schema.tasks.attempt}, 0) + 1`;
if (opts.reason) {
updates.metadata = { ...(existing.metadata ?? {}), leaseReleaseReason: opts.reason };
}
}
const [row] = await this.db
.update(schema.tasks)
.set(updates)
.where(eq(schema.tasks.id, existing.id))
.returning();
async releaseClaim(
id: string,
opts?: { claimedBy?: string; requeue?: boolean; reason?: string },
): Promise<Task | null> {
const existing = await this.get(id);
if (!existing) return null;
if (opts?.claimedBy && existing.claimedBy !== opts.claimedBy) return null;
const now = new Date();
const updates: Record<string, any> = {
claimedBy: null,
claimTtl: null,
updatedAt: now,
};
if (opts?.requeue && existing.status === "in_progress") {
updates.status = "pending";
updates.attempt = sql`COALESCE(${schema.tasks.attempt}, 0) + 1`;
if (opts.reason) {
updates.metadata = { ...(existing.metadata ?? {}), leaseReleaseReason: opts.reason };
}
}
const releaseWhere =
opts?.claimedBy
? and(eq(schema.tasks.id, existing.id), eq(schema.tasks.claimedBy, opts.claimedBy))
: eq(schema.tasks.id, existing.id);
const [row] = await this.db
.update(schema.tasks)
.set(updates)
.where(releaseWhere)
.returning();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/data/pg-adapter.ts` around lines 2277 - 2301, The `releaseClaim()` path
in `PgAdapter` is missing the claimant guard in the update itself, so the
pre-check can be bypassed by a race. Update the
`this.db.update(schema.tasks)...where(...)` predicate to include the current
claimant condition using `existing.claimedBy` (or `opts.claimedBy` when
provided) alongside the task id, so only the same worker can clear the lease
even if ownership changes between read and write.

Comment thread src/data/pg-adapter.ts
Comment on lines +3663 to +3671
const [row] = await this.sqlClient<{ tasks: string | null; agents: string | null }[]>`
SELECT to_regclass('public.tasks')::text AS tasks,
to_regclass('public.agents')::text AS agents
`;
if (row?.tasks || row?.agents) {
return;
}
log.info("pg adapter: fresh database detected — applying bootstrap schema");
await this.sqlClient.unsafe(PG_BOOTSTRAP_SCHEMA_SQL);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bootstrap when either core table is missing, not only when both are missing.

The current guard returns if tasks or agents exists. A partially initialized database with agents but no tasks will skip bootstrap, then ensureCoreSchema() immediately tries ALTER TABLE tasks and fails.

Proposed fix
-    if (row?.tasks || row?.agents) {
+    if (row?.tasks && row?.agents) {
       return;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [row] = await this.sqlClient<{ tasks: string | null; agents: string | null }[]>`
SELECT to_regclass('public.tasks')::text AS tasks,
to_regclass('public.agents')::text AS agents
`;
if (row?.tasks || row?.agents) {
return;
}
log.info("pg adapter: fresh database detected — applying bootstrap schema");
await this.sqlClient.unsafe(PG_BOOTSTRAP_SCHEMA_SQL);
const [row] = await this.sqlClient<{ tasks: string | null; agents: string | null }[]>`
SELECT to_regclass('public.tasks')::text AS tasks,
to_regclass('public.agents')::text AS agents
`;
if (row?.tasks && row?.agents) {
return;
}
log.info("pg adapter: fresh database detected — applying bootstrap schema");
await this.sqlClient.unsafe(PG_BOOTSTRAP_SCHEMA_SQL);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/data/pg-adapter.ts` around lines 3663 - 3671, The bootstrap guard in the
core schema initialization logic is too broad and only runs when both tables are
absent. Update the check in the pg adapter’s bootstrap path so it detects a
partially initialized database and applies the bootstrap schema whenever either
core table is missing, using the existing `tasks`/`agents` lookup and the
surrounding `ensureCoreSchema` flow as the reference point. Keep the `log.info`
and `PG_BOOTSTRAP_SCHEMA_SQL` path the same, but change the condition so `ALTER
TABLE tasks` is never reached before the base schema exists.

Comment thread src/data/tasks.ts
Comment on lines +810 to +813
claimedBy: row.claimed_by || undefined,
claimTtl: row.claim_ttl || undefined,
claimAcquiredAt: row.claim_acquired_at || undefined,
attempt: row.attempt || undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

attempt: row.attempt || undefined eats the legit 0 value — not fair to the tasks that never got retried!

attempt defaults to 0 in the schema (line 123), but 0 || undefined evaluates to undefined. Every fresh task will report attempt: undefined instead of 0. Use nullish coalescing to keep the real value.

🛠️ Proposed fix
-      attempt: row.attempt || undefined,
+      attempt: row.attempt ?? undefined,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
claimedBy: row.claimed_by || undefined,
claimTtl: row.claim_ttl || undefined,
claimAcquiredAt: row.claim_acquired_at || undefined,
attempt: row.attempt || undefined,
claimedBy: row.claimed_by || undefined,
claimTtl: row.claim_ttl || undefined,
claimAcquiredAt: row.claim_acquired_at || undefined,
attempt: row.attempt ?? undefined,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/data/tasks.ts` around lines 810 - 813, The task mapping in the
row-to-task conversion is treating a valid 0 attempt count as missing because
`attempt` uses a falsy check. Update the conversion logic in the task
hydration/mapping code that reads `row.attempt` so it preserves 0 by using
nullish handling instead of a truthy fallback, matching the schema default and
keeping fresh tasks at attempt 0.

Comment on lines +761 to +836
// Worker Runtime v2 D6 (full #445 semantics): an open task gates task
// creation for the assignment. Operator intent on Run Now is obviously
// "try again", so a BLOCKED gating task is re-queued as a fresh attempt
// by default; { fresh: true } supersedes it and cuts a new task. Silent
// OK-but-nothing is structurally impossible — the response names what
// it did. Gating task resolved via job runs — task metadata is not
// stable linkage (tasks.block replaces it).
let blockedBy: { taskId: string; status: string; title: string } | undefined;
let action: string | undefined;
let dispatched = queuedTasks > 0;
if (queuedTasks === 0) {
const runs = await storage.jobs.listRuns({ assignmentId, limit: 10 });
const seenTaskIds = new Set<string>();
for (const run of runs) {
if (!run.taskId || seenTaskIds.has(run.taskId)) {
continue;
}
seenTaskIds.add(run.taskId);
const task = await storage.tasks.get(run.taskId);
if (
!task ||
(task.status !== "pending" &&
task.status !== "in_progress" &&
task.status !== "blocked")
) {
continue;
}
blockedBy = { taskId: task.id, status: task.status, title: task.title };
if (task.status === "blocked" && fresh) {
const priorAttempts = task.attempt ?? 0;
await storage.tasks.update(task.id, {
status: "cancelled",
metadata: {
...(task.metadata ?? {}),
superseded: true,
supersededAt: now,
supersededBy: "runNow(fresh)",
},
});
// The first ensureDueTasks pass consumed the assignment's
// due-ness (advanced nextRunAt) while the blocked task still
// gated it — make it due again so the fresh task actually cuts.
await storage.jobs.updateAssignment(assignmentId, { nextRunAt: now });
queuedTasks = await storage.jobs.ensureDueTasks({ now, agentId: assignment.agentId });
dispatched = queuedTasks > 0;
action = `superseded blocked task ${task.id} (after ${priorAttempts} attempt(s)) and queued ${queuedTasks} fresh task(s)`;
} else if (task.status === "blocked") {
const priorAttempts = task.attempt ?? 0;
// Drop the stale block reason (pg writes blockReason, sqlite
// blockedReason) — the task is honestly pending again.
const {
blockReason: _blockReason,
blockedReason: _blockedReason,
...carriedMetadata
} = (task.metadata ?? {}) as Record<string, unknown>;
await storage.tasks.update(task.id, {
status: "pending",
attempt: 0,
metadata: {
...carriedMetadata,
requeuedBy: "runNow",
requeuedAt: now,
...(priorAttempts > 0 ? { attemptHistory: priorAttempts } : {}),
},
});
dispatched = true;
action = `re-queued blocked task ${task.id} as a fresh attempt (history: ${priorAttempts} prior attempt(s)); pass fresh:true to supersede it instead`;
} else if (task.status === "pending") {
dispatched = true;
action = `pending task ${task.id} is already queued for this assignment — dispatched the worker now`;
} else {
action = `task ${task.id} is in_progress${task.claimedBy ? ` (leased by ${task.claimedBy})` : ""} — letting the live run finish`;
}
break;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The "no silent nothing" promise has a loophole — let's plug it.

The comment says the response always "names what it did," but if queuedTasks === 0 and none of the last 10 runs have a matching pending/in_progress/blocked task, action and blockedBy stay undefined and the response silently reports dispatched: false with zero explanation. Small gap, big promise broken.

🩹 Proposed fix — always set a fallback action
       if (queuedTasks === 0) {
         const runs = await storage.jobs.listRuns({ assignmentId, limit: 10 });
         const seenTaskIds = new Set<string>();
+        let foundBlocker = false;
         for (const run of runs) {
           if (!run.taskId || seenTaskIds.has(run.taskId)) {
             continue;
           }
           seenTaskIds.add(run.taskId);
           const task = await storage.tasks.get(run.taskId);
           if (
             !task ||
             (task.status !== "pending" &&
               task.status !== "in_progress" &&
               task.status !== "blocked")
           ) {
             continue;
           }
+          foundBlocker = true;
           blockedBy = { taskId: task.id, status: task.status, title: task.title };
           ...
           break;
         }
+        if (!foundBlocker) {
+          action = "no due or blocking task found for this assignment — nothing to dispatch";
+        }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Worker Runtime v2 D6 (full #445 semantics): an open task gates task
// creation for the assignment. Operator intent on Run Now is obviously
// "try again", so a BLOCKED gating task is re-queued as a fresh attempt
// by default; { fresh: true } supersedes it and cuts a new task. Silent
// OK-but-nothing is structurally impossible — the response names what
// it did. Gating task resolved via job runs — task metadata is not
// stable linkage (tasks.block replaces it).
let blockedBy: { taskId: string; status: string; title: string } | undefined;
let action: string | undefined;
let dispatched = queuedTasks > 0;
if (queuedTasks === 0) {
const runs = await storage.jobs.listRuns({ assignmentId, limit: 10 });
const seenTaskIds = new Set<string>();
for (const run of runs) {
if (!run.taskId || seenTaskIds.has(run.taskId)) {
continue;
}
seenTaskIds.add(run.taskId);
const task = await storage.tasks.get(run.taskId);
if (
!task ||
(task.status !== "pending" &&
task.status !== "in_progress" &&
task.status !== "blocked")
) {
continue;
}
blockedBy = { taskId: task.id, status: task.status, title: task.title };
if (task.status === "blocked" && fresh) {
const priorAttempts = task.attempt ?? 0;
await storage.tasks.update(task.id, {
status: "cancelled",
metadata: {
...(task.metadata ?? {}),
superseded: true,
supersededAt: now,
supersededBy: "runNow(fresh)",
},
});
// The first ensureDueTasks pass consumed the assignment's
// due-ness (advanced nextRunAt) while the blocked task still
// gated it — make it due again so the fresh task actually cuts.
await storage.jobs.updateAssignment(assignmentId, { nextRunAt: now });
queuedTasks = await storage.jobs.ensureDueTasks({ now, agentId: assignment.agentId });
dispatched = queuedTasks > 0;
action = `superseded blocked task ${task.id} (after ${priorAttempts} attempt(s)) and queued ${queuedTasks} fresh task(s)`;
} else if (task.status === "blocked") {
const priorAttempts = task.attempt ?? 0;
// Drop the stale block reason (pg writes blockReason, sqlite
// blockedReason) — the task is honestly pending again.
const {
blockReason: _blockReason,
blockedReason: _blockedReason,
...carriedMetadata
} = (task.metadata ?? {}) as Record<string, unknown>;
await storage.tasks.update(task.id, {
status: "pending",
attempt: 0,
metadata: {
...carriedMetadata,
requeuedBy: "runNow",
requeuedAt: now,
...(priorAttempts > 0 ? { attemptHistory: priorAttempts } : {}),
},
});
dispatched = true;
action = `re-queued blocked task ${task.id} as a fresh attempt (history: ${priorAttempts} prior attempt(s)); pass fresh:true to supersede it instead`;
} else if (task.status === "pending") {
dispatched = true;
action = `pending task ${task.id} is already queued for this assignment — dispatched the worker now`;
} else {
action = `task ${task.id} is in_progress${task.claimedBy ? ` (leased by ${task.claimedBy})` : ""} — letting the live run finish`;
}
break;
}
}
// Worker Runtime v2 D6 (full `#445` semantics): an open task gates task
// creation for the assignment. Operator intent on Run Now is obviously
// "try again", so a BLOCKED gating task is re-queued as a fresh attempt
// by default; { fresh: true } supersedes it and cuts a new task. Silent
// OK-but-nothing is structurally impossible — the response names what
// it did. Gating task resolved via job runs — task metadata is not
// stable linkage (tasks.block replaces it).
let blockedBy: { taskId: string; status: string; title: string } | undefined;
let action: string | undefined;
let dispatched = queuedTasks > 0;
if (queuedTasks === 0) {
const runs = await storage.jobs.listRuns({ assignmentId, limit: 10 });
const seenTaskIds = new Set<string>();
let foundBlocker = false;
for (const run of runs) {
if (!run.taskId || seenTaskIds.has(run.taskId)) {
continue;
}
seenTaskIds.add(run.taskId);
const task = await storage.tasks.get(run.taskId);
if (
!task ||
(task.status !== "pending" &&
task.status !== "in_progress" &&
task.status !== "blocked")
) {
continue;
}
foundBlocker = true;
blockedBy = { taskId: task.id, status: task.status, title: task.title };
if (task.status === "blocked" && fresh) {
const priorAttempts = task.attempt ?? 0;
await storage.tasks.update(task.id, {
status: "cancelled",
metadata: {
...(task.metadata ?? {}),
superseded: true,
supersededAt: now,
supersededBy: "runNow(fresh)",
},
});
// The first ensureDueTasks pass consumed the assignment's
// due-ness (advanced nextRunAt) while the blocked task still
// gated it — make it due again so the fresh task actually cuts.
await storage.jobs.updateAssignment(assignmentId, { nextRunAt: now });
queuedTasks = await storage.jobs.ensureDueTasks({ now, agentId: assignment.agentId });
dispatched = queuedTasks > 0;
action = `superseded blocked task ${task.id} (after ${priorAttempts} attempt(s)) and queued ${queuedTasks} fresh task(s)`;
} else if (task.status === "blocked") {
const priorAttempts = task.attempt ?? 0;
// Drop the stale block reason (pg writes blockReason, sqlite
// blockedReason) — the task is honestly pending again.
const {
blockReason: _blockReason,
blockedReason: _blockedReason,
...carriedMetadata
} = (task.metadata ?? {}) as Record<string, unknown>;
await storage.tasks.update(task.id, {
status: "pending",
attempt: 0,
metadata: {
...carriedMetadata,
requeuedBy: "runNow",
requeuedAt: now,
...(priorAttempts > 0 ? { attemptHistory: priorAttempts } : {}),
},
});
dispatched = true;
action = `re-queued blocked task ${task.id} as a fresh attempt (history: ${priorAttempts} prior attempt(s)); pass fresh:true to supersede it instead`;
} else if (task.status === "pending") {
dispatched = true;
action = `pending task ${task.id} is already queued for this assignment — dispatched the worker now`;
} else {
action = `task ${task.id} is in_progress${task.claimedBy ? ` (leased by ${task.claimedBy})` : ""} — letting the live run finish`;
}
break;
}
if (!foundBlocker) {
action = "no due or blocking task found for this assignment — nothing to dispatch";
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gateway/server-methods/jobs.ts` around lines 761 - 836, The runNow flow
in jobs.ts can still return without any explanation when queuedTasks is 0 and no
matching task is found from storage.jobs.listRuns; make sure the handler always
sets a fallback action instead of leaving action and blockedBy undefined. Update
the logic around the queuedTasks check and the run/task scan so the response
from the runNow path always names what happened, even when no eligible task
exists. Use the existing blockedBy, action, and dispatched variables in the
runNow handler to add a clear default message for the “nothing found” case.

…k.yaml + scripts/install-hosted.sh in favor of release) for v2026.7.7 promotion

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCTvhCHX79hvV5DWQWqqFq
@webdevtodayjason
webdevtodayjason merged commit acbf929 into main Jul 9, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment