@akagilnc/pi-workflow-roles 0.1.3652 → 0.1.3663

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.
@@ -1,302 +0,0 @@
1
- /**
2
- * 起居郎 pre-court ticket resolution (#582 / diarist-resolves-ticket-llm-layer).
3
- * Unbound countersign: LLM typed assertion over accepted instruction, then two
4
- * mechanical checks on N (complete decimal number in instruction; live GitHub issue).
5
- * Verification failure is controlled failure — never washed to true-unbound.
6
- */
7
- import { existsSync, readFileSync } from "node:fs";
8
- import { join } from "node:path";
9
- import { fileURLToPath } from "node:url";
10
-
11
- import {
12
- createGhApiRunner,
13
- projectGhIssueBody,
14
- type GhApiRunner,
15
- } from "./collector-github.ts";
16
- import {
17
- ENGINE_DETOUR_STAGED_PROMPT_TOKEN,
18
- runEngineDetourOnce,
19
- } from "./engine-detour.ts";
20
-
21
- export const DIARIST_RESOLVE_TICKET_METHOD_RELATIVE =
22
- "resources/diarist-resolve-ticket.md" as const;
23
-
24
- export type DiaristTicketAssertion =
25
- | { readonly kind: "ticket"; readonly ticketNumber: number }
26
- | { readonly kind: "true-unbound" };
27
-
28
- export type DiaristTicketResolution = DiaristTicketAssertion;
29
-
30
- export type DiaristTicketResolver = (input: {
31
- readonly instruction: string;
32
- readonly signal?: AbortSignal;
33
- }) => Promise<DiaristTicketAssertion>;
34
-
35
- export type TicketExistenceChecker = (input: {
36
- readonly owner: string;
37
- readonly repo: string;
38
- readonly ticketNumber: number;
39
- readonly signal?: AbortSignal;
40
- }) => Promise<boolean>;
41
-
42
- export type DiaristTicketResolutionReason =
43
- | "empty-stdout"
44
- | "unparseable-json"
45
- | "not-object"
46
- | "assertion-missing"
47
- | "assertion-uninterpretable"
48
- | "number-not-in-instruction"
49
- | "ticket-missing"
50
- | "ticket-is-pull-request"
51
- | "ticket-unavailable"
52
- | "ticket-invalid"
53
- | "origin-unresolved"
54
- | "engine-failed";
55
-
56
- export class DiaristTicketResolutionError extends Error {
57
- readonly code = "diarist-ticket-resolution" as const;
58
- readonly reason: DiaristTicketResolutionReason;
59
- constructor(
60
- reason: DiaristTicketResolutionReason,
61
- message: string,
62
- options?: ErrorOptions,
63
- ) {
64
- super(message, options);
65
- this.name = "DiaristTicketResolutionError";
66
- this.reason = reason;
67
- }
68
- }
69
-
70
- export function resolveDiaristResolveTicketMethodPath(
71
- packageRoot: string = fileURLToPath(new URL("..", import.meta.url)),
72
- ): string {
73
- return join(packageRoot, DIARIST_RESOLVE_TICKET_METHOD_RELATIVE);
74
- }
75
-
76
- /**
77
- * Decision-key exact identity: complete decimal number N appears in instruction.
78
- * A longer number's digit substring (e.g. 82 inside #582) is not N.
79
- */
80
- export function instructionContainsTicketNumber(
81
- instruction: string,
82
- ticketNumber: number,
83
- ): boolean {
84
- if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) return false;
85
- // Complete number token: not preceded or followed by another digit.
86
- return new RegExp(`(?<!\\d)${String(ticketNumber)}(?!\\d)`).test(instruction);
87
- }
88
-
89
- /** Sole stdout shapes: {assertion:"ticket",ticketNumber:N} | {assertion:"true-unbound"}. */
90
- export function parseDiaristTicketResolverStdout(
91
- stdout: string,
92
- ): DiaristTicketAssertion {
93
- const trimmed = stdout.trim();
94
- if (trimmed.length === 0) {
95
- throw new DiaristTicketResolutionError(
96
- "empty-stdout",
97
- "diarist ticket resolver stdout is empty",
98
- );
99
- }
100
- let parsed: unknown;
101
- try {
102
- parsed = JSON.parse(trimmed);
103
- } catch (error) {
104
- throw new DiaristTicketResolutionError(
105
- "unparseable-json",
106
- "diarist ticket resolver stdout is not JSON",
107
- { cause: error },
108
- );
109
- }
110
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
111
- throw new DiaristTicketResolutionError(
112
- "not-object",
113
- "diarist ticket resolver stdout must be one JSON object",
114
- );
115
- }
116
- const record = parsed as Record<string, unknown>;
117
- if (!Object.prototype.hasOwnProperty.call(record, "assertion")) {
118
- throw new DiaristTicketResolutionError(
119
- "assertion-missing",
120
- "diarist ticket resolver stdout missing assertion",
121
- );
122
- }
123
- if (record.assertion === "true-unbound") return { kind: "true-unbound" };
124
- const n = record.ticketNumber;
125
- if (
126
- record.assertion === "ticket" &&
127
- typeof n === "number" &&
128
- Number.isSafeInteger(n) &&
129
- n >= 1
130
- ) {
131
- return { kind: "ticket", ticketNumber: n };
132
- }
133
- throw new DiaristTicketResolutionError(
134
- "assertion-uninterpretable",
135
- "diarist ticket resolver assertion is not ticket|true-unbound with safe ticketNumber",
136
- );
137
- }
138
-
139
- const HERMES_RESOLVER_TOOLSET = "context_engine" as const;
140
-
141
- /** Hermes resolver: method bytes + instruction only; empty toolset; staged prompt. */
142
- export function createHermesDiaristTicketResolver(options: {
143
- readonly executable?: string;
144
- readonly cwd?: string;
145
- readonly env?: NodeJS.ProcessEnv;
146
- readonly packageRoot?: string;
147
- } = {}): DiaristTicketResolver {
148
- const methodPath = resolveDiaristResolveTicketMethodPath(options.packageRoot);
149
- if (!existsSync(methodPath)) {
150
- throw new Error(`diarist resolve-ticket method material missing (${methodPath})`);
151
- }
152
- const method = readFileSync(methodPath, "utf8");
153
- if (method.trim().length === 0) {
154
- throw new Error(`diarist resolve-ticket method material is empty (${methodPath})`);
155
- }
156
- const executable = options.executable ?? "hermes";
157
- return async (input) => {
158
- const argv = [
159
- executable,
160
- "chat",
161
- "--query-file",
162
- ENGINE_DETOUR_STAGED_PROMPT_TOKEN,
163
- "-Q",
164
- "--no-restore-cwd",
165
- "--ignore-rules",
166
- "-t",
167
- HERMES_RESOLVER_TOOLSET,
168
- ];
169
- let result: Awaited<ReturnType<typeof runEngineDetourOnce>>;
170
- try {
171
- result = await runEngineDetourOnce({
172
- argv,
173
- stagedPrompt: JSON.stringify({ method, instruction: input.instruction }),
174
- cwd: options.cwd ?? process.cwd(),
175
- ...(options.env === undefined ? {} : { env: options.env }),
176
- ...(input.signal === undefined ? {} : { signal: input.signal }),
177
- });
178
- } catch (error) {
179
- throw new DiaristTicketResolutionError(
180
- "engine-failed",
181
- "diarist ticket resolver engine failed to launch",
182
- { cause: error },
183
- );
184
- }
185
- if (result.code !== 0) {
186
- throw new DiaristTicketResolutionError(
187
- "engine-failed",
188
- `diarist ticket resolver engine exited ${result.code}: ${result.stderr.slice(0, 500)}`,
189
- );
190
- }
191
- return parseDiaristTicketResolverStdout(result.stdout);
192
- };
193
- }
194
-
195
- /** Live ticket check over shared gh issue-body projection. */
196
- export function createGhTicketExistenceChecker(options?: {
197
- readonly runner?: GhApiRunner;
198
- }): TicketExistenceChecker {
199
- const runner = options?.runner ?? createGhApiRunner();
200
- return async (input) => {
201
- const projected = await projectGhIssueBody(runner, {
202
- owner: input.owner,
203
- repo: input.repo,
204
- ticketNumber: input.ticketNumber,
205
- ...(input.signal === undefined ? {} : { signal: input.signal }),
206
- });
207
- if (projected.status === "available") return true;
208
- if (
209
- projected.status === "unavailable"
210
- && projected.reason === "http-non-2xx"
211
- && projected.httpStatus === 404
212
- ) {
213
- return false;
214
- }
215
- if (projected.status === "unavailable" && projected.reason === "pull-request") {
216
- throw new DiaristTicketResolutionError(
217
- "ticket-is-pull-request",
218
- `ticket #${input.ticketNumber} resolves to a pull request, not an issue`,
219
- { cause: projected },
220
- );
221
- }
222
- if (projected.status === "unavailable") {
223
- throw new DiaristTicketResolutionError(
224
- "ticket-unavailable",
225
- `ticket verification unavailable for ${input.owner}/${input.repo}#${input.ticketNumber}`,
226
- { cause: projected },
227
- );
228
- }
229
- throw new DiaristTicketResolutionError(
230
- "ticket-invalid",
231
- `ticket verification returned an invalid issue payload for ${input.owner}/${input.repo}#${input.ticketNumber}`,
232
- { cause: projected },
233
- );
234
- };
235
- }
236
-
237
- /** Mechanical verify after LLM assertion. true-unbound passes; ticket needs both checks. */
238
- export async function verifyDiaristTicketAssertion(input: {
239
- readonly assertion: DiaristTicketAssertion;
240
- readonly instruction: string;
241
- readonly origin: { readonly owner: string; readonly repo: string } | undefined;
242
- readonly checkExistence: TicketExistenceChecker;
243
- readonly signal?: AbortSignal;
244
- }): Promise<DiaristTicketResolution> {
245
- if (input.assertion.kind === "true-unbound") return input.assertion;
246
- const n = input.assertion.ticketNumber;
247
- if (!instructionContainsTicketNumber(input.instruction, n)) {
248
- throw new DiaristTicketResolutionError(
249
- "number-not-in-instruction",
250
- `diarist ticket assertion #${n} complete decimal number does not appear in accepted instruction`,
251
- );
252
- }
253
- if (input.origin === undefined) {
254
- throw new DiaristTicketResolutionError(
255
- "origin-unresolved",
256
- `diarist ticket assertion #${n} requires a resolvable github.com origin remote`,
257
- );
258
- }
259
- let exists: boolean;
260
- try {
261
- exists = await input.checkExistence({
262
- owner: input.origin.owner,
263
- repo: input.origin.repo,
264
- ticketNumber: n,
265
- ...(input.signal === undefined ? {} : { signal: input.signal }),
266
- });
267
- } catch (error) {
268
- if (error instanceof DiaristTicketResolutionError) throw error;
269
- throw new DiaristTicketResolutionError(
270
- "ticket-unavailable",
271
- `diarist ticket assertion #${n} live verification failed`,
272
- { cause: error },
273
- );
274
- }
275
- if (!exists) {
276
- throw new DiaristTicketResolutionError(
277
- "ticket-missing",
278
- `diarist ticket assertion #${n} does not exist as a live issue on ${input.origin.owner}/${input.origin.repo}`,
279
- );
280
- }
281
- return input.assertion;
282
- }
283
-
284
- export async function resolveDiaristTicketFromInstruction(input: {
285
- readonly instruction: string;
286
- readonly origin: { readonly owner: string; readonly repo: string } | undefined;
287
- readonly resolver: DiaristTicketResolver;
288
- readonly checkExistence: TicketExistenceChecker;
289
- readonly signal?: AbortSignal;
290
- }): Promise<DiaristTicketResolution> {
291
- const assertion = await input.resolver({
292
- instruction: input.instruction,
293
- ...(input.signal === undefined ? {} : { signal: input.signal }),
294
- });
295
- return verifyDiaristTicketAssertion({
296
- assertion,
297
- instruction: input.instruction,
298
- origin: input.origin,
299
- checkExistence: input.checkExistence,
300
- ...(input.signal === undefined ? {} : { signal: input.signal }),
301
- });
302
- }