@akagilnc/pi-workflow-roles 0.1.1941 → 0.1.2004
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/public-cli/main.js +2448 -75
- package/package.json +1 -1
- package/src/atomic-write.ts +26 -0
- package/src/ledger-session-read.ts +260 -0
- package/src/public-cli/cli.ts +24 -0
- package/src/public-cli/invocation.ts +352 -1
- package/src/public-cli/main.ts +21 -2
- package/src/public-cli/registry.ts +18 -1
- package/src/public-cli/settlement.ts +9 -0
- package/src/public-cli/taishi-run.ts +235 -0
- package/src/run-terminal-artifacts.ts +231 -0
- package/src/taishi-cohort.ts +232 -0
- package/src/taishi-entry.ts +429 -0
- package/src/taishi-index.ts +269 -0
- package/src/taishi-ledger.ts +466 -0
- package/src/taishi-median.ts +15 -0
- package/src/taishi-metric-families/acceptance-success-rework.ts +346 -0
- package/src/taishi-metric-families/b2-frame-buckets-actions.ts +274 -0
- package/src/taishi-metric-families/leg-wall-clock.ts +90 -0
- package/src/taishi-metric-families/round-timeline.ts +201 -0
- package/src/taishi-metric-families.ts +36 -0
- package/src/taishi-metric-family.ts +41 -0
- package/src/taishi-model-groups.ts +198 -0
- package/src/taishi-page.ts +320 -0
- package/src/ticket-trajectory.ts +9 -62
|
@@ -334,6 +334,50 @@ export type ParseMergerArgvResult = {
|
|
|
334
334
|
project?: string;
|
|
335
335
|
};
|
|
336
336
|
|
|
337
|
+
/**
|
|
338
|
+
* #336/#337/#338 taishi public argv — four faces on one registration seam.
|
|
339
|
+
* - issue (default): ticket N and/or project-root P
|
|
340
|
+
* - sweep (#337): optional positional `sweep` and/or --attach paths;
|
|
341
|
+
* sweep payload rides exactly one typed JSON attachment (not argv/stdin)
|
|
342
|
+
* - cohort: two labeled issue-number groups
|
|
343
|
+
* - model-groups: one or more project-root scope keys
|
|
344
|
+
*/
|
|
345
|
+
export type ParseTaishiIssueArgv = {
|
|
346
|
+
readonly query: "issue";
|
|
347
|
+
/** Caller ticket / issue number face (#176 numbering space). */
|
|
348
|
+
readonly ticket?: number;
|
|
349
|
+
/** Direct projectRoot mechanical key (ADR 0068). */
|
|
350
|
+
readonly projectRoot?: string;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
export type ParseTaishiSweepArgv = {
|
|
354
|
+
readonly query: "sweep";
|
|
355
|
+
/**
|
|
356
|
+
* Public CLI attachment paths (--attach). Sweep mode only (#337).
|
|
357
|
+
* Cardinality validated on the sweep run path (exactly one).
|
|
358
|
+
*/
|
|
359
|
+
readonly attachmentPaths: readonly string[];
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
export type ParseTaishiCohortArgv = {
|
|
363
|
+
readonly query: "cohort";
|
|
364
|
+
readonly groups: readonly [
|
|
365
|
+
{ readonly groupLabel: string; readonly issues: readonly number[] },
|
|
366
|
+
{ readonly groupLabel: string; readonly issues: readonly number[] },
|
|
367
|
+
];
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export type ParseTaishiModelGroupsArgv = {
|
|
371
|
+
readonly query: "model-groups";
|
|
372
|
+
readonly projectRoots: readonly string[];
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
export type ParseTaishiArgvResult =
|
|
376
|
+
| ParseTaishiIssueArgv
|
|
377
|
+
| ParseTaishiSweepArgv
|
|
378
|
+
| ParseTaishiCohortArgv
|
|
379
|
+
| ParseTaishiModelGroupsArgv;
|
|
380
|
+
|
|
337
381
|
/** Honest activation-class failure while deriving the active-merge envelope. */
|
|
338
382
|
export class MergerEnvelopeDerivationError extends Error {
|
|
339
383
|
readonly code = "merger-envelope-derivation" as const;
|
|
@@ -347,7 +391,13 @@ export class MergerEnvelopeDerivationError extends Error {
|
|
|
347
391
|
|
|
348
392
|
/** Reject missing/blank path values so empty overrides cannot silently degrade. */
|
|
349
393
|
function requireOptionPath(
|
|
350
|
-
flag:
|
|
394
|
+
flag:
|
|
395
|
+
| "--project"
|
|
396
|
+
| "--attach"
|
|
397
|
+
| "--prerequisites"
|
|
398
|
+
| "--request-manifest"
|
|
399
|
+
| "--base"
|
|
400
|
+
| "--project-root",
|
|
351
401
|
value: string | undefined,
|
|
352
402
|
): string {
|
|
353
403
|
if (value === undefined || value.trim() === "") {
|
|
@@ -2041,3 +2091,304 @@ export function buildMergerTransportPrompt(
|
|
|
2041
2091
|
}
|
|
2042
2092
|
return lines.join("\n");
|
|
2043
2093
|
}
|
|
2094
|
+
|
|
2095
|
+
const TAISHI_TICKET_NUMBER_PATTERN = /^[1-9]\d*$/;
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Parse a positive ticket / issue number for public taishi admission.
|
|
2099
|
+
* Leading zeros and non-integers are structural rejects (same face as #176).
|
|
2100
|
+
* `flag` names the actual argv face in diagnostics (cohort group lists reuse this).
|
|
2101
|
+
*/
|
|
2102
|
+
export function parseTaishiTicketNumber(
|
|
2103
|
+
raw: string,
|
|
2104
|
+
flag: string = "--ticket",
|
|
2105
|
+
): number {
|
|
2106
|
+
const trimmed = raw.trim();
|
|
2107
|
+
if (!TAISHI_TICKET_NUMBER_PATTERN.test(trimmed)) {
|
|
2108
|
+
throw new CliUsageError(
|
|
2109
|
+
`taishi ${flag} must be a positive integer, got ${raw}`,
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
const value = Number(trimmed);
|
|
2113
|
+
// Digit-only strings beyond MAX_SAFE_INTEGER round or become Infinity — reject.
|
|
2114
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
2115
|
+
throw new CliUsageError(
|
|
2116
|
+
`taishi ${flag} must be a positive integer, got ${raw}`,
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
return value;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function parseTaishiIssueNumberList(raw: string, flag: string): number[] {
|
|
2123
|
+
const trimmed = raw.trim();
|
|
2124
|
+
if (trimmed === "") {
|
|
2125
|
+
throw new CliUsageError(`${flag} requires a comma-separated positive integer list`);
|
|
2126
|
+
}
|
|
2127
|
+
const parts = trimmed.split(",").map((part) => part.trim());
|
|
2128
|
+
if (parts.some((part) => part === "")) {
|
|
2129
|
+
throw new CliUsageError(`${flag} requires a comma-separated positive integer list`);
|
|
2130
|
+
}
|
|
2131
|
+
// Same numeric rule as --ticket; diagnostic names the actual group flag.
|
|
2132
|
+
return parts.map((part) => parseTaishiTicketNumber(part, flag));
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function requireOptionValue(
|
|
2136
|
+
flag: string,
|
|
2137
|
+
value: string | undefined,
|
|
2138
|
+
what: string,
|
|
2139
|
+
): string {
|
|
2140
|
+
if (value === undefined || value.trim() === "") {
|
|
2141
|
+
throw new CliUsageError(`${flag} requires ${what}`);
|
|
2142
|
+
}
|
|
2143
|
+
return value;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/**
|
|
2147
|
+
* Parse taishi-specific argv after the `taishi` token (#336/#337/#338).
|
|
2148
|
+
* Issue: at least one of --ticket / --project-root.
|
|
2149
|
+
* Sweep: positional `sweep` and/or --attach; payload is the attachment body only.
|
|
2150
|
+
* Cohort / model-groups: explicit query flags with their own required faces.
|
|
2151
|
+
* Faces are mutually exclusive.
|
|
2152
|
+
*/
|
|
2153
|
+
export function parseTaishiArgv(args: readonly string[]): ParseTaishiArgvResult {
|
|
2154
|
+
let query: "issue" | "cohort" | "model-groups" = "issue";
|
|
2155
|
+
let ticketRaw: string | undefined;
|
|
2156
|
+
const projectRoots: string[] = [];
|
|
2157
|
+
let groupALabel: string | undefined;
|
|
2158
|
+
let groupAIssuesRaw: string | undefined;
|
|
2159
|
+
let groupBLabel: string | undefined;
|
|
2160
|
+
let groupBIssuesRaw: string | undefined;
|
|
2161
|
+
let sweepToken = false;
|
|
2162
|
+
const attachmentPaths: string[] = [];
|
|
2163
|
+
const tokens = [...args];
|
|
2164
|
+
|
|
2165
|
+
while (tokens.length > 0) {
|
|
2166
|
+
const token = tokens.shift()!;
|
|
2167
|
+
if (token === "--") {
|
|
2168
|
+
if (tokens.length > 0) {
|
|
2169
|
+
throw new CliUsageError(`unexpected taishi argument: ${tokens[0]}`);
|
|
2170
|
+
}
|
|
2171
|
+
break;
|
|
2172
|
+
}
|
|
2173
|
+
if (token === "--cohort") {
|
|
2174
|
+
if (query !== "issue") {
|
|
2175
|
+
throw new CliUsageError("taishi accepts only one of --cohort / --model-groups");
|
|
2176
|
+
}
|
|
2177
|
+
query = "cohort";
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2180
|
+
if (token === "--model-groups") {
|
|
2181
|
+
if (query !== "issue") {
|
|
2182
|
+
throw new CliUsageError("taishi accepts only one of --cohort / --model-groups");
|
|
2183
|
+
}
|
|
2184
|
+
query = "model-groups";
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
if (token === "--ticket") {
|
|
2188
|
+
const value = tokens.shift();
|
|
2189
|
+
if (value === undefined || value.trim() === "") {
|
|
2190
|
+
throw new CliUsageError("taishi --ticket requires a positive integer");
|
|
2191
|
+
}
|
|
2192
|
+
ticketRaw = value;
|
|
2193
|
+
continue;
|
|
2194
|
+
}
|
|
2195
|
+
if (token.startsWith("--ticket=")) {
|
|
2196
|
+
ticketRaw = token.slice("--ticket=".length);
|
|
2197
|
+
if (ticketRaw.trim() === "") {
|
|
2198
|
+
throw new CliUsageError("taishi --ticket requires a positive integer");
|
|
2199
|
+
}
|
|
2200
|
+
continue;
|
|
2201
|
+
}
|
|
2202
|
+
if (token === "--project-root") {
|
|
2203
|
+
projectRoots.push(requireOptionPath("--project-root", tokens.shift()));
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
if (token.startsWith("--project-root=")) {
|
|
2207
|
+
projectRoots.push(
|
|
2208
|
+
requireOptionPath("--project-root", token.slice("--project-root=".length)),
|
|
2209
|
+
);
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
if (token === "--group-a-label") {
|
|
2213
|
+
groupALabel = requireOptionValue("--group-a-label", tokens.shift(), "a label");
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
if (token.startsWith("--group-a-label=")) {
|
|
2217
|
+
groupALabel = requireOptionValue(
|
|
2218
|
+
"--group-a-label",
|
|
2219
|
+
token.slice("--group-a-label=".length),
|
|
2220
|
+
"a label",
|
|
2221
|
+
);
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
if (token === "--group-a-issues") {
|
|
2225
|
+
groupAIssuesRaw = requireOptionValue(
|
|
2226
|
+
"--group-a-issues",
|
|
2227
|
+
tokens.shift(),
|
|
2228
|
+
"a comma-separated positive integer list",
|
|
2229
|
+
);
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
if (token.startsWith("--group-a-issues=")) {
|
|
2233
|
+
groupAIssuesRaw = requireOptionValue(
|
|
2234
|
+
"--group-a-issues",
|
|
2235
|
+
token.slice("--group-a-issues=".length),
|
|
2236
|
+
"a comma-separated positive integer list",
|
|
2237
|
+
);
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
if (token === "--group-b-label") {
|
|
2241
|
+
groupBLabel = requireOptionValue("--group-b-label", tokens.shift(), "a label");
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
if (token.startsWith("--group-b-label=")) {
|
|
2245
|
+
groupBLabel = requireOptionValue(
|
|
2246
|
+
"--group-b-label",
|
|
2247
|
+
token.slice("--group-b-label=".length),
|
|
2248
|
+
"a label",
|
|
2249
|
+
);
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
if (token === "--group-b-issues") {
|
|
2253
|
+
groupBIssuesRaw = requireOptionValue(
|
|
2254
|
+
"--group-b-issues",
|
|
2255
|
+
tokens.shift(),
|
|
2256
|
+
"a comma-separated positive integer list",
|
|
2257
|
+
);
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
if (token.startsWith("--group-b-issues=")) {
|
|
2261
|
+
groupBIssuesRaw = requireOptionValue(
|
|
2262
|
+
"--group-b-issues",
|
|
2263
|
+
token.slice("--group-b-issues=".length),
|
|
2264
|
+
"a comma-separated positive integer list",
|
|
2265
|
+
);
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
if (token === "--attach") {
|
|
2269
|
+
attachmentPaths.push(requireOptionPath("--attach", tokens.shift()));
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
if (token.startsWith("--attach=")) {
|
|
2273
|
+
attachmentPaths.push(
|
|
2274
|
+
requireOptionPath("--attach", token.slice("--attach=".length)),
|
|
2275
|
+
);
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
2278
|
+
if (token.startsWith("-") && token !== "-") {
|
|
2279
|
+
throw new CliUsageError(`unknown taishi option: ${token}`);
|
|
2280
|
+
}
|
|
2281
|
+
// Optional sweep mode token (like coder plan/apply); only once, no other positionals.
|
|
2282
|
+
if (token === "sweep") {
|
|
2283
|
+
if (sweepToken) {
|
|
2284
|
+
throw new CliUsageError("unexpected taishi argument: sweep");
|
|
2285
|
+
}
|
|
2286
|
+
sweepToken = true;
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
throw new CliUsageError(`unexpected taishi argument: ${token}`);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
const hasSweepFace = sweepToken || attachmentPaths.length > 0;
|
|
2293
|
+
const hasCohortFlags =
|
|
2294
|
+
groupALabel !== undefined
|
|
2295
|
+
|| groupAIssuesRaw !== undefined
|
|
2296
|
+
|| groupBLabel !== undefined
|
|
2297
|
+
|| groupBIssuesRaw !== undefined;
|
|
2298
|
+
|
|
2299
|
+
if (query === "cohort") {
|
|
2300
|
+
if (
|
|
2301
|
+
groupALabel === undefined
|
|
2302
|
+
|| groupAIssuesRaw === undefined
|
|
2303
|
+
|| groupBLabel === undefined
|
|
2304
|
+
|| groupBIssuesRaw === undefined
|
|
2305
|
+
) {
|
|
2306
|
+
throw new CliUsageError(
|
|
2307
|
+
"usage: ak-role taishi --cohort --group-a-label <L> --group-a-issues <N[,N...]> --group-b-label <L> --group-b-issues <N[,N...]>",
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
if (ticketRaw !== undefined || projectRoots.length > 0) {
|
|
2311
|
+
throw new CliUsageError(
|
|
2312
|
+
"taishi --cohort does not accept --ticket or --project-root",
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
if (hasSweepFace) {
|
|
2316
|
+
throw new CliUsageError(
|
|
2317
|
+
"taishi --cohort does not accept sweep --attach",
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
return {
|
|
2321
|
+
query: "cohort",
|
|
2322
|
+
groups: [
|
|
2323
|
+
{
|
|
2324
|
+
groupLabel: groupALabel,
|
|
2325
|
+
issues: parseTaishiIssueNumberList(groupAIssuesRaw, "--group-a-issues"),
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
groupLabel: groupBLabel,
|
|
2329
|
+
issues: parseTaishiIssueNumberList(groupBIssuesRaw, "--group-b-issues"),
|
|
2330
|
+
},
|
|
2331
|
+
],
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
if (query === "model-groups") {
|
|
2336
|
+
if (projectRoots.length === 0) {
|
|
2337
|
+
throw new CliUsageError(
|
|
2338
|
+
"usage: ak-role taishi --model-groups --project-root <P> [--project-root <P> ...]",
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
if (ticketRaw !== undefined) {
|
|
2342
|
+
throw new CliUsageError("taishi --model-groups does not accept --ticket");
|
|
2343
|
+
}
|
|
2344
|
+
if (hasCohortFlags) {
|
|
2345
|
+
throw new CliUsageError("taishi --model-groups does not accept cohort group flags");
|
|
2346
|
+
}
|
|
2347
|
+
if (hasSweepFace) {
|
|
2348
|
+
throw new CliUsageError(
|
|
2349
|
+
"taishi --model-groups does not accept sweep --attach",
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
return {
|
|
2353
|
+
query: "model-groups",
|
|
2354
|
+
projectRoots,
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// default issue or sweep (#336/#337 faces)
|
|
2359
|
+
if (hasCohortFlags) {
|
|
2360
|
+
throw new CliUsageError("taishi issue query does not accept cohort group flags");
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
if (hasSweepFace) {
|
|
2364
|
+
if (ticketRaw !== undefined || projectRoots.length > 0) {
|
|
2365
|
+
throw new CliUsageError(
|
|
2366
|
+
"taishi sweep --attach cannot combine with --ticket or --project-root",
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
return {
|
|
2370
|
+
query: "sweep",
|
|
2371
|
+
attachmentPaths,
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
if (projectRoots.length > 1) {
|
|
2376
|
+
throw new CliUsageError(
|
|
2377
|
+
"taishi issue query accepts at most one --project-root (use --model-groups for many)",
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
const projectRoot = projectRoots[0];
|
|
2381
|
+
if (ticketRaw === undefined && projectRoot === undefined) {
|
|
2382
|
+
throw new CliUsageError(
|
|
2383
|
+
"usage: ak-role taishi ((--ticket <N> | --project-root <P>) | [sweep] --attach <sweep.json> | --cohort ... | --model-groups ...)",
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
return {
|
|
2388
|
+
query: "issue",
|
|
2389
|
+
...(ticketRaw === undefined
|
|
2390
|
+
? {}
|
|
2391
|
+
: { ticket: parseTaishiTicketNumber(ticketRaw) }),
|
|
2392
|
+
...(projectRoot === undefined ? {} : { projectRoot }),
|
|
2393
|
+
};
|
|
2394
|
+
}
|
package/src/public-cli/main.ts
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { dirname, join } from "node:path";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
|
|
4
5
|
import { ensureHostPiRuntimeResolvable } from "./host-pi-runtime.ts";
|
|
5
6
|
|
|
6
7
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve install package root from the public bin location.
|
|
11
|
+
*
|
|
12
|
+
* Shipped layout is `<packageRoot>/dist/public-cli/main.js` → two levels up when
|
|
13
|
+
* that ancestor owns package.json. A relocated single-file bundle (no package
|
|
14
|
+
* tree beside the bin) must NOT keep climbing: `join("/tmp/<bin>","..","..")`
|
|
15
|
+
* is `"/"` on Linux CI, and host-pi linking then does
|
|
16
|
+
* `mkdir('/node_modules/@earendil-works')` → EACCES. Fall back to the bin
|
|
17
|
+
* directory so links stay on the ESM ancestor walk and remain writable.
|
|
18
|
+
*/
|
|
19
|
+
function resolvePackageRoot(binDir: string): string {
|
|
20
|
+
const canonical = join(binDir, "..", "..");
|
|
21
|
+
if (existsSync(join(canonical, "package.json"))) {
|
|
22
|
+
return canonical;
|
|
23
|
+
}
|
|
24
|
+
return binDir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const packageRoot = resolvePackageRoot(here);
|
|
9
28
|
|
|
10
29
|
// The host-provided runtime must be resolvable before the CLI module graph loads it.
|
|
11
30
|
ensureHostPiRuntimeResolvable(packageRoot);
|
|
@@ -97,6 +97,12 @@ export function publicStartupCandidates(
|
|
|
97
97
|
return STARTUP_CANDIDATES[seat];
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** Deterministic public commands — discoverable, never LLM-configurable seats. */
|
|
101
|
+
export const PUBLIC_DETERMINISTIC_COMMANDS = ["taishi"] as const;
|
|
102
|
+
|
|
103
|
+
export type PublicDeterministicCommand =
|
|
104
|
+
(typeof PUBLIC_DETERMINISTIC_COMMANDS)[number];
|
|
105
|
+
|
|
100
106
|
export type HelpCapability =
|
|
101
107
|
| {
|
|
102
108
|
kind: "support";
|
|
@@ -107,11 +113,16 @@ export type HelpCapability =
|
|
|
107
113
|
name: PublicCallableRole;
|
|
108
114
|
phases: readonly (string | null)[];
|
|
109
115
|
defaultPhase: string | null;
|
|
116
|
+
}
|
|
117
|
+
| {
|
|
118
|
+
kind: "deterministic";
|
|
119
|
+
name: PublicDeterministicCommand;
|
|
110
120
|
};
|
|
111
121
|
|
|
112
122
|
/**
|
|
113
123
|
* Typed help surface. Presentation formats these facts; tests must not assert
|
|
114
124
|
* exact help prose or layout (锚定宪法 / ADR 0016 / #105 AC).
|
|
125
|
+
* taishi is a deterministic analysis command on the public CLI — not an LLM seat.
|
|
115
126
|
*/
|
|
116
127
|
export function listHelpCapabilities(): readonly HelpCapability[] {
|
|
117
128
|
const support: HelpCapability[] = PUBLIC_CLI_SUPPORT_COMMANDS.map((name) => ({
|
|
@@ -133,7 +144,13 @@ export function listHelpCapabilities(): readonly HelpCapability[] {
|
|
|
133
144
|
defaultPhase,
|
|
134
145
|
};
|
|
135
146
|
});
|
|
136
|
-
|
|
147
|
+
const deterministic: HelpCapability[] = PUBLIC_DETERMINISTIC_COMMANDS.map(
|
|
148
|
+
(name) => ({
|
|
149
|
+
kind: "deterministic" as const,
|
|
150
|
+
name,
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
return [...support, ...roles, ...deterministic];
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
export function isPublicCallableRole(value: string): value is PublicCallableRole {
|
|
@@ -242,6 +242,15 @@ export function presentStructuralRejection(
|
|
|
242
242
|
io.stderr(formatCliDiagnostic(error.message));
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/** ControlledFailure face without admitted-run Terminal (stdout body + stderr line). */
|
|
246
|
+
export function presentControlledFailure(
|
|
247
|
+
failure: ControlledFailure,
|
|
248
|
+
io: { stdout: (text: string) => void; stderr: (text: string) => void },
|
|
249
|
+
): void {
|
|
250
|
+
io.stdout(`${JSON.stringify(failure, null, 2)}\n`);
|
|
251
|
+
io.stderr(formatFailureStderrDiagnostic(failure));
|
|
252
|
+
}
|
|
253
|
+
|
|
245
254
|
/** Session readiness after an admitted activation attempt. */
|
|
246
255
|
export type SessionReadiness =
|
|
247
256
|
| { readonly state: "missing" }
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public taishi adapter (#336/#337/#338): argv → typed query → runTaishi family.
|
|
3
|
+
* Deterministic analysis seat — no Pi runner, no admission lease.
|
|
4
|
+
* Reuses existing CLI failure envelope (CliUsageError + structural reject +
|
|
5
|
+
* ControlledFailure).
|
|
6
|
+
* Index read reuses readTaishiLibraryIndexPage / findTaishiLibraryIndexRow.
|
|
7
|
+
* #337 sweep: exactly one typed JSON attachment → TaishiSweepModeInput → #329 kernel.
|
|
8
|
+
* #338: three query faces; sync compute-if-missing; whole-compute failure →
|
|
9
|
+
* ControlledFailure terminal (code/projectRoot/issueNumber/real cause).
|
|
10
|
+
* "Unobtrusive" binds #337 merge auto-trigger only, not this user-initiated query.
|
|
11
|
+
*/
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { isAbsolute, resolve } from "node:path";
|
|
14
|
+
import { Value } from "typebox/value";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
errnoCode,
|
|
18
|
+
physicalPathIdentity,
|
|
19
|
+
resolveActivationLedgerHome,
|
|
20
|
+
} from "../activation-ledger-topology.ts";
|
|
21
|
+
import { exactUtf8 } from "../exact-utf8.ts";
|
|
22
|
+
import {
|
|
23
|
+
findTaishiLibraryIndexRow,
|
|
24
|
+
readTaishiLibraryIndexPage,
|
|
25
|
+
} from "../taishi-index.ts";
|
|
26
|
+
import {
|
|
27
|
+
readOrComputeTaishiIssuePage,
|
|
28
|
+
runTaishi,
|
|
29
|
+
taishiSweepModeInputSchema,
|
|
30
|
+
TaishiIssueComputeError,
|
|
31
|
+
type TaishiIssueModeInput,
|
|
32
|
+
type TaishiSweepModeInput,
|
|
33
|
+
} from "../taishi-entry.ts";
|
|
34
|
+
import { CliUsageError } from "./cli-errors.ts";
|
|
35
|
+
import type { CliIo } from "./cli-io.ts";
|
|
36
|
+
import type {
|
|
37
|
+
ParseTaishiArgvResult,
|
|
38
|
+
ParseTaishiIssueArgv,
|
|
39
|
+
} from "./invocation.ts";
|
|
40
|
+
import { presentControlledFailure, presentStructuralRejection } from "./settlement.ts";
|
|
41
|
+
|
|
42
|
+
export type TaishiRunEnv = {
|
|
43
|
+
readonly home: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the sole library issue-mode input from public argv faces.
|
|
48
|
+
* - ticket N → issueNumber = ticketNumber = N; projectRoot from index (or project-root fallback).
|
|
49
|
+
* - project-root P → direct mechanical key.
|
|
50
|
+
* - both + index hit → index projectRoot wins; when direct root differs, retain it as
|
|
51
|
+
* conflictingProjectRoot so runTaishi records the C4 dual-param conflict fact on the page.
|
|
52
|
+
* - both + index miss → project-root fallback.
|
|
53
|
+
* Bare both-missing is owned by parseTaishiArgv — no second reject here.
|
|
54
|
+
*/
|
|
55
|
+
export async function buildTaishiIssueModeInputFromPublicArgv(
|
|
56
|
+
parsed: ParseTaishiIssueArgv,
|
|
57
|
+
ledgerHome: string,
|
|
58
|
+
): Promise<TaishiIssueModeInput> {
|
|
59
|
+
const ticket = parsed.ticket;
|
|
60
|
+
const directRoot = parsed.projectRoot;
|
|
61
|
+
|
|
62
|
+
if (ticket === undefined) {
|
|
63
|
+
return {
|
|
64
|
+
mode: "issue",
|
|
65
|
+
projectRoot: directRoot!,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ticket N = issueNumber (no conversion); also the C4 typed ticket face.
|
|
70
|
+
const index = await readTaishiLibraryIndexPage(ledgerHome);
|
|
71
|
+
const row = findTaishiLibraryIndexRow(index, ticket);
|
|
72
|
+
|
|
73
|
+
let projectRoot: string;
|
|
74
|
+
if (row !== undefined) {
|
|
75
|
+
// Ticket-resolved index projectRoot wins over any concurrent --project-root.
|
|
76
|
+
projectRoot = row.projectRoot;
|
|
77
|
+
} else if (directRoot !== undefined) {
|
|
78
|
+
// Index miss with project-root fallback (ticket faces still set for C4).
|
|
79
|
+
projectRoot = directRoot;
|
|
80
|
+
} else {
|
|
81
|
+
throw new CliUsageError(
|
|
82
|
+
`taishi library index has no row for ticket ${ticket}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Dual-param conflict: index root won, but caller also supplied a distinct --project-root.
|
|
87
|
+
// Carry the losing root so the metrics page records the call-face conflict fact.
|
|
88
|
+
const dualParamConflict =
|
|
89
|
+
row !== undefined
|
|
90
|
+
&& directRoot !== undefined
|
|
91
|
+
&& physicalPathIdentity(directRoot) !== physicalPathIdentity(projectRoot);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
mode: "issue",
|
|
95
|
+
projectRoot,
|
|
96
|
+
ticketNumber: ticket,
|
|
97
|
+
issueNumber: ticket,
|
|
98
|
+
...(dualParamConflict ? { conflictingProjectRoot: directRoot } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Attachment JSON → library TaishiSweepModeInput via the sole schema (#337).
|
|
104
|
+
* No parallel hand shape; rejects missing/extra/wrong-type fields only.
|
|
105
|
+
*/
|
|
106
|
+
export function parseTaishiSweepModeInputFromJsonValue(
|
|
107
|
+
value: unknown,
|
|
108
|
+
): TaishiSweepModeInput {
|
|
109
|
+
if (!Value.Check(taishiSweepModeInputSchema, value)) {
|
|
110
|
+
throw new CliUsageError(
|
|
111
|
+
"taishi sweep attachment must match TaishiSweepModeInput",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load sweep typed input from exactly one public CLI attachment path.
|
|
119
|
+
* Rejects: wrong cardinality, unreadable path, non-UTF-8, JSON fail, field contract.
|
|
120
|
+
* Zero ledger writes — read-only path resolve + parse.
|
|
121
|
+
*/
|
|
122
|
+
export async function buildTaishiSweepModeInputFromAttachmentPaths(
|
|
123
|
+
attachmentPaths: readonly string[],
|
|
124
|
+
): Promise<TaishiSweepModeInput> {
|
|
125
|
+
if (attachmentPaths.length !== 1) {
|
|
126
|
+
throw new CliUsageError(
|
|
127
|
+
"taishi sweep requires exactly one --attach typed JSON attachment",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const sourcePath = attachmentPaths[0]!;
|
|
132
|
+
const absolute = isAbsolute(sourcePath) ? sourcePath : resolve(sourcePath);
|
|
133
|
+
|
|
134
|
+
let bytes: Buffer;
|
|
135
|
+
try {
|
|
136
|
+
bytes = await readFile(absolute);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new CliUsageError(
|
|
139
|
+
`taishi sweep attachment is not a readable regular file: ${sourcePath}`,
|
|
140
|
+
{ cause: error },
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let text: string;
|
|
145
|
+
try {
|
|
146
|
+
text = exactUtf8(bytes, "taishi sweep attachment");
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
149
|
+
throw new CliUsageError(detail, { cause: error });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let parsed: unknown;
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(text);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new CliUsageError(
|
|
157
|
+
"taishi sweep attachment is not valid JSON",
|
|
158
|
+
{ cause: error },
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return parseTaishiSweepModeInputFromJsonValue(parsed);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Public taishi run path — parse → resolve → query → typed receipt on stdout.
|
|
167
|
+
* Issue (#336/#338 compute-if-missing), sweep (#337), cohort/model-groups (#338).
|
|
168
|
+
*/
|
|
169
|
+
export async function runPublicTaishi(
|
|
170
|
+
argv: readonly string[],
|
|
171
|
+
_env: TaishiRunEnv,
|
|
172
|
+
io: CliIo,
|
|
173
|
+
parseTaishiArgv: (args: readonly string[]) => ParseTaishiArgvResult,
|
|
174
|
+
): Promise<{ exitCode: number }> {
|
|
175
|
+
try {
|
|
176
|
+
const parsed = parseTaishiArgv(argv);
|
|
177
|
+
// Machine home is package-owned (ADR 0048) — same primitive runTaishi uses.
|
|
178
|
+
const ledgerHome = resolveActivationLedgerHome();
|
|
179
|
+
|
|
180
|
+
if (parsed.query === "sweep") {
|
|
181
|
+
const input = await buildTaishiSweepModeInputFromAttachmentPaths(
|
|
182
|
+
parsed.attachmentPaths,
|
|
183
|
+
);
|
|
184
|
+
const result = await runTaishi(input);
|
|
185
|
+
io.stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
186
|
+
return { exitCode: 0 };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (parsed.query === "cohort") {
|
|
190
|
+
const result = await runTaishi({
|
|
191
|
+
mode: "cohort",
|
|
192
|
+
groups: parsed.groups,
|
|
193
|
+
});
|
|
194
|
+
io.stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
195
|
+
return { exitCode: 0 };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (parsed.query === "model-groups") {
|
|
199
|
+
const result = await runTaishi({
|
|
200
|
+
mode: "model-groups",
|
|
201
|
+
projectRoots: parsed.projectRoots,
|
|
202
|
+
});
|
|
203
|
+
io.stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
204
|
+
return { exitCode: 0 };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// issue query — compute-if-missing (#338); sole kernel on miss.
|
|
208
|
+
const input = await buildTaishiIssueModeInputFromPublicArgv(parsed, ledgerHome);
|
|
209
|
+
const result = await readOrComputeTaishiIssuePage(input);
|
|
210
|
+
io.stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
211
|
+
return { exitCode: 0 };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error instanceof CliUsageError) {
|
|
214
|
+
presentStructuralRejection(error, io);
|
|
215
|
+
return { exitCode: 2 };
|
|
216
|
+
}
|
|
217
|
+
if (error instanceof TaishiIssueComputeError) {
|
|
218
|
+
// Existing ControlledFailure: details carry code/projectRoot/issueNumber;
|
|
219
|
+
// identity.code carries distinguishable real cause (errno). No parallel schema.
|
|
220
|
+
const code = errnoCode(error.cause);
|
|
221
|
+
presentControlledFailure({
|
|
222
|
+
cause: "output",
|
|
223
|
+
diagnostic: error.message,
|
|
224
|
+
...(code === undefined ? {} : { identity: { code } }),
|
|
225
|
+
details: {
|
|
226
|
+
code: error.code,
|
|
227
|
+
projectRoot: error.projectRoot,
|
|
228
|
+
...(error.issueNumber === undefined ? {} : { issueNumber: error.issueNumber }),
|
|
229
|
+
},
|
|
230
|
+
}, io);
|
|
231
|
+
return { exitCode: 1 };
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|