@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.
- package/README.md +0 -2
- package/dist/acp-host/production-host.js +1100 -1256
- package/dist/public-cli/case-dossier-delivery.js +43 -0
- package/dist/public-cli/inspector-run.js +9 -6
- package/dist/public-cli/instruction-seat-run.js +24 -24
- package/dist/public-cli/invocation.js +2 -24
- package/dist/public-cli/main.js +1936 -2189
- package/dist/public-cli/notary-run.js +20 -5
- package/dist/public-cli/post-admission.js +61 -42
- package/dist/public-cli/run-lifecycle.js +39 -20
- package/dist/public-cli/seat-ticket-binding.js +93 -70
- package/package.json +1 -1
- package/src/public-cli/case-dossier-delivery.ts +51 -0
- package/src/public-cli/countersign-run.ts +14 -17
- package/src/public-cli/diarist-run.ts +11 -13
- package/src/public-cli/inspector-run.ts +9 -10
- package/src/public-cli/instruction-seat-run.ts +28 -35
- package/src/public-cli/invocation.ts +4 -46
- package/src/public-cli/notary-run.ts +18 -6
- package/src/public-cli/post-admission.ts +78 -37
- package/src/public-cli/run-lifecycle.ts +46 -27
- package/src/public-cli/seat-ticket-binding.ts +108 -104
- package/dist/diarist-ticket-resolution.js +0 -177
- package/resources/diarist-resolve-ticket.md +0 -39
- package/src/diarist-ticket-resolution.ts +0 -302
|
@@ -163,10 +163,11 @@ function resumeRereadInstruction(materialPath: string, handbook: boolean): strin
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
166
|
+
* Resume-only material lines (#736 ticket exemption to ADR 0073 §3).
|
|
167
|
+
* Neutral `- <absolute path>` pointers already on the continuation
|
|
168
|
+
* (handbook from appendEngineSessionMaterial; frozen attachments from
|
|
169
|
+
* instruction-seat transport) become reread instructions.
|
|
170
|
+
* Handbook keeps the argv suffix; other materials are path-only.
|
|
170
171
|
* First-round delivery is unchanged; never pastes material body.
|
|
171
172
|
*/
|
|
172
173
|
export function instructResumeHandbookRead(
|
|
@@ -188,9 +189,19 @@ export function instructResumeHandbookRead(
|
|
|
188
189
|
.join("\n");
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
/** Append a live-path reread instruction when the pointer is not already on the continuation. */
|
|
193
|
+
export function appendResumeMaterialReread(
|
|
194
|
+
prompt: string,
|
|
195
|
+
materialPath: string,
|
|
196
|
+
): string {
|
|
197
|
+
const instruction = resumeRereadInstruction(materialPath, false);
|
|
198
|
+
const lines = prompt.split("\n");
|
|
199
|
+
if (lines.includes(instruction)) return prompt;
|
|
200
|
+
return prompt.length === 0 ? instruction : `${prompt}\n${instruction}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
191
203
|
/**
|
|
192
|
-
* Unique continuation-prompt selector for manual/auto
|
|
193
|
-
* (#471 / #600 / #736). Not used for 审核循环 same-ticket summons (#755).
|
|
204
|
+
* Unique continuation-prompt selector for manual/auto resume (#471 / #600 / #736).
|
|
194
205
|
* Message present → base bytes unchanged; absent → package transport envelope.
|
|
195
206
|
* When engine material is present, append structured engine coordinates then
|
|
196
207
|
* rewrite absolute material pointer lines into 重新读 instructions. Zero parse,
|
|
@@ -1128,6 +1139,34 @@ export async function findLatestRunIdForSeatTicket(input: {
|
|
|
1128
1139
|
return best;
|
|
1129
1140
|
}
|
|
1130
1141
|
|
|
1142
|
+
/**
|
|
1143
|
+
* Ticket identities this book's retained runs already record (#709).
|
|
1144
|
+
* Read-only reuse surface for seat ticket binding — no ticket number is minted here.
|
|
1145
|
+
*/
|
|
1146
|
+
export async function collectBookRunTicketNumbers(input: {
|
|
1147
|
+
readonly home: string;
|
|
1148
|
+
readonly bookKey: string;
|
|
1149
|
+
}): Promise<ReadonlySet<number>> {
|
|
1150
|
+
const ledgerHome = resolveActivationLedgerHome(input.home);
|
|
1151
|
+
const runsDir = join(
|
|
1152
|
+
activationBookDirectory(ledgerHome, input.bookKey),
|
|
1153
|
+
"runs",
|
|
1154
|
+
);
|
|
1155
|
+
let entries: string[];
|
|
1156
|
+
try {
|
|
1157
|
+
entries = await readdir(runsDir);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
if (errorCodeOf(error) === "ENOENT") return new Set();
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
const known = new Set<number>();
|
|
1163
|
+
for (const entry of entries) {
|
|
1164
|
+
const ticketNumber = await readRunTicketNumber(join(runsDir, entry));
|
|
1165
|
+
if (ticketNumber !== undefined) known.add(ticketNumber);
|
|
1166
|
+
}
|
|
1167
|
+
return known;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1131
1170
|
type LoadedAdmittedRequestFields = {
|
|
1132
1171
|
readonly instruction: string;
|
|
1133
1172
|
readonly instructionEmpty: boolean;
|
|
@@ -1143,8 +1182,6 @@ type LoadedAdmittedRequestFields = {
|
|
|
1143
1182
|
readonly derived?: DerivedMergerEnvelope;
|
|
1144
1183
|
readonly correlationId?: string;
|
|
1145
1184
|
readonly ticketNumber?: number;
|
|
1146
|
-
/** Durable seat LLM true-unbound conclusion (#635); restored on resume. */
|
|
1147
|
-
readonly ticketResolution?: "true-unbound";
|
|
1148
1185
|
/** Collector — admitted repository/PR identity restored on resume (#633). */
|
|
1149
1186
|
readonly prNumber?: number;
|
|
1150
1187
|
readonly repository?: string;
|
|
@@ -1168,7 +1205,6 @@ function parsePersistedTicketIdentity(
|
|
|
1168
1205
|
): {
|
|
1169
1206
|
correlationId?: string;
|
|
1170
1207
|
ticketNumber?: number;
|
|
1171
|
-
ticketResolution?: "true-unbound";
|
|
1172
1208
|
} {
|
|
1173
1209
|
const correlationId =
|
|
1174
1210
|
typeof record.correlationId === "string" && record.correlationId.trim() !== ""
|
|
@@ -1180,26 +1216,19 @@ function parsePersistedTicketIdentity(
|
|
|
1180
1216
|
record.ticketNumber >= 1
|
|
1181
1217
|
? record.ticketNumber
|
|
1182
1218
|
: undefined;
|
|
1183
|
-
const ticketResolution =
|
|
1184
|
-
record.ticketResolution === "true-unbound" ? "true-unbound" : undefined;
|
|
1185
1219
|
return {
|
|
1186
1220
|
...(correlationId === undefined ? {} : { correlationId }),
|
|
1187
1221
|
...(ticketNumber === undefined ? {} : { ticketNumber }),
|
|
1188
|
-
...(ticketResolution === undefined ? {} : { ticketResolution }),
|
|
1189
1222
|
};
|
|
1190
1223
|
}
|
|
1191
1224
|
|
|
1192
1225
|
function restoredTicketFields(fields: LoadedAdmittedRequestFields): {
|
|
1193
1226
|
correlationId?: string;
|
|
1194
1227
|
ticketNumber?: number;
|
|
1195
|
-
ticketResolution?: "true-unbound";
|
|
1196
1228
|
} {
|
|
1197
1229
|
return {
|
|
1198
1230
|
...(fields.correlationId === undefined ? {} : { correlationId: fields.correlationId }),
|
|
1199
1231
|
...(fields.ticketNumber === undefined ? {} : { ticketNumber: fields.ticketNumber }),
|
|
1200
|
-
...(fields.ticketResolution === undefined
|
|
1201
|
-
? {}
|
|
1202
|
-
: { ticketResolution: fields.ticketResolution }),
|
|
1203
1232
|
};
|
|
1204
1233
|
}
|
|
1205
1234
|
|
|
@@ -1249,7 +1278,6 @@ async function loadResumableRunRecord(
|
|
|
1249
1278
|
let derived: DerivedMergerEnvelope | undefined;
|
|
1250
1279
|
let correlationId: string | undefined;
|
|
1251
1280
|
let ticketNumber: number | undefined;
|
|
1252
|
-
let ticketResolution: "true-unbound" | undefined;
|
|
1253
1281
|
let prNumber: number | undefined;
|
|
1254
1282
|
let repository: string | undefined;
|
|
1255
1283
|
let repositoryDisplay: string | undefined;
|
|
@@ -1409,7 +1437,6 @@ async function loadResumableRunRecord(
|
|
|
1409
1437
|
const fromAdmitted = parsePersistedTicketIdentity(record);
|
|
1410
1438
|
correlationId = fromAdmitted.correlationId;
|
|
1411
1439
|
ticketNumber = fromAdmitted.ticketNumber;
|
|
1412
|
-
ticketResolution = fromAdmitted.ticketResolution;
|
|
1413
1440
|
}
|
|
1414
1441
|
} catch (error) {
|
|
1415
1442
|
// Preserve unique --authority-ref grammar failures; do not collapse to unreadable.
|
|
@@ -1439,17 +1466,10 @@ async function loadResumableRunRecord(
|
|
|
1439
1466
|
: {}),
|
|
1440
1467
|
};
|
|
1441
1468
|
}
|
|
1442
|
-
if (
|
|
1443
|
-
correlationId === undefined ||
|
|
1444
|
-
ticketNumber === undefined ||
|
|
1445
|
-
ticketResolution === undefined
|
|
1446
|
-
) {
|
|
1469
|
+
if (correlationId === undefined || ticketNumber === undefined) {
|
|
1447
1470
|
const fromInvocation = parsePersistedTicketIdentity(rec);
|
|
1448
1471
|
if (correlationId === undefined) correlationId = fromInvocation.correlationId;
|
|
1449
1472
|
if (ticketNumber === undefined) ticketNumber = fromInvocation.ticketNumber;
|
|
1450
|
-
if (ticketResolution === undefined) {
|
|
1451
|
-
ticketResolution = fromInvocation.ticketResolution;
|
|
1452
|
-
}
|
|
1453
1473
|
}
|
|
1454
1474
|
}
|
|
1455
1475
|
} catch (error) {
|
|
@@ -1485,7 +1505,6 @@ async function loadResumableRunRecord(
|
|
|
1485
1505
|
...(derived === undefined ? {} : { derived }),
|
|
1486
1506
|
...(correlationId === undefined ? {} : { correlationId }),
|
|
1487
1507
|
...(ticketNumber === undefined ? {} : { ticketNumber }),
|
|
1488
|
-
...(ticketResolution === undefined ? {} : { ticketResolution }),
|
|
1489
1508
|
...(prNumber === undefined ? {} : { prNumber }),
|
|
1490
1509
|
...(repository === undefined ? {} : { repository }),
|
|
1491
1510
|
...(repositoryDisplay === undefined ? {} : { repositoryDisplay }),
|
|
@@ -1,151 +1,155 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared
|
|
3
|
-
* One path: unbound admission →
|
|
2
|
+
* Shared ticket identity seam for public court seats (#635 / #637 / #709 / #747).
|
|
3
|
+
* One path: unbound admission → reuse a ticket number this book already records
|
|
4
|
+
* (retained run pages + 起居录 volumes) → bindAdmittedTicketNumber.
|
|
5
|
+
* ADR 0081 `reuse-case-ticket-without-extra-llm`: no seat-side model call, no
|
|
6
|
+
* second ticket-number source of truth, no minting a number from human titles.
|
|
7
|
+
* Nothing matched is not 真无票 — the run simply stays unbound, which is lawful.
|
|
4
8
|
* No CLI --ticket and no attachment frontmatter binding.
|
|
5
|
-
* #
|
|
9
|
+
* #747: officer same-parent resume also lives here (shared seam).
|
|
6
10
|
*/
|
|
7
|
-
import {
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
|
|
8
13
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
type DiaristTicketResolution,
|
|
14
|
-
} from "../diarist-ticket-resolution.ts";
|
|
15
|
-
import { resolveDiaristGithubOrigin } from "../diarist.ts";
|
|
14
|
+
ActivationGitRepositoryRequiredError,
|
|
15
|
+
resolveBookKeyFromGit,
|
|
16
|
+
} from "../activation-ledger-git.ts";
|
|
17
|
+
import { resolveTicketProvenanceVolume } from "../ticket-provenance.ts";
|
|
16
18
|
import {
|
|
17
19
|
bindAdmittedTicketNumber,
|
|
18
|
-
recordTrueUnboundTicketResolution,
|
|
19
20
|
type AdmittedRoleInvocation,
|
|
20
21
|
} from "./invocation.ts";
|
|
21
22
|
import {
|
|
23
|
+
collectBookRunTicketNumbers,
|
|
22
24
|
findLatestRunIdForSeatTicket,
|
|
23
25
|
type RoleRunRecord,
|
|
24
26
|
type SameTicketSummonsMaterials,
|
|
25
27
|
} from "./run-lifecycle.ts";
|
|
26
28
|
|
|
27
29
|
export type SeatTicketBindingEnv = {
|
|
28
|
-
readonly
|
|
30
|
+
readonly home: string;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* admit and settle the failure inside the post-admission controlled path —
|
|
36
|
-
* bare throw before admit would skip terminal settlement (失败诚实, no wash).
|
|
37
|
-
*/
|
|
38
|
-
export type InstructionTicketProbe =
|
|
39
|
-
| { readonly kind: "resolved"; readonly resolution: DiaristTicketResolution }
|
|
40
|
-
| { readonly kind: "failed"; readonly error: DiaristTicketResolutionError };
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Instruction → ticket resolution without admission (#635 / #637).
|
|
44
|
-
* Used to locate a prior same-ticket run before minting a new one.
|
|
45
|
-
* Throws on resolution failure — prefer probeInstructionTicket at seat entry.
|
|
34
|
+
* Complete decimal ticket tokens in an instruction.
|
|
35
|
+
* A digit run is one token: `82` inside `#582` is not 82, and a leading zero
|
|
36
|
+
* makes the run a different literal than the ticket number it would parse to.
|
|
46
37
|
*/
|
|
47
|
-
export
|
|
38
|
+
export function instructionTicketTokens(
|
|
48
39
|
instruction: string,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return resolveDiaristTicketFromInstruction({
|
|
59
|
-
instruction,
|
|
60
|
-
origin,
|
|
61
|
-
resolver,
|
|
62
|
-
checkExistence,
|
|
63
|
-
});
|
|
40
|
+
): readonly number[] {
|
|
41
|
+
const tokens = new Set<number>();
|
|
42
|
+
for (const run of instruction.match(/\d+/g) ?? []) {
|
|
43
|
+
if (run.startsWith("0")) continue;
|
|
44
|
+
const parsed = Number(run);
|
|
45
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) continue;
|
|
46
|
+
tokens.add(parsed);
|
|
47
|
+
}
|
|
48
|
+
return [...tokens];
|
|
64
49
|
}
|
|
65
50
|
|
|
66
51
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
52
|
+
* Ticket identity this summons reuses, or undefined when none is unambiguous.
|
|
53
|
+
* Known identities come only from records this book already holds: 起居录 volume
|
|
54
|
+
* partitions and ticket numbers on retained run pages. Zero or several known
|
|
55
|
+
* tokens in the instruction leave the run unbound — never guess, never fail.
|
|
69
56
|
*/
|
|
70
|
-
export async function
|
|
71
|
-
instruction: string
|
|
72
|
-
projectRoot: string
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
57
|
+
export async function resolveKnownTicketNumber(input: {
|
|
58
|
+
readonly instruction: string;
|
|
59
|
+
readonly projectRoot: string;
|
|
60
|
+
readonly home: string;
|
|
61
|
+
readonly bookKey?: string;
|
|
62
|
+
}): Promise<number | undefined> {
|
|
63
|
+
const tokens = instructionTicketTokens(input.instruction);
|
|
64
|
+
if (tokens.length === 0) return undefined;
|
|
65
|
+
const known: number[] = [];
|
|
66
|
+
let runTickets: ReadonlySet<number> | undefined;
|
|
67
|
+
for (const token of tokens) {
|
|
68
|
+
const volume = resolveTicketProvenanceVolume(
|
|
69
|
+
token,
|
|
70
|
+
input.projectRoot,
|
|
71
|
+
input.home,
|
|
80
72
|
);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return { kind: "failed", error };
|
|
73
|
+
if (existsSync(volume.volumeDir)) {
|
|
74
|
+
known.push(token);
|
|
75
|
+
continue;
|
|
85
76
|
}
|
|
86
|
-
|
|
77
|
+
if (runTickets === undefined) {
|
|
78
|
+
runTickets = await readBookRunTickets(input);
|
|
79
|
+
}
|
|
80
|
+
if (runTickets.has(token)) known.push(token);
|
|
87
81
|
}
|
|
82
|
+
return known.length === 1 ? known[0] : undefined;
|
|
88
83
|
}
|
|
89
84
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Diarist first-summons identity (ADR 0081 `initial-court-ticket-supplied`).
|
|
87
|
+
* Prefer a book-known reuse; when the book has no record yet, the sole complete
|
|
88
|
+
* decimal token in the caller's dispatch is the supplied ticket — the working
|
|
89
|
+
* 起居郎 round establishes the volume. Several tokens or none stay unbound.
|
|
90
|
+
* Not a second ticket-number source of truth and not an extra recognizer call.
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveDiaristSummonsTicketNumber(input: {
|
|
93
|
+
readonly instruction: string;
|
|
94
|
+
readonly projectRoot: string;
|
|
95
|
+
readonly home: string;
|
|
96
|
+
readonly bookKey?: string;
|
|
97
|
+
}): Promise<number | undefined> {
|
|
98
|
+
const known = await resolveKnownTicketNumber(input);
|
|
99
|
+
if (known !== undefined) return known;
|
|
100
|
+
const tokens = instructionTicketTokens(input.instruction);
|
|
101
|
+
return tokens.length === 1 ? tokens[0] : undefined;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
105
|
+
* Ticket numbers on this book's retained runs.
|
|
106
|
+
* A directory with no book (not a git repository) simply holds no run history —
|
|
107
|
+
* admission owns that rejection face, this lookup does not pre-empt it.
|
|
102
108
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
async function readBookRunTickets(input: {
|
|
110
|
+
readonly projectRoot: string;
|
|
111
|
+
readonly home: string;
|
|
112
|
+
readonly bookKey?: string;
|
|
113
|
+
}): Promise<ReadonlySet<number>> {
|
|
114
|
+
let bookKey: string;
|
|
115
|
+
try {
|
|
116
|
+
bookKey = input.bookKey ?? resolveBookKeyFromGit(input.projectRoot);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error instanceof ActivationGitRepositoryRequiredError) return new Set();
|
|
119
|
+
throw error;
|
|
111
120
|
}
|
|
121
|
+
return await collectBookRunTicketNumbers({ home: input.home, bookKey });
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
* Failed probes rethrow — caller must be inside the controlled-failure boundary
|
|
117
|
-
* (post-admission beforeDispatch), never before admit/markRunning.
|
|
118
|
-
*/
|
|
119
|
-
export async function applyInstructionTicketProbe(
|
|
124
|
+
/** Bind a reused ticket number onto an admission that is still unbound. */
|
|
125
|
+
export async function bindReusedTicketNumber(
|
|
120
126
|
admitted: AdmittedRoleInvocation,
|
|
121
|
-
|
|
127
|
+
ticketNumber: number | undefined,
|
|
122
128
|
): Promise<void> {
|
|
123
|
-
if (
|
|
124
|
-
|
|
129
|
+
if (ticketNumber === undefined) return;
|
|
130
|
+
if (admitted.ticketNumber !== undefined) return;
|
|
131
|
+
await bindAdmittedTicketNumber(admitted, ticketNumber);
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
/**
|
|
128
|
-
*
|
|
129
|
-
* Already-
|
|
130
|
-
*
|
|
131
|
-
*
|
|
135
|
+
* Sole seat disposition for an unbound admission (#635 / #709).
|
|
136
|
+
* Already-bound admissions short-circuit — resume keeps the identity it has.
|
|
137
|
+
* Used by seats that do not pre-resolve for same-ticket resume; the seats that
|
|
138
|
+
* do (countersign / inspector / diarist) reuse the number they already resolved.
|
|
132
139
|
*/
|
|
133
140
|
export async function resolveSeatTicketBinding(
|
|
134
141
|
admitted: AdmittedRoleInvocation,
|
|
135
|
-
env: SeatTicketBindingEnv
|
|
136
|
-
): Promise<
|
|
137
|
-
if (admitted.ticketNumber !== undefined) return
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
);
|
|
147
|
-
await applyTicketResolution(admitted, resolution);
|
|
148
|
-
return resolution;
|
|
142
|
+
env: SeatTicketBindingEnv,
|
|
143
|
+
): Promise<number | undefined> {
|
|
144
|
+
if (admitted.ticketNumber !== undefined) return admitted.ticketNumber;
|
|
145
|
+
const ticketNumber = await resolveKnownTicketNumber({
|
|
146
|
+
instruction: admitted.instruction,
|
|
147
|
+
projectRoot: admitted.projectRoot,
|
|
148
|
+
home: env.home,
|
|
149
|
+
bookKey: admitted.bookKey,
|
|
150
|
+
});
|
|
151
|
+
await bindReusedTicketNumber(admitted, ticketNumber);
|
|
152
|
+
return ticketNumber;
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/**
|
|
@@ -1,177 +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
|
-
import { createGhApiRunner, projectGhIssueBody, } from "./collector-github.js";
|
|
11
|
-
import { ENGINE_DETOUR_STAGED_PROMPT_TOKEN, runEngineDetourOnce, } from "./engine-detour.js";
|
|
12
|
-
export const DIARIST_RESOLVE_TICKET_METHOD_RELATIVE = "resources/diarist-resolve-ticket.md";
|
|
13
|
-
export class DiaristTicketResolutionError extends Error {
|
|
14
|
-
code = "diarist-ticket-resolution";
|
|
15
|
-
reason;
|
|
16
|
-
constructor(reason, message, options) {
|
|
17
|
-
super(message, options);
|
|
18
|
-
this.name = "DiaristTicketResolutionError";
|
|
19
|
-
this.reason = reason;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export function resolveDiaristResolveTicketMethodPath(packageRoot = fileURLToPath(new URL("..", import.meta.url))) {
|
|
23
|
-
return join(packageRoot, DIARIST_RESOLVE_TICKET_METHOD_RELATIVE);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Decision-key exact identity: complete decimal number N appears in instruction.
|
|
27
|
-
* A longer number's digit substring (e.g. 82 inside #582) is not N.
|
|
28
|
-
*/
|
|
29
|
-
export function instructionContainsTicketNumber(instruction, ticketNumber) {
|
|
30
|
-
if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1)
|
|
31
|
-
return false;
|
|
32
|
-
// Complete number token: not preceded or followed by another digit.
|
|
33
|
-
return new RegExp(`(?<!\\d)${String(ticketNumber)}(?!\\d)`).test(instruction);
|
|
34
|
-
}
|
|
35
|
-
/** Sole stdout shapes: {assertion:"ticket",ticketNumber:N} | {assertion:"true-unbound"}. */
|
|
36
|
-
export function parseDiaristTicketResolverStdout(stdout) {
|
|
37
|
-
const trimmed = stdout.trim();
|
|
38
|
-
if (trimmed.length === 0) {
|
|
39
|
-
throw new DiaristTicketResolutionError("empty-stdout", "diarist ticket resolver stdout is empty");
|
|
40
|
-
}
|
|
41
|
-
let parsed;
|
|
42
|
-
try {
|
|
43
|
-
parsed = JSON.parse(trimmed);
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
throw new DiaristTicketResolutionError("unparseable-json", "diarist ticket resolver stdout is not JSON", { cause: error });
|
|
47
|
-
}
|
|
48
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
49
|
-
throw new DiaristTicketResolutionError("not-object", "diarist ticket resolver stdout must be one JSON object");
|
|
50
|
-
}
|
|
51
|
-
const record = parsed;
|
|
52
|
-
if (!Object.prototype.hasOwnProperty.call(record, "assertion")) {
|
|
53
|
-
throw new DiaristTicketResolutionError("assertion-missing", "diarist ticket resolver stdout missing assertion");
|
|
54
|
-
}
|
|
55
|
-
if (record.assertion === "true-unbound")
|
|
56
|
-
return { kind: "true-unbound" };
|
|
57
|
-
const n = record.ticketNumber;
|
|
58
|
-
if (record.assertion === "ticket" &&
|
|
59
|
-
typeof n === "number" &&
|
|
60
|
-
Number.isSafeInteger(n) &&
|
|
61
|
-
n >= 1) {
|
|
62
|
-
return { kind: "ticket", ticketNumber: n };
|
|
63
|
-
}
|
|
64
|
-
throw new DiaristTicketResolutionError("assertion-uninterpretable", "diarist ticket resolver assertion is not ticket|true-unbound with safe ticketNumber");
|
|
65
|
-
}
|
|
66
|
-
const HERMES_RESOLVER_TOOLSET = "context_engine";
|
|
67
|
-
/** Hermes resolver: method bytes + instruction only; empty toolset; staged prompt. */
|
|
68
|
-
export function createHermesDiaristTicketResolver(options = {}) {
|
|
69
|
-
const methodPath = resolveDiaristResolveTicketMethodPath(options.packageRoot);
|
|
70
|
-
if (!existsSync(methodPath)) {
|
|
71
|
-
throw new Error(`diarist resolve-ticket method material missing (${methodPath})`);
|
|
72
|
-
}
|
|
73
|
-
const method = readFileSync(methodPath, "utf8");
|
|
74
|
-
if (method.trim().length === 0) {
|
|
75
|
-
throw new Error(`diarist resolve-ticket method material is empty (${methodPath})`);
|
|
76
|
-
}
|
|
77
|
-
const executable = options.executable ?? "hermes";
|
|
78
|
-
return async (input) => {
|
|
79
|
-
const argv = [
|
|
80
|
-
executable,
|
|
81
|
-
"chat",
|
|
82
|
-
"--query-file",
|
|
83
|
-
ENGINE_DETOUR_STAGED_PROMPT_TOKEN,
|
|
84
|
-
"-Q",
|
|
85
|
-
"--no-restore-cwd",
|
|
86
|
-
"--ignore-rules",
|
|
87
|
-
"-t",
|
|
88
|
-
HERMES_RESOLVER_TOOLSET,
|
|
89
|
-
];
|
|
90
|
-
let result;
|
|
91
|
-
try {
|
|
92
|
-
result = await runEngineDetourOnce({
|
|
93
|
-
argv,
|
|
94
|
-
stagedPrompt: JSON.stringify({ method, instruction: input.instruction }),
|
|
95
|
-
cwd: options.cwd ?? process.cwd(),
|
|
96
|
-
...(options.env === undefined ? {} : { env: options.env }),
|
|
97
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
throw new DiaristTicketResolutionError("engine-failed", "diarist ticket resolver engine failed to launch", { cause: error });
|
|
102
|
-
}
|
|
103
|
-
if (result.code !== 0) {
|
|
104
|
-
throw new DiaristTicketResolutionError("engine-failed", `diarist ticket resolver engine exited ${result.code}: ${result.stderr.slice(0, 500)}`);
|
|
105
|
-
}
|
|
106
|
-
return parseDiaristTicketResolverStdout(result.stdout);
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/** Live ticket check over shared gh issue-body projection. */
|
|
110
|
-
export function createGhTicketExistenceChecker(options) {
|
|
111
|
-
const runner = options?.runner ?? createGhApiRunner();
|
|
112
|
-
return async (input) => {
|
|
113
|
-
const projected = await projectGhIssueBody(runner, {
|
|
114
|
-
owner: input.owner,
|
|
115
|
-
repo: input.repo,
|
|
116
|
-
ticketNumber: input.ticketNumber,
|
|
117
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
118
|
-
});
|
|
119
|
-
if (projected.status === "available")
|
|
120
|
-
return true;
|
|
121
|
-
if (projected.status === "unavailable"
|
|
122
|
-
&& projected.reason === "http-non-2xx"
|
|
123
|
-
&& projected.httpStatus === 404) {
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
if (projected.status === "unavailable" && projected.reason === "pull-request") {
|
|
127
|
-
throw new DiaristTicketResolutionError("ticket-is-pull-request", `ticket #${input.ticketNumber} resolves to a pull request, not an issue`, { cause: projected });
|
|
128
|
-
}
|
|
129
|
-
if (projected.status === "unavailable") {
|
|
130
|
-
throw new DiaristTicketResolutionError("ticket-unavailable", `ticket verification unavailable for ${input.owner}/${input.repo}#${input.ticketNumber}`, { cause: projected });
|
|
131
|
-
}
|
|
132
|
-
throw new DiaristTicketResolutionError("ticket-invalid", `ticket verification returned an invalid issue payload for ${input.owner}/${input.repo}#${input.ticketNumber}`, { cause: projected });
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
/** Mechanical verify after LLM assertion. true-unbound passes; ticket needs both checks. */
|
|
136
|
-
export async function verifyDiaristTicketAssertion(input) {
|
|
137
|
-
if (input.assertion.kind === "true-unbound")
|
|
138
|
-
return input.assertion;
|
|
139
|
-
const n = input.assertion.ticketNumber;
|
|
140
|
-
if (!instructionContainsTicketNumber(input.instruction, n)) {
|
|
141
|
-
throw new DiaristTicketResolutionError("number-not-in-instruction", `diarist ticket assertion #${n} complete decimal number does not appear in accepted instruction`);
|
|
142
|
-
}
|
|
143
|
-
if (input.origin === undefined) {
|
|
144
|
-
throw new DiaristTicketResolutionError("origin-unresolved", `diarist ticket assertion #${n} requires a resolvable github.com origin remote`);
|
|
145
|
-
}
|
|
146
|
-
let exists;
|
|
147
|
-
try {
|
|
148
|
-
exists = await input.checkExistence({
|
|
149
|
-
owner: input.origin.owner,
|
|
150
|
-
repo: input.origin.repo,
|
|
151
|
-
ticketNumber: n,
|
|
152
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
if (error instanceof DiaristTicketResolutionError)
|
|
157
|
-
throw error;
|
|
158
|
-
throw new DiaristTicketResolutionError("ticket-unavailable", `diarist ticket assertion #${n} live verification failed`, { cause: error });
|
|
159
|
-
}
|
|
160
|
-
if (!exists) {
|
|
161
|
-
throw new DiaristTicketResolutionError("ticket-missing", `diarist ticket assertion #${n} does not exist as a live issue on ${input.origin.owner}/${input.origin.repo}`);
|
|
162
|
-
}
|
|
163
|
-
return input.assertion;
|
|
164
|
-
}
|
|
165
|
-
export async function resolveDiaristTicketFromInstruction(input) {
|
|
166
|
-
const assertion = await input.resolver({
|
|
167
|
-
instruction: input.instruction,
|
|
168
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
169
|
-
});
|
|
170
|
-
return verifyDiaristTicketAssertion({
|
|
171
|
-
assertion,
|
|
172
|
-
instruction: input.instruction,
|
|
173
|
-
origin: input.origin,
|
|
174
|
-
checkExistence: input.checkExistence,
|
|
175
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
176
|
-
});
|
|
177
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# 起居郎认票方法(owner-domain)
|
|
2
|
-
|
|
3
|
-
本文件是起居郎 LLM 认票推理的语义方法真源(decision key `diarist-resolves-ticket-llm-layer` / #582)。机器经共享 engine 接缝 staged prompt 递送本文件字节与已受理 instruction(不入 argv);判断以本方法为准。
|
|
4
|
-
|
|
5
|
-
## 任务
|
|
6
|
-
|
|
7
|
-
只根据已受理 instruction,断言本庭的票面身份:
|
|
8
|
-
|
|
9
|
-
- 认出明确票号 N → 断言 `ticket`
|
|
10
|
-
- 真无票(instruction 不指向任何具体票)→ 断言 `true-unbound`
|
|
11
|
-
|
|
12
|
-
## 何谓认出票号
|
|
13
|
-
|
|
14
|
-
instruction 明确以本庭任务绑定某一 GitHub issue / 票号时,才断言 `ticket`。常见形态包括但不限于:
|
|
15
|
-
|
|
16
|
-
- 「票 #582」「#582」「issue 582」
|
|
17
|
-
- 明确写出要审/修/续的票号
|
|
18
|
-
|
|
19
|
-
拿不准、只是顺带提到他票、或 instruction 本身是无票的一般性问询 → 断言 `true-unbound`。**不得猜测**。
|
|
20
|
-
|
|
21
|
-
## 边界
|
|
22
|
-
|
|
23
|
-
- 只读 instruction;不得假设附件、仓库状态或历史 run。
|
|
24
|
-
- 相关性/授权内容不归你管——你只回答「是哪张票 / 真无票」。
|
|
25
|
-
- 不得输出多个票号;若 instruction 含多个候选且无法唯一确定,断言 `true-unbound`。
|
|
26
|
-
|
|
27
|
-
## 输出
|
|
28
|
-
|
|
29
|
-
只输出一个 JSON 对象,二者择一:
|
|
30
|
-
|
|
31
|
-
```json
|
|
32
|
-
{"assertion":"ticket","ticketNumber":582}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
```json
|
|
36
|
-
{"assertion":"true-unbound"}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
`ticketNumber` 必须是正整数。不要输出 JSON 以外的文字。
|