@akagilnc/pi-workflow-roles 0.1.5241 → 0.1.5252

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.
Files changed (41) hide show
  1. package/README.md +2 -3
  2. package/README.zh-CN.md +2 -3
  3. package/dist/acp-host/production-host.js +482 -3141
  4. package/dist/collector-config.js +0 -1
  5. package/dist/headless-host/production-host.js +590 -3249
  6. package/dist/migrate-book-topology.js +40 -107
  7. package/dist/navigator-invocation-identity.js +0 -57
  8. package/dist/package-contracts/collector-output.js +1 -90
  9. package/dist/package-contracts/terminating-tools.js +2 -2
  10. package/dist/packaged-role-registry.js +8 -11
  11. package/dist/public-cli/invocation.js +2 -22
  12. package/dist/public-cli/main.js +256 -749
  13. package/dist/public-cli/option-definitions.js +4 -19
  14. package/dist/public-cli/settlement.js +12 -138
  15. package/package.json +1 -1
  16. package/resources/collector-bot-handbook.md +4 -2
  17. package/src/collector-config.ts +0 -1
  18. package/src/collector-role.ts +40 -535
  19. package/src/collector-tool-schemas.ts +7 -117
  20. package/src/gh-api-runner.ts +102 -0
  21. package/src/navigator-invocation-identity.ts +4 -121
  22. package/src/package-contracts/collector-output.ts +15 -174
  23. package/src/package-contracts/terminating-tools.ts +0 -2
  24. package/src/packaged-role-registry.ts +7 -10
  25. package/src/public-cli/invocation.ts +3 -27
  26. package/src/public-cli/option-definitions.ts +4 -19
  27. package/src/public-cli/settlement.ts +11 -177
  28. package/src/role-runtime-dependencies.ts +0 -4
  29. package/src/role-runtime.ts +39 -103
  30. package/src/ticket-snapshot.ts +1 -1
  31. package/dist/collector-evidence.js +0 -319
  32. package/dist/collector-github.js +0 -725
  33. package/dist/collector-identity.js +0 -92
  34. package/dist/collector-ledger.js +0 -888
  35. package/dist/collector-target.js +0 -169
  36. package/src/collector-evidence.ts +0 -466
  37. package/src/collector-github.ts +0 -1032
  38. package/src/collector-handbook.ts +0 -194
  39. package/src/collector-identity.ts +0 -151
  40. package/src/collector-ledger.ts +0 -1186
  41. package/src/collector-target.ts +0 -207
@@ -2,136 +2,26 @@ import { Type, type Static } from "typebox";
2
2
  import { openToolObject } from "./open-tool-schema.ts";
3
3
  import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
4
4
 
