From d7723e22374622185583505230b546ebfbdae532 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 4 Oct 2026 14:56:14 -0500 Subject: [PATCH] docs(prd): PRD draft 0.3; slice 1 addendum A; lead decision 49 Darkwing's addendum as a record. Broker verbs enforce field ownership, broker push from a bare repo, polling without webhooks, Vikunja probes before the adapter interface is fixed. Co-Authored-By: Claude Opus 5.5 --- .../slice1-data-model-addendum-2026-10-04.md | 465 ++++++++++++++++++ docs/SESSIONS.md | 1 + docs/plans/2026-09-26_lead-decisions.md | 30 ++ docs/prd/mosaic-stack.md | 21 +- 4 files changed, 508 insertions(+), 9 deletions(-) create mode 100644 agents/darkwing/work/slice1-data-model-addendum-2026-10-04.md diff --git a/agents/darkwing/work/slice1-data-model-addendum-2026-10-04.md b/agents/darkwing/work/slice1-data-model-addendum-2026-10-04.md new file mode 100644 index 00000000..121e5add --- /dev/null +++ b/agents/darkwing/work/slice1-data-model-addendum-2026-10-04.md @@ -0,0 +1,465 @@ +# Slice 1 data model, addendum A (design note, 2026-10-04) + +Darkwing (CTO for Mosaic Stack, REQ-ROLE-4), for Sage. Design only. +Nothing here is built or committed. + +This addendum corrects and extends my note +`agents/darkwing/work/slice1-data-model-2026-10-04.md` (sha256 +948b94ce…, commit bb7e37dd). The note stays as written. Where the two +disagree, this file wins. Sources: +- Researcher's report `agents/researcher/work/2026-10-04_vikunja-pocketid.md` + (sha256 fcfc970f…), cited below as "R" plus a section number; +- PRD draft 0.2 `docs/prd/mosaic-stack.md` (sha256 6a4d23bc…, commit + c5799877), cited by requirement id; +- lead decisions 46, 47 and 48. + +Nothing below was run against a Vikunja instance. Every item the report +marks unverified stays unverified here, and section 9 lists the probes +that would settle each one. + +## Summary + +- The adapter targets `/api/v2` (REQ-TASK-1, R 1.1). The note's `/api/v1` + assumption is withdrawn. +- Vikunja token scopes are a map of route group to verbs, so the role + file's `scopes` for Vikunja becomes an object (section 2). +- Route groups are coarse. A token with `tasks: update` can edit every + task its bot can see, so "only the assigned role writes" can't live in + the credential. With the broker holding tokens (REQ-CRED-1), the + broker's verbs are where it is checked (section 3). +- REQ-CRED-1 overturns the note's section 2, which handed tokens to the + agent. The biggest consequence is `git push` for the coder, and the + obvious fix, a credential helper, leaks the token. Section 4 proposes a + broker push from a broker-owned repository. +- Sync is polling with a keyset cursor plus a periodic full reconcile. + I recommend slice 1 ship no webhook receiver at all (section 5). +- 6.8 stands: compare, then write. The ETag on GET makes the compare + cheap, and a partial PATCH narrows the race to fields both sides touch + (section 6). +- REQ-LAUNCH-1, REQ-DEC-4 and REQ-CLI-2 each need a small schema or + business-file change (section 8). +- Two places in PRD 0.2 read too strictly against the design. Section 10 + proposes wording for 0.3. + +## 1. Vikunja version and references + +- The adapter speaks `/api/v2` only. Its one exception is the version + check: R 1.7 names `GET /api/v1/info`, and the report found no v2 + equivalent. That call breaks at Vikunja 4.0 (R 3.3 item 3), so it sits + behind one function the adapter can swap. +- Version gate, fail closed: refuse below 2.4.0 (bots and v2 arrive + there, R 1.4) and refuse any major other than 2. A tested version is a + reviewed change to a constant, the same rule as the Pi pin. R 3.3 + item 14 found docs and source already disagreeing in two places, which + is the case for pinning. +- The tracker ref stays `vikunja:/` with numeric ids. + The project-scoped `index` (the "#12" a person sees) is display only. + It can change if a task moves projects. +- v2 lists come back as `{items,total,page,per_page,total_pages}` and + errors as RFC 9457 bodies, 422 for validation (R 1.1). The adapter maps + 422 to a refusal code and logs the problem body in `action.refused`, + never the request headers. + +## 2. Role files: Vikunja scopes as group-to-verb maps + +R 1.4: a token's `permissions` maps a route group to verbs (`read_all`, +`read_one`, `create`, `update`, `delete`, `_bulk` variants). `expires_at` +is required. The note's `["tasks.read", "tasks.update"]` was a guess and +is wrong. Corrected coder entry: + +```json +{ "service": "vikunja", + "scopes": { "tasks": ["read_all", "read_one", "update"], + "tasks_comments": ["read_all", "create"], + "projects_views": ["read_all"], + "projects_buckets": ["read_all"], + "projects_views_tasks": ["update"] } } +``` + +- `scopes` is an array for Gitea and an object for Vikunja. The + validator checks Vikunja group names against a frozen list for the + pinned version, taken from `/api/v1/routes` on 2.7.0 (R 1.4 says that + route lists the real set). An unknown group or verb refuses the role + file (REQ-ROLE-1). +- `projects_views_tasks` covers a bucket move if the adapter moves tasks + through the view route. If a `bucket_id` in the task PATCH is enough, + the group drops out. Unverified, probe P3. + +What each slice 1 action needs from Vikunja. Every role also gets the +read set: `tasks` read_all and read_one, `tasks_comments` read_all, +`projects_views` and `projects_buckets` read_all. + +| Action (note 1.4) | Roles | Vikunja group and verbs | +|---|---|---| +| `task.create` | pm | `tasks` create | +| `task.assign`, `task.reassign` | pm (reassign is cross-role for others) | `tasks_assignees` create, delete | +| `task.schedule`, `task.priority.change`, `task.close` | pm | `tasks` update | +| `task.update.assigned` | all four | `tasks` update, `tasks_comments` create, bucket move | +| relations (blocked-by, subtask) | pm | `tasks_relations` create, delete | +| labels | pm | `tasks_labels` create, delete | + +No role gets `projects` create or update, `projects_webhooks` or +`teams_members`. R 1.4: a project a bot creates belongs to the owner and +the bot gets admin on it, so a role that can create projects can also +reach admin on what it made. + +## 3. Bots, minting and who checks what + +Facts from R 1.2, 1.4 and 3.1: +- Tokens can't create users or bots, or mint tokens. Only a human JWT + can, from `POST /api/v2/login`. +- A bot belongs to a human owner, and deleting the owner deletes its + bots and their tokens. +- There's no instance admin without a paid licence (R 1.6), and the plan + doesn't use one (PRD, technical considerations). + +Design: +- One bot per role instance, named `bot--`, for + example `bot-mosaic-stack-coder`. A change of holder doesn't change the + bot (note 5.1 still holds). +- The owner is a dedicated account, not Jason's daily login. Jason's own + account is an ordinary user shared into the projects. That keeps the + owner password out of daily use. It's the one high-value Vikunja secret + (R 3.3 item 1). +- v1 doesn't mint (PRD out of scope; REQ-CRED-1). Jason creates the + owner, the bots, the project shares and the tokens by hand. The + share level is write for all four roles. The report's sequence (R 3.1) + is the runbook. The slice 1 brief should give it as numbered steps + with the exact scopes from section 2, because a token's permissions + can't be read back later. The `tokens` group is excluded from token + scopes (R 1.2 item 1), so the broker can't introspect its own token. +- So the broker checks scope by probe at startup (REQ-HARN-2). For each + role it makes one call that should succeed and one that should be + refused, for example the coder reads its project's tasks and tries + `tasks` create. The coder's create must fail. If it succeeds, the token + is broader than declared and the broker refuses to start that role. +- Bots can't read their own user record (`user_*` is excluded). The + business file therefore records each bot's username and numeric id + next to the credential reference. Assignment needs the id. +- A person must log in to Vikunja once before anyone can assign them + (R 3.3 item 11). The brief should list that as an install step for + Jason's account. + +Field ownership. Route groups can't say "assigned tasks only". A +`tasks: update` token edits any task its bot can see in a shared +project. The rule in REQ-TASK-1 is enforced by the broker verb, which +checks the assignee before it writes. Since the agent never holds the +token (REQ-CRED-1), that verb is the only path to a write, subject to +the same-OS-user caveat the PRD already records under risks. This is a +real loss against the note's claim in 1.4 that credential scope is the +hard line for task writes. It is still the hard line for whole groups: +the coder can't create, assign or relabel at all. + +The verbs need a field table, since PRD wording "only the assigned role +writes a task's mutable fields" would stop the PM from rescheduling an +assigned task: + +| Field | Writer | Action | +|---|---|---| +| title, description | pm at create; after assignment only through a resolved `task.scope.change` | `task.create`, `task.scope.change` | +| assignee | pm | `task.assign`, `task.reassign` | +| due date, priority | pm | `task.schedule`, `task.priority.change` | +| state (bucket), percent done | the assigned role, except `done` | `task.update.assigned` | +| `done` | pm, after a review verdict | `task.close` | +| relations, labels | pm | `task.create`, `task.schedule` | +| comments | any role on the task, append only | `task.update.assigned`, `message.send` | + +The requirement id (REQ-TASK-1) is not a label. R 1.4 says a bot edits +only the labels it created, and label changes fire no webhook (R 1.3). +The `task.created` event in `bus.sqlite` holds the authoritative +`requirement_ref`. The adapter also writes a last line +`Requirement: REQ-TASK-1` into the description for people to read. If a +person deletes that line, the event still holds the link. `create` +refuses an id that isn't in the PRD version the business file names. + +## 4. The broker holds tokens (REQ-CRED-1) + +This replaces the note's section 2 sentence "The launcher hands it to +the agent as a read-only mount or an environment variable". R 3.1 step 3 +says the same thing as my note did, and REQ-CRED-1 overrules both. + +- The resolver and launcher still only `stat` a reference (REQ-VAR-2). + The broker is the one component that reads token contents. It holds + them in memory, never logs them, never writes them, and never passes + them to a child that runs in an agent's workspace. +- A `{env: NAME}` reference names a variable in the broker's own + environment. The launcher starts every agent with an allow-listed + environment, so those names never reach it. Decision 47 item 6 already + puts the existing worker provider-key exposure in DEFERRED. This rule + stops new exposure. +- Agents reach Vikunja and Gitea only through typed tools in the launch + bundle (REQ-HARN-1). Each tool is one vocabulary action. The broker + stamps role and run from the launch record (REQ-DEC-3) and emits + `action.allowed` or `action.refused`. + +Coder `git push` is the hard case. Three options: +1. A git credential helper that asks the broker per push. Rejected. Git + prints the token to whoever calls `git credential fill`, and the agent + can run that itself. +2. The broker runs `git push` inside the agent's workspace with the + token on `GIT_ASKPASS`. Rejected. The workspace's `.git/hooks/pre-push` + and `.git/config` (`core.sshCommand`, `credential.helper`, + `core.hooksPath`) belong to the agent. A planted hook runs in the + broker's push with the askpass helper in reach. +3. Recommended: a `git.push.working` tool. The broker fetches the named + branch from the workspace into a bare repository it owns + (`/broker/git//.git`). It checks that the + ref is the role's working branch from the resolved variables. Then it + pushes from its own repository with hooks off + (`-c core.hooksPath=/dev/null`) and the token supplied only to that + process. The fetch is served by `git-upload-pack`, which runs no + hooks, and git ignores `uploadpack.packObjectsHook` in repository + config. Probe P8 should still prove it: plant a pre-push hook and a + `credential.helper` in a workspace and check that neither runs. + +Gitea branch protection on `next` and `main` stays the server-side line +(REQ-HARN-2, credential scope). The broker's ref check sits in front of +it. It isn't a replacement for it. + +Expiry and rotation (R 1.4, R 3.3 item 8; PRD technical considerations): +- Vikunja tokens carry a mandatory `expires_at`. Gitea tokens have no + expiry. Since the broker can't read either back, Jason records a date + on each reference when he creates the token: `expires` for Vikunja, + `rotateBy` for Gitea. A missing date refuses the reference. +- New event kinds, all with service and role instance, never the value: + `credential.expiring` (7 days ahead, goes in the daily digest), + `credential.expired` (the broker refuses that service for that role and + raises a blocking gated decision, so Jason gets a DM under REQ-DEC-4), + and `credential.changed`. The broker writes `credential.changed` when a + referenced file's inode, size or mtime changes, which is how a rotation + shows up without anyone reading the file twice. + +## 5. Sync (REQ-TASK-2) + +Polling is the truth and webhooks are hints (R 1.3, R 3.3 item 4). The +report found docs saying "no retries" while v2.7.0 source retries five +times from an in-memory bus. + +Incremental poll, every `tracker.pollSeconds` (default 30): +- `GET` the project's tasks with `filter=updated >= `, `sort_by=updated` + (then `id`, if the API takes a second sort key), ascending, `per_page=50` (R 1.3: the default cap is + `service.maxitemsperpage` 50). The exact v2 path comes from + `/api/v2/openapi.json` on 2.7.0. +- The cursor is `{T, seen}`: the last `updated` value and the ids already + handled at exactly that value. `>=` plus `seen` tolerates several tasks + sharing one timestamp. A strict `>` would drop a task updated in the + same second as the last one read. +- No offset paging across the range. A task updated mid-scan jumps to the + end, every later item shifts left by one, and page 2 skips one task. + Each request asks for page 1 from the current cursor instead. If 50 or + more tasks share one timestamp, the adapter pages within + `updated = ` alone, which holds still while the scan runs. +- `ratelimit.enabled` is off by default and 100 a minute when on (R 1.7). + One request per project per 30 seconds stays far under it. + +Full reconcile, every `tracker.reconcileMinutes` (default 60) and at +startup: +- List every task in the project, with `expand=comment_count`, and compare + with the last snapshot of each. +- A task that's gone becomes `task.missing`. R 3.4 couldn't confirm that + polling returns soft-deleted rows, so the adapter doesn't claim + "deleted". +- Label differences become `task.changed.external`. Label changes fire + no webhook, and whether they bump `updated` is unverified (probe P5). +- A comment count that moved means the adapter fetches comments. Whether + a comment bumps the task's `updated` is unverified (probe P5). + +Snapshots and who changed what: +- The adapter needs the last known state of each task to tell a change + from a non-change. I propose one more append-only table in + `bus.sqlite`, `task_snapshots` (seq, at, task_ref, updated, etag, + digest, fields JSON, source `self` or `poll`, role, run), with the same + four guards as the others. It isn't in the prototype yet. +- Every broker write stores the task the server returns as a `self` + snapshot. A polled version whose digest matches no `self` snapshot is a + person's edit: `task.changed.external` with the changed field names + (REQ-TASK-1). Comparing the digest, not only `updated`, catches a person + editing in the same second as a broker write. + +Webhooks: I recommend slice 1 ship none. A 30-second poll meets every +slice 1 need. A receiver brings an HMAC secret to hold, and +`outgoingrequests.allownonroutableips` on the instance (R 1.3). On path +A that's a setting on someone else's server (R 1.7). When a receiver +comes, it checks `X-Vikunja-Signature`, is idempotent, and does nothing +but start a poll early. + +## 6. Concurrency (6.8 stays) + +The report found ETag and If-None-Match on single GETs only, and nothing +on conditional updates (R 1.3, R 1.1). Decision 46 kept 6.8 +provisionally, and Sage's message keeps it as decided. The refinement: +1. GET the task with `If-None-Match: `. A + 304 means nothing changed since the broker last saw it. +2. On 200, compare `updated` and the digest with the caller's + `expectUpdated`. If they differ, refuse with `task.conflict` and write + nothing. +3. PATCH only the fields the action owns (section 3 table), never a full + PUT. A person's concurrent edit to a different field then survives. + v2 accepts PATCH (R 1.4), but its merge semantics are unverified + (probe P4). If PATCH turns out to replace the whole object, the + adapter keeps compare-then-write and the race covers every field. +4. Store the PATCH response as a `self` snapshot. + +The window between steps 2 and 3 remains. A person who edits the same +field in that gap loses the edit with no signal. One holder per role, +one writer per field and a sub-second window keep it rare. The brief +should say it exists and not claim the write is atomic. + +## 7. Installer and variables (REQ-TASK-3) + +New and changed keys (REQ-VAR-1). All are plain values, and the most +specific layer wins. + +| Key | Layers | Note | +|---|---|---| +| `tracker.kind` | business | `vikunja` only in v1 | +| `tracker.baseUrl` | business | | +| `tracker.project` | project | the Vikunja project id; the note had no project key | +| `tracker.pollSeconds` | business, project | default 30, minimum 10 | +| `tracker.reconcileMinutes` | business | default 60 | + +- Bucket titles are fixed in code, not a variable: `todo`, + `in-progress`, `in-review`, `blocked`, `done`. The adapter finds their + ids in the project's kanban view at startup. A missing title refuses, + and creating buckets is an install step, not an agent action. +- Path A, existing instance (R 1.7): base URL, the version gate from + section 1, an owner account with local login, the bots and tokens. + `allownonroutableips` stops being a prerequisite if slice 1 has no + receiver. +- Path B, bundled: `vikunja/vikunja:2.7.0`, pinned by full tag (better, + by digest), unmodified, no Vikunja code in the repository (REQ-TASK-3). + `VIKUNJA_SERVICE_SECRET` and the database password are runtime-only + (invariant 3). The owner comes from `vikunja user create` in the + container, then `VIKUNJA_SERVICE_ENABLEREGISTRATION=false`. +- The installer never writes the business file (REQ-ROLE-3). It prints + the block Jason pastes, with the reference paths and placeholders for + bot ids and dates, and validates the file he saves. + +Business file credential entry, revised from the note's section 2 (no +secret values): + +```json +"coder": { "definition": "coder", "vars": { "harness": "pi" }, + "tracker": { "bot": "bot-mosaic-stack-coder", "botId": 7 }, + "credentials": { + "gitea": { "file": "/abs/path/coder-gitea.token", "rotateBy": "2027-01-04" }, + "vikunja": { "file": "/abs/path/coder-vikunja.token", "expires": "2027-01-04" } } } +``` + +## 8. PRD answers that change the note + +REQ-LAUNCH-1 settles 6.6: +- The `pm` role file lists `role.launch` under `withinRole`. The verb + also requires a `launch` block in the business file. With no block, the + action is gated, so a business that never wrote the block fails closed: + + ```json + "launch": { "by": "pm", "instances": ["coder", "reviewer"], + "max": { "opus": 4, "sonnet": 4 } } + ``` +- "Role instances only" means `instances` must name roles the business + file declares. The PM can't launch a seat that holds no role, or itself. +- The broker counts live sessions per model family from `session.launched` + without a matching `session.ended`. It maps a model id to a family + through a reviewed table, and an unknown model refuses. A launch over a + limit is `action.refused` with reason `launch-limit`. +- `session.launched` gains `launched_by` (the human, or the PM's run) and + `model_family`. +- "Revocable with one word": the business file is Jason's and the stack + never writes it, so the stack can't revoke by editing it. Instead, a + CLI command started outside any agent run writes a `launch.revoked` + `human.input` event, and `launch.restored` reverses it. The verb checks + the latest of the two. My assumption is that the word stops new + launches. Ending a running session is a separate `mosaic stop + `. Sage should confirm that reading with Jason in round 3 if + it matters. + +REQ-DEC-4: +- `decisions` gains `blocking INTEGER NOT NULL CHECK (blocking IN (0,1))`. + The raiser sets it when its task can't move until the decision + resolves. A blocking decision should cite `task_ref`, so the field is + checkable. +- A gated decision with `blocking = 1` goes to the CLI inbox and a + Discord DM at once. Delivery is a `messages` row to the human plus + `deliveries` with transports `cli` and `discord-dm`. Every other item + waits for the daily digest. That's one message per day listing decision + ids, with a `digest.sent` event. +- The DM goes only to Jason, so it isn't external speech (hard limit B). + His Discord user id is a business variable, `human.discordUserId`. The + bot token stays with the connector. + +REQ-CLI-2: +- The note's business file sketch set `"harness": "t3"` for the PM. That + is wrong under REQ-CLI-2. Allowed values are `pi` and `claude-code`, + and the PM in this repository should start with `pi` (decision 47 item + 7). T3 can still be a message address on a claim. It isn't a harness. + +REQ-ROLE-4: Sage holds `pm` and I hold `cto`. A claim belongs to a run, +not a name, and REQ-CLI-2 means the product PM is a stack-launched +session, not Sage's T3 thread. During slice 1, a T3 seat's claim stays +advisory (decision 46, 6.3). The brief needs to say at which step Sage +moves to a stack-launched session and starts holding `pm` for real. + +REQ-TASK-4: the note's 5.2 row "Vikunja owns task state, the queue +mirrors it on claim and close" has the state field moving both ways. +Tightened: for a task with a queue row, the queue is the source for +in-progress, in-review and done. The adapter writes those to Vikunja one +way and never reads state back into the queue. A person's state change +in Vikunja becomes `task.changed.external` plus `task.conflict`, raised +to the PM as the note already says. For a task with no queue row, the +bucket in Vikunja is the only state. + +REQ-EVT-1: new kinds from this addendum are `credential.expiring`, +`credential.expired`, `credential.changed`, `task.missing`, +`launch.revoked`, `launch.restored` and `digest.sent`. + +## 9. Probes before the build relies on any of this + +Run against a scratch `vikunja/vikunja:2.7.0` with SQLite. No real +tokens are needed, since the scratch owner mints test ones: +- P1. Exact v2 bodies for bot create and project share (R 3.4). +- P2. A token created with `expires_at` in the past (R 3.4). +- P3. Whether a PATCH with `bucket_id` moves a task, or the view route + is needed. +- P4. Whether PATCH merges fields or replaces the task. +- P5. Whether a label change or a new comment bumps the task's `updated`. +- P6. Whether a task list ever returns soft-deleted tasks (R 3.4). +- P7. The scope probe from section 3: a coder token refused on + `tasks` create, and the refusal's status code. +- P8. The broker push from section 4: a planted pre-push hook and a + `credential.helper` in the workspace do not run. + +P1 to P7 need only a scratch container and its owner login. P8 belongs +to whoever builds the broker. + +## 10. Proposed PRD 0.3 wording + +- REQ-VAR-2: "The stack checks a referenced file with `stat` and never + reads its contents" contradicts REQ-CRED-1, because the broker has to + read a token to use it. Proposed: "Only the broker reads a secret's + contents. It holds them in memory and never logs or writes them. + Everything else checks a referenced file with `stat`." +- REQ-TASK-1: "Only the assigned role writes a task's mutable fields" + would stop the PM from rescheduling an assigned task. Proposed: "Each + task field has one writing role, set in the field table of the slice 1 + brief. The assigned role writes the task's state." That table is the + one in section 3. + +## 11. Open questions, each with a recommendation + +A1. **Coder push.** Recommendation: the broker push from section 4, +option 3. Branch protection on Gitea stays the server-side line. + +A2. **Webhooks in slice 1.** Recommendation: none. Poll every 30 +seconds and reconcile hourly. + +A3. **`task_snapshots` table.** Recommendation: add it, append-only with +the same guards. I can extend the prototype if Sage wants it run before +the brief. + +A4. **Probes P1 to P7.** Recommendation: one seat runs them on a scratch +container before the brief freezes the adapter interface. + +A5. **The word that revokes launching.** Recommendation: it stops new +launches, and `mosaic stop` ends a running one. Confirm with Jason only +if he meant otherwise. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 0c6c5a42..dcbb8e96 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -472,3 +472,4 @@ are never rewritten or removed; corrections are new entries. 2026-10-04T19:26:12Z | Filbert (T3 Claude Code, thread 9cb9731e) | meta-harness survey done (slice 1 step 8) | agents/filbert/work/meta-harness-survey-2026-10-04.md sha256 05f83807…0989; Pi tool_call is the only fail-closed hook of the three, so hard lines are credential scope, verbs, tool ceiling and container; no commits 2026-10-04T19:26:50Z | Sage (T3 Claude Code, thread 1ef1e4f8) | meta-harness survey received | Filbert's survey (05f83807) committed as a record; lead decision 47 rules on its section 8; two DEFERRED items (host-seat --approve, worker provider-key exposure) 2026-10-04T19:48:15Z | Sage (T3 Claude Code, thread 1ef1e4f8) | PRDY round 2, PRD 0.2 | lead decision 48; PRD draft 0.2 with requirement ids; Researcher's Vikunja and Pocket ID report (fcfc970f) committed as a record (Researcher wrote no SESSIONS line, per its limits) +2026-10-04T19:56:14Z | Sage (T3 Claude Code, thread 1ef1e4f8) | slice 1 addendum A | Darkwing's addendum (0b36acb0) committed as a record; lead decision 49; PRD draft 0.3; assigned Researcher Vikunja probes P1-P7 on a scratch container and Darkwing the task_snapshots prototype extension diff --git a/docs/plans/2026-09-26_lead-decisions.md b/docs/plans/2026-09-26_lead-decisions.md index e6affb1f..f0a62dbf 100644 --- a/docs/plans/2026-09-26_lead-decisions.md +++ b/docs/plans/2026-09-26_lead-decisions.md @@ -773,3 +773,33 @@ which stay with him. Each item names who decided it and what happened. (`agents/researcher/work/2026-10-04_vikunja-pocketid.md`, sha256 fcfc970f…) was committed as a record with this entry. The PRD's technical considerations draw on it. +49. **Sage's rulings on slice 1 addendum A (2026-10-04).** Source: + Darkwing (CTO), `agents/darkwing/work/slice1-data-model-addendum-2026-10-04.md` + (sha256 0b36acb0…). Where the addendum and the original note + disagree, the addendum wins. + - Vikunja token scopes cover whole route groups. So "one writing role + per field" is enforced by broker verbs, not by tokens. That works + only because agents never hold the tokens. + - A1, coder push: accepted. The broker fetches the branch into a bare + repository it owns and pushes from there with hooks off. Gitea branch + protection stays the server-side line. + - A2: no webhooks in slice 1. Poll every 30 seconds with a keyset + cursor and reconcile hourly. That removes the + `allownonroutableips` prerequisite. + - 6.8 stands, with an ETag check on GET and a PATCH of only the owned + fields. The brief will say the race window is still there. + - A3: accepted. A `task_snapshots` table, `decisions.blocking` and the + new event kinds. Darkwing extends the prototype before the brief. + - A4: accepted. Researcher runs probes P1 to P7 against a scratch + `vikunja/vikunja:2.7.0` container with SQLite, bound to + 127.0.0.1, removed afterwards, test tokens only. P8 goes to whoever + builds the broker. + - A5: the one word stops new launches, and `mosaic stop` ends a running + session. Sage reads Jason's "revocable with one word" that way. If he + meant otherwise, he says so. + - The PM's `role.launch` needs a `launch` block in the business file. + Without that block the action is gated. + - The PM moving from T3 to a session the stack launches is a named step + in the slice 1 brief (REQ-CLI-2). + - PRD wording for REQ-VAR-2 and REQ-TASK-1 adopted in draft 0.3, with + REQ-TASK-2 updated for A2. diff --git a/docs/prd/mosaic-stack.md b/docs/prd/mosaic-stack.md index 58eff9ce..cfc4c117 100644 --- a/docs/prd/mosaic-stack.md +++ b/docs/prd/mosaic-stack.md @@ -1,13 +1,13 @@ # PRD: Mosaic Stack -- Status: draft, version 0.2. Jason approves it, and once approved it is +- Status: draft, version 0.3. Jason approves it, and once approved it is never edited in place. Changes after approval are a new version. - Owner: Jason. Sage writes it from the PRDY interview. - Template: PRDY "software" (`v1/packages/prdy/src/templates.ts`), filled by hand until PRDY is ported. - Interview record: Sage's thread 1ef1e4f8. Round 1 is lead decision 45. Round 2 is lead decision 48. Design inputs are lead decisions 43, 44, - 46 and 47. + 46, 47 and 49. ## Introduction @@ -111,9 +111,10 @@ parent requirement is refused. allowed in. - **REQ-VAR-2.** `~/.config/mosaic-dev/config.json` stays the only system config, and nothing writes it automatically. A secret appears only as a - reference, either a file path or an environment variable name. The - stack checks a referenced file with `stat` and never reads its - contents. + reference, either a file path or an environment variable name. Only + the broker reads a secret's contents. It holds them in memory and + never logs or writes them. Everything else checks a referenced file + with `stat`. ### Credentials @@ -153,11 +154,13 @@ parent requirement is refused. ### Tasks - **REQ-TASK-1.** Tasks live in Vikunja, reached through `/api/v2` with a - bot token for each role. Each task cites a requirement id. Only the - assigned role writes a task's mutable fields. A person's edits come in - as events. + bot token for each role. Each task cites a requirement id. Each task + field has one writing role, listed in the slice 1 brief's field table, + and the assigned role writes the task's state. Vikunja's token scopes + cover whole route groups, so the broker's verbs enforce this, not the + tokens. A person's edits come in as events. - **REQ-TASK-2.** Polling for changes since the last check is the source - of truth, and webhooks only trigger a check sooner. + of truth, with an hourly full reconcile. Slice 1 uses no webhooks. - **REQ-TASK-3.** The installer offers two choices: point at an existing Vikunja, or deploy the bundled one. The bundled one is the unmodified upstream image. No Vikunja code enters the repository.