@akagilnc/pi-workflow-roles 0.1.3572 → 0.1.3621

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 (129) hide show
  1. package/dist/acp-host/production-host.js +20239 -10556
  2. package/dist/analyst-gate-cycles-read.js +387 -0
  3. package/dist/archivist-record-entry.js +22 -1
  4. package/dist/audit-escalation.js +6 -0
  5. package/dist/auditor-soul.js +74 -0
  6. package/dist/collector-config.js +86 -0
  7. package/dist/collector-evidence.js +316 -0
  8. package/dist/collector-github.js +527 -0
  9. package/dist/collector-ledger.js +853 -0
  10. package/dist/collector-tool-schemas.js +33 -0
  11. package/dist/compliance-transport.js +104 -52
  12. package/dist/diarist-mechanical.js +411 -0
  13. package/dist/diarist-ticket-resolution.js +177 -0
  14. package/dist/diarist.js +311 -0
  15. package/dist/doctor-auditor.js +25 -0
  16. package/dist/doctor-contracts.js +2 -0
  17. package/dist/doctor-evidence.js +142 -0
  18. package/dist/gatekeeper-role.js +104 -81
  19. package/dist/host-transition-prior-native.js +78 -0
  20. package/dist/institutional-resolution.js +1 -95
  21. package/dist/judge-auditor.js +26 -0
  22. package/dist/ledger-session-read.js +227 -0
  23. package/dist/merger-git-state.js +78 -0
  24. package/dist/navigator-attendance.js +15 -7
  25. package/dist/navigator-public-session.js +170 -0
  26. package/dist/navigator-session-contracts.js +36 -40
  27. package/dist/notary-source-run.js +122 -0
  28. package/dist/package-contracts/auditor-output.js +66 -0
  29. package/dist/package-contracts/evidence-child-output.js +34 -0
  30. package/dist/package-contracts/judge-output.js +2 -0
  31. package/dist/package-contracts/terminating-tools.js +25 -2
  32. package/dist/package-resources/method-skill.js +254 -0
  33. package/dist/packaged-role-registry.js +36 -0
  34. package/dist/pi/durable-principal.js +61 -0
  35. package/dist/pi/in-process-session.js +25 -6
  36. package/dist/pi/known-failure.js +52 -0
  37. package/dist/pi/role-turn-host.js +430 -0
  38. package/dist/public-cli/auto-resume.js +414 -0
  39. package/dist/public-cli/cli-errors.js +8 -0
  40. package/dist/public-cli/cli-io.js +1 -0
  41. package/dist/public-cli/command-renderer.js +5 -0
  42. package/dist/public-cli/doctor-run.js +84 -0
  43. package/dist/public-cli/inspector-run.js +136 -0
  44. package/dist/public-cli/instruction-seat-run.js +256 -0
  45. package/dist/public-cli/invocation.js +2225 -0
  46. package/dist/public-cli/judge-run.js +118 -0
  47. package/dist/public-cli/load-production-acp-host.js +40 -0
  48. package/dist/public-cli/main.js +1140 -652
  49. package/dist/public-cli/notary-run.js +161 -0
  50. package/dist/public-cli/option-definitions.js +1192 -0
  51. package/dist/public-cli/post-admission.js +667 -0
  52. package/dist/public-cli/public-run-credentials.js +49 -0
  53. package/dist/public-cli/registry.js +4 -1
  54. package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
  55. package/dist/public-cli/run-lifecycle.js +1344 -0
  56. package/dist/public-cli/seat-ticket-binding.js +113 -0
  57. package/dist/public-cli/settlement.js +3441 -0
  58. package/dist/public-cli/terminal.js +135 -0
  59. package/dist/public-cli/turn-request.js +25 -0
  60. package/dist/public-role-summons.js +301 -0
  61. package/dist/receipt-delivery-policy.js +10 -0
  62. package/dist/reviewer-child-executor.js +80 -8
  63. package/dist/reviewer-execution-ledger.js +2 -1
  64. package/dist/run-terminal-artifacts.js +195 -0
  65. package/dist/run-ticket-number.js +40 -0
  66. package/dist/session-assistant-usage.js +107 -0
  67. package/dist/shape-unreadable-failure.js +34 -0
  68. package/dist/submission-errors.js +3 -0
  69. package/dist/submission-ledger.js +388 -0
  70. package/dist/ticket-provenance-contracts.js +115 -0
  71. package/dist/ticket-provenance.js +340 -0
  72. package/extensions/role-runtime.ts +7 -0
  73. package/package.json +1 -1
  74. package/scripts/build-package.mjs +2 -1
  75. package/souls/doctor-auditor.md +1 -0
  76. package/souls/evidence-child.md +1 -0
  77. package/src/acp-host/production-host.ts +3 -0
  78. package/src/acp-host/role-envelope.ts +2 -2
  79. package/src/acp-host/role-turn-host.ts +9 -1
  80. package/src/analyst-gate-cycles-read.ts +37 -2
  81. package/src/archivist-record-entry.ts +45 -1
  82. package/src/audit-escalation.ts +17 -0
  83. package/src/auditor-role.ts +22 -0
  84. package/src/auditor-soul.ts +42 -0
  85. package/src/compliance-transport.ts +177 -66
  86. package/src/doctor-auditor.ts +10 -14
  87. package/src/doctor-contracts.ts +1 -0
  88. package/src/doctor-role.ts +2 -2
  89. package/src/evidence-child-role.ts +22 -0
  90. package/src/gatekeeper-pass-envelope.ts +109 -0
  91. package/src/gatekeeper-role.ts +145 -104
  92. package/src/host-contracts.ts +9 -1
  93. package/src/institutional-resolution.ts +8 -163
  94. package/src/judge-auditor.ts +10 -15
  95. package/src/judge-role.ts +8 -0
  96. package/src/navigator-attendance.ts +25 -9
  97. package/src/navigator-public-session.ts +217 -0
  98. package/src/navigator-session-contracts.ts +61 -54
  99. package/src/notary-source-run.ts +3 -1
  100. package/src/package-contracts/auditor-output.ts +82 -0
  101. package/src/package-contracts/evidence-child-output.ts +51 -0
  102. package/src/package-contracts/judge-output.ts +1 -0
  103. package/src/package-contracts/reviewer-output.ts +2 -2
  104. package/src/package-contracts/terminating-tools.ts +31 -1
  105. package/src/packaged-role-registry.ts +43 -0
  106. package/src/pi/adapter.ts +2 -1
  107. package/src/pi/in-process-session.ts +38 -12
  108. package/src/pi/role-turn-host.ts +34 -0
  109. package/src/public-cli/cli.ts +14 -6
  110. package/src/public-cli/instruction-seat-run.ts +289 -50
  111. package/src/public-cli/invocation.ts +66 -25
  112. package/src/public-cli/option-definitions.ts +61 -0
  113. package/src/public-cli/post-admission.ts +7 -1
  114. package/src/public-cli/registry.ts +4 -1
  115. package/src/public-cli/run-lifecycle.ts +22 -3
  116. package/src/public-cli/settlement.ts +120 -5
  117. package/src/public-cli/terminal.ts +2 -0
  118. package/src/public-role-summons.ts +431 -0
  119. package/src/receipt-delivery-policy.ts +10 -0
  120. package/src/reviewer-agent.ts +1 -1
  121. package/src/reviewer-child-executor.ts +114 -12
  122. package/src/reviewer-execution-ledger.ts +3 -2
  123. package/src/role-runtime.ts +158 -17
  124. package/src/session-assistant-usage.ts +119 -0
  125. package/src/session-opening-materials.ts +1 -1
  126. package/src/shape-unreadable-failure.ts +49 -0
  127. package/src/submission-errors.ts +3 -0
  128. package/dist/evidence-child-executor.js +0 -829
  129. package/src/evidence-child-executor.ts +0 -1140
