@astrosheep/keiyaku 1.0.0 → 1.0.2

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 (123) hide show
  1. package/README.md +73 -56
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms_v2.js +103 -0
  4. package/build/agents/harness/activity-values.js +54 -0
  5. package/build/agents/harness/event-channel.js +55 -20
  6. package/build/agents/harness/events.js +47 -1
  7. package/build/agents/harness/execution-handle.js +292 -61
  8. package/build/agents/harness/index.js +103 -121
  9. package/build/agents/harness/pump.js +36 -47
  10. package/build/agents/harness/runtime.js +3 -0
  11. package/build/agents/launch-snapshot_v2.js +348 -0
  12. package/build/agents/opencode-sdk.js +42 -169
  13. package/build/agents/providers/claude-agent-sdk.js +240 -79
  14. package/build/agents/providers/codex-app-server.js +280 -72
  15. package/build/agents/providers/codex-sdk.js +159 -41
  16. package/build/agents/providers/event-type-tail.js +16 -0
  17. package/build/agents/providers/opencode-sdk.js +165 -59
  18. package/build/agents/providers/pi.js +254 -42
  19. package/build/agents/selector_v2.js +122 -0
  20. package/build/cli/commands/akuma.js +120 -42
  21. package/build/cli/completion.js +6 -7
  22. package/build/cli/flags.js +36 -10
  23. package/build/cli/help.js +42 -26
  24. package/build/cli/index.js +225 -93
  25. package/build/cli/parse.js +194 -75
  26. package/build/cli/projection-address.js +11 -0
  27. package/build/cli/render/address.js +88 -0
  28. package/build/cli/render/arc.js +10 -10
  29. package/build/cli/render/call.js +59 -0
  30. package/build/cli/render/compact-text.js +3 -0
  31. package/build/cli/render/errors.js +4 -2
  32. package/build/cli/render/format.js +6 -0
  33. package/build/cli/render/line-width.js +70 -0
  34. package/build/cli/render/misc.js +7 -4
  35. package/build/cli/render/petition.js +4 -3
  36. package/build/cli/render/projection-activity.js +201 -0
  37. package/build/cli/render/shared.js +46 -12
  38. package/build/cli/render/status.js +59 -10
  39. package/build/cli/render/tool-ledger-rollup.js +74 -0
  40. package/build/cli/render/tool-presentation.js +97 -0
  41. package/build/cli/render/wait.js +229 -0
  42. package/build/cli/skills-install.js +42 -24
  43. package/build/cli/subagent-guard.js +1 -1
  44. package/build/config/akuma-loader_v2.js +222 -0
  45. package/build/config/env-keys.js +0 -4
  46. package/build/config/env.js +0 -6
  47. package/build/config/settings.js +325 -12
  48. package/build/core/addressing.js +224 -0
  49. package/build/core/amend.js +30 -47
  50. package/build/core/arc.js +68 -60
  51. package/build/core/bind.js +105 -25
  52. package/build/core/call-persist.js +69 -0
  53. package/build/core/call.js +658 -0
  54. package/build/core/claim.js +72 -44
  55. package/build/core/command-io.js +19 -19
  56. package/build/core/context.js +11 -36
  57. package/build/core/draft.js +2 -3
  58. package/build/core/entry.js +101 -6
  59. package/build/core/execution-coordinate.js +62 -0
  60. package/build/core/execution-pact.js +141 -0
  61. package/build/core/forfeit.js +2 -3
  62. package/build/core/hints.js +10 -10
  63. package/build/core/ids.js +6 -0
  64. package/build/core/ledger.js +16 -1
  65. package/build/core/petition-claim-gates.js +14 -12
  66. package/build/core/petition-claim.js +1 -0
  67. package/build/core/petition-run.js +6 -2
  68. package/build/core/petition.js +63 -21
  69. package/build/core/places.js +106 -13
  70. package/build/core/projection/generation.js +412 -0
  71. package/build/core/projection/heart.js +316 -0
  72. package/build/core/projection/identity.js +81 -0
  73. package/build/core/projection/leash.js +87 -0
  74. package/build/core/projection/mint.js +154 -0
  75. package/build/core/projection-activity.js +219 -0
  76. package/build/core/projection-coordinate.js +35 -0
  77. package/build/core/projection-core.js +69 -463
  78. package/build/core/projection-generation-continuation.js +31 -0
  79. package/build/core/projection-generation-execution.js +148 -0
  80. package/build/core/projection-generation-identity.js +17 -0
  81. package/build/core/projection-generation-launcher.js +90 -0
  82. package/build/core/projection-generation-process.js +43 -0
  83. package/build/core/projection-generation-runner.js +144 -0
  84. package/build/core/projection-generation-runtime.js +15 -0
  85. package/build/core/projection-generation-store.js +707 -0
  86. package/build/core/projection-identity.js +11 -0
  87. package/build/core/projection-life-observer.js +87 -0
  88. package/build/core/projection-life-protocol.js +95 -0
  89. package/build/core/projection-mint.js +96 -24
  90. package/build/core/projection-runner-lock.js +230 -0
  91. package/build/core/projection-status.js +207 -107
  92. package/build/core/projection-tells.js +401 -0
  93. package/build/core/projection-wait.js +155 -0
  94. package/build/core/projection-wake.js +109 -231
  95. package/build/core/queue_v2.js +342 -0
  96. package/build/core/registry.js +6 -3
  97. package/build/core/render.js +63 -1
  98. package/build/core/renew.js +80 -53
  99. package/build/core/scope.js +186 -137
  100. package/build/core/seal.js +16 -19
  101. package/build/core/status.js +88 -25
  102. package/build/core/stored-agent-event.js +101 -0
  103. package/build/core/transcripts.js +214 -228
  104. package/build/core/verdict.js +27 -15
  105. package/build/core/worktree-bootstrap.js +187 -0
  106. package/build/core/worktree-path.js +72 -18
  107. package/build/flow-error.js +4 -6
  108. package/build/generated/version.js +1 -1
  109. package/build/git/branches.js +6 -1
  110. package/build/index.js +8 -5
  111. package/build/keiyaku.js +1 -3
  112. package/package.json +6 -5
  113. package/skills/keiyaku/SKILL.md +6 -47
  114. package/skills/keiyaku-akuma/SKILL.md +75 -0
  115. package/build/agents/effective-policy.js +0 -32
  116. package/build/agents/index.js +0 -114
  117. package/build/agents/selector.js +0 -28
  118. package/build/cli/render/summon.js +0 -17
  119. package/build/cli/render/tell.js +0 -11
  120. package/build/config/akuma-loader.js +0 -268
  121. package/build/core/queue.js +0 -73
  122. package/build/core/summon-persist.js +0 -73
  123. package/build/core/summon.js +0 -377
