@gitroll/tacitt-cli 0.1.0 → 0.3.0
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.
- package/dist/{agent-setup-B85GaVIz.mjs → agent-setup-CLXdKr2I.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +165 -9
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +25 -4
- package/dist/{frame-D1j9R8xX.mjs → frame-waS5FnN5.mjs} +1 -1
- package/dist/{heap-tfU61Ehy.mjs → heap-eO_uWpD1.mjs} +63 -9
- package/dist/inspection-worker.mjs +57 -0
- package/dist/job-rK-Ho9OD.mjs +613 -0
- package/dist/{locate-Bn32k2se.mjs → locate-U3-nkird.mjs} +29 -6
- package/dist/messages-D3m3bhva.mjs +40 -0
- package/dist/{model-7Nu7szNP.mjs → model-BBAnsFAx.mjs} +1 -1
- package/dist/operation-executor.mjs +3 -3
- package/dist/preview-worker.mjs +1 -1
- package/dist/privacy-validator.mjs +1 -1
- package/dist/{query-Da-KDbSV.mjs → query-DN99jiRc.mjs} +710 -630
- package/dist/{run-CCATsHS2.mjs → run-4G_CvdDj.mjs} +4 -4
- package/dist/session-process.mjs +6 -1
- package/dist/{src-CPPjOyHZ.mjs → src-B4R5XMEP.mjs} +621 -224
- package/dist/{src-BngGpFKd.mjs → src-CPbdnGrk.mjs} +130 -53
- package/dist/{src-DynBRDBc.mjs → src-qoLRvX-A.mjs} +34 -11
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/index-BkuUI3Eu.js +67 -0
- package/dist/web/assets/index-Clg6Pfv_.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skills/tacitt/SKILL.md +15 -11
- package/skills/tacitt/references/b-method.md +1 -1
- package/skills/tacitt/references/classification.md +3 -3
- package/skills/tacitt/references/commands.md +19 -14
- package/skills/tacitt/references/journey.md +17 -11
- package/skills/tacitt/references/parallel-review.md +3 -3
- package/skills/tacitt/references/send.md +5 -5
- package/skills/tacitt/scripts/INDEX.md +6 -2
- package/skills/tacitt/scripts/answer.mjs +29 -8
- package/skills/tacitt/scripts/journey-card-consolidation.d.mts +13 -4
- package/skills/tacitt/scripts/journey-card-consolidation.mjs +170 -21
- package/skills/tacitt/scripts/journey-card-values.d.mts +17 -0
- package/skills/tacitt/scripts/journey-card-values.mjs +116 -0
- package/skills/tacitt/scripts/journey-classification-progress.mjs +2 -1
- package/skills/tacitt/scripts/journey-classification-validation.mjs +49 -28
- package/skills/tacitt/scripts/journey-classification.mjs +6 -2
- package/skills/tacitt/scripts/journey-consolidation-submit.mjs +42 -5
- package/skills/tacitt/scripts/journey-consolidation.d.mts +5 -0
- package/skills/tacitt/scripts/journey-consolidation.mjs +4 -1
- package/skills/tacitt/scripts/journey-panel-host.d.mts +9 -0
- package/skills/tacitt/scripts/journey-panel-host.mjs +56 -0
- package/skills/tacitt/scripts/journey-state.d.mts +1 -0
- package/skills/tacitt/scripts/journey-state.mjs +38 -6
- package/skills/tacitt/scripts/journey-tasks.mjs +5 -3
- package/skills/tacitt/scripts/journey.d.mts +4 -0
- package/skills/tacitt/scripts/journey.mjs +249 -41
- package/skills/tacitt/scripts/privacy-validator.mjs +25 -6
- package/skills/tacitt/scripts/review-stream-client.mjs +1 -1
- package/skills/tacitt/scripts/review.mjs +24 -3
- package/dist/job--TceKk0l.mjs +0 -194
- package/dist/web/assets/index-CWQ05PFl.js +0 -67
- package/dist/web/assets/index-J-AWt_cW.css +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { mkdir, realpath } from 'node:fs/promises'
|
|
2
|
+
import { mkdir, realpath, rm } from 'node:fs/promises'
|
|
3
3
|
import { join, resolve, isAbsolute } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { parseArgs } from 'node:util'
|
|
6
|
-
import { main as review, withJourneyCli } from './review.mjs'
|
|
6
|
+
import { blockedWorks, main as review, withJourneyCli } from './review.mjs'
|
|
7
7
|
import {
|
|
8
8
|
read,
|
|
9
9
|
optional,
|
|
@@ -18,11 +18,17 @@ import {
|
|
|
18
18
|
resolutionEvidence,
|
|
19
19
|
planResolutions,
|
|
20
20
|
assertResolutionFresh,
|
|
21
|
+
uncoveredPending,
|
|
21
22
|
} from './journey-state.mjs'
|
|
22
23
|
import { classificationTasks } from './journey-classification-tasks.mjs'
|
|
23
24
|
import { classificationEvidence, classificationFormat } from './journey-classification.mjs'
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
consolidateClassification,
|
|
27
|
+
classificationSubmission,
|
|
28
|
+
partialClassificationSubmission,
|
|
29
|
+
} from './journey-consolidation.mjs'
|
|
25
30
|
import { consolidateCards, cardSubmission } from './journey-card-consolidation.mjs'
|
|
31
|
+
import { settleCardValues } from './journey-card-values.mjs'
|
|
26
32
|
import {
|
|
27
33
|
classificationProgress,
|
|
28
34
|
publishClassificationProgress,
|
|
@@ -30,6 +36,7 @@ import {
|
|
|
30
36
|
} from './journey-classification-progress.mjs'
|
|
31
37
|
import { answers, createTasks } from './journey-tasks.mjs'
|
|
32
38
|
import { scanTiming } from './journey-scan-timing.mjs'
|
|
39
|
+
import { ensureDetachedHost } from './journey-panel-host.mjs'
|
|
33
40
|
export { phaseOf, stageFingerprint } from './journey-state.mjs'
|
|
34
41
|
|
|
35
42
|
async function publishCompleteClassification(manifest, view, call, proposalRevision) {
|
|
@@ -111,24 +118,94 @@ async function observeScanTiming(run, journeyId, view, start, call) {
|
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
const packagedCount = (view) =>
|
|
122
|
+
view.works.filter(
|
|
123
|
+
(work) =>
|
|
124
|
+
work.access !== 'drop' &&
|
|
125
|
+
work.rootSessionIds.length &&
|
|
126
|
+
work.status === 'ready' &&
|
|
127
|
+
work.artifact?.revision === work.revision,
|
|
128
|
+
).length
|
|
129
|
+
|
|
114
130
|
/** Locked by withJourneyCli in production; injected transport permits deterministic crash tests. */
|
|
115
131
|
/** A running journey render (panel or CLI) with packaged/selected Work counts (BL-185). */
|
|
116
132
|
function packagingProgress(view) {
|
|
117
133
|
if (!view.packaging) return {}
|
|
118
134
|
const selected = view.works.filter((work) => work.access !== 'drop' && work.rootSessionIds.length)
|
|
119
135
|
return {
|
|
120
|
-
packaging: {
|
|
121
|
-
...view.packaging,
|
|
122
|
-
selected: selected.length,
|
|
123
|
-
packaged: selected.filter(
|
|
124
|
-
(work) => work.status === 'ready' && work.artifact?.revision === work.revision,
|
|
125
|
-
).length,
|
|
126
|
-
},
|
|
136
|
+
packaging: { ...view.packaging, selected: selected.length, packaged: packagedCount(view) },
|
|
127
137
|
}
|
|
128
138
|
}
|
|
129
139
|
|
|
130
|
-
|
|
140
|
+
/** One await call's bound: inside a 10-minute tool timeout; the Skill repeats it to 30 minutes. */
|
|
141
|
+
export const maxAwaitMs = 540_000
|
|
142
|
+
const awaitPollMs = 1000
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* ADR 0227: from Ready to send the AI work is done. Packaging, upload, assessment and Passport
|
|
146
|
+
* completion run in the detached panel host (ADR 0217), so the agent ends its turn.
|
|
147
|
+
*/
|
|
148
|
+
const handoffPhases = new Set(['ready-to-send', 'send', 'sending'])
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* D0: the agent waits inside its turn for the person's next panel decision. Returns once the
|
|
152
|
+
* helper phase changes (consent recorded, the last card answered, a Work moved out of Drop
|
|
153
|
+
* needing a read), a new card batch is due or a blocked Work was Dropped or retried, else after
|
|
154
|
+
* `waitMs` with `changed: false`. A retried Work needs `journey.prepare` again.
|
|
155
|
+
*
|
|
156
|
+
* At a handoff phase it returns at once, without waiting, once a detached panel host owns the
|
|
157
|
+
* panel: `handoff` tells the agent to end its turn (ADR 0227, superseding ADR 0225's wait).
|
|
158
|
+
*/
|
|
159
|
+
async function awaitDecision(journeyId, waitMs, call, sleep, ensureHost) {
|
|
160
|
+
if (!Number.isSafeInteger(waitMs) || waitMs < 0 || waitMs > maxAwaitMs)
|
|
161
|
+
throw new Error('INVALID_ARGUMENTS')
|
|
162
|
+
const started = Date.now()
|
|
163
|
+
let view = await call('journey.get', { journeyId })
|
|
164
|
+
const from = phaseOf(view)
|
|
165
|
+
const blockedFrom = blockedWorks(view).map((work) => work.workId)
|
|
166
|
+
for (;;) {
|
|
167
|
+
const phase = phaseOf(view)
|
|
168
|
+
const batchDue = phase === 'browser-answers' && uncoveredPending(view).length > 0
|
|
169
|
+
const blocked = blockedWorks(view)
|
|
170
|
+
const settled = blockedFrom.filter((id) => !blocked.some((work) => work.workId === id))
|
|
171
|
+
const retried = settled.filter((id) =>
|
|
172
|
+
view.works.some((work) => work.id === id && work.access !== 'drop'),
|
|
173
|
+
)
|
|
174
|
+
const changed = phase !== from || batchDue || settled.length > 0
|
|
175
|
+
const handoff = handoffPhases.has(phase)
|
|
176
|
+
if (changed || handoff || Date.now() - started >= waitMs)
|
|
177
|
+
return {
|
|
178
|
+
command: 'await',
|
|
179
|
+
from,
|
|
180
|
+
phase,
|
|
181
|
+
changed,
|
|
182
|
+
waitedMs: Date.now() - started,
|
|
183
|
+
revision: view.revision,
|
|
184
|
+
blockedWorks: blocked,
|
|
185
|
+
...(retried.length ? { retriedWorks: retried, next: 'journey.prepare' } : {}),
|
|
186
|
+
...(handoff ? { handoff: { panel: await ensureHost(), next: 'end-turn' } } : {}),
|
|
187
|
+
}
|
|
188
|
+
await sleep(Math.min(awaitPollMs, Math.max(0, waitMs - (Date.now() - started))))
|
|
189
|
+
view = await call('journey.get', { journeyId })
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function runPhaseCore(
|
|
194
|
+
{ run, journeyId, command, stage, refresh = false, limit = 3, waitMs = 0, sleep, ensureHost },
|
|
195
|
+
call,
|
|
196
|
+
) {
|
|
131
197
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 16) throw new Error('INVALID_ARGUMENTS')
|
|
198
|
+
if (command === 'await')
|
|
199
|
+
return awaitDecision(
|
|
200
|
+
journeyId,
|
|
201
|
+
waitMs,
|
|
202
|
+
call,
|
|
203
|
+
sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
204
|
+
ensureHost ??
|
|
205
|
+
(() => {
|
|
206
|
+
throw new Error('PANEL_HOST_DETACH_FAILED')
|
|
207
|
+
}),
|
|
208
|
+
)
|
|
132
209
|
let view = await call('journey.get', { journeyId })
|
|
133
210
|
if (command === 'scan-start')
|
|
134
211
|
return {
|
|
@@ -155,6 +232,7 @@ async function runPhaseCore({ run, journeyId, command, stage, refresh = false, l
|
|
|
155
232
|
revision: view.revision,
|
|
156
233
|
readingComplete: view.readingComplete,
|
|
157
234
|
pending: activeTerms(view).filter((term) => term.disposition === 'pending').length,
|
|
235
|
+
blockedWorks: blockedWorks(view),
|
|
158
236
|
answersRevision: view.privacy.answersRevision,
|
|
159
237
|
...packagingProgress(view),
|
|
160
238
|
coldElapsedMs,
|
|
@@ -177,14 +255,22 @@ async function runPhaseCore({ run, journeyId, command, stage, refresh = false, l
|
|
|
177
255
|
const recovery = await recoverWrites(active.directory, manifest, call)
|
|
178
256
|
// A failed transport leaves the exact pending request intact and never reaches refresh.
|
|
179
257
|
if (recovery.recovered) view = await call('journey.get', { journeyId })
|
|
180
|
-
|
|
181
|
-
if (
|
|
258
|
+
// Only scan progress refused a classification-bound proposal: it is still current (ADR 0225).
|
|
259
|
+
if (
|
|
260
|
+
recovery.stale &&
|
|
261
|
+
(!refresh || command !== 'tasks') &&
|
|
262
|
+
!(stage === 'classification' && classificationBound(manifest, view))
|
|
263
|
+
)
|
|
264
|
+
throw new Error('REVISION_CONFLICT')
|
|
265
|
+
// A refused card batch is retired: the next `tasks` drafts the current Pending set.
|
|
266
|
+
if (recovery.rejected && !(command === 'tasks' && (refresh || manifest.batch))) {
|
|
182
267
|
const error = new Error(recovery.rejected)
|
|
183
268
|
error.code = recovery.rejected
|
|
184
269
|
throw error
|
|
185
270
|
}
|
|
186
271
|
if (recovery.completed === recovery.total && !recovery.stale && command === 'ingest') {
|
|
187
|
-
|
|
272
|
+
// After the known Works, scans keep moving the revision and Works show grouping (E2).
|
|
273
|
+
if (stage === 'classification' && manifest.consolidation && !manifest.partial)
|
|
188
274
|
await publishCompleteClassification(
|
|
189
275
|
manifest,
|
|
190
276
|
view,
|
|
@@ -197,25 +283,74 @@ async function runPhaseCore({ run, journeyId, command, stage, refresh = false, l
|
|
|
197
283
|
const consolidationRefresh = refresh && command === 'consolidate' && stage === 'classification'
|
|
198
284
|
if (
|
|
199
285
|
consolidationRefresh &&
|
|
200
|
-
!['classification', 'classification-confirmation'].includes(phaseOf(view))
|
|
286
|
+
!['classification', 'classification-confirmation'].includes(phaseOf(view)) &&
|
|
287
|
+
!grouping(view)
|
|
201
288
|
)
|
|
202
289
|
throw new Error('STAGE_NOT_READY')
|
|
203
290
|
if (consolidationRefresh && (await optional(join(active?.directory ?? run, 'receipt.json'))))
|
|
204
291
|
throw new Error('CLASSIFICATION_ALREADY_SUBMITTED')
|
|
292
|
+
// A card batch (D3) whose append was accepted, or had no open value left, is finished; one
|
|
293
|
+
// the CLI refused is retired. Either way the next `tasks` starts another.
|
|
294
|
+
const batchReceipt =
|
|
295
|
+
stage === 'cards' && manifest?.batch
|
|
296
|
+
? (await optional(join(active.directory, 'receipt.json')))?.status
|
|
297
|
+
: undefined
|
|
298
|
+
const batchDone = ['applied', 'replayed', 'skipped'].includes(batchReceipt)
|
|
299
|
+
if (batchDone && command !== 'tasks') return { stage, status: batchReceipt, batchDone: true }
|
|
300
|
+
if (batchDone || batchReceipt === 'rejected') manifest = null
|
|
205
301
|
const fingerprint = stageFingerprint(view, stage)
|
|
206
302
|
if (manifest?.resolutionBatches && !refresh)
|
|
207
303
|
await assertResolutionFresh(active.directory, manifest, view)
|
|
208
|
-
|
|
304
|
+
// A batch names its own open values; the CLI refuses them once they are no longer open.
|
|
305
|
+
// Known Works this helper published (E2) changed the Works it fingerprinted.
|
|
306
|
+
else if (
|
|
307
|
+
manifest &&
|
|
308
|
+
!manifest.batch &&
|
|
309
|
+
!manifest.partial &&
|
|
310
|
+
manifest.fingerprint !== fingerprint &&
|
|
311
|
+
(!refresh || consolidationRefresh)
|
|
312
|
+
)
|
|
209
313
|
throw new Error('STALE_TASKS')
|
|
210
314
|
if (refresh && command !== 'tasks' && !consolidationRefresh) throw new Error('INVALID_ARGUMENTS')
|
|
211
315
|
if (command === 'tasks') {
|
|
212
316
|
const phase = phaseOf(view)
|
|
213
317
|
const revisingCards =
|
|
214
318
|
stage === 'cards' && refresh && ['browser-answers', 'answers-confirmation'].includes(phase)
|
|
319
|
+
// Batches start while reading continues and after earlier batches (D3); while reading
|
|
320
|
+
// continues there is no deck to replace, so `--refresh` also starts a batch.
|
|
321
|
+
const cardBatch =
|
|
322
|
+
stage === 'cards' &&
|
|
323
|
+
(!refresh || phase === 'scan') &&
|
|
324
|
+
view.scope.status === 'confirmed' &&
|
|
325
|
+
!['paused', 'sending', 'completed'].includes(phase) &&
|
|
326
|
+
uncoveredPending(view).length > 0
|
|
327
|
+
if (cardBatch && !manifest) {
|
|
328
|
+
const directory = join(run, stage, `batch-${randomUUID()}`)
|
|
329
|
+
await mkdir(directory, { recursive: true, mode: 0o700 })
|
|
330
|
+
manifest = {
|
|
331
|
+
format: classificationFormat,
|
|
332
|
+
stage,
|
|
333
|
+
batch: true,
|
|
334
|
+
fingerprint,
|
|
335
|
+
existingCards: view.privacy.cards.map((card) => ({
|
|
336
|
+
id: card.id,
|
|
337
|
+
question: card.question,
|
|
338
|
+
options: card.options.map(({ id, label }) => ({ id, label })),
|
|
339
|
+
answer: card.answer?.optionId ?? null,
|
|
340
|
+
termCount: card.termKeys.length,
|
|
341
|
+
})),
|
|
342
|
+
tasks: await createTasks(directory, stage, view, call, { batch: true }),
|
|
343
|
+
answersRevision: view.privacy.answersRevision,
|
|
344
|
+
}
|
|
345
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
346
|
+
await save(activePath, { directory })
|
|
347
|
+
return { stage, tasks: manifest.tasks, batch: true }
|
|
348
|
+
}
|
|
215
349
|
if (
|
|
216
350
|
phase !== stage &&
|
|
217
351
|
!revisingCards &&
|
|
218
|
-
!(
|
|
352
|
+
!(cardBatch && manifest) &&
|
|
353
|
+
!(stage === 'classification' && (phase === 'classification-confirmation' || grouping(view)))
|
|
219
354
|
)
|
|
220
355
|
throw new Error('STAGE_NOT_READY')
|
|
221
356
|
if (manifest && !refresh && stage === 'classification') {
|
|
@@ -240,6 +375,10 @@ async function runPhaseCore({ run, journeyId, command, stage, refresh = false, l
|
|
|
240
375
|
fingerprint,
|
|
241
376
|
tasks: stage === 'classification' ? [] : await createTasks(directory, stage, view, call),
|
|
242
377
|
answersRevision: view.privacy.answersRevision,
|
|
378
|
+
// Refreshed after the known Works (E2): the complete proposal stays bound to classification.
|
|
379
|
+
...(stage === 'classification' && grouping(view)
|
|
380
|
+
? { partial: { status: 'applied', revision: view.classificationRevision } }
|
|
381
|
+
: {}),
|
|
243
382
|
}
|
|
244
383
|
await save(join(directory, 'manifest.json'), manifest)
|
|
245
384
|
await save(activePath, { directory })
|
|
@@ -318,6 +457,15 @@ async function runPhaseCore({ run, journeyId, command, stage, refresh = false, l
|
|
|
318
457
|
total: manifest.resolutionBatches.length,
|
|
319
458
|
}
|
|
320
459
|
}
|
|
460
|
+
if (stage === 'classification' && command === 'ingest' && !manifest.partial) {
|
|
461
|
+
const readiness = manifest.consolidation
|
|
462
|
+
? await consolidateClassification(directory, manifest)
|
|
463
|
+
: { readyForIngest: false }
|
|
464
|
+
if (!readiness.readyForIngest) {
|
|
465
|
+
const partial = await partialClassificationSubmission(manifest)
|
|
466
|
+
if (partial) return ingestKnownWorks(directory, manifest, view, call, partial)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
321
469
|
if (stage === 'classification' && !manifest.consolidation)
|
|
322
470
|
throw new Error('CONSOLIDATION_REQUIRED')
|
|
323
471
|
if (stage === 'classification') {
|
|
@@ -330,42 +478,99 @@ async function runPhaseCore({ run, journeyId, command, stage, refresh = false, l
|
|
|
330
478
|
}
|
|
331
479
|
const key = stage === 'classification' ? 'items' : stage === 'cards' ? 'cards' : 'resolutions'
|
|
332
480
|
const tasks = stage === 'classification' ? [manifest.consolidation] : manifest.tasks
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
? await classificationSubmission(manifest, view)
|
|
340
|
-
: stage === 'cards'
|
|
341
|
-
? await cardSubmission(manifest)
|
|
342
|
-
: await answers(tasks, key),
|
|
343
|
-
...(stage === 'resolution' ? { answersRevision: manifest.answersRevision } : {}),
|
|
481
|
+
// Later scan packs may hide covered values between `tasks` and `ingest`: submit only what is
|
|
482
|
+
// still open, and give every value one card (F4).
|
|
483
|
+
const cards = stage === 'cards' ? settleCardValues(await cardSubmission(manifest), view) : null
|
|
484
|
+
if (cards && manifest.batch && !cards.cards.length && !cards.extend?.length) {
|
|
485
|
+
await save(join(directory, 'receipt.json'), { status: 'skipped' })
|
|
486
|
+
return { stage, status: 'skipped', batchDone: true }
|
|
344
487
|
}
|
|
345
|
-
await save(join(directory, 'request.json'), request)
|
|
346
488
|
let result
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
489
|
+
for (let attempt = 0; ; attempt++) {
|
|
490
|
+
const request = {
|
|
491
|
+
journeyId,
|
|
492
|
+
expectedRevision: view.revision,
|
|
493
|
+
idempotencyKey: randomUUID(),
|
|
494
|
+
// After the known Works, scans keep moving the revision: bind to the classification (E2).
|
|
495
|
+
...(stage === 'classification' && manifest.partial
|
|
496
|
+
? { expectedClassificationRevision: manifest.partial.revision }
|
|
497
|
+
: {}),
|
|
498
|
+
...(stage === 'classification'
|
|
499
|
+
? { [key]: await classificationSubmission(manifest, view) }
|
|
500
|
+
: stage === 'cards'
|
|
501
|
+
? cards
|
|
502
|
+
: { [key]: await answers(tasks, key) }),
|
|
503
|
+
...(stage === 'resolution' ? { answersRevision: manifest.answersRevision } : {}),
|
|
504
|
+
}
|
|
505
|
+
await save(join(directory, 'request.json'), request)
|
|
506
|
+
try {
|
|
507
|
+
result = await call(procedureFor(stage), request)
|
|
355
508
|
await save(join(directory, 'receipt.json'), {
|
|
356
|
-
status:
|
|
357
|
-
|
|
509
|
+
status: result.status,
|
|
510
|
+
revision: result.state?.revision,
|
|
358
511
|
})
|
|
359
|
-
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (definiteWriteRejection(error))
|
|
514
|
+
await save(join(directory, 'receipt.json'), {
|
|
515
|
+
status: 'rejected',
|
|
516
|
+
code: error.code,
|
|
517
|
+
})
|
|
518
|
+
throw error
|
|
519
|
+
}
|
|
520
|
+
if (result.status !== 'stale' || stage !== 'classification' || attempt >= boundRetries) break
|
|
521
|
+
// A scan write between the CLI's read and its commit refuses a still-current proposal
|
|
522
|
+
// (ADR 0225): submit it again against the current revision.
|
|
523
|
+
view = await call('journey.get', { journeyId })
|
|
524
|
+
if (!classificationBound(manifest, view)) break
|
|
360
525
|
}
|
|
361
526
|
if (result.status === 'stale') throw new Error('REVISION_CONFLICT')
|
|
362
|
-
if (stage === 'classification') {
|
|
527
|
+
if (stage === 'classification' && !manifest.partial) {
|
|
363
528
|
const latest = await call('journey.get', { journeyId })
|
|
364
529
|
await publishCompleteClassification(manifest, latest, call, result.state?.revision)
|
|
365
530
|
}
|
|
366
531
|
return { stage, status: result.status, revision: result.state?.revision }
|
|
367
532
|
}
|
|
368
533
|
|
|
534
|
+
/** Classification published known Works and is still grouping the remaining roots (E2). */
|
|
535
|
+
const grouping = (view) => view.works.some((work) => work.placement === 'classifying')
|
|
536
|
+
|
|
537
|
+
/** The complete proposal binds to the classification it replaces, not to scan progress (E2). */
|
|
538
|
+
const classificationBound = (manifest, view) =>
|
|
539
|
+
Boolean(manifest.partial) &&
|
|
540
|
+
view.classificationRevision === manifest.partial.revision &&
|
|
541
|
+
grouping(view)
|
|
542
|
+
/** Submissions of an unchanged classification-bound proposal after a stale answer. */
|
|
543
|
+
const boundRetries = 3
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* E2: publishes known-project Works while unresolved roots are still being grouped, so their
|
|
547
|
+
* scan can start. Its request and receipt are kept apart from the final proposal's.
|
|
548
|
+
*/
|
|
549
|
+
async function ingestKnownWorks(directory, manifest, view, call, partial) {
|
|
550
|
+
const requestPath = join(directory, 'partial.request.json')
|
|
551
|
+
const request = (await optional(requestPath)) ?? {
|
|
552
|
+
journeyId: view.journeyId,
|
|
553
|
+
expectedRevision: view.revision,
|
|
554
|
+
idempotencyKey: randomUUID(),
|
|
555
|
+
...partial,
|
|
556
|
+
}
|
|
557
|
+
await save(requestPath, request)
|
|
558
|
+
const result = await call(procedureFor('classification'), request)
|
|
559
|
+
if (result.status === 'stale') {
|
|
560
|
+
await rm(requestPath, { force: true })
|
|
561
|
+
throw new Error('REVISION_CONFLICT')
|
|
562
|
+
}
|
|
563
|
+
manifest.partial = { status: result.status, revision: result.state?.revision }
|
|
564
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
565
|
+
return {
|
|
566
|
+
stage: 'classification',
|
|
567
|
+
status: result.status,
|
|
568
|
+
partial: true,
|
|
569
|
+
pendingRoots: partial.pendingRootSessionIds.length,
|
|
570
|
+
revision: result.state?.revision,
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
369
574
|
const terminalClassificationErrors = new Set([
|
|
370
575
|
'INVALID_SELECTION',
|
|
371
576
|
'CLASSIFICATION_PROJECT_NAME_CONFLICT',
|
|
@@ -410,6 +615,7 @@ export async function main(argv) {
|
|
|
410
615
|
stage: { type: 'string' },
|
|
411
616
|
refresh: { type: 'boolean', default: false },
|
|
412
617
|
limit: { type: 'string' },
|
|
618
|
+
'wait-ms': { type: 'string' },
|
|
413
619
|
model: { type: 'string' },
|
|
414
620
|
effort: { type: 'string' },
|
|
415
621
|
},
|
|
@@ -434,6 +640,8 @@ export async function main(argv) {
|
|
|
434
640
|
stage: values.stage,
|
|
435
641
|
refresh: values.refresh,
|
|
436
642
|
limit: values.limit === undefined ? 3 : Number(values.limit),
|
|
643
|
+
waitMs: values['wait-ms'] === undefined ? 300_000 : Number(values['wait-ms']),
|
|
644
|
+
ensureHost: () => ensureDetachedHost(config.cli, config.journeyId),
|
|
437
645
|
},
|
|
438
646
|
call,
|
|
439
647
|
),
|
|
@@ -9468,13 +9468,18 @@ const decisionCardInputSchema = object({
|
|
|
9468
9468
|
id: _enum(["hide", "keep"]),
|
|
9469
9469
|
label: string().trim().min(1).max(240),
|
|
9470
9470
|
rule: string().trim().min(1).max(2e3)
|
|
9471
|
-
}).strict()).length(2).refine((options) => new Set(options.map((option) => option.id)).size === 2, { message: "Decision cards require one hide option and one keep option." })
|
|
9471
|
+
}).strict()).length(2).refine((options) => new Set(options.map((option) => option.id)).size === 2, { message: "Decision cards require one hide option and one keep option." }),
|
|
9472
|
+
suggestion: object({
|
|
9473
|
+
optionId: _enum(["hide", "keep"]),
|
|
9474
|
+
source: string().trim().min(1).max(300)
|
|
9475
|
+
}).strict().optional()
|
|
9472
9476
|
}).strict();
|
|
9473
9477
|
const decisionCardSchema = decisionCardInputSchema.extend({ answer: object({
|
|
9474
9478
|
optionId: string(),
|
|
9475
9479
|
rule: string().max(4001),
|
|
9476
9480
|
additionalRule: string().max(2e3),
|
|
9477
|
-
remember: boolean()
|
|
9481
|
+
remember: boolean(),
|
|
9482
|
+
excludedTermKeys: array(string().min(1)).max(2e4).optional()
|
|
9478
9483
|
}).strict().nullable() }).strict();
|
|
9479
9484
|
const journeyPrivacySchema = object({
|
|
9480
9485
|
terms: array(privacyTermSchema),
|
|
@@ -9490,7 +9495,14 @@ const write = object({
|
|
|
9490
9495
|
expectedRevision: count$2,
|
|
9491
9496
|
idempotencyKey: uuid()
|
|
9492
9497
|
});
|
|
9493
|
-
write.extend({
|
|
9498
|
+
write.extend({
|
|
9499
|
+
cards: array(decisionCardInputSchema).max(20),
|
|
9500
|
+
mode: _enum(["replace", "append"]).optional(),
|
|
9501
|
+
extend: array(object({
|
|
9502
|
+
cardId: uuid(),
|
|
9503
|
+
termKeys: array(string().min(1)).min(1).max(2e4)
|
|
9504
|
+
}).strict()).max(20).optional()
|
|
9505
|
+
}).strict();
|
|
9494
9506
|
const privacyResolveSchema = write.extend({
|
|
9495
9507
|
answersRevision: count$2,
|
|
9496
9508
|
resolutions: array(privacyTermSchema.omit({ sources: true }).extend({ disposition: _enum(["hidden", "keep"]) }).strict()).min(1).max(2e4)
|
|
@@ -9500,7 +9512,8 @@ object({
|
|
|
9500
9512
|
cardId: uuid(),
|
|
9501
9513
|
optionId: string().min(1),
|
|
9502
9514
|
rule: string().max(2e3).optional(),
|
|
9503
|
-
remember: boolean()
|
|
9515
|
+
remember: boolean(),
|
|
9516
|
+
excludedTermKeys: array(string().min(1)).max(2e4).optional()
|
|
9504
9517
|
}).strict();
|
|
9505
9518
|
object({
|
|
9506
9519
|
kind: literal("cards-confirm"),
|
|
@@ -9815,7 +9828,11 @@ const journeyWorkSchema = object({
|
|
|
9815
9828
|
"explicit",
|
|
9816
9829
|
"delegate"
|
|
9817
9830
|
]),
|
|
9818
|
-
placement: _enum([
|
|
9831
|
+
placement: _enum([
|
|
9832
|
+
"placed",
|
|
9833
|
+
"unsorted",
|
|
9834
|
+
"classifying"
|
|
9835
|
+
]),
|
|
9819
9836
|
suggestion: journeyWorkSuggestionSchema.nullable(),
|
|
9820
9837
|
classificationRecommendation: object({
|
|
9821
9838
|
access: journeyAccessSchema,
|
|
@@ -9862,7 +9879,9 @@ const journeyWorkSchema = object({
|
|
|
9862
9879
|
staticReview: object({
|
|
9863
9880
|
snapshotId: journeyDigestSchema,
|
|
9864
9881
|
inputRevision: count$1.min(1),
|
|
9865
|
-
digest: journeyDigestSchema
|
|
9882
|
+
digest: journeyDigestSchema,
|
|
9883
|
+
/** Images the Work's sessions leave out (D7); absent when a session receipt predates it. */
|
|
9884
|
+
imagesOmitted: count$1.optional()
|
|
9866
9885
|
}).strict().optional(),
|
|
9867
9886
|
totalBatches: count$1,
|
|
9868
9887
|
completedBatches: count$1,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { phaseOf } from './journey-state.mjs'
|
|
1
|
+
import { cardBatchOpen, phaseOf, uncoveredPending } from './journey-state.mjs'
|
|
2
2
|
import { startReviewStream } from './review-stream-client.mjs'
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto'
|
|
4
4
|
import {
|
|
@@ -398,7 +398,7 @@ export function renderGuidance(guidance) {
|
|
|
398
398
|
"## The person's instructions",
|
|
399
399
|
'A hide instruction is a requirement: report every value it covers under the closest type. A keep instruction is context, not proof that other text is public.',
|
|
400
400
|
instructions.join('\n') || '(none)',
|
|
401
|
-
'Remembered rules (apply only when the context fits):',
|
|
401
|
+
'Remembered rules and answered questions (apply only when the context fits). A value that fits an answered question takes its answer: disposition hidden for Hide, keep for Keep, not pending.',
|
|
402
402
|
...(guidance?.rules ?? []),
|
|
403
403
|
].join('\n\n'),
|
|
404
404
|
) + '\n'
|
|
@@ -1317,7 +1317,7 @@ const termRequest = (ctx, units, terms, completeUnits = true) => ({
|
|
|
1317
1317
|
|
|
1318
1318
|
/** Per-Work scan progress; final preview and render also require the Work's Track B review. */
|
|
1319
1319
|
export function workProgress(view) {
|
|
1320
|
-
const finalPreview = ['
|
|
1320
|
+
const finalPreview = ['ready-to-send', 'send'].includes(phaseOf(view))
|
|
1321
1321
|
return view.works.map((work) => {
|
|
1322
1322
|
const selected = work.access !== 'drop' && work.rootSessionIds.length > 0
|
|
1323
1323
|
const proof = work.emptyReview
|
|
@@ -1384,9 +1384,12 @@ export function questionsReady(view) {
|
|
|
1384
1384
|
/** Reading and question state after this command's recordings. */
|
|
1385
1385
|
async function progressOf(ctx) {
|
|
1386
1386
|
const view = await call(ctx, 'journey.get', { journeyId: ctx.journeyId })
|
|
1387
|
+
const uncovered = uncoveredPending(view).length
|
|
1387
1388
|
return {
|
|
1388
1389
|
readingComplete: view.readingComplete,
|
|
1389
1390
|
questionsReady: questionsReady(view),
|
|
1391
|
+
// E3: a card is raised as soon as a value needs one, unless a batch is still being drafted.
|
|
1392
|
+
cardBatch: { uncovered, ready: uncovered > 0 && !(await cardBatchOpen(ctx.run)) },
|
|
1390
1393
|
works: workProgress(view),
|
|
1391
1394
|
blockedWorks: blockedWorks(view),
|
|
1392
1395
|
}
|
|
@@ -1512,6 +1515,24 @@ export async function withJourneyCli(run, operation) {
|
|
|
1512
1515
|
}
|
|
1513
1516
|
}
|
|
1514
1517
|
|
|
1518
|
+
/**
|
|
1519
|
+
* A CLI binding without the run lock, for a child that only publishes progress (BL-202) while
|
|
1520
|
+
* the agent's helper holds the lock.
|
|
1521
|
+
*/
|
|
1522
|
+
export async function withUnlockedJourneyCli(run, operation) {
|
|
1523
|
+
const config = await readJson(join(run, 'config.json'), null)
|
|
1524
|
+
if (!config) fail('RUN_NOT_INITIALIZED')
|
|
1525
|
+
const ctx = { run, config, journeyId: config.journeyId }
|
|
1526
|
+
try {
|
|
1527
|
+
return await operation(
|
|
1528
|
+
(procedure, input, options) => call(ctx, procedure, input, options),
|
|
1529
|
+
config,
|
|
1530
|
+
)
|
|
1531
|
+
} finally {
|
|
1532
|
+
await Promise.all((ctx.streams ?? []).map((item) => item.close()))
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1515
1536
|
export async function main(argv) {
|
|
1516
1537
|
const { values, positionals } = parseArgs({
|
|
1517
1538
|
args: argv,
|