|
|
|
@@ -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:<projectId>/<taskId>` 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-<business>-<instance>`, 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
|
|
|
|
|
(`<dataRoot>/broker/git/<business>/<project>.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 >= <T>`, `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 = <T>` 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: <etag from the last snapshot>`. 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
|
|
|
|
|
<instance>`. 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.
|