@@ -0,0 +1,101 @@
1
+ import { activityEventBodySchema, AGENT_EVENT_TEXT_MAX_CHARS, AGENT_UNKNOWN_PAYLOAD_MAX_CHARS, } from "../agents/harness/index.js";
2
+ import { EXEC_EVENT_BUFFER_HARD_CAP } from "../agents/harness/event-channel.js";
3
+ import { isSubagentProvider } from "../keiyaku.js";
4
+ const FAIL_CODES = new Set(["SUBAGENT_TIMEOUT", "SUBAGENT_NON_ZERO_EXIT", "SUBAGENT_SPAWN_ERROR", "SUBAGENT_EXEC_ERROR", "SUBAGENT_PROVIDER_UNAVAILABLE", "PROJECTION_STATE_ERROR"]);
5
+ const KNOWN = new Set(["message", "unknown", "activity", "thought", "plan", "turn", "usage", "warning", "diagnostic", "truncation", "done", "fail"]);
6
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
7
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
8
+ const optionalString = (value, key) => value[key] === undefined || typeof value[key] === "string";
9
+ export function classifyStoredAgentEvent(value) {
10
+ if (!record(value) || typeof value.type !== "string")
11
+ return "malformed";
12
+ if (value.type === "action")
13
+ return "malformed";
14
+ if (!KNOWN.has(value.type))
15
+ return "unknown";
16
+ if (typeof value.at !== "string"
17
+ || !Number.isFinite(Date.parse(value.at))
18
+ || !Number.isInteger(value.seq)
19
+ || !Number.isSafeInteger(value.turn)
20
+ || value.turn < 0)
21
+ return "malformed";
22
+ switch (value.type) {
23
+ case "message": return typeof value.text === "string" ? "valid" : "malformed";
24
+ case "unknown": {
25
+ if (typeof value.provider !== "string" || value.provider.length > AGENT_EVENT_TEXT_MAX_CHARS ||
26
+ typeof value.nativeType !== "string" || value.nativeType.length > AGENT_EVENT_TEXT_MAX_CHARS ||
27
+ typeof value.payload !== "string" || value.payload.length > AGENT_UNKNOWN_PAYLOAD_MAX_CHARS ||
28
+ (value.truncated !== undefined && value.truncated !== true))
29
+ return "malformed";
30
+ if (value.elided === undefined)
31
+ return "valid";
32
+ if (!record(value.elided))
33
+ return "malformed";
34
+ if (value.elided.reason !== "truncated" && value.elided.reason !== "unserializable")
35
+ return "malformed";
36
+ if (!Number.isInteger(value.elided.retainedChars) || value.elided.retainedChars < 0)
37
+ return "malformed";
38
+ if (value.elided.retainedChars !== value.payload.length)
39
+ return "malformed";
40
+ if (value.elided.originalChars !== undefined && (!Number.isInteger(value.elided.originalChars) || value.elided.originalChars < 0))
41
+ return "malformed";
42
+ if (value.elided.reason === "truncated" && (!Number.isInteger(value.elided.originalChars) || value.elided.originalChars <= value.payload.length))
43
+ return "malformed";
44
+ return "valid";
45
+ }
46
+ case "activity": {
47
+ const { seq: _seq, at: _at, turn: _turn, ...body } = value;
48
+ return activityEventBodySchema.safeParse(body).success ? "valid" : "malformed";
49
+ }
50
+ case "thought": return typeof value.id === "string" && typeof value.text === "string" && typeof value.final === "boolean" && (value.truncated === undefined || value.truncated === true) ? "valid" : "malformed";
51
+ case "plan": return typeof value.id === "string" && Array.isArray(value.items) && value.items.every((item) => record(item) && typeof item.text === "string" && typeof item.done === "boolean" && (item.active === undefined || item.active === true)) ? "valid" : "malformed";
52
+ case "turn": return value.phase === "started" || value.phase === "completed" ? "valid" : "malformed";
53
+ case "usage": return finite(value.inputTokens) && finite(value.outputTokens) && (value.cachedInputTokens === undefined || finite(value.cachedInputTokens)) && (value.costUsd === undefined || finite(value.costUsd)) && optionalString(value, "model") ? "valid" : "malformed";
54
+ case "warning": return record(value.warning) && (value.warning.reason === "refusal" || value.warning.reason === "overload") && optionalString(value.warning, "originalModel") && optionalString(value.warning, "fallbackModel") && optionalString(value.warning, "direction") && (value.warning.requestId === undefined || value.warning.requestId === null || typeof value.warning.requestId === "string") && optionalString(value.warning, "signerModel") ? "valid" : "malformed";
55
+ case "diagnostic": return typeof value.text === "string" ? "valid" : "malformed";
56
+ case "truncation": {
57
+ return Number.isInteger(value.limit)
58
+ && Number.isInteger(value.admitted)
59
+ && value.limit >= 0
60
+ && value.admitted >= 0
61
+ && value.limit === EXEC_EVENT_BUFFER_HARD_CAP
62
+ && value.admitted === EXEC_EVENT_BUFFER_HARD_CAP
63
+ ? "valid"
64
+ : "malformed";
65
+ }
66
+ case "done": return optionalString(value, "finalMessage") && record(value.session) && typeof value.session.provider === "string" && isSubagentProvider(value.session.provider) && finite(value.session.version) && Number.isInteger(value.session.version) && value.session.version > 0 && "handle" in value.session ? "valid" : "malformed";
67
+ case "fail": return typeof value.message === "string" && (value.code === undefined || (typeof value.code === "string" && FAIL_CODES.has(value.code))) && (value.exitCode === undefined || value.exitCode === null || finite(value.exitCode)) && optionalString(value, "diagnostics") ? "valid" : "malformed";
68
+ }
69
+ return "unknown";
70
+ }
71
+ export function selectValidStoredAgentEvents(values) {
72
+ const events = [];
73
+ const activityTools = new Map();
74
+ let malformed = 0;
75
+ for (const value of values) {
76
+ const shape = classifyStoredAgentEvent(value);
77
+ if (shape === "valid") {
78
+ const event = value;
79
+ if (event.type === "activity") {
80
+ const priorTool = activityTools.get(event.id);
81
+ if (priorTool !== undefined && priorTool !== event.tool) {
82
+ malformed += 1;
83
+ continue;
84
+ }
85
+ activityTools.set(event.id, event.tool);
86
+ }
87
+ events.push(event);
88
+ }
89
+ if (shape === "malformed")
90
+ malformed += 1;
91
+ }
92
+ return { events, malformed };
93
+ }
94
+ export function invalidActivityEventDiagnostic(malformed) {
95
+ return malformed > 0
96
+ ? {
97
+ kind: "invalid-activity-event",
98
+ detail: `${malformed} persisted activity event${malformed === 1 ? "" : "s"} malformed`,
99
+ }
100
+ : undefined;
101
+ }
@@ -1,286 +1,272 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
- import { RESPONSE_ARTIFACT_NOTICE, RESPONSE_HISTORY_DIR } from "../keiyaku.js";
4
+ import { parseResolvedAkumaLaunchSnapshot, } from "../agents/launch-snapshot_v2.js";
4
5
  import { resolvePersistedSubagentSession } from "../agents/session.js";
