@astrosheep/keiyaku 2.9.4 → 2.9.6

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 (143) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/call-terms.js +1 -0
  3. package/build/agents/harness/execution-handle.js +1 -0
  4. package/build/agents/harness/outcome.js +31 -5
  5. package/build/agents/harness/projection.js +151 -78
  6. package/build/agents/harness/router.js +2 -1
  7. package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
  8. package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
  9. package/build/agents/providers/codex-app-server/adapter.js +43 -10
  10. package/build/agents/providers/codex-app-server/events.js +23 -0
  11. package/build/agents/providers/codex-app-server/registration.js +2 -1
  12. package/build/agents/providers/opencode-sdk/adapter.js +7 -0
  13. package/build/agents/providers/opencode-sdk/registration.js +2 -1
  14. package/build/agents/providers/pi/adapter.js +10 -0
  15. package/build/agents/providers/pi/events.js +4 -3
  16. package/build/agents/providers/pi/registration.js +3 -1
  17. package/build/agents/types.js +2 -0
  18. package/build/cli/commands/akuma/akuma/meta.js +1 -1
  19. package/build/cli/commands/akuma/list/meta-ls.js +1 -1
  20. package/build/cli/commands/akuma/view/meta.js +1 -1
  21. package/build/cli/commands/contract/amend/meta.js +4 -4
  22. package/build/cli/commands/contract/arc/handler.js +1 -1
  23. package/build/cli/commands/contract/arc/meta.js +4 -4
  24. package/build/cli/commands/contract/audit/handler.js +16 -0
  25. package/build/cli/commands/contract/audit/meta.js +16 -0
  26. package/build/cli/commands/contract/bind/meta.js +20 -11
  27. package/build/cli/commands/contract/catalog.js +2 -0
  28. package/build/cli/commands/contract/forfeit/meta.js +1 -1
  29. package/build/cli/commands/contract/log/meta.js +1 -1
  30. package/build/cli/commands/contract/petition/handler.js +1 -2
  31. package/build/cli/commands/contract/petition/meta.js +6 -6
  32. package/build/cli/commands/contract/renew/handler.js +2 -2
  33. package/build/cli/commands/contract/renew/meta.js +4 -4
  34. package/build/cli/commands/metadata.js +3 -2
  35. package/build/cli/commands/projection/call/meta.js +4 -1
  36. package/build/cli/commands/projection/kill/meta.js +1 -1
  37. package/build/cli/commands/projection/revive/meta.js +4 -1
  38. package/build/cli/commands/projection/status/meta.js +1 -1
  39. package/build/cli/commands/projection/tell/handler.js +1 -1
  40. package/build/cli/commands/projection/tell/meta.js +4 -1
  41. package/build/cli/commands/projection/wait/meta.js +1 -1
  42. package/build/cli/commands/skills/install/meta.js +1 -1
  43. package/build/cli/commands/skills/skills/meta.js +1 -1
  44. package/build/cli/commands/system/completion/meta.js +1 -1
  45. package/build/cli/commands/system/dump-env/meta.js +1 -1
  46. package/build/cli/commands/system/guide/meta.js +1 -1
  47. package/build/cli/commands/task/add/handler.js +2 -2
  48. package/build/cli/commands/task/add/meta.js +4 -1
  49. package/build/cli/commands/task/doctor/meta.js +1 -1
  50. package/build/cli/commands/task/done/meta.js +1 -1
  51. package/build/cli/commands/task/drop/meta.js +1 -1
  52. package/build/cli/commands/task/ls/handler.js +10 -4
  53. package/build/cli/commands/task/ls/meta.js +3 -3
  54. package/build/cli/commands/task/show/meta.js +1 -1
  55. package/build/cli/commands/task/start/meta.js +1 -1
  56. package/build/cli/commands/task/stop/meta.js +1 -1
  57. package/build/cli/commands/task/task/meta.js +1 -1
  58. package/build/cli/commands/task/update/handler.js +2 -3
  59. package/build/cli/commands/task/update/meta.js +4 -1
  60. package/build/cli/completion.js +9 -2
  61. package/build/cli/flags.js +19 -3
  62. package/build/cli/help.js +3 -3
  63. package/build/cli/index.js +59 -75
  64. package/build/cli/output-stream-error-policy.js +19 -5
  65. package/build/cli/parse-flags.js +19 -2
  66. package/build/cli/parse-metadata.js +1 -7
  67. package/build/cli/parse-selectors.js +1 -0
  68. package/build/cli/parse.js +16 -7
  69. package/build/cli/projection-address.js +6 -1
  70. package/build/cli/render/audit.js +121 -0
  71. package/build/cli/render/errors.js +18 -1
  72. package/build/cli/render/format.js +5 -2
  73. package/build/cli/render/misc.js +3 -1
  74. package/build/cli/render/projection-activity.js +12 -6
  75. package/build/cli/render/status.js +28 -18
  76. package/build/cli/render/success-response.js +6 -5
  77. package/build/cli/render/task.js +12 -43
  78. package/build/cli/render/terminal-failure.js +7 -2
  79. package/build/cli/render/wait.js +36 -8
  80. package/build/config/env-keys.js +2 -0
  81. package/build/config/env.js +3 -0
  82. package/build/core/amend.js +20 -94
  83. package/build/core/arc.js +14 -6
  84. package/build/core/audit/candidate.js +48 -0
  85. package/build/core/audit/coordinates.js +180 -0
  86. package/build/core/audit/evidence.js +158 -0
  87. package/build/core/audit/facade.js +42 -0
  88. package/build/core/audit/readiness.js +4 -0
  89. package/build/core/audit/report.js +38 -0
  90. package/build/core/audit.js +1 -0
  91. package/build/core/bind.js +1 -1
  92. package/build/core/call/execution.js +1 -0
  93. package/build/core/contract-view.js +2 -3
  94. package/build/core/draft.js +33 -20
  95. package/build/core/entry.js +3 -4
  96. package/build/core/markdown/lex.js +2 -1
  97. package/build/core/projection/generation/database.js +41 -16
  98. package/build/core/projection/generation/model.js +22 -2
  99. package/build/core/projection/generation/projection-generation-execution.js +20 -7
  100. package/build/core/projection/generation/projection-generation-launcher.js +4 -4
  101. package/build/core/projection/generation/projection-generation-runner.js +1 -0
  102. package/build/core/projection/generation/store.js +1 -1
  103. package/build/core/projection/generation/transitions.js +19 -3
  104. package/build/core/projection/index.js +2 -2
  105. package/build/core/projection/projection-execution-observer.js +35 -21
  106. package/build/core/projection/projection-life-observer.js +16 -2
  107. package/build/core/projection/projection-status.js +67 -22
  108. package/build/core/projection/projection-terminal-failure.js +2 -0
  109. package/build/core/projection/projection-wait.js +6 -9
  110. package/build/core/projection/projection-wake.js +30 -6
  111. package/build/core/renew.js +41 -22
  112. package/build/core/scope.js +129 -2
  113. package/build/core/seal.js +36 -15
  114. package/build/core/settlement/petition-claim-gates.js +4 -33
  115. package/build/core/settlement/petition-preview.js +1 -1
  116. package/build/core/settlement/petition.js +10 -3
  117. package/build/core/settlement/settlement.js +3 -10
  118. package/build/core/settlement/verification.js +59 -0
  119. package/build/core/status/board.js +48 -28
  120. package/build/core/stored-agent-event.js +3 -2
  121. package/build/core/task/board.js +1 -0
  122. package/build/core/task/commands.js +20 -12
  123. package/build/core/task/document.js +5 -0
  124. package/build/core/task/index.js +1 -1
  125. package/build/core/task/model.js +8 -1
  126. package/build/core/task/query.js +214 -0
  127. package/build/core/task/settlement-git.js +115 -16
  128. package/build/core/task/settlement-policy.js +17 -9
  129. package/build/core/task/source-board.js +1 -0
  130. package/build/core/task/task-contract.js +125 -51
  131. package/build/core/task/task-store-repository.js +1 -0
  132. package/build/core/task/task.js +2 -0
  133. package/build/flow-error.js +2 -4
  134. package/build/generated/version.js +2 -2
  135. package/build/git/diff/parsers.js +23 -13
  136. package/build/git/diff/structured.js +127 -0
  137. package/build/index.js +7 -7
  138. package/build/telemetry/logger.js +4 -2
  139. package/package.json +1 -1
  140. package/skills/keiyaku/SKILL.md +2 -2
  141. package/skills/keiyaku-akuma/SKILL.md +17 -6
  142. package/skills/keiyaku-task/SKILL.md +16 -7
  143. package/skills/keiyaku-workflow/SKILL.md +48 -13
