@gitroll/tacitt-cli 0.6.8 → 0.6.10

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 (46) hide show
  1. package/README.md +1 -1
  2. package/dist/{agent-setup-YlFIJz-h.mjs → agent-setup-LPd8WEsN.mjs} +2 -2
  3. package/dist/agent-setup-worker.mjs +5 -5
  4. package/dist/cli.mjs +19 -10
  5. package/dist/codex-title-worker.mjs +1 -1
  6. package/dist/configured-preview-worker.mjs +5 -5
  7. package/dist/configured-session-worker.mjs +5 -5
  8. package/dist/{frame-Dj5Zs6UW.mjs → frame-ByT1xayJ.mjs} +1 -1
  9. package/dist/{heap-DsE74_-n.mjs → heap-CM298Yu0.mjs} +8 -6
  10. package/dist/inspection-worker.mjs +4 -4
  11. package/dist/{job-CeSf9UCC.mjs → job-BHIwElNF.mjs} +5 -5
  12. package/dist/{locate-BWkblijS.mjs → locate-Cwv6WH8n.mjs} +25 -2
  13. package/dist/{model-f_QnDyyb.mjs → model-zvn5GUJ9.mjs} +1 -1
  14. package/dist/operation-executor.mjs +3 -3
  15. package/dist/preview-worker.mjs +1 -1
  16. package/dist/privacy-validator.mjs +1 -1
  17. package/dist/{query-CaBXQlpF.mjs → query-By41gaKm.mjs} +585 -250
  18. package/dist/{run-RA_8zZsL.mjs → run-ZQx7p4Rw.mjs} +4 -4
  19. package/dist/{src-p_uJNjV3.mjs → src-3jvMvSQn.mjs} +3 -3
  20. package/dist/{src-d7n58sbH.mjs → src-BqtdFVq7.mjs} +683 -341
  21. package/dist/{src-BBjnljUs.mjs → src-D_DfY--2.mjs} +4 -4
  22. package/dist/store-database-worker.mjs +1 -1
  23. package/dist/web/assets/index-B14e6_-z.js +174 -0
  24. package/dist/web/index.html +1 -1
  25. package/package.json +1 -1
  26. package/skills/tacitt/INDEX.md +6 -6
  27. package/skills/tacitt/SKILL.md +4 -4
  28. package/skills/tacitt/references/INDEX.md +3 -3
  29. package/skills/tacitt/references/classification.md +12 -2
  30. package/skills/tacitt/references/commands.md +60 -13
  31. package/skills/tacitt/references/journey.md +10 -2
  32. package/skills/tacitt/references/parallel-review.md +21 -0
  33. package/skills/tacitt/references/send.md +29 -8
  34. package/skills/tacitt/scripts/INDEX.md +39 -36
  35. package/skills/tacitt/scripts/journey-classification-validation.d.mts +13 -0
  36. package/skills/tacitt/scripts/journey-classification-validation.mjs +38 -0
  37. package/skills/tacitt/scripts/journey-scan-timing.d.mts +15 -0
  38. package/skills/tacitt/scripts/journey-scan-timing.mjs +153 -1
  39. package/skills/tacitt/scripts/journey-state.d.mts +6 -0
  40. package/skills/tacitt/scripts/journey-state.mjs +24 -1
  41. package/skills/tacitt/scripts/journey.d.mts +5 -0
  42. package/skills/tacitt/scripts/journey.mjs +48 -3
  43. package/skills/tacitt/scripts/privacy-validator.mjs +11 -1
  44. package/skills/tacitt/scripts/review.mjs +294 -38
  45. package/LICENSE +0 -11
  46. package/dist/web/assets/index-B6kJ6XXF.js +0 -174
@@ -3,45 +3,48 @@
3
3
  > Audience: agents and maintainers
4
4
  > Ownership: contributor-maintained
5
5
 
