diff --git a/.cursor/skills/orchestrator-executor/SKILL.md b/.cursor/skills/orchestrator-executor/SKILL.md new file mode 100644 index 00000000..a7c3d7f7 --- /dev/null +++ b/.cursor/skills/orchestrator-executor/SKILL.md @@ -0,0 +1,6 @@ +--- +name: orchestrator-executor +description: Fable 5 orchestrator-executor pattern for big tasks +--- + +You (Fable) are the orchestrator. Plan, decompose, synthesize. Reasoning-heavy phases go to deep-reasoner (5.6 Terra). Mechanical work goes to fast-worker (5.6 Luna). For high-stakes decisions, run deep-reasoner twice with slightly different framings and synthesize the best of both. Keep your own context lean. Delegate rather than doing mechanical work yourself. diff --git a/docs/effort-graph/CONTEXT.md b/docs/effort-graph/CONTEXT.md new file mode 100644 index 00000000..4b65c6eb --- /dev/null +++ b/docs/effort-graph/CONTEXT.md @@ -0,0 +1,78 @@ +# Effort Graph glossary — reasoning primitives for `@flatbread/proof` + +This glossary defines the **epistemic primitives** that make up the Effort Graph — a persistent, queryable memory layer over the reasoning and planning that happens during long-horizon software work, single-agent or multi-agent. + +The Effort Graph is **built on top of** Flatbread's content-layer vocabulary (see [Flatbread glossary](../glossary.md) for `Collection`, `Record`, `Relation`, `Refs`, `ID`). Each primitive here is a Flatbread **Collection**; instances are **Records**; cross-primitive references are **Relations** wired through frontmatter `refs`. + +**What this is not:** a CMS, an authoring UI, a hosted memory product, or a general task tracker. It is a relational substrate for capturing the **gray-area reasoning** that an ADR-only record loses: open questions, considered alternatives, sticky constraints, prospective risks, and post-hoc invalidations. + +**Operational provenance** (which session produced this, which agent, which model, which DAG run) is captured as **frontmatter fields** on these primitives, not as peer collections. The durable transcript record lives next to the graph under `.flatbread/artifacts/` (see [`packages/proof/README.md`](../../packages/proof/README.md) §Artifact Output). + +--- + +### Effort + +The **anchor** of the graph. One Effort represents a coherent, named thread of work — a feature, a migration, a spike, a research investigation, a refactor. Every epistemic primitive belongs to exactly one Effort. + +An Effort is the stable filter (`effort: { eq: "" }`) that scopes every "what's still open / what did we conclude / what are we considering?" query. Loss of the Effort anchor is the failure mode that vault MCPs and flat memory stores cannot avoid; preserving it is the central wedge. + +An Effort has its own lifecycle (active, paused, completed, abandoned) but carries **no reasoning content of its own** — its body is a short description; the reasoning lives in the primitives that ref back to it. + +### Issue + +A **tracked unit needing attention within an Effort**, in the GitHub-issue sense — broader than "something is wrong." Issues span open questions, observed defects, identified gaps, and explicit blockers. Each Issue carries a `kind` field that names the speech act (`question`, `defect`, `gap`, `blocker`, …) and a status (`open`, `resolved`, `deferred`, `wontfix`). + +An Issue is resolved by a Decision (we'll do X) and/or one or more Findings (here's what we learned that closes this). The `kind` is open-ended (free-form string) so common values emerge from dogfooding rather than from schema enforcement. + +Feature _proposals_ are not Issues — they are `Decision{state: proposed}`. Issues are reactive (something exists that needs attention); proposed Decisions are proactive (let's commit to doing X). + +### Finding + +A **grounded observation** — a claim about reality (the codebase, the user, the literature, the runtime) backed by cited evidence. Findings resolve Issues, support or contradict Decisions, surface Risks, and invalidate prior Findings or Decisions when reality refutes a prior belief. + +The `Finding{kind: retrospective}` variant carries the additional semantic that the Finding was produced **after a Decision shipped** and may invalidate that Decision in light of new evidence. Other Finding kinds (e.g. `measurement`, `survey`, `dead-end`) may emerge from usage but are not load-bearing in the schema. + +### Decision + +A **commitment** — a chosen path among alternatives. Has a `state`: `proposed` (under consideration), `accepted` (committed), `rejected` (an alternative we chose not to take), `superseded` (replaced by a later Decision), or `deprecated` (no longer current but not replaced). + +Multiple `state: proposed` Decisions under the same Effort represent **competing directions under exploration**. When one is `accepted`, the others should transition to `rejected` with a back-pointer to the accepted Decision. This is the schema's substitute for a separate `Proposal` primitive. + +A Decision cites the Findings, Constraints, and Risks it weighed; it does not duplicate their content. + +### Constraint + +A **sticky boundary** that scopes the decision space for an Effort. May be hard (license incompatibility, regulatory rule, irreversible upstream choice) or soft (team preference, budget envelope, performance target). Constraints typically outlive individual Decisions and apply to many of them. + +A Constraint is not a Risk: a Constraint is a known limit you must design within; a Risk is a possible outcome you might suffer. + +### Risk + +A **prospective negative outcome** with a likelihood and a severity. Risks attach to Decisions as part of the rationale for choosing among them. A Risk has a lifecycle: `open` (live, unmitigated), `mitigated` (an accepted Decision exists to reduce likelihood or severity), `realized` (it happened — usually triggers a Finding and possibly a retrospective Finding), or `accepted` (we knowingly proceed despite it). + +--- + +## Cross-cutting edge vocabulary + +These edges are **ubiquitous** — they live on every epistemic primitive. They are the type-agnostic semantic graph that lets a reader trace causality, evolution, and disagreement. + +| Edge | Description | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `derives_from` | Causal upstream — what this artifact is responding to or built on. (A Finding `derives_from` an Issue; a Decision `derives_from` Findings + Constraints; a retrospective Finding `derives_from` the original Decision.) | +| `supersedes` / `superseded_by` | Replaces an earlier artifact of the same primitive. The forward edge (`supersedes`) is canonical; `superseded_by` is a derived projection materialized to disk so any single record can answer "am I current?" in one access (see ADR-0004). | +| `invalidates` / `invalidated_by` | Stronger than `supersedes` — asserts the targeted artifact was _wrong_, not just outdated. Used primarily by retrospective Findings against shipped Decisions. Same forward-canonical / materialized-back-edge rule as `supersedes`. | + +**The edge vocabulary is permitted to grow** as dogfooding surfaces real omissions. Candidate additions to watch for: `refines` (soft non-replacing evolution), `contradicts` (explicit disagreement that doesn't yet rise to invalidation), `blocks` (an open Issue gating progress on another). Any addition must justify itself with a query the existing vocabulary cannot answer. + +--- + +## What is intentionally not modeled + +- **Session, Run, Plan, Artifact, Agent** as collections. These are operational provenance, captured as opaque-string frontmatter fields (`produced_in`, `created_by`, etc.) on the epistemic primitives above. Their durable log-grade record lives under `.flatbread/artifacts/` from `@flatbread/proof` runs. +- **Investigation** as a collection. An investigation is a Session-grouping of Findings (and possibly an Issue with `status: investigating`), not a noun in its own right. +- **Question** as a collection. Collapsed into `Issue{kind: question}` — the speech-act distinction does not warrant a separate primitive. +- **Proposal** as a collection. A Proposal is a `Decision{state: proposed}`. +- **Retrospective** as a collection. A Retrospective is a `Finding{kind: retrospective}`. +- **Branch** as a frontmatter field. Speculative exploration lives on git branches; cross-branch reasoning is preserved by promoting artifacts to the integration branch when an exploration closes (rejected or merged). + +These collapses may be revisited if real usage proves the host primitive cannot carry the missing semantics; Flatbread's `refs` model permits later splitting without ID breakage. diff --git a/docs/effort-graph/adr/0001-effort-graph-memory-location.md b/docs/effort-graph/adr/0001-effort-graph-memory-location.md new file mode 100644 index 00000000..d3cfed52 --- /dev/null +++ b/docs/effort-graph/adr/0001-effort-graph-memory-location.md @@ -0,0 +1,28 @@ +# 0001 — Effort Graph memory location + +Status: Accepted + +## Context + +The Effort Graph stores epistemic artifacts (Issues, Findings, Decisions, Constraints, Risks) as flat markdown files indexed by Flatbread. Where those files live relative to the project repo determines whether reasoning branches with the code, and whether reasoning survives when a speculative branch is abandoned. + +Three modes were considered: + +- **A. In-repo, branch-coupled** — files under `/.flatbread-efforts/`. Reasoning branches with the code. Abandoned branch ⇒ abandoned reasoning. +- **B. In-repo with promote-on-close tooling** — same location, plus a helper that lifts artifacts onto the integration branch when an exploration closes. Requires a robust definition of "close a branch" across squash-merge, rebase-merge, PR-closed-unmerged, branch-deleted-then-resurrected. +- **C. Sibling repo / submodule** — memory has its own git history independent of project branches. Cross-branch reasoning loss disappears because memory commits to memory `main`. + +A cross-branch `ref` mechanism (point a ref at reasoning on another branch) was rejected: refs must resolve at index time against a single on-disk tree; resolving across branches requires either mutating the working tree or a per-branch index (a source-plugin rewrite), breaks the self-contained-artifact review story, and makes staleness invisible on rebase/force-push/delete. + +## Decision + +Ship **mode A as the default**. Make the schema and write API **identical across all three modes** so that **mode C works by repointing `path` in `flatbread.config.ts`** with no code change; document mode C as the supported alternative for teams that abandon exploration often or span multiple repos. + +**Defer mode B.** The promote-on-close ergonomics can be replaced day one by a documented `git cherry-pick ` + status-flip convention (`Decision: rejected_explored`, `Issue: wontfix`, `Finding: archived-from-exploration`). A `flatbread efforts promote-branch-artifacts` helper may land later. + +## Consequences + +- The default (A) accepts that reasoning on a never-merged branch is lost unless the team promotes it. This is acceptable because most efforts merge. +- Teams that care about preserving rejected exploration have two escape hatches without new platform code: cherry-pick promotion (still mode A) or mode C (config-only). +- The schema must not encode git internals (no `branch:`, no `git_ref:` field). Branching is emergent from git itself plus `Decision.state` and `derives_from` edges. +- Deferring B leaves a documented manual convention as the only path for promote-on-close until demand justifies the helper. diff --git a/docs/effort-graph/adr/0002-semantic-mutation-write-surface.md b/docs/effort-graph/adr/0002-semantic-mutation-write-surface.md new file mode 100644 index 00000000..6944e6f1 --- /dev/null +++ b/docs/effort-graph/adr/0002-semantic-mutation-write-surface.md @@ -0,0 +1,28 @@ +# 0002 — Semantic-mutation write surface + +Status: Accepted + +## Context + +Storing edges bidirectionally (`supersedes`/`superseded_by`, `invalidates`/`invalidated_by`) so that any single record can answer "am I current?" in one access (ADR-adjacent decision captured in `CONTEXT.md`) means a single conceptual change — "Decision X supersedes Decision A" — must update **two files atomically**: write X with `supersedes: [A]`, and patch A with `superseded_by: X`. A half-applied edge (X claims supersession, A does not know) is a corruption mode. + +Two stances for the write API the agent calls: + +- **(α) Narrow create-only surface.** Ship only `WriteIssue` / `WriteFinding` / `WriteDecision` / `WriteConstraint` / `WriteRisk` create mutations plus a generic frontmatter patch. The agent is responsible for issuing both halves of a bidirectional edge; the read shim warns about half-applied edges during a validation pass. +- **(β) Semantic-mutation surface.** Each schema-level concept (supersede, invalidate, resolve-issue, mitigate-risk) gets a dedicated typed mutation that atomically updates both ends of the edge. The platform owns bidirectional consistency; the agent does not. + +## Decision + +Adopt **β — a semantic-mutation write surface**. Mutations are validated and expanded at the level of **semantic edits**, not file edits. Each mutation: + +- has its own Zod schema and validation rules (e.g. `SupersedeDecision` requires the target Decision to exist in the index and not already be `superseded`), +- expands into the full set of file writes its semantics imply, and +- completes all of those writes or none (transaction semantics — partial-failure handling is specified separately). + +## Consequences + +- The agent-facing write contract is a set of named mutations, not "validate this YAML and write it." This is a larger design and implementation surface than α. +- Bidirectional-edge integrity is guaranteed by the platform, mirroring how `@flatbread/proof` put convergence-loop semantics in the DAG primitive rather than in agent prompts. +- A transaction/rollback primitive is now required (what happens when the second of two writes fails). This is a new capability for the write path. +- Flatbread has no mutation support today; β raises the question of whether mutations are exposed through GraphQL resolvers in core or through a standalone write library that does not touch the read layer. That fork is decided separately. +- The set of semantic mutations must be enumerated and kept small; each new mutation is API surface that must be versioned and taught to agents. diff --git a/docs/effort-graph/adr/0003-write-path-and-read-after-write-consistency.md b/docs/effort-graph/adr/0003-write-path-and-read-after-write-consistency.md new file mode 100644 index 00000000..9347eaf9 --- /dev/null +++ b/docs/effort-graph/adr/0003-write-path-and-read-after-write-consistency.md @@ -0,0 +1,37 @@ +# 0003 — Write-path architecture and read-after-write consistency + +Status: Accepted + +## Context + +ADR-0002 adopted a semantic-mutation write surface (β). Flatbread today is read-only and in-memory: `FlatbreadProvider` (`packages/core/src/providers/base.ts`) builds the GraphQL schema once at construction and exposes only `query()`. There is no `Mutation` type in `packages/core` and no write-back. The filesystem is the source of truth; the GraphQL graph is a projection built once. Live reload is explicitly unsupported today ([issue #65](https://github.com/FlatbreadLabs/flatbread/issues/65); `docs/positioning.md`). + +Two write-path architectures were considered: + +- **(1) Mutations inside core's GraphQL.** Add a `Mutation` type and resolvers that write files; the provider gains `mutate()`. Forces an in-process cache-coherence subsystem (every successful write must patch/invalidate the cached `EntryNode` graph or the next `query()` is stale) into core, which it does not have today. +- **(2) Standalone writer + read-only GraphQL.** A separate library owns the Zod mutation schemas, file expansion, and transaction semantics, and writes the source-of-truth files directly. The GraphQL read layer stays read-only and re-projects from disk. + +A separate question is the **read-after-write consistency model**: tool-call-boundary re-index (write returns touched ids/paths; next read re-indexes) vs instantaneous in-process read-after-write. + +## Decision + +Adopt **architecture (2): a standalone semantic writer; GraphQL stays read-only.** Mutation logic (validation, multi-file expansion, transactions) lives outside core's resolver layer, consistent with Flatbread's existing files-are-source-of-truth model. A thin GraphQL mutation _facade_ that delegates to the writer may land later, but GraphQL mutations are not the home of the logic. + +Adopt **live-reindex (watch mode) as a v1 dependency** for the consistency model, satisfied by implementing the **"Draft unified watch design"** already specified in `docs/local-dev-loop.md` (reload records → re-run ID/ref/cardinality validation → rebuild schema → hot-swap the live schema only if the new graph validates, else keep the prior schema and log). + +### Consistency contract (v1) + +The in-memory graph that reads are served from is rebuilt after a write; there is a brief window between "file saved" and "rebuild complete." During that window a read does not block and never returns a torn/half-built graph, but a _concurrent_ reader may observe the prior graph (briefly out of date, never wrong-shaped). The window widens with corpus size because rebuilds are full today. The contract that bounds this: + +1. **Read-your-own-writes (mandatory).** A mutation's return payload includes the written/changed artifacts, so the writing agent never re-queries to see its own write. This removes the window entirely for single-agent write-then-read, which is the dominant case. +2. **Default eventual, opt-in strict for concurrent readers (Q7.i → Option 1).** A second, concurrent reader may be a beat behind by default. When a workflow cannot tolerate this (e.g. a downstream `@flatbread/proof` task that must observe an upstream task's writes), the reader opts into a strong read that waits until the index has caught up to the depended-on write before answering. **This relaxed-by-default behavior, and how to request a strict read, must be called out in user-facing documentation when implemented.** +3. **Incremental reindex (v1 dependency, Q7.ii → Option A).** Because the writer knows exactly which files it touched, reindex re-reads only the changed files plus their ref-affected neighbors and patches the in-memory graph, instead of rebuilding the whole graph. This keeps the staleness window small regardless of corpus size (thousands+ of memories) and pairs with retiring the per-resolver `cloneDeep(contentNodesByCollection[...])` cost in `packages/core/src/generators/schema.ts`. + +## Consequences + +- The Effort Graph spec now has a **hard dependency on shipping the unified watch / live schema-swap seam** in Flatbread, **including incremental (changed-files-only) reindex**. This is scoped (a documented design contract and an existing codegen watch loop to factor from), not greenfield, but it is on the critical path and must be sequenced before the write story is considered done. +- Writes cannot destabilize core's read path, since transactional file-writing lives in a separate package. +- The writer must return the ids and file paths it touched, both for read-your-own-writes payloads and so the incremental reindex layer can refresh exactly the affected collections. +- A reader opting into a strict read needs a way to name the write generation it depends on; the writer must therefore expose a monotonic generation/version token in its return payload. +- Transaction/rollback semantics for multi-file mutations remain to be specified (see follow-up). +- If the watch seam slips, the fallback is tool-call-boundary re-index (read shim re-indexes affected collections per invocation); this is a degraded mode, not the target. diff --git a/docs/effort-graph/adr/0004-multi-file-honesty.md b/docs/effort-graph/adr/0004-multi-file-honesty.md new file mode 100644 index 00000000..5a956fef --- /dev/null +++ b/docs/effort-graph/adr/0004-multi-file-honesty.md @@ -0,0 +1,31 @@ +# 0004 — Multi-file honesty: edge authority and the atomicity boundary + +Status: Accepted + +## Context + +ADR-0002 promised transaction semantics for multi-file mutations ("completes all writes or none") but deferred the mechanism; ADR-0003 deferred it again. Two sub-questions remained. + +**Edge authority.** `CONTEXT.md` stores `supersedes`/`superseded_by` and `invalidates`/`invalidated_by` bidirectionally so any single record can answer "am I current?" in one access — a human opening the raw file in a PR or grep, without an index. Naively that makes every edge write a two-file transaction, since both directions are authoritative and a half-applied edge is corruption. + +**Irreducibly multi-authoritative mutations.** Some mutations change authoritative state on several files even after edge authority is settled: `AcceptDecision(A)` sets A to `accepted` and its sibling `proposed` Decisions to `rejected` — each rejection is that file's own state, not derivable from A. Two atomicity boundaries were considered: **(a)** the git commit (writer stages and commits every mutation; a crash leaves a dirty tree that git recovers; bonus: free, high-fidelity evolution history of the reasoning graph — on-theme for a git-native product), and **(b)** a writer-level save-or-undo transaction independent of git. + +## Decision + +**Forward edges are canonical; back-edges are derived, materialized projections.** Only `supersedes` and `invalidates` carry authoritative intent. `superseded_by`/`invalidated_by` are mechanically determined projections that are still written to disk: the writer materializes both sides in the same save group, and the incremental reindexer validates and repairs any drift (hand edits, merge damage, crash residue). On conflict the forward edge wins; a reverse-only manual edit is non-authoritative and will be corrected, with a diagnostic naming the repaired file. "Derived" describes ownership and repair direction, not an optional disk cache — the raw file keeps its one-access honesty after convergence, preserving ADR-0001's self-contained-artifact review story. + +**Writer-level save-or-undo is the default atomicity boundary; git commit is opt-in history, never correctness.** The writer implements a small write-ahead journal: fsync an intent record (transaction id, paths, before-images, target generation), apply each file via same-directory temp-file + rename, write a durable committed marker, then trigger one journal-aware incremental reindex batch; the generation token is published only after reindex and schema swap succeed. Startup recovery is idempotent: uncommitted journal ⇒ roll back; committed journal ⇒ complete and reindex. A per-graph writer lock serializes concurrent writers (two agents in a proof DAG fail/retry rather than interleave). + +The opt-in git mode creates one isolated memory commit per successful mutation using a dedicated temporary index (never touching the user's staged work), and runs only **after** the journal transaction commits. If the commit fails, the mutation stands — report "committed locally, history commit unavailable"; never roll back a committed semantic mutation to preserve git symmetry. + +**The "free reasoning history" of (a) is recovered by session-level checkpoints, not per-mutation commits.** The writer records a session/transaction id and touched paths; `flatbread efforts checkpoint` creates one deliberate commit for a session's coherent reasoning evolution, and `@flatbread/proof` may checkpoint at successful DAG completion — never per node. Per-mutation commits in mode A would interleave dozens of tiny memory commits with code history, complicate rebase/squash, and let agents commit without being asked; mode C (sibling repo) softens those costs and may document a checkpoint-on-session profile, but does not change the default. + +## Consequences + +- Forward-authoritative edges shrink the true multi-authoritative set to lifecycle transitions (sibling rejection, issue resolution), keeping the journal small and simple. +- The transactional guarantee covers the writer API and the indexed read contract. Raw-disk readers can observe a partially applied group mid-rename; between a hand edit and reindex a file's back-edge may be stale. Both windows are bounded by the journal protocol and reindex repair, and are recorded as the honest limit of file-level atomicity. +- The reindexer must recognize active journals and defer affected paths, so the watch seam (ADR-0003) cannot validate a half-applied group. Reindex write-back of repaired back-edges uses the same journaled write path as mutations. +- Generation-token semantics tighten: a generation is not "committed" until back-edge projection repair and the live schema swap complete; strict reads (ADR-0003) wait on committed generations only. +- Merge resolution gets a deterministic rule: reconcile forward edges, regenerate reverse projections. Reverse-edge fields are canonicalized (sorted, dedicated frontmatter keys) to minimize conflict noise; a repair command must exist. +- `CONTEXT.md`'s edge-vocabulary language changes from "stored bidirectionally" to "forward edge canonical, back-edge materialized projection." +- Reversal criteria: 6.i reverses only if raw-file readers demonstrably require atomic cross-file edge visibility without writer/indexer involvement; 6.ii reverses only if dogfooding shows teams want every reasoning transition independently cherry-pickable and per-mutation commits stay low-friction under rebases and concurrent agents. diff --git a/docs/effort-graph/adr/0005-v1-semantic-mutation-enum.md b/docs/effort-graph/adr/0005-v1-semantic-mutation-enum.md new file mode 100644 index 00000000..5b4f1caf --- /dev/null +++ b/docs/effort-graph/adr/0005-v1-semantic-mutation-enum.md @@ -0,0 +1,39 @@ +# 0005 — v1 semantic mutation enumeration + +Status: Accepted + +## Context + +ADR-0002 requires the set of semantic mutations to be enumerated and kept small — each mutation is versioned API surface that must be taught to agents. ADR-0004 settled edge authority (forward edges canonical) and the atomicity boundary (journaled save-or-undo), so each mutation's file-expansion footprint is now specifiable. + +## Decision + +Ship exactly thirteen mutations in v1. Each has its own Zod schema; validation runs against a committed generation of the index (targets must exist, state transitions must be legal, e.g. `Supersede` rejects an already-`superseded` target). + +**Effort lifecycle (2)** + +- `CreateEffort` — creates the anchor record. +- `SetEffortStatus` — `active | paused | completed | abandoned`. + +**Creation (5)** — one per epistemic primitive; single-file writes that accept forward edges at creation (`effort` is required; `derives_from`, `supersedes`, `invalidates` as applicable), expanding to back-edge materialization per ADR-0004: + +- `WriteIssue`, `WriteFinding`, `WriteDecision`, `WriteConstraint`, `WriteRisk`. + +**Edge retro-linking (2)** — for wiring records that already exist: + +- `Supersede(supersederId, targetId)` — same-primitive only (per `CONTEXT.md`); validates the target is not already superseded. +- `Invalidate(findingId, targetId)` — a Finding asserting a prior Finding or Decision was wrong. + +**Lifecycle transitions (4)** + +- `ResolveIssue(issueId, resolution: resolved | deferred | wontfix, resolvedBy: refs)` — resolvedBy cites the closing Decision and/or Findings. +- `AcceptDecision(decisionId, rejectSiblings = true)` — the irreducibly multi-authoritative mutation: sets the target `accepted` and sibling `proposed` Decisions under the same Effort to `rejected` with a back-pointer to the accepted Decision (the `CONTEXT.md` proposal-collapse contract). Runs inside one journal transaction. +- `MitigateRisk(riskId, decisionId)` — flips the Risk to `mitigated`, citing the accepted Decision. +- `SetRiskState(riskId, state: realized | accepted, evidence: refs)` — the remaining Risk transitions; `realized` should cite the triggering Finding. + +## Consequences + +- Deliberately absent from v1: generic frontmatter patch (reintroduces the α surface ADR-0002 rejected), delete/archive mutations (git is the undo story), `RejectDecision` as a standalone (covered by `AcceptDecision` sibling-reject; a lone rejection without an accepted alternative is `SetRiskState`-style scope creep until dogfooding demands it), and body-edit mutations (edit the markdown body directly; only frontmatter semantics are platform-owned). +- Freeform body edits and hand edits to forward edges remain legal — the reindexer validates and repairs projections per ADR-0004. The mutation surface is the supported path, not the only physical path. +- Every mutation returns the RYW payload from ADR-0003: written/changed artifacts, touched ids and paths, and the generation token. +- Adding a mutation later is additive API surface; removing or reshaping one is a breaking change subject to the major-migration process. diff --git a/docs/effort-graph/adr/0006-id-and-slug-strategy.md b/docs/effort-graph/adr/0006-id-and-slug-strategy.md new file mode 100644 index 00000000..cf8169dc --- /dev/null +++ b/docs/effort-graph/adr/0006-id-and-slug-strategy.md @@ -0,0 +1,28 @@ +# 0006 — ID and slug strategy + +Status: Accepted + +## Context + +Effort Graph records need refs that remain stable when files move, titles change, or multiple agents create records concurrently. Filenames are useful for review, but cannot safely serve as identity across branches. + +## Decision + +Use the ID format `---<16 lowercase Crockford-base32 random chars>`. Prefixes are permanent and reserved: `eff` (Effort), `iss` (Issue), `fnd` (Finding), `dec` (Decision), `con` (Constraint), and `rsk` (Risk). For example: `dec-use-standalone-writer--r6dt3vp7k4m9q2x8`. The slug is capped at 48 characters. + +The 80-bit random suffix makes concurrent creation by multiple agents, including agents on branches that later merge, effectively collision-free without coordination. The slug keeps refs reviewable in YAML frontmatter and PRs; the suffix makes same-title records safe; the prefix is a semantic type discriminator. ULID and UUIDv7 are rejected as visual noise. Recency comes only from the required explicit `created_at` field, never from an ID. + +`id` is required frontmatter and the sole identity. Filename and path never participate in identity, so renames and moves preserve refs. The writer defaults the filename to the ID (`dec-use-standalone-writer--r6dt3vp7k4m9q2x8.md`), but a mismatch is advisory, not invalid. + +Efforts also use hybrid IDs rather than pure human slugs. Pure slugs make similarly named Efforts created by uncoordinated agents a duplicate-ID merge failure. Efforts carry an editable `title` and may carry a unique human-facing `slug` alias for CLI lookup; renaming an Effort changes only its title or slug, never its ID or dependent refs. + +The directory layout is collection-first and organizational only: `.flatbread-efforts/efforts/`, `issues/`, `findings/`, `decisions/`, `constraints/`, and `risks/`. Membership is expressed by the `effort: eff-…` ref, not by nesting under an effort directory, which would make Effort renames operationally noisy. + +Refs remain scalar IDs. The mandatory kind prefix already encodes the target primitive type, so future union or multi-collection refs such as `invalidates: [dec-…, fnd-…]` can dispatch by prefix without rewriting stored frontmatter values. Reject `decision:dec-…` wrapper syntax: it duplicates the prefix and forces adapter work immediately. + +## Consequences + +- Union refs still require a core/config/schema migration because Flatbread `refs` config currently targets exactly one collection; this is a `flatbread-major-migration` concern, but requires no content migration. +- The six prefixes must be reserved and documented permanently. +- The promised GitHub issue for union/multi-collection refs remains unfiled (a repo search found none) and is a follow-up. +- The writer must validate ID shape and uniqueness at mutation time. diff --git a/docs/effort-graph/adr/0007-agent-read-shim.md b/docs/effort-graph/adr/0007-agent-read-shim.md new file mode 100644 index 00000000..a2773f8d --- /dev/null +++ b/docs/effort-graph/adr/0007-agent-read-shim.md @@ -0,0 +1,41 @@ +# 0007 — Agent read shim + +Status: Accepted + +## Context + +Agents querying the Effort Graph through MCP tools or the SDK must not dump whole reasoning graphs into their context window. The first planned agent-facing read surface is `blockingDecisions(effortId)` (roadmap). ADR-0003 defined generation tokens and opt-in strict reads. + +## Decision + +Every read tool returns a bounded envelope. The response is navigation; the rendered markdown file is the evidence. Reading it costs the agent one Read tool call, and it can be grepped: + +```json +{ + "summary": "2 results; 1 accepted, 1 proposed; complete", + "artifact_path": ".flatbread/effort-graph/read-cache/42/abc123.md", + "artifact_sha256": "…", + "served_generation": "…", + "consistency": { "mode": "eventual", "min_generation": null }, + "page": { "returned": 2, "has_more": false, "next_cursor": null }, + "hints": ["getRecord(\"dec-…\")"] +} +``` + +`summary` is deterministic and at most 160 tokens, covering result count, material states, and truncation. `hints` contains at most 10 executable follow-up query calls, not prose. Digests are written atomically to `.flatbread/effort-graph/read-cache//.md` (gitignored). Generation in the path prevents a stale projection from being served under a current-looking filename; identical query and generation reuse the cached file. On startup or `flatbread effort cache prune`, prune files older than 24 hours and enforce a 100 MiB ceiling, oldest first. + +Each digest contains a query header (query, served generation, result counts, completeness), an index of anchor links, per-record sections with selected frontmatter, normalized relation lists, and a bounded body excerpt (600 characters / 12 lines), plus an explicit edge table. Full bodies are never inlined; `getRecord(id)` renders a single-record digest. + +V1 caps are 25 primary records, one-hop relation expansion, 50 displayed edges, and a 64 KiB digest. At any cap the digest is marked incomplete and returns an opaque cursor. Scope and body length never silently expand. + +Every response echoes `served_generation`. Reads are eventual by default. Strict callers pass `{consistency: {mode: "strict", min_generation: ""}}`; the server waits for the projection to reach that generation or returns an explicit consistency error, never a stale result labelled strict. Generation tokens are opaque to clients. + +Use one shared read/query-render layer for MCP and CLI: query projection → bounded result model → digest renderer/cache writer. MCP is a thin transport adapter, so agent and human read semantics cannot diverge. + +V1 supports effort-scoped structured predicates, known relation traversal, pagination, record lookup, generation-aware reads, and `blockingDecisions(effortId)`. Defer semantic or embedding search, cross-effort traversal, arbitrary graph queries, rendering templates, durable query artifacts, and cross-machine cache sync. + +## Consequences + +- `blocking decision` needs a precise definition against the current edge vocabulary before implementation. +- Caps should be re-benchmarked in tokens against real Efforts. +- The cache directory must be added to gitignore when implemented.