@gitroll/tacitt-cli 0.6.13 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{agent-setup-Doy_AnPY.mjs → agent-setup-Bv73EZ2b.mjs} +2 -2
  2. package/dist/agent-setup-worker.mjs +5 -5
  3. package/dist/cli.mjs +10 -10
  4. package/dist/codex-title-worker.mjs +1 -1
  5. package/dist/configured-preview-worker.mjs +5 -5
  6. package/dist/configured-session-worker.mjs +5 -5
  7. package/dist/{frame-BPbypzmt.mjs → frame-0v_mLdvG.mjs} +1 -1
  8. package/dist/{heap-DCS_WXIA.mjs → heap-aWke8lrK.mjs} +8 -7
  9. package/dist/inspection-worker.mjs +4 -4
  10. package/dist/{job--zxwgaPy.mjs → job-ILnqW1ge.mjs} +4 -4
  11. package/dist/{locate-PLEyLYW-.mjs → locate-LpCex6Xj.mjs} +81 -16
  12. package/dist/{model-CyXvsWLz.mjs → model-BEWsfkIU.mjs} +1 -1
  13. package/dist/operation-executor.mjs +3 -3
  14. package/dist/preview-worker.mjs +1 -1
  15. package/dist/privacy-validator.mjs +1 -1
  16. package/dist/{query-BC7MIaWc.mjs → query-BX8QwaBi.mjs} +5923 -5721
  17. package/dist/{run-Dod7Vykp.mjs → run-P_uZgSYa.mjs} +4 -4
  18. package/dist/{src-CttNuxOn.mjs → src-CHy3-siB.mjs} +81 -24
  19. package/dist/{src-dL6y2mcu.mjs → src-ikYI-1c5.mjs} +4 -4
  20. package/dist/{src-CP-K2PBv.mjs → src-sERffIem.mjs} +3 -3
  21. package/dist/store-database-worker.mjs +1 -1
  22. package/dist/web/assets/index-XdWDOC5H.js +172 -0
  23. package/dist/web/index.html +1 -1
  24. package/package.json +1 -1
  25. package/skills/tacitt/INDEX.md +1 -1
  26. package/skills/tacitt/SKILL.md +3 -1
  27. package/skills/tacitt/references/INDEX.md +2 -2
  28. package/skills/tacitt/references/bootstrap.md +1 -1
  29. package/skills/tacitt/references/classification.md +2 -0
  30. package/skills/tacitt/references/commands.md +17 -3
  31. package/skills/tacitt/references/journey.md +4 -2
  32. package/skills/tacitt/references/parallel-review.md +3 -3
  33. package/skills/tacitt/scripts/INDEX.md +5 -5
  34. package/skills/tacitt/scripts/journey-classification-progress.mjs +6 -0
  35. package/skills/tacitt/scripts/journey-classification-tasks.mjs +11 -1
  36. package/skills/tacitt/scripts/journey-classification-validation.d.mts +14 -1
  37. package/skills/tacitt/scripts/journey-classification-validation.mjs +45 -0
  38. package/skills/tacitt/scripts/journey-consolidation-submit.mjs +22 -5
  39. package/skills/tacitt/scripts/journey-scan-timing.mjs +1 -1
  40. package/skills/tacitt/scripts/journey-state.d.mts +3 -0
  41. package/skills/tacitt/scripts/journey-state.mjs +8 -1
  42. package/skills/tacitt/scripts/journey.mjs +45 -7
  43. package/skills/tacitt/scripts/privacy-validator.mjs +76 -7
  44. package/skills/tacitt/scripts/review.mjs +2 -2
  45. package/dist/web/assets/index-BgDdPL1c.js +0 -172
@@ -6,7 +6,7 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <meta name="referrer" content="no-referrer" />
8
8
  <title>tacitt · Local session collection</title>
9
- <script type="module" crossorigin src="./assets/index-BgDdPL1c.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-XdWDOC5H.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-DzOozqms.css">
11
11
  </head>
12
12
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitroll/tacitt-cli",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "Private local session review and export for coding agents",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -6,7 +6,7 @@
6
6
  <!-- index-entries
7
7
  ./ | Shipped skill | Workspace-scoped instructions for direct CLI and optional Web collaboration.
8
8
  INDEX.md | Documentation | Directory navigation.
9
- SKILL.md | Agent instructions | Supplies the managed release template, explicit fast/low worker policy and phase routing into classification, paired CLI scan reruns, consent gates, account-seeded access, person-only Public choices, privacy review and approved export.
9
+ SKILL.md | Agent instructions | Supplies the managed release template, explicit fast/low worker policy and phase routing into classification, paired CLI scan reruns, consent gates, Retry-only formation account stop, account-seeded access, person-only Public choices, privacy review and approved export.
10
10
  -->
11
11
 
12
12
  The entry table is not committed: run `pnpm run write:indexes`, then open `INDEX.local.md` in this directory.