5
6
  import { isErrnoException } from "../errno.js";
6
7
  import { FlowError, requireText } from "../flow-error.js";
8
+ import { getCurrentBranch } from "../git/branches.js";
9
+ import { getLatestCommitHash } from "../git/staging.js";
10
+ import { RESPONSE_ARTIFACT_NOTICE, RESPONSE_HISTORY_DIR } from "../keiyaku.js";
11
+ import { parseExecutionCoordinates } from "./execution-coordinate.js";
12
+ import { createUlid } from "./ids.js";
7
13
  import { parseToAST } from "./markdown/parser.js";
8
14
  const RESPONSE_HISTORY_SLUG_MAX_LENGTH = 24;
9
15
  const RESPONSE_HISTORY_SESSION_KEY = "session";
16
+ const RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY = "launch_snapshot";
17
+ const RESPONSE_HISTORY_PROVENANCE_KEY = "provenance";
10
18
  const REVIEW_HISTORY_FILE_RE = /^\d{8}-\d{9}_REVIEW_(\d+)\.md$/;
19
+ export const RESPONSE_ARTIFACT_ID_RE = /^rsp_[0-9A-HJKMNP-TV-Z]{26}$/;
11
20
  export const RESPONSE_ARTIFACT_HEADINGS = {
12
- summon: "# Summon",
21
+ call: "# Call",
13
22
  bind: "# Bind",
14
23
  arc: "# Arc",
15
24
  renew: "# Renew",
16
25
  review: "# Review",
17
26
  };