5
- // #836 r16 class 3: execute() reads no params for this tool
6
- // (src/collector-role.ts:431-459) — a closed root only rejects the role for
7
- // saying more; no reader consumes extra fields.
8
- export const collectorObserveArgsSchema = Type.Object({}, { additionalProperties: true });
9
- // #836 r16 class 4: the regex was a real provider shape rejection ahead of the
10
- // ledger's own identity check — src/collector-ledger.ts:911-972 already trims
11
- // and enforces non-empty/lookup/marker/attemptKey identity on requestId once
12
- // the tool receives it, so the duplicate pattern-based rejection is deleted
13
- // (r15b decision, not reopened here).
14
- export const collectorRequestArgsSchema = Type.Object({
15
- requestId: Type.String({
16
- minLength: 1,
17
- description: "请求身份(配置 id 或角色判定的稳定 id;首尾无空白)",
18
- }),
19
- snapshotId: Type.String({ minLength: 1, description: "最新留存观察快照" }),
20
- // #836 r16 class 3: body.minLength deleted — manifest-configured requestId
21
- // ignores body, and the unknown-requestId non-empty condition is a real
22
- // branch in src/collector-ledger.ts:922-951, not a provider shape gate.
23
- body: Type.Optional(Type.String({
24
- description: "角色判定的请求正文;request-manifest 未收录该 requestId 时必填",
25
- })),
26
- }, { additionalProperties: true });
27
- /** UTF-8 byte ceiling per handbook body — keeps next activation materials inside context budget. */
28
- export const COLLECTOR_HANDBOOK_MAX_BYTES = 64 * 1024;
29
- export const collectorHandbookWriteArgsSchema = Type.Object({
30
- scope: Type.Union([
31
- Type.Literal("general"),
32
- Type.Literal("repo"),
33
- ], { description: "general=通用手册;repo=当前仓库差异" }),
34
- body: Type.String({
35
- description: `手册全文(opaque 工作记忆;整份替换;UTF-8 至多 ${COLLECTOR_HANDBOOK_MAX_BYTES} 字节)`,
36
- }),
37
- }, { additionalProperties: true });
38
- export const collectorReadArgsSchema = Type.Object({
39
- evidenceId: Type.String({ minLength: 1, description: "observe 返回的材料证据 id(evidenceId)" }),
40
- }, { additionalProperties: true });
41
- export const collectorWaitArgsSchema = Type.Object({
42
- durationMs: Type.Integer({
43
- minimum: 1,
44
- description: "等待毫秒;实际睡眠不超过剩余等待窗(#678;无包内单次任意上限)",
45
- }),
46
- }, { additionalProperties: true });
47
-
48
5
  /**
49
- * #678 D4: open the wait window at a work step.
50
- * Omit startedAt for existing-PR trigger-phase end (= now).
51
- * Pass PR creation success time for new-PR auto-trigger rounds.
52
- */
53
- // #836 r16 class 3: startedAt.minLength deleted — an empty string is already
54
- // handled as omission by src/collector-role.ts:586-597, so the provider
55
- // nonblank gate is not load-bearing for any branch.
56
- export const collectorOpenWaitWindowArgsSchema = Type.Object({
57
- startedAt: Type.Optional(Type.String({
58
- description: "等待窗起点 ISO 时间;新建 PR 用创建成功时刻;省略=现在(触发阶段结束)",
59
- })),
60
- }, { additionalProperties: true });
61
-
62
- /**
63
- * #676 A: role-decided target bind. The model judges task materials and submits
64
- * the chosen PR and/or issue identity; runtime only performs online association
65
- * for the role-chosen ticket — never scrapes task text to lock a target.
66
- */
67
- export const collectorBindTargetArgsSchema = Type.Object({
68
- prNumber: Type.Optional(Type.Unknown({
69
- description: "本仓 PR 号(正整数)。",
70
- })),
71
- issueNumber: Type.Optional(Type.Unknown({
72
- description: "本仓 issue 号(正整数)。",
73
- })),
74
- }, { additionalProperties: true });
75
-
76
- /**
77
- * #641 chain① / #676 C / ADR 0057: nested finding item declarations for model
78
- * guidance. Host must not pure-shape-reject the envelope (第 0 条). Runtime binds
79
- * resolvable evidence pointers only; unprojected content stays distinguishable.
80
- */
81
- const collectorFindingItemDeclaration = (() => {
82
- // Nested declarations for model guidance only — open required so host cannot
83
- // pure-shape-reject missing optional fields (第 0 条 / ADR 0057 / #676 C).
84
- const item = Type.Object(
85
- {
86
- evidenceId: Type.Unknown({
87
- description: "observe 返回的材料指针(必填语义)",
88
- }),
89
- category: Type.Unknown({
90
- description: "简短归类标签,不是摘要",
91
- }),
92
- summary: Type.Unknown({
93
- description: "哪个 bot、什么问题的摘要;不誊抄正文",
94
- }),
95
- },
96
- {
97
- additionalProperties: true,
98
- description:
99
- "单条 finding 指针:evidenceId,以及可选 category、summary。",
100
- },
101
- );
102
- (item as unknown as { required: string[] }).required = [];
103
- return item;
104
- })();
105
-
106
- /**
107
- * Field declarations + descriptions are guidance for the model — host must not
108
- * pure-shape-reject the envelope (第 0 条 / ADR 0057).
6
+ * #1088: LLM gathers evidence via host CLI and submits the receipt.
7
+ * Field declarations guide the model — host must not pure-shape-reject (第 0 条 / ADR 0057).
8
+ * Presence of `groups` remains the Collector terminal discriminator for settlement/analyst.
109
9
  */
