@gitroll/tacitt-cli 0.0.0-stage → 0.1.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/README.md +84 -2
- package/bin/INDEX.md +9 -0
- package/bin/tacitt.cjs +16 -0
- package/dist/agent-setup-B85GaVIz.mjs +71 -0
- package/dist/agent-setup-worker.mjs +56 -0
- package/dist/cli.mjs +1343 -0
- package/dist/codex-title-worker.mjs +215 -0
- package/dist/configured-preview-worker.mjs +133 -0
- package/dist/configured-session-worker.mjs +117 -0
- package/dist/frame-D1j9R8xX.mjs +84 -0
- package/dist/heap-1FYhGds4.mjs +75 -0
- package/dist/heap-tfU61Ehy.mjs +7253 -0
- package/dist/job--TceKk0l.mjs +194 -0
- package/dist/locate-Bn32k2se.mjs +10647 -0
- package/dist/model-7Nu7szNP.mjs +41 -0
- package/dist/operation-executor.mjs +181 -0
- package/dist/preview-worker.mjs +22 -0
- package/dist/privacy-validator.mjs +46 -0
- package/dist/query-Da-KDbSV.mjs +14511 -0
- package/dist/run-CCATsHS2.mjs +916 -0
- package/dist/session-process.mjs +90 -0
- package/dist/src-BngGpFKd.mjs +16416 -0
- package/dist/src-CPPjOyHZ.mjs +12782 -0
- package/dist/src-DynBRDBc.mjs +6590 -0
- package/dist/store-database-worker.mjs +14 -0
- package/dist/web/THIRD_PARTY_LICENSES.md +388 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-700-normal-D_uavGvn.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-700-normal-RFb1v-CK.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-700-normal-BmRLXWpJ.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-700-normal-DCQwxkRk.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-700-normal-CkZN0sRy.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-700-normal-wyJ6jzdA.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-700-normal-DZH6Xh-4.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-700-normal-aMgXseD4.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-700-normal-BV5B8Are.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-700-normal-CkEo6LDy.woff2 +0 -0
- package/dist/web/assets/index-CWQ05PFl.js +67 -0
- package/dist/web/assets/index-J-AWt_cW.css +1 -0
- package/dist/web/index.html +14 -0
- package/package.json +56 -3
- package/skills/INDEX.md +9 -0
- package/skills/tacitt/INDEX.md +11 -0
- package/skills/tacitt/SKILL.md +42 -0
- package/skills/tacitt/references/INDEX.md +15 -0
- package/skills/tacitt/references/b-method.md +22 -0
- package/skills/tacitt/references/bootstrap.md +109 -0
- package/skills/tacitt/references/classification.md +83 -0
- package/skills/tacitt/references/commands.md +147 -0
- package/skills/tacitt/references/journey.md +52 -0
- package/skills/tacitt/references/parallel-review.md +64 -0
- package/skills/tacitt/references/send.md +30 -0
- package/skills/tacitt/scripts/INDEX.md +43 -0
- package/skills/tacitt/scripts/answer.mjs +257 -0
- package/skills/tacitt/scripts/describe.mjs +55 -0
- package/skills/tacitt/scripts/journey-card-consolidation.d.mts +8 -0
- package/skills/tacitt/scripts/journey-card-consolidation.mjs +146 -0
- package/skills/tacitt/scripts/journey-classification-metadata.d.mts +16 -0
- package/skills/tacitt/scripts/journey-classification-metadata.mjs +111 -0
- package/skills/tacitt/scripts/journey-classification-progress.mjs +194 -0
- package/skills/tacitt/scripts/journey-classification-tasks.d.mts +18 -0
- package/skills/tacitt/scripts/journey-classification-tasks.mjs +345 -0
- package/skills/tacitt/scripts/journey-classification-validation.d.mts +20 -0
- package/skills/tacitt/scripts/journey-classification-validation.mjs +219 -0
- package/skills/tacitt/scripts/journey-classification.mjs +145 -0
- package/skills/tacitt/scripts/journey-consolidation-submit.mjs +190 -0
- package/skills/tacitt/scripts/journey-consolidation.d.mts +9 -0
- package/skills/tacitt/scripts/journey-consolidation.mjs +224 -0
- package/skills/tacitt/scripts/journey-scan-timing.mjs +143 -0
- package/skills/tacitt/scripts/journey-state.d.mts +42 -0
- package/skills/tacitt/scripts/journey-state.mjs +268 -0
- package/skills/tacitt/scripts/journey-tasks.d.mts +26 -0
- package/skills/tacitt/scripts/journey-tasks.mjs +183 -0
- package/skills/tacitt/scripts/journey.d.mts +38 -0
- package/skills/tacitt/scripts/journey.mjs +451 -0
- package/skills/tacitt/scripts/local-cli.d.mts +18 -0
- package/skills/tacitt/scripts/local-cli.mjs +116 -0
- package/skills/tacitt/scripts/privacy-validator.mjs +10350 -0
- package/skills/tacitt/scripts/review-stream-client.d.mts +22 -0
- package/skills/tacitt/scripts/review-stream-client.mjs +194 -0
- package/skills/tacitt/scripts/review.d.mts +87 -0
- package/skills/tacitt/scripts/review.mjs +1589 -0
- package/skills/tacitt/scripts/verify-cli.mjs +10 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { mkdir, realpath } 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
|
+
} from './journey-state.mjs'
|
|
22
|
+
import { classificationTasks } from './journey-classification-tasks.mjs'
|
|
23
|
+
import { classificationEvidence, classificationFormat } from './journey-classification.mjs'
|
|
24
|
+
import { consolidateClassification, classificationSubmission } from './journey-consolidation.mjs'
|
|
25
|
+
import { consolidateCards, cardSubmission } from './journey-card-consolidation.mjs'
|
|
26
|
+
import {
|
|
27
|
+
classificationProgress,
|
|
28
|
+
publishClassificationProgress,
|
|
29
|
+
publishFailedClassificationProgress,
|
|
30
|
+
} from './journey-classification-progress.mjs'
|
|
31
|
+
import { answers, createTasks } from './journey-tasks.mjs'
|
|
32
|
+
import { scanTiming } from './journey-scan-timing.mjs'
|
|
33
|
+
export { phaseOf, stageFingerprint } from './journey-state.mjs'
|
|
34
|
+
|
|
35
|
+
async function publishCompleteClassification(manifest, view, call, proposalRevision) {
|
|
36
|
+
const baselineRevision = manifest.classification?.baselineRevision
|
|
37
|
+
if (
|
|
38
|
+
!Number.isSafeInteger(baselineRevision) ||
|
|
39
|
+
view.classificationRevision !== view.revision ||
|
|
40
|
+
view.classificationRevision <= baselineRevision
|
|
41
|
+
)
|
|
42
|
+
throw new Error('CLASSIFICATION_PROGRESS_COMPLETION_UNCONFIRMED')
|
|
43
|
+
if (proposalRevision !== view.revision)
|
|
44
|
+
throw new Error('CLASSIFICATION_PROGRESS_COMPLETION_UNCONFIRMED')
|
|
45
|
+
await publishClassificationProgress(manifest, view, call, {
|
|
46
|
+
status: 'complete',
|
|
47
|
+
stage: 'proposal',
|
|
48
|
+
proposalRevision,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Longer than the default request bound, still inside a 2-minute tool timeout.
|
|
53
|
+
const FREEZE_REQUEST_MS = 100_000
|
|
54
|
+
|
|
55
|
+
async function prepareInitialFreeze(run, journeyId, call, view) {
|
|
56
|
+
const path = join(run, 'initial-freeze-timing.json')
|
|
57
|
+
const timing = await optional(path)
|
|
58
|
+
if (!view.consent || view.phase === 'paused' || view.scope?.status === 'confirmed')
|
|
59
|
+
throw new Error('INITIAL_FREEZE_NOT_ALLOWED')
|
|
60
|
+
if (timing?.completedAt && timing.snapshotId !== view.snapshotId)
|
|
61
|
+
throw new Error('INITIAL_FREEZE_SNAPSHOT_CHANGED')
|
|
62
|
+
if (timing?.completedAt && timing.snapshotId === view.snapshotId)
|
|
63
|
+
return {
|
|
64
|
+
stage: 'freeze',
|
|
65
|
+
status: 'complete',
|
|
66
|
+
...timing,
|
|
67
|
+
elapsedMs: Date.parse(timing.completedAt) - Date.parse(timing.startedAt),
|
|
68
|
+
}
|
|
69
|
+
if (view.snapshotId && (!timing?.startedAt || view.classificationRevision !== null))
|
|
70
|
+
throw new Error('INITIAL_FREEZE_NOT_STARTED_HERE')
|
|
71
|
+
const startedAt = timing?.startedAt ?? new Date().toISOString()
|
|
72
|
+
if (!timing) await save(path, { startedAt, completedAt: null })
|
|
73
|
+
// Safe on retry while scope remains pending; journey.prepare resumes the frozen capture.
|
|
74
|
+
try {
|
|
75
|
+
await call('journey.prepare', { journeyId }, { timeoutMs: FREEZE_REQUEST_MS })
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// A large first freeze can outlast one helper command; captured roots persist for the rerun.
|
|
78
|
+
if (error?.code === 'CLI_UNAVAILABLE') return { stage: 'freeze', status: 'running', startedAt }
|
|
79
|
+
throw error
|
|
80
|
+
}
|
|
81
|
+
const completed = await call('journey.get', { journeyId })
|
|
82
|
+
if (
|
|
83
|
+
!completed.snapshotId ||
|
|
84
|
+
completed.scope?.status === 'confirmed' ||
|
|
85
|
+
completed.classificationRevision !== null
|
|
86
|
+
)
|
|
87
|
+
throw new Error('INITIAL_FREEZE_NOT_CONFIRMED')
|
|
88
|
+
const completedAt = new Date().toISOString()
|
|
89
|
+
const saved = { startedAt, completedAt, snapshotId: completed.snapshotId }
|
|
90
|
+
await save(path, saved)
|
|
91
|
+
return {
|
|
92
|
+
stage: 'freeze',
|
|
93
|
+
status: 'complete',
|
|
94
|
+
...saved,
|
|
95
|
+
elapsedMs: Date.parse(completedAt) - Date.parse(startedAt),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Scan timing is an internal diagnostic (BL-180): a receipt that no longer matches the journey
|
|
101
|
+
* marks timing stale or unavailable and never hides the rest of the helper's report.
|
|
102
|
+
*/
|
|
103
|
+
async function observeScanTiming(run, journeyId, view, start, call) {
|
|
104
|
+
try {
|
|
105
|
+
return await scanTiming(run, journeyId, view, start, call)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const code = error instanceof Error ? error.message : ''
|
|
108
|
+
if (!code.startsWith('SCAN_TIMING_')) throw error
|
|
109
|
+
process.stderr.write(code + '\n')
|
|
110
|
+
return { status: code === 'SCAN_TIMING_SCOPE_CHANGED' ? 'stale' : 'unavailable', code }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Locked by withJourneyCli in production; injected transport permits deterministic crash tests. */
|
|
115
|
+
/** A running journey render (panel or CLI) with packaged/selected Work counts (BL-185). */
|
|
116
|
+
function packagingProgress(view) {
|
|
117
|
+
if (!view.packaging) return {}
|
|
118
|
+
const selected = view.works.filter((work) => work.access !== 'drop' && work.rootSessionIds.length)
|
|
119
|
+
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
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function runPhaseCore({ run, journeyId, command, stage, refresh = false, limit = 3 }, call) {
|
|
131
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 16) throw new Error('INVALID_ARGUMENTS')
|
|
132
|
+
let view = await call('journey.get', { journeyId })
|
|
133
|
+
if (command === 'scan-start')
|
|
134
|
+
return {
|
|
135
|
+
scan: await observeScanTiming(run, journeyId, view, true, call),
|
|
136
|
+
next: 'journey.prepare',
|
|
137
|
+
}
|
|
138
|
+
if (command === 'status') {
|
|
139
|
+
const active = await optional(join(run, 'classification-active.json'))
|
|
140
|
+
const manifest = active ? await optional(join(active.directory, 'manifest.json')) : null
|
|
141
|
+
const freeze = await optional(join(run, 'initial-freeze-timing.json'))
|
|
142
|
+
const proposalAt =
|
|
143
|
+
view.classificationProgress?.status === 'complete'
|
|
144
|
+
? view.classificationProgress.updatedAt
|
|
145
|
+
: null
|
|
146
|
+
const coldElapsedMs =
|
|
147
|
+
freeze?.completedAt && freeze.snapshotId === view.snapshotId && proposalAt
|
|
148
|
+
? Math.max(0, Date.parse(proposalAt) - Date.parse(freeze.startedAt))
|
|
149
|
+
: null
|
|
150
|
+
return {
|
|
151
|
+
...(manifest?.classification
|
|
152
|
+
? { classification: await classificationProgress(manifest) }
|
|
153
|
+
: {}),
|
|
154
|
+
phase: phaseOf(view),
|
|
155
|
+
revision: view.revision,
|
|
156
|
+
readingComplete: view.readingComplete,
|
|
157
|
+
pending: activeTerms(view).filter((term) => term.disposition === 'pending').length,
|
|
158
|
+
answersRevision: view.privacy.answersRevision,
|
|
159
|
+
...packagingProgress(view),
|
|
160
|
+
coldElapsedMs,
|
|
161
|
+
scan: await observeScanTiming(run, journeyId, view, false, call),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (command === 'prepare') return prepareInitialFreeze(run, journeyId, call, view)
|
|
165
|
+
if (
|
|
166
|
+
!['classification', 'cards', 'resolution'].includes(stage) ||
|
|
167
|
+
!['tasks', 'evidence', 'consolidate', 'ingest'].includes(command)
|
|
168
|
+
)
|
|
169
|
+
throw new Error('INVALID_ARGUMENTS')
|
|
170
|
+
const activePath = join(run, `${stage}-active.json`)
|
|
171
|
+
const active = await optional(activePath)
|
|
172
|
+
let manifest = active ? await optional(join(active.directory, 'manifest.json')) : null
|
|
173
|
+
if (active && !manifest) throw new Error('CORRUPTED_STATE')
|
|
174
|
+
if (manifest && manifest.format !== classificationFormat)
|
|
175
|
+
throw new Error('INCOMPATIBLE_JOURNEY_TASKS')
|
|
176
|
+
if (manifest) {
|
|
177
|
+
const recovery = await recoverWrites(active.directory, manifest, call)
|
|
178
|
+
// A failed transport leaves the exact pending request intact and never reaches refresh.
|
|
179
|
+
if (recovery.recovered) view = await call('journey.get', { journeyId })
|
|
180
|
+
if (recovery.stale && (!refresh || command !== 'tasks')) throw new Error('REVISION_CONFLICT')
|
|
181
|
+
if (recovery.rejected && !(refresh && command === 'tasks')) {
|
|
182
|
+
const error = new Error(recovery.rejected)
|
|
183
|
+
error.code = recovery.rejected
|
|
184
|
+
throw error
|
|
185
|
+
}
|
|
186
|
+
if (recovery.completed === recovery.total && !recovery.stale && command === 'ingest') {
|
|
187
|
+
if (stage === 'classification' && manifest.consolidation)
|
|
188
|
+
await publishCompleteClassification(
|
|
189
|
+
manifest,
|
|
190
|
+
view,
|
|
191
|
+
call,
|
|
192
|
+
(await optional(join(active.directory, 'receipt.json')))?.revision,
|
|
193
|
+
)
|
|
194
|
+
return { stage, ...recovery }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const consolidationRefresh = refresh && command === 'consolidate' && stage === 'classification'
|
|
198
|
+
if (
|
|
199
|
+
consolidationRefresh &&
|
|
200
|
+
!['classification', 'classification-confirmation'].includes(phaseOf(view))
|
|
201
|
+
)
|
|
202
|
+
throw new Error('STAGE_NOT_READY')
|
|
203
|
+
if (consolidationRefresh && (await optional(join(active?.directory ?? run, 'receipt.json'))))
|
|
204
|
+
throw new Error('CLASSIFICATION_ALREADY_SUBMITTED')
|
|
205
|
+
const fingerprint = stageFingerprint(view, stage)
|
|
206
|
+
if (manifest?.resolutionBatches && !refresh)
|
|
207
|
+
await assertResolutionFresh(active.directory, manifest, view)
|
|
208
|
+
else if (manifest && manifest.fingerprint !== fingerprint && (!refresh || consolidationRefresh))
|
|
209
|
+
throw new Error('STALE_TASKS')
|
|
210
|
+
if (refresh && command !== 'tasks' && !consolidationRefresh) throw new Error('INVALID_ARGUMENTS')
|
|
211
|
+
if (command === 'tasks') {
|
|
212
|
+
const phase = phaseOf(view)
|
|
213
|
+
const revisingCards =
|
|
214
|
+
stage === 'cards' && refresh && ['browser-answers', 'answers-confirmation'].includes(phase)
|
|
215
|
+
if (
|
|
216
|
+
phase !== stage &&
|
|
217
|
+
!revisingCards &&
|
|
218
|
+
!(stage === 'classification' && phase === 'classification-confirmation')
|
|
219
|
+
)
|
|
220
|
+
throw new Error('STAGE_NOT_READY')
|
|
221
|
+
if (manifest && !refresh && stage === 'classification') {
|
|
222
|
+
if (!manifest.classification && manifest.format === classificationFormat)
|
|
223
|
+
throw new Error('INCOMPATIBLE_CLASSIFICATION_TASKS')
|
|
224
|
+
const result = await classificationTasks(active.directory, manifest, view, call, limit)
|
|
225
|
+
await publishClassificationProgress(manifest, view, call)
|
|
226
|
+
return result
|
|
227
|
+
}
|
|
228
|
+
if (manifest && !refresh)
|
|
229
|
+
return {
|
|
230
|
+
stage,
|
|
231
|
+
tasks: manifest.tasks,
|
|
232
|
+
consolidation: manifest.consolidation ?? null,
|
|
233
|
+
reused: true,
|
|
234
|
+
}
|
|
235
|
+
const directory = join(run, stage, `${fingerprint}-${randomUUID()}`)
|
|
236
|
+
await mkdir(directory, { recursive: true, mode: 0o700 })
|
|
237
|
+
manifest = {
|
|
238
|
+
format: classificationFormat,
|
|
239
|
+
stage,
|
|
240
|
+
fingerprint,
|
|
241
|
+
tasks: stage === 'classification' ? [] : await createTasks(directory, stage, view, call),
|
|
242
|
+
answersRevision: view.privacy.answersRevision,
|
|
243
|
+
}
|
|
244
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
245
|
+
await save(activePath, { directory })
|
|
246
|
+
if (stage === 'classification') {
|
|
247
|
+
const result = await classificationTasks(directory, manifest, view, call, limit)
|
|
248
|
+
await publishClassificationProgress(manifest, view, call)
|
|
249
|
+
return result
|
|
250
|
+
}
|
|
251
|
+
return { stage, tasks: manifest.tasks }
|
|
252
|
+
}
|
|
253
|
+
if (!manifest || !active) throw new Error('TASKS_REQUIRED')
|
|
254
|
+
const directory = active.directory
|
|
255
|
+
if (command === 'evidence') {
|
|
256
|
+
if (stage !== 'classification') throw new Error('INVALID_ARGUMENTS')
|
|
257
|
+
const result = await classificationEvidence(directory, manifest, view, call, limit)
|
|
258
|
+
await publishClassificationProgress(manifest, view, call)
|
|
259
|
+
return result
|
|
260
|
+
}
|
|
261
|
+
if (command === 'consolidate') {
|
|
262
|
+
if (stage === 'cards') return consolidateCards(directory, manifest)
|
|
263
|
+
if (stage !== 'classification') throw new Error('INVALID_ARGUMENTS')
|
|
264
|
+
if (!manifest.classification?.complete) throw new Error('CLASSIFICATION_READS_INCOMPLETE')
|
|
265
|
+
let pendingRootAnswers = false
|
|
266
|
+
for (const item of manifest.tasks)
|
|
267
|
+
if (!item.projectKeys && !(await optional(item.answer))) pendingRootAnswers = true
|
|
268
|
+
if (pendingRootAnswers) throw new Error('CLASSIFICATION_ANSWERS_INCOMPLETE')
|
|
269
|
+
const evidence = await classificationEvidence(directory, manifest, view, call, limit)
|
|
270
|
+
await publishClassificationProgress(manifest, view, call)
|
|
271
|
+
if (!evidence.evidenceComplete || evidence.tasks.length)
|
|
272
|
+
return { ...evidence, consolidationReady: false }
|
|
273
|
+
const result = await consolidateClassification(directory, manifest, {
|
|
274
|
+
refresh: consolidationRefresh,
|
|
275
|
+
})
|
|
276
|
+
await publishClassificationProgress(manifest, view, call, {
|
|
277
|
+
stage: 'proposal',
|
|
278
|
+
})
|
|
279
|
+
return result
|
|
280
|
+
}
|
|
281
|
+
if (stage === 'resolution') {
|
|
282
|
+
if (!manifest.resolutionBatches) {
|
|
283
|
+
manifest.resolutionBatches = planResolutions(
|
|
284
|
+
view,
|
|
285
|
+
await answers(manifest.tasks, 'resolutions'),
|
|
286
|
+
)
|
|
287
|
+
manifest.resolutionAuthority = resolutionAuthority(view)
|
|
288
|
+
manifest.resolutionEvidence = resolutionEvidence(view)
|
|
289
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
290
|
+
}
|
|
291
|
+
let recorded = 0
|
|
292
|
+
for (const batch of manifest.resolutionBatches) {
|
|
293
|
+
const requestPath = join(directory, `${batch.id}.request.json`),
|
|
294
|
+
receiptPath = join(directory, `${batch.id}.receipt.json`)
|
|
295
|
+
if (await optional(receiptPath)) continue
|
|
296
|
+
const latest = await call('journey.get', { journeyId })
|
|
297
|
+
await assertResolutionFresh(directory, manifest, latest)
|
|
298
|
+
const request = {
|
|
299
|
+
journeyId,
|
|
300
|
+
expectedRevision: latest.revision,
|
|
301
|
+
idempotencyKey: randomUUID(),
|
|
302
|
+
answersRevision: manifest.answersRevision,
|
|
303
|
+
resolutions: batch.resolutions,
|
|
304
|
+
}
|
|
305
|
+
await save(requestPath, request)
|
|
306
|
+
const result = await call(procedureFor(stage), request)
|
|
307
|
+
await save(receiptPath, {
|
|
308
|
+
status: result.status,
|
|
309
|
+
revision: result.state?.revision,
|
|
310
|
+
})
|
|
311
|
+
if (result.status === 'stale') throw new Error('REVISION_CONFLICT')
|
|
312
|
+
recorded++
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
stage,
|
|
316
|
+
recorded,
|
|
317
|
+
completed: manifest.resolutionBatches.length,
|
|
318
|
+
total: manifest.resolutionBatches.length,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (stage === 'classification' && !manifest.consolidation)
|
|
322
|
+
throw new Error('CONSOLIDATION_REQUIRED')
|
|
323
|
+
if (stage === 'classification') {
|
|
324
|
+
const readiness = await consolidateClassification(directory, manifest)
|
|
325
|
+
if (!readiness.readyForIngest) return readiness
|
|
326
|
+
}
|
|
327
|
+
if (stage === 'cards') {
|
|
328
|
+
const readiness = await consolidateCards(directory, manifest)
|
|
329
|
+
if (!readiness.readyForIngest) return readiness
|
|
330
|
+
}
|
|
331
|
+
const key = stage === 'classification' ? 'items' : stage === 'cards' ? 'cards' : 'resolutions'
|
|
332
|
+
const tasks = stage === 'classification' ? [manifest.consolidation] : manifest.tasks
|
|
333
|
+
const request = {
|
|
334
|
+
journeyId,
|
|
335
|
+
expectedRevision: view.revision,
|
|
336
|
+
idempotencyKey: randomUUID(),
|
|
337
|
+
[key]:
|
|
338
|
+
stage === 'classification'
|
|
339
|
+
? await classificationSubmission(manifest, view)
|
|
340
|
+
: stage === 'cards'
|
|
341
|
+
? await cardSubmission(manifest)
|
|
342
|
+
: await answers(tasks, key),
|
|
343
|
+
...(stage === 'resolution' ? { answersRevision: manifest.answersRevision } : {}),
|
|
344
|
+
}
|
|
345
|
+
await save(join(directory, 'request.json'), request)
|
|
346
|
+
let result
|
|
347
|
+
try {
|
|
348
|
+
result = await call(procedureFor(stage), request)
|
|
349
|
+
await save(join(directory, 'receipt.json'), {
|
|
350
|
+
status: result.status,
|
|
351
|
+
revision: result.state?.revision,
|
|
352
|
+
})
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (definiteWriteRejection(error))
|
|
355
|
+
await save(join(directory, 'receipt.json'), {
|
|
356
|
+
status: 'rejected',
|
|
357
|
+
code: error.code,
|
|
358
|
+
})
|
|
359
|
+
throw error
|
|
360
|
+
}
|
|
361
|
+
if (result.status === 'stale') throw new Error('REVISION_CONFLICT')
|
|
362
|
+
if (stage === 'classification') {
|
|
363
|
+
const latest = await call('journey.get', { journeyId })
|
|
364
|
+
await publishCompleteClassification(manifest, latest, call, result.state?.revision)
|
|
365
|
+
}
|
|
366
|
+
return { stage, status: result.status, revision: result.state?.revision }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const terminalClassificationErrors = new Set([
|
|
370
|
+
'INVALID_SELECTION',
|
|
371
|
+
'CLASSIFICATION_PROJECT_NAME_CONFLICT',
|
|
372
|
+
'CLASSIFICATION_ROOT_COVERAGE_INVALID',
|
|
373
|
+
'CLASSIFICATION_THEME_TOO_SMALL',
|
|
374
|
+
'CLASSIFICATION_UNSORTED_DUPLICATE',
|
|
375
|
+
'INVALID_CLASSIFICATION_GROUP_KIND',
|
|
376
|
+
'INVALID_CLASSIFICATION_ITEM',
|
|
377
|
+
])
|
|
378
|
+
|
|
379
|
+
/** Preserve the original error; publish failure only against a still-current running checkpoint. */
|
|
380
|
+
export async function runPhase(args, call) {
|
|
381
|
+
try {
|
|
382
|
+
return await runPhaseCore(args, call)
|
|
383
|
+
} catch (error) {
|
|
384
|
+
const code = error?.code ?? error?.message
|
|
385
|
+
if (args.stage === 'classification' && terminalClassificationErrors.has(code)) {
|
|
386
|
+
try {
|
|
387
|
+
const active = await optional(join(args.run, 'classification-active.json'))
|
|
388
|
+
const manifest = active ? await optional(join(active.directory, 'manifest.json')) : null
|
|
389
|
+
if (manifest?.format === classificationFormat && manifest.classification) {
|
|
390
|
+
const latest = await call('journey.get', {
|
|
391
|
+
journeyId: args.journeyId,
|
|
392
|
+
})
|
|
393
|
+
await publishFailedClassificationProgress(manifest, latest, call)
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
// Progress is advisory; never mask the classification error that caused this path.
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
throw error
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export async function main(argv) {
|
|
404
|
+
const { values, positionals } = parseArgs({
|
|
405
|
+
args: argv,
|
|
406
|
+
allowPositionals: true,
|
|
407
|
+
options: {
|
|
408
|
+
run: { type: 'string' },
|
|
409
|
+
journey: { type: 'string' },
|
|
410
|
+
stage: { type: 'string' },
|
|
411
|
+
refresh: { type: 'boolean', default: false },
|
|
412
|
+
limit: { type: 'string' },
|
|
413
|
+
model: { type: 'string' },
|
|
414
|
+
effort: { type: 'string' },
|
|
415
|
+
},
|
|
416
|
+
})
|
|
417
|
+
const [command] = positionals
|
|
418
|
+
if (!values.run) throw new Error('INVALID_ARGUMENTS')
|
|
419
|
+
if (command === 'init') {
|
|
420
|
+
const cli = positionals.slice(1)
|
|
421
|
+
const scopes = cli.flatMap((value, index) => (value === '--state-dir' ? [cli[index + 1]] : []))
|
|
422
|
+
if (scopes.length !== 1 || typeof scopes[0] !== 'string' || !isAbsolute(scopes[0]))
|
|
423
|
+
throw new Error('EXPLICIT_CLI_STATE_DIRECTORY_REQUIRED')
|
|
424
|
+
return review(argv)
|
|
425
|
+
}
|
|
426
|
+
const run = resolve(values.run),
|
|
427
|
+
config = await read(join(run, 'config.json'))
|
|
428
|
+
return withJourneyCli(run, (call) =>
|
|
429
|
+
runPhase(
|
|
430
|
+
{
|
|
431
|
+
run,
|
|
432
|
+
journeyId: config.journeyId,
|
|
433
|
+
command,
|
|
434
|
+
stage: values.stage,
|
|
435
|
+
refresh: values.refresh,
|
|
436
|
+
limit: values.limit === undefined ? 3 : Number(values.limit),
|
|
437
|
+
},
|
|
438
|
+
call,
|
|
439
|
+
),
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
const entry = process.argv[1]
|
|
443
|
+
if (entry && (await realpath(entry)) === fileURLToPath(import.meta.url))
|
|
444
|
+
main(process.argv.slice(2)).then(
|
|
445
|
+
(result) => process.stdout.write(JSON.stringify({ ok: true, ...result }) + '\n'),
|
|
446
|
+
(error) => {
|
|
447
|
+
const code = /^[A-Z_]{3,80}$/u.test(error.message) ? error.message : 'JOURNEY_HELPER_FAILED'
|
|
448
|
+
process.stderr.write(code + '\n')
|
|
449
|
+
process.exitCode = 1
|
|
450
|
+
},
|
|
451
|
+
)
|
|
@@ -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>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { lstat, open, readdir, realpath } from 'node:fs/promises'
|
|
3
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const fail = () => {
|
|
6
|
+
throw new Error('LOCAL_CLI_BINDING_INVALID')
|
|
7
|
+
}
|
|
8
|
+
const object = (value) => typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
9
|
+
const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
|
|
10
|
+
const digestPattern = /^[a-f0-9]{64}$/
|
|
11
|
+
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex')
|
|
12
|
+
|
|
13
|
+
async function regularFile(path, limit) {
|
|
14
|
+
const before = await lstat(path)
|
|
15
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > limit) fail()
|
|
16
|
+
const handle = await open(path, 'r')
|
|
17
|
+
try {
|
|
18
|
+
const opened = await handle.stat()
|
|
19
|
+
if (opened.dev !== before.dev || opened.ino !== before.ino) fail()
|
|
20
|
+
const bytes = await handle.readFile()
|
|
21
|
+
const after = await lstat(path)
|
|
22
|
+
if (
|
|
23
|
+
!after.isFile() ||
|
|
24
|
+
after.isSymbolicLink() ||
|
|
25
|
+
after.dev !== before.dev ||
|
|
26
|
+
after.ino !== before.ino ||
|
|
27
|
+
after.size !== before.size ||
|
|
28
|
+
after.mtimeMs !== before.mtimeMs ||
|
|
29
|
+
bytes.length !== before.size
|
|
30
|
+
)
|
|
31
|
+
fail()
|
|
32
|
+
return bytes
|
|
33
|
+
} finally {
|
|
34
|
+
await handle.close()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function packageFiles(root) {
|
|
39
|
+
const files = []
|
|
40
|
+
let bytes = 0
|
|
41
|
+
const visit = async (name, depth = 0) => {
|
|
42
|
+
if (depth > 16 || files.length >= 10000) fail()
|
|
43
|
+
const path = join(root, name)
|
|
44
|
+
const info = await lstat(path)
|
|
45
|
+
if (info.isSymbolicLink()) fail()
|
|
46
|
+
if (info.isDirectory()) {
|
|
47
|
+
for (const child of (await readdir(path)).sort()) await visit(`${name}/${child}`, depth + 1)
|
|
48
|
+
} else if (info.isFile()) {
|
|
49
|
+
const content = await regularFile(path, 512 * 1024 * 1024 - bytes)
|
|
50
|
+
bytes += content.length
|
|
51
|
+
files.push([name, content.length, digest(content)])
|
|
52
|
+
} else fail()
|
|
53
|
+
}
|
|
54
|
+
for (const name of ['package.json', 'bin', 'dist', 'skills']) await visit(name)
|
|
55
|
+
return files
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Reads only the selected installed package; it never downloads or opens application state. */
|
|
59
|
+
export async function readLocalCliBinding(cliPath, expected) {
|
|
60
|
+
if (
|
|
61
|
+
typeof cliPath !== 'string' ||
|
|
62
|
+
!isAbsolute(cliPath) ||
|
|
63
|
+
cliPath
|
|
64
|
+
.split('')
|
|
65
|
+
.some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) ||
|
|
66
|
+
!object(expected) ||
|
|
67
|
+
expected.name !== '@gitroll/tacitt-cli' ||
|
|
68
|
+
typeof expected.version !== 'string' ||
|
|
69
|
+
!versionPattern.test(expected.version)
|
|
70
|
+
)
|
|
71
|
+
fail()
|
|
72
|
+
const requested = await lstat(cliPath)
|
|
73
|
+
if (!requested.isFile() || requested.isSymbolicLink()) fail()
|
|
74
|
+
const executable = await realpath(cliPath)
|
|
75
|
+
const root = dirname(dirname(executable))
|
|
76
|
+
if (executable !== join(root, 'bin', 'tacitt.cjs')) fail()
|
|
77
|
+
const metadataBytes = await regularFile(join(root, 'package.json'), 1024 * 1024)
|
|
78
|
+
const metadata = JSON.parse(metadataBytes.toString('utf8'))
|
|
79
|
+
if (
|
|
80
|
+
!object(metadata) ||
|
|
81
|
+
metadata.name !== expected.name ||
|
|
82
|
+
metadata.version !== expected.version ||
|
|
83
|
+
!object(metadata.bin) ||
|
|
84
|
+
metadata.bin['tacitt'] !== 'bin/tacitt.cjs'
|
|
85
|
+
)
|
|
86
|
+
fail()
|
|
87
|
+
const files = await packageFiles(root)
|
|
88
|
+
if (
|
|
89
|
+
!files.some(([name]) => name === 'dist/cli.mjs') ||
|
|
90
|
+
!files.some(([name, , sha256]) => name === 'package.json' && sha256 === digest(metadataBytes))
|
|
91
|
+
)
|
|
92
|
+
fail()
|
|
93
|
+
return {
|
|
94
|
+
schemaVersion: 1,
|
|
95
|
+
package: expected.name,
|
|
96
|
+
version: expected.version,
|
|
97
|
+
nodePath: await realpath(process.execPath),
|
|
98
|
+
cliPath: executable,
|
|
99
|
+
artifactSha256: digest(JSON.stringify(files)),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A receipt hash comes from the managed entry, independently of the local receipt bytes. */
|
|
104
|
+
export async function verifyLocalCliBinding(receiptPath, expectedReceiptSha256) {
|
|
105
|
+
if (!isAbsolute(receiptPath) || !digestPattern.test(expectedReceiptSha256)) fail()
|
|
106
|
+
const bytes = await regularFile(receiptPath, 8192)
|
|
107
|
+
if (digest(bytes) !== expectedReceiptSha256) fail()
|
|
108
|
+
const receipt = JSON.parse(bytes.toString('utf8'))
|
|
109
|
+
if (!object(receipt) || receipt.schemaVersion !== 1) fail()
|
|
110
|
+
const actual = await readLocalCliBinding(receipt.cliPath, {
|
|
111
|
+
name: receipt.package,
|
|
112
|
+
version: receipt.version,
|
|
113
|
+
})
|
|
114
|
+
if (JSON.stringify(actual) !== JSON.stringify(receipt)) fail()
|
|
115
|
+
return actual
|
|
116
|
+
}
|