@akagilnc/pi-workflow-roles 0.1.1918 → 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/evidence-child-executor.js +12 -6
- package/dist/package-contracts/reviewer-output.js +16 -0
- package/dist/public-cli/main.js +2520 -72
- package/dist/reviewer-construction.js +46 -6
- package/dist/reviewer-dispatch.js +64 -1
- package/dist/reviewer-execution-ledger.js +2 -1
- package/dist/reviewer-pinned-git.js +33 -0
- package/package.json +1 -1
- package/src/atomic-write.ts +26 -0
- package/src/evidence-child-executor.ts +13 -6
- package/src/ledger-session-read.ts +260 -0
- package/src/package-contracts/reviewer-output.ts +19 -0
- package/src/public-cli/cli.ts +24 -0
- package/src/public-cli/invocation.ts +398 -2
- package/src/public-cli/main.ts +21 -2
- package/src/public-cli/registry.ts +18 -1
- package/src/public-cli/reviewer-run.ts +12 -0
- package/src/public-cli/run-lifecycle.ts +22 -1
- package/src/public-cli/settlement.ts +17 -0
- package/src/public-cli/taishi-run.ts +235 -0
- package/src/reviewer-construction.ts +64 -7
- package/src/reviewer-dispatch.ts +81 -2
- package/src/reviewer-execution-ledger.ts +2 -1
- package/src/reviewer-pinned-git.ts +43 -0
- package/src/reviewer-role.ts +143 -132
- package/src/reviewer-settlement.ts +4 -0
- package/src/role-runtime.ts +186 -3
- 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
|
@@ -11,6 +11,21 @@ export const REVIEWER_AXIS_OUTPUT_ADAPTER = Object.freeze({
|
|
|
11
11
|
version: 1,
|
|
12
12
|
implementationSha256: sha256Hex("reviewer-axis-output:v1:single-axis-verbatim-report+standards-three-priorities"),
|
|
13
13
|
});
|
|
14
|
+
/**
|
|
15
|
+
* Package-owned #1185 review verification cadence.
|
|
16
|
+
* Single true source consumed by two real actor carriers: parent Reviewer system-prompt injection
|
|
17
|
+
* and evidence-child system prompt. Not part of axis-adapter identity or axis leg prompts.
|
|
18
|
+
* Graded guidance only: focused tests allowed; full suite not forbidden but avoid frequent every-round reruns.
|
|
19
|
+
* Does not narrow ADR 0064 tools and adds no command ban, allowlist, or runtime block.
|
|
20
|
+
*/
|
|
21
|
+
export const REVIEWER_VERIFICATION_BOUNDARY = [
|
|
22
|
+
"Verification-Boundary: you may run focused product tests during this review turn when independent verification needs them.",
|
|
23
|
+
"A full repository test suite is not forbidden, but do not re-run it every review round;",
|
|
24
|
+
"prefer once at family wrap-up unless this review specifically requires a broader run.",
|
|
25
|
+
"Slice and review work should not trigger frequent full-suite reruns.",
|
|
26
|
+
"Independently discover test facts (including existing coder/fixer receipts and any tests you run);",
|
|
27
|
+
"do not treat caller prose as the source of those facts.",
|
|
28
|
+
].join(" ");
|
|
14
29
|
/** Typed Standards conclusion keys owned by reviewer construction (presentation labels are not the contract). */
|
|
15
30
|
export const REVIEWER_STANDARDS_CONCLUSION_KEYS = Object.freeze([
|
|
16
31
|
"constitutionality",
|
|
@@ -63,8 +78,22 @@ export function reviewerAxisMethodAdapter(axis) {
|
|
|
63
78
|
"The returned report is the complete output envelope and its UTF-8 bytes are preserved verbatim; no heading parser, sanitizer, section splitter, rewrite, aggregation, or replacement leg follows.",
|
|
64
79
|
].join("\n");
|
|
65
80
|
}
|
|
66
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Spec-only evidence-child material carrier for durable authority references.
|
|
83
|
+
* Exact values preserved; no prose extraction and no Standards/parent injection.
|
|
84
|
+
*/
|
|
85
|
+
export function reviewerAuthorityRefsMaterial(authorityRefs) {
|
|
86
|
+
return [
|
|
87
|
+
"Authority-Refs:",
|
|
88
|
+
JSON.stringify(Object.freeze([...authorityRefs])),
|
|
89
|
+
"These are durable authority references only. Read them as Spec grounding materials; do not invent Spec prose from caller instruction.",
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
/** Deterministic compiler: fixed target/range plus discovery product in, dispatch text out. */
|
|
67
93
|
export function constructReviewerDispatch(input) {
|
|
94
|
+
const launchSpec = input.specAuthority.status === "available";
|
|
95
|
+
const authorityRefs = Object.freeze(input.specAuthority.status === "available" ? [...input.specAuthority.refs] : []);
|
|
96
|
+
const specDisposition = launchSpec ? "launched" : "skipped-missing";
|
|
68
97
|
const common = [
|
|
69
98
|
`Target: ${input.range.target}`,
|
|
70
99
|
`Base: ${input.range.base}`,
|
|
@@ -76,11 +105,20 @@ export function constructReviewerDispatch(input) {
|
|
|
76
105
|
"Fixed-Range:",
|
|
77
106
|
JSON.stringify(input.range, null, 2),
|
|
78
107
|
].join("\n");
|
|
79
|
-
const axes =
|
|
80
|
-
|
|
81
|
-
axis:
|
|
82
|
-
|
|
83
|
-
|
|
108
|
+
const axes = launchSpec
|
|
109
|
+
? [{ axis: "standards" }, { axis: "spec" }]
|
|
110
|
+
: [{ axis: "standards" }];
|
|
111
|
+
const legs = axes.map((x) => {
|
|
112
|
+
const parts = [common, reviewerAxisMethodAdapter(x.axis)];
|
|
113
|
+
// Spec evidence-child only — never Standards or a parent replacement Spec leg.
|
|
114
|
+
if (x.axis === "spec" && authorityRefs.length > 0) {
|
|
115
|
+
parts.push(reviewerAuthorityRefsMaterial(authorityRefs));
|
|
116
|
+
}
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
axis: x.axis,
|
|
119
|
+
prompt: `${parts.join("\n")}\n`,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
84
122
|
return Object.freeze({
|
|
85
123
|
identity: input.identity,
|
|
86
124
|
recipe: "reviewer-common-bundle-v1",
|
|
@@ -90,6 +128,8 @@ export function constructReviewerDispatch(input) {
|
|
|
90
128
|
}),
|
|
91
129
|
targetSnapshot: input.target,
|
|
92
130
|
range: input.range,
|
|
131
|
+
authorityRefs,
|
|
132
|
+
specDisposition,
|
|
93
133
|
legs: Object.freeze(legs),
|
|
94
134
|
});
|
|
95
135
|
}
|
|
@@ -3,10 +3,67 @@ import { immutableReviewerPin } from "./reviewer-pinned-git.js";
|
|
|
3
3
|
export { createReviewerPinnedGitReader, immutableReviewerPin } from "./reviewer-pinned-git.js";
|
|
4
4
|
import { isReviewerPromptText, sameReviewerPromptText } from "./reviewer-prompt-identity.js";
|
|
5
5
|
import { sha256Hex } from "./sha256.js";
|
|
6
|
-
import { constructReviewerDispatch } from "./reviewer-construction.js";
|
|
6
|
+
import { constructReviewerDispatch, } from "./reviewer-construction.js";
|
|
7
|
+
export {} from "./reviewer-construction.js";
|
|
7
8
|
import { ReviewerCorrectablePreflightError } from "./reviewer-preflight-error.js";
|
|
8
9
|
export { sha256Hex } from "./sha256.js";
|
|
9
10
|
export { isReviewerPromptText as isReviewerPromptIdentity, sameReviewerPromptText as sameReviewerPromptIdentity } from "./reviewer-prompt-identity.js";
|
|
11
|
+
const GENERIC_FEATURE_TOKENS = new Set(["", "head", "main", "master", "trunk", "develop", "development"]);
|
|
12
|
+
/** Conventional branch shells that must not hide the feature token (feat/login → login). */
|
|
13
|
+
const BRANCH_SHELL_PREFIX = /^(?:feat|feature|fix|bugfix|hotfix|chore|docs|refactor)-/;
|
|
14
|
+
function normalizeFeatureToken(value) {
|
|
15
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
16
|
+
}
|
|
17
|
+
/** Expand one branch/ref name into matchable tokens, stripping conventional shells. */
|
|
18
|
+
function expandFeatureTokens(raw) {
|
|
19
|
+
const normalized = normalizeFeatureToken(raw);
|
|
20
|
+
if (normalized.length === 0)
|
|
21
|
+
return Object.freeze([]);
|
|
22
|
+
const tokens = new Set([normalized]);
|
|
23
|
+
const stripped = normalized.replace(BRANCH_SHELL_PREFIX, "");
|
|
24
|
+
if (stripped.length > 0 && stripped !== normalized)
|
|
25
|
+
tokens.add(stripped);
|
|
26
|
+
return Object.freeze([...tokens]);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Unique production owner of code-review Skill step 2 Spec discovery.
|
|
30
|
+
* Directly yields durable refs Spec child can read, or confirmed missing.
|
|
31
|
+
* - Supplied authorityRefs ⇒ available with those refs as material.
|
|
32
|
+
* - Matching pinned-target docs/specs/.scratch paths ⇒ available with those paths as material.
|
|
33
|
+
* - Commit message bare #N without durable source ⇒ missing (not available).
|
|
34
|
+
* Only confirmed absence yields missing; other Git/I-O failures keep true cause for preflight.
|
|
35
|
+
* Construction builds Standards/Spec solely from this product.
|
|
36
|
+
*/
|
|
37
|
+
export async function discoverReviewerSpecAuthority(input) {
|
|
38
|
+
if (input.authorityRefs.length > 0) {
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
status: "available",
|
|
41
|
+
refs: Object.freeze([...input.authorityRefs]),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const featureTokens = await input.reader.featureTokens();
|
|
45
|
+
const tokens = [
|
|
46
|
+
...new Set(featureTokens
|
|
47
|
+
.flatMap((raw) => expandFeatureTokens(raw))
|
|
48
|
+
.filter((token) => token.length >= 3 && !GENERIC_FEATURE_TOKENS.has(token))),
|
|
49
|
+
];
|
|
50
|
+
if (tokens.length === 0) {
|
|
51
|
+
return Object.freeze({ status: "missing" });
|
|
52
|
+
}
|
|
53
|
+
// Pinned target tree only — Spec child cannot read live-worktree or gitignored paths.
|
|
54
|
+
const candidates = await input.reader.listSpecCandidatePaths();
|
|
55
|
+
const matched = candidates.filter((relativePath) => {
|
|
56
|
+
const normalizedPath = normalizeFeatureToken(relativePath);
|
|
57
|
+
return tokens.some((token) => normalizedPath.includes(token));
|
|
58
|
+
});
|
|
59
|
+
if (matched.length === 0) {
|
|
60
|
+
return Object.freeze({ status: "missing" });
|
|
61
|
+
}
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
status: "available",
|
|
64
|
+
refs: Object.freeze(matched),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
10
67
|
export const REVIEWER_PREFLIGHT_VIOLATIONS = ["base-invalid", "range-invalid", "prompt-identity-invalid", "target-drift"];
|
|
11
68
|
export class ReviewerPreflightError extends Error {
|
|
12
69
|
code;
|
|
@@ -49,12 +106,18 @@ export function createReviewerDispatcher(d) {
|
|
|
49
106
|
try {
|
|
50
107
|
const base = await d.reader.resolve(baseRevision);
|
|
51
108
|
const range = await d.reader.range(base);
|
|
109
|
+
const authorityRefs = Object.freeze([...(d.authorityRefs ?? [])]);
|
|
110
|
+
const specAuthority = await discoverReviewerSpecAuthority({
|
|
111
|
+
authorityRefs,
|
|
112
|
+
reader: d.reader,
|
|
113
|
+
});
|
|
52
114
|
dispatch = constructReviewerDispatch({
|
|
53
115
|
identity,
|
|
54
116
|
canonicalSkill: d.canonicalSkill,
|
|
55
117
|
target,
|
|
56
118
|
range,
|
|
57
119
|
...(d.reviewScopeKeys === undefined ? {} : { reviewScopeKeys: d.reviewScopeKeys }),
|
|
120
|
+
specAuthority,
|
|
58
121
|
});
|
|
59
122
|
if (!sameReviewerPinnedTarget(await d.reader.snapshot(), target)) {
|
|
60
123
|
throw new ReviewerPreflightError("target-drift", "pinned target snapshot changed before child execution");
|
|
@@ -5,7 +5,8 @@ export function projectAcceptedDispatch(dispatch) {
|
|
|
5
5
|
return {
|
|
6
6
|
source: "reviewer-dispatch", type: "accepted", identity: dispatch.identity,
|
|
7
7
|
recipe: dispatch.recipe, input: dispatch.input, target: dispatch.targetSnapshot,
|
|
8
|
-
range: dispatch.range,
|
|
8
|
+
range: dispatch.range, authorityRefs: dispatch.authorityRefs,
|
|
9
|
+
specDisposition: dispatch.specDisposition, legs: dispatch.legs,
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
export function projectReviewerDispatchOutcome(ledger, dispatch, result) {
|
|
@@ -142,5 +142,38 @@ export async function createReviewerPinnedGitReader(root = process.cwd()) {
|
|
|
142
142
|
invalid("range-invalid", "review range must contain a non-empty diff between base and pinned target");
|
|
143
143
|
return Object.freeze({ base: mergeBase, target: targetHead, diffCommand, diffSha256: sha256Hex(Uint8Array.from(diff)), commits: Object.freeze(commitsText ? commitsText.split("\n") : []) });
|
|
144
144
|
},
|
|
145
|
+
async featureTokens() {
|
|
146
|
+
// Pinned ref snapshot is the target-tree fact — no live branch/symbolic-ref walk,
|
|
147
|
+
// no catch-to-empty. Detached/remote-only tips surface via refs/remotes/* entries.
|
|
148
|
+
const names = new Set();
|
|
149
|
+
for (const [refName, entry] of Object.entries(pin.refs)) {
|
|
150
|
+
if (entry.peeledCommitId !== targetHead)
|
|
151
|
+
continue;
|
|
152
|
+
const short = refName.startsWith("refs/heads/")
|
|
153
|
+
? refName.slice("refs/heads/".length)
|
|
154
|
+
: refName.startsWith("refs/tags/")
|
|
155
|
+
? refName.slice("refs/tags/".length)
|
|
156
|
+
: refName.startsWith("refs/remotes/")
|
|
157
|
+
? refName.slice("refs/remotes/".length).replace(/^[^/]+\//, "")
|
|
158
|
+
: refName;
|
|
159
|
+
if (short.trim() !== "")
|
|
160
|
+
names.add(short.trim());
|
|
161
|
+
}
|
|
162
|
+
return Object.freeze([...names]);
|
|
163
|
+
},
|
|
164
|
+
async listSpecCandidatePaths() {
|
|
165
|
+
const roots = ["docs", "specs", ".scratch"];
|
|
166
|
+
// git ls-tree exits 0 with empty stdout when none of the roots exist at targetHead.
|
|
167
|
+
// Other Git/I-O failures keep their true cause for the dispatch preflight path.
|
|
168
|
+
const text = await gitText(repositoryRoot, [
|
|
169
|
+
"ls-tree",
|
|
170
|
+
"-r",
|
|
171
|
+
"--name-only",
|
|
172
|
+
targetHead,
|
|
173
|
+
"--",
|
|
174
|
+
...roots,
|
|
175
|
+
]);
|
|
176
|
+
return Object.freeze(text === "" ? [] : text.split("\n").filter((line) => line.length > 0));
|
|
177
|
+
},
|
|
145
178
|
});
|
|
146
179
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-directory temp + rename atomic file replace.
|
|
3
|
+
* Shared primitive for ledger-adjacent typed pages (taishi metrics, etc.).
|
|
4
|
+
* Does not open/truncate an existing destination inode, so hard-linked twins
|
|
5
|
+
* keep prior bytes until the directory entry is swapped.
|
|
6
|
+
* Parent directory must already exist — callers that write under the package
|
|
7
|
+
* ledger home own confinement via ensureRealDirectoryTree (ADR 0038).
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { rename, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
|
|
13
|
+
export async function writeFileAtomically(
|
|
14
|
+
destination: string,
|
|
15
|
+
contents: string | Uint8Array,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const parent = dirname(destination);
|
|
18
|
+
const temporary = join(parent, `.atomic-write-${randomUUID()}.tmp`);
|
|
19
|
+
try {
|
|
20
|
+
await writeFile(temporary, contents);
|
|
21
|
+
await rename(temporary, destination);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -30,15 +30,26 @@ import {
|
|
|
30
30
|
type AuditorParentAttemptBinding,
|
|
31
31
|
} from "./compliance-transport.ts";
|
|
32
32
|
import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
|
|
33
|
+
import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
34
|
+
import { REVIEWER_VERIFICATION_BOUNDARY } from "./reviewer-construction.ts";
|
|
33
35
|
import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
|
|
34
36
|
import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.ts";
|
|
35
|
-
import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
36
37
|
import {
|
|
37
38
|
hasUpstreamErrorTestimony,
|
|
38
39
|
isNonSuccessHttpStatus,
|
|
39
40
|
projectConfirmedRemotePayload,
|
|
40
41
|
} from "./upstream-error-testimony.ts";
|
|
41
42
|
|
|
43
|
+
/** Package-owned system prompt for Reviewer Standards/Spec evidence children (private carrier). */
|
|
44
|
+
function buildEvidenceChildSystemPrompt(): string {
|
|
45
|
+
return [
|
|
46
|
+
"Work only in the supplied workspace.",
|
|
47
|
+
"Use the available evidence tools to investigate. Do not commit, push, or mutate remotes.",
|
|
48
|
+
REVIEWER_VERIFICATION_BOUNDARY,
|
|
49
|
+
"Return one substantive non-blank report.",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
// ── shared constants / types ──────────────────────────────────────────────
|
|
43
54
|
|
|
44
55
|
export const AUDITOR_TURN_LIMIT = 32;
|
|
@@ -561,11 +572,7 @@ export async function executeEvidenceChild(
|
|
|
561
572
|
model: inherited.model,
|
|
562
573
|
thinkingLevel: context.thinkingLevel ?? "off",
|
|
563
574
|
modelRuntime: inherited.runtime,
|
|
564
|
-
systemPrompt:
|
|
565
|
-
"Work only in the supplied workspace.",
|
|
566
|
-
"Use the available evidence tools to investigate. Do not commit, push, or mutate remotes.",
|
|
567
|
-
"Return one substantive non-blank report.",
|
|
568
|
-
].join("\n"),
|
|
575
|
+
systemPrompt: buildEvidenceChildSystemPrompt(),
|
|
569
576
|
sessionManager: createRecordSession({
|
|
570
577
|
cwd: workspace,
|
|
571
578
|
kind: "evidence-children",
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical ledger session JSONL read primitives (shared owner).
|
|
3
|
+
* Consumers (ticket-trajectory, taishi, …) must import here — no second parse kernel.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
export type LedgerSessionRow = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Loud JSONL failure that still retains rows parsed before the bad line.
|
|
11
|
+
* Callers that only need the throw keep catching Error; owners that must
|
|
12
|
+
* surface partial typed facts (e.g. first-frame timestamp) read prefixRows.
|
|
13
|
+
*/
|
|
14
|
+
export class LedgerSessionJsonlError extends Error {
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly line: number;
|
|
17
|
+
readonly prefixRows: readonly LedgerSessionRow[];
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
message: string,
|
|
21
|
+
init: {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly line: number;
|
|
24
|
+
readonly prefixRows: readonly LedgerSessionRow[];
|
|
25
|
+
},
|
|
26
|
+
) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "LedgerSessionJsonlError";
|
|
29
|
+
this.path = init.path;
|
|
30
|
+
this.line = init.line;
|
|
31
|
+
this.prefixRows = init.prefixRows;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read session JSONL with honest live-tail semantics:
|
|
41
|
+
* a malformed line is tolerated only when it is an unfinished final
|
|
42
|
+
* fragment at EOF (no record terminator after it). Any malformed line
|
|
43
|
+
* completed by a line terminator must fail loudly with file and 1-based
|
|
44
|
+
* line context — even when no non-empty record follows — never silently
|
|
45
|
+
* under-count.
|
|
46
|
+
*
|
|
47
|
+
* Loud failures throw LedgerSessionJsonlError carrying prefixRows so the
|
|
48
|
+
* single parse kernel can still expose facts obtained before the bad line.
|
|
49
|
+
*/
|
|
50
|
+
export async function readLedgerSessionJsonl(path: string): Promise<LedgerSessionRow[]> {
|
|
51
|
+
const text = await readFile(path, "utf8");
|
|
52
|
+
// split keeps a trailing empty segment iff text ends with "\n", so
|
|
53
|
+
// index < lines.length - 1 means this segment was terminated.
|
|
54
|
+
const lines = text.split("\n");
|
|
55
|
+
const rows: LedgerSessionRow[] = [];
|
|
56
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
57
|
+
const line = lines[index]!;
|
|
58
|
+
if (!line.trim()) continue;
|
|
59
|
+
let row: unknown;
|
|
60
|
+
try {
|
|
61
|
+
row = JSON.parse(line);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
64
|
+
const completedByTerminator = index < lines.length - 1;
|
|
65
|
+
if (completedByTerminator) {
|
|
66
|
+
throw new LedgerSessionJsonlError(
|
|
67
|
+
`malformed JSONL record in ${path} at line ${index + 1}: ${error.message}`,
|
|
68
|
+
{ path, line: index + 1, prefixRows: rows },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
// unfinished fragment at EOF — keep prior complete rows
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
// Syntactically complete line: must be a session object. Silent omission
|
|
75
|
+
// would under-count ledger evidence (failure honesty).
|
|
76
|
+
if (!isRecord(row)) {
|
|
77
|
+
const kind = row === null ? "null" : Array.isArray(row) ? "array" : typeof row;
|
|
78
|
+
throw new LedgerSessionJsonlError(
|
|
79
|
+
`complete non-object JSONL record in ${path} at line ${index + 1}: expected object, got ${kind}`,
|
|
80
|
+
{ path, line: index + 1, prefixRows: rows },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
rows.push(row);
|
|
84
|
+
}
|
|
85
|
+
return rows;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** First and last record timestamps in encounter order. */
|
|
89
|
+
export function extractSessionTimestampSpan(
|
|
90
|
+
rows: readonly LedgerSessionRow[],
|
|
91
|
+
): { startedAt?: string; endedAt?: string } {
|
|
92
|
+
let startedAt: string | undefined;
|
|
93
|
+
let endedAt: string | undefined;
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
if (typeof row.timestamp !== "string" || !row.timestamp) continue;
|
|
96
|
+
if (startedAt === undefined) startedAt = row.timestamp;
|
|
97
|
+
endedAt = row.timestamp;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
101
|
+
...(endedAt !== undefined ? { endedAt } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Ordered-unique model ids from session frames (first-seen order).
|
|
107
|
+
* Sources (same faces ticket-trajectory already reads — single parse kernel):
|
|
108
|
+
* - `model_change.modelId`
|
|
109
|
+
* - assistant `message.model`
|
|
110
|
+
* Blank / non-string values are skipped. Does not invent a default model.
|
|
111
|
+
*/
|
|
112
|
+
export function extractSessionModelSequence(
|
|
113
|
+
rows: readonly LedgerSessionRow[],
|
|
114
|
+
): string[] {
|
|
115
|
+
const seen = new Set<string>();
|
|
116
|
+
const ordered: string[] = [];
|
|
117
|
+
const push = (raw: string): void => {
|
|
118
|
+
const model = raw.trim();
|
|
119
|
+
if (model === "" || seen.has(model)) return;
|
|
120
|
+
seen.add(model);
|
|
121
|
+
ordered.push(model);
|
|
122
|
+
};
|
|
123
|
+
for (const row of rows) {
|
|
124
|
+
if (row.type === "model_change" && typeof row.modelId === "string") {
|
|
125
|
+
push(row.modelId);
|
|
126
|
+
}
|
|
127
|
+
const message = isRecord(row.message) ? row.message : undefined;
|
|
128
|
+
if (message?.role === "assistant" && typeof message.model === "string") {
|
|
129
|
+
push(message.model);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return ordered;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** First line of a bash `command` argument (sole owner of this summary). */
|
|
136
|
+
export function bashCommandFirstLine(command: string): string {
|
|
137
|
+
const match = /^[^\r\n]*/.exec(command);
|
|
138
|
+
return match?.[0] ?? "";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type SessionToolInterval = {
|
|
142
|
+
readonly toolCallId: string;
|
|
143
|
+
readonly toolName: string;
|
|
144
|
+
readonly startedAt: string;
|
|
145
|
+
readonly endedAt?: string;
|
|
146
|
+
/**
|
|
147
|
+
* Bash-only first-line command summary from `arguments.command`.
|
|
148
|
+
* Omitted for non-bash tools and when the argument is absent/non-string.
|
|
149
|
+
* Full multi-line bodies are never retained on this typed fact face.
|
|
150
|
+
*/
|
|
151
|
+
readonly command?: string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Pair toolCall frames → toolResult frames by toolCallId.
|
|
156
|
+
* Throws when a tool-bearing frame is structurally unreadable for association
|
|
157
|
+
* (toolCall missing string id, toolResult missing string toolCallId).
|
|
158
|
+
* Unpaired open calls remain without endedAt — that is incomplete, not unreadable.
|
|
159
|
+
*/
|
|
160
|
+
export function extractSessionToolIntervals(
|
|
161
|
+
rows: readonly LedgerSessionRow[],
|
|
162
|
+
): SessionToolInterval[] {
|
|
163
|
+
type Open = {
|
|
164
|
+
toolCallId: string;
|
|
165
|
+
toolName: string;
|
|
166
|
+
startedAt: string;
|
|
167
|
+
endedAt?: string;
|
|
168
|
+
command?: string;
|
|
169
|
+
};
|
|
170
|
+
const order: Open[] = [];
|
|
171
|
+
const openById = new Map<string, Open>();
|
|
172
|
+
|
|
173
|
+
for (const row of rows) {
|
|
174
|
+
const rowTimestamp = typeof row.timestamp === "string" ? row.timestamp : undefined;
|
|
175
|
+
const message = isRecord(row.message) ? row.message : undefined;
|
|
176
|
+
|
|
177
|
+
if (message?.role === "assistant" && Array.isArray(message.content)) {
|
|
178
|
+
const callTimestamp =
|
|
179
|
+
typeof message.timestamp === "string" && message.timestamp
|
|
180
|
+
? message.timestamp
|
|
181
|
+
: rowTimestamp;
|
|
182
|
+
for (const part of message.content) {
|
|
183
|
+
if (!isRecord(part) || part.type !== "toolCall") continue;
|
|
184
|
+
if (typeof part.id !== "string" || part.id.length === 0) {
|
|
185
|
+
throw new Error("toolCall frame missing string id");
|
|
186
|
+
}
|
|
187
|
+
if (typeof part.name !== "string" || part.name.length === 0) {
|
|
188
|
+
throw new Error(`toolCall ${part.id} missing string name`);
|
|
189
|
+
}
|
|
190
|
+
if (callTimestamp === undefined || callTimestamp.length === 0) {
|
|
191
|
+
throw new Error(`toolCall ${part.id} missing timestamp`);
|
|
192
|
+
}
|
|
193
|
+
if (openById.has(part.id)) {
|
|
194
|
+
throw new Error(`duplicate toolCall id ${part.id}`);
|
|
195
|
+
}
|
|
196
|
+
const args = isRecord(part.arguments) ? part.arguments : undefined;
|
|
197
|
+
// Ticket surface: only bash first-line summary is authorized here.
|
|
198
|
+
const command =
|
|
199
|
+
part.name === "bash" &&
|
|
200
|
+
args !== undefined &&
|
|
201
|
+
typeof args.command === "string"
|
|
202
|
+
? bashCommandFirstLine(args.command)
|
|
203
|
+
: undefined;
|
|
204
|
+
const interval: Open = {
|
|
205
|
+
toolCallId: part.id,
|
|
206
|
+
toolName: part.name,
|
|
207
|
+
startedAt: callTimestamp,
|
|
208
|
+
...(command !== undefined ? { command } : {}),
|
|
209
|
+
};
|
|
210
|
+
order.push(interval);
|
|
211
|
+
openById.set(part.id, interval);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (message?.role === "toolResult") {
|
|
216
|
+
if (typeof message.toolCallId !== "string" || message.toolCallId.length === 0) {
|
|
217
|
+
throw new Error("toolResult frame missing string toolCallId");
|
|
218
|
+
}
|
|
219
|
+
const resultTimestamp =
|
|
220
|
+
typeof message.timestamp === "string" && message.timestamp
|
|
221
|
+
? message.timestamp
|
|
222
|
+
: rowTimestamp;
|
|
223
|
+
if (resultTimestamp === undefined || resultTimestamp.length === 0) {
|
|
224
|
+
throw new Error(`toolResult ${message.toolCallId} missing timestamp`);
|
|
225
|
+
}
|
|
226
|
+
const open = openById.get(message.toolCallId);
|
|
227
|
+
if (open === undefined) {
|
|
228
|
+
// Result without a prior call is still associable as a closed interval
|
|
229
|
+
// once a name is known; keep structural readability without inventing a call.
|
|
230
|
+
const toolName =
|
|
231
|
+
typeof message.toolName === "string" && message.toolName.length > 0
|
|
232
|
+
? message.toolName
|
|
233
|
+
: "unknown";
|
|
234
|
+
order.push({
|
|
235
|
+
toolCallId: message.toolCallId,
|
|
236
|
+
toolName,
|
|
237
|
+
startedAt: resultTimestamp,
|
|
238
|
+
endedAt: resultTimestamp,
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (open.endedAt !== undefined) {
|
|
243
|
+
throw new Error(`duplicate toolResult for toolCallId ${message.toolCallId}`);
|
|
244
|
+
}
|
|
245
|
+
open.endedAt = resultTimestamp;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return order.map((interval) => {
|
|
250
|
+
const base = {
|
|
251
|
+
toolCallId: interval.toolCallId,
|
|
252
|
+
toolName: interval.toolName,
|
|
253
|
+
startedAt: interval.startedAt,
|
|
254
|
+
...(interval.command !== undefined ? { command: interval.command } : {}),
|
|
255
|
+
};
|
|
256
|
+
return interval.endedAt === undefined
|
|
257
|
+
? base
|
|
258
|
+
: { ...base, endedAt: interval.endedAt };
|
|
259
|
+
});
|
|
260
|
+
}
|
|
@@ -26,11 +26,15 @@ export type RuntimeReviewerAcceptedBatch = Readonly<{
|
|
|
26
26
|
identity: string;
|
|
27
27
|
legs: readonly Readonly<{ axis: "standards" | "spec"; prompt: ReviewerReceiptPrompt }>[];
|
|
28
28
|
}>;
|
|
29
|
+
/** Honest Spec-child disposition on the receipt face. */
|
|
30
|
+
export type RuntimeReviewerSpecDisposition = "launched" | "skipped-missing";
|
|
29
31
|
export type RuntimeReviewerReceiptV2 = Readonly<{
|
|
30
32
|
version: 2;
|
|
31
33
|
status: "completed" | "refused";
|
|
32
34
|
diagnostic?: string;
|
|
33
35
|
acceptedBatch?: RuntimeReviewerAcceptedBatch;
|
|
36
|
+
/** Present on accepted batches: launched Spec child, or skipped after confirmed missing Spec. */
|
|
37
|
+
specDisposition?: RuntimeReviewerSpecDisposition;
|
|
34
38
|
reports: Readonly<Partial<Record<"standards" | "spec", VerbatimChildReport>>>;
|
|
35
39
|
outcomes: Readonly<Partial<Record<"standards" | "spec", RuntimeReviewerOutcome>>>;
|
|
36
40
|
identities: Readonly<{
|
|
@@ -100,6 +104,21 @@ export function validateRuntimeReviewerReceipt(output: unknown): RuntimeReviewer
|
|
|
100
104
|
throw new Error("Successful Reviewer outcome lacks report");
|
|
101
105
|
if (status === "failed" && report !== undefined) throw new Error("Failed Reviewer outcome cannot bind a report");
|
|
102
106
|
}
|
|
107
|
+
|
|
108
|
+
// One read inside the existing accepted-leg consistency cycle — not a second validator.
|
|
109
|
+
// launched ⇔ Standards+Spec; skipped-missing ⇔ Standards only.
|
|
110
|
+
const specDisposition = read(output, "specDisposition");
|
|
111
|
+
if (specDisposition === "launched") {
|
|
112
|
+
if (expectedAxes.length !== 2 || expectedAxes[0] !== "standards" || expectedAxes[1] !== "spec") {
|
|
113
|
+
throw new Error("Reviewer specDisposition launched requires Standards+Spec accepted legs");
|
|
114
|
+
}
|
|
115
|
+
} else if (specDisposition === "skipped-missing") {
|
|
116
|
+
if (expectedAxes.length !== 1 || expectedAxes[0] !== "standards") {
|
|
117
|
+
throw new Error("Reviewer specDisposition skipped-missing requires Standards-only accepted legs");
|
|
118
|
+
}
|
|
119
|
+
} else if (specDisposition !== undefined) {
|
|
120
|
+
throw new Error("Invalid Reviewer specDisposition");
|
|
121
|
+
}
|
|
103
122
|
}
|
|
104
123
|
return output as RuntimeReviewerReceiptV2;
|
|
105
124
|
}
|
package/src/public-cli/cli.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
parseJudgeArgv,
|
|
33
33
|
parseMergerArgv,
|
|
34
34
|
parseReviewerArgv,
|
|
35
|
+
parseTaishiArgv,
|
|
35
36
|
} from "./invocation.ts";
|
|
36
37
|
import { runPublicCoder, runPublicCoderResume } from "./coder-run.ts";
|
|
37
38
|
import { runPublicCollector } from "./collector-run.ts";
|
|
@@ -40,6 +41,7 @@ import { runPublicFixer, runPublicFixerResume } from "./fixer-run.ts";
|
|
|
40
41
|
import { runPublicJudge, runPublicResume } from "./judge-run.ts";
|
|
41
42
|
import { runPublicMerger, runPublicMergerResume } from "./merger-run.ts";
|
|
42
43
|
import { runPublicReviewer, runPublicReviewerResume } from "./reviewer-run.ts";
|
|
44
|
+
import { runPublicTaishi } from "./taishi-run.ts";
|
|
43
45
|
import { peekRoleRunRole } from "./run-lifecycle.ts";
|
|
44
46
|
import {
|
|
45
47
|
INTERNAL_ROLE_ENTRYPOINT_RELATIVE,
|
|
@@ -73,6 +75,8 @@ export const PUBLIC_ROLE_ARGV = {
|
|
|
73
75
|
doctor: { parse: parseDoctorArgv },
|
|
74
76
|
merger: { parse: parseMergerArgv },
|
|
75
77
|
reviewer: { parse: parseReviewerArgv },
|
|
78
|
+
/** Deterministic analysis seat (#336) — argv parse only; no LLM admission. */
|
|
79
|
+
taishi: { parse: parseTaishiArgv },
|
|
76
80
|
} as const;
|
|
77
81
|
|
|
78
82
|
type TakenPublicGlobalFlag =
|
|
@@ -314,6 +318,12 @@ function renderHelp(): string {
|
|
|
314
318
|
lines.push(` ${cap.name} — ${phaseText}`);
|
|
315
319
|
}
|
|
316
320
|
}
|
|
321
|
+
lines.push("", "Deterministic commands:");
|
|
322
|
+
for (const cap of doc.capabilities) {
|
|
323
|
+
if (cap.kind === "deterministic") {
|
|
324
|
+
lines.push(` ${cap.name}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
317
327
|
lines.push(
|
|
318
328
|
"",
|
|
319
329
|
"Global options: --model provider/model --thinking level",
|
|
@@ -430,6 +440,8 @@ export async function runAkRole(
|
|
|
430
440
|
}
|
|
431
441
|
if (match.kind === "support") {
|
|
432
442
|
io.stdout(`command\t${match.name}\tkind\tsupport\n`);
|
|
443
|
+
} else if (match.kind === "deterministic") {
|
|
444
|
+
io.stdout(`command\t${match.name}\tkind\tdeterministic\n`);
|
|
433
445
|
} else {
|
|
434
446
|
io.stdout(
|
|
435
447
|
`command\t${match.name}\tkind\trole\tphases\t${match.phases
|
|
@@ -949,6 +961,18 @@ export async function runAkRole(
|
|
|
949
961
|
};
|
|
950
962
|
}
|
|
951
963
|
|
|
964
|
+
// Taishi public run path: deterministic analysis seat (#336 issue / #337 sweep).
|
|
965
|
+
// Not an LLM PUBLIC_CALLABLE_ROLE — registered only on PUBLIC_ROLE_ARGV (#176).
|
|
966
|
+
if (parsed.command === "taishi") {
|
|
967
|
+
const result = await runPublicTaishi(
|
|
968
|
+
parsed.args,
|
|
969
|
+
{ home },
|
|
970
|
+
io,
|
|
971
|
+
PUBLIC_ROLE_ARGV.taishi.parse,
|
|
972
|
+
);
|
|
973
|
+
return { exitCode: result.exitCode };
|
|
974
|
+
}
|
|
975
|
+
|
|
952
976
|
// #115: every PUBLIC_CALLABLE_ROLE has a completed handler above. Unknown
|
|
953
977
|
// tokens (including misspelled role names) are structural rejects.
|
|
954
978
|
throw new CliUsageError(`unknown command: ${parsed.command}`);
|