@gitroll/tacitt-cli 0.0.0-stage → 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 (142) hide show
  1. package/README.md +84 -2
  2. package/bin/INDEX.md +9 -0
  3. package/bin/tacitt.cjs +16 -0
  4. package/dist/agent-setup-B8DAiQ6Z.mjs +71 -0
  5. package/dist/agent-setup-worker.mjs +56 -0
  6. package/dist/cli.mjs +1442 -0
  7. package/dist/codex-title-worker.mjs +215 -0
  8. package/dist/configured-preview-worker.mjs +133 -0
  9. package/dist/configured-session-worker.mjs +117 -0
  10. package/dist/frame-waS5FnN5.mjs +84 -0
  11. package/dist/heap-1FYhGds4.mjs +75 -0
  12. package/dist/heap-BHcvxWCw.mjs +7265 -0
  13. package/dist/inspection-worker.mjs +57 -0
  14. package/dist/job-J3NFw3MH.mjs +194 -0
  15. package/dist/locate-U3-nkird.mjs +10670 -0
  16. package/dist/messages-D3m3bhva.mjs +40 -0
  17. package/dist/model-BJdTLNjT.mjs +41 -0
  18. package/dist/operation-executor.mjs +181 -0
  19. package/dist/preview-worker.mjs +22 -0
  20. package/dist/privacy-validator.mjs +46 -0
  21. package/dist/query-BA0XCwnR.mjs +14869 -0
  22. package/dist/run-BeLHsZWb.mjs +916 -0
  23. package/dist/session-process.mjs +90 -0
  24. package/dist/src-B5SArpVY.mjs +6601 -0
  25. package/dist/src-CC95LYYp.mjs +12985 -0
  26. package/dist/src-k0Ya1Bri.mjs +16422 -0
  27. package/dist/store-database-worker.mjs +14 -0
  28. package/dist/web/THIRD_PARTY_LICENSES.md +388 -0
  29. package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  30. package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  31. package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  32. package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  33. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  34. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  35. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  36. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  37. package/dist/web/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  38. package/dist/web/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  39. package/dist/web/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  40. package/dist/web/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  41. package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  42. package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  43. package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  44. package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  45. package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  46. package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  47. package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  48. package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  49. package/dist/web/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  50. package/dist/web/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  51. package/dist/web/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
  52. package/dist/web/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
  53. package/dist/web/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  54. package/dist/web/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  55. package/dist/web/assets/ibm-plex-sans-cyrillic-700-normal-D_uavGvn.woff2 +0 -0
  56. package/dist/web/assets/ibm-plex-sans-cyrillic-700-normal-RFb1v-CK.woff +0 -0
  57. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  58. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  59. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
  60. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
  61. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  62. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  63. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-700-normal-BmRLXWpJ.woff2 +0 -0
  64. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-700-normal-DCQwxkRk.woff +0 -0
  65. package/dist/web/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  66. package/dist/web/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  67. package/dist/web/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
  68. package/dist/web/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
  69. package/dist/web/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  70. package/dist/web/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  71. package/dist/web/assets/ibm-plex-sans-greek-700-normal-CkZN0sRy.woff +0 -0
  72. package/dist/web/assets/ibm-plex-sans-greek-700-normal-wyJ6jzdA.woff2 +0 -0
  73. package/dist/web/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  74. package/dist/web/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  75. package/dist/web/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  76. package/dist/web/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  77. package/dist/web/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  78. package/dist/web/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  79. package/dist/web/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
  80. package/dist/web/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
  81. package/dist/web/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  82. package/dist/web/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  83. package/dist/web/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
  84. package/dist/web/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
  85. package/dist/web/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  86. package/dist/web/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  87. package/dist/web/assets/ibm-plex-sans-latin-ext-700-normal-DZH6Xh-4.woff +0 -0
  88. package/dist/web/assets/ibm-plex-sans-latin-ext-700-normal-aMgXseD4.woff2 +0 -0
  89. package/dist/web/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  90. package/dist/web/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  91. package/dist/web/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
  92. package/dist/web/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
  93. package/dist/web/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  94. package/dist/web/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  95. package/dist/web/assets/ibm-plex-sans-vietnamese-700-normal-BV5B8Are.woff +0 -0
  96. package/dist/web/assets/ibm-plex-sans-vietnamese-700-normal-CkEo6LDy.woff2 +0 -0
  97. package/dist/web/assets/index-Bjos73wK.js +67 -0
  98. package/dist/web/assets/index-Bl4C2b2_.css +1 -0
  99. package/dist/web/index.html +14 -0
  100. package/package.json +56 -3
  101. package/skills/INDEX.md +9 -0
  102. package/skills/tacitt/INDEX.md +11 -0
  103. package/skills/tacitt/SKILL.md +44 -0
  104. package/skills/tacitt/references/INDEX.md +15 -0
  105. package/skills/tacitt/references/b-method.md +22 -0
  106. package/skills/tacitt/references/bootstrap.md +109 -0
  107. package/skills/tacitt/references/classification.md +83 -0
  108. package/skills/tacitt/references/commands.md +147 -0
  109. package/skills/tacitt/references/journey.md +54 -0
  110. package/skills/tacitt/references/parallel-review.md +64 -0
  111. package/skills/tacitt/references/send.md +30 -0
  112. package/skills/tacitt/scripts/INDEX.md +43 -0
  113. package/skills/tacitt/scripts/answer.mjs +278 -0
  114. package/skills/tacitt/scripts/describe.mjs +55 -0
  115. package/skills/tacitt/scripts/journey-card-consolidation.d.mts +17 -0
  116. package/skills/tacitt/scripts/journey-card-consolidation.mjs +209 -0
  117. package/skills/tacitt/scripts/journey-classification-metadata.d.mts +16 -0
  118. package/skills/tacitt/scripts/journey-classification-metadata.mjs +111 -0
  119. package/skills/tacitt/scripts/journey-classification-progress.mjs +195 -0
  120. package/skills/tacitt/scripts/journey-classification-tasks.d.mts +18 -0
  121. package/skills/tacitt/scripts/journey-classification-tasks.mjs +345 -0
  122. package/skills/tacitt/scripts/journey-classification-validation.d.mts +20 -0
  123. package/skills/tacitt/scripts/journey-classification-validation.mjs +240 -0
  124. package/skills/tacitt/scripts/journey-classification.mjs +145 -0
  125. package/skills/tacitt/scripts/journey-consolidation-submit.mjs +227 -0
  126. package/skills/tacitt/scripts/journey-consolidation.d.mts +14 -0
  127. package/skills/tacitt/scripts/journey-consolidation.mjs +227 -0
  128. package/skills/tacitt/scripts/journey-scan-timing.mjs +143 -0
  129. package/skills/tacitt/scripts/journey-state.d.mts +43 -0
  130. package/skills/tacitt/scripts/journey-state.mjs +294 -0
  131. package/skills/tacitt/scripts/journey-tasks.d.mts +26 -0
  132. package/skills/tacitt/scripts/journey-tasks.mjs +185 -0
  133. package/skills/tacitt/scripts/journey.d.mts +40 -0
  134. package/skills/tacitt/scripts/journey.mjs +592 -0
  135. package/skills/tacitt/scripts/local-cli.d.mts +18 -0
  136. package/skills/tacitt/scripts/local-cli.mjs +116 -0
  137. package/skills/tacitt/scripts/privacy-validator.mjs +10369 -0
  138. package/skills/tacitt/scripts/review-stream-client.d.mts +22 -0
  139. package/skills/tacitt/scripts/review-stream-client.mjs +194 -0
  140. package/skills/tacitt/scripts/review.d.mts +87 -0
  141. package/skills/tacitt/scripts/review.mjs +1610 -0
  142. package/skills/tacitt/scripts/verify-cli.mjs +10 -0
