P3 · M1 Events in¶
Running since 2026-10-29 · milestone M1 of step P3, Tasks 1–7 of P1 · Task 1 done on autopilot/P3-t01 (workspace and the spool event contract), PR #73 open into autopilot/integration · Task 2 done on autopilot/P3-t02 (the SQLite store), its PR not open yet · their review-code: approve after 3 rounds (Task 1) and 4 rounds (Task 2) · Tasks 3–7 to do

One Progress page per milestone, opened with its first PR and closed with its done proof (P2 §0). This page covers M1, "Events in" (P2 §1).
Tasks¶
| Task | P1 | Depends on | Branch | Status |
|---|---|---|---|---|
| 1 · Workspace and the spool event contract | Task 1 | none | autopilot/P3-t01 |
done, PR #73 open; CI product passed on 41f0628, before the last code changes; on the new head: pending |
| 2 · SQLite store and its discipline | Task 2 | 1 | autopilot/P3-t02 |
done; its PR into autopilot/integration is not open yet |
| 3 · Spool reader (§15.3 req. 6) | Task 3 | 1 | to do | |
| 4 · Projector, run state and Nextflow mapping | Task 4 | 2, 3 | to do | |
| 5 · Snakemake mapping | Task 5 | 4 | to do | |
6 · nf-spectio writes the spool |
Task 6 | 1 | to do | |
| 7 · Snakemake logger plugin writes the spool | Task 7 | 1 | to do |
- Task 1
- Task 2
- Task 3
- Task 4
- Task 5
- Task 6
- Task 7
- M1 done proof: the
uv run pytest … -vcommand of P2 §1 item 3, then the bridge suites with-m engines
What changed¶
Task 1 · Workspace and the spool event contract¶
Branch autopilot/P3-t01, base 9e08a3e, head b730180 before this page; 5 commits, 31 files, +4250 −6. The behaviour is in api §14.1. This list names the files.
- The uv workspace, the first product code (architecture §6):
- root
pyproject.toml(not a package): memberspackages/*; dev tools ruff, mypy (strict, Pydantic plugin), pytest and import-linter;uv.lock. - Three packages:
packages/spectio-sdk,packages/spectioandpackages/spectio-server. The last two are empty, apart from__init__.pyandpy.typed. - Three import-linter contracts: the runner (
spectio) never imports the server;spectioimports neither FastAPI nor SQLAlchemy;spectio_sdkimports none ofspectio,spectio_server, FastAPI or SQLAlchemy. - ruff
TID251bans dynamic imports inpackages/spectio(importlib.import_module,importlib.__import__,importlib.util.spec_from_file_location,runpy).packages/spectio/tests/test_static_imports.pyalso refuses__import__, which ruff does not resolve.
- root
spectio_sdk.events(packages/spectio-sdk/src/spectio_sdk/events.py, 344 lines):SpoolEvent, the envelope, with no extra key allowed.parse_data(event), which returns the allow-listed model of the event's type, or raisesUnknownEventType.- The caps
MAX_LINE_BYTES(65 536),ROTATE_BYTES(32 MiB),MAX_FILE_BYTES(64 MiB) andMAX_MESSAGE_CHARS(4096). - Hiding the quoted command in
snakemake.errortexts. - Dropping the never-stored keys at any depth, inside free-form values (
resources,rulegraph) too: added after the doc review, test-first (5 more tests); then wildcard names are kept as the rule declares them (SEC-P3T1-12, 1 more test).
- Tests:
packages/spectio-sdk/tests/test_spool_events.py(43 tests) andpackages/spectio/tests/test_static_imports.py(1). - Fixtures:
tests/fixtures/engine-events/convert_t2.pyconverts the spike T2 bridge captures (spikes/engine-events/fixtures/) into 9*.spool.jsonlfiles (2375 lines). It refuses to write when a capture matches a credential pattern.tests/slurm/test_fixture.pynow scans these 24 files (the 9 spool files and the 15 captures) for credentials too. - CI: a
productjob in.github/workflows/ci.yml:- setup-uv pinned by SHA, with the
UV_VERSIONandUV_SHA256ofdev/versions.env; - then
uv sync --lockedonPYTHON_VERSION,ruff check,ruff format --check,mypy packages,lint-importsandpytest; - it is in
ci-ok'sneeds.
- setup-uv pinned by SHA, with the
- Specs:
- api §14 (new): it owns the ingest contract, with its own version. Only the driven-run part, §14.1, is written. It covers the file naming, the three caps, the envelope, one allow-list per Nextflow hook and Snakemake event, the fields never stored, and "dropped and counted, never executed".
- api §8.2:
source_seqis monotonic per (run, launch, producer), no longer per producer and run. - architecture §13.1: the forwarder asks for the highest
source_seqper (run, launch, producer). Dated note. - dev-env §11: the
ci.ymlrow names theproductjob. - dev-env §15.3 req. 6: it now links api §14.1 as the owner of the format, the caps and the allow-lists.
- Decision log: 2026-10-29 — api §14 driven-run part written (inbox #36 a), with what changed after review round 1.
- Review record:
docs/reviews/code/P3-t01.md. - Docs of this page: this page, the
p3-t01-spoolfigure (tools/diagrams/p3_t01_spool.py), and its entry indocs/assets/media.yaml.
Task 2 · SQLite store and its discipline¶
Branch autopilot/P3-t02, base 00a7059, head af8b4bf before this page; 5 commits, 11 files, +1950 −2. The behaviour is owned by api §7 (the tables, the SQLite version rule) and architecture §7.2 (one writer, the refusal of a network file system). Where the database lives: dev-env §15.3, "Where data lives", and req. 5 for the file modes. No spec changed in this task. This list names the files.
- The schema (
packages/spectio-server/src/spectio_server/store/schema.py, 342 lines): 15 tables as SQLAlchemy Core metadata:principals,projects,environments,runners,workflows,workflow_versions,runs,run_inputs,launches,runner_commands,tasks,attempts,events,spool_cursorsandspool_drops.- Text primary keys, JSON (
JSONBon Postgres), and CHECK constraints namedck_<table>_<column>instead of enums. A CHECK value must match[a-z_-]+before it is put into SQL. UTCDateTimeon every timestamp: an aware value is stored in UTC, a naive one is refused, and a value reads back withtzinfo=UTC.principals.kindhas the five kinds of api §6:user,service,agent,runnerandsystem.
- Text primary keys, JSON (
- The migration (
packages/spectio-server/src/spectio_server/migrations/): Alembic, one revision,0001_initial_store(461 lines). It runs online only, on the connection thatopen_dbpasses in, in batch mode for SQLite. store/db.py(309 lines):check_sqlite_version(),default_db_dir(),open_db(),fs_type()andUnsafeDatabase. The figure above shows the checks; in the code the canonical path (4) is resolved before the parents (3) are walked.- At creation:
journal_mode=WALandauto_vacuum=INCREMENTAL. - On each connection: the pins,
busy_timeout=5000,foreign_keys=ON. SQLAlchemy, not pysqlite, emitsBEGIN.
- At creation:
store/writer.py(72 lines):Writer.submit(fn) -> Future. One thread (spectio-writer) and one connection run each job in its ownBEGIN IMMEDIATEtransaction, in submit order.close()finishes the queued jobs. If the thread dies, the job it ran and every queued job fail with an error instead of waiting.- Dependencies:
packages/spectio-server/pyproject.tomladdsalembic>=1.13,<2andsqlalchemy>=2.0,<3;uv.lockresolves Alembic 1.20.1 and SQLAlchemy 2.1.6. - Tests:
packages/spectio-server/tests/test_store.py, 47 tests (464 lines). - Review record:
docs/reviews/code/P3-t02.md. - Docs of this task: this section, the
p3-t02-storefigure (tools/diagrams/p3_t02_store.py), and its entry indocs/assets/media.yaml.
Differences from the plan's interface (CR-P3T2-08)¶
P1 Task 2 gives the interface. The code differs from it as follows. The plan's text is to be updated in the next /apply-findings pass, tracked by inbox #75.
| Plan | Code | Why |
|---|---|---|
fs_type(path, mounts=Path("/proc/self/mounts")) |
fs_type(path, mountinfo=Path("/proc/self/mountinfo")) |
mountinfo gives each mount's device, so stacked mounts can be told apart (SEC-P3T2-03) |
open_db(db_dir) |
open_db(db_dir, mountinfo=...) |
the tests pass a fake table |
| the longest mount prefix decides | the last mount whose device is the directory's st_dev decides, the top of a stack; when no device matches (btrfs subvolumes report their own), the last of the longest mount points |
SEC-P3T2-03 |
the refused types: nfs, nfs4, lustre, gpfs, beegfs, cifs, smb3, ceph, fuse.sshfs |
the same, plus autofs |
an automount point that has not mounted is no proof of a local disk (SEC-P3T2-03, SEC-P3T2-04) |
| no rule for a missing table | on Linux, a mountinfo that cannot be read, or that names no mount for the path, is refused (fail closed); elsewhere (macOS) the type is unknown and accepted |
SEC-P3T2-04 |
open_db checks the file system and the SQLite version |
open_db also requires: |
SEC-P3T2-01, -02, -09, -13 |
| the directory is 0700 and owned by the user, checked on the opened directory, with no fallback | ||
every parent up to / is owned by root or the user and not writable by group or others: OpenSSH's StrictModes rule (misc.c safe_path, read 2026-10-30), plus an exception for sticky directories such as /tmp |
||
| the parent path is canonicalised once, and only that path is used afterwards | ||
spectio.db is created at 0600; spectio.db, -wal and -shm must be regular files of the user with no group or other bits |
||
the device and inode of the directory and of spectio.db are pinned, and compared on every new connection before SQLite reads anything |
||
$TMPDIR or /tmp |
$TMPDIR is used only when it is absolute and passes the same owner and mode rule; else a warning and /tmp; an unsafe /tmp raises UnsafeDatabase naming --db-dir |
round 1 and 2 security minors |
created is true when the database was created |
also true when the file has no Alembic revision (an interrupted first start), so boot rebuilds it | round 1 code minors |
Why¶
Prompt: next-phase.md · P3. Step: P3 in docs/playbooks/pipeline.yaml (wave W5, kind: build). P2 §1 runs Task 1 alone first, because it creates the workspace and the CI job that every later task uses.
- The contract comes before its code. Inbox #36 (a) let api §14 own the ingest contract and left its driven-run part for a later pass (decision log). dev-env §15.3 req. 6 needs that format to say what "validated and size-capped" means. P1 Task 1 writes it, as the spec of the first product code.
- Allow-lists, not deny-lists. Each event type keeps only the keys it lists. A field that a newer bridge adds is dropped until the list names it. This applies P1 Assumption 3: no
script,envor command line is stored (assumed — pending Thomas, inbox #55). - The numbers (64 KiB line, rotation at 32 MiB, 64 MiB per file) are P1 Assumption 12 (assumed — pending Thomas, inbox #28).
Task 2 runs after Task 1, in one of four parallel worktrees: 2 → 4 → 5, beside 3, 6 and 7 (P2 §1). The projector (Task 4) needs it, and Task 8 (M2) may start once it is merged.
- The database is an index, not the source of truth. It lives in a node-local temporary directory, and boot rebuilds it from the run directories when it is new (dev-env §15.3, "Where data lives", (assumed — pending Thomas, inbox #64)). Hence
default_db_dir(), and thecreatedflag ofopen_db. - A shared temporary directory is hostile ground. Another user can create
spectio-<uid>first, or swap a directory or a link between a check and a use. The checks of the figure answer the security review's majors: SEC-P3T2-01, -02, -03, -09 and -13 (record). - SQLite needs one writer and a local disk (architecture §7.2, which cites sqlite.org): one writer with
BEGIN IMMEDIATE, a refusal on a network file system, and the WAL-reset fix of api §7.
Evidence¶
Task 1 · Workspace and the spool event contract¶
Files changed by Task 1, against its base:
$ git log --oneline 9e08a3e..b730180
b730180 docs(reviews): code and security review record of P3 Task 1
3e5342b refactor(sdk): drop a repeated model config and a duplicate fixture walk
c69bccb fix(sdk): bound and widen the command hiding in error texts
3be01c1 fix(sdk): harden the spool event contract after review round 1
9fb1d97 feat(sdk): spool event contract (api §14, driven runs)
$ git diff --stat 9e08a3e b730180 | tail -1
31 files changed, 4250 insertions(+), 6 deletions(-)
Re-run on f456646 (the last code commit of the branch), in a cloud session (uv 0.11.32, Python 3.13.14 from dev/versions.env). An earlier run by doc-writer, on b730180, had 38 tests:
$ uv sync --locked --python 3.13.14
Resolved 27 packages in 2ms
Checked 25 packages in 0.28ms
$ uv run --locked pytest -q
............................................ [100%]
44 passed in 0.37s
$ uv run --locked ruff check .
All checks passed!
$ uv run --locked ruff format --check .
7 files already formatted
$ uv run --locked mypy packages
Success: no issues found in 6 source files
$ uv run --locked lint-imports
The runner never imports the server (architecture §6) KEPT
spectio has no FastAPI and no SQLAlchemy (architecture §6) KEPT
spectio-sdk imports neither spectio, spectio_server, FastAPI nor SQLAlchemy
(architecture §6) KEPT
Contracts: 3 kept, 0 broken.
$ python3 -m unittest discover -s tests/slurm -p 'test_*.py'
Ran 4 tests in 0.188s
OK
These are the commands of the product job, plus the unit job that carries the extended fixture guard. The 44 tests (43 in test_spool_events.py, 1 in test_static_imports.py) include 15 poison-line cases (test_poison_lines_refused). test_t2_fixtures_validate parses every line of the 9 spool files.
$ wc -l tests/fixtures/engine-events/*.spool.jsonl | tail -1
2375 total
CI: on PR #73, the product job passed on 41f0628 (job 123791515502), which predates the code changes of 6d50d3c and f456646. Its result on the new head is pending.
Review (record):
- Round 1, on
9fb1d97:code-reviewer: changes (0 blockers, 2 majors, 4 minors);security-reviewer: changes (0 blockers, 1 major, 5 minors).
- The majors, fixed in
3be01c1:- CR-P3T1-01: the scope of
seqdiffered between §14.1 and §8.2 / §13.1. - CR-P3T1-02 / SEC-P3T1-01:
snakemake.error.msgstored the failed shell command,exportvalues included, in 5 fixture events.
- CR-P3T1-01: the scope of
- Round 2, on
3be01c1: both approve. Its minors CR-07, CR-08, SEC-07 and SEC-10 were fixed test-first inc69bccb. code-simplifier, onc69bccb: commit3e5342b, −12/+3. The 38 tests of that head pass after it.- Doc review of PR #73: block on DR-P3T1-01 (denylisted keys kept inside free-form values), fixed in
6d50d3c. - Round 3, on
7106465(security only): approve. SEC-P3T1-12 fixed inf456646; SEC-P3T1-11 deferred to Task 5.
CODE step=P3-t01 review=approve b/M/m=0/2/6 security=approve b/M/m=0/1/11 rounds=3 simplify=changed -12/+3 tests=pass
Exit criterion. P3 is done when M1 to M8 are done (P2). For M1, the proof is the command of P2 §1 item 3, which needs Tasks 2–7. Task 1's own check is P1 Step 4: uv run pytest packages/spectio-sdk/tests/test_spool_events.py -v passes (above, within the 44) and the strict docs build is clean (below).
Docs gate, run after writing this page:
$ python3 tools/docs/check.py
build: progress hook output checked on 3 pages
mermaid: 5 blocks checked
diagrams: 34 scripts re-run, 68 SVGs compared
media: 162 outputs in the manifest
progress: 49 pages checked
docs gate: 0 failure(s)
$ uvx --with mkdocs-material mkdocs build --strict -d <scratch>/site-t01
INFO - Documentation built in 4.57 seconds
$ echo $?
0
Task 2 · SQLite store and its discipline¶
Files changed by Task 2, against its base:
$ git log --oneline 00a7059..af8b4bf
af8b4bf refactor(server): simplify store
d140832 fix(server): store review round 3
c4a7e2f fix(server): store review round 2
36796dd fix(server): store review round 1
2db4559 feat(server): SQLite store with one writer
$ git diff --stat 00a7059 af8b4bf | tail -1
11 files changed, 1950 insertions(+), 2 deletions(-)
Re-run by doc-writer on af8b4bf, in a cloud session (uv 0.11.32, Python 3.13.14 from dev/versions.env; the SQLite that Python loads is 3.53.1):
$ uv sync --locked --python 3.13.14
Resolved 31 packages in 2ms
Checked 29 packages in 0.40ms
$ uv run --locked pytest -q
........................................................................ [ 79%]
................... [100%]
91 passed in 1.50s
$ uv run --locked ruff check .
All checks passed!
$ uv run --locked ruff format --check .
15 files already formatted
$ uv run --locked mypy packages
Success: no issues found in 14 source files
$ uv run --locked lint-imports
The runner never imports the server (architecture §6) KEPT
spectio has no FastAPI and no SQLAlchemy (architecture §6) KEPT
spectio-sdk imports neither spectio, spectio_server, FastAPI nor SQLAlchemy
(architecture §6) KEPT
Contracts: 3 kept, 0 broken.
$ uv run --locked pytest packages/spectio-server/tests/test_store.py -v | tail -1
============================== 47 passed in 1.01s ==============================
The 91 tests are the 44 of Task 1 and the 47 of test_store.py. Among the 47:
test_writer_serialises: 50 concurrent submits give 50 rows, with noSQLITE_BUSY;test_refuses_network_fs:nfs4,lustre,gpfs,autofs, andautofsundernfs4;test_refuses_old_sqlite: 5 versions without the fix;test_swapped_database_refused_on_connectandtest_symlink_inside_db_dir_is_resolved_once: the swaps of SEC-P3T2-09 and -13.
CI: the product job runs when the PR is opened; no result yet.
Review (record):
- Round 1, on
2db4559:code-reviewer: changes (0 blockers, 2 majors, 4 minors);security-reviewer: changes (0 blockers, 3 majors, 5 minors).
- The round-1 majors, fixed in
36796dd:- CR-P3T2-01:
principals.kindlackedrunnerandsystem; - CR-P3T2-02: timestamps were not UTC both ways;
- SEC-P3T2-01: the database files took the umask, not 0600;
- SEC-P3T2-02:
open_dbtrusteddb_dirand a symlinked database file; - SEC-P3T2-03: stacked mounts and
autofsdefeated the file system check.
- CR-P3T2-01:
- Round 2, on
36796dd: both changes. The majors were fixed inc4a7e2f:- CR-P3T2-07: no test caught a drift between the migration's CHECK constraints and the metadata;
- SEC-P3T2-09: the directory could be swapped after the checks; now the parents are walked and the directory and file are pinned.
- Round 3, on
c4a7e2f:code-reviewerapproves;security-reviewerchanges:- SEC-P3T2-13 (major): a symlink in the middle of the path; fixed in
d140832by canonicalising the path once; - SEC-P3T2-14 (minor): the overflow uid is now named in messages.
- SEC-P3T2-13 (major): a symlink in the middle of the path; fixed in
- Round 4, on
d140832(security only): approve. code-simplifier, ond140832: commitaf8b4bf, −16/+9. The tests pass after it (above).- Four rounds, not two: this broke the rule. The
review-codeskill and autopilot.md set the stepblockedwith an inbox entry after two rounds. Rounds 3 and 4 ran instead, to fix two security majors (SEC-P3T2-09, -13) inside this step; Thomas has given no exception for this. Recorded in inbox #75.
CODE step=P3-t02 review=approve b/M/m=0/3/6 security=approve b/M/m=0/5/9 rounds=4 simplify=changed -16/+9 tests=pass
Exit check. P1 Task 2's Step 4: uv run pytest packages/spectio-server/tests/test_store.py -v passes (47 passed, above).
Docs gate, run after writing this section:
$ python3 tools/docs/check.py
build: progress hook output checked on 3 pages
mermaid: 5 blocks checked
diagrams: 35 scripts re-run, 70 SVGs compared
media: 166 outputs in the manifest
progress: 49 pages checked
docs gate: 0 failure(s)
$ uvx --with mkdocs-material mkdocs build --strict -d <scratch>/site-t02
INFO - Documentation built in 4.67 seconds
$ echo $?
0
Figures and media¶
-
p3-t01-spoolsketch, light and dark, at the top of this page. It is drawn bypython3 tools/diagrams/p3_t01_spool.py, fromevents.py, api §14.1 and the test run above. It shows:- a bridge appending to the spool;
- the file naming and the rotation;
- the envelope and the allow-list;
- the reader's drop reasons, shaded because the reader is Task 3;
- the 15 T2 captures to scale: the 9 converted, and the 6 Tower captures that are not converted.
The PNGs were rendered with Excalifont, the file whose sha256
tools/diagrams/FONT-LICENSE.mdgives. - No cast and no screenshot. Task 1 adds no CLI flow and no screen (documentation-system §3.1,build). The first cast comes with Task 8 (spectio init, M2). -p3-t02-storesketch, light and dark, second at the top of this page. It is drawn bypython3 tools/diagrams/p3_t02_store.py, fromdb.pyandwriter.pyataf8b4bfand the test run above. It shows the eight checks as pictograms (in the code, 4 runs before 3): - the base,$TMPDIRor/tmp; - the private directory/tmp/spectio-<uid>, 0700; - the parents, up to/; - the path, resolved once; - the file system type, with the refused types; - the three files at 0600; - the pins, checked on each connection; - the one writer thread.Under them: the SQLite version check that comes first, and the
createdflag that comes last. The PNGs were rendered with Excalifont, the file whose sha256tools/diagrams/FONT-LICENSE.mdgives (the.woff2source checked:e4423b31…de01c1b48). - No cast and no screenshot for Task 2 either: a library, with no CLI flow and no screen.
Findings and open questions¶
Deferred from Task 1's review (record):
| Id | What | Goes to |
|---|---|---|
| SEC-P3T1-04 | The reader's rules: file names that must match the grammar, wrong_launch, a cap on partial lines, a cap on the number of files |
Task 3, which builds the reader; handed to its implementer with the record |
| SEC-P3T1-06 (rest) | A CI check of the import contracts in a fresh environment; today they are static only | Task 3 |
| SEC-P3T1-08 | Command hiding is a deny-pattern; other Snakemake error forms (a shell: block, for example) would pass through [to be validated] |
Task 5 |
| SEC-P3T1-09 | Nextflow's errorMessage is capped but not hidden |
Task 6 (nf-spectio sends errorMessage, never errorReport) |
Open points from the worker:
- 9 spool fixtures, not 15. P1 Task 1's test comment says "all 15 fixtures". The 6 Tower-protocol captures of spike T2 are HTTP requests to a Tower endpoint (
POST /trace/createand the like), not spool events, so they are not converted.test_one_spool_file_per_bridge_capturepins one spool file per bridge capture. A note for P1 in the next apply pass. snakemake.errorkeepsmsg, with the quoted command hidden. It is not dropped, because the run's failure message comes from it (§14.1). If the pattern does not match a quote it finds, it fails closed in two steps: only the first line is kept, and if that line still holdsCommand ', it is cut there andCommand «hidden»is appended. Whether other quoting forms leak a command is [to be validated] (SEC-P3T1-08, Task 5).seqis per (run, launch, producer). A bridge's counter starts again in each launch's head process. So api §8.2 and architecture §13.1 changed from "per producer and run", with dated notes. The projector (Task 4) comparessource_seqonly within one triple.
Deferred from Task 2's review (record):
| Id | What | Goes to |
|---|---|---|
| SEC-P3T2-04 (rest) | Fail closed on Linux and autofs are done. Extending the network file system deny-list beyond the plan's list is not; which type names to add is [to be validated: type names] |
/apply-findings: the list is only in P1 Task 2 today; architecture §7.2 should take it (inbox #75) |
Open points from Task 2:
- The plan's interface is out of date. See Differences from the plan's interface; P1 Task 2's text changes in the next
/apply-findingspass. - The specs do not name the new checks yet. The parent walk, the canonical path, the pins, the
$TMPDIRbase rule,autofsand fail-closed are decided in code and in this review only. dev-env §15.3 "Where data lives" and architecture §7.2 own them; written in the next/apply-findingspass, not here (this step changes no spec); tracked by inbox #75. - A docstring claim about automount.
fs_type's docstring says the firststattriggers an automount. stat(2) saysAT_NO_AUTOMOUNTis implied since Linux 4.11, and the kernel autofs docs say an access "beyond a stat" triggers the mount. The check still fails closed: a point that has not mounted reportsautofs, which is refused. Whether the earlieropenof the directory in_private_dirtriggers the mount is [to be validated]. The docstring needs a fix in a later code change. - Not built yet, from architecture §7.2's "one writer, short transactions": chunked maintenance writes and the scheduled
wal_checkpoint(TRUNCATE). P1 Task 2 does not ask for them.
Process:
- Attribution trailers in the first commit.
9fb1d97ends with aCo-Authored-Bytrailer that names a model, and aClaude-Sessiontrailer. The other commits of the branch have neither. The squash message of the PR drops both. The branch history keeps them, because the branch is never force-pushed. - Still pending Thomas: inbox #28 (the spool numbers) and #55 (what events may carry). His 2026-10-29 dashboard answer to #55 asks why not send
scriptandenvas Tower does. It is not a decision yet, and §14.1 says so. - Follow-up FU-P3-01:
hooks/progress.pyshould link milestone pages (P3-M<n>) from the P3 row. - Doc review:
doc-reviewerhas not reviewed this page yet;reviewisnull. The Task 2 sections are not reviewed either.