18
27
  const RESUMABLE_RESPONSE_HEADINGS = Object.values(RESPONSE_ARTIFACT_HEADINGS);
19
- function toTimestampParts(now) {
20
- const year = String(now.getFullYear());
21
- const month = String(now.getMonth() + 1).padStart(2, "0");
22
- const day = String(now.getDate()).padStart(2, "0");
23
- const hour = String(now.getHours()).padStart(2, "0");
24
- const minute = String(now.getMinutes()).padStart(2, "0");
25
- const second = String(now.getSeconds()).padStart(2, "0");
26
- const millisecond = String(now.getMilliseconds()).padStart(3, "0");
27
- return {
28
- fileStamp: `${year}${month}${day}-${hour}${minute}${second}${millisecond}`,
29
- iso: now.toISOString(),
30
- };
28
+ function artifactError(locator, field, reason) {
29
+ return new FlowError("INVALID_RESPONSE_PATH", `Invalid response artifact '${locator}': field '${field}' ${reason}.`);
31
30
  }
32
- function toSlug(text, fallback) {
33
- const normalized = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-");
34
- const truncated = normalized
35
- .slice(0, RESPONSE_HISTORY_SLUG_MAX_LENGTH)
36
- .replace(/^-+/, "")
37
- .replace(/-+$/, "");
38
- return truncated || fallback;
31
+ function isRecord(value) {
32
+ return value !== null && typeof value === "object" && !Array.isArray(value);
39
33
  }
40
- function toYamlSingleQuoted(value) {
41
- return `'${value.replace(/'/g, "''")}'`;
34
+ function assertExactFields(value, required, locator, prefix) {
35
+ for (const field of required) {
36
+ if (!(field in value))
37
+ throw artifactError(locator, `${prefix}.${field}`, "is missing");
38
+ }
39
+ const allowed = new Set(required);
40
+ const unknown = Object.keys(value).filter((field) => !allowed.has(field)).sort()[0];
41
+ if (unknown !== undefined)
42
+ throw artifactError(locator, `${prefix}.${unknown}`, "is unknown");
42
43
  }
43
- function toSectionBody(value) {
44
- const trimmed = value?.trim();
45
- return trimmed && trimmed.length > 0 ? value : "(none)";
44
+ function nonBlank(value, locator, field) {
45
+ if (typeof value !== "string" || value.trim() === "")
46
+ throw artifactError(locator, field, "must be a non-blank string");
46
47
  }
47
- function buildResponseHistoryMarkdown(input, timestampIso) {
48
- const branch = input.branch?.trim() || "(unknown)";
49
- const commit = input.commit?.trim();
50
- const sessionText = input.session ? JSON.stringify(input.session) : undefined;
51
- const subagentName = input.subagentName?.trim();
52
- const frontMatterLines = [
53
- "---",
54
- `branch: ${toYamlSingleQuoted(branch)}`,
55
- ...(commit ? [`commit: ${toYamlSingleQuoted(commit)}`] : []),
56
- ...(sessionText ? [`${RESPONSE_HISTORY_SESSION_KEY}: ${toYamlSingleQuoted(sessionText)}`] : []),
57
- ...(subagentName ? [`subagent_name: ${toYamlSingleQuoted(subagentName)}`] : []),
58
- ...(input.effectivePolicy ? [`effective_policy: ${toYamlSingleQuoted(JSON.stringify(input.effectivePolicy))}`] : []),
59
- `time: ${toYamlSingleQuoted(timestampIso)}`,
60
- `notice: ${toYamlSingleQuoted(RESPONSE_ARTIFACT_NOTICE)}`,
61
- "---",
62
- "",
63
- ];
64
- if (input.tool === "summon") {
65
- return [
66
- ...frontMatterLines,
67
- "# Summon",
68
- "",
69
- "## Task",
70
- toSectionBody(input.task),
71
- "",
72
- "## Context",
73
- toSectionBody(input.context),
74
- "",
75
- "## Response",
76
- toSectionBody(input.response),
77
- "",
78
- ].join("\n");
48
+ /** The sole durable provenance parser. */
49
+ export function parseResponseArtifactProvenance(value, locator) {
50
+ if (!isRecord(value))
51
+ throw artifactError(locator, "provenance", "must be an object");
52
+ assertExactFields(value, ["artifactId", "authority", "binding", "evidence"], locator, "provenance");
53
+ nonBlank(value.artifactId, locator, "provenance.artifactId");
54
+ if (!RESPONSE_ARTIFACT_ID_RE.test(value.artifactId)) {
55
+ throw artifactError(locator, "provenance.artifactId", "must be rsp_ followed by a ULID");
79
56
  }
80
- if (input.tool === "review") {
81
- return [
82
- ...frontMatterLines,
83
- "# Review",
84
- "",
85
- "## Prompt",
86
- toSectionBody(input.prompt),
87
- "",
88
- "## Response",
89
- toSectionBody(input.response),
90
- "",
91
- ].join("\n");
57
+ const { authority, binding } = parseExecutionCoordinates(value.authority, value.binding, (field, reason) => artifactError(locator, `provenance.${field}`, reason));
58
+ if (!isRecord(value.evidence))
59
+ throw artifactError(locator, "provenance.evidence", "must be an object");
60
+ if (authority.kind === "repository") {
61
+ assertExactFields(value.evidence, ["branch", "commit"], locator, "provenance.evidence");
62
+ nonBlank(value.evidence.branch, locator, "provenance.evidence.branch");
63
+ nonBlank(value.evidence.commit, locator, "provenance.evidence.commit");
64
+ return { artifactId: value.artifactId, authority, binding, evidence: { branch: value.evidence.branch, commit: value.evidence.commit } };
92
65
  }
93
- if (input.tool === "arc") {
94
- return [
95
- ...frontMatterLines,
96
- "# Arc",
97
- "",
98
- "## Objective",
99
- toSectionBody(input.objective),
100
- "",
101
- "## Brief",
102
- toSectionBody(input.brief),
103
- "",
104
- "## Ledger Commit",
105
- toSectionBody(input.ledgerCommit),
106
- "",
107
- "## Summary",
108
- toSectionBody(input.summary),
109
- "",
110
- "## Diff",
111
- toSectionBody(input.diff),
112
- "",
113
- ].join("\n");
66
+ assertExactFields(value.evidence, ["workdir"], locator, "provenance.evidence");
67
+ nonBlank(value.evidence.workdir, locator, "provenance.evidence.workdir");
68
+ if (!path.isAbsolute(value.evidence.workdir)) {
69
+ throw artifactError(locator, "provenance.evidence.workdir", "must be an absolute path");
114
70
  }
115
- return [
116
- ...frontMatterLines,
117
- "# Renew",
118
- "",
119
- "## Base",
120
- `${input.oldBase} -> ${input.newBase}`,
121
- "",
122
- "## Head",
123
- `${input.oldHead} -> ${input.newHead}`,
124
- "",
125
- "## Ledger Commit",
126
- toSectionBody(input.ledgerCommit),
127
- "",
128
- "## Summary",
129
- toSectionBody(input.summary),
130
- "",
131
- "## Diff",
132
- toSectionBody(input.diff),
133
- "",
134
- ].join("\n");
71
+ return { artifactId: value.artifactId, authority, binding, evidence: { workdir: value.evidence.workdir } };
135
72
  }
73
+ async function captureArtifactEvidence(coordinate) {
74
+ const evidenceCwd = path.resolve(coordinate.evidenceCwd);
75
+ if (coordinate.authority.kind === "user")
76
+ return { workdir: evidenceCwd };
77
+ const [branch, commit] = await Promise.all([getCurrentBranch(evidenceCwd), getLatestCommitHash(evidenceCwd)]);
78
+ if (!branch.trim())
79
+ throw artifactError("new response artifact", "provenance.evidence.branch", "must be a non-blank string");
80
+ if (!commit.trim())
81
+ throw artifactError("new response artifact", "provenance.evidence.commit", "must be a non-blank string");
82
+ return { branch, commit };
83
+ }
84
+ function timestampParts(now) {
85
+ const part = (value, width) => String(value).padStart(width, "0");
86
+ return {
87
+ fileStamp: `${now.getFullYear()}${part(now.getMonth() + 1, 2)}${part(now.getDate(), 2)}-${part(now.getHours(), 2)}${part(now.getMinutes(), 2)}${part(now.getSeconds(), 2)}${part(now.getMilliseconds(), 3)}`,
88
+ iso: now.toISOString(),
89
+ };
90
+ }
91
+ function slug(text, fallback) {
92
+ return text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, RESPONSE_HISTORY_SLUG_MAX_LENGTH).replace(/^-+|-+$/g, "") || fallback;
93
+ }
94
+ function yamlQuote(value) { return `'${value.replace(/'/g, "''")}'`; }
95
+ function section(value) { return value?.trim() ? value : "(none)"; }
96
+ function buildMarkdown(input, iso, provenance) {
97
+ const frontmatter = [
98
+ "---",
99
+ `${RESPONSE_HISTORY_PROVENANCE_KEY}: ${yamlQuote(JSON.stringify(provenance))}`,
100
+ ...(input.session ? [`${RESPONSE_HISTORY_SESSION_KEY}: ${yamlQuote(JSON.stringify(input.session))}`] : []),
101
+ ...(input.launchSnapshot ? [`${RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY}: ${yamlQuote(JSON.stringify(input.launchSnapshot))}`] : []),
102
+ ...(input.subagentName?.trim() ? [`subagent_name: ${yamlQuote(input.subagentName.trim())}`] : []),
103
+ ...(input.effectivePolicy ? [`effective_policy: ${yamlQuote(JSON.stringify(input.effectivePolicy))}`] : []),
104
+ `time: ${yamlQuote(iso)}`,
105
+ `notice: ${yamlQuote(RESPONSE_ARTIFACT_NOTICE)}`,
106
+ "---", "",
107
+ ];
108
+ if (input.tool === "call")
109
+ return [...frontmatter, "# Call", "", "## Task", section(input.task), "", "## Context", section(input.context), "", "## Response", section(input.response), ""].join("\n");
110
+ if (input.tool === "review")
111
+ return [...frontmatter, "# Review", "", "## Prompt", section(input.prompt), "", "## Response", section(input.response), ""].join("\n");
112
+ if (input.tool === "arc")
113
+ return [...frontmatter, "# Arc", "", "## Objective", section(input.objective), "", "## Brief", section(input.brief), "", "## Ledger Commit", section(input.ledgerCommit), "", "## Summary", section(input.summary), "", "## Diff", section(input.diff), ""].join("\n");
114
+ return [...frontmatter, "# Renew", "", "## Base", `${input.oldBase} -> ${input.newBase}`, "", "## Head", `${input.oldHead} -> ${input.newHead}`, "", "## Ledger Commit", section(input.ledgerCommit), "", "## Summary", section(input.summary), "", "## Diff", section(input.diff), ""].join("\n");
115
+ }
116
+ /** The sole response artifact writer. */
136
117
  export async function persistResponseHistory(input) {
118
+ const coordinates = parseExecutionCoordinates(input.provenance.authority, input.provenance.binding, (field, reason) => artifactError("new response artifact", `provenance.${field}`, reason));
137
119
  const now = input.now ?? new Date();
138
- const { fileStamp, iso } = toTimestampParts(now);
139
- const slug = toSlug(input.title ?? "", input.tool);
140
- const toolNameForFile = input.tool.toUpperCase();
120
+ const artifactId = `rsp_${createUlid(now.getTime(), [...randomBytes(10)])}`;
121
+ const provenance = {
122
+ artifactId,
123
+ ...coordinates,
124
+ evidence: await captureArtifactEvidence({ ...input.provenance, ...coordinates }),
125
+ };
126
+ const { fileStamp, iso } = timestampParts(now);
141
127
  const historyDir = path.join(input.cwd, RESPONSE_HISTORY_DIR);
142
- const responsePath = path.join(historyDir, `${fileStamp}_${toolNameForFile}_${slug}.md`);
143
- const markdown = buildResponseHistoryMarkdown(input, iso);
128
+ const responsePath = path.join(historyDir, `${fileStamp}_${input.tool.toUpperCase()}_${slug(input.title ?? "", input.tool)}.md`);
144
129
  await fs.mkdir(historyDir, { recursive: true });
145
- await fs.writeFile(responsePath, markdown, "utf-8");
146
- return path.relative(input.cwd, responsePath);
130
+ await fs.writeFile(responsePath, buildMarkdown(input, iso, provenance), "utf8");
131
+ return { artifactId, responsePath: path.relative(input.cwd, responsePath) };
147
132
  }
148
- export async function nextReviewResponseNumber(cwd) {
149
- const historyDir = path.join(cwd, RESPONSE_HISTORY_DIR);
150
- let names;
133
+ function detectTool(document) {
134
+ const lines = document.split("\n");
135
+ let index = 0;
136
+ if (lines[0] === "---") {
137
+ index = lines.indexOf("---", 1);
138
+ if (index < 0)
139
+ return null;
140
+ index += 1;
141
+ }
142
+ for (; index < lines.length; index += 1) {
143
+ const line = lines[index].trim();
144
+ if (!line)
145
+ continue;
146
+ const found = Object.entries(RESPONSE_ARTIFACT_HEADINGS).find(([, heading]) => heading === line);
147
+ return found?.[0] ?? null;
148
+ }
149
+ return null;
150
+ }
151
+ export async function readResponseArtifact(input) {
152
+ const locator = requireText("responsePath", input.responsePath);
153
+ const absolutePath = path.isAbsolute(locator) ? locator : path.resolve(input.cwd, locator);
154
+ let document;
151
155
  try {
152
- names = await fs.readdir(historyDir);
156
+ document = await fs.readFile(absolutePath, "utf8");
153
157
  }
154
158
  catch (error) {
155
- if (isErrnoException(error) && error.code === "ENOENT") {
156
- return 1;
157
- }
159
+ if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "EISDIR"))
160
+ throw new FlowError("INVALID_RESPONSE_PATH", `Response artifact not found: '${locator}'.`);
158
161
  throw error;
159
162
  }
160
- const maxReview = names.reduce((max, name) => {
161
- const match = REVIEW_HISTORY_FILE_RE.exec(name);
162
- if (!match)
163
- return max;
164
- return Math.max(max, Number(match[1]));
165
- }, 0);
166
- return maxReview + 1;
167
- }
168
- export async function listResumableResponseCandidates(cwd, limit) {
169
- const historyDir = path.join(cwd, RESPONSE_HISTORY_DIR);
170
- let names;
163
+ const ast = parseToAST(document);
164
+ const frontmatter = ast.frontmatter?.entries;
165
+ if (!frontmatter)
166
+ throw artifactError(locator, "provenance", "is missing");
167
+ if (frontmatter.notice !== RESPONSE_ARTIFACT_NOTICE)
168
+ throw artifactError(locator, "notice", "does not identify a Keiyaku response artifact");
169
+ const rawProvenance = frontmatter[RESPONSE_HISTORY_PROVENANCE_KEY];
170
+ if (rawProvenance === undefined)
171
+ throw artifactError(locator, "provenance", "is missing");
172
+ let provenanceValue;
171
173
  try {
172
- names = await fs.readdir(historyDir);
174
+ provenanceValue = JSON.parse(rawProvenance);
173
175
  }
174
176
  catch (error) {
175
- if (isErrnoException(error) && error.code === "ENOENT") {
176
- return [];
177
- }
178
- throw error;
177
+ throw artifactError(locator, "provenance", `contains malformed JSON${error instanceof Error ? ` (${error.message})` : ""}`);
178
+ }
179
+ const provenance = parseResponseArtifactProvenance(provenanceValue, locator);
180
+ const tool = detectTool(document);
181
+ if (!tool)
182
+ throw artifactError(locator, "artifactType", "is unsupported");
183
+ if (input.allowedTools?.length && !input.allowedTools.includes(tool)) {
184
+ if (input.onArtifactTypeMismatch === "return-null")
185
+ return undefined;
186
+ throw artifactError(locator, "artifactType", `must be ${input.allowedTools.join("/")}`);
179
187
  }
180
- const candidates = [];
181
- const sorted = names
182
- .filter((name) => name.endsWith(".md"))
183
- .sort((left, right) => right.localeCompare(left));
184
- for (const name of sorted) {
185
- const relativePath = path.join(RESPONSE_HISTORY_DIR, name);
188
+ const session = frontmatter[RESPONSE_HISTORY_SESSION_KEY] === undefined ? undefined : resolvePersistedSubagentSession({ resumePath: locator, sessionText: frontmatter[RESPONSE_HISTORY_SESSION_KEY] });
189
+ let launchSnapshot;
190
+ const rawLaunchSnapshot = frontmatter[RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY];
191
+ if (rawLaunchSnapshot !== undefined) {
192
+ let launchSnapshotValue;
186
193
  try {
187
- const resolution = await resolveSummonResumePath({ cwd, resumePath: relativePath });
188
- candidates.push({ path: relativePath, subagentName: resolution.subagentName });
194
+ launchSnapshotValue = JSON.parse(rawLaunchSnapshot);
189
195
  }
190
196
  catch (error) {
191
- if (isFlowErrorCode(error, "INVALID_RESPONSE_PATH")) {
192
- continue;
193
- }
194
- throw error;
197
+ throw artifactError(locator, RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY, `contains malformed JSON${error instanceof Error ? ` (${error.message})` : ""}`);
198
+ }
199
+ try {
200
+ launchSnapshot = parseResolvedAkumaLaunchSnapshot(launchSnapshotValue);
195
201
  }
196
- if (limit !== undefined && candidates.length >= limit) {
197
- break;
202
+ catch (error) {
203
+ throw artifactError(locator, RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY, `is invalid${error instanceof Error ? ` (${error.message})` : ""}`);
198
204
  }
199
205
  }
200
- return candidates;
206
+ const subagentName = frontmatter.subagent_name?.trim() || undefined;
207
+ const storageRoot = path.resolve(path.dirname(absolutePath), "..", "..");
208
+ return { artifactId: provenance.artifactId, responsePath: path.relative(input.cwd, absolutePath), absolutePath, storageRoot, provenance, tool, ...(session ? { session } : {}), ...(launchSnapshot ? { launchSnapshot } : {}), ...(subagentName ? { subagentName } : {}) };
201
209
  }
202
- export async function resolveResumePath(cwd, explicitPath) {
203
- if (explicitPath)
204
- return explicitPath;
205
- const candidates = await listResumableResponseCandidates(cwd, 4);
206
- if (candidates.length === 1) {
207
- return candidates[0].path;
210
+ export async function locateResponseArtifact(cwd, artifactId) {
211
+ const identity = requireText("artifactId", artifactId);
212
+ if (!RESPONSE_ARTIFACT_ID_RE.test(identity))
213
+ throw artifactError(identity, "provenance.artifactId", "must be rsp_ followed by a ULID");
214
+ const historyDir = path.join(cwd, RESPONSE_HISTORY_DIR);
215
+ let names;
216
+ try {
217
+ names = await fs.readdir(historyDir);
208
218
  }
209
- if (candidates.length === 0) {
210
- throw new FlowError("EMPTY_PARAM", "resume requires a response path because no resumable response artifacts were found.");
219
+ catch (error) {
220
+ if (isErrnoException(error) && error.code === "ENOENT")
221
+ throw new FlowError("INVALID_RESPONSE_PATH", `Response artifact '${identity}' was not found in ${historyDir}.`);
222
+ throw error;
211
223
  }
212
- const shown = candidates.slice(0, 3).map((candidate) => `- ${candidate.path}`);
213
- throw new FlowError("EMPTY_PARAM", ["resume found multiple resumable response artifacts; choose one explicitly:", ...shown].join("\n"));
214
- }
215
- function isFlowErrorCode(error, code) {
216
- return error instanceof FlowError && error.code === code;
217
- }
218
- function detectArtifactHeading(document) {
219
- const lines = document.split("\n");
220
- let index = 0;
221
- if (lines[0] === "---") {
222
- index = 1;
223
- while (index < lines.length && lines[index] !== "---") {
224
- index += 1;
225
- }
226
- if (index === lines.length) {
227
- return null;
224
+ let match;
225
+ for (const name of names.filter((candidate) => candidate.endsWith(".md")).sort()) {
226
+ const responsePath = path.join(RESPONSE_HISTORY_DIR, name);
227
+ const document = await fs.readFile(path.join(cwd, responsePath), "utf8");
228
+ const encoded = parseToAST(document).frontmatter?.entries?.[RESPONSE_HISTORY_PROVENANCE_KEY];
229
+ if (encoded === undefined)
230
+ continue;
231
+ let indexed;
232
+ try {
233
+ indexed = JSON.parse(encoded);
228
234
  }
229
- index += 1;
230
- }
231
- for (; index < lines.length; index += 1) {
232
- const line = lines[index].trim();
233
- if (line.length === 0) {
235
+ catch {
234
236
  continue;
235
237
  }
236
- return RESUMABLE_RESPONSE_HEADINGS.find((heading) => heading === line) ?? null;
238
+ if (!isRecord(indexed) || indexed.artifactId !== identity)
239
+ continue;
240
+ const parsed = await readResponseArtifact({ cwd, responsePath });
241
+ if (!parsed)
242
+ continue;
243
+ if (match)
244
+ throw new FlowError("INVALID_RESPONSE_PATH", `Response artifact identity '${identity}' is duplicated in ${historyDir}.`);
245
+ match = parsed;
237
246
  }
238
- return null;
247
+ if (!match)
248
+ throw new FlowError("INVALID_RESPONSE_PATH", `Response artifact '${identity}' was not found in ${historyDir}.`);
249
+ return match;
239
250
  }
240
- export async function resolveSummonResumePath(input) {
241
- const responsePath = requireText("resumePath", input.resumePath);
242
- const resolvedPath = path.isAbsolute(responsePath) ? responsePath : path.resolve(input.cwd, responsePath);
243
- let document;
251
+ export function requireResumableArtifact(artifact) {
252
+ if (!artifact.session)
253
+ throw artifactError(artifact.artifactId, RESPONSE_HISTORY_SESSION_KEY, "is missing; this conversation cannot be revived");
254
+ if (!artifact.subagentName)
255
+ throw artifactError(artifact.artifactId, "subagent_name", "is missing; this conversation cannot be revived");
256
+ if (!artifact.launchSnapshot)
257
+ throw artifactError(artifact.artifactId, RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY, "is missing; this conversation cannot be revived");
258
+ return artifact;
259
+ }
260
+ export async function nextReviewResponseNumber(cwd) {
261
+ const historyDir = path.join(cwd, RESPONSE_HISTORY_DIR);
262
+ let names;
244
263
  try {
245
- document = await fs.readFile(resolvedPath, "utf-8");
264
+ names = await fs.readdir(historyDir);
246
265
  }
247
266
  catch (error) {
248
- if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "EISDIR")) {
249
- throw new FlowError("INVALID_RESPONSE_PATH", `Resume path file not found: '${responsePath}'.`);
250
- }
267
+ if (isErrnoException(error) && error.code === "ENOENT")
268
+ return 1;
251
269
  throw error;
252
270
  }
253
- const ast = parseToAST(document);
254
- const frontmatter = ast.frontmatter?.entries;
255
- if (!frontmatter) {
256
- throw new FlowError("INVALID_RESPONSE_PATH", `Resume path file '${responsePath}' is missing YAML frontmatter.`);
257
- }
258
- if (frontmatter.notice !== RESPONSE_ARTIFACT_NOTICE) {
259
- throw new FlowError("INVALID_RESPONSE_PATH", `Resume path file '${responsePath}' is not a Keiyaku response artifact.`);
260
- }
261
- const artifactHeading = detectArtifactHeading(document);
262
- if (!artifactHeading) {
263
- throw new FlowError("INVALID_RESPONSE_PATH", `Resume path file '${responsePath}' is not a supported Keiyaku response artifact.`);
264
- }
265
- if (input.allowedTools?.length) {
266
- const allowedHeadings = input.allowedTools.map((tool) => RESPONSE_ARTIFACT_HEADINGS[tool]);
267
- if (!allowedHeadings.includes(artifactHeading)) {
268
- if (input.onArtifactTypeMismatch === "return-null") {
269
- return undefined;
270
- }
271
- throw new FlowError("INVALID_RESPONSE_PATH", `Resume path file '${responsePath}' is not a supported ${input.allowedTools.join("/")} response artifact.`);
272
- }
273
- }
274
- const session = resolvePersistedSubagentSession({
275
- resumePath: responsePath,
276
- sessionText: frontmatter[RESPONSE_HISTORY_SESSION_KEY],
277
- });
278
- if (!session) {
279
- throw new FlowError("INVALID_RESPONSE_PATH", `Resume path file '${responsePath}' does not include '${RESPONSE_HISTORY_SESSION_KEY}'; this conversation cannot be resumed.`);
280
- }
281
- const subagentName = frontmatter.subagent_name?.trim();
282
- if (!subagentName) {
283
- throw new FlowError("INVALID_RESPONSE_PATH", `Resume path file '${responsePath}' does not include 'subagent_name'; this conversation cannot be resumed.`);
284
- }
285
- return { session, subagentName };
271
+ return names.reduce((max, name) => Math.max(max, Number(REVIEW_HISTORY_FILE_RE.exec(name)?.[1] ?? 0)), 0) + 1;
286
272
  }