Commit Graph

77 Commits

Author SHA1 Message Date
kami d1b84a1a9f fix(gates,tools): stop pinning deleted files; match edit targets across blank lines (#419, #417)
Both defects come from live session fced377e, where scaffold_frontend burned 89
turns without converging.

#419 — the stage output manifest never tracked deletions. A deletion is a
FileWrittenEvent with a null postImageHash, and stageWrittenPaths filtered that
per-event instead of per-path, so a written-then-deleted file stayed in the
manifest forever. The contract gate stamps file_exists on every entry, which made
deleting — and therefore renaming, which is delete plus write — a permanent
contract violation. That deadlocked against the build gate: it ordered "rename it
to use the '.cjs' file extension", the agent complied, and the contract gate then
failed file_exists on the .js it had just been told to remove. Keep the last
mutation per path instead. Fixed in the shared function, so the build-gate
toolchain probe, the review gate, and verification all stop seeing deleted files
too. Moved to its own file to keep the gates file under the detekt function cap.

#417 — file_edit failed 4 of 6 live calls, all "Target not found" with a correct
"did you mean" suggestion attached. flexibleMatch already tolerated indentation
drift but walked target lines against file lines positionally, so one blank line
the model dropped shifted every later index and missed the whole block. Compare
only non-blank lines and map the hit back to real file lines. Ambiguity still
fails rather than picking a match, and reindent keeps handling the writeback.

Guard tests both ways, each mutation-verified against the old logic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:23:51 +04:00
kami ac4601562a fix(shell): timeout is recoverable + coaches detach, not a workflow kill
A foreground long-running command (npm run dev, vite, a --watch task) hit
the 30s timeout and returned recoverable=false, giving the stage no
transition to take -> WorkflowFailed "no transition condition matched"
(killed session d734e1de). A non-zero exit right below already returns
recoverable=true; a timeout is no more fatal. Return recoverable and coach
the model to start the process detached (nohup ... &) and verify separately,
or run a one-shot that exits. No background-process registry: the model
backgrounds it itself via `&`, which already routes through `sh -c`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMbPmZZjcXhR2crU82zZ8S
2026-07-21 20:38:04 +04:00
kami 12775d56da refactor(acr): ACR Store 1 as a disposable memo, not a durable side-store (#305)
The shipped Store 1 persisted repo-file descriptors in a new SqliteObservationStore
— a second, unsynchronized SQLite writer on the event-log DB (no WAL/busy_timeout,
one shared Connection across coroutines) holding a fact the log already carries. That
violates "the event log is the only source of truth" (inv #1/#8): an Observation is
just FileWrittenEvent.path + postImageHash + describe(bytes).render(), and render is a
pure function of content-addressed CAS bytes.

Replace the whole durable apparatus with an in-process memo (descriptorMemo, keyed on
repoRoot+path+contentHash) on the orchestrator, next to the existing artifactContentCache.
Rebuilt from the log on restart, disposable, no external store. Preserves the perf win
(skip CAS read + regex on repeat describes) with none of the concurrency/lock risk.

Deletes: ObservationStore/InMemoryObservationStore/SqliteObservationStore (+ tests),
InfrastructureModule.createObservationStore, the FileReadTool priming path (which never
hit anyway — it keyed on a workspace-relative path while every consumer looks up the
absolute FileWrittenEvent.path), the FileReadConfig/SessionOrchestrator plumbing, and
five orphaned build.gradle deps.

Store 2 (soft-confidence hints) unchanged — it was correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMbPmZZjcXhR2crU82zZ8S
2026-07-21 18:42:47 +04:00
kami 8806de1628 feat(acr): Store 2 soft-confidence hints + Store 1 FileReadTool priming (#305)
Store 2 (Fixes): ConceptCompilerProjection already tracked the
unconfirmed(validatedFixes>=1)/falsified(contradicted)/confirmed(promoted)
lifecycle as data, but only confirmed (>=threshold) clusters were ever
delivered. unconfirmedFixEntries reactively matches the CURRENT retry's
classKey against that state and injects a soft one-shot hint (below
threshold) or steer-away (contradicted) before hard promotion — wired
into stage context build in SessionOrchestratorExecution.kt.

Store 3 (Plan-shapes): already shipped as SessionOrchestratorPlanPatterns.kt
(deterministic keyword-Jaccard over eventStore.allEvents(), no embedder
needed) — confirmed complete, no new code required.

FileReadTool hot-path extension to Store 1: a whole-file read now primes
the cross-session ObservationStore (when wired) the same way the write
path already does, so a later describeCached lookup for the same content
hash is warm even if the file was only read, not written, this session.
2026-07-21 17:55:19 +04:00
kami f08a784432 fix(orchestration): break the no-op-write loop + un-rot retry feedback
A stage (configure_ethos_theme) looped forever re-writing an identical
postcss.config.js: file_write returned "written successfully" on a byte-
identical write, and the retry-feedback entry that says WHY it failed got
truncated out of context every turn, so the model cold-started on the same
wrong idea. CAS hashes leaked into its face from two sides too.

- SandboxedToolExecutor: a write whose result is byte-identical to disk now
  returns "No change: … nothing was written" (+ noop=true) instead of a false
  success signal. Uses the pre-image hashes already captured for reversibility.
- DefaultContextPackBuilder: pin "retryFeedback" in neverDropSourceTypes so the
  failure reason + already-written-files list survives truncation.
- ContextFeedback: drop the opaque "— CAS <hash>" from the retry entry; path only.
- tool_output withheld until the session actually spills over-cap output to CAS
  (StageConfig.ALWAYS_AVAILABLE_READ_TOOLS -> on-demand via sessionHasSpilledOutput),
  so a hash-eating tool isn't advertised to every stage that never spills.
- Wrap 10 long lines in kernel to bring detekt back under maxIssues (99 -> 89).

Tests: infrastructure:tools (+2 no-op cases), core:{transitions,context,kernel} green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:22:39 +04:00
kami 1acb5cc8ff fix(tools): tolerate indent drift + content alias in file_edit; concrete scope-widen remedy
file_edit failed en masse for small models on two ergonomics traps:
- replace() rejected calls that sent `content` (the append param) instead of
  `replacement` — intent unambiguous; now accepted as an alias.
- exact-string match died on leading-whitespace drift (model can't reproduce
  indentation). Added whitespace-flexible line matching + replacement reindent,
  wired into both the pre-exec validation gate and replace().

Also made the WRITE_SCOPE / PATH_OUTSIDE_MANIFEST block messages emit a literal
copy-pasteable task_update(id=..., affected_paths=[...]) call and warn against
action=block — the exact wrong turn models kept taking (18x in one session).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rdo9fe7SujNVeyZA8YkpkD
2026-07-21 00:25:54 +04:00
kami 9d6a0ce4ee fix(shell): recover per-token JSON separator leak in argv
Weak models re-emit `npm -v` as ["npm,","-v"] or ["npm\",","\"-v\""] —
the JSON array separators leak into the tokens. The collapsed-array guard
rejected these, and the model re-emitted the identical mangle until
stage_loop_break killed the run (observed 6x on the web-ui QA workflow,
session 508c8d58). Strip stray leading/trailing quote/comma per token so
the call runs instead of looping the stage to death. Internal commas
(--foo=a,b) are kept; a single fully-collapsed token still rejects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:09:46 +04:00
kami 95b16a5047 fix(weak-model-gates): stop three gates from stalling weak stage models
Diagnosed from session 508c8d58 (frontend freestyle run, WorkflowFailed):
three independent weak-model-hostile gates, none a model-capability problem.

- shell: split a collapsed single-string command line (["npm create vite …"])
  into tokens instead of rejecting it as a "collapsed array". The model
  reliably re-emits this shape; rejecting looped bootstrap_frontend until
  stage_loop_break. JSON-escape mangles (quotes/commas in argv[0]) stay rejected.
- recovery: a stage_loop_break route now gets a "Stuck-loop ticket", not the
  "Contract arbitration ticket". The arbitration prompt told the model to read
  and reconcile "the files named below" — but a tool-syntax loop names zero
  files, sending the recovery agent grepping the repo for 40+ turns until the
  repair ladder exhausted.
- plan lint: H3 (unreferenced_prompt_artifact) demoted from hard failure to
  soft finding, and seeds excluded from it. It word-matches artifact IDs in
  free prose and cannot tell a forgotten dep from a descriptive mention, so as
  a hard gate it burned architect retries on words it couldn't reword away
  (analysis/dod). Hard tier is now deterministic graph facts only (H1/H2).

Tests: ShellToolTest, PlanLinterTest, RecoveryRoutingTest green; detekt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ly2mMnt9TCZbvhcC1JfuV
2026-07-19 22:33:01 +04:00
kami 1b58bc325e wip(freestyle/acr): grounding & edit-tool fixes + ACR-compiler experiment
This branch's uncommitted WIP, committed together (entangled at file level).
Distinct pieces of work:

Freestyle QA fixes (this session):
- FileEditTool: pre-validate replace anchor in validateRequest — reject a
  missing/ambiguous target BEFORE the approval gate, mirroring read/write's
  file-not-found / read-before-write pre-checks. Shared not-found/ambiguous
  messages between validate and execute so they can't drift.
- PlanGrounder: add `scanned` flag; when no RepoMapComputedEvent was recorded,
  repoMapPaths is "unknown" not "empty workspace" — skip scope grounding
  (which proves a path ABSENT) so real paths (apps/server/**) aren't falsely
  rejected. Build-manifest check still runs.
- FreestyleDriver: wire scanned=(repoMap!=null); on plan rejection emit a
  session-terminal WorkflowFailedEvent so a rejected run reads FAILED, not the
  COMPLETED-lie (last verdict was the planning-phase WorkflowCompleted).
- ServerModule: resolve project-memory workspace root from the session's bound
  workspace (sessionWorkspaceRoot) instead of boot-static pm.repoRoot(), fixing
  the workspace-binding divergence (correx vs empty scratch dir). Retire tracked
  in Vikunja #266.
- LaunchRegistrationRaceTest: join registered jobs before asserting launchCount
  — computeIfAbsent returns the Job immediately but the fire-and-forget launch
  body lagged awaitAll (the 49-vs-50 flake).

ACR concept-compiler experiment (pre-existing WIP on this branch):
- ExecutionPlanCompiler/Model/PlanLinter, #264 needs-seam (sessionArtifacts),
  LSP diagnostics subsystem (LspDiagnosticEvents/Runner/Lsp4j), BootWorkspace,
  config surface, workflow prompts/schemas, orchestrator advance-don't-rerun.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 01:20:37 +04:00
kami 0e1095e9ba feat(tools): clobber guard blocks file_write overwriting real code with elided stubs (#245)
A full file_write that shrinks a non-trivial existing file (>=30 non-blank
lines) by >60% into a stub with literal `...` placeholders is almost always
the model rewriting a file from memory instead of editing it — the incident
that silently broke SessionRoutes.kt (273->28 lines) in run f11afb08. Reject it
(recoverable) and steer to file_edit. Requires BOTH the hard shrink AND elision
markers, so dead-code refactors and new-file scaffolds pass untouched. Runs
before the write regardless of approval tier, so the auto-driver can't wave it
through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:05:53 +04:00
kami e5a5cddc96 feat(tools): salience-aware shell output compression — retain error lines through HeadTail (#67)
HeadTail now accepts an optional salience regex + cap: middle lines matching
it (error|fail|exception|panic|traceback|✗, case-insensitive) survive
truncation in place instead of being silently dropped, so a decisive error
buried in the middle of a long build/test log still reaches the model-facing
context entry. ShellTool's outputCompressor spec wires this in; the raw
build-gate receipt path is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 12:13:31 +04:00
kami 0a89fafd45 fix(tools): restore stdin pipe for MCP stdio transport
ChildProcess defaults stdin to /dev/null (scaffolders fail fast rather than
hang). MCP stdio is the inverse: the server reads requests from stdin and sees
EOF then exits immediately without a live pipe. spawn() now restores
Redirect.PIPE. Verified end-to-end: Correx spawns the real codebase-memory-mcp
v0.9.0 and mounts its 8 tools as mcp__codebase-memory__*.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 19:49:57 +04:00
kami 63e8b4f5d6 feat(tools): native MCP client — mount stdio MCP servers as Correx tools
Adds an MCP host layer so external MCP servers (AST/LSP code-intelligence,
package resolvers, etc.) can be mounted at startup and surface their tools/list
as first-class Correx tools named mcp__<server>__<tool>. Each MCP tool is a
Tool+ToolExecutor, so it rides the normal ToolExecutor path and inherits tier
gating, receipts, and event-recorded execution (#5) with zero special-casing;
replay reads the recorded receipt rather than re-calling the server (#8/#9).

- McpProtocol/McpStdioClient/McpTool/McpMounter in infrastructure:tools
  (stdio JSON-RPC 2.0, tools/* slice only). Capabilities empty; safety via
  default T2 tier since external side effects are opaque.
- [[mcp]] config (id/command/env/tier) + array-of-tables parsing.
- Main wires mounted servers into extraTools (main + per-workspace paths) with
  a shutdown hook; a server that fails to start is logged and skipped.
- Tests: in-process fake transport (handshake/list/call/error + tool mapping),
  [[mcp]] parser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 19:26:01 +04:00
kami ed7efb6072 Implement closed-loop workspace follow-ups 2026-07-15 23:48:12 +04:00
kami aee2e67c66 refactor(detekt): extract magic-number constants, wrap long lines, drop legit suppressions
Mechanical detekt cleanup across apps/core/infrastructure (behavior-preserving):
- MagicNumber 62->10: named constants + removed 'legit' MagicNumber suppressions
  (Tier level from ordinal, ReplayInferenceProvider CHARS_PER_TOKEN, ConfigLoader
  DEFAULT_* consts, capability scores, HTTP ranges, column widths, etc.)
- MaxLineLength cut to ~0 in production modules (line wraps, no logic change)
- Dropped one dead parameter (ConfigLoader.parseArray lineNum)

Structural findings (ReturnCount/LongMethod/Complexity/LargeClass) left for a
deliberate decomposition pass. Full build + detekt gate green.
2026-07-12 23:12:28 +04:00
kami 2912799fe0 feat(tools): bound+frame tool results, spill full output to CAS, tool_output retrieval
Tool results injected into model context are now consistently framed and
globally bounded. Success output over the floor (TOOL_RESULT_MAX_CHARS=8000)
is head/tail-truncated with a marker naming a retrieval ref; the full raw
output spills to the artifact store (CAS) and its hash is recorded on the
ToolReceipt (fullOutputHash) in the event log. Agents recover the full text
via the new read-only tool_output(ref=...) tool.

- SessionOrchestrator: frameTruncatedToolResult + renderToolResult; char-cap
  head/tail so a single pathological long line can't defeat the bound.
- ToolReceipt.fullOutputHash (additive, nullable).
- ToolOutputTool (Tier T1, no fs capability) resolves ref -> full bytes.
- Wired via extraTools in Main.kt; tool_output added to ALWAYS_AVAILABLE_READ_TOOLS.
- Failure path keeps ERROR:/FATAL: prefixes (all-rejected breaker dependency).

Tests: FrameTruncatedToolResultTest, ToolOutputToolTest.
2026-07-12 19:46:02 +04:00
kami bfd925b518 feat(tools): glob + grep read-only workspace search tools
Weak local models reached for shell find/grep -r (unjailed, dumps
.gitignore'd trees, floods context) to answer 'does frontend/ exist?' /
'where is symbol X?'. Add two jailed, gitignore-aware read-only tools:

- GlobTool (name 'glob', T1, FILE_READ+DIRECTORY_LIST): find files by
  glob pattern; survey-exempt from anti-hallucination read gates like
  list_dir.
- GrepTool (name 'grep', T1, FILE_READ): regex content search, returns
  path:line: text; skips gitignored, binary (NUL), and >2MB files.

Both reuse GitignoreMatcher + PathJail from the filesystem package,
share FileReadTool's jail/anchor/toggle, cap at 200 results. Registered
in ToolConfig.buildTools (fileRead block) and added to
StageConfig.ALWAYS_AVAILABLE_READ_TOOLS so every tool-granting stage can
call them. 6 tests green.

Vikunja #37.
2026-07-12 18:33:36 +04:00
kami b93729008d fix(infra): single-read file_read (no double disk I/O for hash) + bail health poll on dead process 2026-07-12 12:55:12 +04:00
kami 46fbd597c3 fix(tools): atomic file_edit writes + bound the patch subprocess 2026-07-12 12:48:30 +04:00
kami c2336ae6f7 fix(tools): don't follow redirects in web_fetch (SSRF) + drop ArrayList<Byte> boxing 2026-07-12 12:46:37 +04:00
kami ddf2c014f1 fix(shell): interruptible timeout + kill the whole process tree (#62)
withTimeout wrapped a blocking process.waitFor() that coroutine cancellation
can't interrupt, so a hung process hung the stage until it exited on its own;
and destroyForcibly() killed only the direct child, leaving sh -c grandchildren
(the real npm/tsc) as orphans. Use process.waitFor(timeout, MILLISECONDS) for
an on-the-clock deadline, and kill via toHandle().descendants() + the parent.
2026-07-12 12:42:04 +04:00
kami 44e15ea1b7 fix(shell): validate every command position against allowlist, not just argv[0] (#61)
An operator argv runs via `sh -c`, so ["ls","&&","rm","-rf","/"] passed an
argv[0]-only allowlist check with {ls} then executed rm. Now every command
position (argv[0] + the token after each &&/||/|/;/& separator) must be
allowlisted; shell builtins are implicit, redirect targets are treated as
filenames. Tests for the bypass, an all-allowed chain, and a redirect target.
2026-07-12 12:40:42 +04:00
kami 8ef957cd53 fix(sandbox): unique backup names to prevent same-basename collision (#64)
Backups were named workingDir/${fileName}.bak — two affected paths with the
same filename in different dirs overwrote each other's backup, so failure
restore wrote the wrong content. Name by full-path hash. Latent today
(single-path tools) but a correctness landmine for any multi-path tool.
2026-07-12 12:38:27 +04:00
kami d0ab027353 fix(list_dir): file-vs-dir mismatch gives an actionable hint (#45)
The 'Not a directory' branch returned a bare message; models re-issued the
identical list_dir until cancel (session a60c54b0). Name the remedy: the path
is a FILE — file_read it or delete/convert it first. Regression test added.
2026-07-12 12:34:37 +04:00
kami 15248cae8a feat(recovery,context): tier-2 intent-holder arbiter + remaining-delta pinning + surfaced signal events
- recovery: two-tier repair ladder — owner then arbiter (recovery stage recast
  as intent-holder, holds initial intent, reconciles cross-file contract disputes)
  with independent INTENT_ROUTE_BUDGET; RecoveryRoutingTest coverage
- context: remaining-delta checklist pinning (RemainingDelta{Pinning,Entry}Test)
- surface write-only events (session name, quality signals) into stats/browse
- parseToolArguments lenient-Json fallback for bare-key drift
- tools: ChildProcess seam
2026-07-11 23:56:52 +04:00
kami 3d5e05c1fb feat(context): reasoning-thread continuity, L3 doc filtering, and gate hardening
Threads reasoning_content across inference calls and journal renders, filters
markdown noise out of L3 repo-knowledge retrieval, adds PlanLinter H3 checks,
and tightens filesystem tool output/dir-listing behavior surfaced by prior
live QA (see project_readloop_campaign memory).
2026-07-10 11:13:43 +04:00
kami f51a8dada4 feat(recovery): route failed write-less stages to recovery instead of futile retry
Adds the failure-ticket + recovery-routing mechanism: a deterministic
gate->capability table gates whether a stage has agency to fix its own
failure, opens a FailureTicketOpenedEvent when it doesn't, and routes to
a metadata role=recovery stage (per-stage budget, cap 2, not reset by
TransitionExecuted) instead of retrying in place. Extends salvage
decisions with a RECOVER option so the review-gate judge can also route
to recovery, unifies deterministic-gate and review-gate routing through
routeToRecovery(), and has ExecutionPlanCompiler synthesize a
write-capable recovery stage + edge for freestyle plans. Read-only tools
(file_read, list_dir) are now always available on any tool-granting
stage so a recovery stage can inspect the write-less stage's failure
without flooding context via shell ls -R.
2026-07-08 10:28:53 +04:00
kami c849fd9921 feat(tools): cap file_read output so one read can't flood L2
On a real (large) codebase, a file_read of a big file dumped the whole
body into L2 uncapped, evicting the files the stage actually needed —
the ContextTruncated we saw on real-repo runs. Bound it:

- readFile: auto-truncate a read-to-EOF past MAX_READ_LINES (400) with a
  note pointing at start_line/end_line; hard char backstop (20k) for
  very long lines. An explicit end_line is deliberate paging, honoured
  in full. No contentHash baseline on a truncated view, so the
  stale-write gate forces a real read before an edit.
- listDir (single-level): cap at LIST_MAX_ENTRIES (400) with a note.
  (ListDirTool's recursive walk was already capped.)
2026-07-07 14:59:09 +04:00
kami a8c496e909 fix(toolintent): exempt list_dir from reference-exists + rejection-loop breaker
Greenfield discovery/analyst stages thrashed on plane-2 REFERENCE_EXISTS
rejections: the model listed a not-yet-created dir (e.g. frontend/), got
hard-BLOCKED every round, and burned MAX_TOOL_ROUNDS without progress.

- New ToolCapability.DIRECTORY_LIST; ListDirTool declares it alongside
  FILE_READ. ReferenceExistsRule now exempts directory enumeration (a
  listing of an absent dir truthfully reports empty; only hallucinated
  file *reads* still fail loud). Dispatch stays on capability, not name.
- Stage-agnostic rejection-loop breaker in the tool loop: after 3 rounds
  where every tool call is rejected (BLOCKED/ERROR) with no success,
  force the model to produce its output. Covers JSON-emitting stages the
  read-loop breaker (file_written-only) never protected.
2026-07-07 14:09:03 +04:00
kami 41ed6414c6 feat(guardrails): steering channel + shell-in-file rule + capability-gap detector
Bundles three operator-reliability guardrails (Vikunja #28/#29/#30) plus the
in-flight branch WIP they were built on top of (reasoning_content capture,
operator/project profile editor, write-jail workspaceRoot fix) — the tree is
interdependent (SessionOrchestrator references reasoningArtifactId from the WIP)
and does not compile as separable subsets, so it lands as one commit.

Guardrails:
- #28 mid-stage steering: ClientMessage.SteerSession -> GlobalStreamHandler ->
  orchestrator.submitSteering, reusing SteeringNoteAddedEvent + existing context
  fold (advisory, non-authoritative; invariants #3/#7). Closes the gap where
  steering typed off an approval gate was silently dropped.
- #29 shell-in-file guardrail: ShellInFileContentRule (core:toolintent) blocks a
  file_write whose content is a bare shell command (e.g. "mkdir -p ..."); FileWriteTool
  description now advertises auto-mkdir of parent dirs. Basename-allowlist so the
  extensionless case is caught; scripts/Makefiles/multiline exempt.
- #30 pt1 capability-gap detector: deterministic CapabilityGapDetector maps stage
  intent -> implied ToolCapability, compares to granted tools, emits advisory
  CapabilityGapDetectedEvent in FreestyleDriver.lockAndRun. Recorded, never fails
  the gate and never auto-grants (invariants #3/#4/#5). Reflection rung is pt2.

Verified: ./gradlew check green (whole tree).
2026-07-07 13:27:59 +04:00
kami 2698971082 fix(kernel): de-dup tool events, restore approval diff, break read-before-write deadlock
Three tool-path correctness fixes:

- Tool-execution events were emitted twice — SandboxedToolExecutor and
  SessionOrchestrator.recordToolExecution both emitted Completed/Failed/
  FileWritten per call (double TUI rows, double-counted metrics). Split
  ownership: orchestrator is the sole recorder of Completed/Failed (truncates
  output, drives the read-before-write gate); executor owns Started +
  FileWritten (pre/post-image hashes it alone can capture).

- computeToolPreview still gated on operation == "write", but file_write lost
  its operation param when delete was split out, so it bailed to raw-args for
  every write (approval card showed raw JSON). Drop the dead guard.

- A file_read blocked by REFERENCE_EXISTS can never complete, so it could never
  lift read-only mode: a stage writing a NEW file deadlocked (writes filtered,
  every read of the not-yet-created target blocked) until MAX_TOOL_ROUNDS.
  Lift read-only on a REFERENCE_EXISTS block, and reword the block message to
  tell the model to file_write directly. Regression test added.
2026-07-03 01:01:19 +04:00
kami c1de2281c8 fix(tools): file_write to a directory is recoverable, not fatal
Writing to an existing directory path threw an IOException marked recoverable=false, which the orchestrator turns into a FATAL non-retryable stage failure — dead-ending the workflow on a correctable model mistake. Guard the directory case up front with a clear message and make write IO errors recoverable so the retry-with-feedback loop can self-correct.
2026-07-02 12:23:29 +04:00
kami 18cbd34739 fix(kernel,tools,workflow): session-robustness QA sweep
Uncommitted work from the session-robustness-and-dox branch sweep
(docs/qa/QA-session-robustness-and-dox.md), verified alongside the
compression/context fixes:

- SandboxedToolExecutor: validate tool args centrally before dispatch. A
  malformed/missing-arg call becomes a recoverable ERROR: (surfaced with the
  tool's arg schema so the model can correct + retry) instead of stranding
  the stage with no artifact. + validation test.
- PlanLinter: seed artifacts (analysis) produced by the planning phase count
  as available producers, so a plan stage that `needs` them isn't flagged as
  an unproduced-need; H1 unproduced-needs + trap-state checks. + tests.
- DefaultSessionOrchestrator: live-QA robustness fixes (event-tail /
  per-stage budget + retry handling).
- workflow prompts/configs: DOX AGENTS.md alignment + freestyle/task/role
  prompt tweaks.
- SessionOrchestratorIntegrationTest: coverage for the above.
- FreestylePlanningWorkflowTest: allow list_dir in analyst tools (follows the
  list_dir wiring in 968cbfa).
- QA plan doc for the branch sweep.
2026-07-02 00:56:45 +04:00
kami 968cbfa973 fix(context,tools): context hygiene + tool ergonomics from freestyle QA
Found while live-QAing freestyle_planning on a 12B local model:

- list_dir tool: recursive, .gitignore-aware listing so weak models stop
  flooding context with `ls -R` over node_modules/build/dist. Wired into
  the fileRead toggle + advertised to the planner (architect_freestyle).
- ContextClassifier: assistantToolCall turns are STRUCTURED, so the token
  pruner never shreds the model's own tool-call history — that was causing
  amnesia loops (re-issuing calls it had already made).
- Retire instruction-doc LLMLingua pruning (DOC_SOURCE_TYPES emptied): it
  fused load-bearing procedural text into unparseable soup. The static
  block stays small by dropping CLAUDE.md at the loader instead.
- AgentInstructionsLoader: load only AGENTS.md, not CLAUDE.md — the latter
  targets the outer assistant and polluted the agent's stage context.
- DefaultSessionReducer: WorkflowFailed flips session status to FAILED (was
  stuck ACTIVE forever, so clients/approval loops never saw a terminal).
- ShellTool: run shell command lines (cd/&&/pipes) via `sh -c`; unrunnable
  program is recoverable instead of an uncaught IOException killing the
  stage; malformed argv (non-string/collapsed-array) rejected with guidance.
- llmlingua sidecar: cap force_tokens to max_force_token (big docs blew the
  assert and 500'd, so doc pruning silently failed open).

Tests added/updated across all of the above.
2026-07-02 00:44:04 +04:00
kami fe6e530a75 Merge remote-tracking branch 'origin/feat/session-robustness-and-dox' into feat/session-robustness-and-dox 2026-06-29 20:58:07 +04:00
kami 238d353653 feat(qa): remote NIM provider + headless-QA robustness
Enable autonomous QA through a remote OpenAI-compatible provider (NVIDIA NIM)
and harden the tool/approval path so unattended multi-stage runs complete.

- inference: add openai_compat provider (Bearer chat-completions for NIM/OpenAI),
  dispatched by provider type "nim"/"openai"; key via api_key/api_key_env.
- server: bind configured [server] host/port instead of a hardcoded 8080;
  POST /sessions accepts an optional `intent` (WS parity) for intent-driven workflows.
- kernel: thread the bound operator profile's approval_mode into per-tool gating so
  auto/yolo enable unattended approval (engine still consulted; policy/plane-2 BLOCK
  stays terminal); on a recoverable tool failure feed the tool's arg-schema back into
  context so the model self-corrects instead of repeating a malformed call.
- tools: split deletion out of file_write into a separate, explicitly-named file_delete
  tool — a model can no longer delete a file by getting a write-mode parameter wrong.
- server: add GET /metrics/tool-reliability — per-model tool-call validity from the
  event log (measurement groundwork for capability-aware routing).
- docs: update AGENTS.md across kernel, tools, server, inference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:50:16 +00:00
kami a00bd4aade feat(tools): propose_scope recalibration valve for the execution loop
When a write is blocked for being outside the claimed task's
affected_paths, the implementer can propose widening scope via a new
T2 propose_scope tool. The operator decides (invariant #4 — the agent
can't self-grant). Approve unions the proposed paths into the task's
affected_paths (existing TaskAffectedPathsSetEvent); activeScope
re-derives the wider manifest from events so the same write then passes.
Reject blocks the task (via TaskClaimCoordinator.blockActiveTask, hooked
on both approval-denial branches, scoped to propose_scope) so the loop
advances. No new event types.
2026-06-29 01:04:48 +04:00
kami d26f20c316 docs(dox): build out the DOX AGENTS.md hierarchy
Root Child DOX Index assembled, plus a per-module AGENTS.md across the tree
(core/*, infrastructure/*, apps/*, testing/*, and docs/examples/frontend/etc),
each following the DOX section shape: Purpose, Ownership, Local Contracts,
Work Guidance, Verification, Child DOX Index.
2026-06-28 20:43:27 +04:00
kami 21e01da3ac feat(tasks): task_decompose splits a goal into a dependency-linked graph in one approval
The freestyle analyst can now break a large goal with dependency seams or
independent review/handoff points into a parent epic + DEPENDS_ON-linked children
in a single T2 approval, instead of N separate task_create calls. Parent
DEPENDS_ON every child (completes last); each child IMPLEMENTS parent. Resolves
depends_on by ref or index; rejects cycles, unresolved refs, missing title/goal,
and empty batches; same batch dedup + force_reason convention as task_create.

A session works one active task, so multi-task work is multi-session by
construction: the analyst names the single ready task this run works, the architect
threads only that one, and siblings are claimed by later runs via task_ready
(claim-driven; no scheduler, /tasks/next stays rejected).

Doctrine: analyst_freestyle.md picks one-task-vs-decompose and names the ready
task; architect_freestyle.md threads only that one; plus the L0 policy line.
freestyle_planning.toml analyst gains task_decompose (pinned by
FreestylePlanningWorkflowTest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:39:03 +00:00
kami 67384691e0 feat(tasks): force overrides require a recorded reason
Bypassing a gate with force=true now requires a force_reason, recorded as a
"[force] ..." note on the task so the override is auditable (visible in history
and the context bundle) rather than a silent escape hatch. Applies to the agent
tools: task_create (dedup override) and task_update (claim over unmet blockers,
complete with no in-scope writes). force without a reason is rejected.

The REST POST /tasks create override (operator/CLI path) still takes a bare force
and is left for a separate call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 06:46:41 +00:00
kami 6a779861c2 feat(toolintent): stale-write gate
Block writing a file whose on-disk content changed since the session read it — a
concurrent/external edit the agent's view doesn't reflect. file_read records a
content hash on whole-file reads; the orchestrator's completion path now carries
the tool's structuredOutput (it previously dropped it, unlike SandboxedToolExecutor
— the two paths were inconsistent), so SessionContext.readHashes folds it.
StaleWriteRule compares the current hash (WorldProbe.contentHash) against the
read-time hash and BLOCKs a mismatch, telling the agent to re-read first. Files the
session wrote itself are excluded, so its own edits never look stale; partial reads
set no baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 06:15:15 +00:00
kami 6b5a758081 feat(tasks): cite-before-claim gate on completion
Completing a task that declared affected_paths but saw no matching write this
session is now blocked ("marked done without doing it"), escapable with force.
TaskUpdateTool reads the session's writes through a SessionWrites port whose
adapter folds SessionContext (writes = recorded receipt.affectedEntities), and
matches them against the task's affected_paths globs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:31:08 +00:00
kami 30321e08a5 feat(toolintent): record the session's active task as a session-local fact
Claiming a task now emits SessionWorkingTaskEvent{taskId, affectedPaths} into the
session's own stream (via a SessionFactRecorder port + event-store adapter), and
SessionContextProjection folds it into SessionContext.activeTask. So a gate learns
"which task is this session working, and its scope" by reading the session stream
— no cross-stream scan, no kernel->tasks dependency. Re-emitted on an affected_paths
edit by the claimant, so the snapshot scope stays current; latest wins on replay.

This is the data source the write-scope gate needs (next).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:35:40 +00:00
kami 78e7a4fefb feat(tasks): dependency hard gate on claim
Harden the claim warning into a block: task_update action=claim on a task with
unmet blockers is now rejected (no mutation) with the blocker list, instead of
claiming-with-a-warning. Escapable with force=true, which claims anyway and keeps
the "claimed despite unmet blockers" warning for the audit trail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:43:17 +00:00
kami 5d48c26ec8 feat(tasks): duplicate-title guard on task creation
The doctrine says "search before creating", but that leans on the LLM; this is
the backstop against an agent re-creating a task across runs. TaskService.
findDuplicates matches active (non-terminal) same-title tasks, case- and
whitespace-insensitively — a DONE/CANCELLED look-alike is not a duplicate, since
that work may legitimately recur.

- task_create rejects a duplicate, naming the look-alike and offering force=true.
- POST /tasks → 409 with the duplicates, or force:true to override;
  correx task create --force.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:34:24 +00:00
kami 215a951b52 feat(tasks): agent-loop integration — task_ready tool + claim guard
Bring the work graph into the agent loop:

- task_ready (T1): lists tasks ready to work now (TODO, all dependencies
  satisfied) so an agent looking for work can pick one and claim it. Surfaces
  work; never assigns (honouring the rejected /tasks/next). Registered in
  TaskTools — left unwired in the request-driven example workflows, where it
  would be noise; it belongs in an autonomous work-pull workflow.
- claim guard: task_update action=claim appends a WARNING naming any unmet
  blockers, so an agent knows when it has jumped a dependency. Surfaced, not
  hard-blocked — status is derived, so we don't fight the reducer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:34:24 +00:00
kami 06c225e3bd feat(tasks): teach agents when to use the task tools
The task tools were registered but had only mechanical descriptions and no
standing policy, so agents knew the tools existed but not when to reach for
them. Lead each of the five descriptions with its trigger (when to use, when
not to) and add a task-tracking convention to .correx/project.toml, which is
rendered into every stage's L0 context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:40:44 +00:00
kami 99f781687f feat(tasks): task search across REST, tool, and TUI
Ranked exact/substring search: TaskSearch (all terms AND, ranked title >
key > goal > criteria > notes) over one project or the whole board via
TaskService.search. Surfaced as GET /tasks?q=, a read-only task_search tool
for agents, and a `/` filter in the TUI board.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 22:08:11 +00:00
kami 70b4357316 feat(tasks): auto-link the originating agent session
When an agent creates or claims a task via the tool, record its session as
a CONTEXT/SESSION link so the context bundle's session resolver has real
edges to inline (status/intent/workflow). Shared linkOriginSession helper;
reducer dedupes so create+claim from one session is a single edge. The REST
create path stays unlinked (no agent session there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 20:15:54 +00:00
kami 6b00d89c2c feat(tasks): inline ARTIFACT/SESSION links in the context bundle
The context bundle resolved only TASK and DOC link targets; ARTIFACT and
SESSION stayed raw. Add two ports (TaskArtifactResolver/TaskSessionResolver)
with apps/server adapters: artifacts resolve to producing stage/session +
content excerpt from CAS, sessions to status/intent/workflow. Unresolved
targets still fall back to raw links. Wired through the tool and REST bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:59:18 +00:00