63036ed52efd9f3e152b65aa5fd50fa25742f275
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2e39dca9cf |
fix(tasks): import minted_kind by name — a stubbed service must not stub the guard (#3129)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 21s
CI & Build / integration (push) Successful in 23s
CI & Build / Python tests (push) Successful in 1m5s
CI & Build / Build & push image (push) Successful in 31s
CI 4682: test_create_task_issue_sets_kind_provenance_and_systems asserted task_kind == "issue" and got a MagicMock. That test patches the whole notes_svc module to keep the database out, so reaching validation through `notes_svc.minted_kind(...)` handed back a mock — the guard approved anything and returned nothing real. The product was wrong, not the test. minted_kind is pure validation, not a service call, so it is imported by name. A test that stubs the service to avoid I/O now keeps the guard intact, which is the behaviour you want from a guard: the only way to disable it should be to say so explicitly. That test now exercises the real validation, so it doubles as the guard against this recurring. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
69d93898d9 |
fix(tasks): a task's kind is correctable — the Kind select stops lying (#3129)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 48s
CI & Build / Build & push image (push) Skipped
`kind` was accepted at CREATE on both doors and dropped at UPDATE on both: update_task had no such parameter, and the REST PATCH allow-list never read the field. So a task filed under the wrong kind could never be corrected. The frontend made it worse by looking like it worked. TaskEditorView binds a Kind select, marks the form dirty, and HAS ALWAYS SENT `kind` in the update payload — the store even types it. The route ignored it, returned 200, the view optimistically updated, the toast said "Task saved", and the old value came back on reload. Silent success, same class as #2709. Found by trying to re-file #3126 as a spike after deploying 0091. It could not be done; the task had to be recreated as #3128 and the original cancelled. One seam, not two doors. `minted_kind()` lives in services/notes.py because the REST route cannot import an MCP tool module and a second spelling of the list is how the doors would come to disagree. Both create and update route through it, so a bogus kind is now a readable error rather than a CheckViolationError surfacing as a 500. TaskKind joins TaskStatus and TaskPriority as a real enum, and update_note validates task_kind exactly as it already validated those two — the field had been reaching setattr through the hasattr guard with no validation at all, unnoticed only because no door ever offered it. The `-> plan` question #3129 raised is answered in code rather than left implicit: MINTABLE_KINDS is work/issue/spike, deliberately NARROWER than the column's CHECK. `plan` stays a valid stored value because historical plan-tasks carry it and must stay writable; it is simply not a value any door hands out, and the refusal names start_planning because a caller reaching for it wants a plan. The whitelist and the policy answer different questions and are not the same list. Every new test reads the value BACK. One that only asserted the call succeeded would have passed against the broken code — the route returned 200 while dropping the field, which is how this survived long enough to be found by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
88e9c0b0bd |
feat(tasks): task_kind gains 'spike' — the investigation, not the change (#3099, milestone 312 step 5)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Failing after 32s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / Python tests (push) Successful in 1m8s
CI & Build / Build & push image (push) Successful in 31s
A spike is a shape the other kinds cannot hold. `work` ships a change; `issue` fixes something broken. A spike is time-boxed and its output is KNOWLEDGE — it succeeds by producing an answer, and nothing ships at the end of it. Filing one as `work` makes a finished investigation look like an abandoned change, which is why the distinction earns a value rather than a convention. It is also the record a failed check asks for. This milestone gave rules a verify_with; when one fails the rule is wrong, and the next move is often to go and find out what replaced it. notes.arose_from_id already exists (0065), so constraint -> spike provenance needed no schema at all — only a docstring saying it is there. Rule 36: the value and the widened CHECK land in the same migration, DROP then ADD, exactly as 0065 did for 'issue'. The two whitelists live in one tuple each so upgrade and downgrade cannot disagree about what the list was on either side. The downgrade demotes existing spikes to 'work' first — lossy, deliberately, because the alternative is a downgrade that fails on real data, and one that says what it did beats one that cannot run. 'plan' stays whitelisted though retired: historical plan-tasks carry it, and a row that cannot be rewritten cannot be edited, restored or migrated. The integration test asserts both halves. A test that only proved 'spike' is accepted would pass just as happily against a table whose CHECK had been dropped and never re-added — which is the other way rule 36's failure happens — so an unknown kind is asserted to still raise. Not in scope, deliberately: any special lifecycle, time-box enforcement, or gating relationship. It is a kind, not a workflow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c211e12b61 |
refactor(mcp): one rules_payload() for every surface that hands rules to an agent; drop the dead bearer resolver (#2828, milestone 296 area 4)
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 56s
CI & Build / Build & push image (push) Successful in 26s
Reading the 16 tool modules against each other: the six-key applicable-rules block (applicable_rules, applicable_rules_truncated, subscribed_rulebooks, project_rules, suppressed_rules, suppressed_topics) was hand-built in five places — enter_project, get_project, get_task (legacy plans), get_milestone (three of the six) and services/planning.start_planning. rulebooks_svc. rules_payload() is now the one place that names them; get_milestone gains the three it lacked, so every rules-carrying payload reads the same. list_rules / list_always_on_rules share _rule_summary. mcp/auth.resolve_bearer_to_user_id duplicated resolve_bearer's parsing and had no product caller (only its own tests) — removed; the tests now exercise resolve_bearer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3455f9cb9a |
refactor(systems): one seam — the Systems question rides every read and write of a record
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 12s
CI & Build / Python tests (push) Failing after 29s
CI & Build / Build & push image (push) Skipped
Scribe issue #2570, from the operator's challenge: the invariant is "always be asking whether what you're touching is a System's territory and whether the work is filed there" — not a nudge in one corner. The prior shape failed it twice: the hint fired only on creates (with a special-cased zero-Systems branch), and get_task/get_note returned records WITHOUT their Systems, so the read-side reflex had nothing to fire on (same per-kind asymmetry as #2481). - attach_systems(): single helper used by get/create/update for tasks, notes, and snippets, plus add_task_log. Tagged records always show `systems`; an untagged project record carries the `systems_hint` question instead. Neither field attaches empty (#2483). Hint is owner-only; everything fail-open (#2109). - untagged_systems_hint unified to ONE question — the vocabulary listing varies, the question doesn't; the zero-Systems branch stops being special text. - Docstrings state the uniform contract; floor prose now names the read-side reflex (systems visible -> list_system_records the pile). - Plugin 0.1.27 -> 0.1.28. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3ff8803593 |
feat(instructions): fit the delivery fold — 2k server map, floor Systems reflex, write-time systems_hint
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 47s
CI & Build / Build & push image (push) Successful in 28s
Claude Code injects only the first ~2,048 chars of an MCP server's instructions and silently cuts the rest mid-word (#2562, observed live): _INSTRUCTIONS was 20,002 chars, so ~90% — including all Systems tagging guidance — never reached any session. Rearchitect delivery around what each surface actually delivers: - _INSTRUCTIONS becomes a 1,997-char purpose-sorted map, with a header comment stating the budget and where detail belongs instead. - Tool docstrings keep the per-tool HOW (audit: nearly all displaced topics were already duplicated there); backfill the four gaps — enter_project session scoping + project bootstrap, create_rule entity-vs-rule test, create_design_system not-a-rulebook, create_system two-records test. - The plugin static context (the delivery floor) gains the tag-to-Systems reflex and a surfaces-layering statement; plugin 0.1.25 -> 0.1.26 so the executing cache refreshes (#2209). - create_task / create_note / create_snippet return a systems_hint when a record is created untagged in a project that has Systems — in-band at the exact write it applies to, fail-open like the dedup gate. - Guards: _INSTRUCTIONS length budget, floor-states-the-reflex, and a displaced-topics sweep asserting every cut topic still lives on a delivered surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6eedb0f6b9 |
fix(instructions): stop mandating a milestone for every non-trivial task
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 11s
CI & Build / integration (push) Successful in 19s
CI & Build / Python tests (push) Successful in 43s
CI & Build / TypeScript typecheck (push) Successful in 26s
CI & Build / Build & push image (push) Successful in 28s
Four product surfaces told the agent to call start_planning FIRST for any "non-trivial" work, while a fifth — the milestone bullet four lines up in the same file — had the criterion right: use one when the work has an arc. The loudest surface won, so sessions wrapped bug fixes and one-file changes in milestones that never meant anything. Mandating one project shape is what rule #115 forbids: some projects are milestone-shaped, others are a flat task list and always will be. Now the arc test is stated ONCE in full, in writing-plans, along with what to do when there is no arc (a task, driven by status and work-logs). The other surfaces name it and defer: - writing-plans/SKILL.md gains a "first decide whether this work wants a plan" section; its frontmatter trigger is the arc, not "non-trivial" - using-scribe reflex #4 points at the skill instead of restating it - server.py's Plan bullet adopts the milestone bullet's own criterion - server.py's planning paragraph drops from 11 lines to 6: it keeps the claim MCP instructions should make (a plan's HOME is a milestone, not a local .md) and drops the how, which the skill carries - start_planning's docstring gains the when Also removed "call start_planning FIRST — before any brainstorming, design, or plan-writing skill runs." That was the server asserting priority over the skill layer. Tools describe what they do; skills decide when they apply. The structural point outlasts the wording: a surface that restates a rule is a surface that will eventually contradict it, and nothing checks prose against prose. Closes #2322. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs |
||
|
|
6ca215d2b6 |
fix(telemetry): record a pull on get_task, closing the surfaced→pulled loop
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 12s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build & push image (push) Successful in 39s
Closes #2245. note_usage_events recorded `surfaced` for every auto-inject menu line regardless of kind, but `pulled` only from get_note, get_snippet and the REST snippet route. get_task recorded nothing. Auto-inject ranks kind-blind over a corpus that is overwhelmingly tasks and issues, so tasks are most of what it surfaces. Measured live, "write a function to debounce a callback in the frontend" returned three tasks and zero snippets — all three written as surfaced, none able to record a pull. surfaced and pulled only mean anything as a PAIR; the rate between them is what #1038 and #2085 gate on. So the gap sat exactly where the volume is, and the metric would have said "auto-inject surfaces things nobody opens" for its own dominant kind — an artifact of the instrumentation, not a fact about the feature, and one that pointed at a plausible-sounding wrong conclusion. get_note already carried a comment stating this was meant to cover ANY note kind precisely so tasks wouldn't look like dead weight. get_task is a separate tool in a separate module and never got the call — sibling drift, invisible because a missing side effect changes no return value. Guarded by a rule-#33 contract test that asserts, by source inspection, that every getter reachable from an auto-inject menu calls record_pulled. Source inspection because no behavioural test can see a call that isn't there. Not fixed here: the REST note/task detail routes still record nothing while the REST snippet route records `rest_snippet`. That asymmetry is real, but a human reading a note in a browser is arguably not the same event as an agent recalling one, and collapsing them could skew the signal the other way. Raised as a question for the retrieval survey instead of decided in passing. Pre-fix rows under-count task pulls, one-sidedly by kind — treat them as unknown rather than zero. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs |
||
|
|
9fa474b3c4 |
fix(mcp): make get_note / get_task share-aware
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 31s
CI & Build / TypeScript typecheck (push) Successful in 42s
CI & Build / Python tests (push) Successful in 56s
CI & Build / Build & push image (push) Successful in 1m0s
The injected menu tells the agent to open any hit with get_note(id), and that menu can list a collaborator's record reached through a shared project. The fetch was still owner-only, so those lines answered "not found" — for a record the same user opens fine in the browser. The agent path was strictly narrower than the web path for the same id. Same boundary miss as #2093: the list side was widened for sharing, the fetch side wasn't. Both tools now resolve through get_note_for_user, apply the trash filter themselves (permission resolution says nothing about liveness), and attach describe_provenance so a shared record arrives marked as someone else's rather than passing as the caller's. routes/tasks.py's parent-title lookup had the same narrowness: a shared subtask rendered as an orphan when its parent was equally shared. Four fetch tests across three files were patching notes_svc.get_note and had to be retargeted — note 2109's third sub-case, caught by grepping tests/ for the old name before pushing rather than by CI (#2159). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLwAaV4DQEmVyn496HnEvt |
||
|
|
322cbc3b5e |
feat(mcp): Phase 5 — write-time near-duplicate gate (update-over-create)
#755 Phase 5. create_note / create_task now BLOCK a near-duplicate instead of silently inserting: they return {"duplicate": true, "existing_id", message} pointing at the record to UPDATE. Fights store bloat and stale competing copies that semantic search (RAG) would otherwise resurface for reconciliation. A force=true override creates anyway for genuinely-distinct records. - services/dedup.py: find_duplicate_note — two signals, scoped to owner + same project + same kind: (1) normalized-title exact match (cheap, always); (2) semantic cosine ≥ 0.90 but ONLY when body ≥ 200 chars (short/title-only embeddings false-positive — the pre-pivot lesson). Project-less (orphan) records compare only to other orphans on BOTH signals (orphan_only on the semantic call) — they're not matched across every project. - Gate wired into the MCP create_note/create_task tools (the LLM write path) with force override; _INSTRUCTIONS documents the duplicate response + force. - Opt-in by design: the service helper is only called from the interactive create tools. Internal/programmatic creates (recurrence spawn, imports) go straight through services.create_note and are NOT gated — a recurring task spawning its next same-titled instance must not be blocked. - Scope v1: MCP tools only. REST/web (human CRUD, needs a UI affordance) and create_rule (not a RAG surface; _INSTRUCTIONS already steer it) are follow-ups. - tests: dedup service (title/semantic/body-gate/type-filter) + tool gate (blocks, force bypasses) for notes and tasks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f7742173aa |
chore(plans): make kind=plan retirement consistent across MCP, REST, UI, skills
Audit of the plugin + MCP surface after milestone-as-plan (T3): every path that could still create a kind=plan task or describe the old plan-task model is now aligned with the hard-retire decision. - create_task (MCP + REST POST /api/tasks): reject kind=plan with a message pointing to start_planning. The 'plan' enum value stays valid so legacy plan-tasks remain readable; update paths never touch kind, so they round-trip. - create_task / get_task docstrings: 'plan' dropped from creatable kinds; get_task's rules-augmentation noted as legacy-only (get_milestone for new plans). - skills/writing-plans: rewritten for milestone-as-plan (body = design, steps = child tasks, get_milestone to read back). - skills/using-scribe: "plans live in milestones via start_planning", not kind=plan. - TaskEditorView Kind selector: offers Work/Issue; "Plan (legacy)" shown only when the loaded task is already kind=plan (display round-trip). - test: create_task rejects kind=plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1f6c592226 |
feat(plans): milestone-as-plan-container; retire kind=plan (T3)
The milestone becomes the plan container: a new nullable milestones.body holds the design/intent (Goal/Approach/Verification) and individual steps live as first-class child tasks (milestone_id) instead of checkboxes crammed into one kind=plan task body. start_planning now creates a MILESTONE seeded with the body template (not a kind=plan task) and returns it with applicable rules; a new get_milestone MCP tool reads the plan back (body + steps + rules). kind=plan is hard-retired going forward — start_planning never creates one. The 'plan' task_kind enum value stays valid so the 11 historical plan-tasks remain readable in place; no body-shredding backfill (corpus review showed auto-splitting their checklists into tasks would be lossy: embedded code blocks, a non-binary [~] state, tables, ID-encoded hierarchy). - migration 0066: add milestones.body - model/service/route/MCP: body passthrough on create+update; get_milestone - server _INSTRUCTIONS: "plan" = milestone w/ body + child step-tasks - UI: ProjectView shows/edits a milestone's plan body; start_planning expands the new milestone and opens its plan editor - tests updated to the milestone contract + new body/get_milestone coverage Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
85e0501705 |
feat(issues): S2 MCP tools — system CRUD + issue/system wiring
Second slice of Issues + Systems (spec #825). New mcp/tools/systems.py: create_system, list_systems, get_system (records split into issues/tasks/notes), update_system (incl. archive via status), list_system_records (kind/open_only filters), delete_system. Registered in register_all; read tools (get_system, list_systems, list_system_records) added to the read-only-key allowlist (write tools default-deny). create_task/update_task: kind now accepts 'issue'; new system_ids (set-semantics associations) and arose_from_id (provenance, 0=unchanged/-1=clear) args. create_note/update_note: new system_ids arg (notes associate with systems too). services/notes.create_note: arose_from_id passthrough (update_note already handles it via setattr). Tests: MCP system tools + create_task issue-wiring (kind/provenance/systems), service layer mocked. Refs plan 825 (S2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e3c6124912 |
feat(mcp): make Scribe reflexively recall + scope reads to the active project
The MCP surface advertised writing well but recall poorly, and project
scoping had no anchor that survived past enter_project's snapshot:
- search / list_notes dropped the project_id their services already
support, so a scoped search was impossible — every query swept all
projects and bled unrelated work into the session.
- The tool descriptions were mechanical ("Semantic search over the
user's notes and tasks") with no trigger telling Claude WHEN to reach
for them; the server instructions were all write-discipline and said
nothing about searching before answering or starting work.
Changes:
- search, list_notes: add project_id param, wired to the service.
- search, list_notes, list_tasks: trigger-worded descriptions that push
passing the active project's id and reserve project_id=0 for a
deliberate cross-project sweep.
- _INSTRUCTIONS: add a 'Reach for Scribe to RECALL, not just to record'
block — search before answering/starting, check for an existing ticket
before create_task, scope reads to the active project (which does not
stick on the server).
Paired with always-on rule #75 in the FabledSword-family rulebook.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
b255a0f90e |
refactor: rename package fabledassistant -> scribe (code-only)
Renames src/fabledassistant -> src/scribe and all imports, plus the default DB name and DB user/password (fabled -> scribe) in config + compose. 952 refs / 154 files. Reverses the old 'internal name stays fabledassistant' convention. Code-only: live databases are still physically named 'fabledassistant'. Deployed environments must set POSTGRES_DB / POSTGRES_USER (or rename the DB) since the defaults now resolve to 'scribe'. Repo (FabledScribe), git host (fabledsword), MCP (fabled-git) and the image name (fabledscribe) are intentionally unchanged. ruff check src/ clean locally; CI (typecheck + pytest) is the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |