@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.
Files changed (50) hide show
  1. package/dist/{agent-setup-B85GaVIz.mjs → agent-setup-B8DAiQ6Z.mjs} +2 -2
  2. package/dist/agent-setup-worker.mjs +5 -5
  3. package/dist/cli.mjs +108 -9
  4. package/dist/codex-title-worker.mjs +1 -1
  5. package/dist/configured-preview-worker.mjs +5 -5
  6. package/dist/configured-session-worker.mjs +4 -4
  7. package/dist/{frame-D1j9R8xX.mjs → frame-waS5FnN5.mjs} +1 -1
  8. package/dist/{heap-tfU61Ehy.mjs → heap-BHcvxWCw.mjs} +20 -8
  9. package/dist/inspection-worker.mjs +57 -0
  10. package/dist/{job--TceKk0l.mjs → job-J3NFw3MH.mjs} +2 -2
  11. package/dist/{locate-Bn32k2se.mjs → locate-U3-nkird.mjs} +29 -6
  12. package/dist/messages-D3m3bhva.mjs +40 -0
  13. package/dist/{model-7Nu7szNP.mjs → model-BJdTLNjT.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-Da-KDbSV.mjs → query-BA0XCwnR.mjs} +486 -128
  18. package/dist/{run-CCATsHS2.mjs → run-BeLHsZWb.mjs} +4 -4
  19. package/dist/{src-DynBRDBc.mjs → src-B5SArpVY.mjs} +20 -9
  20. package/dist/{src-CPPjOyHZ.mjs → src-CC95LYYp.mjs} +271 -68
  21. package/dist/{src-BngGpFKd.mjs → src-k0Ya1Bri.mjs} +13 -7
  22. package/dist/store-database-worker.mjs +1 -1
  23. package/dist/web/assets/index-Bjos73wK.js +67 -0
  24. package/dist/web/assets/index-Bl4C2b2_.css +1 -0
  25. package/dist/web/index.html +2 -2
  26. package/package.json +1 -1
  27. package/skills/tacitt/SKILL.md +13 -11
  28. package/skills/tacitt/references/b-method.md +1 -1
  29. package/skills/tacitt/references/classification.md +3 -3
  30. package/skills/tacitt/references/commands.md +6 -6
  31. package/skills/tacitt/references/journey.md +13 -11
  32. package/skills/tacitt/references/parallel-review.md +3 -3
  33. package/skills/tacitt/references/send.md +5 -5
  34. package/skills/tacitt/scripts/answer.mjs +29 -8
  35. package/skills/tacitt/scripts/journey-card-consolidation.d.mts +13 -4
  36. package/skills/tacitt/scripts/journey-card-consolidation.mjs +81 -18
  37. package/skills/tacitt/scripts/journey-classification-progress.mjs +2 -1
  38. package/skills/tacitt/scripts/journey-classification-validation.mjs +49 -28
  39. package/skills/tacitt/scripts/journey-consolidation-submit.mjs +42 -5
  40. package/skills/tacitt/scripts/journey-consolidation.d.mts +5 -0
  41. package/skills/tacitt/scripts/journey-consolidation.mjs +4 -1
  42. package/skills/tacitt/scripts/journey-state.d.mts +1 -0
  43. package/skills/tacitt/scripts/journey-state.mjs +29 -3
  44. package/skills/tacitt/scripts/journey-tasks.mjs +4 -2
  45. package/skills/tacitt/scripts/journey.d.mts +2 -0
  46. package/skills/tacitt/scripts/journey.mjs +154 -13
  47. package/skills/tacitt/scripts/privacy-validator.mjs +25 -6
  48. package/skills/tacitt/scripts/review.mjs +24 -3
  49. package/dist/web/assets/index-CWQ05PFl.js +0 -67
  50. 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 { consolidateClassification, classificationSubmission } from './journey-consolidation.mjs'
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
- async function runPhaseCore({ run, journeyId, command, stage, refresh = false, limit = 3 }, call) {
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
- else if (manifest && manifest.fingerprint !== fingerprint && (!refresh || consolidationRefresh))
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
- !(stage === 'classification' && phase === 'classification-confirmation')
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
- [key]:
338
- stage === 'classification'
339
- ? await classificationSubmission(manifest, view)
340
- : stage === 'cards'
341
- ? await cardSubmission(manifest)
342
- : await answers(tasks, key),
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({ cards: array(decisionCardInputSchema).max(20) }).strict();
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(["placed", "unsorted"]),
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 = ['final-preview-and-confirmation', 'package', 'send'].includes(phaseOf(view))
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,