@@ -0,0 +1,33 @@
1
+ import { Type } from "typebox";
2
+ import { COLLECTOR_ELIGIBILITY_MS } from "./collector-evidence.js";
3
+ import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.js";
4
+ export const collectorObserveArgsSchema = Type.Object({}, { additionalProperties: false });
5
+ export const collectorRequestArgsSchema = Type.Object({
6
+ requestId: Type.String({ minLength: 1, description: "配置请求身份" }),
7
+ snapshotId: Type.String({ minLength: 1, description: "最新留存观察快照" }),
8
+ }, { additionalProperties: false });
9
+ export const collectorReadArgsSchema = Type.Object({
10
+ evidenceId: Type.String({ minLength: 1, description: "observe 返回的材料证据 id(evidenceId)" }),
11
+ }, { additionalProperties: false });
12
+ export const collectorWaitArgsSchema = Type.Object({
13
+ durationMs: Type.Integer({ minimum: 1, maximum: COLLECTOR_ELIGIBILITY_MS, description: "等待毫秒;单次上限五分钟且不超剩余资格" }),
14
+ }, { additionalProperties: false });
15
+ /**
16
+ * #641 chain①: the collector LLM does the splitting/classification (#245:
17
+ * 他自己就是llm为什么不能读不能拆). Findings are submitted as pointer refs into
18
+ * the stored evidence; the runtime enriches each with the machine pointer
19
+ * (repo/PR/comment id/url/author/kind/时间) and validates resolvability.
20
+ * Category is a short LLM classification label, never a body transcription.
21
+ */
22
+ export const collectorFindingArgsSchema = Type.Object({
23
+ evidenceId: Type.String({ minLength: 1, description: "observe 返回的材料证据 id(evidenceId)" }),
24
+ category: Type.Optional(Type.String({ minLength: 1, maxLength: 200, description: "该 finding 的简短归类标签;需要头部之外的正文时先用 ak_collector_read 开卷再判读,不得誊写评论正文" })),
25
+ }, { additionalProperties: false });
26
+ export const collectorOutputBaseSchema = Type.Object({
27
+ findings: Type.Optional(Type.Array(collectorFindingArgsSchema, {
28
+ description: "本次收集到的逐条 findings;零 finding 的模板通知不得进入。正常完工无 finding 时省略。",
29
+ })),
30
+ }, { additionalProperties: true, description: "正常完工提交空对象 {};仅在基础设施真实失败时才填写 infrastructureFailure,无失败时必须省略该字段。" });
31
+ /** Runtime owns the observed evidence; the model submits findings and signals sole-final submission. */
32
+ export const collectorOutputArgsSchema = withInfrastructureFailureDeclaration(collectorOutputBaseSchema);
33
+ collectorOutputArgsSchema.required = [];
@@ -1,8 +1,6 @@
1
1
  import { Type } from "typebox";
