@akagilnc/pi-workflow-roles 0.1.3749 → 0.1.3771
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/CLAUDE.md +4 -0
- package/README.md +3 -2
- package/README.zh-CN.md +3 -2
- package/dist/acp-host/production-host.js +1389 -751
- package/dist/collector-config.js +0 -1
- package/dist/collector-github.js +199 -2
- package/dist/collector-identity.js +128 -41
- package/dist/collector-ledger.js +38 -9
- package/dist/collector-receipt.js +19 -7
- package/dist/collector-role.js +330 -369
- package/dist/collector-target.js +169 -0
- package/dist/collector-tool-schemas.js +51 -14
- package/dist/package-contracts/collector-output.js +32 -0
- package/dist/package-contracts/terminating-infrastructure.js +13 -12
- package/dist/pi/role-turn-host.js +1 -2
- package/dist/public-cli/github-remote.js +45 -0
- package/dist/public-cli/invocation.js +53 -54
- package/dist/public-cli/main.js +615 -148
- package/dist/public-cli/option-definitions.js +6 -4
- package/dist/public-cli/run-lifecycle.js +3 -3
- package/dist/public-cli/settlement.js +83 -6
- package/dist/role-runtime.js +137 -7
- package/dist/submission-correctable-error.js +24 -0
- package/extensions/role-runtime.ts +0 -1
- package/package.json +1 -1
- package/souls/coder.md +11 -6
- package/souls/fixer.md +10 -9
- package/src/acp-host/role-envelope.ts +8 -22
- package/src/collector-config.ts +0 -1
- package/src/collector-github.ts +236 -2
- package/src/collector-identity.ts +148 -40
- package/src/collector-ledger.ts +48 -10
- package/src/collector-receipt.ts +33 -14
- package/src/collector-role.ts +376 -450
- package/src/collector-target.ts +207 -0
- package/src/collector-tool-schemas.ts +62 -15
- package/src/host-contracts.ts +2 -1
- package/src/package-contracts/collector-output.ts +72 -0
- package/src/package-contracts/terminating-infrastructure.ts +24 -13
- package/src/pi/role-turn-host.ts +1 -2
- package/src/public-cli/cli.ts +10 -1
- package/src/public-cli/collector-run.ts +3 -2
- package/src/public-cli/github-remote.ts +45 -0
- package/src/public-cli/invocation.ts +60 -59
- package/src/public-cli/option-definitions.ts +6 -4
- package/src/public-cli/run-lifecycle.ts +2 -2
- package/src/public-cli/settlement.ts +82 -6
- package/src/role-runtime.ts +166 -13
- package/src/submission-correctable-error.ts +38 -0
|
@@ -31,13 +31,14 @@ import {
|
|
|
31
31
|
} from "../doctor-evidence.ts";
|
|
32
32
|
import type { DoctorCaseIdentity } from "../doctor-contracts.ts";
|
|
33
33
|
import {
|
|
34
|
-
COLLECTOR_FIXED_KICKOFF,
|
|
35
34
|
emptyCollectorManifest,
|
|
36
35
|
loadCollectorManifest,
|
|
37
36
|
parseCollectorPrNumber,
|
|
38
37
|
parseCollectorRepository,
|
|
39
38
|
type CollectorRepository,
|
|
40
39
|
} from "../collector-config.ts";
|
|
40
|
+
import { resolveCollectorTarget } from "../collector-target.ts";
|
|
41
|
+
import { ownerRepoFromGitHubRemoteUrl } from "./github-remote.ts";
|
|
41
42
|
import {
|
|
42
43
|
FixerPacketValidationError,
|
|
43
44
|
parseFixerPrerequisites,
|
|
@@ -172,7 +173,8 @@ export type AdmittedFixerInvocation = AdmittedRoleInvocationBase & {
|
|
|
172
173
|
|
|
173
174
|
export type AdmittedCollectorInvocation = AdmittedRoleInvocationBase & {
|
|
174
175
|
readonly role: "collector";
|
|
175
|
-
|
|
176
|
+
/** Bound at admission when explicit/--pr or unique head/commit; otherwise role binds. */
|
|
177
|
+
readonly prNumber?: number;
|
|
176
178
|
readonly repository: CollectorRepository;
|
|
177
179
|
readonly requestManifestPath?: string;
|
|
178
180
|
readonly manifestDigest: string;
|
|
@@ -718,7 +720,8 @@ export type ParseFixerArgvResult = {
|
|
|
718
720
|
};
|
|
719
721
|
|
|
720
722
|
export type ParseCollectorArgvResult = {
|
|
721
|
-
|
|
723
|
+
/** Explicit --pr when provided; admission resolves from context when absent (#676 D1). */
|
|
724
|
+
prNumber?: number;
|
|
722
725
|
instruction: string;
|
|
723
726
|
attachmentPaths: string[];
|
|
724
727
|
project?: string;
|
|
@@ -1759,10 +1762,11 @@ export function parseCollectorArgv(args: readonly string[]): ParseCollectorArgvR
|
|
|
1759
1762
|
}
|
|
1760
1763
|
positional.push(token);
|
|
1761
1764
|
}
|
|
1762
|
-
// Unconditional required
|
|
1765
|
+
// Unconditional required from typed table via shared consumer (#342).
|
|
1766
|
+
// #676 D1: --pr is optional; ambiguous targets reject at admission, not by guessing.
|
|
1763
1767
|
options.assertRequired();
|
|
1764
1768
|
return {
|
|
1765
|
-
prNumber: prNumber
|
|
1769
|
+
...(prNumber === undefined ? {} : { prNumber }),
|
|
1766
1770
|
instruction: positional.join(" "),
|
|
1767
1771
|
attachmentPaths,
|
|
1768
1772
|
...(project === undefined ? {} : { project }),
|
|
@@ -1774,6 +1778,7 @@ export function parseCollectorArgv(args: readonly string[]): ParseCollectorArgvR
|
|
|
1774
1778
|
/**
|
|
1775
1779
|
* Resolve owner/repo from the project's `origin` remote (github.com only).
|
|
1776
1780
|
* Supports https and SSH GitHub URL shapes; never scrapes instruction prose.
|
|
1781
|
+
* Missing origin / non-github remote → usage. Git execution failure → true cause (exit 1).
|
|
1777
1782
|
*/
|
|
1778
1783
|
export function resolveGitHubRemoteRepository(
|
|
1779
1784
|
projectRoot: string,
|
|
@@ -1786,10 +1791,16 @@ export function resolveGitHubRemoteRepository(
|
|
|
1786
1791
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1787
1792
|
}).trim();
|
|
1788
1793
|
} catch (error) {
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1794
|
+
// git remote get-url exit 2 = no such remote; other failures keep true cause.
|
|
1795
|
+
if (isGitRemoteMissing(error)) {
|
|
1796
|
+
throw new CliUsageError(
|
|
1797
|
+
"collector requires a github.com origin remote or an explicit --repo owner/repo",
|
|
1798
|
+
{ cause: error },
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
throw new Error("collector git failed: cannot read origin remote URL", {
|
|
1802
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
1803
|
+
});
|
|
1793
1804
|
}
|
|
1794
1805
|
if (remoteUrl.length === 0) {
|
|
1795
1806
|
throw new CliUsageError(
|
|
@@ -1811,44 +1822,22 @@ export function resolveGitHubRemoteRepository(
|
|
|
1811
1822
|
}
|
|
1812
1823
|
}
|
|
1813
1824
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
const ssh = /^ssh:\/\/git@github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i.exec(
|
|
1823
|
-
trimmed,
|
|
1824
|
-
);
|
|
1825
|
-
if (ssh) {
|
|
1826
|
-
return `${ssh[1]}/${stripGitSuffix(ssh[2]!)}`;
|
|
1827
|
-
}
|
|
1828
|
-
// https://github.com/owner/repo(.git) and git://github.com/... — exact two-segment path.
|
|
1829
|
-
let parsed: URL;
|
|
1830
|
-
try {
|
|
1831
|
-
parsed = new URL(trimmed);
|
|
1832
|
-
} catch {
|
|
1833
|
-
return undefined;
|
|
1834
|
-
}
|
|
1835
|
-
if (!/^github\.com$/i.test(parsed.hostname)) return undefined;
|
|
1836
|
-
// Non-identity URL material (query/hash/extra path) is not a repository remote.
|
|
1837
|
-
if (parsed.search !== "" || parsed.hash !== "") return undefined;
|
|
1838
|
-
const parts = parsed.pathname.split("/").filter((p) => p.length > 0);
|
|
1839
|
-
if (parts.length !== 2) return undefined;
|
|
1840
|
-
return `${parts[0]}/${stripGitSuffix(parts[1]!)}`;
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
function stripGitSuffix(name: string): string {
|
|
1844
|
-
return name.toLowerCase().endsWith(".git") ? name.slice(0, -4) : name;
|
|
1825
|
+
/**
|
|
1826
|
+
* git remote get-url: exit 2 = no such remote on common git (missing config).
|
|
1827
|
+
* Other statuses keep true cause — do not broaden into usage (#676 B).
|
|
1828
|
+
*/
|
|
1829
|
+
function isGitRemoteMissing(error: unknown): boolean {
|
|
1830
|
+
if (typeof error !== "object" || error === null) return false;
|
|
1831
|
+
const status = (error as { status?: unknown }).status;
|
|
1832
|
+
return status === 2;
|
|
1845
1833
|
}
|
|
1846
1834
|
|
|
1847
1835
|
export type AdmitCollectorInvocationOptions = {
|
|
1848
1836
|
home: string;
|
|
1849
1837
|
principalAuthority: DurablePrincipalAuthority;
|
|
1850
1838
|
cwd: string;
|
|
1851
|
-
|
|
1839
|
+
/** Explicit PR when provided; resolved from context when absent (#676 D1). */
|
|
1840
|
+
prNumber?: number;
|
|
1852
1841
|
instruction?: string;
|
|
1853
1842
|
attachmentPaths?: readonly string[];
|
|
1854
1843
|
project?: string;
|
|
@@ -1863,8 +1852,8 @@ export type AdmitCollectorInvocationOptions = {
|
|
|
1863
1852
|
|
|
1864
1853
|
/**
|
|
1865
1854
|
* Admit a Collector Role run: assemble the retained leg manifest from typed
|
|
1866
|
-
* declarations, resolve repository
|
|
1867
|
-
*
|
|
1855
|
+
* declarations, resolve repository + PR target (#676 D1), and place the session under #78.
|
|
1856
|
+
* Explicit PR is not preflighted for existence; context resolution uses online association.
|
|
1868
1857
|
*/
|
|
1869
1858
|
export async function admitCollectorInvocation(
|
|
1870
1859
|
options: AdmitCollectorInvocationOptions,
|
|
@@ -1872,12 +1861,14 @@ export async function admitCollectorInvocation(
|
|
|
1872
1861
|
if (options.project !== undefined) {
|
|
1873
1862
|
requireOptionPath("--project", options.project);
|
|
1874
1863
|
}
|
|
1875
|
-
let
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1864
|
+
let explicitPrNumber: number | undefined;
|
|
1865
|
+
if (options.prNumber !== undefined) {
|
|
1866
|
+
try {
|
|
1867
|
+
explicitPrNumber = parseCollectorPrNumber(options.prNumber);
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1870
|
+
throw new CliUsageError(detail, { cause: error });
|
|
1871
|
+
}
|
|
1881
1872
|
}
|
|
1882
1873
|
|
|
1883
1874
|
const projectRoot = resolve(options.project ?? options.cwd);
|
|
@@ -1924,16 +1915,25 @@ export async function admitCollectorInvocation(
|
|
|
1924
1915
|
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
1925
1916
|
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
1926
1917
|
|
|
1918
|
+
// #676 A: freeze task materials BEFORE target resolution so the role receives
|
|
1919
|
+
// real instruction + attachments. Admission binds only explicit --pr or unique
|
|
1920
|
+
// head/commit association — task-text scrape is not a target lock.
|
|
1927
1921
|
const attachments = await freezeAttachments(options.attachmentPaths ?? [], attachmentsDirectory);
|
|
1922
|
+
const instruction = options.instruction ?? "";
|
|
1923
|
+
const instructionEmpty = instruction.trim() === "";
|
|
1924
|
+
|
|
1925
|
+
const target = await resolveCollectorTarget({
|
|
1926
|
+
projectRoot,
|
|
1927
|
+
repository,
|
|
1928
|
+
...(explicitPrNumber === undefined ? {} : { explicitPrNumber }),
|
|
1929
|
+
});
|
|
1930
|
+
const prNumber = target.kind === "bound" ? target.prNumber : undefined;
|
|
1928
1931
|
|
|
1929
1932
|
let requestManifestPath: string | undefined;
|
|
1930
1933
|
if (manifestCanonicalJson !== undefined) {
|
|
1931
1934
|
requestManifestPath = join(runDirectory, "request-manifest.json");
|
|
1932
1935
|
await writeFile(requestManifestPath, manifestCanonicalJson, "utf8");
|
|
1933
1936
|
}
|
|
1934
|
-
|
|
1935
|
-
const instruction = options.instruction ?? "";
|
|
1936
|
-
const instructionEmpty = instruction.trim() === "";
|
|
1937
1937
|
const admitted = {
|
|
1938
1938
|
role: "collector" as const,
|
|
1939
1939
|
runId,
|
|
@@ -1943,7 +1943,7 @@ export async function admitCollectorInvocation(
|
|
|
1943
1943
|
principal,
|
|
1944
1944
|
instruction,
|
|
1945
1945
|
instructionEmpty,
|
|
1946
|
-
prNumber,
|
|
1946
|
+
...(prNumber === undefined ? {} : { prNumber }),
|
|
1947
1947
|
repository: repository.canonical,
|
|
1948
1948
|
repositoryDisplay: repository.display,
|
|
1949
1949
|
...(requestManifestPath === undefined ? {} : { requestManifestPath }),
|
|
@@ -1974,7 +1974,7 @@ export async function admitCollectorInvocation(
|
|
|
1974
1974
|
runDirectory,
|
|
1975
1975
|
principal,
|
|
1976
1976
|
admittedRequestPath,
|
|
1977
|
-
prNumber,
|
|
1977
|
+
...(prNumber === undefined ? {} : { prNumber }),
|
|
1978
1978
|
repository,
|
|
1979
1979
|
...(requestManifestPath === undefined ? {} : { requestManifestPath }),
|
|
1980
1980
|
manifestDigest,
|
|
@@ -1982,15 +1982,16 @@ export async function admitCollectorInvocation(
|
|
|
1982
1982
|
}
|
|
1983
1983
|
|
|
1984
1984
|
/**
|
|
1985
|
-
* Collector
|
|
1986
|
-
*
|
|
1985
|
+
* #676 A: Collector consumes the real call task + frozen attachments so the role
|
|
1986
|
+
* can identify issue/PR from materials via ak_collector_bind_target. Explicit --pr
|
|
1987
|
+
* still wins at admission; unique head/commit association also binds. No fixed
|
|
1988
|
+
* kickoff rewrite of the caller task; no mechanical task-text target lock.
|
|
1987
1989
|
*/
|
|
1988
1990
|
export function buildCollectorTransportPrompt(
|
|
1989
|
-
|
|
1991
|
+
admitted: AdmittedCollectorInvocation,
|
|
1990
1992
|
engineMaterial?: EngineSessionMaterial,
|
|
1991
1993
|
): string {
|
|
1992
|
-
|
|
1993
|
-
return appendEngineSessionMaterial([COLLECTOR_FIXED_KICKOFF], engineMaterial).join("\n");
|
|
1994
|
+
return buildInstructionTransportPrompt(admitted, engineMaterial);
|
|
1994
1995
|
}
|
|
1995
1996
|
|
|
1996
1997
|
/** Positive Issue number grammar shared with Doctor case path identity. */
|
|
@@ -565,12 +565,13 @@ const COLLECTOR_OPTIONS = [
|
|
|
565
565
|
canonical: "--pr",
|
|
566
566
|
aliases: [],
|
|
567
567
|
valueMetavar: "number",
|
|
568
|
-
|
|
568
|
+
// #676 D1: optional when context uniquely determines the PR; ambiguous → require explicit.
|
|
569
|
+
required: false,
|
|
569
570
|
repeatable: false,
|
|
570
571
|
form: "option",
|
|
571
572
|
description: {
|
|
572
|
-
en: "
|
|
573
|
-
zh: "
|
|
573
|
+
en: "Positive GitHub pull request number. Optional when unique branch/HEAD association binds the PR, or when the Collector role can decide the target from task materials via bind-target; multi-candidate git context or a role that cannot decide requires an explicit value.",
|
|
574
|
+
zh: "正整数 GitHub PR 号。分支/HEAD 唯一关联可绑定时可省略;亦可由通进司从任务材料经 bind-target 判定。git 多候选或角色无法判定时必须显式提供。",
|
|
574
575
|
},
|
|
575
576
|
},
|
|
576
577
|
{
|
|
@@ -1199,9 +1200,10 @@ const ROLE_COMMAND_HELP = {
|
|
|
1199
1200
|
collector: {
|
|
1200
1201
|
command: "collector",
|
|
1201
1202
|
summary: "Collect GitHub PR review evidence.",
|
|
1202
|
-
usage: ["ak-role collector --pr <number> [options] [instruction]"],
|
|
1203
|
+
usage: ["ak-role collector [--pr <number>] [options] [instruction]"],
|
|
1203
1204
|
examples: [
|
|
1204
1205
|
"ak-role collector --pr 42 --repo owner/repository",
|
|
1206
|
+
"ak-role collector --repo owner/repository \"Collect findings for #42\"",
|
|
1205
1207
|
"ak-role collector --pr 42 --request-manifest ./requests.json",
|
|
1206
1208
|
],
|
|
1207
1209
|
},
|
|
@@ -1883,8 +1883,8 @@ export async function loadResumableCollectorRun(
|
|
|
1883
1883
|
);
|
|
1884
1884
|
}
|
|
1885
1885
|
const { prNumber, repository, repositoryDisplay, manifestDigest } = loaded.admittedFields;
|
|
1886
|
+
// #676 A: prNumber may be unbound at admission (role bind-target). Repository is required.
|
|
1886
1887
|
if (
|
|
1887
|
-
prNumber === undefined ||
|
|
1888
1888
|
repository === undefined ||
|
|
1889
1889
|
repositoryDisplay === undefined ||
|
|
1890
1890
|
manifestDigest === undefined
|
|
@@ -1908,7 +1908,7 @@ export async function loadResumableCollectorRun(
|
|
|
1908
1908
|
const admitted: AdmittedCollectorInvocation = {
|
|
1909
1909
|
role: "collector",
|
|
1910
1910
|
...resumedBaseAdmitted(loaded),
|
|
1911
|
-
prNumber,
|
|
1911
|
+
...(prNumber === undefined ? {} : { prNumber }),
|
|
1912
1912
|
repository: parsedRepository,
|
|
1913
1913
|
...(loaded.admittedFields.requestManifestPath === undefined
|
|
1914
1914
|
? {}
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
// COMPLIANCE_RESPONSE_ENTRY_TYPE remains for boundRetainedAuditResponse (call/result
|
|
45
45
|
// interval binding on historical session bytes). Provider-stop retain authority is Sitian.
|
|
46
46
|
import {
|
|
47
|
+
COLLECTOR_BIND_TARGET_TOOL,
|
|
47
48
|
COLLECTOR_OBSERVE_TOOL,
|
|
48
49
|
COLLECTOR_READ_TOOL,
|
|
49
50
|
COLLECTOR_REQUEST_TOOL,
|
|
@@ -375,12 +376,36 @@ export function formatFailureStderrDiagnostic(failure: ControlledFailure): strin
|
|
|
375
376
|
return formatCliDiagnostic(boundConciseDiagnostic(oneLine));
|
|
376
377
|
}
|
|
377
378
|
|
|
378
|
-
/**
|
|
379
|
+
/**
|
|
380
|
+
* #676 B: one cause face for public stderr — Error.message, else object JSON with
|
|
381
|
+
* status/body/headers when present, else String. Never wash objects to [object Object].
|
|
382
|
+
*/
|
|
383
|
+
export function formatErrorCauseDetail(cause: unknown): string {
|
|
384
|
+
if (cause instanceof Error) return cause.message;
|
|
385
|
+
if (typeof cause === "object" && cause !== null) {
|
|
386
|
+
try {
|
|
387
|
+
return JSON.stringify(cause);
|
|
388
|
+
} catch {
|
|
389
|
+
return String(cause);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return String(cause);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Pre-admission structural rejection: stderr only, no run, no Terminal. Cause retained when present. */
|
|
379
396
|
export function presentStructuralRejection(
|
|
380
|
-
error: { message: string },
|
|
397
|
+
error: { message: string; cause?: unknown },
|
|
381
398
|
io: { stderr: (text: string) => void },
|
|
382
399
|
): void {
|
|
383
|
-
|
|
400
|
+
let message = error.message;
|
|
401
|
+
const cause = error.cause;
|
|
402
|
+
if (cause !== undefined) {
|
|
403
|
+
const detail = formatErrorCauseDetail(cause);
|
|
404
|
+
if (detail.trim().length > 0) {
|
|
405
|
+
message = `${message}; cause: ${detail}`;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
io.stderr(formatCliDiagnostic(message));
|
|
384
409
|
}
|
|
385
410
|
|
|
386
411
|
/** ControlledFailure face without admitted-run Terminal (stdout body + stderr line). */
|
|
@@ -1520,6 +1545,34 @@ function toolResultText(message: SessionMessage): string {
|
|
|
1520
1545
|
.trim();
|
|
1521
1546
|
}
|
|
1522
1547
|
|
|
1548
|
+
/**
|
|
1549
|
+
* #676 D1/J3: durable bind-target rejection on the current attempt → public no_receipt facts.
|
|
1550
|
+
* Callers must learn they need explicit --pr without opening the session 正本.
|
|
1551
|
+
* Authority is the latest bind-target toolResult only — a later success clears an earlier failure.
|
|
1552
|
+
*/
|
|
1553
|
+
function extractCollectorTargetBindRejection(
|
|
1554
|
+
entries: readonly SessionEntry[],
|
|
1555
|
+
): { readonly diagnostic: string; readonly code?: string } | undefined {
|
|
1556
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1557
|
+
const entry = entries[i];
|
|
1558
|
+
if (entry?.type !== "message") continue;
|
|
1559
|
+
const message = entry.message;
|
|
1560
|
+
if (message?.role !== "toolResult") continue;
|
|
1561
|
+
if (message.toolName !== COLLECTOR_BIND_TARGET_TOOL) continue;
|
|
1562
|
+
// Latest bind result wins: success means the attempt is no longer rejected.
|
|
1563
|
+
if (message.isError !== true) return undefined;
|
|
1564
|
+
const diagnostic = toolResultText(message);
|
|
1565
|
+
if (diagnostic.length === 0) return undefined;
|
|
1566
|
+
const details = message.details;
|
|
1567
|
+
const code =
|
|
1568
|
+
isRecord(details) && typeof details.code === "string" && details.code.trim() !== ""
|
|
1569
|
+
? details.code
|
|
1570
|
+
: undefined;
|
|
1571
|
+
return code === undefined ? { diagnostic } : { diagnostic, code };
|
|
1572
|
+
}
|
|
1573
|
+
return undefined;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1523
1576
|
type BoundErroredToolCandidate = {
|
|
1524
1577
|
candidate: unknown;
|
|
1525
1578
|
diagnostic: string;
|
|
@@ -1697,7 +1750,9 @@ export function assertCollectorReceiptMatchesAdmitted(
|
|
|
1697
1750
|
`Collector receipt repository "${receipt.repository}" does not match admitted repository "${admitted.repository.canonical}"`,
|
|
1698
1751
|
);
|
|
1699
1752
|
}
|
|
1700
|
-
|
|
1753
|
+
// #676 A: when admission left the target unbound, receipt.prNumber is the role bind.
|
|
1754
|
+
// When admission bound explicitly/head-commit, receipt must match.
|
|
1755
|
+
if (admitted.prNumber !== undefined && receipt.prNumber !== admitted.prNumber) {
|
|
1701
1756
|
throw collectorReceiptBindingFailure(
|
|
1702
1757
|
`Collector receipt prNumber ${receipt.prNumber} does not match admitted prNumber ${admitted.prNumber}`,
|
|
1703
1758
|
);
|
|
@@ -2787,7 +2842,7 @@ export async function publishCollectorArtifacts(
|
|
|
2787
2842
|
{
|
|
2788
2843
|
runId: admitted.runId,
|
|
2789
2844
|
role: "collector",
|
|
2790
|
-
prNumber: admitted.prNumber,
|
|
2845
|
+
...(admitted.prNumber === undefined ? {} : { prNumber: admitted.prNumber }),
|
|
2791
2846
|
repository: admitted.repository.canonical,
|
|
2792
2847
|
manifestDigest: admitted.manifestDigest,
|
|
2793
2848
|
sessionDirectory: coordinates.sessionDirectory,
|
|
@@ -4178,7 +4233,19 @@ export async function settleFailureTerminalResult(
|
|
|
4178
4233
|
try {
|
|
4179
4234
|
const facts = parseNoReceiptLifecycleFacts(raw);
|
|
4180
4235
|
if (facts.runPointer === admitted.runDirectory && facts.attemptPointer === `current:${admitted.runDirectory}`) {
|
|
4181
|
-
|
|
4236
|
+
let decisiveFacts: NoReceiptLifecycleFacts & Record<string, unknown> = facts;
|
|
4237
|
+
// #676 D1/J3: project durable bind-target rejection onto public no_receipt facts.
|
|
4238
|
+
if (admitted.role === "collector") {
|
|
4239
|
+
const bindRejection = extractCollectorTargetBindRejection(entries.slice(attemptStart));
|
|
4240
|
+
if (bindRejection !== undefined) {
|
|
4241
|
+
decisiveFacts = {
|
|
4242
|
+
...facts,
|
|
4243
|
+
targetBindRejected: true,
|
|
4244
|
+
targetBindDiagnostic: bindRejection.diagnostic,
|
|
4245
|
+
...(bindRejection.code === undefined ? {} : { targetBindCode: bindRejection.code }),
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4182
4249
|
// #478: no_receipt is still a public Terminal — project accepted gate facts.
|
|
4183
4250
|
return withOptionalGateProjection(
|
|
4184
4251
|
{
|
|
@@ -4285,6 +4352,15 @@ export function presentFailureTerminal(
|
|
|
4285
4352
|
cause: terminal.roleOutcome.cause,
|
|
4286
4353
|
diagnostic: terminal.roleOutcome.diagnostic,
|
|
4287
4354
|
}));
|
|
4355
|
+
return;
|
|
4356
|
+
}
|
|
4357
|
+
// #676 D1/J3: no_receipt stays lawful exit 0; surface durable target-bind clarification on stderr.
|
|
4358
|
+
const bindDiagnostic = terminal.roleOutcome.decisiveFacts.targetBindDiagnostic;
|
|
4359
|
+
if (typeof bindDiagnostic === "string" && bindDiagnostic.trim() !== "") {
|
|
4360
|
+
io.stderr(formatFailureStderrDiagnostic({
|
|
4361
|
+
cause: "output",
|
|
4362
|
+
diagnostic: bindDiagnostic,
|
|
4363
|
+
}));
|
|
4288
4364
|
}
|
|
4289
4365
|
}
|
|
4290
4366
|
|
package/src/role-runtime.ts
CHANGED
|
@@ -43,12 +43,10 @@ import type { AnyCanonicalSkillBinding } from "./canonical-skill-binding.ts";
|
|
|
43
43
|
import type { CollectorClock } from "./collector-evidence.ts";
|
|
44
44
|
import type { CollectorGitHubTransport } from "./collector-github.ts";
|
|
45
45
|
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
COLLECTOR_READ_TOOL,
|
|
49
|
-
COLLECTOR_REQUEST_TOOL,
|
|
50
|
-
COLLECTOR_WAIT_TOOL,
|
|
46
|
+
COLLECTOR_REQUIRED_TOOLS,
|
|
47
|
+
COLLECTOR_TRANSPORT_FLAGS,
|
|
51
48
|
createCollectorRoleRuntime,
|
|
49
|
+
type CollectorActivation,
|
|
52
50
|
} from "./collector-role.ts";
|
|
53
51
|
import type { ComplianceDecision } from "./compliance-transport.ts";
|
|
54
52
|
import { createDoctorRoleRuntime } from "./doctor-role.ts";
|
|
@@ -449,7 +447,9 @@ type ActivationRuntime = {
|
|
|
449
447
|
decodeReviewerAdmitted(): ReviewerAdmittedInputs;
|
|
450
448
|
/** Envelope stores live parent activation for agent_start prompt assembly. */
|
|
451
449
|
bindReviewerParent(activation: ReviewerActivation): void;
|
|
452
|
-
collector: {
|
|
450
|
+
collector: {
|
|
451
|
+
activate(context: HostContext, event: { reason: string }): Promise<void>;
|
|
452
|
+
};
|
|
453
453
|
doctor: { activate(): Promise<void> };
|
|
454
454
|
notary: {
|
|
455
455
|
activate(admitted?: import("./notary-role.ts").NotaryAdmittedTicket): Promise<void>;
|
|
@@ -629,7 +629,6 @@ export type RoleRuntimeDependencies = {
|
|
|
629
629
|
createMergerGitState?(repositoryRoot: string): MergerRoleDependencies["gitState"];
|
|
630
630
|
auditDoctorCompliance?(options: { context: HostContext; signal?: AbortSignal }): Promise<ComplianceDecision>;
|
|
631
631
|
createCollectorClock?(): CollectorClock;
|
|
632
|
-
collectorPackageExtensionPath?: string;
|
|
633
632
|
createNavigatorAttendance?(options: { context: HostContext; role: string; phase: NavigatorPhase; subjectKey: string; subject: string; authority: string; contextError?: unknown; invocationId: string; onEvent: (event: import("./navigator-attendance.ts").NavigatorEvent, report: import("./navigator-attendance.ts").NavigatorReport) => void | Promise<void> }): NavigatorAttendanceDependency | Promise<NavigatorAttendanceDependency>;
|
|
634
633
|
loadNavigatorWorkContext?(options: { context: HostContext; role: string; phase: NavigatorPhase; getFlag?: (name: string) => unknown }): Promise<NavigatorWorkContext>;
|
|
635
634
|
loadCanonicalSkillBinding?(
|
|
@@ -1144,6 +1143,10 @@ export function createRoleRuntimeExtension(
|
|
|
1144
1143
|
for (const flag of GLEANER_LEFT_TRANSPORT_FLAGS) {
|
|
1145
1144
|
roleHost.registerFlag(flag.name, flag.definition);
|
|
1146
1145
|
}
|
|
1146
|
+
// Collector transport flags: shared envelope owns registration (ADR 0018 / #676 E).
|
|
1147
|
+
for (const flag of COLLECTOR_TRANSPORT_FLAGS) {
|
|
1148
|
+
roleHost.registerFlag(flag.name, flag.definition);
|
|
1149
|
+
}
|
|
1147
1150
|
|
|
1148
1151
|
let admitted = false;
|
|
1149
1152
|
let selectedRole: string | undefined;
|
|
@@ -1305,10 +1308,46 @@ export function createRoleRuntimeExtension(
|
|
|
1305
1308
|
}),
|
|
1306
1309
|
};
|
|
1307
1310
|
}
|
|
1311
|
+
// #676 E / J1: collector materials + drift gates share this envelope hook (no parallel register).
|
|
1312
|
+
if (role === "collector" && activeCollector !== undefined) {
|
|
1313
|
+
const options = event.systemPromptOptions;
|
|
1314
|
+
if (options.skills && options.skills.length > 0) {
|
|
1315
|
+
failInfrastructure(
|
|
1316
|
+
activeCollector.ledger.latchFatal("通进司检测到系统提示中的环境 skills"),
|
|
1317
|
+
ctx,
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
if (options.contextFiles && options.contextFiles.length > 0) {
|
|
1321
|
+
failInfrastructure(
|
|
1322
|
+
activeCollector.ledger.latchFatal("通进司检测到系统提示中的环境 context files"),
|
|
1323
|
+
ctx,
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
if (
|
|
1327
|
+
typeof options.appendSystemPrompt === "string"
|
|
1328
|
+
&& options.appendSystemPrompt.trim().length > 0
|
|
1329
|
+
) {
|
|
1330
|
+
failInfrastructure(
|
|
1331
|
+
activeCollector.ledger.latchFatal("通进司检测到 appendSystemPrompt 漂移"),
|
|
1332
|
+
ctx,
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
if (!collectorFirstDispatchDone) {
|
|
1336
|
+
collectorFirstDispatchDone = true;
|
|
1337
|
+
activeCollector.ledger.recordActivation(activeCollector.clock);
|
|
1338
|
+
}
|
|
1339
|
+
return {
|
|
1340
|
+
systemPrompt: collectorBusiness.assembleMaterials(activeCollector, event.systemPrompt),
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1308
1343
|
});
|
|
1309
1344
|
roleHost.on("tool_result", async (event) => {
|
|
1310
1345
|
const role = selectedRole;
|
|
1311
1346
|
if (role === undefined) return;
|
|
1347
|
+
// #676 E / J1: collector operational bookkeeping on the shared tool_result seam.
|
|
1348
|
+
if (role === "collector" && activeCollector !== undefined) {
|
|
1349
|
+
collectorBusiness.onToolResult(activeCollector, event);
|
|
1350
|
+
}
|
|
1312
1351
|
const pendingInfra = pendingInfrastructureFailures.get(event.toolCallId);
|
|
1313
1352
|
const isRoleInfrastructureFailure = pendingInfra !== undefined;
|
|
1314
1353
|
if (pendingInfra !== undefined) pendingInfrastructureFailures.delete(event.toolCallId);
|
|
@@ -1439,6 +1478,16 @@ export function createRoleRuntimeExtension(
|
|
|
1439
1478
|
priorFetch = undefined;
|
|
1440
1479
|
fetchWrapped = false;
|
|
1441
1480
|
}
|
|
1481
|
+
// #676 J4: collector fatal latch must surface nonzero exit on shutdown (envelope-owned).
|
|
1482
|
+
if (
|
|
1483
|
+
selectedRole === "collector"
|
|
1484
|
+
&& activeCollector !== undefined
|
|
1485
|
+
&& activeCollector.ledger.fatal
|
|
1486
|
+
) {
|
|
1487
|
+
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
1488
|
+
process.exitCode = 1;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1442
1491
|
// Flush any still-pending affirmative attendance before teardown. Accepted
|
|
1443
1492
|
// grace-timeout paths normally emit on agent_settled; abort can skip that hook.
|
|
1444
1493
|
const presentation = pendingNavigatorPresentation;
|
|
@@ -1630,7 +1679,11 @@ export function createRoleRuntimeExtension(
|
|
|
1630
1679
|
completedMerge(mergeCommitId, automaticMergeTreeId) { if (!sessionMergerGitState) throw new Error("Merger runtime dependencies are not configured"); return sessionMergerGitState.completedMerge(mergeCommitId, automaticMergeTreeId); },
|
|
1631
1680
|
},
|
|
1632
1681
|
}, hostActions);
|
|
1633
|
-
|
|
1682
|
+
// #676 E: shared envelope owns collector lifecycle (mode/fork, tool surface,
|
|
1683
|
+
// event gates). Role module supplies business activate/tools/materials only.
|
|
1684
|
+
let activeCollector: CollectorActivation | undefined;
|
|
1685
|
+
let collectorFirstDispatchDone = false;
|
|
1686
|
+
const collectorBusiness = createCollectorRoleRuntime(
|
|
1634
1687
|
roleHost,
|
|
1635
1688
|
{
|
|
1636
1689
|
async loadSoul() {
|
|
@@ -1664,14 +1717,114 @@ export function createRoleRuntimeExtension(
|
|
|
1664
1717
|
...(dependencies.createCollectorClock === undefined
|
|
1665
1718
|
? {}
|
|
1666
1719
|
: { createClock: dependencies.createCollectorClock }),
|
|
1667
|
-
...(dependencies.collectorPackageExtensionPath === undefined
|
|
1668
|
-
? {}
|
|
1669
|
-
: {
|
|
1670
|
-
packageExtensionPath: dependencies.collectorPackageExtensionPath,
|
|
1671
|
-
}),
|
|
1672
1720
|
},
|
|
1673
1721
|
hostActions,
|
|
1674
1722
|
);
|
|
1723
|
+
// #676 J1: business tools + tool_call gate register only behind admission/activation.
|
|
1724
|
+
// before_agent_start / tool_result collector branches live on the shared envelope hooks above.
|
|
1725
|
+
//
|
|
1726
|
+
// #676 J4 dispositions for guards removed from the role-private collector module:
|
|
1727
|
+
// - skills/contextFiles/appendSystemPrompt fail-closed → shared before_agent_start (above).
|
|
1728
|
+
// - ambient skill/prompt/template commands → activate (below).
|
|
1729
|
+
// - session_shutdown fatal exitCode → shared session_shutdown (above).
|
|
1730
|
+
// - subsequent-input latchFatal + fixed-kickoff rewrite → intentionally not migrated:
|
|
1731
|
+
// #676 materials-first multi-turn abolished the single-shot fixed kickoff; multi-turn
|
|
1732
|
+
// observe/request/wait requires later inputs. Judge r1: fixed-kickoff equality delete is authorized.
|
|
1733
|
+
// - tool sourceInfo path override check → intentionally not migrated: depended on
|
|
1734
|
+
// packageExtensionPath deleted under ADR 0018 / #676 E envelope ownership; uniqueness
|
|
1735
|
+
// + setActiveTools inventory checks remain on activate.
|
|
1736
|
+
let collectorToolCallRegistered = false;
|
|
1737
|
+
const collector = {
|
|
1738
|
+
async activate(context: HostContext, event: { reason: string }) {
|
|
1739
|
+
activeCollector = undefined;
|
|
1740
|
+
collectorFirstDispatchDone = false;
|
|
1741
|
+
// Envelope-owned mode / fork-reload gates (not role-private lifecycle).
|
|
1742
|
+
if (context.mode !== "print" && context.mode !== "json") {
|
|
1743
|
+
throw new Error(
|
|
1744
|
+
`Collector supports only print or json mode (got ${context.mode})`,
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
if (event.reason === "fork" || event.reason === "reload") {
|
|
1748
|
+
throw new Error(
|
|
1749
|
+
`Collector does not support session_start reason ${event.reason}`,
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
// #676 J4: ambient skill/prompt/template commands fail closed at activation.
|
|
1753
|
+
const commands = roleHost.getCommands?.() ?? [];
|
|
1754
|
+
const ambientCommands = commands.filter((command) => {
|
|
1755
|
+
const name = command.name.toLowerCase();
|
|
1756
|
+
return (
|
|
1757
|
+
name.includes("skill")
|
|
1758
|
+
|| name.includes("prompt")
|
|
1759
|
+
|| name.startsWith("template")
|
|
1760
|
+
);
|
|
1761
|
+
});
|
|
1762
|
+
if (ambientCommands.length > 0) {
|
|
1763
|
+
throw new Error(
|
|
1764
|
+
`Collector detected ambient instruction commands: ${
|
|
1765
|
+
ambientCommands.map((c) => c.name).join(", ")
|
|
1766
|
+
}`,
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
// Business tools behind admission barrier (inert-without-role invariant).
|
|
1770
|
+
// First activation: fail closed if a required name is already occupied.
|
|
1771
|
+
// Later activations reuse the once-registered tools (registerBusinessTools is idempotent).
|
|
1772
|
+
const preExisting = roleHost.getAllTools();
|
|
1773
|
+
const alreadyRegistered = COLLECTOR_REQUIRED_TOOLS.every((required) =>
|
|
1774
|
+
preExisting.some((tool) => tool.name === required),
|
|
1775
|
+
);
|
|
1776
|
+
if (!alreadyRegistered) {
|
|
1777
|
+
for (const required of COLLECTOR_REQUIRED_TOOLS) {
|
|
1778
|
+
const prior = preExisting.filter((tool) => tool.name === required);
|
|
1779
|
+
if (prior.length > 0) {
|
|
1780
|
+
throw new Error(`Collector required tool name collision: ${required}`);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
collectorBusiness.registerBusinessTools(() => activeCollector);
|
|
1785
|
+
const allTools = roleHost.getAllTools();
|
|
1786
|
+
for (const required of COLLECTOR_REQUIRED_TOOLS) {
|
|
1787
|
+
const matches = allTools.filter((tool) => tool.name === required);
|
|
1788
|
+
if (matches.length === 0) {
|
|
1789
|
+
throw new Error(`Collector required tool missing: ${required}`);
|
|
1790
|
+
}
|
|
1791
|
+
if (matches.length > 1) {
|
|
1792
|
+
throw new Error(`Collector required tool name collision: ${required}`);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
roleHost.setActiveTools([...COLLECTOR_REQUIRED_TOOLS]);
|
|
1796
|
+
const active = new Set(roleHost.getActiveTools());
|
|
1797
|
+
for (const required of COLLECTOR_REQUIRED_TOOLS) {
|
|
1798
|
+
if (!active.has(required)) {
|
|
1799
|
+
throw new Error(`Collector failed to activate required tool ${required}`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
for (const name of active) {
|
|
1803
|
+
if (!(COLLECTOR_REQUIRED_TOOLS as readonly string[]).includes(name)) {
|
|
1804
|
+
throw new Error(`Collector active tool surface includes unexpected ${name}`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
// Seat-scoped tool_call gate — registered only after collector admission (no install-time tool_call).
|
|
1808
|
+
if (!collectorToolCallRegistered) {
|
|
1809
|
+
collectorToolCallRegistered = true;
|
|
1810
|
+
roleHost.on("tool_call", (toolEvent) => {
|
|
1811
|
+
if (activeCollector === undefined || selectedRole !== "collector") return;
|
|
1812
|
+
if (!(COLLECTOR_REQUIRED_TOOLS as readonly string[]).includes(toolEvent.toolName)) {
|
|
1813
|
+
return {
|
|
1814
|
+
block: true,
|
|
1815
|
+
reason: `通进司禁用工具 ${toolEvent.toolName}`,
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
return collectorBusiness.onToolCall(activeCollector, toolEvent);
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
const activation = await collectorBusiness.activate(context);
|
|
1822
|
+
if (activation.ledger.activationRecorded) {
|
|
1823
|
+
collectorFirstDispatchDone = true;
|
|
1824
|
+
}
|
|
1825
|
+
activeCollector = activation;
|
|
1826
|
+
},
|
|
1827
|
+
};
|
|
1675
1828
|
|
|
1676
1829
|
const clock = dependencies.activationClock ?? (() => new Date().toISOString());
|
|
1677
1830
|
const writeTrace = dependencies.activationTraceWriter ?? writeActivationTraceRecord;
|