@@ -14,7 +14,7 @@ Prepare the [exact paired CLI](references/bootstrap.md), then follow the [classi
14
14
 
15
15
  Before initializing the run, inspect the native host's actually available subagent models. Select the newest available supported native fast-tier model at explicit `low` reasoning, using fresh context for a newly delegated task; use the SAME exact model and effort for classification, bounded evidence, keyword/privacy scanning, internal card drafting and final resolution. Do not inherit the main agent's model or effort. On the currently documented Codex host this is `gpt-6-luna` with `reasoning_effort: "low"`; resolve availability again on a later host rather than assuming this dated example is universal. Use fresh-context children and pass the model/effort in the native spawn tool arguments, not just in a prompt. If the host cannot set the required profile, report that capability failure; do not silently switch model, raise effort or claim compliance.
16
16
 
17
- Initialize the shared phase/review run with `--model EXACT_NATIVE_MODEL --effort low`. Keep at most twenty-four native child agents active across all phases, further limited by actual available slots; a delegated coordinator also occupies a slot. Launch ready assignments up to available capacity together when the host supports parallel native spawns. Otherwise issue consecutive native spawns without waiting for results between launches. Every helper response returns the pinned `agentProfile`; use it for dispatch and record the actual spawn receipts and available slots. Helpers record requested settings, not proof that a model executed them. No global client configuration changes or extra API provider are needed.
17
+ Initialize the shared phase/review run with `--model EXACT_NATIVE_MODEL --effort low`. Keep at most forty-eight native child agents active across all phases, further limited by actual available slots; a delegated coordinator also occupies a slot. Launch ready assignments up to available capacity together when the host supports parallel native spawns. Otherwise issue consecutive native spawns without waiting for results between launches. Every helper response returns the pinned `agentProfile`; use it for dispatch and record the actual spawn receipts and available slots. Helpers record requested settings, not proof that a model executed them. No global client configuration changes or extra API provider are needed.
18
18
 
19
19
  ## Coordinator ownership
20
20
 
@@ -22,6 +22,8 @@ The agent invoking this Skill is its Main coordinator, including when another ag
22
22
 
23
23
  ## Required order
24
24
 
25
+ Before first Work formation, `journey.prepare` reads the account inventory once, across all pages, with `include=title,removedFromWork,unitAccess` and the shared 30-second bound. A failed or partial read stops before classification and forms no Work. On `ACCOUNT_WORKS_UNAVAILABLE`, say "Tacitt could not read your profile, so it cannot tell which Works are already Private there." On `ACCOUNT_SIGNED_OUT`, ask the person to sign in. The helper's `accountReadRecovery` offers Retry only: get the current journey revision and submit `{kind:"account-read"}` once, then wait if the read still fails. For the first formation of a journey, never continue classification without a successful read or offer to group without the profile. A journey that already has a formed snapshot from an earlier build continues under its existing rules with no formation read. Consent without a subject keeps the existing signed-out exception. A successful read stores account facts for the later rebuild slice; it does not yet change Work ids, titles, grouping, access or Send.
26
+
25
27
  For scan starts and reruns after scope confirmation, follow the [paired CLI background preparation, restart budget and prepare-outcome rules](references/commands.md#scan-results).
26
28
 
27
29
  The person decides in the panel; you keep the journey moving. Never ask the person to return to chat to confirm or continue, except for the explicit [remote-worker host consent](references/remote-workers.md) before classification freezes. Until Ready to send, while the next step is theirs, [wait inside this turn](references/journey.md#waiting-for-the-person); at Ready to send your turn ends.
@@ -9,8 +9,8 @@ INDEX.md | Directory index | Locates the shipped native journey references.
9
9
  b-method.md | Method reference | Defines the dictionary method: analysed scope, the child answer, the ten term types and how the hidden set is applied.
10
10
  bootstrap.md | Skill reference | Verifies the exact paired CLI release and preserves installation boundaries.
11
11
  classification.md | Skill reference | Owns priority dispatch of unresolved/evidence tasks, authoritative task-pointer prompts, semantic evidence safeguards, bounded reconciliation, final metadata and full-coverage publication and unconfirmed Public suggestions.
12
- commands.md | Command reference | Supplies classification assignment and answer-publication, session review, decision-card, final-resolution, phase-helper, batched reader lifecycle events, explicit paired-CLI scan reruns, account inheritance, person-only Public choices and Send contracts.
13
- journey.md | Skill reference | Guides explicit consent, row-assigned classification, CLI-owned answer publication and same-child recovery, account-seeded scan scope, explicit Public choices, contextual cards, final resolution, preview and approved packaging.
12
+ commands.md | Command reference | Supplies classification assignment and answer-publication, session review, decision-card, final-resolution, phase-helper, batched reader lifecycle events, explicit paired-CLI scan reruns, Retry-only formation reads, account inheritance, person-only Public choices and Send contracts.
13
+ journey.md | Skill reference | Guides explicit consent, row-assigned classification, CLI-owned answer publication and same-child recovery, Retry-only formation reads, account-seeded scan scope, explicit Public choices, contextual cards, final resolution, preview and approved packaging.
14
14
  parallel-review.md | Review protocol | Runs the review helper loop: bounded current-provider analysis agents, CLI-published drafts, same-child repair, batched launches and exits, busy slots, durable replay and recovery.
15
15
  remote-workers.md | Collection protocol | Owns per-Send host consent, bounded remote worker import before freeze and disclosure of unreadable dispatch lines or collection failures.
16
16
  send.md | Send reference | Owns browser Send, owner login, scope confirmation, incremental session intent, concurrent transfers with serialized commits, narrow pre-upload access blocking, upload-bound completion, background assessment, fresh review recovery and deliberate upgrades.
@@ -110,7 +110,7 @@ the intended Skill; do not report native-client discovery without observing it.
110
110
 
111
111
  ## Client concurrency setup
112
112
 
113
- The installer offers one default-No terminal question to save a setting of twenty-four
113
+ The installer offers one default-No terminal question to save a setting of forty-eight
114
114
  concurrent subagents for the selected clients. It explains speed, concurrent usage,
115
115
  the user/project scope and restart. Explicit `--configure-agents` (JSON
116
116
  `configureAgents:true`) authorizes the same change; `--skip-agent-config` declines.
@@ -73,6 +73,8 @@ Publish accepted completed-root progress at helper checkpoints while refilling s
73
73
 
74
74
  After scope confirmation, one item may hold roots of one published Work, plus roots still being grouped. It may join several published Works only when they have the same visibility record, as a Work and the blocked Work split from it do. Otherwise the helper stops with `CLASSIFICATION_LATE_SEVERAL_WORKS` before commit (CLI would refuse with `INVALID_SELECTION`) and prints one line per refused item, such as `items[0] joins Works <id>, <id>`, because the joined roots would take the first Work's visibility, Public included. Keep each published Work's roots in its own item, and keep any other root pending or in a new item.
75
75
 
76
+ A rebuilt profile Work (`accountWork: true`, shown with its id and profile title) is a join target only through the catalog: a known project whose catalog entry carries `accountWorkId` is submitted by the helper as `{accountWorkId, rootSessionIds}`, with no description, project type or recommendation, and gets no description task (ADR 0276 slice H). Never name a rebuilt Work yourself and never put its roots in an item. The helper checks joins before commit and stops with `CLASSIFICATION_ACCOUNT_WORK_JOIN`, printing `items[<index>] joins Works <id>` per refused item; CLI also refuses a joined session the profile already lists (`INVALID_SELECTION`). Do not retry such a refusal by hand: report it and stop the classification step. Joining keeps the Work's title and visibility; a Public profile Work becomes a Public suggestion again for the person to confirm.
77
+
76
78
  ## One-shot workflow acceptance
77
79
 
78
80
  The human-approved acceptance bar is a complete Skill workflow with useful
@@ -25,6 +25,7 @@ Consent arrives from the panel's Continue button (or, after an explicit chat agr
25
25
  - `journey.classificationSubmit`: `{journeyId,expectedRevision,idempotencyKey,items}`. Each item is `{projectName,description,projectType,rootSessionIds,recommendation:{access,reason}}`. Types are `work`, `side`, `coursework`, `personal`, `scratch`, `other`; access is `public`, `private`, `drop`. Cover every root exactly once, preserve families and merge matching project identities; matching names alone are not identity. Recommendations do not change access by themselves.
26
26
  - Apply explicit visibility using `{kind:"work-access",workId,access}` or `{kind:"works-access",workIds,access}`. Browser choices are already committed; never overwrite them with an older recommendation.
27
27
  - Before `scope-confirm`, an unchosen Work may store a placeholder `access`; the panel shows its recommendation. Scope confirmation preserves confirmed person choices and a delegate's Private choice. It seeds account access while signed in (`accessOrigin:"account"`): all Private confirms, every root held Public confirms with account proof, partial Public history stays a suggestion, and mixed starts Private with a visible conflict. No account history uses the model path. Model Public also stays `accessConfirmed:false`; model Private may confirm. The panel asks the person for every suggested-Public Work. A suggested Public Work requires that person's explicit visibility choice, including choosing the same displayed Public value. Never use delegation to confirm Public.
28
+ - Before first formation, `journey.prepare` performs the all-or-nothing account read. `ACCOUNT_WORKS_UNAVAILABLE` or `ACCOUNT_SIGNED_OUT` stops before classification with no Work formed. Get the current revision and submit `{kind:"account-read"}` once as Retry; the helper returns this sole choice in `accountReadRecovery`. If Retry still fails, wait for the profile read (or sign-in) to recover; never continue without the profile. A successful formation read changes no Work grouping or visibility in slice A.
28
29
  - As soon as the known Works (an `ingest` result with `partial: true`) or the complete proposal is ingested, record `{kind:"scope-confirm"}` for scan scope. This does not confirm suggested Public (a Work whose every root session is already Public on the account is confirmed by that account history and needs no question); name each suggested-Public Work and ask the person to choose Public or Private, in the panel or conversation. `scope-confirm` reads the account inventory first (30 s bound). A failed read refuses it with `ACCOUNT_ACCESS_UNAVAILABLE`, and a signed-out read with `ACCOUNT_SIGNED_OUT`; each records `accountRead` and changes no Work, and only a later `scope-confirm` that reads the account confirms the scope (Retry; it decides nothing, so run it once without asking). A signed-out `scope-confirm` continues on the model path only when this journey has recorded consent without an account subject. That covers consent given before sign-in was required or in a build with no sign-in client. A calling process with no sign-in client cannot exempt consent that records a subject. After confirmation, `{kind:"account-read"}` reads the account again from the panel or chat: on a read it re-applies account access to Public Works while preserving confirmed person choices, every Private Work and every left-out Work; on a failure it records the status only. The view's `accountReadCurrent` is true only for a `read` under the account signed in now; while it is false and an included Work is suggested Public or Public by account proof, the panel shows a stop with Retry or Sign in in place of the Send button. Then `journey.prepare` runs selected Work static scans and, while grouping continues, waits for and prepares the Works the complete proposal publishes or extends. `journey.classificationSubmit` takes optional `pendingRootSessionIds` (roots still being grouped, kept in one unread Work) and, after confirmation, `expectedClassificationRevision` (the classification it completes) instead of a current `expectedRevision`. Visibility stays open until Send: Drop stops that Work's scan, and a Work moved out of Drop needs `journey.prepare` again.
29
30
 
30
31
  ## Remote worker collection
@@ -57,6 +58,19 @@ Ingest and `next-packs` list `blockedWorks[]` only while one exists.
57
58
  Phase-helper `status` and `await` also print `leftOutSessions`, the number of root sessions in
58
59
  those Works, only when it is above zero; say it in chat before the review is complete, Ready
59
60
  to send or nothing is left to send.
61
+ They also print `accountFormation: {leftOutSessions, heldBackSessions}` when formation kept
62
+ sessions out of every Work because of the person's profile (ADR 0276). Say each nonzero count
63
+ once in the journey, never a session or Work name: "N sessions are left out because you
64
+ removed their Work on your profile." and "N sessions stay in their Works on your profile and
65
+ are not sent this time." (singular: "1 session is left out because you removed its Work on
66
+ your profile.", "1 session stays in its Work on your profile and is not sent this time.").
67
+ When every session is already on the profile, left out or held back, `journey.prepare` fails with
68
+ `ACCOUNT_NOTHING_TO_SEND` and the helper phase is `nothing-to-send` with `accountFormation`: say
69
+ the counts once and that nothing is left to send this time, then end the turn. Running `/tacitt`
70
+ again reads the profile again.
71
+ Works with `accountWork: true` are the person's profile Works rebuilt with their profile title;
72
+ classification never groups or describes them. A new session whose project already has such a
73
+ Work joins it instead of forming another (ADR 0276 slice H): see the classification reference.
60
74
  When no selected Work is left (every one skipped or Dropped), the helper phase is
61
75
  `nothing-to-send`: there is no Send, and `await` waits for a Retry or a move out of Drop.
62
76
  When the person asks to retry, respond
@@ -172,11 +186,11 @@ exhausted command budget or unknown write stops refill. Known repairs and partia
172
186
  `next-packs --agents FREE_SLOTS` remains available. Refill observes current input
173
187
  and guidance again; accepted results remain bound to existing source receipts.
174
188
 
175
- For native orchestration, use automatic refill only when all twenty-four host slots are free with no pending launches. While any child is live, wait on native completion and pass the current numeric free count. An answer can appear before its worker exits.
189
+ For native orchestration, use automatic refill only when all forty-eight host slots are free with no pending launches. While any child is live, wait on native completion and pass the current numeric free count. An answer can appear before its worker exits.
176
190
 
177
191
  Aggregate `ingest` defaults to automatic refill. `review.mjs ingest --run PRIVATE_RUN --agents auto --wait-ms 30000`
178
192
  ingests published answers, then reserves packs for every free slot, where free
179
- slots are twenty-four minus outstanding scan packs, unacknowledged lost children,
193
+ slots are forty-eight minus outstanding scan packs, unacknowledged lost children,
180
194
  active card children and due-card reservations (reported as `freeSlots`). When nothing was ingested or reserved, it waits up to `--wait-ms`
181
195
  (bounded by half the command budget) for a published answer, rechecking journey
182
196
  input every few seconds, and returns as soon as something changes. Within that wait
@@ -198,7 +212,7 @@ within confirmed available native capacity and calls again, until `next` is
198
212
  AI agent slots and static CPU workers are separate resources: background static
199
213
  processing (Track B), its progress and the core count never reduce `freeSlots`.
200
214
  Auto mode subtracts `--card-agents N` active card children as well as outstanding
201
- privacy packs and unacknowledged lost children from twenty-four. When a card batch
215
+ privacy packs and unacknowledged lost children from forty-eight. When a card batch
202
216
  is due, `--planned-card-agents N` (default one) reserves upcoming card children
203
217
  before scan refill; `cardBatch.plannedAgents` reports the reservation available
204
218
  within capacity. Dispatch at most that count in the same turn as scan packs and
@@ -1,5 +1,7 @@
1
1
  # Classification-first native journey
2
2
 
3
+ Before first Work formation, `journey.prepare` reads the account inventory once, across all pages, with `include=title,removedFromWork,unitAccess` and the shared 30-second bound. A failed or partial read stops before classification and forms no Work. On `ACCOUNT_WORKS_UNAVAILABLE`, say "Tacitt could not read your profile, so it cannot tell which Works are already Private there." On `ACCOUNT_SIGNED_OUT`, ask the person to sign in. The helper's `accountReadRecovery` offers Retry only: get the current journey revision and submit `{kind:"account-read"}` once, then wait if the read still fails. For the first formation of a journey, never continue classification without a successful read or offer to group without the profile. A journey that already has a formed snapshot from an earlier build continues under its existing rules with no formation read. Consent without a subject keeps the existing signed-out exception. A successful read stores account facts for the later rebuild slice; it does not yet change Work ids, titles, grouping, access or Send.
4
+
3
5
  > Audience: native Codex and Claude Code agents
4
6
  > Ownership: Tacitt CLI maintainers
5
7
 
@@ -25,9 +27,9 @@ A concise stage card explains the current next step and Public/Private/Drop mean
25
27
 
26
28
  ## Scan
27
29
 
28
- Run `journey.prepare` after scope confirmation to perform static processing only for selected Works; while grouping continues it waits and also prepares the Works the complete proposal publishes or extends. Use the compact `result:"receipt"` response for this owned preparation. Keep this operation running and use `review.mjs next-packs` as each session publishes authenticated static receipts and review input, without waiting for the remaining sessions in its Work or creating ZIPs. Dispatch printed prompts verbatim, ingest completed answers and refill available slots within the shared twenty-four-child ceiling across phases. The helper saves task files and repeatable batched requests; do not replace it with custom scripts.
30
+ Run `journey.prepare` after scope confirmation to perform static processing only for selected Works; while grouping continues it waits and also prepares the Works the complete proposal publishes or extends. Use the compact `result:"receipt"` response for this owned preparation. Keep this operation running and use `review.mjs next-packs` as each session publishes authenticated static receipts and review input, without waiting for the remaining sessions in its Work or creating ZIPs. Dispatch printed prompts verbatim, ingest completed answers and refill available slots within the shared forty-eight-child ceiling across phases. The helper saves task files and repeatable batched requests; do not replace it with custom scripts.
29
31
 
30
- At the first ready unit and each completion, run the [shared dispatch loop](parallel-review.md#dispatch-while-stages-overlap). Pass the actual free count to `review.mjs ingest --run PRIVATE_RUN --agents FREE_SLOTS --card-agents ACTIVE_CARDS` while any child is live or the host supplies fewer than twenty-four slots. Automatic capacity is for a verified empty pool with all twenty-four slots free. Spawn one fresh native child per returned `packs[].prompt` using the pinned `agentProfile`; then check both queues again. When the only runnable work is already in flight, wait for a native completion for up to 30 seconds, then recount free host slots before ingesting. Background static processing uses separate CPU workers. Follow returned `ingested[].prompt`, `retryPrompt` or repair instructions before calling again. Bind every launched reader and report verified closure through the [reader recovery contract](parallel-review.md#recovery). Lost readers retain their slots until Main confirms closure. Accepted writes and pending repairs keep their recovery authority. A confirmed technical failure may release the remaining batches for a fresh attempt. Refused units remain visibly incomplete and receive no new reader. Launch independent work first, then handle ongoing repair. When `cardBatch.ready` is true, dispatch at most `cardBatch.plannedAgents` [card drafting](#decision-cards) children in the same turn as scan refills, queueing the rest. The helper reserves one planned card slot by default; request more with `--planned-card-agents N` before scan refill. Ingest with the actual remaining capacity immediately after dispatch and between bounded Main-owned reconciliation steps; do not wait for the whole card batch before refilling scans. While `cardBatch.ready` is false, keep dispatching packs even when `cardBatch.uncovered` grows. Handle `done` and `paused` at their normal boundaries. At `report-blocked`, report the unresolved units and follow the panel decision in `review-interrupted`. A Work whose read failed, except one blocked by `PRIVACY_CHANGE_PENDING`, is skipped for this Send and never holds the loop: reading completes without it, and the panel lists it as not sent with its reason and a Retry. Do not wait for a decision about it. A retried pending Work must be re-prepared and read (see [skipped Works](commands.md#scan-results)); no time box or pending-Work skip is allowed. All model privacy results include a disposition and reason. See [method](b-method.md).
32
+ At the first ready unit and each completion, run the [shared dispatch loop](parallel-review.md#dispatch-while-stages-overlap). Pass the actual free count to `review.mjs ingest --run PRIVATE_RUN --agents FREE_SLOTS --card-agents ACTIVE_CARDS` while any child is live or the host supplies fewer than forty-eight slots. Automatic capacity is for a verified empty pool with all forty-eight slots free. Spawn one fresh native child per returned `packs[].prompt` using the pinned `agentProfile`; then check both queues again. When the only runnable work is already in flight, wait for a native completion for up to 30 seconds, then recount free host slots before ingesting. Background static processing uses separate CPU workers. Follow returned `ingested[].prompt`, `retryPrompt` or repair instructions before calling again. Bind every launched reader and report verified closure through the [reader recovery contract](parallel-review.md#recovery). Lost readers retain their slots until Main confirms closure. Accepted writes and pending repairs keep their recovery authority. A confirmed technical failure may release the remaining batches for a fresh attempt. Refused units remain visibly incomplete and receive no new reader. Launch independent work first, then handle ongoing repair. When `cardBatch.ready` is true, dispatch at most `cardBatch.plannedAgents` [card drafting](#decision-cards) children in the same turn as scan refills, queueing the rest. The helper reserves one planned card slot by default; request more with `--planned-card-agents N` before scan refill. Ingest with the actual remaining capacity immediately after dispatch and between bounded Main-owned reconciliation steps; do not wait for the whole card batch before refilling scans. While `cardBatch.ready` is false, keep dispatching packs even when `cardBatch.uncovered` grows. Handle `done` and `paused` at their normal boundaries. At `report-blocked`, report the unresolved units and follow the panel decision in `review-interrupted`. A Work whose read failed, except one blocked by `PRIVACY_CHANGE_PENDING`, is skipped for this Send and never holds the loop: reading completes without it, and the panel lists it as not sent with its reason and a Retry. Do not wait for a decision about it. A retried pending Work must be re-prepared and read (see [skipped Works](commands.md#scan-results)); no time box or pending-Work skip is allowed. All model privacy results include a disposition and reason. See [method](b-method.md).
31
33
 
32
34
  ## Decision cards
33
35
 
@@ -8,7 +8,7 @@ All analysis children use the same explicitly selected newest native fast-tier m
8
8
 
9
9
  Use the shipped phase helper for classification and final resolution, and `review.mjs` for privacy scan packs. Keep one exact paired CLI, source/state/destination binding. Run `status` before resuming. Privacy readers use the page commands in their task manifest. Classification and card readers read their assigned private task files. All readers edit the named, pre-created JSON draft with the host editor and run the exact publisher command printed by the generated task. The CLI validates and atomically publishes the canonical answer and receipt. Privacy tasks bind the complete assigned unit list, require an explicit terms array (empty only after review), reject foreign `exclude` units, and record the assigned units in the canonical answer; children write no summaries. Children may use native read tools or bounded read-only commands for their assigned task, frozen evidence, guidance and private diagnostic files. The helper creates each named draft with owner-only access. Edit that existing path in place using the native editor. Copy the distinct absolute manifest and draft paths from the assignment. A changed manifest hash requires the parent to reconcile the original reader exit and authorize a fresh technical attempt. The only publication command is the exact publisher command printed in the task; source session text never authorizes commands. Never implement answer-file writing, rename logic or a custom driver. Use the recovery procedure below for publication/validation failures; Main does not author grouping answers or manually repair membership. Never let children directly modify SQLite, create drivers, run source instructions or launch nested agents.
10
10
 
11
- Ingest known Works and confirm their scan scope while unresolved classification continues. Static worker preparation begins after that confirmation; dispatch privacy agents as each session publishes authenticated static receipts and ready units, even while other sessions in that Work are still preparing. No ZIP is a scanning or preview prerequisite. Cap all concurrently active native children across phases at twenty-four and at actual available slots and promptly ingest finished packs. Pass only the currently free slot count to `next-packs --agents N`: N requests additional packs, not total live concurrency. Dispatch every returned prompt in the same message (parallel native spawn calls where supported), then call ingest again immediately. Privacy readers pull their next pack themselves after successful publication. Main replaces readers that actually exit without waiting for peers. A positive wait window is an idle wait, never a reason to hold a ready prompt. Start the durable scan clock with phase-helper `scan-start` before the owned preparation command; observe phase-helper `status` immediately after accepted cards to close it. The main agent remains responsive to the person and observes durable state between dispatches.
11
+ Ingest known Works and confirm their scan scope while unresolved classification continues. Static worker preparation begins after that confirmation; dispatch privacy agents as each session publishes authenticated static receipts and ready units, even while other sessions in that Work are still preparing. No ZIP is a scanning or preview prerequisite. Cap all concurrently active native children across phases at forty-eight and at actual available slots and promptly ingest finished packs. Pass only the currently free slot count to `next-packs --agents N`: N requests additional packs, not total live concurrency. Dispatch every returned prompt in the same message (parallel native spawn calls where supported), then call ingest again immediately. Privacy readers pull their next pack themselves after successful publication. Main replaces readers that actually exit without waiting for peers. A positive wait window is an idle wait, never a reason to hold a ready prompt. Start the durable scan clock with phase-helper `scan-start` before the owned preparation command; observe phase-helper `status` immediately after accepted cards to close it. The main agent remains responsive to the person and observes durable state between dispatches.
12
12
 
13
13
  Work metadata (title, project name, description and reason) is never reviewed or Hidden. Each task names its phase, immutable input and complete guidance. Read all pages; incomplete coverage is failure. Keep each saved request key and exact body for retry. An unknown response does not permit a second task with new authority. Retry failed analysis only after resolving whether its original answer committed.
14
14
 
@@ -24,8 +24,8 @@ Card-authoring inputs are bounded to 48,000 estimated tokens per task and read i
24
24
 
25
25
  Main runs one lifecycle loop across classification, scanning and card work. It launches the initial privacy pool together; persistent privacy readers pull successive assignments without Main dispatch between packs. Main binds any missing native receipts, replaces actual exits and handles cards. On each native completion, new input or preparation milestone, check receipts and ready tasks. Keep the existing classification priority for unresolved roots, requested evidence and reconciliation. Admit known Works as soon as their metadata is published. Continue launching ready privacy packs while grouping or its repairs run.
26
26
 
27
- 1. Count native capacity across the whole tree, including a delegated Main, other live children and reservations awaiting launch. Use the smaller of the host's available slots and the space left under twenty-four. Maintain one owner per task; publication alone does not prove child exit. Static preparation uses separate CPU workers.
28
- 2. After allocating slots to other stage tasks, pass the remaining free count to `review.mjs ingest --agents FREE_SLOTS --card-agents ACTIVE_CARDS`. This single call records batched reader events, ingests and refills. Pass `--reader-starts JSON` only for native spawn receipts not already self-bound by readers, and `--reader-exits JSON` for confirmed exits since the last call; starts are applied before exits. Each array contains objects with `packId`, `readerId` (the native ID), optional `turn`, and, for exits, `result` (`published`, `retry` or `refused`). Use zero for ingestion only. Bare aggregate `ingest` and `--agents auto` infer capacity; use them only when all twenty-four native slots are confirmed free and no launch is outstanding. While any child is live, use a bounded native completion wait, then recount and pass the numeric free count. Never infer a free slot from the configured maximum alone.
27
+ 1. Count native capacity across the whole tree, including a delegated Main, other live children and reservations awaiting launch. Use the smaller of the host's available slots and the space left under forty-eight. Maintain one owner per task; publication alone does not prove child exit. Static preparation uses separate CPU workers.
28
+ 2. After allocating slots to other stage tasks, pass the remaining free count to `review.mjs ingest --agents FREE_SLOTS --card-agents ACTIVE_CARDS`. This single call records batched reader events, ingests and refills. Pass `--reader-starts JSON` only for native spawn receipts not already self-bound by readers, and `--reader-exits JSON` for confirmed exits since the last call; starts are applied before exits. Each array contains objects with `packId`, `readerId` (the native ID), optional `turn`, and, for exits, `result` (`published`, `retry` or `refused`). Use zero for ingestion only. Bare aggregate `ingest` and `--agents auto` infer capacity; use them only when all forty-eight native slots are confirmed free and no launch is outstanding. While any child is live, use a bounded native completion wait, then recount and pass the numeric free count. Never infer a free slot from the configured maximum alone.
29
29
  3. For a due batch, request ready card capacity with `--planned-card-agents N`. The default reserves one slot. Open the due batch promptly. Once it is open the helper stops reserving for a new batch. If it produces fewer unowned drafts than planned, run ingest again immediately with the actual remaining capacity. When card drafts and privacy packs are both ready, advance both in the same dispatch when capacity permits. Keep request counts within the ready card backlog and give surplus capacity to ready work in the other stage. Launch at most `cardBatch.plannedAgents` card children alongside every returned privacy pack. On an already-open card batch, count its queued drafts explicitly before assigning remaining slots to scans. Recompute from the host at the next completion.
30
30
  4. If the complete global card catalog needs substantial analysis while other work is ready, assign one fresh pinned child the read-only comparison of all candidates against `existingCards`. Give it only the task's bounded catalog, existing policies and output schema. Ask for a proposal referencing candidate IDs, without canonical publication, CLI mutations or panel actions. Count this child in capacity. Main keeps refilling, reviews the complete catalog and proposal in bounded portions between dispatches, verifies every mapping and policy, then solely publishes the global answer and ingests it. For an extend-only batch already ready for ingestion, ingest immediately.
31
31
  5. Dispatch unowned ready work before tracing diagnostics, writing timing reports or waiting. Spawn all returned privacy packs and planned card children in the same message, using parallel native spawn calls where supported. Readers bind their own actual native ID with the generated `reader-start` as their first command when the host exposes self identity. Collect any remaining spawn receipts and all confirmed exits into the next ingest call; do not spend a separate coordinator turn per child start or exit. Run ingest again without waiting after the dispatch batch and when children exit. When no stage child is active and owned preparation is the only producer, pass the positive actual host budget and `--wait-ms 30000` to the same ingest call. Repeated probes inside that wait reuse one authenticated CLI runtime. When stage children are active, wait up to 30 seconds for the first change; return to both queues immediately.
@@ -16,11 +16,11 @@ journey-classification-metadata.d.mts | Declaration | Types generated AI metadat
16
16
  journey-classification-metadata.mjs | Runtime | Validates global reconciliation, reduces oversized Works from all evidence slices and creates hash-bound, token-bounded AI metadata tasks for new Works and Unsorted.
17
17
  journey-classification-progress.mjs | Runtime | Accepted-answer counters and fenced progress checkpoints.
18
18
  journey-classification-tasks.d.mts | Declaration | Types initial classification routing and catalog/evidence task generation for helper consumers and tests.
19
- journey-classification-tasks.mjs | Runtime | Original-root routing, capacity-balanced bounded tasks and unresolved-first ready ordering with stable assignments.
19
+ journey-classification-tasks.mjs | Runtime | Original-root routing, capacity-balanced bounded tasks and unresolved-first ready ordering with stable assignments; known projects the catalog ties to a rebuilt Work become joins, not tasks.
20
20
  journey-classification-validation.d.mts | Declaration | Shared classification validator contracts.
21
- journey-classification-validation.mjs | Runtime | Shared root judgment, stable target identity, fixed project metadata, consolidation validation, target-preserving assignment expansion and the late check that an item joins only placed Works with one access record (ADR 0274).
21
+ journey-classification-validation.mjs | Runtime | Shared root judgment, stable target identity, fixed project metadata, consolidation validation, target-preserving assignment expansion and the late check that an item joins only placed Works with one access record (ADR 0274), and the account Work join check (ADR 0276 slice H).
22
22
  journey-classification.mjs | Runtime | Batched known-project metadata, unknown-root grouping and streamed four-row evidence, versioned checkpoints and generation progress.
23
- journey-consolidation-submit.mjs | Runtime | Expands candidate assignments, applies per-root overrides, combines small topics into Unsorted, attaches known projects and checks exact root coverage.
23
+ journey-consolidation-submit.mjs | Runtime | Expands candidate assignments, applies per-root overrides, combines small topics into Unsorted, attaches known projects, adds account Work join items (ADR 0276 slice H) and checks exact root coverage.
24
24
  journey-consolidation.d.mts | Declaration | Consolidation helper contract for bounded evidence regression tests.
25
25
  journey-consolidation.mjs | Runtime | Builds proposed-name and stable-target candidate buckets, persists complete reconciliation input, and returns compact dispatch descriptors.
26
26
  journey-panel-host.d.mts | Declaration | Types the detached-host handoff for helper tests.
@@ -32,13 +32,13 @@ journey-state.mjs | Runtime state | Pending-Public-choice, confirmed-answer and
32
32
  journey-tasks.d.mts | Declaration | Types task creation, rendered-token sizing, canonical answer metadata and batch answer reads.
33
33
  journey-tasks.mjs | Task builder | Exact rendered-token budgets, private drafts, a 200-value cap (ADR 0234), compact existing policies with bounded overhead (ADR 0251), and one-term-per-line card/resolution payloads with complete bounded read ranges.
34
34
  journey.d.mts | Declaration | Phase helper contract for integration tests.
35
- journey.mjs | Runtime | Phase tasks with compact existing-card identity snapshots, durable exact batch writes, the person-decision `await` that hands off at Ready to send, the late placed-Work check before the classification commit (its stderr names only item indexes and Work ids) and shared CLI transport.
35
+ journey.mjs | Runtime | Retry-only formation failure/status projection and phase tasks with compact existing-card identity snapshots, durable exact batch writes, the person-decision `await` that hands off at Ready to send, the late placed-Work check before the classification commit (its stderr names only item indexes and Work ids) and shared CLI transport.
36
36
  local-cli.d.mts | Type declarations | Defines the shared installed-package binding used by the CLI and standalone verifier.
37
37
  local-cli.mjs | Read-only package verifier | Validates an explicit installed CLI identity and aggregate artifact digest without network or application-state access.
38
38
  review-stream-client.d.mts | Type declarations | Types the fixed request/shutdown limits, private lifecycle callbacks and review-stream client methods.
39
39
  review-stream-client.mjs | Runtime | Bounds requests and owned-child shutdown, permits retry only for explicit pre-request startup busy, and preserves unknown lost-write outcomes for receipt replay.
40
40
  review.d.mts | Type declarations | Types the review helper's exported task rendering, answer reading, per-Work progress and entry functions for tests.
41
- review.mjs | Runtime | Session-ready scanning, ready-pack-first twenty-four-slot refill, 30000-token pack target, manifest-derived page byte capture, default reader pull with native self-binding, host-capability fallback and replayable slot transfer, blocked-Work exclusion, largest-Work queue ordering, isolated retries, batched native reader and turn receipts with launch reservations, bounded reissue and visible interruption recovery, planned card capacity, outcome-reported preparation recovery and read-only status, metadata-gated questions, Track B-gated final preview and hash-bound answer recovery.
41
+ review.mjs | Runtime | Session-ready scanning, ready-pack-first forty-eight-slot refill, 30000-token pack target, manifest-derived page byte capture, default reader pull with native self-binding, host-capability fallback and replayable slot transfer, blocked-Work exclusion, largest-Work queue ordering, isolated retries, batched native reader and turn receipts with launch reservations, bounded reissue and visible interruption recovery, planned card capacity, outcome-reported preparation recovery and read-only status, metadata-gated questions, Track B-gated final preview and hash-bound answer recovery.
42
42
  verify-cli.mjs | Verification entry | Checks a managed local receipt and its current package bytes before the Skill starts a journey.
43
43
  -->
44
44
 
@@ -64,6 +64,12 @@ export async function publishClassificationProgress(
64
64
  const knownKeys = new Set(classification.projects.flatMap((project) => project.rootKeys))
65
65
  const aiCompleted = new Set()
66
66
  const completed = new Set()
67
+ // Roots joining a rebuilt account Work are grouped by routing alone (ADR 0276 slice H).
68
+ for (const join of classification.accountJoins ?? [])
69
+ for (const key of join.rootKeys) {
70
+ knownKeys.add(key)
71
+ completed.add(key)
72
+ }
67
73
  const latestJudgments = new Set()
68
74
  for (const item of manifest.tasks) {
69
75
  if (item.evidenceRound > 0 && !(await optional(item.answer))) continue
@@ -21,6 +21,8 @@ const projectInstruction =
21
21
  const rootUniverse = (view) => {
22
22
  const result = []
23
23
  for (const work of view.works) {
24
+ // Rebuilt account Works are not classified (ADR 0276 D1).
25
+ if (work.accountWork) continue
24
26
  for (const rootSessionId of work.rootSessionIds) {
25
27
  result.push({ rootSessionId, workId: work.id })
26
28
  }
@@ -114,9 +116,17 @@ async function initialize(directory, manifest, view, call) {
114
116
  })
115
117
  if (projectRootKeys.size + unknown.length !== roots.length)
116
118
  throw new Error('CLASSIFICATION_ROUTING_COVERAGE_INVALID')
119
+ // ADR 0276 slice H: a known project the catalog ties to a rebuilt account Work joins it. It is
120
+ // neither described nor offered to the classifier as a target.
121
+ const accountJoins = projects.flatMap((project) =>
122
+ project.metadata.accountWorkId
123
+ ? [{ accountWorkId: project.metadata.accountWorkId, rootKeys: project.rootKeys }]
124
+ : [],
125
+ )
117
126
  manifest.classification = {
118
127
  roots,
119
- projects,
128
+ projects: projects.filter((project) => !project.metadata.accountWorkId),
129
+ accountJoins,
120
130
  unknown,
121
131
  routingAssignments: assignments,
122
132
  evidence: [],
@@ -18,8 +18,21 @@ export function validateConsolidationAnswer(
18
18
  projectCatalog: unknown[]
19
19
  },
20
20
  ): { code: string | null; details: string[] }
21
+ export function validateAccountWorkJoins(
22
+ items: ReadonlyArray<{ rootSessionIds: readonly string[]; accountWorkId?: string | undefined }>,
23
+ works: ReadonlyArray<{
24
+ id: string
25
+ placement?: string
26
+ rootSessionIds: readonly string[]
27
+ accountWork?: boolean
28
+ }>,
29
+ confirmed: boolean,
30
+ ): void
21
31
  export function validateLatePlacedWorks(
22
- items: ReadonlyArray<{ projectName: string; rootSessionIds: readonly string[] }>,
32
+ items: ReadonlyArray<
33
+ | { projectName: string; rootSessionIds: readonly string[] }
34
+ | { accountWorkId: string; rootSessionIds: readonly string[] }
35
+ >,
23
36
  works: ReadonlyArray<{
24
37
  id: string
25
38
  placement?: string
@@ -257,6 +257,8 @@ export function validateLatePlacedWorks(items, works) {
257
257
  const details = []
258
258
  const conflicts = []
259
259
  items.forEach((item, index) => {
260
+ // Account Work joins are checked by `validateAccountWorkJoins`.
261
+ if (Object.hasOwn(item, 'accountWorkId')) return
260
262
  const owners = placed.filter((work) =>
261
263
  work.rootSessionIds.some((root) => item.rootSessionIds.includes(root)),
262
264
  )
@@ -276,3 +278,46 @@ export function validateLatePlacedWorks(items, works) {
276
278
  conflicts,
277
279
  })
278
280
  }
281
+
282
+ /**
283
+ * ADR 0276 slice H, mirroring `createClassificationProposal` in the CLI, which stays
284
+ * authoritative: a join item has exactly `accountWorkId` and `rootSessionIds` and names a rebuilt
285
+ * Work; after scope confirmation it takes only roots still being grouped; no item takes a root of
286
+ * a rebuilt Work. Whether the account lists a joined root's keys only the CLI can check.
287
+ */
288
+ export function validateAccountWorkJoins(items, works, confirmed) {
289
+ const rebuilt = works.filter((work) => work.accountWork)
290
+ const grouping = new Set(
291
+ works.flatMap((work) => (work.placement === 'classifying' ? work.rootSessionIds : [])),
292
+ )
293
+ const details = []
294
+ const conflicts = []
295
+ items.forEach((item, index) => {
296
+ const holders = rebuilt.filter((work) =>
297
+ work.rootSessionIds.some((root) => item.rootSessionIds.includes(root)),
298
+ )
299
+ const join = Object.hasOwn(item, 'accountWorkId')
300
+ const problem = holders.length
301
+ ? 'takes roots of a rebuilt Work'
302
+ : !join
303
+ ? null
304
+ : Object.keys(item).some((field) => !['accountWorkId', 'rootSessionIds'].includes(field))
305
+ ? 'joins with fields other than accountWorkId and rootSessionIds'
306
+ : !rebuilt.some((work) => work.id === item.accountWorkId)
307
+ ? 'names a Work that is not a rebuilt account Work'
308
+ : confirmed && item.rootSessionIds.some((root) => !grouping.has(root))
309
+ ? 'joins roots that are no longer being grouped'
310
+ : null
311
+ if (!problem) return
312
+ const workIds = holders.length ? holders.map((work) => work.id) : [item.accountWorkId]
313
+ conflicts.push({ item: index, workIds })
314
+ details.push(`items[${index}] ${problem}`)
315
+ })
316
+ // `conflicts` carries only item indexes and Work ids, so the helper may print it to stderr.
317
+ if (details.length)
318
+ throw Object.assign(new Error('CLASSIFICATION_ACCOUNT_WORK_JOIN'), {
319
+ code: 'CLASSIFICATION_ACCOUNT_WORK_JOIN',
320
+ details,
321
+ conflicts,
322
+ })
323
+ }
@@ -50,10 +50,23 @@ async function knownProjects(classification, manifest) {
50
50
  return result
51
51
  }
52
52
 
53
+ /** ADR 0276 slice H: join items, with no description, project type or recommendation (Q6). */
54
+ function accountJoinItems(classification) {
55
+ return (classification.accountJoins ?? []).map((join) => ({
56
+ accountWorkId: join.accountWorkId,
57
+ rootSessionIds: join.rootKeys.map((key) => {
58
+ const root = classification.roots.find((entry) => entry.key === key)
59
+ if (!root) throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
60
+ return root.rootSessionId
61
+ }),
62
+ }))
63
+ }
64
+
53
65
  /**
54
66
  * E2: once every known project has its metadata, its Works are published while the unresolved
55
67
  * roots are still being grouped; they wait as `pendingRootSessionIds`. Null while metadata is
56
- * missing or when nothing remains to group.
68
+ * missing or when nothing remains to group. Roots joining a rebuilt Work also wait, so the
69
+ * complete proposal joins them once from the pending Work (ADR 0276 slice H).
57
70
  */
58
71
  export async function partialClassificationSubmission(manifest) {
59
72
  const classification = manifest.classification
@@ -67,8 +80,11 @@ export async function partialClassificationSubmission(manifest) {
67
80
  if (!(await optional(item.answer))) return null
68
81
  const known = await knownProjects(classification, manifest)
69
82
  const rootByKey = new Map(classification.roots.map((root) => [root.key, root.rootSessionId]))
70
- const pendingRootSessionIds = classification.unknown.map((item) => {
71
- const id = rootByKey.get(item.rootKey)
83
+ const pendingRootSessionIds = [
84
+ ...classification.unknown.map((item) => item.rootKey),
85
+ ...(classification.accountJoins ?? []).flatMap((join) => join.rootKeys),
86
+ ].map((key) => {
87
+ const id = rootByKey.get(key)
72
88
  if (!id) throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
73
89
  return id
74
90
  })
@@ -192,10 +208,11 @@ export async function classificationSubmission(manifest) {
192
208
  })
193
209
  }
194
210
  }
211
+ const output = [...normalized(result), ...accountJoinItems(classification)]
195
212
  const expectedRootIds = classification.roots
196
213
  .map((root) => root.rootSessionId)
197
214
  .toSorted((left, right) => left.localeCompare(right))
198
- const submittedRootIds = result
215
+ const submittedRootIds = output
199
216
  .flatMap((item) => item.rootSessionIds)
200
217
  .toSorted((left, right) => left.localeCompare(right))
201
218
  if (
@@ -203,7 +220,7 @@ export async function classificationSubmission(manifest) {
203
220
  JSON.stringify(expectedRootIds) !== JSON.stringify(submittedRootIds)
204
221
  )
205
222
  throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
206
- return normalized(result)
223
+ return output
207
224
  }
208
225
 
209
226
  /** Trimmed submission items; one project name never carries two different metadata sets. */
@@ -198,7 +198,7 @@ export async function observeScanTiming(run, observation) {
198
198
  if (!receipt || receipt.completedAt) return
199
199
  const at = observation.at
200
200
  const measurements = receipt.measurements ?? {
201
- slots: { capacity: 24, busyChildMs: 0, waitingBatchWallMs: 0, refillGapMs: [], children: [] },
201
+ slots: { capacity: 48, busyChildMs: 0, waitingBatchWallMs: 0, refillGapMs: [], children: [] },
202
202
  preparation: {
203
203
  intervals: [],
204
204
  milliseconds: { 'before-first-batch': 0, 'mid-run': 0, tail: 0 },
@@ -44,6 +44,9 @@ export function isSkipped(
44
44
  work: Pick<SkipRuleWork, 'status' | 'blockedCodes' | 'inputRevision' | 'reviewInterruptions'>,
45
45
  ): boolean
46
46
  export function isIncluded(work: SkipRuleWork): boolean
47
+ export function accountFormationField(view: {
48
+ accountWorks?: { status: string; leftOut?: number; heldBack?: number }
49
+ }): { accountFormation?: { leftOutSessions: number; heldBackSessions: number } }
47
50
  export function leftOutField(view: { works: readonly SkipRuleWork[] }): {
48
51
  leftOutSessions?: number
49
52
  }
@@ -69,6 +69,13 @@ export const isSkipped = (work) =>
69
69
  * Issue #59: the sessions (roots) of every skipped, not Dropped Work, which the agent says in chat
70
70
  * before Ready to send. One number, printed only when above zero (issue #55).
71
71
  */
72
+ /** ADR 0276 D1 formation counts, as `accountFormationLines` of the panel; tell the person once. */
73
+ export const accountFormationField = (view) => {
74
+ const read = view.accountWorks?.status === 'read' ? view.accountWorks : null
75
+ return read && (read.leftOut || read.heldBack)
76
+ ? { accountFormation: { leftOutSessions: read.leftOut, heldBackSessions: read.heldBack } }
77
+ : {}
78
+ }
72
79
  export const leftOutField = (view) => {
73
80
  const sessions = view.works
74
81
  .filter((work) => work.access !== 'drop' && isSkipped(work))
@@ -120,7 +127,7 @@ export function phaseOf(view, drainedRevision) {
120
127
  if (view.phase === 'paused') return 'paused'
121
128
  if (view.phase === 'completed') return 'completed'
122
129
  if (view.phase === 'sending') return 'sending'
123
- if (!view.snapshotId) return 'freeze'
130
+ if (!view.snapshotId) return view.accountWorks?.nothingToSend ? 'nothing-to-send' : 'freeze'
124
131
  if (view.classificationRevision === null) return 'classification'
125
132
  if (view.scope.status !== 'confirmed') return 'classification-confirmation'
126
133
  // A pending Hide needs journey.prepare to recover it before anything else (ADR 0236).
@@ -12,6 +12,7 @@ import {
12
12
  interruptedReviews,
13
13
  isIncluded,
14
14
  leftOutField,
15
+ accountFormationField,
15
16
  activeTerms,
16
17
  stageFingerprint,
17
18
  procedureFor,
@@ -24,7 +25,10 @@ import {
24
25
  uncoveredPending,
25
26
  } from './journey-state.mjs'
26
27
  import { classificationTasks } from './journey-classification-tasks.mjs'
27
- import { validateLatePlacedWorks } from './journey-classification-validation.mjs'
28
+ import {
29
+ validateAccountWorkJoins,
30
+ validateLatePlacedWorks,
31
+ } from './journey-classification-validation.mjs'
28
32
  import { classificationEvidence, classificationFormat } from './journey-classification.mjs'
29
33
  import {
30
34
  consolidateClassification,
@@ -87,6 +91,27 @@ async function prepareInitialFreeze(run, journeyId, call, view) {
87
91
  } catch (error) {
88
92
  // A large first freeze can outlast one helper command; captured roots persist for the rerun.
89
93
  if (error?.code === 'CLI_UNAVAILABLE') return { stage: 'freeze', status: 'running', startedAt }
94
+ // Every session is already on the profile or left out (ADR 0276): there is no Send.
95
+ if (error?.code === 'ACCOUNT_NOTHING_TO_SEND') {
96
+ const stopped = await call('journey.get', { journeyId })
97
+ return {
98
+ stage: 'freeze',
99
+ status: 'nothing-to-send',
100
+ startedAt,
101
+ ...accountFormationField(stopped),
102
+ }
103
+ }
104
+ if (['ACCOUNT_WORKS_UNAVAILABLE', 'ACCOUNT_SIGNED_OUT'].includes(error?.code)) {
105
+ const stopped = await call('journey.get', { journeyId })
106
+ if (stopped.accountReadRecovery)
107
+ return {
108
+ stage: 'freeze',
109
+ status: 'blocked',
110
+ startedAt,
111
+ accountReadRecovery: stopped.accountReadRecovery,
112
+ next: 'journey.prepare',
113
+ }
114
+ }
90
115
  throw error
91
116
  }
92
117
  const completed = await call('journey.get', { journeyId })
@@ -194,12 +219,16 @@ async function awaitDecision(run, journeyId, waitMs, call, sleep, ensureHost) {
194
219
  command: 'await',
195
220
  from,
196
221
  phase,
222
+ ...(view.accountReadRecovery
223
+ ? { accountReadRecovery: view.accountReadRecovery, next: 'journey.prepare' }
224
+ : {}),
197
225
  changed,
198
226
  waitedMs: Date.now() - started,
199
227
  revision: view.revision,
200
228
  blockedWorks: blocked,
201
229
  ...(held.length ? { interruptedReviews: held } : {}),
202
230
  ...leftOutField(view),
231
+ ...accountFormationField(view),
203
232
  ...(retried.length ? { retriedWorks: retried, next: 'journey.prepare' } : {}),
204
233
  ...(handoff ? { handoff: { panel: await ensureHost(), next: 'end-turn' } } : {}),
205
234
  }
@@ -212,7 +241,7 @@ async function runPhaseCore(
212
241
  { run, journeyId, command, stage, refresh = false, limit = 3, waitMs = 0, sleep, ensureHost },
213
242
  call,
214
243
  ) {
215
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 24) throw new Error('INVALID_ARGUMENTS')
244
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 48) throw new Error('INVALID_ARGUMENTS')
216
245
  // Append-batch consolidation uses immutable local drafts. No current journey view is
217
246
  // needed until publication; retain write recovery and live value settlement on that path.
218
247
  if (stage === 'cards' && !refresh && ['consolidate', 'ingest'].includes(command)) {
@@ -264,12 +293,16 @@ async function runPhaseCore(
264
293
  ? { classification: await classificationProgress(manifest) }
265
294
  : {}),
266
295
  phase: await currentPhase(run, view),
296
+ ...(view.accountReadRecovery
297
+ ? { accountReadRecovery: view.accountReadRecovery, next: 'journey.prepare' }
298
+ : {}),
267
299
  revision: view.revision,
268
300
  readingComplete: view.readingComplete,
269
301
  pending: activeTerms(view).filter((term) => term.disposition === 'pending').length,
270
302
  blockedWorks: blockedWorks(view),
271
303
  ...(interruptions.length ? { interruptedReviews: interruptions } : {}),
272
304
  ...leftOutField(view),
305
+ ...accountFormationField(view),
273
306
  answersRevision: view.privacy.answersRevision,
274
307
  ...packagingProgress(view),
275
308
  coldElapsedMs,
@@ -575,11 +608,14 @@ async function runPhaseCore(
575
608
 
576
609
  /**
577
610
  * The complete proposal, checked before commit: after scope confirmation an item never joins
578
- * placed Works with different visibility (ADR 0274), which CLI would refuse.
611
+ * placed Works with different visibility (ADR 0274), and account Work joins follow ADR 0276
612
+ * slice H; CLI would refuse either.
579
613
  */
580
614
  async function classificationItems(manifest, view) {
581
615
  const items = await classificationSubmission(manifest, view)
582
- if (view.scope?.status === 'confirmed') validateLatePlacedWorks(items, view.works)
616
+ const confirmed = view.scope?.status === 'confirmed'
617
+ validateAccountWorkJoins(items, view.works, confirmed)
618
+ if (confirmed) validateLatePlacedWorks(items, view.works)
583
619
  return items
584
620
  }
585
621
 
@@ -626,6 +662,7 @@ async function ingestKnownWorks(directory, manifest, view, call, partial) {
626
662
  const terminalClassificationErrors = new Set([
627
663
  'INVALID_SELECTION',
628
664
  'CLASSIFICATION_LATE_SEVERAL_WORKS',
665
+ 'CLASSIFICATION_ACCOUNT_WORK_JOIN',
629
666
  'CLASSIFICATION_PROJECT_NAME_CONFLICT',
630
667
  'CLASSIFICATION_ROOT_COVERAGE_INVALID',
631
668
  'CLASSIFICATION_THEME_TOO_SMALL',
@@ -705,15 +742,16 @@ export async function main(argv) {
705
742
  }
706
743
  const safeId = /^[A-Za-z0-9._:-]{1,128}$/u
707
744
  /**
708
- * Stderr for a failed command: the code, and for CLASSIFICATION_LATE_SEVERAL_WORKS one line per
709
- * refused item with its index and the placed Work ids, never a title or path (ADR 0274).
745
+ * Stderr for a failed command: the code, and for CLASSIFICATION_LATE_SEVERAL_WORKS and
746
+ * CLASSIFICATION_ACCOUNT_WORK_JOIN one line per refused item with its index and the Work ids,
747
+ * never a title or path (ADR 0274, ADR 0276 slice H).
710
748
  */
711
749
  export function failureOutput(error) {
712
750
  const message = error?.message ?? ''
713
751
  if (error?.code?.startsWith('ERR_PARSE_ARGS')) return 'INVALID_ARGUMENTS\n'
714
752
  const code = /^[A-Z_]{3,80}$/u.test(message) ? message : 'JOURNEY_HELPER_FAILED'
715
753
  const lines = [code]
716
- if (code === 'CLASSIFICATION_LATE_SEVERAL_WORKS')
754
+ if (code === 'CLASSIFICATION_LATE_SEVERAL_WORKS' || code === 'CLASSIFICATION_ACCOUNT_WORK_JOIN')
717
755
  for (const conflict of error.conflicts ?? [])
718
756
  if (Number.isInteger(conflict.item))
719
757
  lines.push(