@akagilnc/pi-workflow-roles 0.1.3671 → 0.1.3682

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/src/diarist.ts CHANGED
@@ -45,6 +45,130 @@ import { execFileSync } from "node:child_process";
45
45
 
46
46
  export type { DiaristIssueFace } from "./diarist-mechanical.ts";
47
47
 
48
+ /** Live existence check for an asserted ticket number. */
49
+ export type TicketExistenceChecker = (input: {
50
+ readonly owner: string;
51
+ readonly repo: string;
52
+ readonly ticketNumber: number;
53
+ readonly signal?: AbortSignal;
54
+ }) => Promise<boolean>;
55
+
56
+ export type DiaristTicketVerificationReason =
57
+ | "number-not-in-instruction"
58
+ | "ticket-missing"
59
+ | "origin-unresolved"
60
+ | "assertion-uninterpretable";
61
+
62
+ /**
63
+ * Honest failure of mechanical ticket verification (ADR 0075).
64
+ * Must settle as failure — never wash into true-unbound / 无录.
65
+ */
66
+ export class DiaristTicketVerificationError extends Error {
67
+ readonly code = "diarist-ticket-verification" as const;
68
+ readonly reason: DiaristTicketVerificationReason;
69
+ constructor(
70
+ reason: DiaristTicketVerificationReason,
71
+ message: string,
72
+ options?: ErrorOptions,
73
+ ) {
74
+ super(message, options);
75
+ this.name = "DiaristTicketVerificationError";
76
+ this.reason = reason;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Decision-key exact identity: complete decimal number N appears in instruction.
82
+ * A longer number's digit substring (e.g. 82 inside 582) is not N.
83
+ * Verifies a typed claim — does not harvest candidates from prose.
84
+ */
85
+ export function instructionContainsTicketNumber(
86
+ instruction: string,
87
+ ticketNumber: number,
88
+ ): boolean {
89
+ if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) return false;
90
+ // Complete decimal token: not preceded or followed by another digit.
91
+ // Plain string (not template) so the digit-class backslash survives emit.
92
+ return new RegExp("(?<!\\d)" + String(ticketNumber) + "(?!\\d)").test(
93
+ instruction,
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Production live-ticket check over shared gh issue-body projection.
99
+ * Available issue face → exists; unavailable/invalid → missing.
100
+ */
101
+ export function createGhTicketExistenceChecker(options?: {
102
+ readonly runner?: GhApiRunner;
103
+ }): TicketExistenceChecker {
104
+ const runner = options?.runner ?? createGhApiRunner();
105
+ return async (input) => {
106
+ const projected = await projectGhIssueBody(runner, {
107
+ owner: input.owner,
108
+ repo: input.repo,
109
+ ticketNumber: input.ticketNumber,
110
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
111
+ });
112
+ return projected.status === "available";
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Mechanical verify of an LLM typed ticket assertion (ADR 0075).
118
+ * Complete decimal of N must appear in the summons; ticket must exist live.
119
+ * Throws DiaristTicketVerificationError — caller must not wash into 无录.
120
+ */
121
+ export async function verifyAssertedTicketNumber(input: {
122
+ readonly ticketNumber: number;
123
+ readonly instruction: string;
124
+ readonly projectRoot: string;
125
+ readonly checkExistence?: TicketExistenceChecker;
126
+ readonly signal?: AbortSignal;
127
+ }): Promise<void> {
128
+ const n = input.ticketNumber;
129
+ if (!Number.isSafeInteger(n) || n < 1) {
130
+ throw new DiaristTicketVerificationError(
131
+ "assertion-uninterpretable",
132
+ `diarist ticket assertion must be a safe integer >= 1, got ${String(n)}`,
133
+ );
134
+ }
135
+ if (!instructionContainsTicketNumber(input.instruction, n)) {
136
+ throw new DiaristTicketVerificationError(
137
+ "number-not-in-instruction",
138
+ `diarist ticket assertion #${n} complete decimal number does not appear in accepted instruction`,
139
+ );
140
+ }
141
+ const origin = resolveDiaristGithubOrigin(input.projectRoot);
142
+ if (origin === undefined) {
143
+ throw new DiaristTicketVerificationError(
144
+ "origin-unresolved",
145
+ `diarist ticket assertion #${n} requires a resolvable github.com origin remote for live verification`,
146
+ );
147
+ }
148
+ const checkExistence = input.checkExistence ?? createGhTicketExistenceChecker();
149
+ let exists: boolean;
150
+ try {
151
+ exists = await checkExistence({
152
+ owner: origin.owner,
153
+ repo: origin.repo,
154
+ ticketNumber: n,
155
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
156
+ });
157
+ } catch (error) {
158
+ throw new DiaristTicketVerificationError(
159
+ "ticket-missing",
160
+ `diarist ticket assertion #${n} live verification failed`,
161
+ { cause: error },
162
+ );
163
+ }
164
+ if (!exists) {
165
+ throw new DiaristTicketVerificationError(
166
+ "ticket-missing",
167
+ `diarist ticket assertion #${n} does not exist as a live issue on ${origin.owner}/${origin.repo}`,
168
+ );
169
+ }
170
+ }
171
+
48
172
  /** Typed reasons when bound-ticket issue face cannot be acquired honestly. */
49
173
  export type DiaristIssueSourceReason =
50
174
  | "origin-unresolved"
@@ -232,12 +356,19 @@ export type DiaristSourceCandidate = DiaristSourceBlock & {
232
356
  };
233
357
 
234
358
  /**
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.
359
+ * Frozen catalog handed to the diarist turn and re-read by the envelope at
360
+ * accept time. Carries summons text + run coordinates so accept can verify an
361
+ * LLM ticket assertion (ADR 0075) without re-deriving ambient state.
362
+ * ticketNumber absent = first summons still open for LLM assertion.
238
363
  */
239
364
  export type DiaristSourceCatalog = {
240
- readonly ticketNumber: number;
365
+ /** Bound before turn when book already records the ticket; else LLM asserts. */
366
+ readonly ticketNumber?: number;
367
+ /** Caller summons — sole text mechanical verify reads for complete-decimal check. */
368
+ readonly instruction: string;
369
+ /** Run dossier pages accept binds after a verified first-summons assertion. */
370
+ readonly runDirectory: string;
371
+ readonly projectRoot: string;
241
372
  readonly cwd: string;
242
373
  readonly home?: string;
243
374
  readonly candidates: readonly DiaristSourceCandidate[];
@@ -311,7 +442,11 @@ function loadSourceBlocks(input: {
311
442
  }
312
443
 
313
444
  export type PrepareDiaristSourceCatalogInput = {
314
- readonly ticketNumber: number;
445
+ /** Bound ticket when known before the turn; omit on first-summons identity round. */
446
+ readonly ticketNumber?: number;
447
+ readonly instruction: string;
448
+ readonly runDirectory: string;
449
+ readonly projectRoot: string;
315
450
  readonly cwd: string;
316
451
  /** Explicit package home (admitted run / tests); never process.env.HOME (#604). */
317
452
  readonly home?: string;
@@ -323,32 +458,44 @@ export type PrepareDiaristSourceCatalogInput = {
323
458
 
324
459
  /**
325
460
  * 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 (增量幂等).
461
+ * When ticketNumber is known: establishes the per-ticket volume + human view
462
+ * (ADR 0075 `ticket-provenance-file` 每票一份起居录) and offers only blocks not
463
+ * yet on the volume / offered watermark (增量幂等).
464
+ * When ticketNumber is absent (first summons): offers session candidates without
465
+ * minting a volume — the LLM turn asserts identity; accept verifies and mints.
329
466
  */
330
467
  export async function prepareDiaristSourceCatalog(
331
468
  input: PrepareDiaristSourceCatalogInput,
332
469
  ): Promise<DiaristSourceCatalog> {
333
- ensureTicketProvenanceVolume(input.ticketNumber, input.cwd, input.home);
334
- const volume = await readTicketProvenance(input.ticketNumber, input.cwd, input.home);
335
- writeTicketProvenanceHumanView({
336
- ticketNumber: input.ticketNumber,
337
- cwd: input.cwd,
338
- ...(input.home === undefined ? {} : { home: input.home }),
339
- entries: volume.entries,
340
- });
470
+ const known = input.ticketNumber;
471
+ if (known !== undefined) {
472
+ ensureTicketProvenanceVolume(known, input.cwd, input.home);
473
+ const volume = await readTicketProvenance(known, input.cwd, input.home);
474
+ writeTicketProvenanceHumanView({
475
+ ticketNumber: known,
476
+ cwd: input.cwd,
477
+ ...(input.home === undefined ? {} : { home: input.home }),
478
+ entries: volume.entries,
479
+ });
480
+ }
341
481
 
342
482
  const rawBlocks = loadSourceBlocks(input);
343
483
  // Safeguard only (notify filter + dedupe) — never prose-based exclusion.
344
484
  const safeguarded = mechanicalSafeguardPipeline(rawBlocks);
345
- const seen = await loadSeenEntryIdentities(input.ticketNumber, input.cwd, input.home);
485
+ const seen =
486
+ known === undefined
487
+ ? new Set<string>()
488
+ : await loadSeenEntryIdentities(known, input.cwd, input.home);
346
489
  const fresh = safeguarded.filter(
347
- (block) => !seen.has(blockEntryIdentity(input.ticketNumber, block)),
490
+ (block) =>
491
+ known === undefined || !seen.has(blockEntryIdentity(known, block)),
348
492
  );
349
493
 
350
494
  return {
351
- ticketNumber: input.ticketNumber,
495
+ ...(known === undefined ? {} : { ticketNumber: known }),
496
+ instruction: input.instruction,
497
+ runDirectory: input.runDirectory,
498
+ projectRoot: input.projectRoot,
352
499
  cwd: input.cwd,
353
500
  ...(input.home === undefined ? {} : { home: input.home }),
354
501
  candidates: fresh.map((block, candidateIndex) => ({ ...block, candidateIndex })),
@@ -366,8 +513,25 @@ export function loadDiaristSourceCatalog(path: string): DiaristSourceCatalog {
366
513
  throw new Error(`diarist source catalog is not an object (${path})`);
367
514
  }
368
515
  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})`);
516
+ if (typeof record.cwd !== "string") {
517
+ throw new Error(`diarist source catalog is missing cwd (${path})`);
518
+ }
519
+ if (typeof record.instruction !== "string") {
520
+ throw new Error(`diarist source catalog is missing instruction (${path})`);
521
+ }
522
+ if (typeof record.runDirectory !== "string") {
523
+ throw new Error(`diarist source catalog is missing runDirectory (${path})`);
524
+ }
525
+ if (typeof record.projectRoot !== "string") {
526
+ throw new Error(`diarist source catalog is missing projectRoot (${path})`);
527
+ }
528
+ if (
529
+ record.ticketNumber !== undefined &&
530
+ (typeof record.ticketNumber !== "number" ||
531
+ !Number.isSafeInteger(record.ticketNumber) ||
532
+ record.ticketNumber < 1)
533
+ ) {
534
+ throw new Error(`diarist source catalog ticketNumber is not a safe ticket (${path})`);
371
535
  }
372
536
  if (!Array.isArray(record.candidates)) {
373
537
  throw new Error(`diarist source catalog is missing candidates (${path})`);
@@ -426,7 +590,13 @@ export async function commitDiaristSelections(input: {
426
590
  readonly catalog: DiaristSourceCatalog;
427
591
  readonly selections: readonly DiaristSelection[];
428
592
  }): Promise<DiaristCommitFacts> {
429
- const { ticketNumber, cwd } = input.catalog;
593
+ const ticketNumber = input.catalog.ticketNumber;
594
+ if (ticketNumber === undefined) {
595
+ throw new Error(
596
+ "commitDiaristSelections requires a bound ticketNumber on the catalog",
597
+ );
598
+ }
599
+ const { cwd } = input.catalog;
430
600
  const homeOpt = input.catalog.home === undefined ? {} : { home: input.catalog.home };
431
601
  const volumePaths = ensureTicketProvenanceVolume(ticketNumber, cwd, input.catalog.home);
432
602
 
@@ -228,7 +228,7 @@ export function validateAcceptedDetails(
228
228
  [GATEKEEPER_OUTPUT_TOOL_NAME]: ["dispatch", "pass"],
229
229
  [NAVIGATOR_OUTPUT_TOOL_NAME]: ["advice"],
230
230
  [AUDITOR_OUTPUT_TOOL_NAME]: ["pass", "bounce", "escalate"],
231
- [DIARIST_OUTPUT_TOOL_NAME]: ["completed"],
231
+ [DIARIST_OUTPUT_TOOL_NAME]: ["completed", "escalate"],
232
232
  };
233
233
  const collectorDiscriminator = toolName === COLLECTOR_OUTPUT_TOOL && Array.isArray(candidate?.groups);
234
234
  const baseDiscriminator = discriminator;
@@ -21,7 +21,6 @@ import {
21
21
  buildCoderTransportPrompt,
22
22
  type AdmittedCoderInvocation,
23
23
  } from "./invocation.ts";
24
- import { resolveSeatTicketBinding } from "./seat-ticket-binding.ts";
25
24
  import {
26
25
  buildResumeContinuationPrompt,
27
26
  loadResumableCoderRun,
@@ -199,12 +198,7 @@ export async function runPublicCoder(
199
198
  }),
200
199
  },
201
200
  }),
202
- adapters: {
203
- ...coderAdapters(methodProvenance),
204
- beforeDispatch: async (admittedSeat) => {
205
- await resolveSeatTicketBinding(admittedSeat, env);
206
- },
207
- },
201
+ adapters: coderAdapters(methodProvenance),
208
202
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
209
203
  });
210
204
  }