@@ -0,0 +1,592 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { mkdir, realpath, rm } from 'node:fs/promises'
3
+ import { join, resolve, isAbsolute } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { parseArgs } from 'node:util'
6
+ import { main as review, withJourneyCli } from './review.mjs'
7
+ import {
8
+ read,
9
+ optional,
10
+ save,
11
+ phaseOf,
12
+ activeTerms,
13
+ stageFingerprint,
14
+ procedureFor,
15
+ recoverWrites,
16
+ definiteWriteRejection,
17
+ resolutionAuthority,
18
+ resolutionEvidence,
19
+ planResolutions,
20
+ assertResolutionFresh,
21
+ uncoveredPending,
22
+ } from './journey-state.mjs'
23
+ import { classificationTasks } from './journey-classification-tasks.mjs'
24
+ import { classificationEvidence, classificationFormat } from './journey-classification.mjs'
25
+ import {
26
+ consolidateClassification,
27
+ classificationSubmission,
28
+ partialClassificationSubmission,
29
+ } from './journey-consolidation.mjs'
30
+ import { consolidateCards, cardSubmission } from './journey-card-consolidation.mjs'
31
+ import {
32
+ classificationProgress,
33
+ publishClassificationProgress,
34
+ publishFailedClassificationProgress,
35
+ } from './journey-classification-progress.mjs'
36
+ import { answers, createTasks } from './journey-tasks.mjs'
37
+ import { scanTiming } from './journey-scan-timing.mjs'
38
+ export { phaseOf, stageFingerprint } from './journey-state.mjs'
39
+
40
+ async function publishCompleteClassification(manifest, view, call, proposalRevision) {
41
+ const baselineRevision = manifest.classification?.baselineRevision
42
+ if (
43
+ !Number.isSafeInteger(baselineRevision) ||
44
+ view.classificationRevision !== view.revision ||
45
+ view.classificationRevision <= baselineRevision
46
+ )
47
+ throw new Error('CLASSIFICATION_PROGRESS_COMPLETION_UNCONFIRMED')
48
+ if (proposalRevision !== view.revision)
49
+ throw new Error('CLASSIFICATION_PROGRESS_COMPLETION_UNCONFIRMED')
50
+ await publishClassificationProgress(manifest, view, call, {
51
+ status: 'complete',
52
+ stage: 'proposal',
53
+ proposalRevision,
54
+ })
55
+ }
56
+
57
+ // Longer than the default request bound, still inside a 2-minute tool timeout.
58
+ const FREEZE_REQUEST_MS = 100_000
59
+
60
+ async function prepareInitialFreeze(run, journeyId, call, view) {
61
+ const path = join(run, 'initial-freeze-timing.json')
62
+ const timing = await optional(path)
63
+ if (!view.consent || view.phase === 'paused' || view.scope?.status === 'confirmed')
64
+ throw new Error('INITIAL_FREEZE_NOT_ALLOWED')
65
+ if (timing?.completedAt && timing.snapshotId !== view.snapshotId)
66
+ throw new Error('INITIAL_FREEZE_SNAPSHOT_CHANGED')
67
+ if (timing?.completedAt && timing.snapshotId === view.snapshotId)
68
+ return {
69
+ stage: 'freeze',
70
+ status: 'complete',
71
+ ...timing,
72
+ elapsedMs: Date.parse(timing.completedAt) - Date.parse(timing.startedAt),
73
+ }
74
+ if (view.snapshotId && (!timing?.startedAt || view.classificationRevision !== null))
75
+ throw new Error('INITIAL_FREEZE_NOT_STARTED_HERE')
76
+ const startedAt = timing?.startedAt ?? new Date().toISOString()
77
+ if (!timing) await save(path, { startedAt, completedAt: null })
78
+ // Safe on retry while scope remains pending; journey.prepare resumes the frozen capture.
79
+ try {
80
+ await call('journey.prepare', { journeyId }, { timeoutMs: FREEZE_REQUEST_MS })
81
+ } catch (error) {
82
+ // A large first freeze can outlast one helper command; captured roots persist for the rerun.
83
+ if (error?.code === 'CLI_UNAVAILABLE') return { stage: 'freeze', status: 'running', startedAt }
84
+ throw error
85
+ }
86
+ const completed = await call('journey.get', { journeyId })
87
+ if (
88
+ !completed.snapshotId ||
89
+ completed.scope?.status === 'confirmed' ||
90
+ completed.classificationRevision !== null
91
+ )
92
+ throw new Error('INITIAL_FREEZE_NOT_CONFIRMED')
93
+ const completedAt = new Date().toISOString()
94
+ const saved = { startedAt, completedAt, snapshotId: completed.snapshotId }
95
+ await save(path, saved)
96
+ return {
97
+ stage: 'freeze',
98
+ status: 'complete',
99
+ ...saved,
100
+ elapsedMs: Date.parse(completedAt) - Date.parse(startedAt),
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Scan timing is an internal diagnostic (BL-180): a receipt that no longer matches the journey
106
+ * marks timing stale or unavailable and never hides the rest of the helper's report.
107
+ */
108
+ async function observeScanTiming(run, journeyId, view, start, call) {
109
+ try {
110
+ return await scanTiming(run, journeyId, view, start, call)
111
+ } catch (error) {
112
+ const code = error instanceof Error ? error.message : ''
113
+ if (!code.startsWith('SCAN_TIMING_')) throw error
114
+ process.stderr.write(code + '\n')
115
+ return { status: code === 'SCAN_TIMING_SCOPE_CHANGED' ? 'stale' : 'unavailable', code }
116
+ }
117
+ }
118
+
119
+ /** Locked by withJourneyCli in production; injected transport permits deterministic crash tests. */
120
+ /** A running journey render (panel or CLI) with packaged/selected Work counts (BL-185). */
121
+ function packagingProgress(view) {
122
+ if (!view.packaging) return {}
123
+ const selected = view.works.filter((work) => work.access !== 'drop' && work.rootSessionIds.length)
124
+ return {
125
+ packaging: {
126
+ ...view.packaging,
127
+ selected: selected.length,
128
+ packaged: selected.filter(
129
+ (work) => work.status === 'ready' && work.artifact?.revision === work.revision,
130
+ ).length,
131
+ },
132
+ }
133
+ }
134
+
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
+ ) {
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
+ )
179
+ let view = await call('journey.get', { journeyId })
180
+ if (command === 'scan-start')
181
+ return {
182
+ scan: await observeScanTiming(run, journeyId, view, true, call),
183
+ next: 'journey.prepare',
184
+ }
185
+ if (command === 'status') {
186
+ const active = await optional(join(run, 'classification-active.json'))
187
+ const manifest = active ? await optional(join(active.directory, 'manifest.json')) : null
188
+ const freeze = await optional(join(run, 'initial-freeze-timing.json'))
189
+ const proposalAt =
190
+ view.classificationProgress?.status === 'complete'
191
+ ? view.classificationProgress.updatedAt
192
+ : null
193
+ const coldElapsedMs =
194
+ freeze?.completedAt && freeze.snapshotId === view.snapshotId && proposalAt
195
+ ? Math.max(0, Date.parse(proposalAt) - Date.parse(freeze.startedAt))
196
+ : null
197
+ return {
198
+ ...(manifest?.classification
199
+ ? { classification: await classificationProgress(manifest) }
200
+ : {}),
201
+ phase: phaseOf(view),
202
+ revision: view.revision,
203
+ readingComplete: view.readingComplete,
204
+ pending: activeTerms(view).filter((term) => term.disposition === 'pending').length,
205
+ answersRevision: view.privacy.answersRevision,
206
+ ...packagingProgress(view),
207
+ coldElapsedMs,
208
+ scan: await observeScanTiming(run, journeyId, view, false, call),
209
+ }
210
+ }
211
+ if (command === 'prepare') return prepareInitialFreeze(run, journeyId, call, view)
212
+ if (
213
+ !['classification', 'cards', 'resolution'].includes(stage) ||
214
+ !['tasks', 'evidence', 'consolidate', 'ingest'].includes(command)
215
+ )
216
+ throw new Error('INVALID_ARGUMENTS')
217
+ const activePath = join(run, `${stage}-active.json`)
218
+ const active = await optional(activePath)
219
+ let manifest = active ? await optional(join(active.directory, 'manifest.json')) : null
220
+ if (active && !manifest) throw new Error('CORRUPTED_STATE')
221
+ if (manifest && manifest.format !== classificationFormat)
222
+ throw new Error('INCOMPATIBLE_JOURNEY_TASKS')
223
+ if (manifest) {
224
+ const recovery = await recoverWrites(active.directory, manifest, call)
225
+ // A failed transport leaves the exact pending request intact and never reaches refresh.
226
+ if (recovery.recovered) view = await call('journey.get', { journeyId })
227
+ if (recovery.stale && (!refresh || command !== 'tasks')) throw new Error('REVISION_CONFLICT')
228
+ if (recovery.rejected && !(refresh && command === 'tasks')) {
229
+ const error = new Error(recovery.rejected)
230
+ error.code = recovery.rejected
231
+ throw error
232
+ }
233
+ if (recovery.completed === recovery.total && !recovery.stale && command === 'ingest') {
234
+ if (stage === 'classification' && manifest.consolidation)
235
+ await publishCompleteClassification(
236
+ manifest,
237
+ view,
238
+ call,
239
+ (await optional(join(active.directory, 'receipt.json')))?.revision,
240
+ )
241
+ return { stage, ...recovery }
242
+ }
243
+ }
244
+ const consolidationRefresh = refresh && command === 'consolidate' && stage === 'classification'
245
+ if (
246
+ consolidationRefresh &&
247
+ !['classification', 'classification-confirmation'].includes(phaseOf(view)) &&
248
+ !grouping(view)
249
+ )
250
+ throw new Error('STAGE_NOT_READY')
251
+ if (consolidationRefresh && (await optional(join(active?.directory ?? run, 'receipt.json'))))
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
262
+ const fingerprint = stageFingerprint(view, stage)
263
+ if (manifest?.resolutionBatches && !refresh)
264
+ await assertResolutionFresh(active.directory, manifest, view)
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
+ )
274
+ throw new Error('STALE_TASKS')
275
+ if (refresh && command !== 'tasks' && !consolidationRefresh) throw new Error('INVALID_ARGUMENTS')
276
+ if (command === 'tasks') {
277
+ const phase = phaseOf(view)
278
+ const revisingCards =
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
+ }
309
+ if (
310
+ phase !== stage &&
311
+ !revisingCards &&
312
+ !(cardBatch && manifest) &&
313
+ !(stage === 'classification' && (phase === 'classification-confirmation' || grouping(view)))
314
+ )
315
+ throw new Error('STAGE_NOT_READY')
316
+ if (manifest && !refresh && stage === 'classification') {
317
+ if (!manifest.classification && manifest.format === classificationFormat)
318
+ throw new Error('INCOMPATIBLE_CLASSIFICATION_TASKS')
319
+ const result = await classificationTasks(active.directory, manifest, view, call, limit)
320
+ await publishClassificationProgress(manifest, view, call)
321
+ return result
322
+ }
323
+ if (manifest && !refresh)
324
+ return {
325
+ stage,
326
+ tasks: manifest.tasks,
327
+ consolidation: manifest.consolidation ?? null,
328
+ reused: true,
329
+ }
330
+ const directory = join(run, stage, `${fingerprint}-${randomUUID()}`)
331
+ await mkdir(directory, { recursive: true, mode: 0o700 })
332
+ manifest = {
333
+ format: classificationFormat,
334
+ stage,
335
+ fingerprint,
336
+ tasks: stage === 'classification' ? [] : await createTasks(directory, stage, view, call),
337
+ answersRevision: view.privacy.answersRevision,
338
+ }
339
+ await save(join(directory, 'manifest.json'), manifest)
340
+ await save(activePath, { directory })
341
+ if (stage === 'classification') {
342
+ const result = await classificationTasks(directory, manifest, view, call, limit)
343
+ await publishClassificationProgress(manifest, view, call)
344
+ return result
345
+ }
346
+ return { stage, tasks: manifest.tasks }
347
+ }
348
+ if (!manifest || !active) throw new Error('TASKS_REQUIRED')
349
+ const directory = active.directory
350
+ if (command === 'evidence') {
351
+ if (stage !== 'classification') throw new Error('INVALID_ARGUMENTS')
352
+ const result = await classificationEvidence(directory, manifest, view, call, limit)
353
+ await publishClassificationProgress(manifest, view, call)
354
+ return result
355
+ }
356
+ if (command === 'consolidate') {
357
+ if (stage === 'cards') return consolidateCards(directory, manifest)
358
+ if (stage !== 'classification') throw new Error('INVALID_ARGUMENTS')
359
+ if (!manifest.classification?.complete) throw new Error('CLASSIFICATION_READS_INCOMPLETE')
360
+ let pendingRootAnswers = false
361
+ for (const item of manifest.tasks)
362
+ if (!item.projectKeys && !(await optional(item.answer))) pendingRootAnswers = true
363
+ if (pendingRootAnswers) throw new Error('CLASSIFICATION_ANSWERS_INCOMPLETE')
364
+ const evidence = await classificationEvidence(directory, manifest, view, call, limit)
365
+ await publishClassificationProgress(manifest, view, call)
366
+ if (!evidence.evidenceComplete || evidence.tasks.length)
367
+ return { ...evidence, consolidationReady: false }
368
+ const result = await consolidateClassification(directory, manifest, {
369
+ refresh: consolidationRefresh,
370
+ })
371
+ await publishClassificationProgress(manifest, view, call, {
372
+ stage: 'proposal',
373
+ })
374
+ return result
375
+ }
376
+ if (stage === 'resolution') {
377
+ if (!manifest.resolutionBatches) {
378
+ manifest.resolutionBatches = planResolutions(
379
+ view,
380
+ await answers(manifest.tasks, 'resolutions'),
381
+ )
382
+ manifest.resolutionAuthority = resolutionAuthority(view)
383
+ manifest.resolutionEvidence = resolutionEvidence(view)
384
+ await save(join(directory, 'manifest.json'), manifest)
385
+ }
386
+ let recorded = 0
387
+ for (const batch of manifest.resolutionBatches) {
388
+ const requestPath = join(directory, `${batch.id}.request.json`),
389
+ receiptPath = join(directory, `${batch.id}.receipt.json`)
390
+ if (await optional(receiptPath)) continue
391
+ const latest = await call('journey.get', { journeyId })
392
+ await assertResolutionFresh(directory, manifest, latest)
393
+ const request = {
394
+ journeyId,
395
+ expectedRevision: latest.revision,
396
+ idempotencyKey: randomUUID(),
397
+ answersRevision: manifest.answersRevision,
398
+ resolutions: batch.resolutions,
399
+ }
400
+ await save(requestPath, request)
401
+ const result = await call(procedureFor(stage), request)
402
+ await save(receiptPath, {
403
+ status: result.status,
404
+ revision: result.state?.revision,
405
+ })
406
+ if (result.status === 'stale') throw new Error('REVISION_CONFLICT')
407
+ recorded++
408
+ }
409
+ return {
410
+ stage,
411
+ recorded,
412
+ completed: manifest.resolutionBatches.length,
413
+ total: manifest.resolutionBatches.length,
414
+ }
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
+ }
425
+ if (stage === 'classification' && !manifest.consolidation)
426
+ throw new Error('CONSOLIDATION_REQUIRED')
427
+ if (stage === 'classification') {
428
+ const readiness = await consolidateClassification(directory, manifest)
429
+ if (!readiness.readyForIngest) return readiness
430
+ }
431
+ if (stage === 'cards') {
432
+ const readiness = await consolidateCards(directory, manifest)
433
+ if (!readiness.readyForIngest) return readiness
434
+ }
435
+ const key = stage === 'classification' ? 'items' : stage === 'cards' ? 'cards' : 'resolutions'
436
+ const tasks = stage === 'classification' ? [manifest.consolidation] : manifest.tasks
437
+ const request = {
438
+ journeyId,
439
+ expectedRevision: view.revision,
440
+ idempotencyKey: randomUUID(),
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) }),
450
+ ...(stage === 'resolution' ? { answersRevision: manifest.answersRevision } : {}),
451
+ }
452
+ await save(join(directory, 'request.json'), request)
453
+ let result
454
+ try {
455
+ result = await call(procedureFor(stage), request)
456
+ await save(join(directory, 'receipt.json'), {
457
+ status: result.status,
458
+ revision: result.state?.revision,
459
+ })
460
+ } catch (error) {
461
+ if (definiteWriteRejection(error))
462
+ await save(join(directory, 'receipt.json'), {
463
+ status: 'rejected',
464
+ code: error.code,
465
+ })
466
+ throw error
467
+ }
468
+ if (result.status === 'stale') throw new Error('REVISION_CONFLICT')
469
+ if (stage === 'classification' && !manifest.partial) {
470
+ const latest = await call('journey.get', { journeyId })
471
+ await publishCompleteClassification(manifest, latest, call, result.state?.revision)
472
+ }
473
+ return { stage, status: result.status, revision: result.state?.revision }
474
+ }
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
+
508
+ const terminalClassificationErrors = new Set([
509
+ 'INVALID_SELECTION',
510
+ 'CLASSIFICATION_PROJECT_NAME_CONFLICT',
511
+ 'CLASSIFICATION_ROOT_COVERAGE_INVALID',
512
+ 'CLASSIFICATION_THEME_TOO_SMALL',
513
+ 'CLASSIFICATION_UNSORTED_DUPLICATE',
514
+ 'INVALID_CLASSIFICATION_GROUP_KIND',
515
+ 'INVALID_CLASSIFICATION_ITEM',
516
+ ])
517
+
518
+ /** Preserve the original error; publish failure only against a still-current running checkpoint. */
519
+ export async function runPhase(args, call) {
520
+ try {
521
+ return await runPhaseCore(args, call)
522
+ } catch (error) {
523
+ const code = error?.code ?? error?.message
524
+ if (args.stage === 'classification' && terminalClassificationErrors.has(code)) {
525
+ try {
526
+ const active = await optional(join(args.run, 'classification-active.json'))
527
+ const manifest = active ? await optional(join(active.directory, 'manifest.json')) : null
528
+ if (manifest?.format === classificationFormat && manifest.classification) {
529
+ const latest = await call('journey.get', {
530
+ journeyId: args.journeyId,
531
+ })
532
+ await publishFailedClassificationProgress(manifest, latest, call)
533
+ }
534
+ } catch {
535
+ // Progress is advisory; never mask the classification error that caused this path.
536
+ }
537
+ }
538
+ throw error
539
+ }
540
+ }
541
+
542
+ export async function main(argv) {
543
+ const { values, positionals } = parseArgs({
544
+ args: argv,
545
+ allowPositionals: true,
546
+ options: {
547
+ run: { type: 'string' },
548
+ journey: { type: 'string' },
549
+ stage: { type: 'string' },
550
+ refresh: { type: 'boolean', default: false },
551
+ limit: { type: 'string' },
552
+ 'wait-ms': { type: 'string' },
553
+ model: { type: 'string' },
554
+ effort: { type: 'string' },
555
+ },
556
+ })
557
+ const [command] = positionals
558
+ if (!values.run) throw new Error('INVALID_ARGUMENTS')
559
+ if (command === 'init') {
560
+ const cli = positionals.slice(1)
561
+ const scopes = cli.flatMap((value, index) => (value === '--state-dir' ? [cli[index + 1]] : []))
562
+ if (scopes.length !== 1 || typeof scopes[0] !== 'string' || !isAbsolute(scopes[0]))
563
+ throw new Error('EXPLICIT_CLI_STATE_DIRECTORY_REQUIRED')
564
+ return review(argv)
565
+ }
566
+ const run = resolve(values.run),
567
+ config = await read(join(run, 'config.json'))
568
+ return withJourneyCli(run, (call) =>
569
+ runPhase(
570
+ {
571
+ run,
572
+ journeyId: config.journeyId,
573
+ command,
574
+ stage: values.stage,
575
+ refresh: values.refresh,
576
+ limit: values.limit === undefined ? 3 : Number(values.limit),
577
+ waitMs: values['wait-ms'] === undefined ? 300_000 : Number(values['wait-ms']),
578
+ },
579
+ call,
580
+ ),
581
+ )
582
+ }
583
+ const entry = process.argv[1]
584
+ if (entry && (await realpath(entry)) === fileURLToPath(import.meta.url))
585
+ main(process.argv.slice(2)).then(
586
+ (result) => process.stdout.write(JSON.stringify({ ok: true, ...result }) + '\n'),
587
+ (error) => {
588
+ const code = /^[A-Z_]{3,80}$/u.test(error.message) ? error.message : 'JOURNEY_HELPER_FAILED'
589
+ process.stderr.write(code + '\n')
590
+ process.exitCode = 1
591
+ },
592
+ )
@@ -0,0 +1,18 @@
1
+ export interface LocalCliBinding {
2
+ schemaVersion: 1
3
+ package: '@gitroll/tacitt-cli'
4
+ version: string
5
+ nodePath: string
6
+ cliPath: string
7
+ artifactSha256: string
8
+ }
9
+
10
+ export function readLocalCliBinding(
11
+ cliPath: string,
12
+ expected: { name: string; version: string },
13
+ ): Promise<LocalCliBinding>
14
+
15
+ export function verifyLocalCliBinding(
16
+ receiptPath: string,
17
+ expectedReceiptSha256: string,
18
+ ): Promise<LocalCliBinding>