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 <[email protected]>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user