110
10
  export const collectorOutputBaseSchema = openToolObject(
111
11
  Type.Object({
112
- // No root type:array — host must not shape-reject non-array findings (#676 C).
113
- // Nested item declarations ride `items` for registration preservation (ADR 0057).
114
- findings: Type.Unsafe({
12
+ groups: Type.Unsafe({
115
13
  description:
116
- "本次收集到的逐条 findings 指针。",
117
- items: collectorFindingItemDeclaration,
14
+ "按机器身份归组的出席、材料与逐条 findings(LLM 自行从宿主 CLI 取证整理;代码不代收不归并)。",
118
15
  }),
119
16
  unfinishedReasons: Type.Unknown({
120
- description:
121
- "未完成原因。",
17
+ description: "未完成原因;取证受限时如实报告。",
122
18
  }),
123
19
  }),
124
20
  );
125
21
 
126
- /** Runtime owns the observed evidence; the model submits findings and signals sole-final submission. */
22
+ /** Runtime does not enrich findings; the model submits the receipt and signals sole-final. */
127
23
  export const collectorOutputArgsSchema = withTerminatingOutputDeclarations(
128
24
  collectorOutputBaseSchema,
129
25
  );
130
26
 
131
- export type CollectorObserveArgs = Static<typeof collectorObserveArgsSchema>;
132
- export type CollectorRequestArgs = Static<typeof collectorRequestArgsSchema>;
133
- export type CollectorReadArgs = Static<typeof collectorReadArgsSchema>;
134
- export type CollectorWaitArgs = Static<typeof collectorWaitArgsSchema>;
135
- export type CollectorOpenWaitWindowArgs = Static<typeof collectorOpenWaitWindowArgsSchema>;
136
- export type CollectorBindTargetArgs = Static<typeof collectorBindTargetArgsSchema>;
137
27
  export type CollectorOutputArgs = Static<typeof collectorOutputArgsSchema>;
@@ -0,0 +1,102 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export type GhApiRunOptions = {
4
+ signal?: AbortSignal;
5
+ };
6
+
7
+ export type GhApiResponse = {
8
+ status: number;
9
+ headers: Record<string, string>;
10
+ bodyText: string;
11
+ };
12
+
13
+ export type GhApiRunner = (
14
+ args: readonly string[],
15
+ options?: GhApiRunOptions,
16
+ ) => Promise<GhApiResponse>;
17
+
18
+ export function createGhApiRunner(
19
+ options: {
20
+ spawnImpl?: typeof spawn;
21
+ env?: NodeJS.ProcessEnv;
22
+ } = {},
23
+ ): GhApiRunner {
24
+ const spawnImpl = options.spawnImpl ?? spawn;
25
+ return async (args, runOptions = {}) => {
26
+ return await new Promise<GhApiResponse>((resolve, reject) => {
27
+ const signal = runOptions.signal;
28
+ if (signal?.aborted) {
29
+ reject(signal.reason ?? new Error("aborted"));
30
+ return;
31
+ }
32
+ const child = spawnImpl("gh", args, {
33
+ env: options.env ?? process.env,
34
+ stdio: ["ignore", "pipe", "pipe"],
35
+ });
36
+ let stdout = "";
37
+ let stderr = "";
38
+ let settled = false;
39
+ const settle = (fn: () => void) => {
40
+ if (settled) return;
41
+ settled = true;
42
+ if (signal !== undefined) {
43
+ signal.removeEventListener("abort", onAbort);
44
+ }
45
+ fn();
46
+ };
47
+ const onAbort = () => {
48
+ try {
49
+ child.kill("SIGTERM");
50
+ } catch (error) {
51
+ settle(() => reject(error));
52
+ return;
53
+ }
54
+ settle(() => {
55
+ reject(signal?.reason ?? new Error("aborted"));
56
+ });
57
+ };
58
+ if (signal !== undefined) {
59
+ signal.addEventListener("abort", onAbort, { once: true });
60
+ }
61
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
62
+ stdout += chunk;
63
+ });
64
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
65
+ stderr += chunk;
66
+ });
67
+ child.on("error", (error) => {
68
+ settle(() => reject(error));
69
+ });
70
+ child.on("close", (code, signal) => {
71
+ settle(() => {
72
+ // gh api --include prints: HTTP/ headers blank-line body
73
+ const match = stdout.match(/^HTTP\/[\d.]+\s+(\d+)[^\n]*\r?\n([\s\S]*?)\r?\n\r?\n([\s\S]*)$/);
74
+ if (match) {
75
+ const status = Number(match[1]);
76
+ const headerText = match[2] ?? "";
77
+ const bodyText = match[3] ?? "";
78
+ const headers: Record<string, string> = {};
79
+ for (const line of headerText.split(/\r?\n/)) {
80
+ const idx = line.indexOf(":");
81
+ if (idx === -1) continue;
82
+ const name = line.slice(0, idx).trim().toLowerCase();
83
+ const value = line.slice(idx + 1).trim();
84
+ headers[name] = value;
85
+ }
86
+ resolve({ status, headers, bodyText });
87
+ return;
88
+ }
89
+ if (code === 0) {
90
+ resolve({ status: 200, headers: {}, bodyText: stdout });
91
+ return;
92
+ }
93
+ const failure = new Error(
94
+ `gh api failed without a parseable HTTP response (code=${String(code)}): ${stderr || stdout}`,
95
+ { cause: { code, signal, stderr, stdout } },
96
+ );
97
+ reject(failure);
98
+ });
99
+ });
100
+ });
101
+ };
102
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Exact current-invocation principal for Navigator attendance correlation.
2
+ * Exact current-invocation principal for shared role lifecycle (#1087).
3
3
  * Shared role lifecycle owns one opaque uuidv7 per Role invocation and persists
4
4
  * it on the role session via Pi's guaranteed `pi.appendEntry` boundary.
5
5
  *
@@ -7,21 +7,14 @@
7
7
  * unfinished exact-session resume reuses the latest valid marker only when its
8
8
  * role/phase/subjectKey still match the expected identity; a packaged role
9
9
  * terminal completing that marker starts the next invocation (fresh mint).
10
- * Terminal settlement binds the nearest independent marker strictly before the
11
- * current durable packaged role terminal and compares equality — never attendance
12
- * self-shape, markers after the terminal, or stale older markers behind a
13
- * malformed nearest. Non-UUIDv7 principals are never accepted.
10
+ * Public settlement does not re-correlate attendance against this principal
11
+ * (ADR 0042); lifecycle dispose/grace owns call-boundary isolation (ADR 0084).
14
12
  *
15
13
  * One shared typed terminal classifier owns durable completion for lifecycle,
16
14
  * publicNavigatorSettlement, and every public CLI Receipt extractor:
17
15
  * - accepted/human: isError exactly false and no infrastructure-failure fact
18
16
  * - infrastructure: isError exactly true plus base kind/source/reasonCode identity
19
17
  * - retryable/missing/nonboolean/contradictory/malformed: nonterminal
20
- *
21
- * One truth table also owns marker↔terminal cardinality:
22
- * - a marker binds exactly one durable packaged role terminal
23
- * - multiple durable terminals after the same marker → ambiguous / fail-closed
24
- * - marker role/phase/subjectKey must match the terminal + independent expected
25
18
  */
26
19
 
27
20
  import { LEGACY_REVIEW_OUTPUT_ROLES, PACKAGED_ROLE_REGISTRY } from "./packaged-role-registry.ts";
@@ -147,7 +140,7 @@ export type InvocationMarkerIdentity = {
147
140
  readonly subjectKey: string;
148
141
  };
149
142
 
150
- /** Independently admitted / registry / cwd expected identity for resume and correlation. */
143
+ /** Independently admitted / registry / cwd expected identity for lifecycle resume. */
151
144
  export type ExpectedInvocationIdentity = {
152
145
  readonly role: string;
153
146
  readonly phase?: InvocationPhase;
@@ -342,83 +335,6 @@ export function findLatestDurablePackagedRoleTerminal(
342
335
  return undefined;
343
336
  }
344
337
 
345
- /**
346
- * Truth-table binding of the current durable packaged role terminal to its
347
- * owning invocation marker. Singleton cardinality: multiple durable terminals
348
- * after the same marker are ambiguous and fail closed.
349
- */
350
- export type DurableTerminalMarkerBinding =
351
- | {
352
- readonly kind: "bound";
353
- readonly terminal: DurablePackagedRoleTerminalRef;
354
- readonly marker: InvocationMarkerIdentity & { readonly index: number };
355
- }
356
- | {
357
- readonly kind: "unbound";
358
- readonly terminal: DurablePackagedRoleTerminalRef;
359
- }
360
- | { readonly kind: "absent" }
361
- | { readonly kind: "ambiguous" };
362
-
363
- export function bindCurrentDurableTerminalToMarker(
364
- entries: readonly NavigatorInvocationEntryLike[],
365
- ): DurableTerminalMarkerBinding {
366
- const terminal = findLatestDurablePackagedRoleTerminal(entries);
367
- if (terminal === undefined) return { kind: "absent" };
368
-
369
- let markerIndex = -1;
370
- for (let i = terminal.index - 1; i >= 0; i -= 1) {
371
- if (isInvocationMarkerEntry(entries[i])) {
372
- markerIndex = i;
373
- break;
374
- }
375
- }
376
- if (markerIndex < 0) {
377
- return { kind: "unbound", terminal };
378
- }
379
-
380
- const marker = parseInvocationMarkerIdentity(entries[markerIndex]?.data);
381
- if (marker === undefined) {
382
- // Malformed nearest marker: no stale fallback; terminal exists but is unbound.
383
- return { kind: "unbound", terminal };
384
- }
385
-
386
- let windowEnd = entries.length;
387
- for (let i = markerIndex + 1; i < entries.length; i += 1) {
388
- if (isInvocationMarkerEntry(entries[i])) {
389
- windowEnd = i;
390
- break;
391
- }
392
- }
393
-
394
- let durableCount = 0;
395
- for (let i = markerIndex + 1; i < windowEnd; i += 1) {
396
- if (durableTerminalAt(entries, i) !== undefined) durableCount += 1;
397
- }
398
- if (durableCount !== 1) return { kind: "ambiguous" };
399
- // Current terminal must be the singleton inside this marker window.
400
- if (terminal.index <= markerIndex || terminal.index >= windowEnd) {
401
- return { kind: "ambiguous" };
402
- }
403
-
404
- return {
405
- kind: "bound",
406
- terminal,
407
- marker: { ...marker, index: markerIndex },
408
- };
409
- }
410
-
411
- /**
412
- * Whether public Receipt settlement may proceed for the current durable terminal.
413
- * Only ambiguous multi-terminal bindings fail closed; attendance mismatch does not
414
- * pollute an otherwise legal single Receipt.
415
- */
416
- export function isReceiptSettlementBindingClear(
417
- entries: readonly NavigatorInvocationEntryLike[],
418
- ): boolean {
419
- return bindCurrentDurableTerminalToMarker(entries).kind !== "ambiguous";
420
- }
421
-
422
338
  export type LifecycleInvocationPrincipal = {
423
339
  readonly invocationId: string;
424
340
  /** True when the principal is already on the admitted session (resume; do not re-append). */
@@ -463,36 +379,3 @@ export function resolveLifecycleInvocationPrincipal(
463
379
 
464
380
  return { invocationId: marker.invocationId, resume: true };
465
381
  }
466
-
467
- /**
468
- * Exact current invocation principal already present on the role session.
469
- * `beforeIndex` is the current packaged role terminal index: only the nearest
470
- * marker strictly before that bound is applicable. A malformed nearest marker
471
- * fails closed (undefined) — never falls back to a stale older valid marker.
472
- * Markers at/after the terminal (future prepare/event) are never considered.
473
- * Non-UUIDv7 values are rejected.
474
- */
475
- export function currentInvocationPrincipalFromSession(
476
- entries: readonly NavigatorInvocationEntryLike[],
477
- beforeIndex: number = entries.length,
478
- ): string | undefined {
479
- return currentInvocationMarkerFromSession(entries, beforeIndex)?.invocationId;
480
- }
481
-
482
- /**
483
- * Full marker identity nearest before `beforeIndex` (current durable terminal).
484
- * Malformed nearest fails closed — no stale older fallback.
485
- */
486
- export function currentInvocationMarkerFromSession(
487
- entries: readonly NavigatorInvocationEntryLike[],
488
- beforeIndex: number = entries.length,
489
- ): InvocationMarkerIdentity | undefined {
490
- const limit = Math.min(Math.max(beforeIndex, 0), entries.length);
491
- for (let i = limit - 1; i >= 0; i -= 1) {
492
- const entry = entries[i];
493
- if (!isInvocationMarkerEntry(entry)) continue;
494
- // Nearest marker only — parse full identity or fail closed.
495
- return parseInvocationMarkerIdentity(entry?.data);
496
- }
497
- return undefined;
498
- }
@@ -1,188 +1,29 @@
1
1
  /** Package-owned Collector receipt leaf — no role registration surface. */
2
2
 
3
3
  export const COLLECTOR_OUTPUT_TOOL = "ak_collector_output";
4
- export const COLLECTOR_HOST = "github.com";
5
4
 
5
+ /**
6
+ * One identity group on the LLM-submitted receipt.
7
+ * #1088: code no longer builds ledger projections; the model submits groups.
8
+ */
6
9
  export type CollectorIdentityGroup = {
7
10
  identity: Record<string, unknown> | null;
8
11
  displayLogin?: string;
9
- attendance: true;
10
- materials: Array<Record<string, unknown>>;
11
- findings: Array<Record<string, unknown>>;
12
- };
13
-
14
- export type CollectorRequestAttempt = {
15
- attemptId: string;
16
- requestId: string;
17
- observedHead: string;
18
- snapshotId: string;
19
- marker: string;
20
- body: string;
21
- startedAt: string;
22
- status: "started" | "succeeded" | "rejected" | "ambiguous_loss" | "recovered";
23
- responseDiagnostics?: string;
24
- commentEvidenceId?: string;
25
- recoverySnapshotId?: string;
26
- };
27
-
28
- export type CollectorSnapshot = {
29
- snapshotId: string;
30
- observedAt: string;
31
- completedAt: string;
32
- completedMono: number;
33
- host: "github.com";
34
- repository: string;
35
- prNumber: number;
36
- prState: string;
37
- headOid: string;
38
- complete: boolean;
39
- evidenceIds: string[];
40
- pageDiagnostics: Array<Record<string, unknown>>;
41
- normalizedByteLength: number;
12
+ attendance?: true;
13
+ materials?: Array<Record<string, unknown>>;
14
+ findings?: Array<Record<string, unknown>>;
42
15
  };
43
16
 
44
17
  /**
45
- * #641 chain① receipt evidence record: pointer/integrity facts only. Bodies and
46
- * raw payloads stay in the volume (observe details + GitHub pointer) and are
47
- * never transcribed into the receipt.
18
+ * Collector receipt as submitted. Presence of `groups` is the terminal
19
+ * discriminator for settlement/analyst (#836: original object is the receipt).
48
20
  */
49
- export type CollectorEvidenceRecord = {
50
- evidenceId: string;
51
- kind: string;
52
- versionId: string;
53
- contentDigest: string;
54
- firstObservedAt: string;
55
- githubId?: number;
56
- authorLogin?: string;
57
- htmlUrl?: string;
58
- authoritativeTime?: string | null;
59
- };
60
-
61
- /**
62
- * #676 C: sole structured projection-fact type (builder + package leaf).
63
- * Builder always emits the full object; package leaf keeps it optional so old
64
- * volumes without the field stay readable — missing is unknown, never defaulted.
65
- */
66
- export type CollectorSubmissionProjection = {
67
- findingsSource: "absent" | "array" | "unreadable";
68
- findingsProjectedCount: number;
69
- findingsUnprojected: boolean;
70
- unfinishedReasonsSource: "absent" | "array" | "unreadable";
71
- unfinishedReasonsProjectedCount: number;
72
- unfinishedReasonsUnprojected: boolean;
73
- };
74
-
75
21
  export type CollectorReceipt = {
76
- host: "github.com";
77
- repository: string;
78
- prNumber: number;
79
- /**
80
- * Final observed PR state; non-OPEN still delivers materials (#676 D6).
81
- * Absent/unreadable on old volumes is unknown — never defaulted to OPEN.
82
- */
22
+ groups: CollectorIdentityGroup[] | ReadonlyArray<Record<string, unknown>>;
23
+ unfinishedReasons?: unknown;
24
+ host?: string;
25
+ repository?: string;
26
+ prNumber?: number;
83
27
  prState?: string;
84
- manifestDigest: string;
85
- activationTime: string;
86
- deadlineTime: string;
87
- finalObservationTime: string;
88
- finalSnapshotId: string;
89
- targetHead: string;
90
- groups: CollectorIdentityGroup[];
91
- unfinishedReasons?: string[];
92
- /**
93
- * Distinguishes readable-empty from unprojected original submission content (#676 C).
94
- * Package leaf may carry a partial projection when some sibling keys are unreadable —
95
- * missing keys stay absent (never defaulted false/0/OPEN).
96
- */
97
- submissionProjection?: Partial<CollectorSubmissionProjection>;
98
- requestAttempts: CollectorRequestAttempt[];
99
- snapshots: CollectorSnapshot[];
100
- evidenceRecords: CollectorEvidenceRecord[];
28
+ manifestDigest?: string;
101
29
  };
102
-
103
- function safeGet(value: unknown, key: string): unknown {
104
- if ((typeof value !== "object" && typeof value !== "function") || value === null) return undefined;
105
- try { return (value as Record<string, unknown>)[key]; } catch { return undefined; }
106
- }
107
- function records(value: unknown): Record<string, unknown>[] {
108
- return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => item !== null && typeof item === "object") : [];
109
- }
110
- function strings(value: unknown): string[] {
111
- return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
112
- }
113
-
114
- /** Settlement projection. Presence of the canonical groups array is the one Collector terminal discriminator. */
115
- export function validateAcceptedCollectorReceipt(value: unknown): CollectorReceipt {
116
- const rawGroups = safeGet(value, "groups");
117
- if (!Array.isArray(rawGroups)) throw new Error("Collector receipt has no typed groups terminal discriminator");
118
- const groups = records(rawGroups).map((group) => ({
119
- identity: (safeGet(group, "identity") ?? null) as Record<string, unknown> | null,
120
- ...(typeof safeGet(group, "displayLogin") === "string" ? { displayLogin: safeGet(group, "displayLogin") as string } : {}),
121
- attendance: true as const,
122
- materials: records(safeGet(group, "materials")),
123
- findings: records(safeGet(group, "findings")),
124
- }));
125
- const unfinishedRaw = safeGet(value, "unfinishedReasons");
126
- const unfinishedReasons = strings(unfinishedRaw);
127
- // #676 C: project readable submissionProjection fields independently — do not
128
- // drop the whole block when one sibling key is the wrong type (left-拾遗1).
129
- const submissionProjection = projectSubmissionProjection(safeGet(value, "submissionProjection"));
130
- const prStateRaw = safeGet(value, "prState");
131
- const prState = typeof prStateRaw === "string" ? prStateRaw : undefined;
132
- return {
133
- host: safeGet(value, "host") as "github.com",
134
- repository: safeGet(value, "repository") as string,
135
- prNumber: safeGet(value, "prNumber") as number,
136
- ...(prState === undefined ? {} : { prState }),
137
- manifestDigest: safeGet(value, "manifestDigest") as string,
138
- activationTime: safeGet(value, "activationTime") as string,
139
- deadlineTime: safeGet(value, "deadlineTime") as string,
140
- finalObservationTime: safeGet(value, "finalObservationTime") as string,
141
- finalSnapshotId: safeGet(value, "finalSnapshotId") as string,
142
- targetHead: safeGet(value, "targetHead") as string,
143
- groups,
144
- ...(unfinishedReasons.length > 0 ? { unfinishedReasons } : {}),
145
- ...(submissionProjection === undefined ? {} : { submissionProjection }),
146
- requestAttempts: records(safeGet(value, "requestAttempts")) as unknown as CollectorRequestAttempt[],
147
- snapshots: records(safeGet(value, "snapshots")).map((snapshot) => ({
148
- snapshotId: safeGet(snapshot, "snapshotId"), observedAt: safeGet(snapshot, "observedAt"), completedAt: safeGet(snapshot, "completedAt"), completedMono: safeGet(snapshot, "completedMono"), host: safeGet(snapshot, "host"), repository: safeGet(snapshot, "repository"), prNumber: safeGet(snapshot, "prNumber"), prState: safeGet(snapshot, "prState"), headOid: safeGet(snapshot, "headOid"), complete: safeGet(snapshot, "complete"), evidenceIds: strings(safeGet(snapshot, "evidenceIds")), pageDiagnostics: records(safeGet(snapshot, "pageDiagnostics")), normalizedByteLength: safeGet(snapshot, "normalizedByteLength"),
149
- } as CollectorSnapshot)),
150
- evidenceRecords: records(safeGet(value, "evidenceRecords")).map((record) => ({ evidenceId: safeGet(record, "evidenceId"), kind: safeGet(record, "kind"), versionId: safeGet(record, "versionId"), contentDigest: safeGet(record, "contentDigest"), firstObservedAt: safeGet(record, "firstObservedAt"), githubId: safeGet(record, "githubId"), authorLogin: safeGet(record, "authorLogin"), htmlUrl: safeGet(record, "htmlUrl"), authoritativeTime: safeGet(record, "authoritativeTime") } as CollectorEvidenceRecord)),
151
- };
152
- }
153
-
154
- /** Readable projection facts only; omit keys that are the wrong type — never fabricate defaults. */
155
- function projectSubmissionProjection(
156
- raw: unknown,
157
- ): Partial<CollectorSubmissionProjection> | undefined {
158
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
159
- const p = raw as Record<string, unknown>;
160
- const out: Partial<CollectorSubmissionProjection> = {};
161
- if (
162
- p["findingsSource"] === "absent" ||
163
- p["findingsSource"] === "array" ||
164
- p["findingsSource"] === "unreadable"
165
- ) {
166
- out.findingsSource = p["findingsSource"];
167
- }
168
- if (typeof p["findingsProjectedCount"] === "number") {
169
- out.findingsProjectedCount = p["findingsProjectedCount"];
170
- }
171
- if (typeof p["findingsUnprojected"] === "boolean") {
172
- out.findingsUnprojected = p["findingsUnprojected"];
173
- }
174
- if (
175
- p["unfinishedReasonsSource"] === "absent" ||
176
- p["unfinishedReasonsSource"] === "array" ||
177
- p["unfinishedReasonsSource"] === "unreadable"
178
- ) {
179
- out.unfinishedReasonsSource = p["unfinishedReasonsSource"];
180
- }
181
- if (typeof p["unfinishedReasonsProjectedCount"] === "number") {
182
- out.unfinishedReasonsProjectedCount = p["unfinishedReasonsProjectedCount"];
183
- }
184
- if (typeof p["unfinishedReasonsUnprojected"] === "boolean") {
185
- out.unfinishedReasonsUnprojected = p["unfinishedReasonsUnprojected"];
186
- }
187
- return Object.keys(out).length > 0 ? out : undefined;
188
- }
@@ -5,7 +5,6 @@
5
5
 
6
6
  import {
7
7
  COLLECTOR_OUTPUT_TOOL,
8
- validateAcceptedCollectorReceipt,
9
8
  type CollectorReceipt,
10
9
  } from "./collector-output.ts";
11
10
  import {
@@ -44,7 +43,6 @@ export {
44
43
  JUDGE_OUTPUT_TOOL_NAME,
45
44
  REVIEWER_OUTPUT_TOOL_NAME,
46
45
  MERGER_OUTPUT_TOOL_NAME,
47
- validateAcceptedCollectorReceipt,
48
46
  validateReviewerIntent,
49
47
  validateAcceptedWorkerDetails,
50
48
  validateDoctorSubmissionShape,
@@ -1,6 +1,5 @@
1
1
  /** Composition-root unique authoritative public-role records (#509 / #524). */
2
2
  import { COLLECTOR_OUTPUT_TOOL } from "./package-contracts/collector-output.ts";
3
- import { COLLECTOR_WAIT_TOOL } from "./collector-ledger.ts";
4
3
  import { GATEKEEPER_OUTPUT_TOOL_NAME } from "./package-contracts/gatekeeper-output.ts";
5
4
  import { NAVIGATOR_OUTPUT_TOOL_NAME } from "./package-contracts/navigator-output.ts";
6
5
  import { JUDGE_OUTPUT_TOOL_NAME } from "./package-contracts/judge-output.ts";
@@ -232,23 +231,18 @@ export const PUBLIC_ROLE_RECORDS = [
232
231
  ],
233
232
  },
234
233
  // ak-collector-repo is GitHub owner/repo identity, not a local material path (#438).
234
+ // #1088: seat + request-manifest kept; code no longer collects — LLM uses host CLI.
235
235
  {
236
236
  role: "collector",
237
237
  inCallAutoResume: false,
238
238
  presentSettled: "always",
239
239
  summonResume: false,
240
240
  admission: "collect-target",
241
- /** Public no_receipt projects a durable bind-target rejection. */
242
- projectTargetBindRejection: true,
243
241
  sameParent: "none",
244
242
  phases: [null],
245
243
  bareCommand: false,
246
244
  outputTool: COLLECTOR_OUTPUT_TOOL,
247
- settlement: "residual",
248
- /** #633: a prior wait-tool residual must not mask this attempt. */
249
- residualScan: "current-attempt",
250
- residualTool: COLLECTOR_WAIT_TOOL,
251
- runnerFailure: "collector-known-first",
245
+ settlement: "sealed",
252
246
  artifactFace: {
253
247
  evidenceRole: true,
254
248
  leaves: [
@@ -264,10 +258,13 @@ export const PUBLIC_ROLE_RECORDS = [
264
258
  { field: "repo", from: "repository.display", flag: "ak-collector-repo" },
265
259
  { field: "pr", from: "prNumber", text: true, flag: "ak-collector-pr" },
266
260
  { field: "requestManifestPath", flag: "ak-collector-request-manifest" },
267
- { field: "waitMs", from: "waitWindowMs", text: true, flag: "ak-collector-wait-ms" },
268
261
  ],
269
262
  activationStage: "load-and-install",
270
- sessionMaterials: ["CLAUDE.md", "souls/collector.md"],
263
+ sessionMaterials: [
264
+ "CLAUDE.md",
265
+ "souls/collector.md",
266
+ "resources/collector-bot-handbook.md",
267
+ ],
271
268
  },
272
269
  {
273
270
  role: "doctor",