2
- import {
3
- executeAuditorChild
4
- } from "./evidence-child-executor.js";
5
- import { createAuditorDossierTool } from "./auditor-dossier-tool.js";
2
+ import { auditorRunDirectory } from "./auditor-dossier-tool.js";
3
+ import { retainedShapeUnreadable } from "./shape-unreadable-failure.js";
6
4
  class ComplianceCandidateUnreadableError extends Error {
7
5
  observation;
8
6
  candidate;
@@ -20,43 +18,39 @@ const AUDITOR_DOSSIER_PROMPT = "\u672C run \u5377\u5B97\u5DF2\u5C31\u7EEA\u3002"
20
18
  const nonblank = Type.String({ minLength: 1, pattern: "\\S" });
21
19
  const decisionGateSchema = Type.Object({ question: nonblank, options: Type.Array(nonblank, { minItems: 1 }) }, { additionalProperties: false });
22
20
  const complianceDecisionSchema = Type.Object({ status: Type.Unknown({ description: "pass | revise | escalate \u2014 \u5F62\u72B6\u6307\u5F15\uFF0C\u975E schema \u95F8" }), violations: Type.Array(nonblank, { description: "\u89C2\u5BDF\u5230\u7684\u5408\u89C4\u8FDD\u89C4" }), conflicts: Type.Array(nonblank, { description: "\u672A\u51B3\u6743\u5A01\u6216\u6267\u884C\u51B2\u7A81" }), decisionGate: Type.Union([decisionGateSchema, Type.Null()], { description: "\u5347\u7EA7\u95EE\u9898\u4E0E\u53EF\u9009\u9009\u9879" }) }, { additionalProperties: true, required: [] });
23
- function createComplianceDecisionTool(name, description) {
24
- return { name, description, parameters: complianceDecisionSchema, async execute(_id, params) {
25
- return { content: [{ type: "text", text: "\u5BA1\u8BA1\u51B3\u8BAE\u5DF2\u6536" }], details: params, terminate: true };
26
- } };
27
- }
28
21
  const COMPLIANCE_RESPONSE_ENTRY_TYPE = "ak_compliance_response";
29
22
  const AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE = "ak_auditor_parent_attempt_binding";
30
23
  const AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE = "ak_auditor_compliance_failure";
31
- import { attachDirectErrnoCode, sitianReport } from "./sitian-facade.js";
32
24
  class ComplianceResponseRetentionError extends Error {
33
25
  constructor(message, options) {
34
26
  super(message, options);
35
27
  this.name = "ComplianceResponseRetentionError";
36
- attachDirectErrnoCode(this, options?.cause);
37
- }
38
- }
39
- function retainComplianceResponse(context, response) {
40
- try {
41
- sitianReport({
42
- level: "event",
43
- kind: "auditor",
44
- cwd: context.cwd,
45
- sessionParent: context.sessionManager.getSessionFile(),
46
- payload: { version: 1, response },
47
- source: "compliance-transport"
48
- });
49
- } catch (error) {
50
- throw new ComplianceResponseRetentionError(
51
- `compliance response retention failed: ${error instanceof Error ? error.message : String(error)}`,
52
- { cause: error }
53
- );
54
28
  }
55
29
  }
56
30
  function readListField(value) {
57
31
  return Array.isArray(value) ? value : value === void 0 ? [] : [value];
58
32
  }
33
+ function tryReadComplianceCandidate(arguments_, usage) {
34
+ if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
35
+ return void 0;
36
+ }
37
+ const args = arguments_;
38
+ const status = args.status;
39
+ if (status === "pass") return { status, ...usage === void 0 ? {} : { usage } };
40
+ if (status === "revise") return { status, violations: readListField(args.violations), ...usage === void 0 ? {} : { usage } };
41
+ if (status === "escalate") {
42
+ return {
43
+ status,
44
+ ...Object.hasOwn(args, "conflicts") ? { conflicts: args.conflicts } : {},
45
+ ...Object.hasOwn(args, "decisionGate") ? { decisionGate: args.decisionGate } : {},
46
+ ...usage === void 0 ? {} : { usage }
47
+ };
48
+ }
49
+ return void 0;
50
+ }
59
51
  function readComplianceCandidate(arguments_, usage) {
52
+ const projected = tryReadComplianceCandidate(arguments_, usage);
53
+ if (projected !== void 0) return projected;
60
54
  if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
61
55
  throw new ComplianceCandidateUnreadableError(
62
56
  { kind: "non-object-arguments", type: arguments_ === null ? "null" : Array.isArray(arguments_) ? "array" : typeof arguments_ },
@@ -64,38 +58,96 @@ function readComplianceCandidate(arguments_, usage) {
64
58
  usage
65
59
  );
66
60
  }
67
- const args = arguments_;
68
- const status = args.status;
69
- if (status === "pass") return { status, ...usage === void 0 ? {} : { usage } };
70
- if (status === "revise") return { status, violations: readListField(args.violations), ...usage === void 0 ? {} : { usage } };
71
- if (status === "escalate") return { status, ...Object.hasOwn(args, "conflicts") ? { conflicts: args.conflicts } : {}, ...Object.hasOwn(args, "decisionGate") ? { decisionGate: args.decisionGate } : {}, ...usage === void 0 ? {} : { usage } };
61
+ const status = arguments_.status;
72
62
  throw new ComplianceCandidateUnreadableError(
73
63
  { kind: "object-status-unreadable", status: status === void 0 ? "missing" : "unknown" },
74
64
  arguments_,
75
65
  usage
76
66
  );
77
67
  }
