8000 DAGOS 0→1: core runtime, context management, tools, and semantic review by Mnehmos · Pull Request #22 · Mnehmos/dagos · GitHub
[go: up one dir, main page]

Skip to content

DAGOS 0→1: core runtime, context management, tools, and semantic review - #22

Merged
Mnehmos merged 54 commits into
mainfrom
impl/0-to-1
Sep 24, 2026
Merged

Mnehmos merged 54 commits into
mainfrom
impl/0-to-1

Conversation

@Mnehmos
@Mnehmos Mnehmos commented Sep 22, 2026 •
Copy link
Copy Markdown
Owner

DAGOS 0→1: the core runtime from the Spec Kit backlog (#2–#20), converged with the implementation (#21), plus the scope the project owner added along the way. Every expansion beyond the original brief is recorded as a constitution amendment in .specify/memory/constitution.md.

What is in it

  • Core pipeline (dagos-core): durable DAG in SQLite → Jev classifies active context → versioned IR → provider → fail-closed validation → DAG emissions, with every transition an ordered event. Layer boundaries are enforced by tests/boundaries.rs.
  • Providers (dagos-openai): OpenAI-compatible endpoints (OpenRouter, OpenAI, Ollama, …), native tool calling with a remembered fallback to the JSON protocol, TypeSafe's Jev through the Decisions API, and an offline Jev fallback.
  • Chats and projects: conversations inside projects. The project DAG is shared memory across chats.
  • Context management, not caps:
    • Jev picks nodes, recalls every relevant earlier turn from any chat (whole), and leaves out stale tool results.
    • Each step is fitted to the model's window by relevance.
    • "Load all context" is recognised as an intent.
  • MCP tools (dagos-mcp):
    • off / ask / allow per tool, approvals in the chat, a step limit, and a Stop button;
    • meta-tools get the strictest policy of the tools they name;
    • a Jev risk guard only ever escalates to asking the person.
  • Semantic lint (dagos-lint):
    • plain-English rules judged per function by Jev;
    • a bounded review loop that hands findings back to the model;
    • the Lint view, dagos lint for CI, and open findings remembered as nodes.
  • App (dagos serve, loopback only): chat, inspector, DAG panel, Lint view, and settings for providers, keys, Jev, tools, the review loop and the guard.

How to review

  • specs/001-core-runtime/checklists/traceability.md maps every requirement (FR-001…FR-027) to its implementation and tests.
  • research.md records the design decisions and the unresolved tradeoffs.
  • A high-effort code review of this branch found 10 issues (tool gate, network exposure, streaming, races, recall cost); all are fixed in 655c1d0.

Verification

  • cargo test --workspace (258 tests) and the UI tests (29) pass locally. CI passes on Linux and Windows.
  • Live checks against the real stack (gpt-6-luna, TypeSafe Jev, OODA with 93 tools):
    • native tool calls;
    • runs that wait for tools at startup;
    • the guard stopping a destructive command while leaving a read-only one alone;
    • recall across chats;
    • in-run compaction;
    • the review loop both fixing and disputing findings;
    • "load all context";
    • Stop;
    • the Lint view API.

Closes #21
Closes #1

🤖 Generated with Claude Code

@coderabbitai
coderabbitai Bot commented Sep 22, 2026 •
Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4ed0fdae-aa1f-47ac-b851-d984feeb5a0d


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Mnehmos and others added 5 commits September 22, 2026 12:40
- Cargo workspace (edition 2024): `dagos-core` library and `dagos` CLI.
- dagos-core declares the architecture layers (domain, contracts, store,
  context, ir, provider, response, runtime) with documented dependency rules.
- tests/boundaries.rs enforces the rules: providers cannot reference the
  store, the domain depends on nothing, and dagos-core cannot depend on HTTP
  stacks or provider SDKs.
- CI runs fmt, clippy (-D warnings), and tests on Ubuntu and Windows.

Closes #2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Migration 0001 creates projects, dag_nodes, dag_edges, runs, events, and
  active_context as STRICT tables.
- Constraints protect the core invariants at the database level: typed
  node/edge enums, JSON-object payloads, same-project edges (composite FKs),
  no self-loops or duplicate edges, run status/completion consistency, one
  running run per project, unique positive event sequences, and
  active-context rows that only reference nodes.
- Triggers make DAG nodes/edges durable (no delete) and events append-only.
- Migrations are tracked in PRAGMA user_version and applied one transaction
  per step; failed migrations roll back and newer databases are rejected.
- Store opens file or in-memory databases with foreign keys enabled.
- data-model.md records the project scoping and schema-enforced rules.

Closes #3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Prefixed, validated ID newtypes (proj_/node_/edge_/run_/evt_) with an
  injectable IdGenerator: UUIDv7 in production, per-prefix sequential IDs
  for deterministic tests.
- Timestamp: UTC millisecond instants in one fixed-width RFC 3339 form that
  sorts chronologically; strict parsing rejects non-canonical and impossible
  dates. Injectable Clock (system / stepping).
- Closed enums with fixed wire names for node types, edge types, run status,
  error codes, classifications, and context sources; unknown values are
  rejected by FromStr and serde alike.
- DagNode, DagEdge, Run, RunConfig, ContextMember, Project, and typed Event
  data split into stored (type, payload) pairs.
- Deterministic JSON: declaration-ordered fields and sorted payload keys
  (serde_json preserve_order is not enabled anywhere in the tree).
- contracts module embeds the Spec Kit JSON Schemas verbatim, compiles them
  once, and reports every violation with its JSON Pointer.
- dag.schema.json finalized to match the data model: node ID and timestamp
  patterns, required timestamps.
- tests/contracts.rs keeps the domain types and published schemas in
  agreement.

Closes #4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Store::transaction runs work in one IMMEDIATE SQLite transaction that
  commits on Ok and rolls back on Err; repository methods live on Tx.
- The store owns the injected Clock and IdGenerator, so every record gets
  its ID and timestamps from one place (deterministic in tests).
- Projects: create, get, list.
- Nodes: insert, get, list in creation order, payload update (advances
  updated_at only). Nodes are never deleted.
- Edges: insert with typed DagViolation errors for unknown/cross-project
  endpoints, self-loops, duplicates, and cycles (recursive reachability
  check keeps the graph acyclic); get, list in creation order.
- Column conversions re-validate domain values on read.
- Integration tests cover round trips, project scoping, validation,
  transactional rollback, and persistence across restart.
- rustfmt uses max small heuristics for compact, readable code.

Closes #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@repowise-bot
repowise-bot Bot commented Sep 22, 2026 •
Copy link
Copy Markdown

🔍 1 thing to check

  1. This PR introduces 208 new findings (2 high) across 48 files: 143× error handling, 23× dry violation, 14× duplicated assertion block (+9 more kinds). The worst are marked on the diff (see all)

✅ Health of changed files: 9.3


📊 See the full report for this PR
Your repo map with this PR's blast radius lit up, every caller of the contracts it changes, and health before and after. No sign-in. · ⭐ Star Repowise · 📥 Install bot · Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings/bot · Updated 2026-09-22 21:09 UTC (since the last push: 8 new findings)

Mnehmos and others added 23 commits September 22, 2026 12:59
- Runs: create (records run.started as event 1 with provider, model, and
  system prompt), get, list, previous run, running run(s), complete and
  fail. Terminal events are written in the same transaction as the status
  change; only running runs change state; one running run per project
  (explicit RunInProgress error).
- Events: append with store-assigned sequences (1, 2, 3, ... per run),
  only while the run is running; read all or after a sequence cursor.
  Stored payloads are re-validated into typed EventData on read.
- Active context: replace_context writes a running run's whole membership
  atomically, ordered by node creation (created_at, id) with its source
  (carried/jev); members must be unique nodes of the run's project. Leaving
  a node out removes only its membership row, never the node. A finished
  run's context is frozen history.
- Integration tests cover identity retention, per-run monotonic
  sequences, lifecycle freezing, context ordering/isolation/removal, and
  restart persistence including crashed-run discovery.

Closes #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- kiss.jev-context.v1 finalized: output is a list of {node_id,
  classification} with a closed label set (active | inactive) and no
  additional properties anywhere, so plans, actions, tool calls, provider
  routing, reasons, and prose have nowhere to go.
