@gitroll/tacitt-cli 0.1.0 → 0.2.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-B8DAiQ6Z.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +108 -9
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +4 -4
- package/dist/{frame-D1j9R8xX.mjs → frame-waS5FnN5.mjs} +1 -1
- package/dist/{heap-tfU61Ehy.mjs → heap-BHcvxWCw.mjs} +20 -8
- package/dist/inspection-worker.mjs +57 -0
- package/dist/{job--TceKk0l.mjs → job-J3NFw3MH.mjs} +2 -2
- package/dist/{locate-Bn32k2se.mjs → locate-U3-nkird.mjs} +29 -6
- package/dist/messages-D3m3bhva.mjs +40 -0
- package/dist/{model-7Nu7szNP.mjs → model-BJdTLNjT.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-BA0XCwnR.mjs} +486 -128
- package/dist/{run-CCATsHS2.mjs → run-BeLHsZWb.mjs} +4 -4
- package/dist/{src-DynBRDBc.mjs → src-B5SArpVY.mjs} +20 -9
- package/dist/{src-CPPjOyHZ.mjs → src-CC95LYYp.mjs} +271 -68
- package/dist/{src-BngGpFKd.mjs → src-k0Ya1Bri.mjs} +13 -7
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/index-Bjos73wK.js +67 -0
- package/dist/web/assets/index-Bl4C2b2_.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skills/tacitt/SKILL.md +13 -11
- package/skills/tacitt/references/b-method.md +1 -1
- package/skills/tacitt/references/classification.md +3 -3
- package/skills/tacitt/references/commands.md +6 -6
- package/skills/tacitt/references/journey.md +13 -11
- package/skills/tacitt/references/parallel-review.md +3 -3
- package/skills/tacitt/references/send.md +5 -5
- 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 +81 -18
- 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-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-state.d.mts +1 -0
- package/skills/tacitt/scripts/journey-state.mjs +29 -3
- package/skills/tacitt/scripts/journey-tasks.mjs +4 -2
- package/skills/tacitt/scripts/journey.d.mts +2 -0
- package/skills/tacitt/scripts/journey.mjs +154 -13
- package/skills/tacitt/scripts/privacy-validator.mjs +25 -6
- package/skills/tacitt/scripts/review.mjs +24 -3
- package/dist/web/assets/index-CWQ05PFl.js +0 -67
- package/dist/web/assets/index-J-AWt_cW.css +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
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'
|
|
@@ -18,10 +18,15 @@ 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'
|
|
26
31
|
import {
|
|
27
32
|
classificationProgress,
|
|
@@ -127,8 +132,50 @@ function packagingProgress(view) {
|
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
134
|
|
|
130
|
-
|
|
135
|
+
/** One await call's bound: inside a 10-minute tool timeout; the Skill repeats it to 30 minutes. */
|
|
136
|
+
export const maxAwaitMs = 540_000
|
|
137
|
+
const awaitPollMs = 1000
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* D0: the agent waits inside its turn for the person's next panel decision. Returns once the
|
|
141
|
+
* helper phase changes (consent recorded, the last card answered, a Work moved out of Drop
|
|
142
|
+
* needing a read) or a new card batch is due, else after `waitMs` with `changed: false`.
|
|
143
|
+
*/
|
|
144
|
+
async function awaitDecision(journeyId, waitMs, call, sleep) {
|
|
145
|
+
if (!Number.isSafeInteger(waitMs) || waitMs < 0 || waitMs > maxAwaitMs)
|
|
146
|
+
throw new Error('INVALID_ARGUMENTS')
|
|
147
|
+
const started = Date.now()
|
|
148
|
+
let view = await call('journey.get', { journeyId })
|
|
149
|
+
const from = phaseOf(view)
|
|
150
|
+
for (;;) {
|
|
151
|
+
const phase = phaseOf(view)
|
|
152
|
+
const batchDue = phase === 'browser-answers' && uncoveredPending(view).length > 0
|
|
153
|
+
if (phase !== from || batchDue || Date.now() - started >= waitMs)
|
|
154
|
+
return {
|
|
155
|
+
command: 'await',
|
|
156
|
+
from,
|
|
157
|
+
phase,
|
|
158
|
+
changed: phase !== from || batchDue,
|
|
159
|
+
waitedMs: Date.now() - started,
|
|
160
|
+
revision: view.revision,
|
|
161
|
+
}
|
|
162
|
+
await sleep(Math.min(awaitPollMs, Math.max(0, waitMs - (Date.now() - started))))
|
|
163
|
+
view = await call('journey.get', { journeyId })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function runPhaseCore(
|
|
168
|
+
{ run, journeyId, command, stage, refresh = false, limit = 3, waitMs = 0, sleep },
|
|
169
|
+
call,
|
|
170
|
+
) {
|
|
131
171
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 16) throw new Error('INVALID_ARGUMENTS')
|
|
172
|
+
if (command === 'await')
|
|
173
|
+
return awaitDecision(
|
|
174
|
+
journeyId,
|
|
175
|
+
waitMs,
|
|
176
|
+
call,
|
|
177
|
+
sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
178
|
+
)
|
|
132
179
|
let view = await call('journey.get', { journeyId })
|
|
133
180
|
if (command === 'scan-start')
|
|
134
181
|
return {
|
|
@@ -197,25 +244,73 @@ async function runPhaseCore({ run, journeyId, command, stage, refresh = false, l
|
|
|
197
244
|
const consolidationRefresh = refresh && command === 'consolidate' && stage === 'classification'
|
|
198
245
|
if (
|
|
199
246
|
consolidationRefresh &&
|
|
200
|
-
!['classification', 'classification-confirmation'].includes(phaseOf(view))
|
|
247
|
+
!['classification', 'classification-confirmation'].includes(phaseOf(view)) &&
|
|
248
|
+
!grouping(view)
|
|
201
249
|
)
|
|
202
250
|
throw new Error('STAGE_NOT_READY')
|
|
203
251
|
if (consolidationRefresh && (await optional(join(active?.directory ?? run, 'receipt.json'))))
|
|
204
252
|
throw new Error('CLASSIFICATION_ALREADY_SUBMITTED')
|
|
253
|
+
// A card batch (D3) whose append was accepted is finished: the next `tasks` starts another.
|
|
254
|
+
const batchDone =
|
|
255
|
+
stage === 'cards' &&
|
|
256
|
+
manifest?.batch &&
|
|
257
|
+
['applied', 'replayed'].includes(
|
|
258
|
+
(await optional(join(active.directory, 'receipt.json')))?.status,
|
|
259
|
+
)
|
|
260
|
+
if (batchDone && command !== 'tasks') return { stage, status: 'applied', batchDone: true }
|
|
261
|
+
if (batchDone) manifest = null
|
|
205
262
|
const fingerprint = stageFingerprint(view, stage)
|
|
206
263
|
if (manifest?.resolutionBatches && !refresh)
|
|
207
264
|
await assertResolutionFresh(active.directory, manifest, view)
|
|
208
|
-
|
|
265
|
+
// A batch names its own open values; the CLI refuses them once they are no longer open.
|
|
266
|
+
// Known Works this helper published (E2) changed the Works it fingerprinted.
|
|
267
|
+
else if (
|
|
268
|
+
manifest &&
|
|
269
|
+
!manifest.batch &&
|
|
270
|
+
!manifest.partial &&
|
|
271
|
+
manifest.fingerprint !== fingerprint &&
|
|
272
|
+
(!refresh || consolidationRefresh)
|
|
273
|
+
)
|
|
209
274
|
throw new Error('STALE_TASKS')
|
|
210
275
|
if (refresh && command !== 'tasks' && !consolidationRefresh) throw new Error('INVALID_ARGUMENTS')
|
|
211
276
|
if (command === 'tasks') {
|
|
212
277
|
const phase = phaseOf(view)
|
|
213
278
|
const revisingCards =
|
|
214
279
|
stage === 'cards' && refresh && ['browser-answers', 'answers-confirmation'].includes(phase)
|
|
280
|
+
// Batches start while reading continues and after earlier batches (D3).
|
|
281
|
+
const cardBatch =
|
|
282
|
+
stage === 'cards' &&
|
|
283
|
+
!refresh &&
|
|
284
|
+
view.scope.status === 'confirmed' &&
|
|
285
|
+
!['paused', 'sending', 'completed'].includes(phase) &&
|
|
286
|
+
uncoveredPending(view).length > 0
|
|
287
|
+
if (cardBatch && !manifest) {
|
|
288
|
+
const directory = join(run, stage, `batch-${randomUUID()}`)
|
|
289
|
+
await mkdir(directory, { recursive: true, mode: 0o700 })
|
|
290
|
+
manifest = {
|
|
291
|
+
format: classificationFormat,
|
|
292
|
+
stage,
|
|
293
|
+
batch: true,
|
|
294
|
+
fingerprint,
|
|
295
|
+
existingCards: view.privacy.cards.map((card) => ({
|
|
296
|
+
id: card.id,
|
|
297
|
+
question: card.question,
|
|
298
|
+
options: card.options.map(({ id, label }) => ({ id, label })),
|
|
299
|
+
answer: card.answer?.optionId ?? null,
|
|
300
|
+
termCount: card.termKeys.length,
|
|
301
|
+
})),
|
|
302
|
+
tasks: await createTasks(directory, stage, view, call, { batch: true }),
|
|
303
|
+
answersRevision: view.privacy.answersRevision,
|
|
304
|
+
}
|
|
305
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
306
|
+
await save(activePath, { directory })
|
|
307
|
+
return { stage, tasks: manifest.tasks, batch: true }
|
|
308
|
+
}
|
|
215
309
|
if (
|
|
216
310
|
phase !== stage &&
|
|
217
311
|
!revisingCards &&
|
|
218
|
-
!(
|
|
312
|
+
!(cardBatch && manifest) &&
|
|
313
|
+
!(stage === 'classification' && (phase === 'classification-confirmation' || grouping(view)))
|
|
219
314
|
)
|
|
220
315
|
throw new Error('STAGE_NOT_READY')
|
|
221
316
|
if (manifest && !refresh && stage === 'classification') {
|
|
@@ -318,6 +413,15 @@ async function runPhaseCore({ run, journeyId, command, stage, refresh = false, l
|
|
|
318
413
|
total: manifest.resolutionBatches.length,
|
|
319
414
|
}
|
|
320
415
|
}
|
|
416
|
+
if (stage === 'classification' && command === 'ingest' && !manifest.partial) {
|
|
417
|
+
const readiness = manifest.consolidation
|
|
418
|
+
? await consolidateClassification(directory, manifest)
|
|
419
|
+
: { readyForIngest: false }
|
|
420
|
+
if (!readiness.readyForIngest) {
|
|
421
|
+
const partial = await partialClassificationSubmission(manifest)
|
|
422
|
+
if (partial) return ingestKnownWorks(directory, manifest, view, call, partial)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
321
425
|
if (stage === 'classification' && !manifest.consolidation)
|
|
322
426
|
throw new Error('CONSOLIDATION_REQUIRED')
|
|
323
427
|
if (stage === 'classification') {
|
|
@@ -334,12 +438,15 @@ async function runPhaseCore({ run, journeyId, command, stage, refresh = false, l
|
|
|
334
438
|
journeyId,
|
|
335
439
|
expectedRevision: view.revision,
|
|
336
440
|
idempotencyKey: randomUUID(),
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
441
|
+
// After the known Works, scans keep moving the revision: bind to the classification (E2).
|
|
442
|
+
...(stage === 'classification' && manifest.partial
|
|
443
|
+
? { expectedClassificationRevision: manifest.partial.revision }
|
|
444
|
+
: {}),
|
|
445
|
+
...(stage === 'classification'
|
|
446
|
+
? { [key]: await classificationSubmission(manifest, view) }
|
|
447
|
+
: stage === 'cards'
|
|
448
|
+
? await cardSubmission(manifest)
|
|
449
|
+
: { [key]: await answers(tasks, key) }),
|
|
343
450
|
...(stage === 'resolution' ? { answersRevision: manifest.answersRevision } : {}),
|
|
344
451
|
}
|
|
345
452
|
await save(join(directory, 'request.json'), request)
|
|
@@ -359,13 +466,45 @@ async function runPhaseCore({ run, journeyId, command, stage, refresh = false, l
|
|
|
359
466
|
throw error
|
|
360
467
|
}
|
|
361
468
|
if (result.status === 'stale') throw new Error('REVISION_CONFLICT')
|
|
362
|
-
if (stage === 'classification') {
|
|
469
|
+
if (stage === 'classification' && !manifest.partial) {
|
|
363
470
|
const latest = await call('journey.get', { journeyId })
|
|
364
471
|
await publishCompleteClassification(manifest, latest, call, result.state?.revision)
|
|
365
472
|
}
|
|
366
473
|
return { stage, status: result.status, revision: result.state?.revision }
|
|
367
474
|
}
|
|
368
475
|
|
|
476
|
+
/** Classification published known Works and is still grouping the remaining roots (E2). */
|
|
477
|
+
const grouping = (view) => view.works.some((work) => work.placement === 'classifying')
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* E2: publishes known-project Works while unresolved roots are still being grouped, so their
|
|
481
|
+
* scan can start. Its request and receipt are kept apart from the final proposal's.
|
|
482
|
+
*/
|
|
483
|
+
async function ingestKnownWorks(directory, manifest, view, call, partial) {
|
|
484
|
+
const requestPath = join(directory, 'partial.request.json')
|
|
485
|
+
const request = (await optional(requestPath)) ?? {
|
|
486
|
+
journeyId: view.journeyId,
|
|
487
|
+
expectedRevision: view.revision,
|
|
488
|
+
idempotencyKey: randomUUID(),
|
|
489
|
+
...partial,
|
|
490
|
+
}
|
|
491
|
+
await save(requestPath, request)
|
|
492
|
+
const result = await call(procedureFor('classification'), request)
|
|
493
|
+
if (result.status === 'stale') {
|
|
494
|
+
await rm(requestPath, { force: true })
|
|
495
|
+
throw new Error('REVISION_CONFLICT')
|
|
496
|
+
}
|
|
497
|
+
manifest.partial = { status: result.status, revision: result.state?.revision }
|
|
498
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
499
|
+
return {
|
|
500
|
+
stage: 'classification',
|
|
501
|
+
status: result.status,
|
|
502
|
+
partial: true,
|
|
503
|
+
pendingRoots: partial.pendingRootSessionIds.length,
|
|
504
|
+
revision: result.state?.revision,
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
369
508
|
const terminalClassificationErrors = new Set([
|
|
370
509
|
'INVALID_SELECTION',
|
|
371
510
|
'CLASSIFICATION_PROJECT_NAME_CONFLICT',
|
|
@@ -410,6 +549,7 @@ export async function main(argv) {
|
|
|
410
549
|
stage: { type: 'string' },
|
|
411
550
|
refresh: { type: 'boolean', default: false },
|
|
412
551
|
limit: { type: 'string' },
|
|
552
|
+
'wait-ms': { type: 'string' },
|
|
413
553
|
model: { type: 'string' },
|
|
414
554
|
effort: { type: 'string' },
|
|
415
555
|
},
|
|
@@ -434,6 +574,7 @@ export async function main(argv) {
|
|
|
434
574
|
stage: values.stage,
|
|
435
575
|
refresh: values.refresh,
|
|
436
576
|
limit: values.limit === undefined ? 3 : Number(values.limit),
|
|
577
|
+
waitMs: values['wait-ms'] === undefined ? 300_000 : Number(values['wait-ms']),
|
|
437
578
|
},
|
|
438
579
|
call,
|
|
439
580
|
),
|
|
@@ -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,
|