6
- | Entry | Kind | Responsibility |
7
- | ---------------------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8
- | [INDEX.md](INDEX.md) | Directory index | Lists executable helpers shipped with the exact Skill release. |
9
- | [journey-scan-timing.mjs](journey-scan-timing.mjs) | Runtime measurement | Scope-bound cold static/AI/metadata/card wall clock, finite start and exact completion observation. |
10
- | [describe.mjs](describe.mjs) | Runtime | Binds proposed generated metadata, never Hidden, to the current decision/context. |
11
- | [local-cli.mjs](local-cli.mjs) | Read-only package verifier | Validates an explicit installed CLI identity and aggregate artifact digest without network or application-state access. |
12
- | [review.mjs](review.mjs) | Runtime | Session-ready scanning, sixteen-slot refill, outcome-reported preparation recovery and read-only status, metadata-gated questions, Track B-gated final preview and hash-bound answer recovery. |
13
- | [review.d.mts](review.d.mts) | Type declarations | Types the review helper's exported task rendering, answer reading, per-Work progress and entry functions for tests. |
14
- | [local-cli.d.mts](local-cli.d.mts) | Type declarations | Defines the shared installed-package binding used by the CLI and standalone verifier. |
15
- | [verify-cli.mjs](verify-cli.mjs) | Verification entry | Checks a managed local receipt and its current package bytes before the Skill starts a journey. |
16
- | [journey.mjs](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 and shared CLI transport. |
17
- | [journey.d.mts](journey.d.mts) | Declaration | Phase helper contract for integration tests. |
18
- | [journey-panel-host.mjs](journey-panel-host.mjs) | Runtime | Runs the paired CLI's `journey.serve --detach` before the handoff so a detached host owns the panel (ADR 0227). |
19
- | [journey-panel-host.d.mts](journey-panel-host.d.mts) | Declaration | Types the detached-host handoff for helper tests. |
20
- | [journey-state.mjs](journey-state.mjs) | Runtime state | Confirmed-answer and static-finishing phase selection, skip and included rules (ADR 0236), card batch limits, threshold and wait clock (ADR 0234), input fingerprints and exact pending-write recovery with affected-Pending-only resolution. |
21
- | [answer.mjs](answer.mjs) | CLI publisher | Validates task-bound classification and privacy drafts, including required privacy unit coverage and reused-card policy with repair diagnostics, reports card issue paths without mutating validator errors, and atomically publishes canonical answers. |
22
- | [journey-tasks.mjs](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. |
23
- | [journey-tasks.d.mts](journey-tasks.d.mts) | Declaration | Types task creation, rendered-token sizing, canonical answer metadata and batch answer reads. |
24
- | [journey-classification.mjs](journey-classification.mjs) | Runtime | Batched known-project metadata, unknown-root grouping and bounded evidence, versioned checkpoints and generation progress. |
25
- | [journey-consolidation.mjs](journey-consolidation.mjs) | Runtime | Builds proposed-name and stable-target candidate buckets, persists complete reconciliation input, and returns compact dispatch descriptors. |
26
- | [journey-classification-metadata.mjs](journey-classification-metadata.mjs) | Runtime | Validates global reconciliation and creates hash-bound, token-bounded AI metadata tasks for new Works and Unsorted. |
27
- | [journey-classification-metadata.d.mts](journey-classification-metadata.d.mts) | Declaration | Types generated AI metadata tasks for final project groups and Unsorted. |
28
- | [journey-consolidation-submit.mjs](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. |
29
- | [journey-consolidation.d.mts](journey-consolidation.d.mts) | Declaration | Consolidation helper contract for bounded evidence regression tests. |
30
- | [journey-classification-tasks.mjs](journey-classification-tasks.mjs) | Runtime | Original-root routing, capacity-balanced bounded tasks and unresolved-first ready ordering with stable assignments. |
31
- | [journey-classification-tasks.d.mts](journey-classification-tasks.d.mts) | Declaration | Types initial classification routing and catalog/evidence task generation for helper consumers and tests. |
32
- | [journey-classification-progress.mjs](journey-classification-progress.mjs) | Runtime | Accepted-answer counters and fenced progress checkpoints. |
33
- | [journey-state.d.mts](journey-state.d.mts) | Declaration | Journey state helper contracts used by typed recovery tests. |
34
- | [journey-classification-validation.mjs](journey-classification-validation.mjs) | Runtime | Shared root judgment, stable target identity, fixed project metadata, consolidation validation and target-preserving assignment expansion. |
35
- | [journey-classification-validation.d.mts](journey-classification-validation.d.mts) | Declaration | Shared classification validator contracts. |
36
- | [journey-card-consolidation.mjs](journey-card-consolidation.mjs) | Runtime | Immutably merges matching existing-policy extensions without a second model task (ADR 0251); policy or identity mismatches fall back to digest-bound global review, with exact key coverage and five/ten question limits. |
37
- | [journey-card-consolidation.d.mts](journey-card-consolidation.d.mts) | Declaration | Types global card generation, policy comparison, identity boundaries, answer validation and final key expansion. |
38
- | [journey-card-values.mjs](journey-card-values.mjs) | Runtime | Settles a card publication against the current journey: only still-open values, one card per normalized value, own identity on its own card by the journey-wide own-value rule, distinct short examples. |
39
- | [journey-card-values.d.mts](journey-card-values.d.mts) | Declaration | Types card value settlement for helper tests. |
40
- | [review-stream-client.mjs](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. |
41
- | [review-stream-client.d.mts](review-stream-client.d.mts) | Type declarations | Types the fixed request/shutdown limits, private lifecycle callbacks and review-stream client methods. |
6
+ | Entry | Kind | Responsibility |
7
+ | ---------------------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8
+ | [INDEX.md](INDEX.md) | Directory index | Lists executable helpers shipped with the exact Skill release. |
9
+ | [journey-scan-timing.d.mts](journey-scan-timing.d.mts) | Declaration | Content-free scan observation interface for helper measurements and tests. |
10
+ | [journey-scan-timing.mjs](journey-scan-timing.mjs) | Runtime measurement | Scope-bound scan wall clock plus content-free slot occupancy, ingest latency and preparation wait observations. |
11
+ | [describe.mjs](describe.mjs) | Runtime | Binds proposed generated metadata, never Hidden, to the current decision/context. |
12
+ | [local-cli.mjs](local-cli.mjs) | Read-only package verifier | Validates an explicit installed CLI identity and aggregate artifact digest without network or application-state access. |
13
+ | [review.mjs](review.mjs) | Runtime | Session-ready scanning, immediate sixteen-slot refill, draft heartbeat and acknowledged lost-pack 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. |
14
+ | [review.d.mts](review.d.mts) | Type declarations | Types the review helper's exported task rendering, answer reading, per-Work progress and entry functions for tests. |
15
+ | [local-cli.d.mts](local-cli.d.mts) | Type declarations | Defines the shared installed-package binding used by the CLI and standalone verifier. |
16
+ | [verify-cli.mjs](verify-cli.mjs) | Verification entry | Checks a managed local receipt and its current package bytes before the Skill starts a journey. |
17
+ | [journey.mjs](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. |
18
+ | [journey.d.mts](journey.d.mts) | Declaration | Phase helper contract for integration tests. |
19
+ | [journey-panel-host.mjs](journey-panel-host.mjs) | Runtime | Runs the paired CLI's `journey.serve --detach` before the handoff so a detached host owns the panel (ADR 0227). |
20
+ | [journey-panel-host.d.mts](journey-panel-host.d.mts) | Declaration | Types the detached-host handoff for helper tests. |
21
+ | [journey-state.mjs](journey-state.mjs) | Runtime state | Pending-Public-choice, confirmed-answer and static-finishing phase selection, skip and included rules (ADR 0236), card batch limits, threshold and wait clock (ADR 0234), input fingerprints and exact pending-write recovery including definite scope rejection, with affected-Pending-only resolution. |
22
+ | [answer.mjs](answer.mjs) | CLI publisher | Validates task-bound classification and privacy drafts, including required privacy unit coverage and reused-card policy with repair diagnostics, reports card issue paths without mutating validator errors, and atomically publishes canonical answers. |
23
+ | [journey-tasks.mjs](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. |
24
+ | [journey-tasks.d.mts](journey-tasks.d.mts) | Declaration | Types task creation, rendered-token sizing, canonical answer metadata and batch answer reads. |
25
+ | [journey-classification.mjs](journey-classification.mjs) | Runtime | Batched known-project metadata, unknown-root grouping and bounded evidence, versioned checkpoints and generation progress. |
26
+ | [journey-consolidation.mjs](journey-consolidation.mjs) | Runtime | Builds proposed-name and stable-target candidate buckets, persists complete reconciliation input, and returns compact dispatch descriptors. |
27
+ | [journey-classification-metadata.mjs](journey-classification-metadata.mjs) | Runtime | Validates global reconciliation and creates hash-bound, token-bounded AI metadata tasks for new Works and Unsorted. |
28
+ | [journey-classification-metadata.d.mts](journey-classification-metadata.d.mts) | Declaration | Types generated AI metadata tasks for final project groups and Unsorted. |
29
+ | [journey-consolidation-submit.mjs](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. |
30
+ | [journey-consolidation.d.mts](journey-consolidation.d.mts) | Declaration | Consolidation helper contract for bounded evidence regression tests. |
31
+ | [journey-classification-tasks.mjs](journey-classification-tasks.mjs) | Runtime | Original-root routing, capacity-balanced bounded tasks and unresolved-first ready ordering with stable assignments. |
32
+ | [journey-classification-tasks.d.mts](journey-classification-tasks.d.mts) | Declaration | Types initial classification routing and catalog/evidence task generation for helper consumers and tests. |
33
+ | [journey-classification-progress.mjs](journey-classification-progress.mjs) | Runtime | Accepted-answer counters and fenced progress checkpoints. |
34
+ | [journey-state.d.mts](journey-state.d.mts) | Declaration | Journey state helper contracts used by typed recovery tests. |
35
+ | [journey-classification-validation.mjs](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). |
36
+ | [journey-classification-validation.d.mts](journey-classification-validation.d.mts) | Declaration | Shared classification validator contracts. |
37
+ | [journey-card-consolidation.mjs](journey-card-consolidation.mjs) | Runtime | Immutably merges matching existing-policy extensions without a second model task (ADR 0251); policy or identity mismatches fall back to digest-bound global review, with exact key coverage and five/ten question limits. |
38
+ | [journey-card-consolidation.d.mts](journey-card-consolidation.d.mts) | Declaration | Types global card generation, policy comparison, identity boundaries, answer validation and final key expansion. |
39
+ | [journey-card-values.mjs](journey-card-values.mjs) | Runtime | Settles a card publication against the current journey: only still-open values, one card per normalized value, own identity on its own card by the journey-wide own-value rule, distinct short examples. |
40
+ | [journey-card-values.d.mts](journey-card-values.d.mts) | Declaration | Types card value settlement for helper tests. |
41
+ | [review-stream-client.mjs](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. |
42
+ | [review-stream-client.d.mts](review-stream-client.d.mts) | Type declarations | Types the fixed request/shutdown limits, private lifecycle callbacks and review-stream client methods. |
42
43
 
43
44
  `review.mjs` also owns bounded ingest-and-refill stream reuse and slot-limited recovery of reserved packs.
44
45
 
45
46
  `journey-tasks.mjs` preserves exact greedy card/resolution boundaries using bounded prefix search rather than serializing every growing prefix.
46
47
 
48
+ `journey.mjs` uses local immutable drafts for append-card consolidation and incomplete ingest when no request or receipt needs recovery; ready publication retains live-state settlement.
49
+
47
50
  `review.mjs` amortizes small-task instructions and fills confirmed free native slots before distributing additional complete units by current pack load.
@@ -18,3 +18,16 @@ export function validateConsolidationAnswer(
18
18
  projectCatalog: unknown[]
19
19
  },
20
20
  ): { code: string | null; details: string[] }
21
+ export function validateLatePlacedWorks(
22
+ items: ReadonlyArray<{ projectName: string; rootSessionIds: readonly string[] }>,
23
+ works: ReadonlyArray<{
24
+ id: string
25
+ placement?: string
26
+ rootSessionIds: readonly string[]
27
+ access: string
28
+ accessOrigin?: string
29
+ accessConfirmed?: boolean
30
+ accessChoice?: { channel: string; at: string }
31
+ accountAccessConflict?: boolean
32
+ }>,
33
+ ): void
@@ -238,3 +238,41 @@ export function compileAssignments(candidates, assignments, overrides = []) {
238
238
  for (const override of overrides) result.set(override.rootKey, override.groupId)
239
239
  return result
240
240
  }
241
+
242
+ const sameAccessRecord = (work, other) =>
243
+ work.access === other.access &&
244
+ work.accessOrigin === other.accessOrigin &&
245
+ work.accessConfirmed === other.accessConfirmed &&
246
+ work.accessChoice?.channel === other.accessChoice?.channel &&
247
+ work.accessChoice?.at === other.accessChoice?.at &&
248
+ work.accountAccessConflict === other.accountAccessConflict
249
+
250
+ /**
251
+ * ADR 0274: after scope confirmation one item may hold roots of several placed Works only when
252
+ * they share one access record (an isolated Work and its parent). Mirrors `sameAccessRecord` in
253
+ * `@sess-pilot/application` scope/proposal.ts, which refuses the commit otherwise.
254
+ */
255
+ export function validateLatePlacedWorks(items, works) {
256
+ const placed = works.filter((work) => work.placement === 'placed')
257
+ const details = []
258
+ const conflicts = []
259
+ items.forEach((item, index) => {
260
+ const owners = placed.filter((work) =>
261
+ work.rootSessionIds.some((root) => item.rootSessionIds.includes(root)),
262
+ )
263
+ if (!owners.some((work) => !sameAccessRecord(work, owners[0]))) return
264
+ conflicts.push({ item: index, workIds: owners.map((work) => work.id) })
265
+ details.push(
266
+ `items[${index}] (${item.projectName}) joins roots of Works ${owners
267
+ .map((work) => work.id)
268
+ .join(', ')}, which have different visibility; keep each Work's roots in its own item`,
269
+ )
270
+ })
271
+ // `conflicts` carries only item indexes and Work ids, so the helper may print it to stderr.
272
+ if (details.length)
273
+ throw Object.assign(new Error('CLASSIFICATION_LATE_SEVERAL_WORKS'), {
274
+ code: 'CLASSIFICATION_LATE_SEVERAL_WORKS',
275
+ details,
276
+ conflicts,
277
+ })
278
+ }
@@ -0,0 +1,15 @@
1
+ export interface ScanTimingObservation {
2
+ at: number
3
+ packs: readonly {
4
+ id: string
5
+ dispatchedAt: number | string
6
+ publishedAt: number | null
7
+ }[]
8
+ activeCardChildren: number
9
+ queuedBatches: number
10
+ unreadBatches: number
11
+ waiting: string | null | undefined
12
+ pendingWorkIds: readonly string[]
13
+ ingestStartedAt?: number
14
+ }
15
+ export function observeScanTiming(run: string, observation: ScanTimingObservation): Promise<void>
@@ -119,6 +119,7 @@ export async function scanTiming(run, journeyId, view, start = false, call) {
119
119
  if (receipt.binding !== identity || (authority !== null && receipt.authority !== authority))
120
120
  throw new Error('SCAN_TIMING_SCOPE_CHANGED')
121
121
  if (!receipt.completedAt && complete(view)) {
122
+ closePreparationInterval(receipt.measurements?.preparation, Date.now())
122
123
  receipt = {
123
124
  ...receipt,
124
125
  completedAt: new Date().toISOString(),
@@ -130,9 +131,10 @@ export async function scanTiming(run, journeyId, view, start = false, call) {
130
131
  }
131
132
  const elapsedMs =
132
133
  Date.parse(receipt.completedAt ?? new Date().toISOString()) - Date.parse(receipt.startedAt)
133
- const { initialWorks, finalWorks, ...summary } = receipt
134
+ const { initialWorks, finalWorks, measurements, ...summary } = receipt
134
135
  return {
135
136
  ...summary,
137
+ ...(measurements ? { measurements: measurementSummary(measurements) } : {}),
136
138
  initialWorkCount: initialWorks.length,
137
139
  ...(finalWorks ? { finalWorkCount: finalWorks.length } : {}),
138
140
  receipt: path,
@@ -142,3 +144,153 @@ export async function scanTiming(run, journeyId, view, start = false, call) {
142
144
  withinBudget: receipt.completedAt ? elapsedMs < 30 * 60 * 1000 : null,
143
145
  }
144
146
  }
147
+
148
+ // Keep per-child and per-ingest evidence in the receipt, outside repeated parent replies.
149
+ function measurementSummary(measurements) {
150
+ const { slots, preparation } = measurements
151
+ return {
152
+ observationBasis: measurements.observationBasis,
153
+ slots: {
154
+ capacity: slots.capacity,
155
+ busyChildMs: slots.busyChildMs,
156
+ waitingBatchWallMs: slots.waitingBatchWallMs,
157
+ occupancy: slots.occupancy,
158
+ refillGapPercentilesMs: slots.refillGapPercentilesMs,
159
+ childCount: slots.children.length,
160
+ },
161
+ preparation: {
162
+ milliseconds: preparation.milliseconds,
163
+ intervalCount: preparation.intervals.length,
164
+ },
165
+ ingestCount: measurements.ingests.length,
166
+ ingestPercentilesMs: measurements.ingestPercentilesMs,
167
+ }
168
+ }
169
+
170
+ function closePreparationInterval(preparation, at) {
171
+ const interval = preparation?.intervals.at(-1)
172
+ if (interval?.endedAt === null) {
173
+ interval.endedAt = at
174
+ preparation.milliseconds[interval.phase] += Math.max(0, at - interval.startedAt)
175
+ }
176
+ }
177
+
178
+ function percentiles(values) {
179
+ const sorted = [...values].sort((a, b) => a - b)
180
+ const percentile = (fraction) =>
181
+ sorted[Math.max(0, Math.ceil(sorted.length * fraction) - 1)] ?? null
182
+ return { p50: percentile(0.5), p95: percentile(0.95) }
183
+ }
184
+
185
+ /** Content-free helper observations; publication timestamps come from answer-file metadata.
186
+ * Card occupancy and slot release are observed at helper boundaries, not native scheduler events.
187
+ * The RPC protocol does not expose store lock wait, so it remains explicitly unavailable.
188
+ */
189
+ export async function observeScanTiming(run, observation) {
190
+ const path = join(run, 'scan-timing.json')
191
+ const receipt = await optional(path)
192
+ if (!receipt || receipt.completedAt) return
193
+ const at = observation.at
194
+ const measurements = receipt.measurements ?? {
195
+ slots: { capacity: 16, busyChildMs: 0, waitingBatchWallMs: 0, refillGapMs: [], children: [] },
196
+ preparation: {
197
+ intervals: [],
198
+ milliseconds: { 'before-first-batch': 0, 'mid-run': 0, tail: 0 },
199
+ },
200
+ ingests: [],
201
+ observationBasis: {
202
+ dispatch: 'helper-reservation',
203
+ publication: 'answer-file-mtime',
204
+ slotRelease: 'helper-observed-publication-or-retirement',
205
+ cards: 'parent-reported-count-at-helper-boundary',
206
+ preparation: 'helper-observed-interval',
207
+ lockWait: 'unavailable-in-rpc',
208
+ },
209
+ seenBatch: false,
210
+ last: null,
211
+ }
212
+ const { slots, preparation, last } = measurements
213
+ const packs = observation.packs.map((pack) => ({
214
+ id: hash(pack.id),
215
+ dispatchedAt:
216
+ typeof pack.dispatchedAt === 'number' ? pack.dispatchedAt : Date.parse(pack.dispatchedAt),
217
+ publishedAt: pack.publishedAt ?? null,
218
+ }))
219
+ for (const pack of packs) {
220
+ let child = slots.children.find((item) => item.id === pack.id)
221
+ if (!child) {
222
+ const freed = slots.children.find(
223
+ (item) =>
224
+ item.freedAt !== null &&
225
+ item.freedAt <= pack.dispatchedAt &&
226
+ item.nextDispatchAt === null,
227
+ )
228
+ if (freed) {
229
+ freed.nextDispatchAt = pack.dispatchedAt
230
+ slots.refillGapMs.push(Math.max(0, pack.dispatchedAt - freed.freedAt))
231
+ }
232
+ child = { ...pack, freedAt: null, nextDispatchAt: null }
233
+ slots.children.push(child)
234
+ }
235
+ if (pack.publishedAt !== null) {
236
+ child.publishedAt = pack.publishedAt
237
+ child.freedAt ??= at
238
+ }
239
+ }
240
+ // A retired/cancelled child also frees a slot, without claiming it published an answer.
241
+ for (const child of slots.children)
242
+ if (child.freedAt === null && !packs.some((pack) => pack.id === child.id)) child.freedAt = at
243
+ if (last && at >= last.at && last.batchesWaiting) {
244
+ const elapsed = at - last.at
245
+ slots.waitingBatchWallMs += elapsed
246
+ slots.busyChildMs += elapsed * last.activeCardChildren
247
+ for (const id of last.activePackIds) {
248
+ const child = slots.children.find((item) => item.id === id)
249
+ if (child) slots.busyChildMs += Math.max(0, Math.min(at, child.publishedAt ?? at) - last.at)
250
+ }
251
+ }
252
+ slots.occupancy = slots.waitingBatchWallMs
253
+ ? slots.busyChildMs / (slots.capacity * slots.waitingBatchWallMs)
254
+ : null
255
+ const activePacks = packs.filter((pack) => pack.publishedAt === null)
256
+ const batchesWaiting = observation.queuedBatches > 0 || observation.unreadBatches > 0
257
+ measurements.seenBatch ||= batchesWaiting || packs.length > 0
258
+ const phase = !measurements.seenBatch
259
+ ? 'before-first-batch'
260
+ : batchesWaiting || activePacks.length
261
+ ? 'mid-run'
262
+ : 'tail'
263
+ const pendingWorkHashes = [...new Set(observation.pendingWorkIds.map((id) => hash(id)))].sort(
264
+ (a, b) => a.localeCompare(b),
265
+ )
266
+ const waiting = observation.waiting === 'preparation'
267
+ const interval = preparation.intervals.at(-1)
268
+ if (
269
+ interval?.endedAt === null &&
270
+ (!waiting ||
271
+ interval.phase !== phase ||
272
+ JSON.stringify(interval.pendingWorkHashes) !== JSON.stringify(pendingWorkHashes))
273
+ ) {
274
+ closePreparationInterval(preparation, at)
275
+ }
276
+ if (waiting && (!interval || interval.endedAt !== null))
277
+ preparation.intervals.push({ phase, startedAt: at, endedAt: null, pendingWorkHashes })
278
+ if (observation.ingestStartedAt !== undefined)
279
+ measurements.ingests.push({
280
+ startedAt: observation.ingestStartedAt,
281
+ completedAt: at,
282
+ elapsedMs: Math.max(0, at - observation.ingestStartedAt),
283
+ lockWaitMs: null,
284
+ })
285
+ slots.refillGapPercentilesMs = percentiles(slots.refillGapMs)
286
+ measurements.ingestPercentilesMs = percentiles(
287
+ measurements.ingests.map((ingest) => ingest.elapsedMs),
288
+ )
289
+ measurements.last = {
290
+ at,
291
+ batchesWaiting,
292
+ activeCardChildren: observation.activeCardChildren,
293
+ activePackIds: activePacks.map((pack) => pack.id),
294
+ }
295
+ await save(path, { ...receipt, measurements })
296
+ }
@@ -40,6 +40,12 @@ type SkipRuleWork = {
40
40
  }
41
41
  export function isSkipped(work: Pick<SkipRuleWork, 'status' | 'blockedCodes'>): boolean
42
42
  export function isIncluded(work: SkipRuleWork): boolean
43
+ export function hasPublicChoice(work: {
44
+ access: string
45
+ accessConfirmed?: boolean
46
+ accessOrigin?: string
47
+ accessChoice?: unknown
48
+ }): boolean
43
49
  export function definiteWriteRejection(error: unknown): boolean
44
50
  export function stageFingerprint(view: unknown, stage: string): string
45
51
  export function planResolutions(
@@ -33,6 +33,17 @@ export const isIncluded = (work) =>
33
33
  work.rootSessionIds.length > 0 &&
34
34
  work.placement !== 'classifying' &&
35
35
  !isSkipped(work)
36
+ /**
37
+ * Public needs a recorded choice (ADR 0274): the person's, or the account's when every root was
38
+ * already Public (ADR 0275), as `hasPublicChoice` of `@sess-pilot/application`;
39
+ * journey-helper.test.ts checks the older-format and account cases.
40
+ */
41
+ export const hasPublicChoice = (work) =>
42
+ work.access !== 'public' ||
43
+ (work.accessConfirmed === true &&
44
+ work.accessChoice !== undefined &&
45
+ work.accessChoice !== null &&
46
+ work.accessOrigin === (work.accessChoice.channel === 'account' ? 'account' : 'explicit'))
36
47
  /** A journey-wide dictionary remains relevant while any Work is selected. */
37
48
  export function activeTerms(view) {
38
49
  return view.works.some((work) => work.access !== 'drop' && work.rootSessionIds.length)
@@ -62,6 +73,13 @@ export function phaseOf(view) {
62
73
  const selected = view.works.filter(isIncluded)
63
74
  // Every Work failed to read or was Dropped: there is no Send, only Retry or un-Drop in the panel.
64
75
  if (!selected.length) return 'nothing-to-send'
76
+ if (
77
+ selected.some(
78
+ (work) =>
79
+ (work.access === 'private' && work.accessConfirmed === false) || !hasPublicChoice(work),
80
+ )
81
+ )
82
+ return 'public-choice'
65
83
  // Track B builds each Work's private packaging base in the background (ADR 0171).
66
84
  if (selected.some((work) => !work.staticReview)) return 'static-finishing'
67
85
  // The person's Send click packages and uploads (D5); the agent has nothing left to run.
@@ -186,9 +204,14 @@ export const procedureFor = (stage) =>
186
204
  resolution: 'journey.resolve',
187
205
  })[stage]
188
206
 
189
- /** These authoritative failures cannot have committed a phase write. */
207
+ /**
208
+ * Record these explicit refusals as rejected. INVALID_SELECTION alone does not prove an
209
+ * earlier attempt never committed: append replay relies on the service returning its receipt
210
+ * before surfacing a refusal from validation against newer state.
211
+ */
190
212
  export const definiteWriteRejection = (error) =>
191
213
  [
214
+ 'SCOPE_NOT_READY',
192
215
  'INVALID_SELECTION',
193
216
  'INVALID_ARGUMENTS',
194
217
  'INVALID_REVIEW_SPAN',
@@ -17,6 +17,9 @@ export function phaseOf(view: {
17
17
  metadataReviewedDigest?: string
18
18
  classificationMetadata?: { digest: string }
19
19
  access: string
20
+ accessOrigin?: string
21
+ accessConfirmed?: boolean
22
+ accessChoice?: unknown
20
23
  rootSessionIds: readonly string[]
21
24
  status?: string
22
25
  blockedCodes?: readonly string[]
@@ -27,6 +30,8 @@ export function phaseOf(view: {
27
30
  }[]
28
31
  }): string
29
32
  export function main(argv: string[]): Promise<Record<string, unknown>>
33
+ /** The helper's stderr for a failed command: the code and, for a late join, safe ids only. */
34
+ export function failureOutput(error: unknown): string
30
35
 
31
36
  export function stageFingerprint(view: unknown, stage: string): string
32
37
  export function runPhase(
@@ -22,6 +22,7 @@ import {
22
22
  uncoveredPending,
23
23
  } from './journey-state.mjs'
24
24
  import { classificationTasks } from './journey-classification-tasks.mjs'
25
+ import { validateLatePlacedWorks } from './journey-classification-validation.mjs'
25
26
  import { classificationEvidence, classificationFormat } from './journey-classification.mjs'
26
27
  import {
27
28
  consolidateClassification,
@@ -197,6 +198,21 @@ async function runPhaseCore(
197
198
  call,
198
199
  ) {
199
200
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 16) throw new Error('INVALID_ARGUMENTS')
201
+ // Append-batch consolidation uses immutable local drafts. No current journey view is
202
+ // needed until publication; retain write recovery and live value settlement on that path.
203
+ if (stage === 'cards' && !refresh && ['consolidate', 'ingest'].includes(command)) {
204
+ const active = await optional(join(run, 'cards-active.json'))
205
+ const manifest = active ? await optional(join(active.directory, 'manifest.json')) : null
206
+ if (
207
+ manifest?.format === classificationFormat &&
208
+ manifest.batch &&
209
+ !(await optional(join(active.directory, 'request.json'))) &&
210
+ !(await optional(join(active.directory, 'receipt.json')))
211
+ ) {
212
+ const readiness = await consolidateCards(active.directory, manifest)
213
+ if (command === 'consolidate' || !readiness.readyForIngest) return readiness
214
+ }
215
+ }
200
216
  if (command === 'await')
201
217
  return awaitDecision(
202
218
  journeyId,
@@ -499,7 +515,7 @@ async function runPhaseCore(
499
515
  ? { expectedClassificationRevision: manifest.partial.revision }
500
516
  : {}),
501
517
  ...(stage === 'classification'
502
- ? { [key]: await classificationSubmission(manifest, view) }
518
+ ? { [key]: await classificationItems(manifest, view) }
503
519
  : stage === 'cards'
504
520
  ? cards
505
521
  : { [key]: await answers(tasks, key) }),
@@ -534,6 +550,16 @@ async function runPhaseCore(
534
550
  return { stage, status: result.status, revision: result.state?.revision }
535
551
  }
536
552
 
553
+ /**
554
+ * The complete proposal, checked before commit: after scope confirmation an item never joins
555
+ * placed Works with different visibility (ADR 0274), which CLI would refuse.
556
+ */
557
+ async function classificationItems(manifest, view) {
558
+ const items = await classificationSubmission(manifest, view)
559
+ if (view.scope?.status === 'confirmed') validateLatePlacedWorks(items, view.works)
560
+ return items
561
+ }
562
+
537
563
  /** Classification published known Works and is still grouping the remaining roots (E2). */
538
564
  const grouping = (view) => view.works.some((work) => work.placement === 'classifying')
539
565
 
@@ -576,6 +602,7 @@ async function ingestKnownWorks(directory, manifest, view, call, partial) {
576
602
 
577
603
  const terminalClassificationErrors = new Set([
578
604
  'INVALID_SELECTION',
605
+ 'CLASSIFICATION_LATE_SEVERAL_WORKS',
579
606
  'CLASSIFICATION_PROJECT_NAME_CONFLICT',
580
607
  'CLASSIFICATION_ROOT_COVERAGE_INVALID',
581
608
  'CLASSIFICATION_THEME_TOO_SMALL',
@@ -650,13 +677,31 @@ export async function main(argv) {
650
677
  ),
651
678
  )
652
679
  }
680
+ const safeId = /^[A-Za-z0-9._:-]{1,128}$/u
681
+ /**
682
+ * Stderr for a failed command: the code, and for CLASSIFICATION_LATE_SEVERAL_WORKS one line per
683
+ * refused item with its index and the placed Work ids, never a title or path (ADR 0274).
684
+ */
685
+ export function failureOutput(error) {
686
+ const message = error?.message ?? ''
687
+ const code = /^[A-Z_]{3,80}$/u.test(message) ? message : 'JOURNEY_HELPER_FAILED'
688
+ const lines = [code]
689
+ if (code === 'CLASSIFICATION_LATE_SEVERAL_WORKS')
690
+ for (const conflict of error.conflicts ?? [])
691
+ if (Number.isInteger(conflict.item))
692
+ lines.push(
693
+ `items[${conflict.item}] joins Works ${conflict.workIds
694
+ .map((id) => (safeId.test(id) ? id : '?'))
695
+ .join(', ')}`,
696
+ )
697
+ return lines.join('\n') + '\n'
698
+ }
653
699
  const entry = process.argv[1]
654
700
  if (entry && (await realpath(entry)) === fileURLToPath(import.meta.url))
655
701
  main(process.argv.slice(2)).then(
656
702
  (result) => process.stdout.write(JSON.stringify({ ok: true, ...result }) + '\n'),
657
703
  (error) => {
658
- const code = /^[A-Z_]{3,80}$/u.test(error.message) ? error.message : 'JOURNEY_HELPER_FAILED'
659
- process.stderr.write(code + '\n')
704
+ process.stderr.write(failureOutput(error))
660
705
  process.exitCode = 1
661
706
  },
662
707
  )
@@ -10131,7 +10131,8 @@ const journeyWorkSchema = object({
10131
10131
  "default",
10132
10132
  "model",
10133
10133
  "explicit",
10134
- "delegate"
10134
+ "delegate",
10135
+ "account"
10135
10136
  ]),
10136
10137
  placement: _enum([
10137
10138
  "placed",
@@ -10148,6 +10149,15 @@ const journeyWorkSchema = object({
10148
10149
  grouping: _enum(["workspace", "regrouped"]),
10149
10150
  access: journeyAccessSchema,
10150
10151
  accessConfirmed: boolean(),
10152
+ accessChoice: object({
10153
+ channel: _enum([
10154
+ "conversation",
10155
+ "panel",
10156
+ "account"
10157
+ ]),
10158
+ at: datetime()
10159
+ }).strict().optional(),
10160
+ accountAccessConflict: boolean().optional(),
10151
10161
  reason: string().max(1e3),
10152
10162
  revision: count$1,
10153
10163
  inputRevision: count$1.min(1),