Commit Graph

175 Commits

Author SHA1 Message Date
claude d18075925d fix(toolintent): key the read-before-write exemption on content provenance, not parameter shape
The exemption added in 6a8a7b31 was broader than the invariant it stood on. "Tool
declares a SOURCE_PATH" is a claim about the parameter list; the safe property is
"every byte written derives from an existing source object rather than from
model-supplied content". A future transform or import tool could name a source and
still write model-controlled output, and would have inherited the exemption.

ToolCapability.CONTENT_FROM_SOURCE now carries that provenance claim explicitly.
file_copy declares it; ReadBeforeWriteRule.appliesTo stands down only for calls that
do, so ToolCallAssessor skips the rule rather than the rule skipping itself. The
capability is recorded on the invocation event like every other one, so replay
classifies a call by what it actually claimed instead of re-deriving it from
parameters.

Tool availability is by declared tool name, not capability-set containment, so the
extra capability does not narrow which stages can reach file_copy.

Tests: the exemption is asserted through ToolCallAssessor, plus a source-naming tool
WITHOUT the provenance capability that stays gated. ./gradlew check green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:04:04 +04:00
claude 6a8a7b31c1 fix(events,tools,toolintent): failure attribution, one path normalization rule, file_copy (#713)
Three generic harness fixes from the web-ui postmortem dataset. Nothing here keys
on a language, framework, build tool or task type.

1. Failure attribution. WorkflowFailedEvent carries one primary FailureAttribution
   (AGENT | HARNESS | WORKFLOW | ENVIRONMENT | PROVIDER | OPERATOR | UNKNOWN),
   defaulted to UNKNOWN so pre-field events replay unchanged. FailureAttributor is
   the deterministic reason->layer mapping, used both at emission and when
   classifying history, so the baseline and the live metric are one measurement.
   Emission sites set it: failWorkflow derives from the reason unless the caller
   knows the layer, cancellation is OPERATOR, the server catch-all falls back to
   HARNESS, a grounding-rejected plan is AGENT. Multi-cause chains stay on
   FailureTicketOpened — no second causal structure.

   GET /metrics/failure-attribution (FailureAttributionInspectionService, mirroring
   ToolReliabilityInspectionService) reports counts, share, UNKNOWN share, the
   preserved reasons and the ticket categories from the same sessions. Read-only:
   historical events are classified at READ time and reported as `inferred`, never
   written back over an append-only log.

   Baseline over the local log, 122 terminal failures: AGENT 51 (41.8%),
   OPERATOR 28 (23.0%), WORKFLOW 19 (15.6%), PROVIDER 15 (12.3%), HARNESS 6 (4.9%),
   ENVIRONMENT 3 (2.5%), UNKNOWN 0.

2. The `~` guard bug. ToolPath is now the ONE canonical normalization rule
   (expand a leading `~`/`~/`, keep absolutes, anchor relatives on the session
   working dir). Every filesystem tool, all six plane-2 path rules and the approval
   preview resolve through it, so policy and existence checks inspect the path the
   tool will operate on. `~/.gradle/init.d/offline.gradle` used to resolve to
   `<workspace>/~/.gradle/...`: reported non-existent AND in-workspace, so the
   reference gate called a real file a hallucination and the out-of-workspace prompt
   never fired. Containment and external-read approval behaviour are unchanged —
   the expanded path is simply outside the workspace, where it always belonged.

3. file_copy (#713). A first-class tool with the writer's jail, tier, receipt,
   replay and CAS pre/post images; static and binary assets no longer move through
   the model's token stream. Needed one generic split: ParamRole.SOURCE_PATH marks a
   path a call reads FROM, so containment gates judge both params while write-target
   gates (read-before-write, stale-write, write scope, write manifest) judge the
   mutated one. ReadBeforeWriteRule exempts any call declaring a SOURCE_PATH: its
   content comes from disk, not from memory, and requiring a read of a binary is
   unsatisfiable. Existing tools declare no SOURCE_PATH, so their behaviour is
   byte-identical.

Tests: ToolPathTest (9), FailureAttributionTest (10), PathNormalizationRuleTest (6),
FileCopyToolTest (10), plus a home-relative FileReadTool read. ./gradlew check green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 11:57:49 +04:00
claude d892587420 fix(kernel,workflow): five fixes from the 954da1a9 post-mortem (#705,#706,#709,#710,#712)
The run died on a build gate running the wrong toolchain and burned 43% of its
tool calls on repeats, rejections and failures. Five fixes, each traceable to a
measured cost in docs/audits/2026-08-11-session-954da1a9-postmortem.md.

#705 build gate toolchain scope. The gate is armed session-scoped but read the
toolchain stage-scoped, so a reviewer stage that wrote nothing fell back to the
flat `build` alias and ran ./gradlew assemble on an all-frontend session. It now
falls back to the session's own manifest first. 32.5 min, 33% of that run.

#706 action ledger. L2 keeps ten conversation entries, so a stage past round
five has no memory of what it tried; 29% of tool calls were byte-identical
repeats. One pinned line per call (tool, target, outcome) with repeats collapsed
to a count, ~3k tokens for a whole run.

#709 plan-compile lint for blocked runners. Plans prescribed `npx tailwindcss
init -p`, rejected by the shell denylist at every attempt. Rejected at compile
time now, where the architect can still rewrite the step.

#710 near-greedy sampling on tool-call rounds. temperature 1.0 on argv emitted
`./gradlew_`, `npm_prefix=frontend`, `create_vite@latest`. Prose rounds keep the
operator's sampling.

#712 auto-approve manifest-contained writes. 94 approvals, all APPROVED, no
steering, 19 min. A write inside the declared manifest already proved its
containment by getting past ManifestContainmentRule. DENY mode still denies.

Tests: core:kernel 133, infrastructure:workflow 99, testing:integration 176,
testing:deterministic 79, all green. detekt clean (no new findings).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dVqqci5H5b3s6xzv6Lojq
2026-08-11 22:15:49 +04:00
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 867e99d1cb fix(build-gate): a mid-plan declared build never suppresses the terminal floor (#263)
autoGateStages returned an empty set as soon as any stage declared
PROJECT/TESTS, so a plan that builds at stage 3 of 9 had nothing
verifying the six stages written after it. Keep the suppression for the
redundant per-writing-stage gates, always keep the terminal stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:27:09 +04:00
kami f61864ff1d fix(lsp-gate): lint-tagged diagnostics never fail a stage (#311)
tsserver tags TS6133 (unused import) Unnecessary, but a tsconfig with
noUnusedLocals promotes it to error severity — which hard-failed whole
runs no rewrite could clear. Carry LSP DiagnosticTag through the
LspDiagnostic event and gate on untagged errors only; lint diagnostics
stay recorded and visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:16:33 +04:00
kami a95475be2a fix: anchor LSP diagnostics to server readiness 2026-07-26 10:25:32 +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 5df35879eb feat(acr): ACR Store 1 — content-hash observation cache for repo-file descriptors (#305)
Adds ObservationStore (core:context) keyed on (repoRoot, path), an in-memory default and
a durable SqliteObservationStore (infrastructure:persistence), and wires it into the two
SessionOrchestratorArtifacts call sites that re-derive a SourceDescriptor from CAS bytes
on every call — a hit on matching content hash skips both the CAS read and the regex
extraction. First slice of docs/plans/2026-07-21-acr-knowledge-accretion.md (build order:
observations before fixes/plan-shapes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMbPmZZjcXhR2crU82zZ8S
2026-07-21 17:37:25 +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 ae0b23df3f chore: sprint handoffs + Lsp4j runner live-proof
Add per-agent sprint handoff docs (Sonnet/Codex/opencode) mapping the
1-week sprint's two goals to concrete Vikunja tasks. Kept in repo root
(docs/ is gitignored). Includes hanging Lsp4jDiagnosticsRunner change +
its live-proof test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rdo9fe7SujNVeyZA8YkpkD
2026-07-21 00:36:25 +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 04336308f5 feat(inference): clamp max_tokens to context-window runway (#291)
The stage completion cap (e.g. 24_576) is a ceiling, not a promise. When the
rendered prompt + tool schemas fill most of the model window, sending the raw
cap makes llama.cpp truncate the prompt from the left — the traced Gemma4
32_767-token blow-up. Compute effectiveMaxTokens = min(cap, contextSize -
promptTokens - toolSchemas - templateOverhead - reserve), counting prompt/tool
tokens with the model's own tokenizer (char/4 fallback). Live-path only;
deterministic replay never calls infer, so no event recording needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 18:11:43 +04:00
kami 159b3f1eb9 fix(build-gate): close two frontend COMPLETE-lie holes (#277)
Run 771c0b96 marked COMPLETE with a frontend that did not build. Two
verification holes let a broken import through:

1. A MODULE build_expectation delegates to LSP and the execution gate
   returned Success trusting it — but tsserver failed to initialize every
   stage, so empty diagnostics read as clean. runExecutionGate now only
   trusts the MODULE->LSP short-circuit when the LSP run actually ran
   (new lspDiagnosticsSkipped projection); on skip it falls through to the
   real build command.

2. ExecutionPlanCompiler disabled the terminal whole-project auto build gate
   whenever ANY stage declared a build_expectation — so a MODULE (typecheck-
   only) declaration removed the real `npm run build` floor. Now only a real
   whole-project build (PROJECT/TESTS) suppresses the auto gate; MODULE/NONE
   do not. Extracted autoGateStages() helper. Two new compiler tests.

core:kernel + infrastructure:workflow tests green; no new detekt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:13:22 +04:00
kami 9ac32c9e93 fix(plan-grounding): credit a file-writing stage's scope as will-exist
Session d038468a: architect's plans compiled clean (3x) but plan grounding
then rejected them. The scaffold_frontend_project stage runs `npm create vite`
(allowed_tools [file_write, shell], touches [frontend/]) to create frontend/ +
package.json at run time. PlanGrounder only credited declared writes/
expectedFiles, so the scaffolder's generated files were invisible — it
false-rejected all three: frontend/ "doesn't exist", verify stage has "no
manifest". That rejects exactly the plan the architect prompt asks for ("use
the real scaffolder, don't hand-write package.json").

Credit a file_write-capable stage's declared `touches` scope as populated by
run time: it satisfies the build-manifest prerequisite and any scope (its own
or a later stage's) that overlaps it. Keyed on file_write (create-intent), NOT
shell, so a read-only shell stage — log inspection, test runs, grep — creates
nothing and does not wrongly credit its scope. Runtime precondition handling
(#167/#170) remains the backstop for a build whose prerequisite genuinely
never appears.

Tests: scaffolder case (mirrors the session) grounds; read-only shell stage
does NOT; existing "missing prerequisite" reject still holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ly2mMnt9TCZbvhcC1JfuV
2026-07-19 23:28:48 +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 7b90944b61 feat(inference): pass [[models]] params through to llama-server (enables draft-MTP) (#243)
ModelConfig.params was loaded from config but never consumed — the spawn
command in DefaultModelManager was hardcoded. Thread it through: params (a
flag->value map) flattens to token order on ModelDescriptor.extraArgs and
appends to the llama-server argv. This engages Multi-Token Prediction
speculative decoding purely from config:

  [[models]]
  params = { "-md" = "/path/draft.gguf", "-ngld" = "99", "--spec-type" = "draft-mtp", "--spec-draft-n-max" = "4" }

Each entry stays a distinct argv token (no shell), so paths with spaces are safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:28:26 +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 1dca8c7f25 test(workflow): update shipped-workflow tool-grant expectations for codebase-memory MCP tools
Follow-up to 3e31ebcc, which granted the MCP read-only query tools to
review_loop and role_pipeline stages but left the exact-set assertions stale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 12:15:33 +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 53d8108189 feat(orchestration): plan grounding, positive-pattern mining, stage-prompt audition rig
Vikunja #167/#168/#169.

#167 PlanGrounder (infrastructure/workflow): build-prereq + touches-scope
existence grounding, emits PlanGroundingEvaluatedEvent. Deterministic fold
over recorded workspace/plan events (invariants #8/#9).

#168 positive-pattern mining (SessionOrchestratorPlanPatterns): mine
SuccessfulPlanShape from cross-session log — a locked plan whose own
workflow later completed — and inject the closest-resembling plan shape as
L0/SYSTEM advisory context for the planning stage. Keyword-Jaccard
resemblance, no LLM/embedder, replays identically. Advisory only (#3).

#169 stage-prompt audit + CORREX_STAGE_GUIDANCE ON/OFF toggle in
SessionOrchestratorExecution. RunCommand gains --intent to seed a
freestyle session over REST.

Also: workspace verification events + concept-compiler wiring. ./gradlew
check green.
2026-07-16 14:55:56 +04:00
kami ed7efb6072 Implement closed-loop workspace follow-ups 2026-07-15 23:48:12 +04:00
kami d69cb12ce9 refactor: decomposition WIP (orchestrator/server/execution-plan)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhFXmKe4WisSSPf9LrmmTg
2026-07-15 13:17:01 +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 d89d4e32a9 feat(inference): operator-tunable sampling knobs (top_k/min_p/repeat_penalty) for stage requests
GenerationConfig only carried temperature/top_p/max_tokens/stop/seed. Added nullable
topK/minP/repeatPenalty, serialized to the llama.cpp and OpenAI-compat request bodies
via @EncodeDefault(NEVER) so an unset knob is omitted (the model keeps its own default)
and behavior is unchanged unless the operator opts in.

Surfaced as a new [sampling] config section feeding the default stage GenerationConfig
(the main agentic loop) through TomlWorkflowLoader + ExecutionPlanCompiler; the former
hardcoded temperature=0.7/topP=1.0 stage defaults now come from config. Talkie
chat/narration keep their own generation settings.

Vikunja #46 (task 76) — sampling half.
2026-07-12 17:54:25 +04:00
kami 8d7c827ebb fix(server,persistence): stop slow WS client stalling kernel; log dropped emits (#53)
Two event-delivery back-pressure faults from the core audit (#7).

(a) streamGlobal forwarded globalFlow → a per-connection Channel with
buffer.send (SUSPEND). globalFlow is also SUSPEND-overflow, so one wedged
WebSocket client back-pressured globalFlow → append() suspended → the whole
kernel stalled. Now the collector uses trySend; on overflow it closes the
buffer with SlowClientException, tearing down that one connection (client
reconnects and re-syncs via replaySnapshot) instead of blocking append().

(b) Per-session subscriptions (extraBufferCapacity 64) used tryEmit with the
return ignored: a lagging in-process collector (e.g. LiveArtifactRepository)
silently diverged from the durable log. Both SqliteEventStore and
InMemoryEventStore now warnIfDropped() — the event is still persisted; only the
live SharedFlow drops, and we surface it. ponytail: log-only; bounded
rebuild-on-lag if divergence ever bites.

Added slf4j-api to :infrastructure:persistence (already the standard logging
dep across modules) for the warn.

Green: :infrastructure:persistence + :apps:server compile+detekt+test (the one
pre-existing ModelLifecycleWiringTest failure needs a real llama-server, fails
identically on clean checkout).
2026-07-12 17:11:41 +04:00
kami 65d5e9de83 fix(persistence): assign event sequences atomically inside INSERT (#56)
nextGlobalSequence()/nextSessionSequence() computed MAX+1 with two extra
SELECT round-trips, guarded only by an in-process mutex — two processes on the
same correx.db (CLI + server) could read the same MAX and collide on the unique
index. Compute both the global sequence and session_sequence as MAX+1 subqueries
inside the INSERT and RETURNING the assigned values. SQLite serializes writers,
so the subqueries are atomic across processes; the unique index is the backstop.
Also removes the two pre-SELECTs per append.
2026-07-12 13:19:50 +04:00
kami 759828c0f5 fix(persistence): serialize SqliteEventStore reads with append transaction (#52)
Reads (read/readFrom/lastSequence/allEvents/allSessionIds/lastGlobalSequence)
shared the single JDBC Connection with append's transaction unsynchronized.
The coroutine appendMutex only excludes append-vs-append; a read on the caller
thread could hit the connection mid-transaction (setAutoCommit(false)..commit)
and corrupt tx state — SQLite JDBC is not thread-safe. Guard every connection
access with a shared JVM monitor (synchronized(connection)) via withConnection,
so reads and the IO-thread append transaction are mutually exclusive.
2026-07-12 13:17:38 +04:00
kami 8c45650e21 fix(artifacts): rebuild LiveArtifactRepository from event log on cold access 2026-07-12 13:05:52 +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 363d880a69 fix(l3): persistent sidecar reader, kill-on-desync, wire persistPath save/load 2026-07-12 12:52:07 +04:00
kami 3ff9c8281a fix(inference): single ContentNegotiation config + firstOrNull on empty choices 2026-07-12 12:49:46 +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