@astrosheep/keiyaku 2.9.11 → 2.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -72
- package/build/.tsbuildinfo +1 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +15 -2
- package/build/cli/commands/contract/bind/meta.js +4 -4
- package/build/cli/commands/metadata.js +3 -2
- package/build/cli/commands/projection/kill/handler.js +4 -5
- package/build/cli/commands/shared.js +1 -1
- package/build/cli/commands/task/catalog.js +2 -0
- package/build/cli/commands/task/compose/handler.js +18 -0
- package/build/cli/commands/task/compose/meta.js +10 -0
- package/build/cli/flags.js +5 -1
- package/build/cli/index.js +11 -6
- package/build/cli/parse-flags.js +21 -21
- package/build/cli/parse-metadata.js +5 -2
- package/build/cli/parse-selectors.js +4 -4
- package/build/cli/parse.js +23 -23
- package/build/cli/projection-address.js +7 -7
- package/build/cli/render/task.js +27 -0
- package/build/cli/types.js +1 -1
- package/build/core/addressing.js +3 -3
- package/build/core/call/context.js +3 -3
- package/build/core/call/prompt.js +1 -1
- package/build/core/contract-view.js +1 -1
- package/build/core/draft.js +51 -22
- package/build/core/projection/generation/model.js +1 -1
- package/build/core/projection/generation/projection-generation-runner.js +13 -3
- package/build/core/projection/generation/transitions.js +10 -0
- package/build/core/projection/projection-history.js +1 -1
- package/build/core/projection/projection-kill.js +5 -4
- package/build/core/projection/projection-runner-lock.js +58 -229
- package/build/core/projection/projection-wait.js +2 -2
- package/build/core/projection/projection-wake.js +2 -0
- package/build/core/repository-ledger/claim-evidence.js +1 -1
- package/build/core/repository-ledger/current-state-store.js +455 -337
- package/build/core/repository-ledger/read-model.js +3 -3
- package/build/core/repository-ledger/write-transaction.js +15 -13
- package/build/core/sqlite-process-lifetime-lock.js +194 -0
- package/build/core/task/compose.js +327 -0
- package/build/core/task/index.js +1 -0
- package/build/core/task/settlement-git.js +69 -26
- package/build/core/task/task-git-store.js +1 -0
- package/build/core/task/task-store-repository.js +1 -0
- package/build/flow-error.js +6 -1
- package/build/generated/version.js +2 -2
- package/package.json +2 -2
- package/skills/keiyaku-akuma/SKILL.md +6 -1
- package/skills/keiyaku-task/SKILL.md +23 -1
- package/skills/keiyaku-workflow/SKILL.md +33 -18
|
@@ -56,7 +56,7 @@ export async function resolveProjectionAddress(input) {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
if (isResponseArtifactId(input.address)) {
|
|
59
|
-
throw new AddressResolutionError("
|
|
59
|
+
throw new AddressResolutionError("UNKNOWN_ADDRESS", `artifact ${input.address} is a response artifact; ${input.verb} expects a projection id or alias`, { ...attempt, actual: "artifact" });
|
|
60
60
|
}
|
|
61
61
|
const suffix = input.scope
|
|
62
62
|
? `within ${input.scope.spelling.asTyped}`
|
|
@@ -65,22 +65,22 @@ export async function resolveProjectionAddress(input) {
|
|
|
65
65
|
let alias;
|
|
66
66
|
if (input.address.includes("/")) {
|
|
67
67
|
if (!isProjectionId(input.address))
|
|
68
|
-
throw new AddressResolutionError("
|
|
68
|
+
throw new AddressResolutionError("UNKNOWN_ADDRESS", `projection ${input.address} has no match ${suffix}`, attempt);
|
|
69
69
|
id = input.address;
|
|
70
70
|
}
|
|
71
71
|
else {
|
|
72
72
|
if (!isProjectionAlias(input.address))
|
|
73
|
-
throw new AddressResolutionError("
|
|
73
|
+
throw new AddressResolutionError("UNKNOWN_ADDRESS", `projection alias ${input.address} has no match ${suffix}`, attempt);
|
|
74
74
|
const target = await readProjectionAliasAtRoot(projectionAliasRootAtCoordinate(coordinate), input.address);
|
|
75
75
|
if (!target)
|
|
76
|
-
throw new AddressResolutionError("
|
|
76
|
+
throw new AddressResolutionError("UNKNOWN_ADDRESS", `projection alias ${input.address} has no match ${suffix}`, attempt);
|
|
77
77
|
id = target;
|
|
78
78
|
alias = input.address;
|
|
79
79
|
}
|
|
80
80
|
const directory = projectionDirAtCoordinate(coordinate, id);
|
|
81
81
|
if (!isRealDirectory(directory)) {
|
|
82
82
|
const subject = alias ? `projection alias ${alias} points to missing projection ${id}` : `projection ${id}`;
|
|
83
|
-
throw new AddressResolutionError("
|
|
83
|
+
throw new AddressResolutionError("UNKNOWN_ADDRESS", `${subject} has no match ${suffix}`, attempt);
|
|
84
84
|
}
|
|
85
85
|
// Historical projection storage has no current identity to resolve. Tell
|
|
86
86
|
// still reaches its typed no-mutation refusal instead of exposing storage
|
|
@@ -94,12 +94,12 @@ export async function resolveProjectionAddress(input) {
|
|
|
94
94
|
catch (error) {
|
|
95
95
|
if (error instanceof Error && /ENOENT|no such file/i.test(error.message)) {
|
|
96
96
|
const subject = alias ? `projection alias ${alias} points to missing projection ${id}` : `projection ${id}`;
|
|
97
|
-
throw new AddressResolutionError("
|
|
97
|
+
throw new AddressResolutionError("UNKNOWN_ADDRESS", `${subject} has no match ${suffix}`, attempt);
|
|
98
98
|
}
|
|
99
99
|
throw error;
|
|
100
100
|
}
|
|
101
101
|
if (input.scope && (identity.binding.kind !== "commission" || identity.binding.commissionId !== input.scope.binding.commissionId)) {
|
|
102
|
-
throw new AddressResolutionError("
|
|
102
|
+
throw new AddressResolutionError("UNKNOWN_ADDRESS", `projection ${id} has no match ${suffix}`, attempt);
|
|
103
103
|
}
|
|
104
104
|
return { id, directory, ...(alias ? { alias } : {}) };
|
|
105
105
|
}
|
package/build/cli/render/task.js
CHANGED
|
@@ -32,6 +32,33 @@ export function renderTaskUpdateJson(result) {
|
|
|
32
32
|
...(result.bodyChange === undefined ? {} : { bodyChange: result.bodyChange }),
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
export function renderTaskComposition(plan, check) {
|
|
36
|
+
const lines = [check ? "Task composition check" : "Task composition"];
|
|
37
|
+
for (const allocation of plan.allocations)
|
|
38
|
+
lines.push(`allocate: ${allocation.coordinate} · ${allocation.title}`);
|
|
39
|
+
for (const delta of plan.deltas) {
|
|
40
|
+
const changes = [
|
|
41
|
+
...(delta.parent ? [`parent ${delta.parent.before ?? "(none)"} -> ${delta.parent.after ?? "(none)"}`] : []),
|
|
42
|
+
...(delta.needs ? [`needs ${delta.needs.before.join(",") || "(none)"} -> ${delta.needs.after.join(",") || "(none)"}`] : []),
|
|
43
|
+
...(delta.supersedes ? [`supersedes ${delta.supersedes.before.join(",") || "(none)"} -> ${delta.supersedes.after.join(",") || "(none)"}`] : []),
|
|
44
|
+
...(delta.relatesTo ? [`relates ${delta.relatesTo.before.join(",") || "(none)"} -> ${delta.relatesTo.after.join(",") || "(none)"}`] : []),
|
|
45
|
+
];
|
|
46
|
+
lines.push(`change: ${delta.coordinate} · ${changes.join("; ")}`);
|
|
47
|
+
}
|
|
48
|
+
for (const warning of plan.warnings) {
|
|
49
|
+
const detail = warning.related === undefined ? warning.coordinate : `${warning.coordinate} · ${warning.related}`;
|
|
50
|
+
lines.push(`warning: ${warning.kind} · ${detail}`);
|
|
51
|
+
}
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
54
|
+
export function renderTaskCompositionJson(plan, check) {
|
|
55
|
+
return JSON.stringify({
|
|
56
|
+
mode: check ? "check" : "apply",
|
|
57
|
+
allocations: plan.allocations,
|
|
58
|
+
deltas: plan.deltas,
|
|
59
|
+
warnings: plan.warnings,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
35
62
|
export function renderTaskRow(task, completeId = false) {
|
|
36
63
|
return completeId ? renderTaskMutationRow(task) : renderTaskListRow(task);
|
|
37
64
|
}
|
package/build/cli/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const TASK_SUBCOMMANDS = ["add", "show", "ls", "log", "start", "stop", "hold", "resume", "done", "drop", "update", "doctor"];
|
|
1
|
+
export const TASK_SUBCOMMANDS = ["add", "compose", "show", "ls", "log", "start", "stop", "hold", "resume", "done", "drop", "update", "doctor"];
|
package/build/core/addressing.js
CHANGED
|
@@ -237,7 +237,7 @@ export function resolveExplicitContractAddressFromRead(input) {
|
|
|
237
237
|
const source = input.source ?? (input.raw.startsWith("@") ? "at-address" : "explicit-flag");
|
|
238
238
|
const spelling = { asTyped: commissionSelectorAsTyped(query, source), source };
|
|
239
239
|
if (isResponseArtifactId(query)) {
|
|
240
|
-
throw new AddressResolutionError("
|
|
240
|
+
throw new AddressResolutionError("UNKNOWN_ADDRESS", `artifact ${query} is a response artifact; expected a commission address`, { repo, workdir, expected: "commission", spelling, candidates: [], inference: [], actual: "artifact" });
|
|
241
241
|
}
|
|
242
242
|
const identities = explicitAddressIdentities(input.read, query);
|
|
243
243
|
const attempt = {
|
|
@@ -282,7 +282,7 @@ export function resolveExplicitContractAddressFromRead(input) {
|
|
|
282
282
|
if (unknown.status === "diseased" || unknown.status === "isolated" || unknown.status === "indeterminate") {
|
|
283
283
|
throw contractReadOutcomeError(query, unknown);
|
|
284
284
|
}
|
|
285
|
-
throw new AddressResolutionError("
|
|
285
|
+
throw new AddressResolutionError("UNKNOWN_ADDRESS", `unknown contract address @${query}`, attempt);
|
|
286
286
|
}
|
|
287
287
|
export async function resolveContractAddress(cwd, raw, source = raw?.startsWith("@") ? "at-address" : "explicit-flag", effectiveDirectory = cwd) {
|
|
288
288
|
const workdir = path.resolve(effectiveDirectory);
|
|
@@ -348,7 +348,7 @@ export async function resolveContractAddress(cwd, raw, source = raw?.startsWith(
|
|
|
348
348
|
inference: attempt.inference,
|
|
349
349
|
});
|
|
350
350
|
}
|
|
351
|
-
throw new AddressResolutionError(operable.length === 0 ? "
|
|
351
|
+
throw new AddressResolutionError(operable.length === 0 ? "UNKNOWN_ADDRESS" : "CONTRACT_AMBIGUOUS", operable.length === 0
|
|
352
352
|
? "no active commission found; pass @place, @slug, full @commission-id, or --contract <addr>"
|
|
353
353
|
: "multiple active commissions found; pass @place, @slug, full @commission-id, or --contract <addr>", attempt);
|
|
354
354
|
}
|
|
@@ -24,7 +24,7 @@ export async function readCallKeiyakuContext(executionCwd, input, ports = {}) {
|
|
|
24
24
|
const asTyped = input.contractAddressSource === "at-address"
|
|
25
25
|
? `@${input.contractId}`
|
|
26
26
|
: `--contract ${input.contractId}`;
|
|
27
|
-
throw new AddressResolutionError("
|
|
27
|
+
throw new AddressResolutionError("INVALID_USAGE", `commission address ${asTyped} cannot be combined with --bare`, buildResolutionAttempt({
|
|
28
28
|
repo: { kind: "user" },
|
|
29
29
|
workdir: executionCwd,
|
|
30
30
|
spelling: {
|
|
@@ -37,7 +37,7 @@ export async function readCallKeiyakuContext(executionCwd, input, ports = {}) {
|
|
|
37
37
|
const asTyped = input.contractAddressSource === "at-address"
|
|
38
38
|
? `@${input.contractId}`
|
|
39
39
|
: `--contract ${input.contractId}`;
|
|
40
|
-
throw new AddressResolutionError("
|
|
40
|
+
throw new AddressResolutionError("UNKNOWN_ADDRESS", `${asTyped} is a commission address, but ${ledgerCandidate} has no repository ledger; contract addressing requires a ledger`, buildResolutionAttempt({
|
|
41
41
|
repo: { kind: "user" },
|
|
42
42
|
workdir: executionCwd,
|
|
43
43
|
spelling: {
|
|
@@ -89,7 +89,7 @@ export async function readCallKeiyakuContext(executionCwd, input, ports = {}) {
|
|
|
89
89
|
const asTyped = input.contractAddressSource === "at-address"
|
|
90
90
|
? `@${input.contractId}`
|
|
91
91
|
: `--contract ${input.contractId}`;
|
|
92
|
-
throw new AddressResolutionError("
|
|
92
|
+
throw new AddressResolutionError("INVALID_USAGE", `commission address ${asTyped} cannot be combined with --bare`, buildResolutionAttempt({
|
|
93
93
|
repo: repositoryAddress.repo,
|
|
94
94
|
workdir: repositoryAddress.workdir,
|
|
95
95
|
spelling: {
|
|
@@ -35,7 +35,7 @@ export function buildCallPrompt(input) {
|
|
|
35
35
|
`Contract: ${bind.title}`,
|
|
36
36
|
`Context:\n${bind.context}`,
|
|
37
37
|
`Objective:\n${bind.objective}`,
|
|
38
|
-
`Design:\n${bind.design}`,
|
|
38
|
+
`Design & Approach:\n${bind.design}`,
|
|
39
39
|
`Scope:\n${bind.scope.map((value) => `- ${value}`).join("\n")}`,
|
|
40
40
|
`Criteria:\n${bind.criteria.map((value) => `- ${value}`).join("\n")}`,
|
|
41
41
|
`Verification:\n${bind.verification.map((value) => `- ${value}`).join("\n")}`,
|
package/build/core/draft.js
CHANGED
|
@@ -3,8 +3,15 @@ import { parseToAST } from "./markdown/parser.js";
|
|
|
3
3
|
import { extractListItems, renderSectionContent } from "./markdown/render.js";
|
|
4
4
|
import { normalizeSectionTitle } from "./markdown/titles.js";
|
|
5
5
|
import { markdownSourceSections, renderScopeFragment, validateScopeFragmentLines } from "./scope.js";
|
|
6
|
-
const KNOWN_SECTIONS = new
|
|
7
|
-
|
|
6
|
+
const KNOWN_SECTIONS = new Map([
|
|
7
|
+
["context", "context"],
|
|
8
|
+
["objective", "objective"],
|
|
9
|
+
["design & approach", "design"],
|
|
10
|
+
["scope", "scope"],
|
|
11
|
+
["criteria", "criteria"],
|
|
12
|
+
["verification", "verification"],
|
|
13
|
+
]);
|
|
14
|
+
const RETIRED_OR_MALFORMED_CORE_SECTIONS = new Set(["title", "name", "design", "checks"]);
|
|
8
15
|
const BIND_DRAFT_SECTION_ORDER = ["title", "context", "objective", "design", "scope", "criteria", "verification"];
|
|
9
16
|
function invalidBindDraft(message) {
|
|
10
17
|
throw new FlowError("INVALID_KEIYAKU_DRAFT", `INVALID_BIND_DRAFT: ${message}`);
|
|
@@ -105,14 +112,19 @@ function collectBindDraftNodes(content) {
|
|
|
105
112
|
continue;
|
|
106
113
|
const normalized = normalizeSectionName(node.title);
|
|
107
114
|
if (RETIRED_OR_MALFORMED_CORE_SECTIONS.has(normalized)) {
|
|
108
|
-
const replacement = normalized === "checks"
|
|
115
|
+
const replacement = normalized === "checks"
|
|
116
|
+
? "Criteria and Verification"
|
|
117
|
+
: normalized === "design"
|
|
118
|
+
? "Design & Approach"
|
|
119
|
+
: "the H1 Title";
|
|
109
120
|
invalidBindDraft(`obsolete or malformed core section '${node.title}'; use ${replacement}`);
|
|
110
121
|
}
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
const coreSection = KNOWN_SECTIONS.get(normalized);
|
|
123
|
+
if (coreSection) {
|
|
124
|
+
if (sections.has(coreSection)) {
|
|
113
125
|
invalidBindDraft(`duplicate section '${node.title}' in bind draft`);
|
|
114
126
|
}
|
|
115
|
-
sections.set(
|
|
127
|
+
sections.set(coreSection, node);
|
|
116
128
|
continue;
|
|
117
129
|
}
|
|
118
130
|
extensions.push({
|
|
@@ -155,7 +167,9 @@ export function parsePartialBindDraft(content) {
|
|
|
155
167
|
if (!section)
|
|
156
168
|
continue;
|
|
157
169
|
const text = renderSectionContent(section).trim();
|
|
158
|
-
const heading =
|
|
170
|
+
const heading = sectionName === "design"
|
|
171
|
+
? "Design & Approach"
|
|
172
|
+
: `${sectionName[0].toUpperCase()}${sectionName.slice(1)}`;
|
|
159
173
|
if (!text)
|
|
160
174
|
invalidBindDraft(`${heading} section cannot be empty`);
|
|
161
175
|
partial[sectionName] = text;
|
|
@@ -229,7 +243,7 @@ export function renderMissingBindSectionsTemplate(missing) {
|
|
|
229
243
|
continue;
|
|
230
244
|
}
|
|
231
245
|
if (section === "design") {
|
|
232
|
-
lines.push("## Design", "<settled owner, boundaries, flow, state, and failure behavior>");
|
|
246
|
+
lines.push("## Design & Approach", "<settled owner, boundaries, flow, state, and failure behavior>");
|
|
233
247
|
continue;
|
|
234
248
|
}
|
|
235
249
|
if (section === "scope") {
|
|
@@ -245,13 +259,23 @@ export function renderMissingBindSectionsTemplate(missing) {
|
|
|
245
259
|
return lines.join("\n");
|
|
246
260
|
}
|
|
247
261
|
export function describeMissingBindSections(missing) {
|
|
248
|
-
|
|
262
|
+
const labels = {
|
|
263
|
+
title: "Title",
|
|
264
|
+
context: "Context",
|
|
265
|
+
objective: "Objective",
|
|
266
|
+
design: "Design & Approach",
|
|
267
|
+
scope: "Scope",
|
|
268
|
+
criteria: "Criteria",
|
|
269
|
+
verification: "Verification",
|
|
270
|
+
};
|
|
271
|
+
const named = missing.map((section) => labels[section]);
|
|
272
|
+
if (named.length === 0)
|
|
249
273
|
return "";
|
|
250
|
-
if (
|
|
251
|
-
return
|
|
252
|
-
if (
|
|
253
|
-
return `${
|
|
254
|
-
return `${
|
|
274
|
+
if (named.length === 1)
|
|
275
|
+
return named[0];
|
|
276
|
+
if (named.length === 2)
|
|
277
|
+
return `${named[0]} or ${named[1]}`;
|
|
278
|
+
return `${named.slice(0, -1).join(", ")}, or ${named[named.length - 1]}`;
|
|
255
279
|
}
|
|
256
280
|
export function materializeBindDraft(partial) {
|
|
257
281
|
const missing = missingBindDraftSections(partial);
|
|
@@ -272,7 +296,7 @@ export function materializeBindDraft(partial) {
|
|
|
272
296
|
export function parseBindDraft(content) {
|
|
273
297
|
const { title, sections, extensions } = collectBindDraftNodes(content);
|
|
274
298
|
if (!title) {
|
|
275
|
-
invalidBindDraft("missing H1 Title in bind draft; expected '# <title>' followed by Context, Objective, Design, Scope, and Criteria; Verification is optional");
|
|
299
|
+
invalidBindDraft("missing H1 Title in bind draft; expected '# <title>' followed by Context, Objective, Design & Approach, Scope, and Criteria; Verification is optional");
|
|
276
300
|
}
|
|
277
301
|
const context = sections.get("context");
|
|
278
302
|
const objective = sections.get("objective");
|
|
@@ -285,7 +309,7 @@ export function parseBindDraft(content) {
|
|
|
285
309
|
if (!objective)
|
|
286
310
|
invalidBindDraft("missing Objective section in bind draft");
|
|
287
311
|
if (!design)
|
|
288
|
-
invalidBindDraft("missing Design section in bind draft");
|
|
312
|
+
invalidBindDraft("missing Design & Approach section in bind draft");
|
|
289
313
|
if (!scope)
|
|
290
314
|
invalidBindDraft("missing Scope section in bind draft");
|
|
291
315
|
const parsedScope = parseScopeSection(content);
|
|
@@ -299,7 +323,7 @@ export function parseBindDraft(content) {
|
|
|
299
323
|
if (!objectiveText)
|
|
300
324
|
invalidBindDraft("Objective section cannot be empty");
|
|
301
325
|
if (!designText)
|
|
302
|
-
invalidBindDraft("Design section cannot be empty");
|
|
326
|
+
invalidBindDraft("Design & Approach section cannot be empty");
|
|
303
327
|
return {
|
|
304
328
|
title,
|
|
305
329
|
context: contextText,
|
|
@@ -316,7 +340,7 @@ export function renderBindDraft(draft) {
|
|
|
316
340
|
`# ${draft.title}`,
|
|
317
341
|
`## Context\n${draft.context}`,
|
|
318
342
|
`## Objective\n${draft.objective}`,
|
|
319
|
-
`## Design\n${draft.design}`,
|
|
343
|
+
`## Design & Approach\n${draft.design}`,
|
|
320
344
|
`## Scope\n${renderScopeFragment(draft.scope)}`,
|
|
321
345
|
`## Criteria\n${draft.criteria.map((criterion) => `- ${criterion}`).join("\n")}`,
|
|
322
346
|
];
|
|
@@ -340,7 +364,6 @@ export function compileBindDraftFlags(input) {
|
|
|
340
364
|
["--context", input.context],
|
|
341
365
|
["--objective", input.objective],
|
|
342
366
|
["--design", input.design],
|
|
343
|
-
["--scope", input.scope],
|
|
344
367
|
["--criteria", input.criteria],
|
|
345
368
|
["--verification", input.verification],
|
|
346
369
|
]) {
|
|
@@ -348,6 +371,12 @@ export function compileBindDraftFlags(input) {
|
|
|
348
371
|
throw new FlowError("EMPTY_PARAM", `${flag} must be a single line`);
|
|
349
372
|
}
|
|
350
373
|
}
|
|
374
|
+
const scope = typeof input.scope === "string" ? [input.scope] : input.scope;
|
|
375
|
+
for (const pattern of scope ?? []) {
|
|
376
|
+
if (pattern.includes("\n") || pattern.includes("\r")) {
|
|
377
|
+
throw new FlowError("EMPTY_PARAM", "--scope must be a single line");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
351
380
|
const sections = [];
|
|
352
381
|
if (input.title !== undefined)
|
|
353
382
|
sections.push(`# ${input.title}`);
|
|
@@ -356,9 +385,9 @@ export function compileBindDraftFlags(input) {
|
|
|
356
385
|
if (input.objective !== undefined)
|
|
357
386
|
sections.push(`## Objective\n${input.objective}`);
|
|
358
387
|
if (input.design !== undefined)
|
|
359
|
-
sections.push(`## Design\n${input.design}`);
|
|
360
|
-
if (
|
|
361
|
-
sections.push(`## Scope\n~~~\n${
|
|
388
|
+
sections.push(`## Design & Approach\n${input.design}`);
|
|
389
|
+
if (scope !== undefined)
|
|
390
|
+
sections.push(`## Scope\n~~~\n${scope.join("\n")}\n~~~`);
|
|
362
391
|
if (input.criteria !== undefined)
|
|
363
392
|
sections.push(`## Criteria\n- ${input.criteria}`);
|
|
364
393
|
if (input.verification !== undefined)
|
|
@@ -247,7 +247,7 @@ export function validateHistory(records) {
|
|
|
247
247
|
export function extractTerminalTimestamp(facts) {
|
|
248
248
|
if (!facts)
|
|
249
249
|
return undefined;
|
|
250
|
-
for (const key of ["completedAt", "failedAt", "dismissedAt", "killedAt"
|
|
250
|
+
for (const key of ["terminalAt", "completedAt", "failedAt", "dismissedAt", "killedAt"]) {
|
|
251
251
|
const value = facts[key];
|
|
252
252
|
if (typeof value === "string" && Number.isFinite(Date.parse(value)))
|
|
253
253
|
return value;
|
|
@@ -112,7 +112,10 @@ export async function runProjectionGeneration(input) {
|
|
|
112
112
|
// Adoption is the provider-admission boundary. A kill that committed first
|
|
113
113
|
// settles without constructing a provider execution.
|
|
114
114
|
if (current.killIntent) {
|
|
115
|
-
const killed = store.runnerKilledIfRequested({
|
|
115
|
+
const killed = store.runnerKilledIfRequested({
|
|
116
|
+
executionId: input.executionId,
|
|
117
|
+
terminalAt: new Date(now()).toISOString(),
|
|
118
|
+
});
|
|
116
119
|
if (killed.status !== "committed")
|
|
117
120
|
return { status: "rejected" };
|
|
118
121
|
settled = true;
|
|
@@ -126,7 +129,10 @@ export async function runProjectionGeneration(input) {
|
|
|
126
129
|
}
|
|
127
130
|
if (current.killIntent) {
|
|
128
131
|
await prepared.dispose();
|
|
129
|
-
const killed = store.runnerKilledIfRequested({
|
|
132
|
+
const killed = store.runnerKilledIfRequested({
|
|
133
|
+
executionId: input.executionId,
|
|
134
|
+
terminalAt: new Date(now()).toISOString(),
|
|
135
|
+
});
|
|
130
136
|
if (killed.status !== "committed")
|
|
131
137
|
return { status: "rejected" };
|
|
132
138
|
settled = true;
|
|
@@ -278,6 +284,7 @@ export async function runProjectionGeneration(input) {
|
|
|
278
284
|
const killed = store.runnerKilledIfRequested({
|
|
279
285
|
executionId: input.executionId,
|
|
280
286
|
...(facts && Object.keys(facts).length > 0 ? { facts } : {}),
|
|
287
|
+
terminalAt: new Date(now()).toISOString(),
|
|
281
288
|
});
|
|
282
289
|
if (killed.status !== "committed")
|
|
283
290
|
return { status: "rejected" };
|
|
@@ -290,7 +297,10 @@ export async function runProjectionGeneration(input) {
|
|
|
290
297
|
await handle.abort("force");
|
|
291
298
|
if (sawInterruptIntent)
|
|
292
299
|
return settleInterrupted();
|
|
293
|
-
const killed = store.runnerKilledIfRequested({
|
|
300
|
+
const killed = store.runnerKilledIfRequested({
|
|
301
|
+
executionId: input.executionId,
|
|
302
|
+
terminalAt: new Date(now()).toISOString(),
|
|
303
|
+
});
|
|
294
304
|
if (killed.status !== "committed")
|
|
295
305
|
return { status: "rejected" };
|
|
296
306
|
settled = true;
|
|
@@ -111,6 +111,7 @@ export function requestInterruptIfOpen(database, readOnly, input) {
|
|
|
111
111
|
}
|
|
112
112
|
export function runnerKilledIfRequested(database, readOnly, input) {
|
|
113
113
|
const executionId = assertExecutionId(input.executionId);
|
|
114
|
+
const terminalAt = assertIsoTimestamp(input.terminalAt, "terminalAt");
|
|
114
115
|
const callerFacts = cloneFacts(input.facts ?? {}, "killed verdict facts");
|
|
115
116
|
if ("state" in callerFacts) {
|
|
116
117
|
throw new ProjectionGenerationStoreError("killed verdict facts.state is reserved");
|
|
@@ -121,6 +122,9 @@ export function runnerKilledIfRequested(database, readOnly, input) {
|
|
|
121
122
|
if ("killedAt" in callerFacts) {
|
|
122
123
|
throw new ProjectionGenerationStoreError("killed verdict facts.killedAt is reserved");
|
|
123
124
|
}
|
|
125
|
+
if ("terminalAt" in callerFacts) {
|
|
126
|
+
throw new ProjectionGenerationStoreError("killed verdict facts.terminalAt is reserved");
|
|
127
|
+
}
|
|
124
128
|
return writeTransition(database, readOnly, () => {
|
|
125
129
|
const history = readValidatedHistory(database);
|
|
126
130
|
const current = history.current;
|
|
@@ -137,6 +141,7 @@ export function runnerKilledIfRequested(database, readOnly, input) {
|
|
|
137
141
|
state: "killed",
|
|
138
142
|
killIntentSeq: current.killIntent.seq,
|
|
139
143
|
killedAt: current.killIntent.facts.killedAt,
|
|
144
|
+
terminalAt,
|
|
140
145
|
}, "killed verdict facts");
|
|
141
146
|
return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
|
|
142
147
|
});
|
|
@@ -221,6 +226,7 @@ export function runnerVerdictIfOpen(database, readOnly, input) {
|
|
|
221
226
|
}
|
|
222
227
|
export function settleKillIfRunnerDead(database, readOnly, input) {
|
|
223
228
|
const executionId = assertExecutionId(input.executionId);
|
|
229
|
+
const terminalAt = assertIsoTimestamp(input.terminalAt, "terminalAt");
|
|
224
230
|
const releasedLockEvidence = cloneFacts(input.releasedLockEvidence, "released runner-lock evidence");
|
|
225
231
|
if (Object.keys(releasedLockEvidence).length === 0) {
|
|
226
232
|
throw new ProjectionGenerationStoreError("released runner-lock evidence must not be empty");
|
|
@@ -235,6 +241,9 @@ export function settleKillIfRunnerDead(database, readOnly, input) {
|
|
|
235
241
|
if ("killedAt" in callerFacts) {
|
|
236
242
|
throw new ProjectionGenerationStoreError("killed verdict facts.killedAt is reserved");
|
|
237
243
|
}
|
|
244
|
+
if ("terminalAt" in callerFacts) {
|
|
245
|
+
throw new ProjectionGenerationStoreError("killed verdict facts.terminalAt is reserved");
|
|
246
|
+
}
|
|
238
247
|
return writeTransition(database, readOnly, () => {
|
|
239
248
|
const history = readValidatedHistory(database);
|
|
240
249
|
const current = history.current;
|
|
@@ -251,6 +260,7 @@ export function settleKillIfRunnerDead(database, readOnly, input) {
|
|
|
251
260
|
state: "killed",
|
|
252
261
|
killIntentSeq: current.killIntent.seq,
|
|
253
262
|
killedAt: current.killIntent.facts.killedAt,
|
|
263
|
+
terminalAt,
|
|
254
264
|
releasedLockEvidence,
|
|
255
265
|
}, "killed verdict facts");
|
|
256
266
|
return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
|
|
@@ -8,7 +8,7 @@ import { parseStructuredQuery, STRUCTURED_QUERY_COMPLETION, STRUCTURED_QUERY_GRA
|
|
|
8
8
|
import { selectValidStoredAgentEvents } from "../stored-agent-event.js";
|
|
9
9
|
import { extractTerminalTimestamp } from "./generation/model.js";
|
|
10
10
|
import { readProjectionAlias } from "./projection-alias.js";
|
|
11
|
-
import { foldPublicProjectionActivity } from "./projection-activity.js";
|
|
11
|
+
import { foldPublicProjectionActivity, } from "./projection-activity.js";
|
|
12
12
|
import { isProjectionId, projectionDir, projectionRoot, readGenerationEventsWithRetention, readProjectionEvents } from "./projection-core.js";
|
|
13
13
|
import { projectionCoordinateRoot } from "./projection-coordinate.js";
|
|
14
14
|
import { readProjectionIdentity } from "./projection-identity.js";
|
|
@@ -31,7 +31,7 @@ export function interruptProjectionGeneration(projectionDirectory, nowMs = Date.
|
|
|
31
31
|
store.close();
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
function settleReleasedLeash(store, projectionDirectory, executionId) {
|
|
34
|
+
function settleReleasedLeash(store, projectionDirectory, executionId, terminalAt) {
|
|
35
35
|
const current = store.readCurrentGeneration();
|
|
36
36
|
if (current?.launch.executionId !== executionId
|
|
37
37
|
|| current.verdict) {
|
|
@@ -50,7 +50,8 @@ function settleReleasedLeash(store, projectionDirectory, executionId) {
|
|
|
50
50
|
try {
|
|
51
51
|
store.settleKillIfRunnerDead({
|
|
52
52
|
executionId,
|
|
53
|
-
releasedLockEvidence: { state: "acquired", executionId },
|
|
53
|
+
releasedLockEvidence: { state: "acquired", executionId, observedAt: terminalAt },
|
|
54
|
+
terminalAt,
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
57
|
finally {
|
|
@@ -78,11 +79,11 @@ export function killProjectionGeneration(projectionDirectory, nowMs = Date.now()
|
|
|
78
79
|
const executionId = current.launch.executionId;
|
|
79
80
|
const request = store.requestKillIfOpen({ executionId, killedAt });
|
|
80
81
|
if (request.status === "committed") {
|
|
81
|
-
return settleReleasedLeash(store, projectionDirectory, executionId);
|
|
82
|
+
return settleReleasedLeash(store, projectionDirectory, executionId, new Date(nowMs).toISOString());
|
|
82
83
|
}
|
|
83
84
|
if (request.reason === "kill-requested") {
|
|
84
85
|
// Idempotent first intent: still accepted, then one leash observation.
|
|
85
|
-
return settleReleasedLeash(store, projectionDirectory, executionId);
|
|
86
|
+
return settleReleasedLeash(store, projectionDirectory, executionId, new Date(nowMs).toISOString());
|
|
86
87
|
}
|
|
87
88
|
if (request.reason === "already-terminal") {
|
|
88
89
|
return { status: "already-closed", executionId };
|