- New kiss.jev-request.v1: the run's message, every candidate node (oldest
  first, with whether it is in the carried-over context), and the edges
  between candidates.
- Domain mirror: JevRequest, JevCandidate, JevEdge, ContextClassification,
  NodeClassification, and zero-sized schema version tags that refuse any
  other document kind or version.
- Contract::parse fails closed for untrusted text: JSON, then schema, then
  the typed mirror.
- tests/jev_contract.rs rejects agent-shaped output (plan, actions,
  tool_calls, provider, route, reasoning, prose, extra per-entry data,
  unknown labels, wrong versions, non-JSON) at both the schema and the
  typed mirror.

Closes #7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- JevClassifier: the runtime depends on this async adapter, not a model
  API. Adapters receive a kiss.jev-request.v1 request and return raw
  kiss.jev-context.v1 text; DAGOS validates it, so adapters cannot bypass
  the contract. JevError distinguishes "no output" from invalid output.
- FakeJev: offline and deterministic. Default policy: superseded nodes are
  inactive, conversation turns beyond the most recent window (default 6)
  are inactive, everything else is active. Scripted and unavailable modes
  exercise fail-closed handling.
- Tests: contract-conforming output, the policy rules, and byte-identical
  replay of a request recorded as JSON.

Closes #8

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- carry_context seeds a new run's context with the previous run's members
  (source carried) and records context.carried (from_run_id null for a
  project's first run), so context persists across turns until Jev
  changes it.
- classification_request builds kiss.jev-request.v1 from store state:
  every project node except the run's own message (oldest first, flagged
  with current membership) and the edges between candidates.
