Files
stack/agents/darkwing/work/slice1-s1/build.md
T
jason.woltjeandClaude Opus 5.5 b851f83c39 docs(slice1): row 36 S1 build packet, v3a trigger count correction (darkwing)
S1 candidate for Filbert's review on #1518: build.md, build.patch and
build-manifest.sha256 (34 files at base fef4b362). The candidate itself
is not committed.

proto-v3a-notes-correction: the schema has 36 triggers; the printed 35 is
counted after the tamper check's DROP TRIGGER. Found by Rocko.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 22:28:30 -05:00

154 lines
7.9 KiB
Markdown

# Row 36, S1: roles v2, business and project files, variable layers
Darkwing, 2026-10-04. Issue #1518, reviewer Filbert. Brief:
`docs/plans/2026-10-04_slice-1.md`, section "Slice 1 S1". Design:
`agents/darkwing/work/slice1-data-model-2026-10-04.md` sections 1 and 2,
addendum A sections 7 and 8, addendum B section 2. Nothing in the
candidate is committed, staged or pushed.
Base is fef4b362 (origin/refactor at build time). In a fresh clone at that
commit the patch applies, the result matches the manifest 34/34, and
`node --test packages/business/tests/` passes 57/57 in three runs.
## Files
`build.patch` (sha256
`8067882052abc6e75684b318f738ef990f53c7a35913a8313a341282d259bd94`) changes
4 files and adds 30. `build-manifest.sha256` (sha256
`d934e5af30cd646194bbf1d9bffc2b4d51486a4bb4c097ec43376352830b0195`) pins all
34 after the patch.
New:
- `roles/pm.json`, `cto.json`, `coder.json`, `reviewer.json`: version 2
definitions. `roles/pm.md`, `cto.md`, `coder.md`, `reviewer.md`: their
duty contracts (Duties, Authority, Protocol).
- `packages/business/src/`: `vocabulary.mjs` (the closed lists),
`role.mjs` (versions 1 and 2), `vars.mjs` (the key registry and merge),
`credentials.mjs` (reference parse and stat checks), `business.mjs`,
`project.mjs`, `resolve.mjs` (`resolveInstance`, `classify`), `cli.mjs`,
`util.mjs`, `errors.mjs`, `index.mjs`.
- `packages/business/tests/`: 57 tests in 7 files plus `helpers.mjs`.
- `packages/business/examples/mosaic-stack.example.json`: the business
file for Sage's SR template. It refuses as shipped (every `botId` is 0,
every date `YYYY-MM-DD`); a test proves it validates once filled in.
- `packages/business/README.md`, `package.json`.
Changed:
- `scripts/mosaic-task.mjs`: `validateRole` delegates to the package, so
both versions share one validator. `resolve-role` also prints
`MOSAIC_ROLE_CONTRACT=<absolute path>` for a version 2 role.
- `scripts/test-task.sh`: 8 new checks (version 1 prints no contract line;
the four shipped roles resolve with their contracts; a minimal version 2
role passes; unknown action, gated-only action, a Vikunja verb no role
may hold, a missing contract and a linked contract each exit 2).
- `scripts/mosaic`: a `business` branch, like `queue`. This is outside the
brief's file list. The brief asks for a TOOLS entry for any new command,
and a command needs an entry point; the change is four lines.
- `docs/TOOLS.md`: a section for `scripts/mosaic business`, and the
`resolve-role` row now says it takes versions 1 and 2.
## Does the vocabulary go under `contracts/`?
No. `contracts/` is baked into the worker image because workers read it.
Nothing in a worker reads the action vocabulary: the broker classifies
actions on the host, and the role files are read on the host by the
launcher and the broker. Baking it in would give the image a copy that
nobody checks against. The vocabulary lives in
`packages/business/src/vocabulary.mjs`, and a change to it is a reviewed
commit, like a role file. `Containerfile` copies `package.json`,
`contracts`, `src` and `adapters` only, so neither `packages/` nor
`scripts/mosaic-task.mjs` is in the image, and the new import doesn't
reach it. No contracts change, so no review by Sage for one.
## Choices and deviations from the design
1. `contract` is a file name in the role file's own directory
(`"coder.md"`), not the note's `"roles/coder.md"`. A path relative to
the repository breaks under `MOSAIC_ROLES_DIR`, which `agent.sh` and
the tests use for sandbox role directories. The validator refuses a
name with a slash, a non-`.md` name, a missing, empty or linked file.
2. pm authority adds three actions to the note's table (section 1.2):
- `task.update.assigned` (within): addendum A section 2 says all four
roles update the tasks assigned to them, and the note's pm row left
it out.
- `task.reassign` (within): addendum A's action table gives
reassign to pm, cross-role for the others.
- `task.scope.change` (cross): pm splits and rewrites tasks, and a
scope change on one in technical work needs the CTO, the technical
arbiter. Filbert, push back if you read it differently.
3. The business file must belong to the user and not be group or other
writable. It holds authority, so a file someone else can edit would let
them widen it. Missing file: exit 4. A linked file: exit 4.
4. Bot names and bot ids are unique across role instances and the sync
bot. Two instances sharing one Vikunja bot would make the tracker's
record of who did what wrong.
5. `role.launch` stays within-role only for the instance the business
file's `launch.by` names. Everywhere else the resolver removes it, so
`classify` returns gated. With no `launch` block nobody launches.
6. `limits.authority` is an allowlist. A role action it doesn't name
becomes gated. It can't add an action the definition lacks.
7. Credential checks use `lstat` and `realpath` only. A test makes a token
file mode 0200 (write-only) and the check still passes, which shows the
package never opens it. A token inside the repository, `dataRoot` or a
project root refuses, also through a linked directory. Vikunja: past
`expires` refuses, within 7 days warns. Gitea: past `rotateBy` warns;
Gitea tokens don't expire on their own. An `env` reference that isn't
set warns, because the launcher sets it.
8. Lookups use `Object.hasOwn`. Ids may be any lowercase name, and
`constructor` is one, so `roles[name]` would have treated it as
declared. Tests cover arbiters, launch, the model family and a label
titled `__proto__`.
9. `validate` treats a declared project with no project file as a
warning ("absent"). `resolve --project` treats it as a missing file,
exit 4. When a project file exists, `validate` resolves every instance
against it, so a project file naming an undeclared instance refuses
there.
10. Error text for version 1 roles now comes from the package. Every
existing `test-task.sh` case still passes; only wording changed.
11. The package says "role file" for `roles/<name>.json`, so it doesn't
share a name with the Markdown contract. `mosaic-task.mjs` and
`agent.sh` keep their old wording ("role contract"); I didn't touch
those messages.
## Tests and suites
Node 26.8.1 on the host, outputs teed:
- `node --test packages/business/tests/`: 57/57. Files: role 12, vars 6,
credentials 6, business 14, project 2, resolve 8, cli 9.
- `scripts/test-config.sh` 24/24, `scripts/test-task.sh` 98/98,
`scripts/test-conductor.sh` 17/17, `scripts/test-queue.sh` 27/27
(`scripts/mosaic` changed).
Node 24.21.0 in the `node:24` image, no network, the clone mounted
read-only: `node --test "packages/business/tests/*.test.mjs"` 57/57. The
glob is needed because Node 24 reads `node --test tests/` as a module
path and fails. That is true of every package in the repository; I
checked `packages/queue` the same way. Follow-up for Sage, not part of
S1.
Mutants, each run against the full test set and each caught: the
forbidden-root check off (3 failures), the `role.launch` gate off (1),
the token mode mask widened to other only (1), `hasOwn` dropped from the
instance lookup (2), the business file mode check off (1), limit
intersection replaced by overwrite (2).
The CLI tests point `MOSAIC_CONFIG`, `HOME` and `MOSAIC_ROLES_DIR` into a
temp directory. No test reads the real `~/.config`.
## For other rows
- S2 (Rocko) consumes `loadBusiness` and `resolveInstance`. The frozen
API is in `packages/business/README.md`, section "API".
- S3 (mine) opens token files, so it repeats the stat checks at open
time and trims one trailing newline.
- S6 decides how a launcher passes `MOSAIC_ROLE_CONTRACT` to the session.
`agent.sh` reads only `MOSAIC_ROLE_TOOLS` today and ignores the new
line.
## Follow-ups, not in this candidate
- `node --test <dir>/` fails on Node 24 in every package (above).
- `scripts/mosaic-task.mjs` keeps its own `SUPPORTED_TOOLS` for tasks.
The package has the same list in `TOOLS`. One should import the other
in a later change.