@gitroll/tacitt-cli 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-setup-B85GaVIz.mjs → agent-setup-CLXdKr2I.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +165 -9
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +25 -4
- package/dist/{frame-D1j9R8xX.mjs → frame-waS5FnN5.mjs} +1 -1
- package/dist/{heap-tfU61Ehy.mjs → heap-eO_uWpD1.mjs} +63 -9
- package/dist/inspection-worker.mjs +57 -0
- package/dist/job-rK-Ho9OD.mjs +613 -0
- package/dist/{locate-Bn32k2se.mjs → locate-U3-nkird.mjs} +29 -6
- package/dist/messages-D3m3bhva.mjs +40 -0
- package/dist/{model-7Nu7szNP.mjs → model-BBAnsFAx.mjs} +1 -1
- package/dist/operation-executor.mjs +3 -3
- package/dist/preview-worker.mjs +1 -1
- package/dist/privacy-validator.mjs +1 -1
- package/dist/{query-Da-KDbSV.mjs → query-DN99jiRc.mjs} +710 -630
- package/dist/{run-CCATsHS2.mjs → run-4G_CvdDj.mjs} +4 -4
- package/dist/session-process.mjs +6 -1
- package/dist/{src-CPPjOyHZ.mjs → src-B4R5XMEP.mjs} +621 -224
- package/dist/{src-BngGpFKd.mjs → src-CPbdnGrk.mjs} +130 -53
- package/dist/{src-DynBRDBc.mjs → src-qoLRvX-A.mjs} +34 -11
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/index-BkuUI3Eu.js +67 -0
- package/dist/web/assets/index-Clg6Pfv_.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skills/tacitt/SKILL.md +15 -11
- package/skills/tacitt/references/b-method.md +1 -1
- package/skills/tacitt/references/classification.md +3 -3
- package/skills/tacitt/references/commands.md +19 -14
- package/skills/tacitt/references/journey.md +17 -11
- package/skills/tacitt/references/parallel-review.md +3 -3
- package/skills/tacitt/references/send.md +5 -5
- package/skills/tacitt/scripts/INDEX.md +6 -2
- package/skills/tacitt/scripts/answer.mjs +29 -8
- package/skills/tacitt/scripts/journey-card-consolidation.d.mts +13 -4
- package/skills/tacitt/scripts/journey-card-consolidation.mjs +170 -21
- package/skills/tacitt/scripts/journey-card-values.d.mts +17 -0
- package/skills/tacitt/scripts/journey-card-values.mjs +116 -0
- package/skills/tacitt/scripts/journey-classification-progress.mjs +2 -1
- package/skills/tacitt/scripts/journey-classification-validation.mjs +49 -28
- package/skills/tacitt/scripts/journey-classification.mjs +6 -2
- package/skills/tacitt/scripts/journey-consolidation-submit.mjs +42 -5
- package/skills/tacitt/scripts/journey-consolidation.d.mts +5 -0
- package/skills/tacitt/scripts/journey-consolidation.mjs +4 -1
- package/skills/tacitt/scripts/journey-panel-host.d.mts +9 -0
- package/skills/tacitt/scripts/journey-panel-host.mjs +56 -0
- package/skills/tacitt/scripts/journey-state.d.mts +1 -0
- package/skills/tacitt/scripts/journey-state.mjs +38 -6
- package/skills/tacitt/scripts/journey-tasks.mjs +5 -3
- package/skills/tacitt/scripts/journey.d.mts +4 -0
- package/skills/tacitt/scripts/journey.mjs +249 -41
- package/skills/tacitt/scripts/privacy-validator.mjs +25 -6
- package/skills/tacitt/scripts/review-stream-client.mjs +1 -1
- package/skills/tacitt/scripts/review.mjs +24 -3
- package/dist/job--TceKk0l.mjs +0 -194
- package/dist/web/assets/index-CWQ05PFl.js +0 -67
- package/dist/web/assets/index-J-AWt_cW.css +0 -1
|
@@ -1,40 +1,61 @@
|
|
|
1
1
|
const types = ['work', 'side', 'coursework', 'personal', 'scratch', 'other']
|
|
2
2
|
const access = ['public', 'private', 'drop']
|
|
3
|
+
const unknownField = (value, allowed) => {
|
|
4
|
+
const field = Object.keys(value).find((name) => !allowed(name))
|
|
5
|
+
return field && `unknown field ${field}`
|
|
6
|
+
}
|
|
3
7
|
const text = (value, max) =>
|
|
4
8
|
typeof value === 'string' && value.trim().length > 0 && value.length <= max
|
|
5
9
|
|
|
6
10
|
export function validateClassificationJudgment(value, key) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
value.rootKey !== key
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
const problem = !value
|
|
12
|
+
? 'missing'
|
|
13
|
+
: value.rootKey !== key
|
|
14
|
+
? 'rootKey does not match'
|
|
15
|
+
: !text(value.projectName, 240)
|
|
16
|
+
? 'projectName must be 1-240 characters'
|
|
17
|
+
: !text(value.workIntent, 240)
|
|
18
|
+
? 'workIntent must be 1-240 characters'
|
|
19
|
+
: !text(value.groupingBasis, 200)
|
|
20
|
+
? 'groupingBasis must be 1-200 characters'
|
|
21
|
+
: value.targetProjectId !== undefined &&
|
|
22
|
+
(typeof value.targetProjectId !== 'string' || !value.targetProjectId)
|
|
23
|
+
? 'targetProjectId must be a non-empty string'
|
|
24
|
+
: unknownField(value, (field) =>
|
|
25
|
+
[
|
|
26
|
+
'rootKey',
|
|
27
|
+
'projectName',
|
|
28
|
+
'workIntent',
|
|
29
|
+
'groupingBasis',
|
|
30
|
+
'targetProjectId',
|
|
31
|
+
].includes(field),
|
|
32
|
+
)
|
|
33
|
+
if (problem)
|
|
34
|
+
throw new Error('CLASSIFICATION_JUDGMENT_INVALID', {
|
|
35
|
+
cause: problem,
|
|
36
|
+
})
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
export function validateClassificationProject(value, projectId) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
value.projectId !== projectId
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
const problem = !value
|
|
41
|
+
? 'missing'
|
|
42
|
+
: value.projectId !== projectId
|
|
43
|
+
? 'projectId does not match'
|
|
44
|
+
: !text(value.description, 1000)
|
|
45
|
+
? 'description must be 1-1000 characters'
|
|
46
|
+
: !types.includes(value.projectType)
|
|
47
|
+
? `projectType must be one of ${types.join(', ')}`
|
|
48
|
+
: !access.includes(value.recommendation?.access)
|
|
49
|
+
? `recommendation.access must be one of ${access.join(', ')}`
|
|
50
|
+
: !text(value.recommendation?.reason, 200)
|
|
51
|
+
? 'recommendation.reason must be 1-200 characters'
|
|
52
|
+
: unknownField(value, (field) =>
|
|
53
|
+
['projectId', 'description', 'projectType', 'recommendation'].includes(field),
|
|
54
|
+
)
|
|
55
|
+
if (problem)
|
|
56
|
+
throw new Error('PROJECT_METADATA_INVALID', {
|
|
57
|
+
cause: problem,
|
|
58
|
+
})
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
export function validateConsolidationAnswer(answer, descriptor) {
|
|
@@ -74,9 +74,13 @@ export async function classificationEvidence(directory, manifest, view, call, li
|
|
|
74
74
|
const root = state.roots.find((entry) => entry.key === key)
|
|
75
75
|
const source = state.unknown.find((entry) => entry.rootKey === key)
|
|
76
76
|
if (!root || !source) throw new Error('INVALID_EVIDENCE_ROOTS')
|
|
77
|
+
// After the known Works (E2), unresolved roots sit in the "Still being grouped" Work.
|
|
78
|
+
const workId =
|
|
79
|
+
view.works.find((work) => work.rootSessionIds?.includes(root.rootSessionId))?.id ??
|
|
80
|
+
root.workId
|
|
77
81
|
const page = await call('journey.classificationSessions', {
|
|
78
82
|
journeyId: view.journeyId,
|
|
79
|
-
workId
|
|
83
|
+
workId,
|
|
80
84
|
includeProjectEvidence: true,
|
|
81
85
|
projectEvidenceRootSessionIds: [root.rootSessionId],
|
|
82
86
|
rootsOnly: true,
|
|
@@ -97,7 +101,7 @@ export async function classificationEvidence(directory, manifest, view, call, li
|
|
|
97
101
|
for (let pageNumber = 0; pageNumber < 2; pageNumber++) {
|
|
98
102
|
const content = await call('journey.classificationRead', {
|
|
99
103
|
journeyId: view.journeyId,
|
|
100
|
-
workId
|
|
104
|
+
workId,
|
|
101
105
|
rootSessionId: root.rootSessionId,
|
|
102
106
|
...(cursor ? { cursor } : {}),
|
|
103
107
|
})
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import { answers } from './journey-tasks.mjs'
|
|
4
|
+
import { optional } from './journey-state.mjs'
|
|
4
5
|
import {
|
|
5
6
|
validateClassificationProject,
|
|
6
7
|
validateConsolidationAnswer,
|
|
7
8
|
compileAssignments,
|
|
8
9
|
} from './journey-classification-validation.mjs'
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
const classification = manifest.classification
|
|
13
|
-
if (!classification?.complete || !manifest.consolidation)
|
|
14
|
-
throw new Error('CONSOLIDATION_REQUIRED')
|
|
11
|
+
/** Known-project Works with their fixed catalog membership and answered metadata. */
|
|
12
|
+
async function knownProjects(classification, manifest) {
|
|
15
13
|
const projectMetadata = new Map()
|
|
16
14
|
for (const item of manifest.tasks.filter((entry) => entry.projectKeys)) {
|
|
17
15
|
const results = await answers([item], 'items')
|
|
@@ -49,6 +47,40 @@ export async function classificationSubmission(manifest) {
|
|
|
49
47
|
projectId: project.projectId,
|
|
50
48
|
}
|
|
51
49
|
})
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* E2: once every known project has its metadata, its Works are published while the unresolved
|
|
55
|
+
* roots are still being grouped; they wait as `pendingRootSessionIds`. Null while metadata is
|
|
56
|
+
* missing or when nothing remains to group.
|
|
57
|
+
*/
|
|
58
|
+
export async function partialClassificationSubmission(manifest) {
|
|
59
|
+
const classification = manifest.classification
|
|
60
|
+
if (
|
|
61
|
+
!classification?.complete ||
|
|
62
|
+
!classification.unknown.length ||
|
|
63
|
+
!classification.projects.length
|
|
64
|
+
)
|
|
65
|
+
return null
|
|
66
|
+
for (const item of manifest.tasks.filter((entry) => entry.projectKeys))
|
|
67
|
+
if (!(await optional(item.answer))) return null
|
|
68
|
+
const known = await knownProjects(classification, manifest)
|
|
69
|
+
const rootByKey = new Map(classification.roots.map((root) => [root.key, root.rootSessionId]))
|
|
70
|
+
const pendingRootSessionIds = classification.unknown.map((item) => {
|
|
71
|
+
const id = rootByKey.get(item.rootKey)
|
|
72
|
+
if (!id) throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
|
|
73
|
+
return id
|
|
74
|
+
})
|
|
75
|
+
return { items: normalized(known), pendingRootSessionIds }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Expand short handles and fixed catalog membership only after exact coverage validation. */
|
|
79
|
+
export async function classificationSubmission(manifest) {
|
|
80
|
+
const classification = manifest.classification
|
|
81
|
+
if (!classification?.complete || !manifest.consolidation)
|
|
82
|
+
throw new Error('CONSOLIDATION_REQUIRED')
|
|
83
|
+
const result = await knownProjects(classification, manifest)
|
|
52
84
|
let answerText
|
|
53
85
|
let unknown
|
|
54
86
|
if (manifest.consolidation.automatic) unknown = { groups: [], assignments: {} }
|
|
@@ -171,6 +203,11 @@ export async function classificationSubmission(manifest) {
|
|
|
171
203
|
JSON.stringify(expectedRootIds) !== JSON.stringify(submittedRootIds)
|
|
172
204
|
)
|
|
173
205
|
throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
|
|
206
|
+
return normalized(result)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Trimmed submission items; one project name never carries two different metadata sets. */
|
|
210
|
+
function normalized(result) {
|
|
174
211
|
const normalizedNames = new Map()
|
|
175
212
|
const output = result.map(({ projectId: _projectId, ...item }) => ({
|
|
176
213
|
...item,
|
|
@@ -7,3 +7,8 @@ export function consolidateClassification(
|
|
|
7
7
|
export function classificationSubmission(
|
|
8
8
|
manifest: Record<string, unknown>,
|
|
9
9
|
): Promise<Array<Record<string, unknown>>>
|
|
10
|
+
|
|
11
|
+
export function partialClassificationSubmission(manifest: Record<string, unknown>): Promise<{
|
|
12
|
+
items: Array<Record<string, unknown>>
|
|
13
|
+
pendingRootSessionIds: string[]
|
|
14
|
+
} | null>
|
|
@@ -221,4 +221,7 @@ export async function consolidateClassification(directory, manifest, { refresh =
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
export {
|
|
224
|
+
export {
|
|
225
|
+
classificationSubmission,
|
|
226
|
+
partialClassificationSubmission,
|
|
227
|
+
} from './journey-consolidation-submit.mjs'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ChildProcess, type SpawnOptions } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export function ensureDetachedHost(
|
|
4
|
+
cli: string[],
|
|
5
|
+
journeyId: string,
|
|
6
|
+
options?: {
|
|
7
|
+
spawnChild?: (command: string, args: string[], options: SpawnOptions) => ChildProcess
|
|
8
|
+
},
|
|
9
|
+
): Promise<{ url: string; pid: number; started: boolean }>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { windowsArgument } from './review-stream-client.mjs'
|
|
3
|
+
|
|
4
|
+
/** `journey.serve --detach` waits up to 60 s for the host; leave room for the CLI's startup. */
|
|
5
|
+
const detachTimeoutMs = 90_000
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ADR 0227: before the agent ends its turn at Ready to send, a detached panel host must own the
|
|
9
|
+
* panel, because some agent hosts (Codex) stop the processes of a turn when it ends. Runs the
|
|
10
|
+
* paired CLI's `journey.serve --detach`, which reuses a detached host already serving the journey
|
|
11
|
+
* and otherwise starts one. Resolves `{url, pid, started}`; `started` means the panel moved to
|
|
12
|
+
* a new URL.
|
|
13
|
+
*/
|
|
14
|
+
export function ensureDetachedHost(cli, journeyId, { spawnChild = spawn } = {}) {
|
|
15
|
+
const [command, ...prefix] = cli
|
|
16
|
+
const shell = process.platform === 'win32' && !/\.(exe|com)$/iu.test(command)
|
|
17
|
+
const args = [...prefix, 'journey.serve', '--detach', '--input', '-']
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const fail = () => reject(new Error('PANEL_HOST_DETACH_FAILED'))
|
|
20
|
+
const child = spawnChild(command, shell ? args.map(windowsArgument) : args, {
|
|
21
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
22
|
+
shell,
|
|
23
|
+
})
|
|
24
|
+
let output = ''
|
|
25
|
+
const timer = setTimeout(() => child.kill(), detachTimeoutMs)
|
|
26
|
+
child.stdout.setEncoding('utf8')
|
|
27
|
+
child.stdout.on('data', (chunk) => (output += chunk))
|
|
28
|
+
child.once('error', () => {
|
|
29
|
+
clearTimeout(timer)
|
|
30
|
+
fail()
|
|
31
|
+
})
|
|
32
|
+
child.once('close', (code) => {
|
|
33
|
+
clearTimeout(timer)
|
|
34
|
+
const ready = output
|
|
35
|
+
.split('\n')
|
|
36
|
+
.flatMap((line) => {
|
|
37
|
+
try {
|
|
38
|
+
return [JSON.parse(line)]
|
|
39
|
+
} catch {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.find((line) => line?.ok === true && line.event === 'ready')?.result
|
|
44
|
+
if (
|
|
45
|
+
code !== 0 ||
|
|
46
|
+
ready?.journeyId !== journeyId ||
|
|
47
|
+
typeof ready.url !== 'string' ||
|
|
48
|
+
!Number.isSafeInteger(ready.pid)
|
|
49
|
+
)
|
|
50
|
+
return fail()
|
|
51
|
+
resolve({ url: ready.url, pid: ready.pid, started: ready.reused !== true })
|
|
52
|
+
})
|
|
53
|
+
child.stdin.on('error', () => {})
|
|
54
|
+
child.stdin.end(JSON.stringify({ journeyId }))
|
|
55
|
+
})
|
|
56
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export function save(path: string, value: unknown): Promise<void>
|
|
2
|
+
export function cardBatchOpen(run: string): Promise<boolean>
|
|
2
3
|
export function hash(value: unknown): string
|
|
3
4
|
export function definiteWriteRejection(error: unknown): boolean
|
|
4
5
|
export function stageFingerprint(view: unknown, stage: string): string
|
|
@@ -36,7 +36,8 @@ export function phaseOf(view) {
|
|
|
36
36
|
if (view.scope.status !== 'confirmed') return 'classification-confirmation'
|
|
37
37
|
if (!view.readingComplete) return 'scan'
|
|
38
38
|
const pending = activeTerms(view).some((term) => term.disposition === 'pending')
|
|
39
|
-
|
|
39
|
+
// Card batches (D3) leave values no card covers yet for the next batch.
|
|
40
|
+
if (uncoveredPending(view).length) return 'cards'
|
|
40
41
|
if (view.privacy.cards.length) {
|
|
41
42
|
if (view.privacy.cards.some((card) => !card.answer)) return 'browser-answers'
|
|
42
43
|
if (view.privacy.confirmedAnswersRevision !== view.privacy.answersRevision)
|
|
@@ -46,20 +47,51 @@ export function phaseOf(view) {
|
|
|
46
47
|
const selected = view.works.filter((work) => work.access !== 'drop' && work.rootSessionIds.length)
|
|
47
48
|
// Track B builds each Work's private packaging base in the background (ADR 0171).
|
|
48
49
|
if (selected.some((work) => !work.staticReview)) return 'static-finishing'
|
|
49
|
-
|
|
50
|
-
if (
|
|
50
|
+
// The person's Send click packages and uploads (D5); the agent has nothing left to run.
|
|
51
|
+
if (
|
|
52
|
+
!view.privacy.packageApproval ||
|
|
53
|
+
selected.some((work) => work.artifact?.revision !== work.revision)
|
|
54
|
+
)
|
|
55
|
+
return 'ready-to-send'
|
|
51
56
|
return 'send'
|
|
52
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* A card batch (E3) whose append is not yet accepted. While one is open, newly uncovered values
|
|
60
|
+
* wait for the next batch, so cards come as soon as they are needed without overlapping.
|
|
61
|
+
*/
|
|
62
|
+
export async function cardBatchOpen(run) {
|
|
63
|
+
const active = await optional(join(run, 'cards-active.json'))
|
|
64
|
+
if (!active) return false
|
|
65
|
+
const manifest = await optional(join(active.directory, 'manifest.json'))
|
|
66
|
+
if (!manifest?.batch) return false
|
|
67
|
+
const receipt = await optional(join(active.directory, 'receipt.json'))
|
|
68
|
+
// A refused batch is retired and an emptied one finished: neither holds the next batch back.
|
|
69
|
+
return !['applied', 'replayed', 'skipped', 'rejected'].includes(receipt?.status)
|
|
70
|
+
}
|
|
71
|
+
/** Pending values that no published card covers yet: the input of the next card batch. */
|
|
72
|
+
export function uncoveredPending(view) {
|
|
73
|
+
const covered = new Set(view.privacy.cards.flatMap((card) => card.termKeys))
|
|
74
|
+
return activeTerms(view).filter(
|
|
75
|
+
(term) =>
|
|
76
|
+
term.disposition === 'pending' &&
|
|
77
|
+
!covered.has(term.key ?? JSON.stringify([term.type, term.value?.normalize('NFC')])),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Classification tasks depend on grouping, not on scan progress: the known Works' scan keeps
|
|
82
|
+
* moving their input revision and the person may change access, so neither retires them.
|
|
83
|
+
*/
|
|
53
84
|
export function stageFingerprint(view, stage) {
|
|
85
|
+
const classification = stage === 'classification'
|
|
54
86
|
return hash({
|
|
55
87
|
snapshotId: view.snapshotId,
|
|
56
88
|
guidance: view.guidanceDigest,
|
|
57
89
|
works: view.works.map((work) => [
|
|
58
90
|
work.id,
|
|
59
|
-
work.inputRevision,
|
|
91
|
+
classification ? null : work.inputRevision,
|
|
60
92
|
work.rootSessionIds,
|
|
61
|
-
work.access,
|
|
62
|
-
|
|
93
|
+
classification ? null : work.access,
|
|
94
|
+
classification
|
|
63
95
|
? [
|
|
64
96
|
work.title,
|
|
65
97
|
work.projectName,
|
|
@@ -79,13 +79,15 @@ export async function answers(tasks, key) {
|
|
|
79
79
|
}
|
|
80
80
|
return entries
|
|
81
81
|
}
|
|
82
|
-
export async function createTasks(directory, stage, view, call) {
|
|
82
|
+
export async function createTasks(directory, stage, view, call, { batch = false } = {}) {
|
|
83
83
|
const tasks = [],
|
|
84
84
|
journeyId = view.journeyId
|
|
85
85
|
const cards = await call('journey.cardsRead', { journeyId })
|
|
86
86
|
const guidance = stage === 'resolution' ? await call('review.guidance', { journeyId }) : null
|
|
87
|
+
// A card batch drafts only values no published card covers yet (D3).
|
|
88
|
+
const covered = new Set(batch ? cards.cards.flatMap((card) => card.termKeys) : [])
|
|
87
89
|
const pending = cards.terms
|
|
88
|
-
.filter((term) => term.disposition === 'pending')
|
|
90
|
+
.filter((term) => term.disposition === 'pending' && !covered.has(term.key))
|
|
89
91
|
.sort((a, b) => a.type.localeCompare(b.type))
|
|
90
92
|
// Card drafting groups by meaning: a short ref replaces the long key, one excerpt replaces
|
|
91
93
|
// full source evidence. The publisher maps refs back to exact keys.
|
|
@@ -120,7 +122,7 @@ export async function createTasks(directory, stage, view, call) {
|
|
|
120
122
|
for (const group of groups) {
|
|
121
123
|
const instruction =
|
|
122
124
|
stage === 'cards'
|
|
123
|
-
?
|
|
125
|
+
? "INTERNAL decision-card drafting for Main (not browser publication): return {cards:[{id,question,examples,termRefs,options:[{id,label,rule}]}]}. Use UUID card IDs. termRefs lists the supplied term refs (for example \"T12\"); cover every supplied ref exactly once. Write the question, option labels, rules and any context in English whatever language the conversation uses; keep example values exactly as they appear in the source. Ask a concise contextual retention question with exactly two options: id hide (hide the covered values) and id keep (retain the covered values). Keep their rules meaningful for final resolution. Group by broad privacy-information category and shared retention policy. Specific projects, tools, filenames and capabilities belong in examples, not separate question topics. Never put the person's own identity and other people on one card: an own-identity card covers only values tied to the person's own signals (own-identity terms, the session author, local account, own handle, email or domain); when unsure whose a value is, put it on the other-people card. Common review or chat jargon (for example LGTM, PTAL, nit, WIP) and generic words are not privacy values: never use them as examples or as a question's subject, and cover their refs on the card that fits the context they came from. Examples show short values, never sentences: list each value once, ignoring case, spacing, hyphens and underscores, and cut a long value with a trailing ellipsis. Main alone reads the complete candidate catalog and reconciles drafts globally into 5–10 final questions for the entire journey, never more than 20. Preserve coherent decision boundaries and exact disjoint keys; batch boundaries must never become final questions. Identify affected Work names only as context. Never answer for the person."
|
|
124
126
|
: 'Final privacy resolution: return {resolutions:[{type,value,disposition,reason}]}. Resolve every supplied term strictly by the confirmed card answer: Hide gives "hidden" and Keep gives "keep" for every covered term; never override the answer of the person with your own judgment; when the same value is also under another card answered Hide, the CLI makes it Hidden, so still resolve by the answer of this card (credentials and contact details are masked separately and never reach these tasks). Use guidance only for a term the answer does not settle. disposition MUST be exactly the string "hidden" or "keep" (the card option id "hide" means "hidden"), never pending. If rules cannot settle a value, report explicit failure instead of inventing an answer.'
|
|
125
127
|
const base = stage === 'cards' ? {} : { card: { ...group.card, termKeys: undefined }, guidance }
|
|
126
128
|
const dataFor = (terms) => {
|
|
@@ -33,6 +33,10 @@ export function runPhase(
|
|
|
33
33
|
stage?: string
|
|
34
34
|
refresh?: boolean
|
|
35
35
|
limit?: number
|
|
36
|
+
waitMs?: number
|
|
37
|
+
sleep?: (ms: number) => Promise<void>
|
|
38
|
+
/** Ensures a detached panel host at a handoff phase (ADR 0227). */
|
|
39
|
+
ensureHost?: () => Promise<{ url: string; pid: number; started: boolean }>
|
|
36
40
|
},
|
|
37
41
|
call: (procedure: string, input: unknown) => Promise<unknown>,
|
|
38
42
|
): Promise<Record<string, unknown>>
|