@akagilnc/pi-workflow-roles 0.1.3565 → 0.1.3572
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 +4 -1
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +755 -60
- package/dist/diarist-contracts.js +72 -0
- package/dist/package-contracts/terminating-tools.js +20 -2
- package/dist/packaged-role-registry.js +20 -0
- package/dist/public-cli/main.js +533 -634
- package/dist/public-cli/registry.js +4 -1
- package/extensions/role-runtime.ts +1 -0
- package/package.json +1 -1
- package/resources/diarist-collect.md +6 -10
- package/souls/diarist.md +9 -0
- package/src/acp-host/production-host.ts +1 -0
- package/src/acp-host/role-envelope.ts +2 -1
- package/src/diarist-contracts.ts +88 -0
- package/src/diarist-role.ts +60 -0
- package/src/diarist.ts +253 -179
- package/src/host-contracts.ts +6 -1
- package/src/package-contracts/terminating-tools.ts +22 -2
- package/src/packaged-role-registry.ts +20 -0
- package/src/pi/role-turn-host.ts +8 -0
- package/src/public-cli/cli.ts +29 -0
- package/src/public-cli/countersign-run.ts +9 -129
- package/src/public-cli/diarist-run.ts +312 -0
- package/src/public-cli/invocation.ts +23 -2
- package/src/public-cli/option-definitions.ts +18 -0
- package/src/public-cli/post-admission.ts +2 -2
- package/src/public-cli/registry.ts +3 -0
- package/src/public-cli/run-lifecycle.ts +31 -3
- package/src/public-cli/settlement.ts +50 -2
- package/src/public-cli/terminal.ts +2 -1
- package/src/role-runtime.ts +117 -6
- package/src/ticket-provenance-contracts.ts +1 -0
- package/src/ticket-provenance.ts +0 -23
- package/src/diarist-llm-collector.ts +0 -324
package/src/diarist.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 起居郎
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 起居郎 mechanical halves — ADR 0075 `diarist-is-role` / `diarist-collector-is-own-turn`.
|
|
3
|
+
* Semantic collection is the diarist role's own LLM turn; this module keeps the
|
|
4
|
+
* mechanical safeguard band only: source enumeration into a frozen catalog, and
|
|
5
|
+
* the verbatim reverse-verify → idempotent sitian append → watermark commit.
|
|
6
|
+
* No lifecycle here (ADR 0018): the seat prepares, the role envelope commits.
|
|
5
7
|
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
|
|
6
10
|
import { extractReferencedAdrPaths } from "./adr-path-refs.ts";
|
|
7
11
|
import {
|
|
8
12
|
createGhApiRunner,
|
|
@@ -22,13 +26,9 @@ import {
|
|
|
22
26
|
type DiaristIssueFace,
|
|
23
27
|
type DiaristSourceBlock,
|
|
24
28
|
} from "./diarist-mechanical.ts";
|
|
25
|
-
import {
|
|
26
|
-
createHermesDiaristCollector,
|
|
27
|
-
type DiaristLlmCollectResult,
|
|
28
|
-
} from "./diarist-llm-collector.ts";
|
|
29
|
+
import type { DiaristSelection } from "./diarist-contracts.ts";
|
|
29
30
|
import { parseGitHubOriginRemote } from "./reviewer-pinned-git.ts";
|
|
30
31
|
import {
|
|
31
|
-
appendCollectorFailureDiagnostic,
|
|
32
32
|
appendIssueSourceFailureDiagnostic,
|
|
33
33
|
appendQuoteVerifyFailureDiagnostic,
|
|
34
34
|
appendTicketProvenanceEntry,
|
|
@@ -40,8 +40,7 @@ import {
|
|
|
40
40
|
ticketProvenanceEntryIdentity,
|
|
41
41
|
writeTicketProvenanceHumanView,
|
|
42
42
|
} from "./ticket-provenance.ts";
|
|
43
|
-
import
|
|
44
|
-
import { readSitianRecords, type RecordPointer } from "./sitian-facade.ts";
|
|
43
|
+
import { readSitianRecords } from "./sitian-facade.ts";
|
|
45
44
|
import { execFileSync } from "node:child_process";
|
|
46
45
|
|
|
47
46
|
export type { DiaristIssueFace } from "./diarist-mechanical.ts";
|
|
@@ -72,55 +71,15 @@ export class DiaristIssueSourceError extends Error {
|
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
/**
|
|
75
|
-
* Issue-face fetch for the diarist
|
|
76
|
-
*
|
|
77
|
-
* Test injectors may return undefined to simulate unavailability — station converts
|
|
78
|
-
* that into a typed DiaristIssueSourceError + durable diagnostic.
|
|
74
|
+
* Issue-face fetch for the diarist seat. Unavailability is a typed
|
|
75
|
+
* DiaristIssueSourceError — there is no soft-undefined face.
|
|
79
76
|
*/
|
|
80
77
|
export type DiaristIssueFaceFetcher = (input: {
|
|
81
78
|
readonly owner: string;
|
|
82
79
|
readonly repo: string;
|
|
83
80
|
readonly ticketNumber: number;
|
|
84
81
|
readonly signal?: AbortSignal;
|
|
85
|
-
}) => Promise<DiaristIssueFace
|
|
86
|
-
|
|
87
|
-
export type DiaristRunInput = {
|
|
88
|
-
readonly ticketNumber: number;
|
|
89
|
-
readonly cwd: string;
|
|
90
|
-
/** Explicit package home (admitted run / tests); never process.env.HOME (#604). */
|
|
91
|
-
readonly home?: string;
|
|
92
|
-
/**
|
|
93
|
-
* Frozen GitHub issue face (body + comments). Production loads via shared gh seam.
|
|
94
|
-
* Soft-unavailable → omit (no fake face from attachments).
|
|
95
|
-
*/
|
|
96
|
-
readonly issueFace?: DiaristIssueFace;
|
|
97
|
-
/** Extra cwd roots whose cc project folders are scanned. */
|
|
98
|
-
readonly sessionCwds?: readonly string[];
|
|
99
|
-
readonly signal?: AbortSignal;
|
|
100
|
-
/** Package root for hermes collector method material resolution. */
|
|
101
|
-
readonly packageRoot?: string;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export type DiaristRunResult = {
|
|
105
|
-
readonly ticketNumber: number;
|
|
106
|
-
/** Safeguard-cleaned source count before incremental filter. */
|
|
107
|
-
readonly candidateCount: number;
|
|
108
|
-
/** Blocks not yet on the volume — sole set sent to the collector this court. */
|
|
109
|
-
readonly freshCount: number;
|
|
110
|
-
readonly appended: number;
|
|
111
|
-
readonly rejectedQuotes: number;
|
|
112
|
-
readonly pointers: readonly RecordPointer[];
|
|
113
|
-
readonly entries: readonly TicketProvenanceEntry[];
|
|
114
|
-
readonly humanViewFile: string;
|
|
115
|
-
readonly volumeRecordFile: string;
|
|
116
|
-
readonly collectorStatus:
|
|
117
|
-
| "ok"
|
|
118
|
-
| "skipped-no-fresh"
|
|
119
|
-
| "failed"
|
|
120
|
-
| "empty-selection";
|
|
121
|
-
readonly collectorError?: string;
|
|
122
|
-
readonly llmRawStdout?: string;
|
|
123
|
-
};
|
|
82
|
+
}) => Promise<DiaristIssueFace>;
|
|
124
83
|
|
|
125
84
|
/**
|
|
126
85
|
* Production issue-face capability over shared gh execution seams.
|
|
@@ -216,10 +175,78 @@ export function resolveDiaristGithubOrigin(
|
|
|
216
175
|
return parseGitHubOriginRemote(remoteUrl);
|
|
217
176
|
}
|
|
218
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Acquire the issue face for a bound ticket. Failures are typed and durable on
|
|
180
|
+
* the ticket-provenance volume, then propagated — never washed into empty face.
|
|
181
|
+
*/
|
|
182
|
+
export async function loadDiaristIssueFace(input: {
|
|
183
|
+
readonly ticketNumber: number;
|
|
184
|
+
readonly projectRoot: string;
|
|
185
|
+
readonly home?: string;
|
|
186
|
+
readonly fetcher?: DiaristIssueFaceFetcher;
|
|
187
|
+
}): Promise<DiaristIssueFace> {
|
|
188
|
+
const persistAndThrow = (error: DiaristIssueSourceError): DiaristIssueSourceError => {
|
|
189
|
+
appendIssueSourceFailureDiagnostic({
|
|
190
|
+
ticketNumber: input.ticketNumber,
|
|
191
|
+
cwd: input.projectRoot,
|
|
192
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
193
|
+
cause: error.message,
|
|
194
|
+
reason: error.reason,
|
|
195
|
+
});
|
|
196
|
+
return error;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const origin = resolveDiaristGithubOrigin(input.projectRoot);
|
|
200
|
+
if (origin === undefined) {
|
|
201
|
+
throw persistAndThrow(
|
|
202
|
+
new DiaristIssueSourceError(
|
|
203
|
+
"origin-unresolved",
|
|
204
|
+
`bound ticket #${input.ticketNumber} issue face requires a resolvable github.com origin remote`,
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const fetcher = input.fetcher ?? createDiaristIssueFaceFetcher();
|
|
210
|
+
try {
|
|
211
|
+
return await fetcher({
|
|
212
|
+
owner: origin.owner,
|
|
213
|
+
repo: origin.repo,
|
|
214
|
+
ticketNumber: input.ticketNumber,
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
throw persistAndThrow(
|
|
218
|
+
error instanceof DiaristIssueSourceError
|
|
219
|
+
? error
|
|
220
|
+
: new DiaristIssueSourceError(
|
|
221
|
+
"issue-unavailable",
|
|
222
|
+
`issue face fetch failed for ${origin.owner}/${origin.repo}#${input.ticketNumber}`,
|
|
223
|
+
{ cause: error },
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** One frozen candidate the diarist turn may select by index. */
|
|
230
|
+
export type DiaristSourceCandidate = DiaristSourceBlock & {
|
|
231
|
+
readonly candidateIndex: number;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Frozen per-ticket catalog handed to the diarist turn and re-read by the
|
|
236
|
+
* envelope at accept time. Carries its own volume coordinates so neither side
|
|
237
|
+
* re-derives them from ambient state.
|
|
238
|
+
*/
|
|
239
|
+
export type DiaristSourceCatalog = {
|
|
240
|
+
readonly ticketNumber: number;
|
|
241
|
+
readonly cwd: string;
|
|
242
|
+
readonly home?: string;
|
|
243
|
+
readonly candidates: readonly DiaristSourceCandidate[];
|
|
244
|
+
};
|
|
245
|
+
|
|
219
246
|
/**
|
|
220
247
|
* Identities already processed for this ticket:
|
|
221
248
|
* - volume record identities (selected / verify-fail residue)
|
|
222
|
-
* - offered watermark (blocks shown to
|
|
249
|
+
* - offered watermark (blocks shown to the diarist, selected or not)
|
|
223
250
|
*/
|
|
224
251
|
async function loadSeenEntryIdentities(
|
|
225
252
|
ticketNumber: number,
|
|
@@ -257,7 +284,11 @@ function blockEntryIdentity(
|
|
|
257
284
|
* Mechanical layer does not prose-filter for relevance (锚定宪法).
|
|
258
285
|
* Attachments are never merged in as fake issue-body-comment.
|
|
259
286
|
*/
|
|
260
|
-
|
|
287
|
+
function loadSourceBlocks(input: {
|
|
288
|
+
readonly cwd: string;
|
|
289
|
+
readonly issueFace?: DiaristIssueFace;
|
|
290
|
+
readonly sessionCwds?: readonly string[];
|
|
291
|
+
}): DiaristSourceBlock[] {
|
|
261
292
|
const cwds = input.sessionCwds ?? [input.cwd];
|
|
262
293
|
const blocks: DiaristSourceBlock[] = [...readCcSessionBlocks({ cwds })];
|
|
263
294
|
if (input.issueFace !== undefined) {
|
|
@@ -279,162 +310,205 @@ async function loadSourceBlocks(input: DiaristRunInput): Promise<DiaristSourceBl
|
|
|
279
310
|
return blocks;
|
|
280
311
|
}
|
|
281
312
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
313
|
+
export type PrepareDiaristSourceCatalogInput = {
|
|
314
|
+
readonly ticketNumber: number;
|
|
315
|
+
readonly cwd: string;
|
|
316
|
+
/** Explicit package home (admitted run / tests); never process.env.HOME (#604). */
|
|
317
|
+
readonly home?: string;
|
|
318
|
+
/** Frozen GitHub issue face (body + comments) from the shared gh seam. */
|
|
319
|
+
readonly issueFace?: DiaristIssueFace;
|
|
320
|
+
/** Extra cwd roots whose cc project folders are scanned. */
|
|
321
|
+
readonly sessionCwds?: readonly string[];
|
|
322
|
+
};
|
|
290
323
|
|
|
291
324
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
325
|
+
* Mechanical half A — source enumeration into a frozen catalog.
|
|
326
|
+
* Establishes the per-ticket volume + human view for every bound run (ADR 0075
|
|
327
|
+
* `ticket-provenance-file` 每票一份起居录), then offers only blocks whose entry
|
|
328
|
+
* identity is not already on the volume or the offered watermark (增量幂等).
|
|
295
329
|
*/
|
|
296
|
-
export async function
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const anchors: DiaristAnchorSet = buildDiaristAnchors({
|
|
330
|
+
export async function prepareDiaristSourceCatalog(
|
|
331
|
+
input: PrepareDiaristSourceCatalogInput,
|
|
332
|
+
): Promise<DiaristSourceCatalog> {
|
|
333
|
+
ensureTicketProvenanceVolume(input.ticketNumber, input.cwd, input.home);
|
|
334
|
+
const volume = await readTicketProvenance(input.ticketNumber, input.cwd, input.home);
|
|
335
|
+
writeTicketProvenanceHumanView({
|
|
303
336
|
ticketNumber: input.ticketNumber,
|
|
304
|
-
|
|
337
|
+
cwd: input.cwd,
|
|
338
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
339
|
+
entries: volume.entries,
|
|
305
340
|
});
|
|
306
|
-
|
|
341
|
+
|
|
342
|
+
const rawBlocks = loadSourceBlocks(input);
|
|
307
343
|
// Safeguard only (notify filter + dedupe) — never prose-based exclusion.
|
|
308
344
|
const safeguarded = mechanicalSafeguardPipeline(rawBlocks);
|
|
309
|
-
// Incremental: only blocks whose entry identity is not yet on the volume
|
|
310
|
-
// are offered to the collector (ADR 0075 refresh-every-court = 增量幂等).
|
|
311
345
|
const seen = await loadSeenEntryIdentities(input.ticketNumber, input.cwd, input.home);
|
|
312
346
|
const fresh = safeguarded.filter(
|
|
313
347
|
(block) => !seen.has(blockEntryIdentity(input.ticketNumber, block)),
|
|
314
348
|
);
|
|
315
349
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
let llmRawStdout: string | undefined;
|
|
319
|
-
let collect: DiaristLlmCollectResult | undefined;
|
|
320
|
-
|
|
321
|
-
// Production composition always runs the hermes collector (ADR 0075).
|
|
322
|
-
// No injectable skip / alternate collector on this seam.
|
|
323
|
-
const collector = createHermesDiaristCollector({
|
|
350
|
+
return {
|
|
351
|
+
ticketNumber: input.ticketNumber,
|
|
324
352
|
cwd: input.cwd,
|
|
325
|
-
...(input.
|
|
326
|
-
|
|
353
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
354
|
+
candidates: fresh.map((block, candidateIndex) => ({ ...block, candidateIndex })),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
327
357
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// before selected entries / quote diagnostics are committed.
|
|
342
|
-
} catch (error) {
|
|
343
|
-
collectorStatus = "failed";
|
|
344
|
-
collectorError =
|
|
345
|
-
error instanceof Error ? error.message : String(error);
|
|
346
|
-
// Durable true-cause on the ticket volume (append-only history).
|
|
347
|
-
appendCollectorFailureDiagnostic({
|
|
348
|
-
ticketNumber: input.ticketNumber,
|
|
349
|
-
cwd: input.cwd,
|
|
350
|
-
...homeOpt,
|
|
351
|
-
collectorError,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
358
|
+
export function serializeDiaristSourceCatalog(catalog: DiaristSourceCatalog): string {
|
|
359
|
+
return JSON.stringify(catalog);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Read a frozen catalog written by the seat. Unreadable/malformed fails loudly. */
|
|
363
|
+
export function loadDiaristSourceCatalog(path: string): DiaristSourceCatalog {
|
|
364
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
365
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
366
|
+
throw new Error(`diarist source catalog is not an object (${path})`);
|
|
367
|
+
}
|
|
368
|
+
const record = parsed as Record<string, unknown>;
|
|
369
|
+
if (typeof record.ticketNumber !== "number" || typeof record.cwd !== "string") {
|
|
370
|
+
throw new Error(`diarist source catalog is missing ticket coordinates (${path})`);
|
|
354
371
|
}
|
|
372
|
+
if (!Array.isArray(record.candidates)) {
|
|
373
|
+
throw new Error(`diarist source catalog is missing candidates (${path})`);
|
|
374
|
+
}
|
|
375
|
+
return parsed as DiaristSourceCatalog;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 失败真因 — typed causes for submitted rows that never reached the volume.
|
|
380
|
+
* Names the cause only; the per-row detail stays on the volume diagnostic.
|
|
381
|
+
*/
|
|
382
|
+
export type DiaristFailureCause =
|
|
383
|
+
/** Row pointed at no candidate in this turn's frozen catalog. */
|
|
384
|
+
| "unknown-candidate"
|
|
385
|
+
/** Row's quotes failed verbatim reverse-verify. */
|
|
386
|
+
| "quote-verify-rejected";
|
|
387
|
+
|
|
388
|
+
/** Honest machine facts about what this turn actually committed to the volume. */
|
|
389
|
+
export type DiaristCommitFacts = {
|
|
390
|
+
readonly ticketNumber: number;
|
|
391
|
+
/** Candidates offered to the diarist this turn. */
|
|
392
|
+
readonly offered: number;
|
|
393
|
+
/** New entries this turn actually put on the volume (entry-count delta). */
|
|
394
|
+
readonly appended: number;
|
|
395
|
+
/** Selections rejected by verbatim reverse-verify. */
|
|
396
|
+
readonly rejectedQuotes: number;
|
|
397
|
+
/** Offered identities newly written to the watermark this turn. */
|
|
398
|
+
readonly watermarked: number;
|
|
399
|
+
readonly volumeRecordFile: string;
|
|
400
|
+
readonly humanViewFile: string;
|
|
401
|
+
/**
|
|
402
|
+
* What this turn's collection amounted to. A turn whose every row was
|
|
403
|
+
* dropped is `nothing-appended`, never `empty-selection` — the diarist
|
|
404
|
+
* selecting nothing and the safeguard band rejecting everything are
|
|
405
|
+
* different events. Why rows were dropped is `failureCauses`.
|
|
406
|
+
*/
|
|
407
|
+
readonly collectorStatus:
|
|
408
|
+
| "ok"
|
|
409
|
+
| "empty-selection"
|
|
410
|
+
| "nothing-appended"
|
|
411
|
+
| "skipped-no-fresh";
|
|
412
|
+
/**
|
|
413
|
+
* 失败真因: empty when every submitted row landed. Populated whenever rows
|
|
414
|
+
* were dropped — including a partial turn whose collectorStatus is `ok`.
|
|
415
|
+
*/
|
|
416
|
+
readonly failureCauses: readonly DiaristFailureCause[];
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Mechanical half B — commit the diarist turn's selections.
|
|
421
|
+
* Verbatim reverse-verify → idempotent sitian append → watermark → human view.
|
|
422
|
+
* Verify failure records a single typed diagnostic and drops that selection; it
|
|
423
|
+
* never bounces the receipt (第 0 条) and never enters the volume as an entry.
|
|
424
|
+
*/
|
|
425
|
+
export async function commitDiaristSelections(input: {
|
|
426
|
+
readonly catalog: DiaristSourceCatalog;
|
|
427
|
+
readonly selections: readonly DiaristSelection[];
|
|
428
|
+
}): Promise<DiaristCommitFacts> {
|
|
429
|
+
const { ticketNumber, cwd } = input.catalog;
|
|
430
|
+
const homeOpt = input.catalog.home === undefined ? {} : { home: input.catalog.home };
|
|
431
|
+
const volumePaths = ensureTicketProvenanceVolume(ticketNumber, cwd, input.catalog.home);
|
|
355
432
|
|
|
356
|
-
const
|
|
357
|
-
|
|
433
|
+
const anchors: DiaristAnchorSet = buildDiaristAnchors({ ticketNumber });
|
|
434
|
+
// Entry-count baseline: sitian entry identity already makes a repeat of an
|
|
435
|
+
// already-recorded block a no-op append, so the honest `appended` is what the
|
|
436
|
+
// volume gained — not how many rows survived verify.
|
|
437
|
+
const before = await readTicketProvenance(ticketNumber, cwd, input.catalog.home);
|
|
438
|
+
let acceptedRows = 0;
|
|
358
439
|
let rejectedQuotes = 0;
|
|
440
|
+
let unknownCandidate = false;
|
|
359
441
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
ticketNumber: input.ticketNumber,
|
|
378
|
-
cwd: input.cwd,
|
|
379
|
-
...homeOpt,
|
|
380
|
-
cause: projected.cause,
|
|
381
|
-
});
|
|
382
|
-
pointers.push(ptr);
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
const ptr = appendTicketProvenanceEntry({
|
|
386
|
-
ticketNumber: input.ticketNumber,
|
|
387
|
-
cwd: input.cwd,
|
|
442
|
+
for (const selection of input.selections) {
|
|
443
|
+
const block = input.catalog.candidates[selection.candidateIndex];
|
|
444
|
+
if (block === undefined) {
|
|
445
|
+
unknownCandidate = true;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const projected = blockToLlmEntry(block, {
|
|
449
|
+
anchors,
|
|
450
|
+
quotes: selection.quotes,
|
|
451
|
+
...(selection.note === undefined ? {} : { note: selection.note }),
|
|
452
|
+
});
|
|
453
|
+
if (!projected.ok) {
|
|
454
|
+
rejectedQuotes += 1;
|
|
455
|
+
// Single diagnostic expression — never a disguised diary entry.
|
|
456
|
+
appendQuoteVerifyFailureDiagnostic({
|
|
457
|
+
ticketNumber,
|
|
458
|
+
cwd,
|
|
388
459
|
...homeOpt,
|
|
389
|
-
|
|
390
|
-
source: "diarist",
|
|
460
|
+
cause: projected.cause,
|
|
391
461
|
});
|
|
392
|
-
|
|
393
|
-
accepted.push(projected.entry);
|
|
462
|
+
continue;
|
|
394
463
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
// identities only after the volume writes above, so a crash mid-commit
|
|
399
|
-
// still retries the batch next court. Entry identity and quote-verify
|
|
400
|
-
// diagnostic identity are stable — retry does not duplicate either.
|
|
401
|
-
// Failure does not advance the watermark (retry honestly).
|
|
402
|
-
if (
|
|
403
|
-
collect !== undefined &&
|
|
404
|
-
(collectorStatus === "ok" || collectorStatus === "empty-selection")
|
|
405
|
-
) {
|
|
406
|
-
recordOfferedIdentities({
|
|
407
|
-
ticketNumber: input.ticketNumber,
|
|
408
|
-
cwd: input.cwd,
|
|
464
|
+
appendTicketProvenanceEntry({
|
|
465
|
+
ticketNumber,
|
|
466
|
+
cwd,
|
|
409
467
|
...homeOpt,
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
),
|
|
468
|
+
entry: projected.entry,
|
|
469
|
+
source: "diarist",
|
|
413
470
|
});
|
|
471
|
+
acceptedRows += 1;
|
|
414
472
|
}
|
|
415
473
|
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
|
|
474
|
+
// Watermark advances only after the durable volume writes above, so a crash
|
|
475
|
+
// mid-commit retries the batch on the next summons. Entry identity and
|
|
476
|
+
// quote-verify diagnostic identity are stable — retry duplicates neither.
|
|
477
|
+
const identities = input.catalog.candidates.map((block) =>
|
|
478
|
+
blockEntryIdentity(ticketNumber, block),
|
|
479
|
+
);
|
|
480
|
+
const alreadyWatermarked = readOfferedIdentities(ticketNumber, cwd, input.catalog.home);
|
|
481
|
+
const watermarked = identities.filter((identity) => !alreadyWatermarked.has(identity)).length;
|
|
482
|
+
recordOfferedIdentities({ ticketNumber, cwd, ...homeOpt, identities });
|
|
483
|
+
|
|
484
|
+
// Refresh the human view from the full volume (includes prior summons).
|
|
485
|
+
const volume = await readTicketProvenance(ticketNumber, cwd, input.catalog.home);
|
|
419
486
|
const humanViewFile = writeTicketProvenanceHumanView({
|
|
420
|
-
ticketNumber
|
|
421
|
-
cwd
|
|
487
|
+
ticketNumber,
|
|
488
|
+
cwd,
|
|
422
489
|
...homeOpt,
|
|
423
490
|
entries: volume.entries,
|
|
424
491
|
});
|
|
425
492
|
|
|
426
493
|
return {
|
|
427
|
-
ticketNumber
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
appended: accepted.length,
|
|
494
|
+
ticketNumber,
|
|
495
|
+
offered: input.catalog.candidates.length,
|
|
496
|
+
appended: volume.entries.length - before.entries.length,
|
|
431
497
|
rejectedQuotes,
|
|
432
|
-
|
|
433
|
-
entries: volume.entries,
|
|
434
|
-
humanViewFile,
|
|
498
|
+
watermarked,
|
|
435
499
|
volumeRecordFile: volumePaths.recordFile,
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
500
|
+
humanViewFile,
|
|
501
|
+
collectorStatus:
|
|
502
|
+
input.catalog.candidates.length === 0
|
|
503
|
+
? "skipped-no-fresh"
|
|
504
|
+
: acceptedRows > 0
|
|
505
|
+
? "ok"
|
|
506
|
+
: input.selections.length === 0
|
|
507
|
+
? "empty-selection"
|
|
508
|
+
: "nothing-appended",
|
|
509
|
+
failureCauses: [
|
|
510
|
+
...(unknownCandidate ? (["unknown-candidate"] as const) : []),
|
|
511
|
+
...(rejectedQuotes > 0 ? (["quote-verify-rejected"] as const) : []),
|
|
512
|
+
],
|
|
439
513
|
};
|
|
440
514
|
}
|
package/src/host-contracts.ts
CHANGED
|
@@ -125,7 +125,12 @@ export type RoleTurnActivation =
|
|
|
125
125
|
}
|
|
126
126
|
| { readonly role: "inspector" }
|
|
127
127
|
| { readonly role: "gatekeeper" }
|
|
128
|
-
| { readonly role: "navigator" }
|
|
128
|
+
| { readonly role: "navigator" }
|
|
129
|
+
| {
|
|
130
|
+
readonly role: "diarist";
|
|
131
|
+
/** Frozen source-catalog path; absent for a true-unbound summons (#708). */
|
|
132
|
+
readonly sourcesPath?: string;
|
|
133
|
+
};
|
|
129
134
|
|
|
130
135
|
export type RoleTurnContinuation =
|
|
131
136
|
| { readonly kind: "initial"; readonly prompt: string }
|
|
@@ -34,6 +34,7 @@ import { NOTARY_ACCEPTED_TEXT, NOTARY_OUTPUT_TOOL_NAME, validateRecordedNotaryOu
|
|
|
34
34
|
import { COUNTERSIGN_ACCEPTED_TEXT, COUNTERSIGN_OUTPUT_TOOL_NAME, validateRecordedCountersignOutput, type CountersignVerdict } from "../countersign-contracts.ts";
|
|
35
35
|
import { GLEANER_LEFT_ACCEPTED_TEXT, GLEANER_LEFT_OUTPUT_TOOL_NAME, validateRecordedGleanerLeftOutput, type GleanerLeftOutput } from "../gleaner-left-contracts.ts";
|
|
36
36
|
import { INSPECTOR_ACCEPTED_TEXT, INSPECTOR_OUTPUT_TOOL_NAME, validateRecordedInspectorOutput, type InspectorOutput } from "../inspector-contracts.ts";
|
|
37
|
+
import { DIARIST_ACCEPTED_TEXT, DIARIST_OUTPUT_TOOL_NAME, validateRecordedDiaristOutput, type DiaristOutput } from "../diarist-contracts.ts";
|
|
37
38
|
import {
|
|
38
39
|
CODER_ACCEPTED_TEXT,
|
|
39
40
|
CODER_OUTPUT_TOOL_NAME,
|
|
@@ -71,6 +72,7 @@ export {
|
|
|
71
72
|
validateRecordedInspectorOutput,
|
|
72
73
|
validateRecordedGatekeeperOutput,
|
|
73
74
|
validateRecordedNavigatorOutput,
|
|
75
|
+
validateRecordedDiaristOutput,
|
|
74
76
|
};
|
|
75
77
|
export type {
|
|
76
78
|
CollectorReceipt,
|
|
@@ -87,6 +89,7 @@ export type {
|
|
|
87
89
|
InspectorOutput,
|
|
88
90
|
GatekeeperDirectOutput,
|
|
89
91
|
NavigatorAdvice,
|
|
92
|
+
DiaristOutput,
|
|
90
93
|
};
|
|
91
94
|
|
|
92
95
|
export const TERMINATING_TOOL_NAMES = [
|
|
@@ -103,6 +106,7 @@ export const TERMINATING_TOOL_NAMES = [
|
|
|
103
106
|
INSPECTOR_OUTPUT_TOOL_NAME,
|
|
104
107
|
GATEKEEPER_OUTPUT_TOOL_NAME,
|
|
105
108
|
NAVIGATOR_OUTPUT_TOOL_NAME,
|
|
109
|
+
DIARIST_OUTPUT_TOOL_NAME,
|
|
106
110
|
] as const;
|
|
107
111
|
|
|
108
112
|
export type TerminatingToolName = (typeof TERMINATING_TOOL_NAMES)[number];
|
|
@@ -119,7 +123,8 @@ export type AcceptedDetails =
|
|
|
119
123
|
| GleanerLeftOutput
|
|
120
124
|
| InspectorOutput
|
|
121
125
|
| GatekeeperDirectOutput
|
|
122
|
-
| NavigatorAdvice
|
|
126
|
+
| NavigatorAdvice
|
|
127
|
+
| DiaristOutput;
|
|
123
128
|
|
|
124
129
|
export function isTerminatingToolName(
|
|
125
130
|
name: string,
|
|
@@ -155,6 +160,8 @@ export function acceptedTextFor(toolName: TerminatingToolName): string {
|
|
|
155
160
|
return GATEKEEPER_ACCEPTED_TEXT;
|
|
156
161
|
case NAVIGATOR_OUTPUT_TOOL_NAME:
|
|
157
162
|
return NAVIGATOR_ACCEPTED_TEXT;
|
|
163
|
+
case DIARIST_OUTPUT_TOOL_NAME:
|
|
164
|
+
return DIARIST_ACCEPTED_TEXT;
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
|
|
@@ -213,6 +220,7 @@ export function validateAcceptedDetails(
|
|
|
213
220
|
[INSPECTOR_OUTPUT_TOOL_NAME]: ["pass", "bounce"],
|
|
214
221
|
[GATEKEEPER_OUTPUT_TOOL_NAME]: ["dispatch", "pass"],
|
|
215
222
|
[NAVIGATOR_OUTPUT_TOOL_NAME]: ["advice"],
|
|
223
|
+
[DIARIST_OUTPUT_TOOL_NAME]: ["completed"],
|
|
216
224
|
};
|
|
217
225
|
const collectorDiscriminator = toolName === COLLECTOR_OUTPUT_TOOL && Array.isArray(candidate?.groups);
|
|
218
226
|
const baseDiscriminator = discriminator;
|
|
@@ -253,6 +261,8 @@ export function validateAcceptedDetails(
|
|
|
253
261
|
return validateRecordedGatekeeperOutput(details);
|
|
254
262
|
case NAVIGATOR_OUTPUT_TOOL_NAME:
|
|
255
263
|
return validateRecordedNavigatorOutput(details);
|
|
264
|
+
case DIARIST_OUTPUT_TOOL_NAME:
|
|
265
|
+
return validateRecordedDiaristOutput(details);
|
|
256
266
|
}
|
|
257
267
|
} catch (error) {
|
|
258
268
|
if (error instanceof Error && error.constructor === Error) throw new AcceptedDetailsContractError(error.message, { cause: error });
|
|
@@ -280,6 +290,15 @@ export function validateAcceptedLifecycle(
|
|
|
280
290
|
if (!deepEqual(testimony, projected)) throw new Error("accepted tool lifecycle details mismatch");
|
|
281
291
|
return details;
|
|
282
292
|
}
|
|
293
|
+
if (toolName === DIARIST_OUTPUT_TOOL_NAME) {
|
|
294
|
+
// Envelope-owned mechanical sitian facts are runtime-bound on details only
|
|
295
|
+
// (same shape as the Doctor runtime cost); the submitted arguments carry the
|
|
296
|
+
// role's own selections. Machine facts never come from model self-report.
|
|
297
|
+
const { sitian: _mechanical, ...submitted } = details as DiaristOutput & { sitian?: unknown };
|
|
298
|
+
const testimony = validateAcceptedDetails(toolName, argumentsValue);
|
|
299
|
+
if (!deepEqual(testimony, submitted)) throw new Error("accepted tool lifecycle details mismatch");
|
|
300
|
+
return details;
|
|
301
|
+
}
|
|
283
302
|
const argumentsDetails = validateAcceptedDetails(toolName, argumentsValue);
|
|
284
303
|
if (!deepEqual(argumentsDetails, details)) throw new Error("accepted tool lifecycle details mismatch");
|
|
285
304
|
return details;
|
|
@@ -301,7 +320,8 @@ export function acceptedFacts(toolName: TerminatingToolName, details: AcceptedDe
|
|
|
301
320
|
case GLEANER_LEFT_OUTPUT_TOOL_NAME:
|
|
302
321
|
case INSPECTOR_OUTPUT_TOOL_NAME:
|
|
303
322
|
case GATEKEEPER_OUTPUT_TOOL_NAME:
|
|
304
|
-
case NAVIGATOR_OUTPUT_TOOL_NAME:
|
|
323
|
+
case NAVIGATOR_OUTPUT_TOOL_NAME:
|
|
324
|
+
case DIARIST_OUTPUT_TOOL_NAME: return { status: (details as { status: string }).status };
|
|
305
325
|
case JUDGE_OUTPUT_TOOL_NAME: return { status: (details as { judgeStatus: string }).judgeStatus };
|
|
306
326
|
case COUNTERSIGN_OUTPUT_TOOL_NAME: return { status: (details as { countersignStatus: string }).countersignStatus };
|
|
307
327
|
case MERGER_OUTPUT_TOOL_NAME: {
|
|
@@ -11,6 +11,10 @@ import { NOTARY_OUTPUT_TOOL_NAME } from "./notary-contracts.ts";
|
|
|
11
11
|
import { COUNTERSIGN_OUTPUT_TOOL_NAME } from "./countersign-contracts.ts";
|
|
12
12
|
import { GLEANER_LEFT_OUTPUT_TOOL_NAME } from "./gleaner-left-contracts.ts";
|
|
13
13
|
import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.ts";
|
|
14
|
+
import {
|
|
15
|
+
DIARIST_OUTPUT_TOOL_NAME,
|
|
16
|
+
DIARIST_SOURCES_FLAG,
|
|
17
|
+
} from "./diarist-contracts.ts";
|
|
14
18
|
|
|
15
19
|
/** Shared by public notary and gatekeeper-province notary. */
|
|
16
20
|
export const NOTARY_SESSION_MATERIALS = [
|
|
@@ -185,6 +189,22 @@ export const PUBLIC_ROLE_RECORDS = [
|
|
|
185
189
|
activationStage: "load-and-install",
|
|
186
190
|
sessionMaterials: ["CLAUDE.md", "souls/navigator.md"],
|
|
187
191
|
},
|
|
192
|
+
// #708 / ADR 0075 `diarist-is-role`: 起居郎 is a seat like any other. The
|
|
193
|
+
// frozen source catalog rides the shared input-flag seam; it is absent for a
|
|
194
|
+
// true-unbound summons (no ticket → no diary).
|
|
195
|
+
{
|
|
196
|
+
role: "diarist",
|
|
197
|
+
phases: [null],
|
|
198
|
+
outputTool: DIARIST_OUTPUT_TOOL_NAME,
|
|
199
|
+
inputFlag: DIARIST_SOURCES_FLAG.name,
|
|
200
|
+
phaseFlag: undefined,
|
|
201
|
+
activationStage: "load-and-install",
|
|
202
|
+
sessionMaterials: [
|
|
203
|
+
"CLAUDE.md",
|
|
204
|
+
"souls/diarist.md",
|
|
205
|
+
"resources/diarist-collect.md",
|
|
206
|
+
],
|
|
207
|
+
},
|
|
188
208
|
] as const;
|
|
189
209
|
|
|
190
210
|
export type PublicRoleRecord = (typeof PUBLIC_ROLE_RECORDS)[number];
|