- validate_classification fails closed: contract violations, nodes that
  were not candidates (including the run's task node), and duplicate
  classifications are rejected before anything is applied. It is pure, so
  the runtime can commit a jev.rejected record for rejected output.
- apply_classification: active -> member (source jev), inactive -> not a
  member, unclassified -> unchanged. Records jev.classified then one
  context.added/context.removed per change. Only membership changes; the
  durable DAG is untouched.
- New events: context.carried, jev.requested, jev.classified,
  jev.rejected, context.added, context.removed.
- Tests cover carrying, request shape, membership-only updates with event
  history, fail-closed validation, and replay of a recorded
  classification with the fake Jev.

Closes #9

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- kiss.inference-ir.v1 finalized as the only provider-facing contract:
  system_prompt, task {node_id, message}, context items {node_id, type,
  payload, relations} in stable order, recent_events (how the project's
  latest earlier runs ended, with failure code and message), and optional
  descriptive tools {name, description, input_schema}. No storage fields
  (timestamps, project scope, payload_json) exist anywhere in it.
- Domain mirror: InferenceIr, IrTask, IrContextItem, IrRelation, IrEvent,
  IrEventType, IrTool, and the kiss.inference-ir.v1 schema tag. IR types are
  distinct from DAG records, so a provider accepting InferenceIr cannot be
  handed a raw DAG record.
- tests/ir_contract.rs: valid IR validates and round-trips; serialization
  is byte-deterministic; storage fields, a raw DagNode as a context item,
  and fields outside the contract are all rejected.

Closes #10

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ir::compile reads a run's state and returns fresh InferenceIr: the
  run's system prompt, the task from its user conversation node, the
  active context in stored (node creation) order, each item's outgoing
  relations to other IR nodes, how the project's 3 most recent earlier
  runs ended (failures carry code and message), and caller-supplied
  capability descriptions. The result is validated against
  kiss.inference-ir.v1 before it is returned.
- Compilation only reads; every value is projected into IR types, so no
  storage record reaches a provider.
- ConversationTurn fixes the {role, text} payload of conversation nodes
  shared by the runtime and the compiler.
- Snapshot tests (tests/snapshots, UPDATE_SNAPSHOTS=1 to rewrite) cover a
  first run and a project with superseded/dependent decisions, a
  completed run, a failed run, and tools. Further tests: identical state
  (including an independently rebuilt store) compiles byte-identically,
  explicit system prompt, stable creation-order context, no storage keys,
  and rejection of a task that is not a user turn.
- Store transactions now panic with an explanation when nested on one
  thread instead of deadlocking on the store mutex.

Closes #11

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- InferenceProvider: the interface every inference backend implements.
  The entire boundary is InferenceRequest { model_id, ir: &InferenceIr },
  so an adapter can only ever receive compiled, versioned IR.
- Providers stream presentation-prose deltas through a DeltaSink and
  return their raw final output; DAGOS validates it before use, so
  adapters cannot bypass the response contract.
- Provider identity (ProviderId) and the model (ModelId, opaque to the
  core, interpreted only by the adapter) are explicit.
- ProviderError separates "no output" (unknown model, request failure)
  from invalid output, which validation catches later.
- tests/provider_boundary.rs: a spy adapter behind Arc<dyn
  InferenceProvider> receives exactly the compiled IR (validated against
  kiss.inference-ir.v1) and the model ID, nothing else.

Closes #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- kiss.inference-response.v1 finalized with its Rust mirror:
  presentation.prose (for people, never canonical), emissions (node
  emissions with a response-local ref that can never look like a node ID;
  edge emissions whose endpoints are refs or node IDs), and optional
  tool_calls and metadata that are recorded but never executed or
  interpreted.
- FakeProvider: offline and deterministic; output is a function of the IR
  and model ID alone. Prose streams word by word (optional per-delta delay
  for people watching). The model ID selects behaviour:
  fake-echo (valid: prose + an observation observed_from the task),
  fake-malformed, fake-invalid-schema, fake-dangling-edge, fake-cycle,
  fake-timeout (never finishes), fake-error.
- Tests: deltas reassemble the final prose, output and streaming order are
  deterministic (also under paced, paused-clock streaming), every failure
  mode is reproducible, identity and models are explicit, and DAG state
  outside the IR cannot influence the provider.

Closes #13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- validate_response parses raw provider output fail-closed: it must be
  JSON, satisfy kiss.inference-response.v1, declare each emission ref once,
  and connect edges only to refs declared in the response or to nodes that
  were present in the IR (a model cannot reference nodes it was never
  shown).
- ValidatedResponse can only be constructed by validation; it is the form
  in which emissions will be applied, so only validated final emissions
  can mutate the DAG. Live-state DAG rules (duplicates, cycles) are left to
  atomic application.
- Tests: separation of prose and emissions with tool_calls/metadata
  preserved, malformed JSON, schema violations rejected by contract and
  typed mirror alike, duplicate refs, unknown/unseen endpoints, and
  streamed prose never making invalid output usable.
- research.md records that assistant prose never becomes DAG state: it
  persists in the event history, and models persist what they need through
  emissions.

Closes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Runtime::run executes the whole pipeline: record the run and the user
  message (a conversation node, the IR task), carry context, ask Jev and
  apply valid classifications, compile and record IR, call the selected
  provider under a deadline while persisting each streamed delta, record
  the raw output, validate it, then atomically record response.validated,
  create the emitted nodes and edges, and complete the run.
- Pipeline failures are run state, not Rust errors: jev_failed,
  jev_invalid_output (with the rejected output kept in jev.rejected),
  ir_invalid, provider_failed, provider_timeout, response_invalid and
  emission_rejected (with response.rejected evidence) all finish the run as
  failed with everything recorded before the failure intact. A DAG
  violation while applying emissions rolls back every emission.
- Runtime errors are reserved for runs that cannot start (unknown provider,
  run in progress, store failure); recover_interrupted_runs fails runs a
  crashed process left running.
- New events: message.recorded, ir.compiled, inference.started,
  inference.delta, inference.completed, response.validated,
  response.rejected, dag.node_created, dag.edge_created.
- IR recent_events now carries a completed run's presented prose, from the
  event history (never DAG state), so conversations keep their thread.
- tests/runtime_fake_run.rs: complete offline run with the exact event
  timeline, contiguous sequences, durable message/observation/edge, IR
  recorded equal to recompiled IR, streamed prose equal to validated prose;
  context and reply carried into the next run; every provider/response
  failure mode explicit with a clean DAG; Jev failures stop before
  inference; unstartable runs create nothing; crash recovery; Send futures.

Closes #15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New crate dagos-openai: one adapter for any streaming Chat Completions
  endpoint (OpenRouter, OpenAI, Z.ai, Ollama, LM Studio, vLLM).
  Authentication, HTTP, SSE streaming, and request formatting stay inside
  it; dagos-core is unchanged and still has no HTTP dependency.
- Request: the system message is the IR's editable system prompt followed
  by the fixed DAGOS protocol instructions and the response schema; the
  user message is the compiled IR itself as compact JSON. Optional
  response_format json_object. API keys are config-only and redacted from
  Debug output.
- Streaming: an SSE decoder safe across chunk and UTF-8 boundaries, and an
  incremental JSON scanner that streams only presentation.prose as deltas
  (escapes and surrogate pairs decoded, "prose" keys elsewhere ignored).
  The raw document is returned verbatim for DAGOS to validate.
- Errors (HTTP status, error events in the stream, unreachable endpoints)
  map to ProviderError::Failed and so to run.failed with provider_failed;
  non-JSON model output becomes response_invalid.
- Tests against a local mock endpoint: request shape (path, bearer auth,
  model, stream, json mode, IR as the user message), prose streaming,
  error mapping, and full DAGOS runs through the adapter. An #[ignore]
  live test runs against a real endpoint when DAGOS_LIVE_* is set.

Closes #16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The earlier escape test's fixture held literal UTF-8 instead of JSON
\uXXXX sequences, so unicode-escape and surrogate-pair decoding were not
actually covered. The new test builds the escapes from parts, asserts the
fixture really contains them, and checks decoding across every small
chunk size, plus a lone high surrogate. The old test is renamed to what
it covers: simple escapes and literal UTF-8.

Refs #16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Per-project run defaults (provider, model, editable system prompt) are
  persisted in SQLite (migration 0002, the first real upgrade step) and
  drive subsequent runs; each run still records the configuration it ran
  with, so edits never rewrite history. Runtime::set_defaults rejects
  unregistered providers; model IDs stay opaque to the core.
- Store::with_event_listener observes committed events only, in order,
  after the store is released; rolled-back events are never seen.
- The dagos CLI (transport) gains init, config, run, and recover. `run`
  streams presentation prose to stdout via the listener, reports explicit
  failures with their error code, exits 1 for failed runs, and supports
  --json; per-run --provider/--model/--system-prompt overrides leave the
  defaults alone; --inference-timeout bounds provider calls.
- Provider connection configuration stays outside core domain types: the
  binary always offers the fake provider, enables openrouter/openai when
  their standard key variables are set, and reads extra OpenAI-compatible
  endpoints from .dagos/providers.json. Keys come only from environment
  variables and are never stored.
- Tests: defaults (validation, per-project, restart), edited prompts only
  affecting later runs, identical IR and DAG when only provider/model
  change, committed-only event observation, provider configuration
  loading, and the CLI end to end (streaming, failures, persistence,
  missing workspace, crash recovery).

Closes #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- dagos becomes a library plus a thin binary; the library holds the
  workspace, provider configuration, inspection views, and HTTP server.
- Read-only views assembled from recorded state (core domain types, so
  they serialize per the contracts): Overview (project, run defaults,
  providers, Jev in use, the durable DAG, every run with its message) and
  RunDetail (message node, active context with sources, carried nodes,
  Jev request and classification, context added/removed, the exact IR,
  streamed prose, raw provider output, validated response, emitted nodes
  and edges, failure with the rejecting stage and reason, full events).
- `dagos inspect [overview|dag|run|events|context|ir|response] [RUN]`
  prints them as JSON; runs are named by ID or `latest`.
- `dagos serve` exposes GET /api/health, /api/overview, and
  /api/runs/{id|latest} on loopback (127.0.0.1:7420 by default), with JSON
  errors and 404s for unknown runs. Nothing in it mutates state.
- quickstart.md now walks the real commands for each scenario.
- Tests: overview contents, a completed run's full pipeline detail, a
  rejected run's explanation and raw output, the HTTP API over a real
  socket, and `dagos inspect` through the binary.

Closes #18

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New crate dagos-mcp: a minimal stdio MCP client (initialize,
  notifications/initialized, paginated tools/list) that turns server tools
  into IR capability descriptions named <server>.<tool>. Each server is
  time-boxed; missing commands, crashes, silence, and non-JSON-RPC output
  are reported per server and skipped. Nothing is ever called, and no MCP
  semantics enter dagos-core or DAG state.
- Workspaces may list servers in .dagos/mcp.json. `dagos run` and
  `dagos serve` discover capabilities and compile them into every run's
  IR via Runtime::with_tools; unavailable servers become warnings and the
  run proceeds without their tools. Read-only commands never start
  servers. The overview reports discovered tools and each server's status.
- A tiny stdio MCP server fixture binary backs the tests: pagination, every
  failure mode, discovery statuses, config validation, and full DAGOS runs
  both without MCP (no tools in IR, run completes) and with it (IR carries
  exactly the discovered tools). A CLI test proves a run succeeds with its
  only MCP server unavailable.
- README and research.md document the boundary.

Closes #19

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Jev calls now have a deadline (Runtime::with_jev_timeout, default 60s):
  a hung classifier fails the run with jev_failed instead of hanging it.
  FakeJev gains a hanging mode to prove it.
- tests/hardening.rs adds runtime-level coverage beyond the per-layer
  suites:
  - context and conversation continue across a process restart (fresh
    store handle and runtime over the same file);
  - a crash mid-run is recovered as `interrupted` and reported to the next
    run through IR recent_events;
  - a model superseding its earlier decision makes Jev remove the old node
    from the next run's context (context.removed) while it stays durable;
  - concurrent runs in four projects sharing one store keep contiguous,
    ordered, run-scoped event histories;
  - 400 seeded random edge insertions never create a cycle, and every
    rejection (self-loop, duplicate, cycle) is justified;
  - rows tampered below the repository API (invalid IDs, unknown event
    types) fail closed on read while other records stay readable.
- Existing suites already cover provider timeout, malformed JSON, invalid
  schema, deterministic IR snapshots, context removal, event ordering, and
  provider isolation; everything passes locally and in CI on Ubuntu and
  Windows.

Closes #20

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Runtime::start/finish split so the server returns a run ID before
  inference; recover_runs_except leaves runs this process executes alone.
- Server: embedded UI assets, POST /api/runs, PUT /api/config,
  POST /api/recover, SSE /api/stream of committed events, loopback
  Host-header guard, CSP on assets.
- UI (vanilla ES modules, no build step): run list, numbered pipeline
  stages, conversation/context/Jev/IR/response/events tabs, durable DAG
  arc diagram with temporary active-context rings. UI tests and Spec Kit
  convergence still to come.

Refs #21

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- dagos-openai: OpenAiCompatibleJev classifies context through any
  OpenAI-compatible endpoint (temperature 0, classifier-only prompt plus
  the kiss.jev-context.v1 schema); output is still validated by the runtime
- dagos: select the Jev via DAGOS_JEV_PROVIDER/DAGOS_JEV_MODEL or the
  providers.json `jev` entry; the offline fake stays the default
- tests: model-backed Jev run over the mock endpoint, a planning Jev is
  rejected with jev_invalid_output, optional live Jev in the ignored test
- README: OpenRouter Jev and inference setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Jev

- keys: per-user keys file outside every project (APPDATA / XDG config,
  owner-only, atomic writes, corrupt files never overwritten); keys never
  reach the project, database, IR, or API responses, only hints (sk-o…7890)
- providers: OpenRouter, OpenAI, and Z.ai presets, available with an env or
  saved key (env wins); custom OpenAI-compatible endpoints editable in-app;
  connection check validates keys and lists models
- runtime: optional Jev fallback; a failed, timed-out, or rejected model Jev
  is recorded (jev.rejected, jev.fallback) and the offline policy classifies
  the same request, so runs never depend on the model Jev
- workspace: provider configuration reloads without a restart; in-flight
  runs keep the runtime they started with
- server: /api/settings endpoints for keys, endpoints, checks, and the Jev
- ui: Providers & keys dialog (s / ⚙): save & test keys, pick models for
  runs, add endpoints, choose the Jev; inspector shows Jev fallbacks
- cli: `dagos keys [list|set|remove]`, set reads the key from stdin
- spec: FR-021 (optional model Jev), FR-022 (in-app key management)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decisions models (`~typesafe/jev-latest`, `typesafe/jev-*`) reject Chat
Completions, so a configured decisions model now uses POST
/api/alpha/decisions: one calibrated `noul` question per candidate node,
answers mapped to active (p >= 0.5) or inactive in a kiss.jev-context.v1
document that the runtime validates as before. Malformed answers fail the
Jev and fall back to the offline policy. The settings Jev picker
recommends TypeSafe's Jev on OpenRouter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- store: migration 0003 adds conversations (rename/archive, never delete)
  and runs.conversation_id; earlier runs become one conversation per
  project titled after its first message
- runtime: Thread::{Latest, Conversation, New}; context carries and the
  IR's recent turns stay within a conversation while the project DAG is
  shared by all of its conversations
- ir: recent_events is the conversation so far (last 8 turns), each with
  the user's request and the reply prose or failure (additive `request`)
- jev: Decisions questions carry recency and the latest user messages, so
  "what did we just discuss" keeps the latest turns active
- server: projects (list/create/rename/overview/config), conversations
  (view as turns, rename, archive), runs target a conversation, a project,
  or a new conversation; run IDs resolve across projects
- cli: `dagos run --new` starts a new conversation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- chat view: conversations as threads (user bubbles, streamed replies,
  safe markdown, inline failures, per-turn context/Jev/emission details
  and an Inspect link); composer with model chip and config popover
- sidebar: new chat, chats by recent activity, archived chats; header
  rename and archive/restore
- project switcher: switch, create, and rename projects, each with its
  own DAG and run configuration
- inspect view: the existing run inspector with turn navigation
- keyboard: n new chat, v chat/inspect, p projects, j/k chats or turns
- tests: node --test UI suite for chat rendering (escaping, markdown,
  turns, menus), run in CI
- docs: README app section, FR-023, data model for conversations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- core: tools layer (ToolGate, ToolExecutor); runs execute the tool calls
  their validated responses request, up to a step limit, recording
  tool.requested / tool.decided / tool.completed events; results reach the
  model through IR tool_results; fake-tool model for offline tests
- mcp: long-lived stdio sessions with tools/call, per-tool policies
  (off/ask/allow), server cwd/enabled, policy changes without restarts,
  image data kept out of IR, long text truncated
- app: approval gate (policy first, then a person in the app, 10 min
  timeout; the CLI denies `ask` calls), API to answer calls and manage
  servers and policies, turn items interleaving prose and tool calls
- protocol: models are told how to request tools and read results
- constitution: principle 7 amended for bounded, approved tool execution

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mnehmos and others added 25 commits September 23, 2026 10:21
- chat: turns interleave replies and tool calls; calls waiting for a
  person show Allow once / Always allow / Deny; cards show arguments,
  results, errors, and denial reasons, and stay open across refreshes
- settings: Tools section per MCP server with status, per-tool and bulk
  off/ask/allow policies, a tool filter, launch settings, and one-click
  import of Claude Desktop's servers (no environment values copied)
- serve starts MCP servers in the background; start timeout 30s
- api: pending flag on calls actually waiting for a person
- tests: UI tests for tool cards and Tools settings; import test
- docs: README tools section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…put fences

Tool-calling chat models sometimes write a one-line preamble before the
JSON document (repeating presentation.prose) or fence the whole reply.
The adapter now removes either when nothing is lost; any other output is
returned unchanged and still fails validation. The protocol also tells
models to keep tool-step notes inside presentation.prose.

Verified live: openai/gpt-6-luna via OpenRouter called
ooda.get_system_info through an approval and answered from its result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment

gpt-6-luna paraphrases its prose before the JSON whenever it calls
several tools, so requiring the preamble to repeat the prose was too
strict. A preamble is now removed when it holds no JSON, is at most 1000
characters, and everything after it is exactly one JSON object; other
output still fails validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- markdown.js: block parser with nested and ordered lists, blockquotes,
  tables, rules, headings, fences (also inside list items), and inline
  code, bold, italics, strikethrough, links; everything escaped first
- tool results render as markdown (OODA's output is markdown), dropping a
  leading heading that repeats the tool name; structured-only output
  stays JSON
- three or more consecutive tool calls fold into one group with a status
  summary, open while a call waits for approval
- sidebar no longer scrolls sideways; long chat titles truncate
- tests: markdown suite, including OODA output

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Full debug info in every test binary grew the build directory to 36 GB
and filled the disk. A full clippy + test build now takes 2.3 GB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All enabled tools are offered to Jev with short descriptions; Jev labels
tools active or inactive like nodes, and only tools it does not hide are
compiled into the turn's IR. TypeSafe's Jev answers one noul question per
tool (keyed tool:<name>). Unlabeled tools stay exposed, so the offline
policy (and any Jev failure) keeps today's behaviour. Tool permission is
unchanged: policies and the person still decide whether a call runs.

- contracts: optional `tools` in kiss.jev-request.v1 and kiss.jev-context.v1
- validation rejects unknown or duplicate tool labels
- ui: turns show "tools N/M"; the Jev tab lists each tool's label
- constitution amendment, FR-024, README

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IR keeps only the conversation's latest turns. When a conversation is longer,
runs offer the built-in dagos.recall tool: every earlier turn is one chunk,
Jev judges each chunk's relevance to the model's query (one noul question per
turn through the Decisions API, batched to judge-sized requests; word overlap
without a model Jev or when it fails), and the relevant turns come back
verbatim with before/after cursors. Recall reads only the run's own
conversation, so it needs no approval. `dagos` is reserved as an MCP server id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…utomatically

Replaces the dagos.recall tool: recall is DAGOS's job, not something the model
has to think of. Chats organize work for people; the project remembers.

- Every run, Jev judges each earlier turn of every chat in the project that the
  IR's recent window does not carry (one noul question per turn through the
  Decisions API); the most relevant reach the IR verbatim as `recalled`.
- Before each step of a run, Jev judges every large tool result from earlier
  rounds against the request and the model's latest step. Results it judges not
  needed are replaced by a note in the IR and come back when relevant again; the
  latest round is always kept and full outputs stay in the event history.
- Without a model Jev that judges relevance, or when it fails, nothing is
  recalled and nothing is left out.
- The chat shows "recalled N" and "N results left out" under a reply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Syntax linters cannot check meaning; this one does. Rules are plain-English
sentences in .dagos/lint.json (14 defaults: "Swallows errors.", "Has a hidden
side effect.", ...), and Jev answers one calibrated yes/no question per rule for
each function, all functions in parallel, cached by content.

The review loop: a model reaches files only through tool calls, so before each
permitted call the runtime shows it to the reviewer, which remembers the project
files it names (paths, and file names inside shell commands). Each time the
model replies without tool calls, the functions it changed are judged; findings
at or above the threshold go back to the model as `review` in the IR and the run
continues, until a review is clean, the code stops changing, or max_rounds (3)
reviews have run. Reviews are recorded as review.completed and shown in the
chat; they never fail a run.

- dagos-core: Reviewer interface, bounded review loop, IR `review`, the general
  JevClassifier::decide (yes/no questions), review events.
- dagos-openai: decide through the Decisions API.
- dagos-lint (new): function extraction for Rust, JS/TS, and Python, rules,
  judging with a cache, LintReviewer.
- dagos: the reviewer wired from lint.json, `dagos lint [files]` for CI (exit 1
  on findings, --json for every judgment), review cards in the chat.
- Constitution amended for the bounded loop; FR-026.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `{`

A preamble that mentioned braces in prose ("returns `{}` on errors") made the
whole reply fail: the document was searched from the first `{` anywhere. It now
starts at the first line beginning with `{` that parses as a whole object; a
preamble may not itself begin with `{`, so only presentation text is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olicies

Tool checks only ever make a call stricter than its policy:
- Meta-tools (tools that run other tools, e.g. OODA's batch_tools) get the
  strictest policy among themselves and every tool of the same server their
  arguments name: naming an off tool refuses the call, naming an ask tool asks.
- The guard: with a Jev that answers yes/no questions, each call about to run
  is judged against eight risks in the light of the user's request (destroys
  data, changes the system, uses the network, touches secrets, changes files
  outside the project, stops other processes, controls the computer, delegates
  to tools chosen at run time). A risk at 0.5 or more, or a check that cannot
  run, turns allow into a question; the approval card says why, and the reason
  is recorded in tool.decided with the person's decision. Without such a Jev,
  policies decide alone.

ToolDecision::Allow now carries an optional note. FR-027; constitution note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Risks now say whether the user's request can excuse them. Changing requested
files, installing requested software, and the like pass when the request plainly
asks for them (the lint loop rewrites files constantly); deleting data (rm, del,
Remove-Item, DROP, git clean, ...), touching secrets, and delegating to tools
chosen at run time always go to the person. Nine risks in all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rson

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- spec: user stories and acceptance scenarios for chats, tools, recall, and the
  review loop; scope now names the two bounded, amended loops instead of ruling
  out all loops.
- plan: the real architecture, crates, and converged phase; the constitution
  gate states the bounded loops.
- research: MCP tools now run under policies; decisions since the core (Jev as
  yes/no judgments, recall over summaries, the tool-call hook, checks only
  tighten, the small extractor, tolerated model quirks); unresolved tradeoffs.
- data-model: event types, what the IR carries and from where, workspace files.
- quickstart: the app, a real model and Jev, tools, lint.
- tasks: T033/T034 done; T036-T045 for the converged scope; T035 (tag) open.
- checklists: requirements checked; new traceability of FR-001..027 to code and
  tests, with the boundary checks re-verified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ll cost

From a high-effort review of PR #22; each finding was checked against the code.

Security
- Meta-tools could run `off` tools: the gate only knew offered tools. It now
  learns every tool the servers describe, `off` ones included.
- `serve` refuses non-loopback addresses: the app runs tools and has no sign-in,
  and a LAN bind let anyone add a server that runs any command.
- The guard judged only the first 6000 characters of arguments. Long arguments
  are now judged in overlapping windows (a risk counts in any window), and
  arguments too long to window fail closed and ask.
- A preamble holding a JSON object is no longer dropped as "presentation text".

Correctness
- The streaming prose extractor starts at a `{` that begins a line, like
  unwrap_document, so a preamble mentioning braces no longer blanks live prose.
- Starting a run and recovering stale runs are serialized, so recovery cannot
  fail a run between its creation and being marked executing.
- "Always allow" also allows the tools a meta-tool named that made it ask; a
  guard-flagged call cannot be remembered (it asks every time), and the button
  is hidden for it.
- Edits of mcp.json hold a lock from load to save.

Performance
- The MCP pool lock is no longer held while a session is busy or starting, so a
  slow call on one server does not stall every other server.
- Recall and compaction read only the event types they use, never the many
  inference.delta events (new Tx::events_of_types).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run races its pipeline against a stop signal: stopping drops whatever it is
waiting for (the provider, a tool, a person's approval, Jev) and records
run.failed with the new error code `cancelled`; everything committed before
stays. POST /api/runs/{run}/stop reaches the runtime executing the run even
after a settings reload. Approvals forget a wait that is dropped, and calls left
unfinished by an ended run show as stopped. In the app, Stop replaces Send while
the chat's reply is running (Esc also stops it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings → Safety and quality has two new panes. Review loop: on/off, the
finding threshold, reviews per run, and the rules themselves (text plus
optional "applies when" / "not a problem when" hints; add, remove, restore the
defaults), saved to lint.json and used by the next runs. Tool guard: on/off and
its threshold, saved to mcp.json (`guard`), with the nine risks listed and the
ones that always ask marked. New endpoints: GET/PUT /api/lint and
PUT /api/tools/guard; both validate before saving.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every chat message is a `conversation` node, and they crowded out what runs
recorded. The panel now hides them by default; a header toggle ("Show 12
messages") brings them back and is remembered in this browser, a selected
message stays visible, and the counts still cover the whole DAG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A third view next to Chat and Inspect (`l`). It judges the files changed since
the last commit, or the files named, and shows every judgment as a heatmap of
functions × rules, findings outlined. Selecting a function shows its source and
each rule's probability, most likely first. A finding can be marked "not a
problem here": dismissals live in lint.json and the review loop and
`dagos lint` skip them; Restore brings one back.

New endpoints: POST /api/lint/run, PUT /api/lint/dismissals. Changed-file
detection moved to the app library, shared by the CLI and the view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When 
10BC8
the review loop ends with findings still open (the model disputed them,
the code stopped changing, or the run ran out of reviews), each is recorded as
an observation node (`kind: "lint_finding"`, with rule, file, function, line,
and probability) and announced with dag.node_created. A finding the project
already has a node for is not recorded again. Later runs in any chat draw on
them through Jev's classification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenAI-compatible adapter now offers the IR's tools through the endpoint's
own `tools` field (names like `ooda.exec_cli` mapped to allowed function names
and back) instead of in the IR text, assembles the model's streamed native tool
calls, and returns them as the response document's `tool_calls`, with any note
the model wrote as prose (streamed live). Runtime, gate, and guard see the same
validated document as before. Final replies still use the JSON document.

A model or endpoint that refuses tools (a 400/404/422 mentioning tools or
functions) is retried with DAGOS's JSON protocol and remembered, so the next
steps skip the attempt. Endpoints can opt out with `"native_tools": false`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ard paths

- The review loop also judges code files a run's calls changed without naming
  them (e.g. a generator a shell command runs), found by modification time since
  the run's first call (build and tool folders skipped), judged whole.
- Judged answers are saved in .dagos/lint-cache.json by the review loop, the
  Lint view, and `dagos lint`, so unchanged functions are not judged again after
  a restart.
- A run started while MCP servers are still starting waits for them (up to 45
  s) and runs with the tools; Stop works during the wait (the run ends as
  cancelled without calling the model).
- The guard sees the project folder in both path styles and without Windows'
  verbatim prefix, which caused an `outside-project` false positive for paths
  inside the project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Context is managed by relevance, not rationed. Recall no longer keeps only the
5 most relevant turns, no longer searches only the latest 300, and no longer
cuts a recalled turn to 4000 characters or its tool results to excerpts: every
earlier turn Jev judges relevant reaches the IR whole. Jev still judges a
trimmed copy of each turn (its own input limit), which never limits what the
model receives. Long histories are judged in parallel Decisions batches, so
they take no longer than short ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ests

The model's context window is the one real limit, so it is managed instead of
failing: providers may report a model's window (the OpenAI-compatible adapter
reads OpenRouter's model list once), and when everything relevant does not fit,
the runtime fits each step's IR to it, leaving out the least relevant recalled
turns first, then earlier rounds' tool outputs (oldest first), then the oldest
chat turns. Nothing is left out while everything fits, and without a known
window nothing changes. What still cannot fit is sent trimmed as far as
possible.

Tests: the inspector and tools API tests no longer read the real per-user key
file (they were not hermetic), the inspector test gets a generous deadline for
busy CI runners, and a run that fails there now says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jev now also answers one yes/no question about the person's own message: does
it ask for all available context rather than about something specific? On a
yes, the run loads every node of the project into its context and recalls
every earlier turn of every chat (newest ranked highest), fitted to the model's
window like any other step, and records context.expanded; the reply shows
"full context". Only the person's message is judged, never tool output, and
without a Jev that answers yes/no questions nothing changes. Constitution note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Mnehmos Mnehmos changed the title DAGOS 0→1: core runtime implementation DAGOS 0→1: core runtime, context management, tools, and semantic review Sep 24, 2026
@Mnehmos
Mnehmos marked this pull request as ready for review September 24, 2026 16:24
@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0eb553463

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +340 to +341
let stopped =
task_state.stop_requests.lock().unwrap_or_else(PoisonError::into_inner).remove(&id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep stop requests visible until Runtime::finish registers

When POST /api/runs/{run}/stop arrives after this removal but before runtime.finish(started) inserts the run's Notify, runtime.stop(&run) returns false and the newly inserted stop_requests entry is never checked again. The worker then runs normally and discards that request at cleanup, so an API call reported as {"stopping": ...} can still proceed to execute allowed tool calls.

Useful? React with 👍 / 👎.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Mnehmos
Mnehmos merged commit 137219d into main Sep 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spec Kit + UI: converge 0→1 with a beautiful DAGOS interface Epic: DAGOS 0→1 implementation

1 participant

0