@@ -0,0 +1,4 @@
1
+ /** Readiness is assembled from the coordinate owner's one frozen read. */
2
+ export function readinessFromFacts(facts) {
3
+ return facts;
4
+ }
@@ -0,0 +1,38 @@
1
+ export function collectFindings(input) {
2
+ const findings = [];
3
+ const selector = `keiyaku @${input.contractId}`;
4
+ if (input.rehearsal.status === "clean")
5
+ findings.push({ code: "BASE_DRIFT", detail: `${input.rehearsal.oldBase} -> ${input.rehearsal.newBase}`, nextCommand: `${selector} renew` });
6
+ if (input.rehearsal.status === "conflict")
7
+ findings.push({ code: "RENEW_CONFLICT", detail: input.rehearsal.paths.join(", "), nextCommand: `${selector} renew` });
8
+ if (input.rehearsal.status === "unknown")
9
+ findings.push({ code: "RENEW_UNKNOWN", detail: `${input.rehearsal.cause.code}: ${input.rehearsal.cause.message}`, nextCommand: `${selector} audit` });
10
+ for (const row of input.scope?.filter((item) => item.class === "violated") ?? [])
11
+ findings.push({ code: "SCOPE_RESIDUE", detail: `${row.path}: ${row.residue.length} unexplained changes`, nextCommand: `${selector} amend --scope-add ${JSON.stringify(row.path)}` });
12
+ if (input.verification.status === "evaluated") {
13
+ for (const result of input.verification.results) {
14
+ if (result.status === "executed" && result.exitCode !== 0)
15
+ findings.push({ code: "VERIFICATION_FAILED", detail: `${result.label} exited ${result.exitCode}`, nextCommand: result.command });
16
+ if (result.status === "unverifiable")
17
+ findings.push({ code: "VERIFICATION_UNVERIFIABLE", detail: `${result.label}: ${result.cause}`, nextCommand: result.command });
18
+ }
19
+ }
20
+ const residue = input.readiness.deliveryResidue;
21
+ if (residue.tracked.length + residue.staged.length + residue.unstaged.length + residue.untracked.length > 0)
22
+ findings.push({ code: "DELIVERY_RESIDUE", detail: `registered delivery worktree has uncommitted residue at ${input.deliveryCwd}`, nextCommand: `git -C ${JSON.stringify(input.deliveryCwd)} status --short` });
23
+ if (input.readiness.arc.max > 0 && input.readiness.arc.open === null && input.readiness.arc.looseCommits.length > 0)
24
+ findings.push({ code: "LOOSE_COMMITS", detail: `${input.readiness.arc.looseCommits.length} loose commits after fence`, nextCommand: `printf '# Continue\\n\\n## Objective\\nReceipt loose commits.\\n\\n## Brief\\nContinue delivery.\\n' | ${selector} arc` });
25
+ if (input.readiness.renewSession.open)
26
+ findings.push({ code: "RENEW_SESSION_OPEN", detail: "renew recovery session is open", nextCommand: `${selector} renew --continue` });
27
+ if (input.readiness.rewriteGuard.liveProjection)
28
+ findings.push({ code: "LIVE_REWRITE_GUARD", detail: "a live projection owns the delivery worktree", nextCommand: "keiyaku status" });
29
+ for (const dependency of input.readiness.dependencies.filter((item) => !item.satisfied))
30
+ findings.push({ code: "DEPENDENCY_OPEN", detail: `dependency ${dependency.id} is ${dependency.state}`, nextCommand: `keiyaku @${dependency.id} log` });
31
+ if (input.readiness.petition.state === "petitioned" && input.readiness.petition.queue?.position !== 1)
32
+ findings.push({ code: "PETITION_QUEUE", detail: `petition queue position ${input.readiness.petition.queue?.position ?? "unknown"}`, nextCommand: "keiyaku status" });
33
+ return findings;
34
+ }
35
+ export function emptyRaceReport(input) {
36
+ const findings = [{ code: "COORDINATE_RACE", detail: input.detail, nextCommand: `keiyaku @${input.contractId} audit`, race: input.race }];
37
+ return { contract: input.contractId, coordinates: input.coordinates, terms: input.terms, rehearsal: { status: "unknown", cause: { code: "COORDINATE_RACE", message: input.detail } }, candidate: null, diff: null, presentation: null, files: null, scope: null, verification: { status: "not_evaluated", reason: "coordinate_race", results: [] }, readiness: null, findings, counts: { files: 0, selected: 0, omitted: 0, inScope: 0, downgraded: 0, violated: 0, findings: 1 } };
38
+ }
@@ -0,0 +1 @@
1
+ export { auditContract } from "./audit/facade.js";
@@ -119,7 +119,7 @@ function incompleteTaskBindError(taskId, missing) {
119
119
  const template = renderMissingBindSectionsTemplate(missing);
120
120
  const message = [
121
121
  `task ${coordinate} carries no ${describeMissingBindSections(missing)}`,
122
- `» keiyaku bind --task ${coordinate} <<'KEIYAKU'`,
122
+ `» keiyaku bind --task ${coordinate} - <<'KEIYAKU'`,
123
123
  template,
124
124
  "KEIYAKU",
125
125
  ].join("\n");
@@ -159,6 +159,7 @@ export async function buildCallConfig(agentName, prompt, cwd, options) {
159
159
  task: options.task ?? prompt,
160
160
  launchSnapshot,
161
161
  idleTimeoutMs: resolvedTerms.idleTimeoutMs,
162
+ executionTimeoutMs: resolvedTerms.executionTimeoutMs,
162
163
  cwd,
163
164
  historyCwd: options.historyStorageCwd ?? cwd,
164
165
  ...(options.evidenceCwd !== undefined ? { evidenceCwd: options.evidenceCwd } : {}),
@@ -5,6 +5,7 @@ import { shortCommitHash } from "../git/commits.js";
5
5
  import { KEIYAKU_FILE } from "../keiyaku.js";
6
6
  import { readLedger } from "./ledger.js";
7
7
  import { deriveContractState } from "./status/lifecycle.js";
8
+ import { renderScopeFragment } from "./scope.js";
8
9
  const VERB_COLUMN_WIDTH = 11;
9
10
  const TIME_COLUMN_WIDTH = 6;
10
11
  const ACTOR_COLUMN_WIDTH = 11;
@@ -41,8 +42,6 @@ function sealAggregate(entries) {
41
42
  function scopeStatusFact(entry) {
42
43
  if (entry.data.scopeAudit.violations.length === 0)
43
44
  return "scope ✓ clean";
44
- if (entry.data.scopeAudit.waived)
45
- return `scope ▲ waived ${entry.data.scopeAudit.violations.length}`;
46
45
  return `scope ✕ ${entry.data.scopeAudit.violations.length}`;
47
46
  }
48
47
  function formatDiffstat(insertions, deletions) {
@@ -258,7 +257,7 @@ export function renderContractView(input) {
258
257
  bind.data.objective,
259
258
  "",
260
259
  "## Scope",
261
- ...bind.data.scope.map((pattern) => `- ${pattern}`),
260
+ ...renderScopeFragment(bind.data.scope).split("\n"),
262
261
  "",
263
262
  "## Checks",
264
263
  ...bind.data.checks.map((check) => `- ${check}`),
@@ -1,7 +1,8 @@
1
1
  import { FlowError } from "../flow-error.js";
2
2
  import { parseToAST } from "./markdown/parser.js";
3
3
  import { extractListItems, renderSectionContent } from "./markdown/render.js";
4
- import { normalizeScopePattern } from "./scope.js";
4
+ import { normalizeSectionTitle } from "./markdown/titles.js";
5
+ import { markdownSourceSections, renderScopeFragment, validateScopeFragmentLines } from "./scope.js";
5
6
  const KNOWN_SECTIONS = new Set(["objective", "scope", "checks"]);
6
7
  const BIND_DRAFT_SECTION_ORDER = ["name", "objective", "scope", "checks"];
7
8
  function invalidBindDraft(message) {
@@ -74,7 +75,16 @@ function extractSingleLineChecks(section) {
74
75
  });
75
76
  }
76
77
  function collectBindDraftNodes(content) {
77
- const ast = parseToAST(content);
78
+ // Scope is parsed from source lines below. Hide its body from the Markdown AST
79
+ // so fence-like and heading-like payload bytes cannot acquire structure there.
80
+ let structuralContent = content;
81
+ const scopeSources = markdownSourceSections(content)
82
+ .filter((section) => section.level === 2 && normalizeSectionTitle(section.title) === "scope")
83
+ .sort((left, right) => right.contentStart - left.contentStart);
84
+ for (const source of scopeSources) {
85
+ structuralContent = `${structuralContent.slice(0, source.contentStart)}\`\`\`\nscope\n\`\`\`\n${structuralContent.slice(source.end)}`;
86
+ }
87
+ const ast = parseToAST(structuralContent);
78
88
  let name;
79
89
  let seenH1 = false;
80
90
  const sections = new Map();
@@ -107,21 +117,22 @@ function collectBindDraftNodes(content) {
107
117
  }
108
118
  return { ...(name === undefined ? {} : { name }), sections, extensions };
109
119
  }
110
- function parseScopeSection(scope) {
111
- const unexpectedScopeLine = findUnexpectedListOnlyLine(scope);
112
- if (unexpectedScopeLine) {
113
- invalidBindDraft(`Scope must be a bullet list; found '${unexpectedScopeLine}'`);
120
+ function parseScopeSection(content) {
121
+ const sourceSections = markdownSourceSections(content).filter((section) => section.level === 2 && normalizeSectionTitle(section.title) === "scope");
122
+ if (sourceSections.length !== 1)
123
+ invalidBindDraft("bind draft must contain exactly one Scope section");
124
+ const source = sourceSections[0];
125
+ try {
126
+ return validateScopeFragmentLines(content.slice(source.contentStart, source.end).split(/\r?\n/), {
127
+ heading: "## Scope",
128
+ emptyMessage: "Scope must be a gitignore-subset fragment; found empty Scope section",
129
+ });
114
130
  }
115
- const scopeItems = extractListItems(scope);
116
- if (scopeItems.length === 0) {
117
- invalidBindDraft("Scope must be a bullet list; found empty Scope section");
131
+ catch (error) {
132
+ if (error instanceof FlowError)
133
+ throw new FlowError(error.code, `INVALID_BIND_DRAFT: ${error.message}`);
134
+ throw error;
118
135
  }
119
- return scopeItems.map((item) => {
120
- if (item.trim().length === 0) {
121
- invalidBindDraft("empty scope pattern in bind draft");
122
- }
123
- return normalizeScopePattern(item);
124
- });
125
136
  }
126
137
  /**
127
138
  * Parse any subset of the bind document grammar. Absent sections stay undefined
@@ -142,8 +153,9 @@ export function parsePartialBindDraft(content) {
142
153
  partial.objective = objectiveText;
143
154
  }
144
155
  const scope = sections.get("scope");
145
- if (scope)
146
- partial.scope = parseScopeSection(scope);
156
+ if (scope) {
157
+ partial.scope = parseScopeSection(content);
158
+ }
147
159
  const checks = sections.get("checks");
148
160
  if (checks)
149
161
  partial.checks = extractSingleLineChecks(checks);
@@ -192,7 +204,7 @@ export function renderMissingBindSectionsTemplate(missing) {
192
204
  continue;
193
205
  }
194
206
  if (section === "scope") {
195
- lines.push("## Scope", "- <pattern>");
207
+ lines.push("## Scope", "```", "<pattern>", "<another-pattern>", "```");
196
208
  continue;
197
209
  }
198
210
  lines.push("## Checks", "- <verification>");
@@ -233,6 +245,7 @@ export function parseBindDraft(content) {
233
245
  invalidBindDraft("missing Objective section in bind draft");
234
246
  if (!scope)
235
247
  invalidBindDraft("missing Scope section in bind draft");
248
+ const parsedScope = parseScopeSection(content);
236
249
  if (!checks)
237
250
  invalidBindDraft("missing Checks section in bind draft");
238
251
  const objectiveText = renderSectionContent(objective).trim();
@@ -241,7 +254,7 @@ export function parseBindDraft(content) {
241
254
  return {
242
255
  name,
243
256
  objective: objectiveText,
244
- scope: parseScopeSection(scope),
257
+ scope: parsedScope,
245
258
  checks: extractSingleLineChecks(checks),
246
259
  extensions,
247
260
  };
@@ -250,7 +263,7 @@ export function renderBindDraft(draft) {
250
263
  const sections = [
251
264
  `# ${draft.name}`,
252
265
  `## Objective\n${draft.objective}`,
253
- `## Scope\n${draft.scope.map((pattern) => `- ${pattern}`).join("\n")}`,
266
+ `## Scope\n${renderScopeFragment(draft.scope)}`,
254
267
  `## Checks\n${draft.checks.map((check) => `- ${check}`).join("\n")}`,
255
268
  ];
256
269
  for (const extension of draft.extensions) {
@@ -35,7 +35,6 @@ const diffstatSchema = z
35
35
  const scopeAuditSchema = z
36
36
  .object({
37
37
  violations: z.array(nonEmptyString),
38
- waived: z.boolean(),
39
38
  })
40
39
  .strict();
41
40
  export const fullBranchRefSchema = z
@@ -200,10 +199,10 @@ export const forfeitEntryDataSchema = z
200
199
  note: nonEmptyString.optional(),
201
200
  })
202
201
  .strict();
203
- function makeEntrySchema(kind, data) {
202
+ function makeEntrySchema(kind, data, version = 1) {
204
203
  return z
205
204
  .object({
206
- v: z.literal(1),
205
+ v: z.literal(version),
207
206
  kind: z.literal(kind),
208
207
  contract: z.string().trim().regex(CONTRACT_ID_RE, "contract id cannot contain whitespace"),
209
208
  at: isoUtcSchema,
@@ -214,7 +213,7 @@ function makeEntrySchema(kind, data) {
214
213
  }
215
214
  export const bindEntrySchema = makeEntrySchema("bind", bindEntryDataSchema);
216
215
  export const arcEntrySchema = makeEntrySchema("arc", arcEntryDataSchema);
217
- export const sealEntrySchema = makeEntrySchema("seal", sealEntryDataSchema);
216
+ export const sealEntrySchema = makeEntrySchema("seal", sealEntryDataSchema, 2);
218
217
  export const amendEntrySchema = makeEntrySchema("amend", amendEntryDataSchema);
219
218
  export const renewEntrySchema = makeEntrySchema("renew", renewEntryDataSchema);
220
219
  export const petitionEntrySchema = makeEntrySchema("petition", petitionEntryDataSchema);
@@ -173,7 +173,8 @@ export function stripTrailingBlankLines(lines) {
173
173
  return trimmed;
174
174
  }
175
175
  export function isSectionHeaderToken(token) {
176
- return token.type === "header" && (token.level === 1 || token.level === 2);
176
+ // Column-0 only: indented ATX-looking lines remain content, not structure.
177
+ return token.type === "header" && token.leadingSpaces === 0 && (token.level === 1 || token.level === 2);
177
178
  }
178
179
  export function isFenceClosingToken(token, fence) {
179
180
  return token.type === "fence" && token.delimiter === fence.delimiter && token.length >= fence.length;
@@ -57,11 +57,40 @@ const TELL_TABLE_SQL = `
57
57
  ) STRICT
58
58
  `;
59
59
  const closedDatabases = new WeakSet();
60
+ /** Classify only proved missing schema evidence as historical incompatibility. */
61
+ export function projectionGenerationStoreInspectionError(databasePath, error) {
62
+ if (error instanceof ProjectionGenerationStoreError)
63
+ return error;
64
+ const code = error && typeof error === "object" && "code" in error
65
+ ? error.code
66
+ : undefined;
67
+ if (code === "ENOENT") {
68
+ return new ProjectionGenerationStoreError(`projection generation store does not exist at ${databasePath}`, {
69
+ cause: error,
70
+ observation: "incompatible",
71
+ });
72
+ }
73
+ return new ProjectionGenerationStoreError(`projection generation store could not be inspected at ${databasePath}`, {
74
+ cause: error,
75
+ });
76
+ }
77
+ function projectionGenerationStoreReadError(databasePath, error) {
78
+ if (error instanceof ProjectionGenerationStoreError)
79
+ return error;
80
+ const sqliteCode = error && typeof error === "object" && "errcode" in error
81
+ ? error.errcode
82
+ : undefined;
83
+ const incompatible = sqliteCode === 11 || sqliteCode === 26;
84
+ return new ProjectionGenerationStoreError(`cannot read projection generation store at ${databasePath}`, {
85
+ cause: error,
86
+ ...(incompatible ? { observation: "incompatible" } : {}),
87
+ });
88
+ }
60
89
  function pragmaNumber(database, pragma) {
61
90
  const row = database.prepare(`PRAGMA ${pragma}`).get();
62
91
  const value = row ? Object.values(row)[0] : undefined;
63
92
  if (typeof value !== "number" && typeof value !== "bigint") {
64
- throw new ProjectionGenerationStoreError(`PRAGMA ${pragma} returned an invalid value`);
93
+ throw new ProjectionGenerationStoreError(`PRAGMA ${pragma} returned an invalid value`, { observation: "incompatible" });
65
94
  }
66
95
  return Number(value);
67
96
  }
@@ -76,7 +105,7 @@ export function rollbackQuietly(database) {
76
105
  function validateSchemaShape(database) {
77
106
  const version = pragmaNumber(database, "user_version");
78
107
  if (version !== PROJECTION_GENERATION_SCHEMA_VERSION) {
79
- throw new ProjectionGenerationStoreError(`incompatible projection generation schema version ${version}; expected ${PROJECTION_GENERATION_SCHEMA_VERSION}`);
108
+ throw new ProjectionGenerationStoreError(`incompatible projection generation schema version ${version}; expected ${PROJECTION_GENERATION_SCHEMA_VERSION}`, { observation: "incompatible" });
80
109
  }
81
110
  const objects = database.prepare(`
82
111
  SELECT name, type
@@ -86,36 +115,36 @@ function validateSchemaShape(database) {
86
115
  `).all();
87
116
  const signature = objects.map((object) => `${object.type}:${object.name}`).join(",");
88
117
  if (signature !== "table:generation_records,table:generation_schema,table:projection_identity,table:projection_tells") {
89
- throw new ProjectionGenerationStoreError(`projection generation schema contains unexpected objects: ${signature || "none"}`);
118
+ throw new ProjectionGenerationStoreError(`projection generation schema contains unexpected objects: ${signature || "none"}`, { observation: "incompatible" });
90
119
  }
91
120
  const metadata = database.prepare("SELECT singleton, version FROM generation_schema").all();
92
121
  if (metadata.length !== 1 || Number(metadata[0].singleton) !== 1 || Number(metadata[0].version) !== PROJECTION_GENERATION_SCHEMA_VERSION) {
93
- throw new ProjectionGenerationStoreError("projection generation schema metadata is malformed");
122
+ throw new ProjectionGenerationStoreError("projection generation schema metadata is malformed", { observation: "incompatible" });
94
123
  }
95
124
  const recordColumns = database.prepare("PRAGMA table_info(generation_records)").all();
96
125
  const columnSignature = recordColumns
97
126
  .map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
98
127
  .join(",");
99
128
  if (columnSignature !== "seq:INTEGER:0:1,kind:TEXT:1:0,execution_id:TEXT:1:0,facts_json:TEXT:1:0") {
100
- throw new ProjectionGenerationStoreError("projection generation record schema is malformed");
129
+ throw new ProjectionGenerationStoreError("projection generation record schema is malformed", { observation: "incompatible" });
101
130
  }
102
131
  const identityColumns = database.prepare("PRAGMA table_info(projection_identity)").all();
103
132
  const identitySignature = identityColumns
104
133
  .map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
105
134
  .join(",");
106
135
  if (identitySignature !== "singleton:INTEGER:0:1,version:INTEGER:1:0,pid:INTEGER:1:0,akuma:TEXT:1:0,provider:TEXT:1:0,authority_json:TEXT:1:0,binding_json:TEXT:1:0,minted_at:TEXT:1:0,workspace_root:TEXT:1:0,effective_policy_json:TEXT:0:0") {
107
- throw new ProjectionGenerationStoreError("projection identity schema is malformed");
136
+ throw new ProjectionGenerationStoreError("projection identity schema is malformed", { observation: "incompatible" });
108
137
  }
109
138
  const tellColumns = database.prepare("PRAGMA table_info(projection_tells)").all();
110
139
  const tellSignature = tellColumns
111
140
  .map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
112
141
  .join(",");
113
142
  if (tellSignature !== "tell_id:TEXT:1:1,text:TEXT:1:0,created_at:TEXT:1:0,effort:TEXT:0:0,state:TEXT:1:0,execution_id:TEXT:0:0,fence_json:TEXT:0:0,replayed_from_json:TEXT:0:0,delivered_at:TEXT:0:0") {
114
- throw new ProjectionGenerationStoreError("projection tell schema is malformed");
143
+ throw new ProjectionGenerationStoreError("projection tell schema is malformed", { observation: "incompatible" });
115
144
  }
116
145
  const quickCheck = database.prepare("PRAGMA quick_check").get();
117
146
  if (!quickCheck || Object.values(quickCheck)[0] !== "ok") {
118
- throw new ProjectionGenerationStoreError("projection generation database failed quick_check");
147
+ throw new ProjectionGenerationStoreError("projection generation database failed quick_check", { observation: "incompatible" });
119
148
  }
120
149
  }
121
150
  function initializeOrValidateSchema(database) {
@@ -138,7 +167,7 @@ function initializeOrValidateSchema(database) {
138
167
  database.exec(`PRAGMA user_version = ${PROJECTION_GENERATION_SCHEMA_VERSION}`);
139
168
  }
140
169
  else if (version !== PROJECTION_GENERATION_SCHEMA_VERSION) {
141
- throw new ProjectionGenerationStoreError(`incompatible projection generation schema version ${version}; expected ${PROJECTION_GENERATION_SCHEMA_VERSION}`);
170
+ throw new ProjectionGenerationStoreError(`incompatible projection generation schema version ${version}; expected ${PROJECTION_GENERATION_SCHEMA_VERSION}`, { observation: "incompatible" });
142
171
  }
143
172
  validateSchemaShape(database);
144
173
  database.exec("COMMIT");
@@ -195,13 +224,11 @@ export function openReadOnlyDatabase(projectionDirectory) {
195
224
  const databasePath = path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
196
225
  try {
197
226
  if (!fs.lstatSync(databasePath).isFile()) {
198
- throw new ProjectionGenerationStoreError("projection generation store must be a regular file");
227
+ throw new ProjectionGenerationStoreError("projection generation store must be a regular file", { observation: "incompatible" });
199
228
  }
200
229
  }
201
230
  catch (error) {
202
- if (error instanceof ProjectionGenerationStoreError)
203
- throw error;
204
- throw new ProjectionGenerationStoreError(`projection generation store does not exist at ${databasePath}`, { cause: error });
231
+ throw projectionGenerationStoreInspectionError(databasePath, error);
205
232
  }
206
233
  let database;
207
234
  try {
@@ -220,9 +247,7 @@ export function openReadOnlyDatabase(projectionDirectory) {
220
247
  database?.close();
221
248
  }
222
249
  catch { /* preserve the read/open failure */ }
223
- if (error instanceof ProjectionGenerationStoreError)
224
- throw error;
225
- throw new ProjectionGenerationStoreError(`cannot read projection generation store at ${databasePath}`, { cause: error });
250
+ throw projectionGenerationStoreReadError(databasePath, error);
226
251
  }
227
252
  }
228
253
  export function assertDatabaseOpen(database) {
@@ -13,11 +13,18 @@ const OUTCOME_ERROR_KEYS = new Set([
13
13
  ]);
14
14
  export class ProjectionGenerationStoreError extends Error {
15
15
  code = "PROJECTION_GENERATION_STORE_ERROR";
16
+ /** Historical schemas/outcomes are not a current lifecycle observation. */
17
+ observation;
16
18
  constructor(message, options) {
17
19
  super(message, options);
18
20
  this.name = "ProjectionGenerationStoreError";
21
+ this.observation = options?.observation ?? "unknown";
19
22
  }
20
23
  }
24
+ export function isIncompatibleProjectionGenerationStoreError(error) {
25
+ return error instanceof ProjectionGenerationStoreError
26
+ && error.observation === "incompatible";
27
+ }
21
28
  export function isPlainObject(value) {
22
29
  if (value === null || typeof value !== "object" || Array.isArray(value))
23
30
  return false;
@@ -131,11 +138,14 @@ export function validateVerdictFacts(value, location) {
131
138
  if (typeof facts.state !== "string" || !VERDICTS.has(facts.state)) {
132
139
  throw new ProjectionGenerationStoreError(`${location}.state is not in the closed verdict vocabulary`);
133
140
  }
141
+ if (facts.terminalAt !== undefined) {
142
+ facts.terminalAt = assertIsoTimestamp(facts.terminalAt, `${location}.terminalAt`);
143
+ }
134
144
  if (facts.state === "failed") {
135
145
  const error = isPlainObject(facts.error) ? facts.error : undefined;
136
146
  const version = error?.version;
137
147
  if (!error || version !== OUTCOME_ERROR_VERSION) {
138
- throw new ProjectionGenerationStoreError(`${location}.error has incompatible version ${version === undefined ? "missing" : String(version)}; expected ${OUTCOME_ERROR_VERSION}`);
148
+ throw new ProjectionGenerationStoreError(`${location}.error has incompatible version ${version === undefined ? "missing" : String(version)}; expected ${OUTCOME_ERROR_VERSION}`, { observation: "incompatible" });
139
149
  }
140
150
  if (Object.keys(error).some((key) => !OUTCOME_ERROR_KEYS.has(key))) {
141
151
  throw new ProjectionGenerationStoreError(`${location}.error is not the current closed OutcomeError shape`);
@@ -181,7 +191,7 @@ export function parseStoredRow(row) {
181
191
  decoded = JSON.parse(row.facts_json);
182
192
  }
183
193
  catch (error) {
184
- throw new ProjectionGenerationStoreError(`generation row ${seq} contains malformed facts JSON`, { cause: error });
194
+ throw new ProjectionGenerationStoreError(`generation row ${seq} contains malformed facts JSON`, { cause: error, observation: "incompatible" });
185
195
  }
186
196
  const location = `generation row ${seq} facts`;
187
197
  const facts = row.kind === "launch"
@@ -240,3 +250,13 @@ export function validateHistory(records) {
240
250
  }
241
251
  return current;
242
252
  }
253
+ export function extractTerminalTimestamp(facts) {
254
+ if (!facts)
255
+ return undefined;
256
+ for (const key of ["completedAt", "failedAt", "dismissedAt", "killedAt", "terminalAt"]) {
257
+ const value = facts[key];
258
+ if (typeof value === "string" && Number.isFinite(Date.parse(value)))
259
+ return value;
260
+ }
261
+ return undefined;
262
+ }
@@ -2,8 +2,8 @@ import { initializeConfig } from "../../../config/env.js";
2
2
  import { buildCallTerms } from "../../../agents/call-terms.js";
3
3
  import { parseResolvedAkumaLaunchSnapshot } from "../../../agents/launch-snapshot/model.js";
4
4
  import { revive, call, } from "../../../agents/harness/index.js";
5
- import { demoteSubmittedTellsForReplay, readProjectionEvents, } from "../projection-core.js";
6
- import { createProjectionExecutionObserver, } from "../projection-execution-observer.js";
5
+ import { claimFencedTells, demoteSubmittedTellsForReplay, readTellOriginal, readProjectionEvents, } from "../projection-core.js";
6
+ import { createProjectionExecutionObserver, renderProjectionTellFrame, } from "../projection-execution-observer.js";
7
7
  import { readProjectionIdentity } from "../projection-identity.js";
8
8
  import { selectValidStoredAgentEvents } from "../../stored-agent-event.js";
9
9
  import { persistResponseHistory } from "../../transcripts.js";
@@ -58,6 +58,7 @@ function parseLaunchInputs(launch) {
58
58
  ...(context ? { context } : {}),
59
59
  launchSnapshot,
60
60
  idleTimeoutMs: requirePositiveSafeInteger(raw.idleTimeoutMs, "idleTimeoutMs"),
61
+ executionTimeoutMs: requirePositiveSafeInteger(raw.executionTimeoutMs, "executionTimeoutMs"),
61
62
  persistProjectionResponse: raw.persistProjectionResponse,
62
63
  ...(raw.session ? { session: raw.session } : {}),
63
64
  ...(resumePath ? { resumePath } : {}),
@@ -140,13 +141,25 @@ export function executeProjectionGeneration(projectionDirectory, launch, provide
140
141
  initializeConfig(process.env, inputs.cwd);
141
142
  const executionId = launch.executionId;
142
143
  demoteSubmittedTellsForReplay(projectionDirectory, executionId);
143
- const terms = attachProviderLoaderPorts(buildCallTerms(inputs.launchSnapshot, inputs.prompt, inputs.cwd, {
144
+ const initialTellIds = claimFencedTells(projectionDirectory, launch.facts.tellFence ?? [], executionId);
145
+ const tellPrompt = initialTellIds.map((tellId) => {
146
+ const tell = readTellOriginal(projectionDirectory, "inflight", tellId);
147
+ return renderProjectionTellFrame(tellId, tell.text);
148
+ }).join("\n\n");
149
+ const prompt = tellPrompt === ""
150
+ ? inputs.prompt
151
+ : inputs.session
152
+ ? tellPrompt
153
+ : `${inputs.prompt}\n\n${tellPrompt}`;
154
+ const terms = attachProviderLoaderPorts(buildCallTerms(inputs.launchSnapshot, prompt, inputs.cwd, {
144
155
  idleTimeoutMs: inputs.idleTimeoutMs,
156
+ executionTimeoutMs: inputs.executionTimeoutMs,
145
157
  initialTurn: maximumValidEnvelopeTurn(projectionDirectory),
146
158
  }), providerLoaderPorts);
147
159
  const observerController = createProjectionExecutionObserver({
148
160
  projectionDirectory,
149
161
  executionId,
162
+ initialTellIds,
150
163
  });
151
164
  let projection;
152
165
  try {
@@ -159,7 +172,7 @@ export function executeProjectionGeneration(projectionDirectory, launch, provide
159
172
  : call(terms, { observer: observerController.observer });
160
173
  }
161
174
  catch (error) {
162
- observerController.close();
175
+ void observerController.close();
163
176
  throw error;
164
177
  }
165
178
  const outcome = projection.outcome.then(async (result) => {
@@ -174,10 +187,10 @@ export function executeProjectionGeneration(projectionDirectory, launch, provide
174
187
  };
175
188
  }
176
189
  finally {
177
- observerController.close();
190
+ await observerController.close();
178
191
  }
179
- }, (error) => {
180
- observerController.close();
192
+ }, async (error) => {
193
+ await observerController.close();
181
194
  throw error;
182
195
  });
183
196
  return {
@@ -6,13 +6,13 @@ export const PROJECTION_ADOPTION_POLL_MS = 20;
6
6
  function errorDetail(error) {
7
7
  return error instanceof Error ? error.message : String(error);
8
8
  }
9
- function recordLaunchFailure(projectionDirectory, executionId, detail, stage) {
9
+ function recordLaunchFailure(projectionDirectory, executionId, detail, stage, terminalAt) {
10
10
  const store = openProjectionGenerationStore(projectionDirectory);
11
11
  try {
12
12
  return store.verdictIfOpen({
13
13
  executionId,
14
14
  verdict: "launch-failed",
15
- facts: { stage, detail },
15
+ facts: { stage, detail, ...(terminalAt ? { terminalAt } : {}) },
16
16
  }).status === "committed";
17
17
  }
18
18
  finally {
@@ -29,7 +29,7 @@ export async function startProjectionGeneration(projectionDirectory, executionId
29
29
  }
30
30
  catch (error) {
31
31
  const detail = `runner spawn failed: ${errorDetail(error)}`;
32
- recordLaunchFailure(projectionDirectory, executionId, detail, "spawn");
32
+ recordLaunchFailure(projectionDirectory, executionId, detail, "spawn", new Date(now()).toISOString());
33
33
  return { status: "failed", cause: "spawn", detail };
34
34
  }
35
35
  const deadline = now() + PROJECTION_ADOPTION_TIMEOUT_MS;
@@ -75,7 +75,7 @@ export async function startProjectionGeneration(projectionDirectory, executionId
75
75
  store.close();
76
76
  }
77
77
  const detail = "runner did not commit adoption before startup timeout";
78
- if (!recordLaunchFailure(projectionDirectory, executionId, detail, "adoption")) {
78
+ if (!recordLaunchFailure(projectionDirectory, executionId, detail, "adoption", new Date(now()).toISOString())) {
79
79
  const raced = openProjectionGenerationStore(projectionDirectory);
80
80
  try {
81
81
  const current = raced.readCurrentGeneration();
@@ -286,6 +286,7 @@ export async function runProjectionGeneration(input) {
286
286
  facts: {
287
287
  stage: "spawn",
288
288
  detail: `successor runner spawn failed: ${errorDetail(error)}`,
289
+ terminalAt: new Date(now()).toISOString(),
289
290
  },
290
291
  });
291
292
  }
@@ -2,7 +2,7 @@ import { PROJECTION_GENERATION_SCHEMA_VERSION, PROJECTION_GENERATION_STORE_FILE,
2
2
  import { initializeIdentity as initializeProjectionIdentity, readIdentity as readProjectionIdentity, } from "./identity.js";
3
3
  import * as transitions from "./transitions.js";
4
4
  export { PROJECTION_GENERATION_SCHEMA_VERSION, PROJECTION_GENERATION_STORE_FILE, };
5
- export { GENERATION_VERDICT_STATES, ProjectionGenerationStoreError, } from "./model.js";
5
+ export { GENERATION_VERDICT_STATES, isIncompatibleProjectionGenerationStoreError, ProjectionGenerationStoreError, } from "./model.js";
6
6
  export class ProjectionGenerationStore {
7
7
  databasePath;
8
8
  readOnly;
@@ -149,7 +149,7 @@ export function verdictIfOpen(database, readOnly, input) {
149
149
  if (input.verdict === "launch-failed" && current.adoption) {
150
150
  return rejected("already-adopted", current);
151
151
  }
152
- const facts = validateVerdictFacts({ ...(input.facts ?? {}), state: input.verdict }, "verdict facts");
152
+ const facts = validateVerdictFacts({ ...(input.facts ?? {}), state: input.verdict, ...(input.terminalAt ? { terminalAt: assertIsoTimestamp(input.terminalAt, "terminalAt") } : {}) }, "verdict facts");
153
153
  return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
154
154
  });
155
155
  }
@@ -160,6 +160,9 @@ export function supersedeStartupTimeout(database, readOnly, input) {
160
160
  const startupTimeoutMs = assertPositiveSafeInteger(input.startupTimeoutMs, "startupTimeoutMs");
161
161
  const diagnostic = cloneFacts(input.diagnostic, "startup-timeout diagnostic");
162
162
  const successorFacts = validateLaunchFacts(input.successorFacts, "successor launch facts");
163
+ const terminalAt = input.terminalAt === undefined
164
+ ? undefined
165
+ : assertIsoTimestamp(input.terminalAt, "terminalAt");
163
166
  return writeTransition(database, readOnly, () => {
164
167
  const history = readValidatedHistory(database);
165
168
  const current = history.current;
@@ -180,7 +183,12 @@ export function supersedeStartupTimeout(database, readOnly, input) {
180
183
  if (observedAtMs - launchedAtMs < startupTimeoutMs) {
181
184
  return rejected("startup-timeout-not-elapsed", current);
182
185
  }
183
- const verdictFacts = validateVerdictFacts({ ...diagnostic, state: "launch-failed" }, "startup-timeout verdict facts");
186
+ const verdictFactsBase = {
187
+ ...diagnostic,
188
+ state: "launch-failed",
189
+ ...(terminalAt === undefined ? {} : { terminalAt }),
190
+ };
191
+ const verdictFacts = validateVerdictFacts(verdictFactsBase, "startup-timeout verdict facts");
184
192
  const firstSeq = history.records.length + 1;
185
193
  return committed([
186
194
  appendRecord(database, "verdict", executionId, verdictFacts, firstSeq),
@@ -196,6 +204,9 @@ export function replaceDeadRunner(database, readOnly, input) {
196
204
  throw new ProjectionGenerationStoreError("released runner-lock evidence must not be empty");
197
205
  }
198
206
  const successorFacts = validateLaunchFacts(input.successorFacts, "dead-runner successor launch facts");
207
+ const terminalAt = input.terminalAt === undefined
208
+ ? undefined
209
+ : assertIsoTimestamp(input.terminalAt, "terminalAt");
199
210
  return writeTransition(database, readOnly, () => {
200
211
  const history = readValidatedHistory(database);
201
212
  const current = history.current;
@@ -212,7 +223,12 @@ export function replaceDeadRunner(database, readOnly, input) {
212
223
  if (history.records.some((record) => record.kind === "launch" && record.executionId === successorExecutionId)) {
213
224
  return rejected("execution-id-already-used", current);
214
225
  }
215
- const verdictFacts = validateVerdictFacts({ state: "lost-runner", releasedLockEvidence }, "dead-runner verdict facts");
226
+ const verdictFactsBase = {
227
+ state: "lost-runner",
228
+ releasedLockEvidence,
229
+ ...(terminalAt === undefined ? {} : { terminalAt }),
230
+ };
231
+ const verdictFacts = validateVerdictFacts(verdictFactsBase, "dead-runner verdict facts");
216
232
  const firstSeq = history.records.length + 1;
217
233
  return committed([
218
234
  appendRecord(database, "verdict", executionId, verdictFacts, firstSeq),
@@ -10,13 +10,13 @@ export { resolveProjectionCoordinate } from "./projection-coordinate.js";
10
10
  export { appendGenerationEvent, executionEventsPath, isProjectionId, isPublishedProjection, isoFromNowMs, projectionDir, projectionRoot, readProjectionEvents, } from "./projection-core.js";
11
11
  export { readProjectionIdentity } from "./projection-identity.js";
12
12
  export { killProjectionGeneration } from "./projection-kill.js";
13
- export { observeProjectionLife } from "./projection-life-observer.js";
13
+ export { observeProjectionLife, observeProjectionLifeSnapshot, } from "./projection-life-observer.js";
14
14
  export { mintProjection, mintRepositoryProjection, ProjectionMintCollisionError, } from "./projection-mint.js";
15
15
  export { acquireProjectionRunnerLock, initializeProjectionRunnerLockDatabase, isProjectionRunnerLockPlatformSupported, observeProjectionRunnerLock, projectionRunnerLockPath, } from "./projection-runner-lock.js";
16
16
  export { hasStrandableProjectionGeneration, prioritizeProjectionStatusRows, readProjectionStatusBoard, } from "./projection-status.js";
17
17
  export { projectTerminalFailure, } from "./projection-terminal-failure.js";
18
18
  export { snapshotCommissionProjectionIds, waitForProjection, waitForProjectionSet, } from "./projection-wait.js";
19
- export { assertTellEffortSupported, tellProjection, } from "./projection-wake.js";
19
+ export { assertTellEffortSupported, assertProjectionTellable, tellProjection, } from "./projection-wake.js";
20
20
  export { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, ProjectionGenerationStore, } from "./generation/store.js";
21
21
  export { createProjectionExecutionId } from "./generation/projection-generation-identity.js";
22
22
  export { PROJECTION_ADOPTION_TIMEOUT_MS, startProjectionGeneration, } from "./generation/projection-generation-launcher.js";