78
- async function runComplianceAudit(options) {
79
- const prompt = options.serializedInput ?? AUDITOR_DOSSIER_PROMPT;
80
- const receipt = await executeAuditorChild({
81
- tool: options.tool,
82
- dossierTool: createAuditorDossierTool(options.runDirectory),
83
- systemPrompt: options.systemPrompt,
84
- prompt,
85
- roleLabel: options.roleLabel,
86
- context: options.context,
87
- retainResponse: (response) => retainComplianceResponse(options.context, response),
88
- ...options.runDirectory === void 0 ? {} : { runDirectory: options.runDirectory },
89
- ...options.signal === void 0 ? {} : { signal: options.signal }
90
- });
91
- if (receipt.noReceiptLifecycle !== void 0) {
68
+ async function usageFromSummonedSession(summoned) {
69
+ const { usageFromPublicSummon } = await import("./session-assistant-usage.js");
70
+ return usageFromPublicSummon(summoned);
71
+ }
72
+ function observationFromUnreadableCandidate(candidate) {
73
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
74
+ return {
75
+ kind: "non-object-arguments",
76
+ type: candidate === null ? "null" : Array.isArray(candidate) ? "array" : typeof candidate
77
+ };
78
+ }
79
+ const status = candidate.status;
80
+ return {
81
+ kind: "object-status-unreadable",
82
+ status: status === void 0 ? "missing" : "unknown"
83
+ };
84
+ }
85
+ function unreadableDecision(candidate, usage) {
86
+ return {
87
+ status: "unreadable",
88
+ observation: observationFromUnreadableCandidate(candidate),
89
+ candidate,
90
+ ...usage === void 0 ? {} : { usage }
91
+ };
92
+ }
93
+ async function projectAuditorTerminal(summoned) {
94
+ const outcome = summoned.terminal?.roleOutcome;
95
+ if (outcome === void 0) {
96
+ throw new Error(`Auditor public summon produced no terminal (exit ${summoned.exitCode})`);
97
+ }
98
+ const usage = await usageFromSummonedSession(summoned);
99
+ if (outcome.kind === "no_receipt") {
100
+ const { status: _ignored, kind: _kind, role: _role, decisiveFacts: _facts, ...facts } = outcome;
92
101
  return {
93
102
  status: "no-receipt",
94
- ...receipt.noReceiptLifecycle,
95
- ...receipt.response.usage === void 0 ? {} : { usage: receipt.response.usage }
103
+ ...facts,
104
+ ...usage === void 0 ? {} : { usage }
96
105
  };
97
106
  }
98
- return readComplianceCandidate(receipt.decision, receipt.response.usage);
107
+ if (outcome.kind === "failure") {
108
+ const shape = retainedShapeUnreadable(outcome.decisiveFacts);
109
+ if (shape !== void 0) {
110
+ return unreadableDecision(shape.candidate, usage);
111
+ }
112
+ throw new Error(outcome.diagnostic);
113
+ }
114
+ if (outcome.kind === "accepted") {
115
+ const candidate = {
116
+ status: outcome.status,
117
+ ...outcome.decisiveFacts
118
+ };
119
+ const projected = tryReadComplianceCandidate(candidate, usage);
120
+ if (projected !== void 0) return projected;
121
+ return unreadableDecision(candidate, usage);
122
+ }
123
+ throw new Error("Auditor public summon returned unusable terminal kind");
124
+ }
125
+ async function runComplianceAudit(options) {
126
+ const runDirectory = options.runDirectory ?? auditorRunDirectory(options.context);
127
+ if (runDirectory === void 0) {
128
+ throw new Error("Compliance audit requires a parent run directory pointer");
129
+ }
130
+ const subject = options.subject;
131
+ const summon = options.summonAuditor ?? (async (auditSubject, sourceRunDirectory, auditSignal) => {
132
+ const { summonPublicRole } = await import("./public-role-summons.js");
133
+ const { homeFromRunDirectory } = await import("./activation-ledger-topology.js");
134
+ const home = homeFromRunDirectory(sourceRunDirectory);
135
+ return await summonPublicRole({
136
+ role: "auditor",
137
+ argv: [
138
+ "--subject",
139
+ auditSubject,
140
+ "--source-run",
141
+ sourceRunDirectory,
142
+ AUDITOR_DOSSIER_PROMPT
143
+ ],
144
+ cwd: options.context.cwd ?? process.cwd(),
145
+ home,
146
+ ...auditSignal === void 0 ? {} : { signal: auditSignal }
147
+ });
148
+ });
149
+ const summoned = await summon(subject, runDirectory, options.signal);
150
+ return await projectAuditorTerminal(summoned);
99
151
  }
100
152
  export {
101
153
  AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE,
@@ -105,7 +157,7 @@ export {
105
157
  ComplianceCandidateUnreadableError,
106
158
  ComplianceResponseRetentionError,
107
159
  complianceDecisionSchema,
108
- createComplianceDecisionTool,
109
160
  readComplianceCandidate,
110
- runComplianceAudit
161
+ runComplianceAudit,
162
+ tryReadComplianceCandidate
111
163
  };
@@ -0,0 +1,411 @@
1
+ /**
2
+ * 起居郎 mechanical safeguard band — ADR 0075 / #582.
3
+ * Deterministic: source enum helpers, candidate anchors, dedup, notification
4
+ * filter, and verbatim quote reverse-verify. Never a production relevance gate.
5
+ */
6
+ import { createHash } from "node:crypto";
7
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
8
+ import { basename, join, resolve } from "node:path";
9
+ import { homedir } from "node:os";
10
+ import { pathContainedIn, physicallyContainedIn, } from "./activation-ledger-topology.js";
11
+ export const DEFAULT_QUOTE_MIN = 6;
12
+ export class DiaristSourceReadError extends Error {
13
+ code = "diarist-source-read";
14
+ reason;
15
+ sourcePath;
16
+ constructor(reason, sourcePath, message, options) {
17
+ super(message, options);
18
+ this.name = "DiaristSourceReadError";
19
+ this.reason = reason;
20
+ this.sourcePath = sourcePath;
21
+ }
22
+ }
23
+ /**
24
+ * Extract 「」 quotes from ticket face text for mechanical anchors.
25
+ * Only quotes meeting min length are kept.
26
+ */
27
+ export function extractCornerQuotes(text, minLength = DEFAULT_QUOTE_MIN) {
28
+ const out = [];
29
+ const re = /「([^」]+)」/g;
30
+ let match;
31
+ while ((match = re.exec(text)) !== null) {
32
+ const q = match[1].trim();
33
+ if (q.length >= minLength)
34
+ out.push(q);
35
+ }
36
+ return out;
37
+ }
38
+ /** Build the mechanical anchor set from ticket number + face body. */
39
+ export function buildDiaristAnchors(input) {
40
+ const fromBody = input.ticketBody === undefined ? [] : extractCornerQuotes(input.ticketBody);
41
+ const quotes = [...fromBody, ...(input.extraQuotes ?? [])];
42
+ const seen = new Set();
43
+ const uniqueQuotes = [];
44
+ for (const q of quotes) {
45
+ if (seen.has(q))
46
+ continue;
47
+ seen.add(q);
48
+ uniqueQuotes.push(q);
49
+ }
50
+ return {
51
+ ticketNumber: input.ticketNumber,
52
+ quotes: uniqueQuotes,
53
+ };
54
+ }
55
+ /** Drop notification/system noise blocks (typed flags only — no prose match). */
56
+ export function filterNotifications(blocks) {
57
+ return blocks.filter((b) => !b.isNotification);
58
+ }
59
+ function blockKey(block) {
60
+ const r = block.sourceRef;
61
+ return [
62
+ block.sourceKind,
63
+ r.sessionFile ?? "",
64
+ r.entryId === undefined ? "" : String(r.entryId),
65
+ r.path ?? "",
66
+ r.url ?? "",
67
+ createHash("sha256").update(block.transcript, "utf8").digest("hex").slice(0, 16),
68
+ ].join("|");
69
+ }
70
+ /** Dedup by source pointer + transcript digest (compression replay ×N). */
71
+ export function dedupeSourceBlocks(blocks) {
72
+ const seen = new Set();
73
+ const out = [];
74
+ for (const block of blocks) {
75
+ const key = blockKey(block);
76
+ if (seen.has(key))
77
+ continue;
78
+ seen.add(key);
79
+ out.push(block);
80
+ }
81
+ return out;
82
+ }
83
+ export function verifyQuotesVerbatim(sourceText, quotes) {
84
+ const failed = [];
85
+ for (const q of quotes) {
86
+ if (q.length === 0)
87
+ continue;
88
+ if (!sourceText.includes(q))
89
+ failed.push(q);
90
+ }
91
+ if (failed.length === 0)
92
+ return { ok: true };
93
+ return { ok: false, failedQuotes: failed };
94
+ }
95
+ /**
96
+ * Mechanical safeguard only — never a relevance gate (ADR 0075 / 锚定宪法).
97
+ * filter notifications (typed) → dedupe. Semantic selection is LLM-only.
98
+ */
99
+ export function mechanicalSafeguardPipeline(blocks) {
100
+ return dedupeSourceBlocks(filterNotifications(blocks));
101
+ }
102
+ /** Claude Code project-dir encoding: abs path with / → -. */
103
+ export function encodeClaudeProjectDir(cwd) {
104
+ const abs = cwd.startsWith("/") ? cwd : `/${cwd}`;
105
+ return abs.replace(/\//g, "-");
106
+ }
107
+ function isRecord(value) {
108
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109
+ }
110
+ function messageText(message) {
111
+ if (typeof message === "string")
112
+ return message;
113
+ if (!isRecord(message))
114
+ return "";
115
+ const content = message.content;
116
+ if (typeof content === "string")
117
+ return content;
118
+ if (!Array.isArray(content))
119
+ return "";
120
+ const parts = [];
121
+ for (const part of content) {
122
+ if (typeof part === "string") {
123
+ parts.push(part);
124
+ continue;
125
+ }
126
+ if (!isRecord(part))
127
+ continue;
128
+ if (typeof part.text === "string")
129
+ parts.push(part.text);
130
+ else if (typeof part.content === "string")
131
+ parts.push(part.content);
132
+ }
133
+ return parts.join("\n");
134
+ }
135
+ function isNotificationRow(type, text) {
136
+ if (type === "queue-operation" || type === "system" || type === "progress") {
137
+ return true;
138
+ }
139
+ if (type === "user") {
140
+ // Local command / meta noise common in cc transcripts.
141
+ if (text.startsWith("<command-name>") || text.startsWith("<local-command-")) {
142
+ return true;
143
+ }
144
+ if (text.startsWith("<command-message>") || text.startsWith("<command-args>")) {
145
+ return true;
146
+ }
147
+ }
148
+ return false;
149
+ }
150
+ /**
151
+ * Enumerate cc session user/assistant turns and human queued-command attachments under Claude projects roots.
152
+ * cc-sessions-first source family (ADR 0075).
153
+ * Directory/file/non-empty JSONL line failures throw DiaristSourceReadError
154
+ * (失败诚实:不得把未读懂洗成没有来源块).
155
+ */
156
+ export function readCcSessionBlocks(options) {
157
+ const root = options.projectsRoot ?? join(homedir(), ".claude", "projects");
158
+ if (!existsSync(root))
159
+ return [];
160
+ const blocks = [];
161
+ const dirs = new Set();
162
+ for (const cwd of options.cwds) {
163
+ dirs.add(join(root, encodeClaudeProjectDir(cwd)));
164
+ }
165
+ // Also accept already-encoded folder names passed as cwd (test convenience).
166
+ for (const cwd of options.cwds) {
167
+ if (cwd.includes("projects"))
168
+ continue;
169
+ const direct = join(root, cwd);
170
+ if (existsSync(direct))
171
+ dirs.add(direct);
172
+ }
173
+ for (const dir of dirs) {
174
+ if (!existsSync(dir))
175
+ continue;
176
+ let dirStat;
177
+ try {
178
+ dirStat = statSync(dir);
179
+ }
180
+ catch (error) {
181
+ throw new DiaristSourceReadError("readdir-failed", dir, `diarist cc session dir unstatable (${dir})`, { cause: error });
182
+ }
183
+ if (!dirStat.isDirectory())
184
+ continue;
185
+ let files;
186
+ try {
187
+ files = readdirSync(dir).filter((name) => name.endsWith(".jsonl"));
188
+ }
189
+ catch (error) {
190
+ throw new DiaristSourceReadError("readdir-failed", dir, `diarist cc session dir unreadable (${dir})`, { cause: error });
191
+ }
192
+ for (const name of files) {
193
+ const sessionFile = join(dir, name);
194
+ let text;
195
+ try {
196
+ text = readFileSync(sessionFile, "utf8");
197
+ }
198
+ catch (error) {
199
+ throw new DiaristSourceReadError("file-unreadable", sessionFile, `diarist cc session file unreadable (${sessionFile})`, { cause: error });
200
+ }
201
+ const lines = text.split("\n");
202
+ for (let lineNo = 0; lineNo < lines.length; lineNo += 1) {
203
+ const line = lines[lineNo];
204
+ if (!line.trim())
205
+ continue;
206
+ let parsed;
207
+ try {
208
+ parsed = JSON.parse(line);
209
+ }
210
+ catch (error) {
211
+ throw new DiaristSourceReadError("jsonl-line-unparseable", sessionFile, `diarist cc session JSONL line ${lineNo + 1} unparseable (${sessionFile})`, { cause: error });
212
+ }
213
+ // Unknown row shape (not user/assistant or a human queued-command attachment) is ignored — not a read failure.
214
+ if (!isRecord(parsed))
215
+ continue;
216
+ const type = typeof parsed.type === "string" ? parsed.type : "";
217
+ const queuedCommand = type === "attachment" &&
218
+ isRecord(parsed.attachment) &&
219
+ parsed.attachment.type === "queued_command" &&
220
+ isRecord(parsed.attachment.origin) &&
221
+ parsed.attachment.origin.kind === "human" &&
222
+ typeof parsed.attachment.prompt === "string"
223
+ ? parsed.attachment.prompt
224
+ : undefined;
225
+ if (type !== "user" && type !== "assistant" && queuedCommand === undefined) {
226
+ continue;
227
+ }
228
+ const transcript = queuedCommand ?? messageText(parsed.message ?? parsed);
229
+ if (transcript.trim() === "")
230
+ continue;
231
+ const entryId = typeof parsed.uuid === "string"
232
+ ? parsed.uuid
233
+ : typeof parsed.id === "string"
234
+ ? parsed.id
235
+ : `${basename(sessionFile)}:${lineNo + 1}`;
236
+ const timestamp = typeof parsed.timestamp === "string"
237
+ ? parsed.timestamp
238
+ : new Date(0).toISOString();
239
+ const notification = isNotificationRow(type, transcript);
240
+ blocks.push({
241
+ sourceKind: "cc-session",
242
+ sourceRef: { sessionFile, entryId },
243
+ transcript,
244
+ timestamp,
245
+ isUserTurn: type === "user" || queuedCommand !== undefined,
246
+ isNotification: notification,
247
+ });
248
+ }
249
+ }
250
+ }
251
+ return blocks;
252
+ }
253
+ /**
254
+ * Issue body + comments + 「」 decree spans as typed source blocks.
255
+ * Each block keeps its own sourceRef (url / comment id) — never merge attachments here.
256
+ */
257
+ export function readIssueFaceBlocks(input) {
258
+ const fallbackTs = input.timestamp ?? new Date(0).toISOString();
259
+ const blocks = [];
260
+ const body = input.face.body;
261
+ if (body.trim() !== "") {
262
+ blocks.push({
263
+ sourceKind: "issue-body-comment",
264
+ sourceRef: { url: input.face.bodyUrl, entryId: "body" },
265
+ transcript: body,
266
+ timestamp: fallbackTs,
267
+ isUserTurn: true,
268
+ isNotification: false,
269
+ });
270
+ const quotes = extractCornerQuotes(body);
271
+ for (let i = 0; i < quotes.length; i += 1) {
272
+ const q = quotes[i];
273
+ blocks.push({
274
+ sourceKind: "ticket-decree-block",
275
+ sourceRef: { url: input.face.bodyUrl, entryId: `decree-${i + 1}` },
276
+ transcript: q,
277
+ timestamp: fallbackTs,
278
+ isUserTurn: true,
279
+ isNotification: false,
280
+ });
281
+ }
282
+ }
283
+ for (const comment of input.face.comments) {
284
+ if (comment.body.trim() === "")
285
+ continue;
286
+ blocks.push({
287
+ sourceKind: "issue-body-comment",
288
+ sourceRef: {
289
+ url: comment.htmlUrl,
290
+ entryId: comment.id,
291
+ },
292
+ transcript: comment.body,
293
+ timestamp: comment.createdAt || fallbackTs,
294
+ isUserTurn: true,
295
+ isNotification: false,
296
+ });
297
+ }
298
+ return blocks;
299
+ }
300
+ /** Relative root every ADR decision-key read must stay under (ADR 0038). */
301
+ export const DIARIST_ADR_ROOT_REL = "docs/adr";
302
+ /**
303
+ * Read applicable ADR files as decision-key source blocks.
304
+ * Real IO seam owns lexical + physical containment under cwd/docs/adr (ADR 0038).
305
+ * Missing / unreadable / escape → typed DiaristSourceReadError (失败诚实).
306
+ */
307
+ export function readAdrDecisionKeyBlocks(input) {
308
+ const timestamp = input.timestamp ?? new Date(0).toISOString();
309
+ const blocks = [];
310
+ const cwdAbs = resolve(input.cwd);
311
+ const adrRootAbs = resolve(cwdAbs, DIARIST_ADR_ROOT_REL);
312
+ let adrRootReal;
313
+ try {
314
+ // Prefer the real ADR root when it exists; otherwise fall back to cwd real
315
+ // so physical checks still have a stable anchor before the first ADR file.
316
+ adrRootReal = existsSync(adrRootAbs)
317
+ ? realpathSync(adrRootAbs)
318
+ : resolve(realpathSync(cwdAbs), DIARIST_ADR_ROOT_REL);
319
+ }
320
+ catch (error) {
321
+ throw new DiaristSourceReadError("adr-unreadable", cwdAbs, `diarist ADR root is not resolvable (${adrRootAbs})`, { cause: error });
322
+ }
323
+ for (const rel of input.adrPaths) {
324
+ const segments = rel.split(/[/\\]/);
325
+ // Claimed path must sit under docs/adr/ with no traversal segments.
326
+ if (segments[0] !== "docs" ||
327
+ segments[1] !== "adr" ||
328
+ segments.length < 3 ||
329
+ segments.some((part) => part === "" || part === "." || part === "..")) {
330
+ throw new DiaristSourceReadError("adr-escape", resolve(cwdAbs, rel), `diarist referenced ADR escapes docs/adr (${rel})`);
331
+ }
332
+ const abs = resolve(cwdAbs, rel);
333
+ // Lexical containment under docs/adr before any read (ADR 0038).
334
+ if (abs !== adrRootAbs && !pathContainedIn(adrRootAbs, abs)) {
335
+ throw new DiaristSourceReadError("adr-escape", abs, `diarist referenced ADR escapes docs/adr (${rel})`);
336
+ }
337
+ if (!existsSync(abs)) {
338
+ throw new DiaristSourceReadError("adr-missing", abs, `diarist referenced ADR missing (${rel})`);
339
+ }
340
+ let realAbs;
341
+ try {
342
+ realAbs = realpathSync(abs);
343
+ }
344
+ catch (error) {
345
+ throw new DiaristSourceReadError("adr-unreadable", abs, `diarist ADR path is not resolvable (${abs})`, { cause: error });
346
+ }
347
+ if (realAbs !== adrRootReal && !physicallyContainedIn(adrRootReal, realAbs)) {
348
+ throw new DiaristSourceReadError("adr-escape", abs, `diarist referenced ADR escapes docs/adr physically (${rel})`);
349
+ }
350
+ let text;
351
+ try {
352
+ text = readFileSync(realAbs, "utf8");
353
+ }
354
+ catch (error) {
355
+ throw new DiaristSourceReadError("adr-unreadable", realAbs, `diarist ADR file unreadable (${realAbs})`, { cause: error });
356
+ }
357
+ if (text.trim() === "")
358
+ continue;
359
+ blocks.push({
360
+ sourceKind: "adr-decision-key",
361
+ sourceRef: { path: rel },
362
+ transcript: text,
363
+ timestamp,
364
+ isUserTurn: true,
365
+ isNotification: false,
366
+ });
367
+ }
368
+ return blocks;
369
+ }
370
+ /**
371
+ * Build an llm-semantic entry after reverse-verify succeeds.
372
+ * `quotes` are the LLM-claimed quotes that must be verbatim in transcript.
373
+ * basis.anchors record ticket # + mechanical/claimed quotes for audit only.
374
+ * Failure returns failedQuotes only — caller records a single diagnostic (not a
375
+ * disguised diary entry / dead basis.method).
376
+ */
377
+ export function blockToLlmEntry(block, input) {
378
+ const verify = verifyQuotesVerbatim(block.transcript, input.quotes);
379
+ if (!verify.ok) {
380
+ return {
381
+ ok: false,
382
+ failedQuotes: verify.failedQuotes,
383
+ cause: `verbatim verify failed: ${verify.failedQuotes.join(" | ")}`,
384
+ };
385
+ }
386
+ // Ticket + mechanical face quotes + claimed quotes — reference/反验 only.
387
+ const anchorNotes = [`#${input.anchors.ticketNumber}`];
388
+ const seen = new Set(anchorNotes);
389
+ for (const q of [...input.anchors.quotes, ...input.quotes]) {
390
+ if (seen.has(q))
391
+ continue;
392
+ seen.add(q);
393
+ anchorNotes.push(q);
394
+ if (anchorNotes.length >= 12)
395
+ break;
396
+ }
397
+ return {
398
+ ok: true,
399
+ entry: {
400
+ basis: {
401
+ method: "llm-semantic",
402
+ anchors: anchorNotes,
403
+ ...(input.note === undefined ? {} : { note: input.note }),
404
+ },
405
+ sourceKind: block.sourceKind,
406
+ sourceRef: block.sourceRef,
407
+ transcript: block.transcript,
408
+ timestamp: block.timestamp,
409
+ },
410
+ };
411
+ }