Release v2026.7.7 — promote to main#471
Conversation
…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>
📝 WalkthroughWalkthroughFolks, 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. ChangesBusiness-to-Core Merge & Runtime
Estimated code review effort: 5 (Critical) | ~180 minutes Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Comment |
There was a problem hiding this comment.
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 liftHuge, 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 winUse
json_stringfor dynamic hello-response strings too.
request.idand 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 winSkip 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/
lastUsedAtbookkeeping. 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/isMinimalPromptRundeclaration.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 winBig, beautiful trap warning — but the actual command doesn't protect you from it, folks!
The comment block correctly warns that
ARGENT_INSTALL_URLdefaults to the LIVE, previous-release installer. But the runnable command on Line 184 is just the bareARGENT_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_URLis 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:smokeSince 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 winString-interpolated
${Number(now)}in the sweep query — swap it for a bound parameter, no reason to live dangerously.
Number()coercion mitigates real injection risk sincenowis 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 winNo index for the new lease-query hot path — could get slow at scale, and nobody likes slow, believe me.
claim()'sWHEREandsweepExpiredClaims()'s candidateSELECTboth filter onstatus/claimed_by/claim_ttl, but the migration only adds columns, no supporting index. On a bigtaskstable 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 valueA little duplication here, folks — same pattern three times over.
The
Array.isArray(...) ? ... : []+.flatMap((entry) => typeof entry === "string" ? [entry] : [])shape repeats forintegrations,neverDo, andrequiresApprovalFor. A smalltoStringArray(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 winThese saves run one-by-one — parallelize 'em, it'll be huge for speed.
The
forloop awaits eachsaveDocPanelArtifactcall sequentially. Since artifacts are independent, running them viaPromise.allSettledwould 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 valueDeep
../../../../import — a little messy, could be tremendous with an alias.Four levels of relative traversal into
src/configis 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 valueA small, very avoidable double-computation — nothing tremendous, but let's tidy it up.
asNumber(shadow.lastBoardMaxUpdatedAt, ...)and the same forlastBoardTaskCountare 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 winFixed 1500ms sleep instead of waiting on the actual
connectack — folks, that's a real race, not fake news.
main()starts its gateway RPC calls (jobs.templates.list, etc.) after a flatsetTimeout(..., 1500), hoping the WebSocket is open and theconnecthandshake (fired insidews.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 winBig, tremendous fallback — but silent, folks.
When
loadSkillsFromDirreturns neither an array nor{ skills: [...] }, this quietly returns[]for that directory. No warning, nobody knows skills just vanished for that source. A smallconsole.warnwould 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 valueManual JSON param scraping strikes again — but hey, it's consistent with the neighborhood.
find_param_bool/find_param_json_string/find_param_numberare hand-rolled scanners rather than using a real JSON parser, but that matches the existingfind_optional_param_stringpattern already in this file. Not asking for a rewrite of the whole file's parsing strategy here — just flagging thatfind_param_number's digit/sign/exponent take_while can build malformed tokens (e.g."1.2.3","--1") that fail.parse()and fall back toNonesilently. Safe today given theunwrap_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 winGive 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 winMark active mode with
aria-pressedtoo.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 valueCosmetic-only tweak, not a real fix.
Wrapping in
String(err)doesn't change behavior for non-Errorthrowables — 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 valueUnvalidated 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 onparsed.escalation.maxAttempts/sentimentThresholdtypes 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 valueNice catch on the silent tool miss — logging before throw is a great, great move.
Minor nit:
console.warnis 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 winThat 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 winFolks, 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 valueRegex-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.registerretries 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 | 🔵 TrivialBoard cache refresh fires an unfiltered
tasks.list()on every tick.
refreshBoardCache()kicks off on every scheduled tick regardless oftickMsor 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 | nullis now always non-null — thenullpath is dead weight.Since
getOptionalIntentApi()unconditionally returns the object now, the| nullin the return type and the downstreamif (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 winTremendous 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 inisModelProviderAvailableForAutomaticRouting(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 valueMinor 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_frontis 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 winLabels aren't wired to their inputs — not great for accessibility, believe me.
None of the
<label>elements in this form usehtmlForpaired with an inputid(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 valueSmall nit: that fallback object at the end is basically dead code.
Since the loop only runs when
mediaUrls.length > 0,finalResultwill 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 winAdd an index for lease-expiry sweeps — big, fast wins here, folks.
claimedBy/claimTtlwill 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 winGive 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 winMove
colorMapout of the component, folks — nobody wants to rebuild this beautiful object every single render!
colorMapis 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 winUse
effectiveToolsfor native-provider tool context.When
simulateWritesis enabled, execution uses wrapped tools, but the native Argent provider context still advertises the originaltools. Feed the sameeffectiveToolsinto 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
⛔ Files ignored due to path filters (2)
dashboard/package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (279)
.argent-repo.json.github/workflows/ci.yml.gitignoreAGENTS.mdCHANGELOG.mdHANDOFF.mdORCHESTRATION.mdREADME.mddashboard/api-server.cjsdashboard/package.jsondashboard/src/App.tsxdashboard/src/components/WorkerFlowModal.tsxdashboard/src/components/WorkerFlowModalBridge.tsdashboard/src/components/WorkforceBoard.tsxdashboard/src/components/WorkforceBoardBridge.tsdashboard/src/components/widgets/OrgChartWidget.tsxdashboard/src/components/widgets/WorkflowsWidget.tsxdashboard/src/components/worker-wizard/AIChatSidebar.tsxdashboard/src/components/worker-wizard/WorkerWizard.tsxdashboard/src/components/worker-wizard/index.tsdashboard/src/components/worker-wizard/shared/HierarchyDiagram.tsxdashboard/src/components/worker-wizard/shared/StepIndicator.tsxdashboard/src/components/worker-wizard/shared/TagInput.tsxdashboard/src/components/worker-wizard/steps/StepBoundaries.tsxdashboard/src/components/worker-wizard/steps/StepDepartment.tsxdashboard/src/components/worker-wizard/steps/StepDeploy.tsxdashboard/src/components/worker-wizard/steps/StepIdentity.tsxdashboard/src/components/worker-wizard/steps/StepReview.tsxdashboard/src/components/worker-wizard/steps/StepSimulation.tsxdashboard/src/components/worker-wizard/types.tsdashboard/src/lib/configSurfaceProfile.tsdashboard/tsconfig.app.jsondocs/concepts/core-business-boundary.mddocs/tools/slash-commands.mdextensions/bluebubbles/package.jsonextensions/canvas-docs-enforcer/package.jsonextensions/copilot-proxy/package.jsonextensions/dashboard-canvas-bridge/package.jsonextensions/diagnostics-otel/package.jsonextensions/discord/package.jsonextensions/feishu/package.jsonextensions/google-gemini-cli-auth/package.jsonextensions/google-meet/package.jsonextensions/google-workspace/package.jsonextensions/googlechat/package.jsonextensions/imessage/package.jsonextensions/line/package.jsonextensions/llm-task/package.jsonextensions/lobster/package.jsonextensions/matrix/CHANGELOG.mdextensions/matrix/package.jsonextensions/mattermost/package.jsonextensions/memory-lancedb/package.jsonextensions/minimax-portal-auth/package.jsonextensions/msteams/CHANGELOG.mdextensions/msteams/package.jsonextensions/nextcloud-talk/package.jsonextensions/nostr/CHANGELOG.mdextensions/nostr/package.jsonextensions/open-prose/package.jsonextensions/response-validator/package.jsonextensions/signal/package.jsonextensions/slack/package.jsonextensions/summarize-tts-enforcer/package.jsonextensions/tasks-projects-enforcer/package.jsonextensions/telegram/package.jsonextensions/telegram/src/channel.sendpayload.test.tsextensions/telegram/src/channel.tsextensions/tlon/package.jsonextensions/twitch/CHANGELOG.mdextensions/twitch/package.jsonextensions/voice-call/CHANGELOG.mdextensions/voice-call/package.jsonextensions/whatsapp/package.jsonextensions/zalo/CHANGELOG.mdextensions/zalo/package.jsonextensions/zalouser/CHANGELOG.mdextensions/zalouser/package.jsonops/AGENT_PERSONA_ONBOARDING_PROMPT.mdops/BUSINESS_MERGE_LANDING_MAP_2026-06-10.mdops/WORKER_RUNTIME_V2_DESIGN_2026-06-11.mdops/known-failing.jsonops/runbooks/release-core.mdpackage.jsonrust/AUTHORITY_DELEGATION_SEAM.mdrust/argent-execd/PROMOTION_CHECKLIST.mdrust/argent-execd/src/main.rsrust/argent-execd/src/server.rsrust/argent-execd/tests/http_control.rsrust/argentd/src/http.rsrust/argentd/src/hub.rsrust/argentd/src/main.rsrust/argentd/src/ws.rsrust/argentd/tests/ws_parity.rsscripts/demo/conductor-demo.mjsscripts/export-public-core.tsscripts/generate-pg-bootstrap.tsscripts/install-hosted.shscripts/ui.jssrc/agents/argent-tools-core.tssrc/agents/argent-tools.public-core.test.tssrc/agents/argent-tools.tssrc/agents/auth-profiles.auth-profile-cooldowns.test.tssrc/agents/auth-profiles/store.tssrc/agents/intent-runtime-gate-core.tssrc/agents/intent-runtime-gate.tssrc/agents/intent-simulation.tssrc/agents/model-auth.test.tssrc/agents/model-auth.tssrc/agents/optional-intent.test.tssrc/agents/optional-intent.tssrc/agents/optional-tool-factory.test.tssrc/agents/optional-tool-factory.tssrc/agents/pi-embedded-runner.test.tssrc/agents/pi-embedded-runner/google.tssrc/agents/pi-embedded-runner/run.overflow-compaction.test.tssrc/agents/pi-embedded-runner/run.tssrc/agents/pi-embedded-runner/run/attempt.tssrc/agents/pi-embedded-runner/run/params.tssrc/agents/pi-embedded-runner/run/types.tssrc/agents/pi-embedded-runner/simulate-tool-stub.test.tssrc/agents/pi-embedded-runner/simulate-tool-stub.tssrc/agents/pi-embedded-runner/types.tssrc/agents/pi-tools.policy.tssrc/agents/pi-tools.tssrc/agents/pi-tools.worker-session-policy.test.tssrc/agents/public-core-tools.tssrc/agents/skills.tssrc/agents/skills/personal-skill-read-cache.test.tssrc/agents/skills/personal-skill-read-cache.tssrc/agents/skills/personal.tssrc/agents/skills/workspace.tssrc/agents/test-helpers/fast-coding-tools.tssrc/agents/test-helpers/fast-core-tools.tssrc/agents/tool-claim-validation.tssrc/agents/tools/async-service-key-tools.test.tssrc/agents/tools/audio-alert-tool.tssrc/agents/tools/audio-generation-tool.tssrc/agents/tools/channel-config-tool.tssrc/agents/tools/coolify-deploy-tool.tssrc/agents/tools/copilot-system-tool.test.tssrc/agents/tools/copilot-system-tool.tssrc/agents/tools/email-delivery-tool.test.tssrc/agents/tools/email-delivery-tool.tssrc/agents/tools/intent-tool.test.tssrc/agents/tools/intent-tool.tssrc/agents/tools/jobs-tool.tssrc/agents/tools/memu-recall-quality.test.tssrc/agents/tools/memu-tools.tssrc/agents/tools/namecheap-dns-tool.tssrc/agents/tools/onboarding-pack-tool.tssrc/agents/tools/personal-skill-tool.tssrc/agents/tools/railway-deploy-tool.tssrc/agents/tools/tts-generate-tool.tssrc/agents/tools/vercel-deploy-tool.tssrc/agents/tools/web-tools.enabled-defaults.test.tssrc/agents/tools/work-report-tool.test.tssrc/agents/tools/work-report-tool.tssrc/agents/tools/workforce-setup-tool.tssrc/agents/worker-role-profile.test.tssrc/agents/worker-role-profile.tssrc/argent-agent/create-agent-session.tssrc/argent-ai/providers/zai.tssrc/auto-reply/reply/commands-compact.tssrc/auto-reply/reply/session.tssrc/cli/intent-cli-core.tssrc/commands/agent.delivery.test.tssrc/commands/agent.test.tssrc/commands/agent.tssrc/commands/agent/types.tssrc/commands/doctor-auth.deprecated-cli-profiles.test.tssrc/commands/doctor-auth.tssrc/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.test.tssrc/commands/doctor.runs-legacy-state-migrations-yes-mode-without.test.tssrc/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.test.tssrc/commands/doctor.warns-state-directory-is-missing.test.tssrc/commands/status.executive-shadow.tssrc/config/load-profile.test.tssrc/config/schema.tssrc/config/types.agent-defaults.tssrc/config/validation.tssrc/config/zod-schema.agent-runtime.tssrc/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.tssrc/dashboard-surface-profile.test.tssrc/data/adapter.tssrc/data/dual-adapter.tssrc/data/jobs.test.tssrc/data/migrate/sqlite-to-pg.tssrc/data/pg-adapter.tssrc/data/pg/bootstrap-schema.tssrc/data/pg/schema.tssrc/data/sqlite-adapter.tssrc/data/tasks.lease.test.tssrc/data/tasks.tssrc/data/types.tssrc/discord/monitor/provider.tssrc/gateway/job-orchestrator-bridge.tssrc/gateway/server-methods-list.tssrc/gateway/server-methods.tssrc/gateway/server-methods/commands.compact-agent-resolution.test.tssrc/gateway/server-methods/commands.tssrc/gateway/server-methods/copilot.tssrc/gateway/server-methods/execution-worker.tssrc/gateway/server-methods/intent-core.tssrc/gateway/server-methods/intent.tssrc/gateway/server-methods/jobs-grades.test.tssrc/gateway/server-methods/jobs.tssrc/gateway/server-methods/skills.tssrc/gateway/server-methods/types.tssrc/gateway/server-methods/workflows.output-channels.test.tssrc/gateway/server-methods/workflows.tssrc/gateway/server-startup.tssrc/gateway/server.impl.tssrc/infra/app-forge-model.test.tssrc/infra/app-forge-views.test.tssrc/infra/consciousness-kernel-salience.test.tssrc/infra/consciousness-kernel-state.tssrc/infra/consciousness-kernel.test.tssrc/infra/consciousness-kernel.tssrc/infra/contemplation-runner.tssrc/infra/execution-worker-runner-core.tssrc/infra/execution-worker-runner-impl.tssrc/infra/execution-worker-runner.test.tssrc/infra/execution-worker-runner.tssrc/infra/executive-shadow-client.integration.test.tssrc/infra/executive-shadow-client.tssrc/infra/gateway-log-rotation.test.tssrc/infra/gateway-log-rotation.tssrc/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.tssrc/infra/heartbeat-runner.returns-default-unset.test.tssrc/infra/heartbeat-runner.tssrc/infra/idle-salience-gate.test.tssrc/infra/idle-salience-gate.tssrc/infra/intent-simulation-runner.tssrc/infra/intent-simulation-scenarios-t1.tssrc/infra/job-orchestrator-runner.tssrc/infra/keychain.tssrc/infra/license-core.tssrc/infra/public-core-denylist.test.tssrc/infra/public-core-export.test.tssrc/infra/public-core-export.tssrc/infra/run-event-log.test.tssrc/infra/run-event-log.tssrc/infra/sis-runner.tssrc/infra/update-runner.test.tssrc/infra/update-runner.tssrc/infra/work-report-crosscheck.test.tssrc/infra/work-report-crosscheck.tssrc/infra/worker-ephemeral-session.test.tssrc/infra/worker-ephemeral-session.tssrc/infra/worker-grading.test.tssrc/infra/worker-grading.tssrc/infra/workflow-approvals.text-command.test.tssrc/infra/workflow-approvals.tssrc/infra/workflow-execution-service.approval-drain.test.tssrc/infra/workflow-execution-service.approval-timeout.test.tssrc/infra/workflow-execution-service.tssrc/infra/workflow-runner.session-policy.test.tssrc/infra/workflow-runner.tssrc/licensing/client.tssrc/licensing/crypto.tssrc/licensing/index.tssrc/licensing/license.tssrc/licensing/manager.tssrc/licensing/secret-sync.tssrc/licensing/storage.tssrc/licensing/types.tssrc/memory/live-inbox/capture.tssrc/plugins/org-scope.tssrc/telegram/bot-handlers.tssrc/tui/components/filterable-select-list.tssrc/tui/components/searchable-select-list.tssrc/utils/dashboard-api.test.tssrc/utils/dashboard-api.tssrc/utils/optional-module.tssrc/wizard/onboarding.test.tstsdown.config.tsui/package.jsonvitest.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
| 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: [], | ||
| }, | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
🎯 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.
| <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> |
There was a problem hiding this comment.
🎯 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:
- 1: https://web.dev/blog/intl-segmenter
- 2: https://developer.mozilla.org/en-US/blog/javascript-intl-segmenter-i18n/
- 3: https://emojifyi.com/ar/stories/how-to-use-emojis-in-javascript/
- 4: https://web-platform-dx.github.io/web-features-explorer/features/intl-segmenter/
- 5: https://docs.w3cub.com/javascript/global_objects/intl/segmenter
- 6: https://monkeyraptor.johanpaul.net/2026/01/string-split-that-does-not-butcher.html
- 7: https://blog.sangeeth.dev/posts/accurate-text-lengths-with-intl-segmenter-api/
- 8: https://github.com/astoilkov/segmenter
- 9: https://tc39.es/proposal-intl-segmenter/
🏁 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.
| // 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 | ||
| } |
There was a problem hiding this comment.
🩺 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.
| // 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.
| <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(); | ||
| } | ||
| }} | ||
| > |
There was a problem hiding this comment.
🩺 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.
| <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.
| "snapshotAt": "2026-06-12T04:06:58.501Z", | ||
| "totalErrors": 189, |
There was a problem hiding this comment.
📐 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.
| 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(); |
There was a problem hiding this comment.
🗄️ 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.
| 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(); |
There was a problem hiding this comment.
🩺 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.
| 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.
| 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); |
There was a problem hiding this comment.
🩺 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.
| 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.
| claimedBy: row.claimed_by || undefined, | ||
| claimTtl: row.claim_ttl || undefined, | ||
| claimAcquiredAt: row.claim_acquired_at || undefined, | ||
| attempt: row.attempt || undefined, |
There was a problem hiding this comment.
🎯 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.
| 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.
| // 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 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.
| // 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
Promotes the v2026.7.7 release commit (
b4cdc1ed, tagv2026.7.7) tomain, 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
Bug Fixes
Documentation