@astrosheep/keiyaku 0.1.87 → 1.0.0

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 (158) hide show
  1. package/README.md +12 -22
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/effective-policy.js +32 -0
  4. package/build/agents/harness/event-channel.js +178 -0
  5. package/build/agents/harness/execution-handle.js +117 -0
  6. package/build/agents/harness/index.js +622 -0
  7. package/build/agents/harness/pump.js +152 -0
  8. package/build/agents/harness/runtime.js +68 -0
  9. package/build/agents/index.js +104 -213
  10. package/build/agents/opencode-sdk.js +12 -18
  11. package/build/agents/providers/claude-agent-sdk.js +529 -0
  12. package/build/agents/providers/codex-app-server.js +656 -0
  13. package/build/agents/providers/codex-sdk.js +193 -0
  14. package/build/agents/providers/opencode-sdk.js +257 -0
  15. package/build/agents/providers/pi.js +345 -0
  16. package/build/agents/selector.js +15 -6
  17. package/build/agents/session.js +29 -0
  18. package/build/agents/types.js +5 -11
  19. package/build/cli/commands/akuma.js +79 -0
  20. package/build/cli/completion.js +71 -0
  21. package/build/cli/draft-artifact.js +50 -0
  22. package/build/cli/flags.js +96 -0
  23. package/build/cli/help.js +108 -0
  24. package/build/cli/index.js +204 -912
  25. package/build/cli/parse.js +280 -0
  26. package/build/cli/render/arc.js +63 -0
  27. package/build/{responses/tool-errors.js → cli/render/errors.js} +15 -16
  28. package/build/cli/render/format.js +199 -0
  29. package/build/cli/render/misc.js +40 -0
  30. package/build/cli/render/petition.js +32 -0
  31. package/build/{responses → cli/render}/response-style.js +4 -4
  32. package/build/cli/render/shared.js +122 -0
  33. package/build/cli/render/status.js +69 -0
  34. package/build/cli/render/summon.js +17 -0
  35. package/build/cli/render/tell.js +11 -0
  36. package/build/cli/skills-install.js +237 -0
  37. package/build/cli/subagent-guard.js +6 -3
  38. package/build/config/akuma-loader.js +268 -0
  39. package/build/config/env-keys.js +0 -10
  40. package/build/config/{schema.js → env.js} +117 -23
  41. package/build/config/settings.js +137 -260
  42. package/build/core/amend.js +120 -0
  43. package/build/core/arc.js +225 -0
  44. package/build/core/atomic-publish.js +64 -0
  45. package/build/core/bind.js +281 -0
  46. package/build/core/claim.js +360 -0
  47. package/build/core/command-io.js +174 -0
  48. package/build/core/context.js +108 -0
  49. package/build/core/contract.js +14 -0
  50. package/build/core/draft.js +82 -0
  51. package/build/core/entry.js +203 -0
  52. package/build/core/forfeit.js +60 -0
  53. package/build/core/hints.js +108 -0
  54. package/build/core/ids.js +70 -0
  55. package/build/core/ledger.js +117 -0
  56. package/build/core/log.js +24 -0
  57. package/build/{protocol → core}/markdown/parser.js +68 -0
  58. package/build/{protocol → core}/markdown/sections.js +2 -28
  59. package/build/core/markdown/titles.js +9 -0
  60. package/build/{tools/petition/claim-gates.js → core/petition-claim-gates.js} +43 -29
  61. package/build/core/petition-claim.js +116 -0
  62. package/build/core/petition-forfeit.js +39 -0
  63. package/build/{tools/round/head-guard.js → core/petition-head-guard.js} +2 -2
  64. package/build/core/petition-run.js +81 -0
  65. package/build/core/petition.js +96 -0
  66. package/build/core/places.js +82 -0
  67. package/build/core/projection-core.js +541 -0
  68. package/build/core/projection-mint.js +79 -0
  69. package/build/core/projection-status.js +179 -0
  70. package/build/core/projection-wake.js +252 -0
  71. package/build/core/queue.js +73 -0
  72. package/build/core/ref-log.js +94 -0
  73. package/build/core/registry.js +96 -0
  74. package/build/core/render.js +326 -0
  75. package/build/core/renew.js +195 -0
  76. package/build/core/scope.js +163 -0
  77. package/build/core/seal.js +195 -0
  78. package/build/core/status.js +476 -0
  79. package/build/{tools/summon/persist.js → core/summon-persist.js} +10 -9
  80. package/build/core/summon.js +377 -0
  81. package/build/{protocol/response-history.js → core/transcripts.js} +83 -39
  82. package/build/core/verdict.js +96 -0
  83. package/build/core/worktree-path.js +134 -0
  84. package/build/flow-error.js +80 -4
  85. package/build/generated/version.js +1 -1
  86. package/build/git/branches.js +8 -9
  87. package/build/git/core.js +1 -1
  88. package/build/git/diff/pathspec.js +2 -3
  89. package/build/git/diff/preview.js +7 -2
  90. package/build/git/diff/read.js +4 -3
  91. package/build/git/staging.js +1 -1
  92. package/build/index.js +12 -6
  93. package/build/keiyaku.js +17 -13
  94. package/build/telemetry/debug-log.js +1 -1
  95. package/package.json +16 -9
  96. package/skills/keiyaku/SKILL.md +16 -13
  97. package/build/agents/claude-agent-sdk-exec.js +0 -352
  98. package/build/agents/codex-cli-exec.js +0 -237
  99. package/build/agents/codex-sdk-exec.js +0 -291
  100. package/build/agents/gemini-cli-core-exec.js +0 -499
  101. package/build/agents/opencode-exec.js +0 -297
  102. package/build/agents/process-runner.js +0 -239
  103. package/build/agents/progress-reporter.js +0 -137
  104. package/build/agents/round-runner.js +0 -53
  105. package/build/config/apply-argument-descriptions.js +0 -16
  106. package/build/config/architect-hints.js +0 -374
  107. package/build/config/base-rules.js +0 -23
  108. package/build/config/config.js +0 -50
  109. package/build/config/dotenv.js +0 -61
  110. package/build/config/keiyaku-home.js +0 -9
  111. package/build/config/path-utils.js +0 -11
  112. package/build/config/render-template.js +0 -8
  113. package/build/git/diff/filter.js +0 -67
  114. package/build/git/keiyaku-state.js +0 -58
  115. package/build/git/snapshot.js +0 -41
  116. package/build/git/stash.js +0 -62
  117. package/build/protocol/bind-draft.js +0 -195
  118. package/build/protocol/draft-artifacts.js +0 -99
  119. package/build/protocol/file-guards.js +0 -24
  120. package/build/protocol/keiyaku-draft.js +0 -168
  121. package/build/protocol/ledger.js +0 -374
  122. package/build/protocol/markdown/index.js +0 -14
  123. package/build/protocol/markdown/normalization.js +0 -71
  124. package/build/protocol/markdown/render.js +0 -68
  125. package/build/protocol/oath.js +0 -36
  126. package/build/protocol/render-keiyaku.js +0 -37
  127. package/build/protocol/review.js +0 -1
  128. package/build/protocol/status-previews.js +0 -68
  129. package/build/responses/responses.js +0 -515
  130. package/build/tools/amend/index.js +0 -18
  131. package/build/tools/amend/run.js +0 -44
  132. package/build/tools/bind/index.js +0 -44
  133. package/build/tools/bind/run.js +0 -237
  134. package/build/tools/contracts.js +0 -1
  135. package/build/tools/create-handler.js +0 -31
  136. package/build/tools/help.js +0 -9
  137. package/build/tools/petition/claim.js +0 -174
  138. package/build/tools/petition/forfeit.js +0 -100
  139. package/build/tools/petition/index.js +0 -83
  140. package/build/tools/petition/run.js +0 -66
  141. package/build/tools/round/guidance.js +0 -7
  142. package/build/tools/round/prompt.js +0 -70
  143. package/build/tools/round/report.js +0 -153
  144. package/build/tools/round/worktree.js +0 -55
  145. package/build/tools/round-close/index.js +0 -19
  146. package/build/tools/round-close/run.js +0 -98
  147. package/build/tools/round-open/index.js +0 -20
  148. package/build/tools/round-open/run.js +0 -77
  149. package/build/tools/schema.js +0 -69
  150. package/build/tools/status/index.js +0 -16
  151. package/build/tools/status/read.js +0 -155
  152. package/build/tools/summon/index.js +0 -27
  153. package/build/tools/summon/run.js +0 -80
  154. /package/build/{config/toml.js → agents/harness/events.js} +0 -0
  155. /package/build/{git/diff/contracts.js → cli/render/types.js} +0 -0
  156. /package/build/{protocol/bind-input.js → cli/types.js} +0 -0
  157. /package/build/{protocol → core}/markdown/lex.js +0 -0
  158. /package/build/{protocol → core}/markdown/types.js +0 -0
@@ -0,0 +1,96 @@
1
+ import { FlowError } from "../flow-error.js";
2
+ import { readLedger } from "./ledger.js";
3
+ import { appendRefLogWithRetry } from "./ref-log.js";
4
+ import { patternsOverlap } from "./scope.js";
5
+ import { deriveContractState, isTerminalState } from "./status.js";
6
+ export const REGISTRY_REF = "refs/keiyaku/registry";
7
+ function parseRegistryEntry(message) {
8
+ const body = message.slice(message.indexOf("\n\n") + 2).trim();
9
+ const parsed = JSON.parse(body);
10
+ if (parsed.v !== 1 || !parsed.contract || !["register", "release", "repair"].includes(parsed.kind)) {
11
+ throw new FlowError("INTERNAL_STATE", `invalid registry entry for ${REGISTRY_REF}`);
12
+ }
13
+ return parsed;
14
+ }
15
+ function registrySubject(entry) {
16
+ return `keiyaku registry: ${entry.kind} ${entry.contract}`;
17
+ }
18
+ async function isActiveNonTerminal(cwd, contractId) {
19
+ const ledger = await readLedger(cwd, contractId);
20
+ if (!ledger)
21
+ return false;
22
+ const state = deriveContractState(ledger.entries);
23
+ return state.state !== "corrupt" && !isTerminalState(state);
24
+ }
25
+ async function activeRegistrations(cwd, entries) {
26
+ const released = new Set(entries.filter((entry) => entry.kind === "release").map((entry) => entry.contract));
27
+ const registrations = new Map();
28
+ for (const entry of entries) {
29
+ if (entry.kind === "register" && !released.has(entry.contract)) {
30
+ registrations.set(entry.contract, { scope: entry.scope, tree: entry.tree });
31
+ }
32
+ }
33
+ const active = [];
34
+ for (const [contract, registration] of registrations) {
35
+ if (await isActiveNonTerminal(cwd, contract))
36
+ active.push({ contract, scope: registration.scope, tree: registration.tree });
37
+ }
38
+ return active;
39
+ }
40
+ export async function registerScope(cwd, input) {
41
+ let warnings = [];
42
+ const result = await appendRefLogWithRetry(cwd, REGISTRY_REF, registrySubject, parseRegistryEntry, async (entries) => {
43
+ const nextWarnings = [];
44
+ const active = await activeRegistrations(cwd, entries);
45
+ if (input.exclusive && active.length > 0) {
46
+ const blocking = active.find((existing) => existing.contract !== input.contractId) ?? active[0];
47
+ throw new FlowError("ACTIVE_KEIYAKU_EXISTS", `exclusive bind rejected: active contract ${blocking.contract} already exists`);
48
+ }
49
+ for (const existing of active) {
50
+ if (existing.contract === input.contractId)
51
+ continue;
52
+ if (existing.tree === input.tree) {
53
+ throw new FlowError("ACTIVE_KEIYAKU_EXISTS", [
54
+ `tree is already occupied by active contract ${existing.contract}`,
55
+ "Use `keiyaku bind --worktree ...` to allocate a separate workspace.",
56
+ `Or run \`keiyaku forfeit @${existing.contract}\` to free this tree.`,
57
+ ].join(" "));
58
+ }
59
+ for (const nextPattern of input.scope) {
60
+ for (const existingPattern of existing.scope) {
61
+ if (patternsOverlap(nextPattern, existingPattern)) {
62
+ nextWarnings.push(`▲ scope overlaps registered active contract ${existing.contract}: ${nextPattern} overlaps ${existingPattern}; consider --after ${existing.contract}`);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ warnings = nextWarnings;
68
+ return {
69
+ v: 1,
70
+ kind: "register",
71
+ contract: input.contractId,
72
+ at: input.at,
73
+ actor: input.actor,
74
+ scope: input.scope,
75
+ tree: input.tree,
76
+ };
77
+ });
78
+ if (!result.ok)
79
+ throw new FlowError("INTERNAL_STATE", `failed to register scope for ${input.contractId}: ${result.reason}`);
80
+ return { head: result.head, warnings };
81
+ }
82
+ export async function releaseScope(cwd, input) {
83
+ const result = await appendRefLogWithRetry(cwd, REGISTRY_REF, registrySubject, parseRegistryEntry, (entries) => {
84
+ const hasActiveRegistration = entries.some((entry) => entry.kind === "register" && entry.contract === input.contractId);
85
+ const alreadyReleased = entries.some((entry) => entry.kind === "release" && entry.contract === input.contractId);
86
+ if (!hasActiveRegistration || alreadyReleased)
87
+ return null;
88
+ return { v: 1, kind: "release", contract: input.contractId, at: input.at, actor: input.actor };
89
+ });
90
+ if (!result.ok && result.reason !== "aborted") {
91
+ throw new FlowError("INTERNAL_STATE", `failed to release scope for ${input.contractId}: ${result.reason}`);
92
+ }
93
+ }
94
+ export function bindScope(entry) {
95
+ return entry.data.scope;
96
+ }
@@ -0,0 +1,326 @@
1
+ import { deriveContractState, } from "./status.js";
2
+ const VERB_COLUMN_WIDTH = 11;
3
+ const TIME_COLUMN_WIDTH = 6;
4
+ const ACTOR_COLUMN_WIDTH = 11;
5
+ export function shortCommitHash(hash) {
6
+ return hash.slice(0, 12);
7
+ }
8
+ function compactWhitespace(value) {
9
+ return value.replace(/\s+/g, " ").trim();
10
+ }
11
+ function elapsedLabel(anchorAt, at) {
12
+ const anchorMs = Date.parse(anchorAt);
13
+ const currentMs = Date.parse(at);
14
+ if (!Number.isFinite(anchorMs) || !Number.isFinite(currentMs) || currentMs >= anchorMs) {
15
+ return "now";
16
+ }
17
+ const seconds = Math.max(0, Math.floor((anchorMs - currentMs) / 1_000));
18
+ if (seconds < 60)
19
+ return `${seconds}s`;
20
+ const minutes = Math.floor(seconds / 60);
21
+ if (minutes < 60)
22
+ return `${minutes}m`;
23
+ const hours = Math.floor(minutes / 60);
24
+ if (hours < 24)
25
+ return `${hours}h`;
26
+ return `${Math.floor(hours / 24)}d`;
27
+ }
28
+ function sealAggregate(entries) {
29
+ return entries.reduce((total, entry) => {
30
+ if (entry.kind !== "seal")
31
+ return total;
32
+ return {
33
+ insertions: total.insertions + entry.data.diffstat.insertions,
34
+ deletions: total.deletions + entry.data.diffstat.deletions,
35
+ };
36
+ }, { insertions: 0, deletions: 0 });
37
+ }
38
+ function scopeStatusFact(entry) {
39
+ if (entry.data.scopeAudit.violations.length === 0)
40
+ return "scope ✓ clean";
41
+ if (entry.data.scopeAudit.waived)
42
+ return `scope ▲ waived ${entry.data.scopeAudit.violations.length}`;
43
+ return `scope ✕ ${entry.data.scopeAudit.violations.length}`;
44
+ }
45
+ function formatDiffstat(insertions, deletions) {
46
+ return `+${insertions} −${deletions}`;
47
+ }
48
+ function joinFactClusters(clusters) {
49
+ return clusters.filter((cluster) => Boolean(cluster)).join(" · ");
50
+ }
51
+ function sealsByArc(entries) {
52
+ const seals = new Map();
53
+ for (const entry of entries) {
54
+ if (entry.kind === "seal") {
55
+ seals.set(entry.data.arc, entry);
56
+ }
57
+ }
58
+ return seals;
59
+ }
60
+ function renderOpenArcFact(entry) {
61
+ return joinFactClusters([`open ${compactWhitespace(entry.data.title)}`, compactWhitespace(entry.data.brief)]);
62
+ }
63
+ function renderSealFacts(entry) {
64
+ return [
65
+ joinFactClusters([
66
+ `${entry.data.receipts.length} receipts`,
67
+ formatDiffstat(entry.data.diffstat.insertions, entry.data.diffstat.deletions),
68
+ scopeStatusFact(entry),
69
+ ]),
70
+ ];
71
+ }
72
+ function renderEventFacts(entry, entries) {
73
+ switch (entry.kind) {
74
+ case "bind":
75
+ return [
76
+ joinFactClusters([
77
+ `workspace ${entry.data.workspace}`,
78
+ entry.data.place ? `place ${entry.data.place}` : null,
79
+ `base ${shortCommitHash(entry.data.base)}`,
80
+ `scope ${entry.data.scope.length}`,
81
+ entry.data.after && entry.data.after.length > 0 ? `after ${entry.data.after.length}` : null,
82
+ ]),
83
+ ];
84
+ case "arc":
85
+ return [renderOpenArcFact(entry)];
86
+ case "seal":
87
+ return renderSealFacts(entry);
88
+ case "amend":
89
+ return [compactWhitespace(entry.data.amendment)];
90
+ case "renew":
91
+ return [
92
+ `${shortCommitHash(entry.data.oldBase)}..${shortCommitHash(entry.data.oldHead)} → ${shortCommitHash(entry.data.newBase)}..${shortCommitHash(entry.data.newHead)}`,
93
+ ];
94
+ case "petition":
95
+ return [joinFactClusters([`intent ${entry.data.intent}`]), "┆ 誓言"];
96
+ case "verdict": {
97
+ const seal = entry.data.result === "approved" ? "準" : "却";
98
+ return [`${seal} ${compactWhitespace(entry.data.summary)}`];
99
+ }
100
+ case "claim": {
101
+ const aggregate = sealAggregate(entries);
102
+ return [joinFactClusters([formatDiffstat(aggregate.insertions, aggregate.deletions), `ledger ${entries.length}`])];
103
+ }
104
+ case "forfeit":
105
+ return [
106
+ joinFactClusters([
107
+ entry.data.reason === "manual" ? "裁决人" : entry.data.reason,
108
+ entry.data.note ? `「${compactWhitespace(entry.data.note)}」` : null,
109
+ entry.data.by ? `by ${entry.data.by}` : null,
110
+ ]),
111
+ ];
112
+ }
113
+ }
114
+ function headGlyph(entry) {
115
+ if (entry.kind === "forfeit")
116
+ return "×";
117
+ return "·";
118
+ }
119
+ function renderDisplayRow(row) {
120
+ const [firstFact = "", ...restFacts] = row.facts;
121
+ return [
122
+ `${row.glyph} ${row.verb.padEnd(VERB_COLUMN_WIDTH)}${row.time.padEnd(TIME_COLUMN_WIDTH)}${row.actor.padEnd(ACTOR_COLUMN_WIDTH)}${firstFact}`.trimEnd(),
123
+ ...restFacts,
124
+ ];
125
+ }
126
+ function displayRowForEntry(entry, anchorAt, entries) {
127
+ const actor = entry.kind === "claim" ? "engine" : entry.kind === "petition" ? "holder" : entry.actor;
128
+ const verb = entry.kind === "arc" ? `arc ${entry.data.arc}` : entry.kind;
129
+ return {
130
+ glyph: headGlyph(entry),
131
+ time: elapsedLabel(anchorAt, entry.at),
132
+ actor,
133
+ verb,
134
+ facts: renderEventFacts(entry, entries),
135
+ };
136
+ }
137
+ function displayRowForSeal(entry, anchorAt) {
138
+ return {
139
+ glyph: headGlyph(entry),
140
+ time: elapsedLabel(anchorAt, entry.at),
141
+ actor: entry.actor,
142
+ verb: `arc ${entry.data.arc}`,
143
+ facts: renderSealFacts(entry),
144
+ };
145
+ }
146
+ function renderLedgerRows(entries) {
147
+ const anchorAt = entries.at(-1)?.at ?? entries[0].at;
148
+ const body = [...terminalHeader(entries)];
149
+ const sealedArcs = sealsByArc(entries);
150
+ const newestFirst = [...entries].reverse();
151
+ for (const entry of newestFirst) {
152
+ if (entry.kind === "arc" && sealedArcs.has(entry.data.arc)) {
153
+ continue;
154
+ }
155
+ const row = entry.kind === "seal"
156
+ ? displayRowForSeal(entry, anchorAt)
157
+ : displayRowForEntry(entry, anchorAt, entries);
158
+ body.push(...renderDisplayRow(row));
159
+ }
160
+ return body;
161
+ }
162
+ export function terminalHeader(entries) {
163
+ const terminal = entries.at(-1);
164
+ if (!terminal || terminal.kind !== "claim")
165
+ return [];
166
+ return [`◆ 現世 ${shortCommitHash(terminal.data.mergeCommit)}`, "├─╯"];
167
+ }
168
+ export function renderVerticalLog(input) {
169
+ if (input.entries.length === 0) {
170
+ return "(empty ledger)";
171
+ }
172
+ return renderLedgerRows(input.entries).join("\n");
173
+ }
174
+ export function renderContractView(input) {
175
+ const bind = input.entries.find((entry) => entry.kind === "bind");
176
+ const state = deriveContractState(input.entries);
177
+ if (!bind) {
178
+ return [
179
+ "# Keiyaku Contract",
180
+ "",
181
+ "generated by keiyaku - edits are not read back",
182
+ "",
183
+ `id: ${input.id}`,
184
+ `state: ${state.state}`,
185
+ "",
186
+ "log:",
187
+ renderVerticalLog(input),
188
+ "",
189
+ ].join("\n");
190
+ }
191
+ return [
192
+ `# ${bind.data.name}`,
193
+ "",
194
+ "generated by keiyaku - edits are not read back",
195
+ "",
196
+ `id: ${input.id}`,
197
+ `state: ${state.state}`,
198
+ `base: ${bind.data.base}`,
199
+ `workspace: ${bind.data.workspace}`,
200
+ `place: ${bind.data.place ?? "(none)"}`,
201
+ `after: ${(bind.data.after ?? []).join(", ") || "(none)"}`,
202
+ "",
203
+ "objective:",
204
+ bind.data.objective,
205
+ "",
206
+ "scope:",
207
+ ...bind.data.scope.map((pattern) => `- ${pattern}`),
208
+ "",
209
+ "log:",
210
+ renderVerticalLog(input),
211
+ "",
212
+ ].join("\n");
213
+ }
214
+ function shortHead(hash) {
215
+ if (!hash)
216
+ return "????????";
217
+ return hash.slice(0, 7);
218
+ }
219
+ export function formatAgeMs(ms) {
220
+ if (ms === undefined)
221
+ return undefined;
222
+ const seconds = Math.max(0, Math.floor(ms / 1_000));
223
+ if (seconds < 60)
224
+ return `${seconds}s`;
225
+ const minutes = Math.floor(seconds / 60);
226
+ if (minutes < 60)
227
+ return `${minutes}m`;
228
+ const hours = Math.floor(minutes / 60);
229
+ if (hours < 24)
230
+ return `${hours}h`;
231
+ return `${Math.floor(hours / 24)}d`;
232
+ }
233
+ function arcSparkline(arcs) {
234
+ if (arcs <= 0)
235
+ return "";
236
+ return Array.from({ length: arcs }, () => "·").join("──");
237
+ }
238
+ function pendingAfterIds(contract) {
239
+ return contract.after.filter((item) => !item.terminal).map((item) => item.id);
240
+ }
241
+ function kanshiGlyph(contract) {
242
+ if (contract.state.state === "corrupt")
243
+ return "×";
244
+ if (pendingAfterIds(contract).length > 0)
245
+ return "○";
246
+ if (contract.state.state === "petitioned")
247
+ return "◎";
248
+ return "●";
249
+ }
250
+ export function projectionGlyph(state) {
251
+ switch (state) {
252
+ case "out":
253
+ return "●";
254
+ case "lost":
255
+ return "○";
256
+ case "done":
257
+ return "◎";
258
+ case "dismissed":
259
+ return "×";
260
+ case "dead":
261
+ return "✕";
262
+ case "minting":
263
+ return "…";
264
+ }
265
+ }
266
+ function renderDriftMark(drift) {
267
+ if (drift.state === "drift")
268
+ return `▲${drift.behindBy}`;
269
+ if (drift.state === "orphaned")
270
+ return "▲orphaned";
271
+ return undefined;
272
+ }
273
+ export function renderKanshiBoard(status) {
274
+ const head = shortHead(status.defaultHead);
275
+ if (status.contracts.length === 0) {
276
+ return `現世 ${head} 无在飞契约\n`;
277
+ }
278
+ const header = `現世 ${head}`;
279
+ const lines = [header, ""];
280
+ for (const contract of status.contracts) {
281
+ const glyph = kanshiGlyph(contract);
282
+ const name = contract.slug;
283
+ const spark = arcSparkline(contract.arcs);
284
+ const facts = [];
285
+ const lifecycle = contract.state.state;
286
+ if (lifecycle === "bound" || lifecycle === "active" || lifecycle === "petitioned") {
287
+ facts.push(lifecycle);
288
+ }
289
+ if (contract.place) {
290
+ facts.push(`@${contract.place}`);
291
+ }
292
+ const arcObjective = compactWhitespace(contract.currentArc.objective || "(no arc objective)");
293
+ const arcMark = contract.currentArc.state === "open"
294
+ ? `a${contract.currentArc.number}+`
295
+ : `a${contract.currentArc.number}`;
296
+ facts.push(`${arcMark} ${arcObjective}`);
297
+ facts.push(`next ${contract.nextStep.detail}`);
298
+ const age = formatAgeMs(contract.lastActiveMs);
299
+ if (age)
300
+ facts.push(age);
301
+ if (contract.queuePosition)
302
+ facts.push(`Q${contract.queuePosition}`);
303
+ const drift = renderDriftMark(contract.baseDrift);
304
+ if (drift)
305
+ facts.push(drift);
306
+ if (contract.nextStep.hint)
307
+ facts.push(contract.nextStep.hint);
308
+ const pending = pendingAfterIds(contract);
309
+ if (pending.length > 0) {
310
+ facts.push(`⧗ ${pending.map((id) => {
311
+ const dot = id.indexOf(".");
312
+ return dot > 0 ? id.slice(0, dot) : id;
313
+ }).join(",")}`);
314
+ }
315
+ if (contract.state.state === "corrupt") {
316
+ facts.push(`corrupt (${contract.state.reason ?? contract.corruptReason ?? "unknown"})`);
317
+ }
318
+ if (contract.queueHeadStalled) {
319
+ facts.push("朱 stall 去广场");
320
+ }
321
+ const sparkCol = spark.padEnd(14);
322
+ const factText = facts.join(" · ");
323
+ lines.push(`${glyph} ${name.padEnd(18)} ${sparkCol} ${factText}`.trimEnd());
324
+ }
325
+ return `${lines.join("\n")}\n`;
326
+ }
@@ -0,0 +1,195 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { FlowError } from "../flow-error.js";
4
+ import { createGit, errorContainsAnyPattern, wrapGitError } from "../git/core.js";
5
+ import { KEIYAKU_FILE } from "../keiyaku.js";
6
+ import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
7
+ import { readProjectionStatusBoard } from "./projection-status.js";
8
+ import { renderContractView } from "./render.js";
9
+ import { captureSeal, findOpenArc, receiptsAfterFence, sealOpenArc } from "./seal.js";
10
+ import { deriveContractState, isTerminalState, latestBase, resolveDefaultBranch } from "./status.js";
11
+ import { findContractWorktreePath } from "./worktree-path.js";
12
+ import { getCurrentBranch, isGitRepo } from "../git/branches.js";
13
+ import { resolveCurrentContractId } from "./context.js";
14
+ import { ACTOR_IDENTITY } from "../keiyaku.js";
15
+ import { persistResponseHistory } from "./transcripts.js";
16
+ function assertOperable(contractId, entries) {
17
+ const state = deriveContractState(entries);
18
+ if (state.state === "corrupt") {
19
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} is corrupt at entry ${state.at}: ${state.reason}`);
20
+ }
21
+ if (isTerminalState(state) || state.state === "petitioned") {
22
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot renew from state ${state.state}`);
23
+ }
24
+ }
25
+ async function resolveCommit(cwd, rev) {
26
+ const git = createGit(cwd);
27
+ try {
28
+ return (await git.raw(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
29
+ }
30
+ catch (error) {
31
+ throw wrapGitError(`rev-parse ${rev}`, error, cwd);
32
+ }
33
+ }
34
+ async function renderUpdatedContract(ledgerCwd, renderCwd, contractId) {
35
+ const ledger = await readLedger(ledgerCwd, contractId);
36
+ if (!ledger) {
37
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} active ledger disappeared before render`);
38
+ }
39
+ await fs.writeFile(path.join(renderCwd, KEIYAKU_FILE), renderContractView({ id: contractId, entries: ledger.entries }), "utf8");
40
+ }
41
+ async function rewriteBranch(cwd, newBase, oldBase, branch) {
42
+ const git = createGit(cwd);
43
+ const args = branch
44
+ ? ["rebase", "--onto", newBase, oldBase, branch]
45
+ : ["rebase", "--onto", newBase, oldBase];
46
+ try {
47
+ await git.raw(args);
48
+ }
49
+ catch (error) {
50
+ try {
51
+ await git.raw(["rebase", "--abort"]);
52
+ }
53
+ catch {
54
+ // Preserve the rewrite failure after best-effort restoration.
55
+ }
56
+ if (errorContainsAnyPattern(error, ["CONFLICT", "could not apply", "Resolve all conflicts manually"])) {
57
+ throw new FlowError("CLOSE_MERGE_CONFLICT", "renew conflict; working tree restored", { cause: error });
58
+ }
59
+ throw wrapGitError(`renew ${branch ?? "current branch"}`, error, cwd);
60
+ }
61
+ }
62
+ function assertNoInFlightProjection(cwd, contractId, nowMs) {
63
+ const board = readProjectionStatusBoard(cwd, nowMs);
64
+ if (!board?.rows.some((row) => row.state === "out"))
65
+ return;
66
+ throw new FlowError("INTERNAL_STATE", `akuma still out on ${contractId}'s worktree; rewriting under it would strand its work`, { hints: ["keiyaku status — watch for the akuma to land, then renew"] });
67
+ }
68
+ function looseCommitError(contractId, receipts) {
69
+ return new FlowError("NO_OPEN_ARC", `${receipts.length} commits after fence have no arc to receipt them`, {
70
+ hints: [`keiyaku @${contractId} arc — opens a new arc to receipt them, then renew`],
71
+ });
72
+ }
73
+ export async function renewContract(input) {
74
+ const ledger = await readLedger(input.cwd, input.contractId);
75
+ if (!ledger) {
76
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
77
+ }
78
+ assertOperable(input.contractId, ledger.entries);
79
+ const target = await resolveDefaultBranch(input.cwd);
80
+ if (!target)
81
+ throw new FlowError("INTERNAL_STATE", `cannot renew ${input.contractId}: no default branch`);
82
+ const oldBase = latestBase(ledger.entries);
83
+ if (!oldBase)
84
+ throw new FlowError("INTERNAL_STATE", "ledger must contain bind entry");
85
+ const branch = `keiyaku/${input.contractId}`;
86
+ const oldHead = await resolveCommit(input.cwd, branch);
87
+ const open = findOpenArc(ledger.entries);
88
+ const capture = open ? await captureSeal(input.cwd, ledger.entries, oldHead) : null;
89
+ if (!open) {
90
+ const loose = await receiptsAfterFence(input.cwd, ledger.entries, oldHead);
91
+ if (loose.length > 0)
92
+ throw looseCommitError(input.contractId, loose);
93
+ }
94
+ if (oldBase === target.head) {
95
+ return {
96
+ contractId: input.contractId,
97
+ oldBase,
98
+ newBase: target.head,
99
+ oldHead,
100
+ newHead: oldHead,
101
+ ledgerHead: ledger.head,
102
+ alreadyFresh: true,
103
+ sealed: null,
104
+ };
105
+ }
106
+ const worktreePath = await findContractWorktreePath(input.cwd, input.contractId);
107
+ const rewriteCwd = worktreePath ?? input.cwd;
108
+ assertNoInFlightProjection(rewriteCwd, input.contractId, input.nowMs);
109
+ await rewriteBranch(rewriteCwd, target.head, oldBase, worktreePath ? undefined : branch);
110
+ const newHead = await resolveCommit(input.cwd, branch);
111
+ const sealed = await sealOpenArc({
112
+ cwd: input.cwd,
113
+ renderCwd: rewriteCwd,
114
+ contractId: input.contractId,
115
+ sealedBy: "renew",
116
+ nowMs: input.nowMs,
117
+ capture: capture ? { ...capture, newFence: newHead } : null,
118
+ });
119
+ const at = new Date(input.nowMs).toISOString();
120
+ const result = await appendWithRetry(input.cwd, activeLedgerRef(input.contractId), (_head, entries) => {
121
+ assertOperable(input.contractId, entries);
122
+ if (latestBase(entries) !== oldBase) {
123
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} changed while renewing`);
124
+ }
125
+ return {
126
+ v: 1,
127
+ kind: "renew",
128
+ contract: input.contractId,
129
+ at,
130
+ actor: input.actor,
131
+ data: { oldBase, newBase: target.head, oldHead, newHead },
132
+ };
133
+ });
134
+ if (!result.ok) {
135
+ throw new FlowError("INTERNAL_STATE", `failed to append renew for ${input.contractId}: ${result.reason}`);
136
+ }
137
+ await renderUpdatedContract(input.cwd, rewriteCwd, input.contractId);
138
+ return {
139
+ contractId: input.contractId,
140
+ oldBase,
141
+ newBase: target.head,
142
+ oldHead,
143
+ newHead,
144
+ ledgerHead: result.head,
145
+ alreadyFresh: false,
146
+ sealed,
147
+ };
148
+ }
149
+ export async function renewKeiyaku(input) {
150
+ if (!(await isGitRepo(input.cwd))) {
151
+ throw new FlowError("NOT_GIT_REPO", `${input.cwd} is not a git repository`);
152
+ }
153
+ const resolution = input.contractId ? undefined : await resolveCurrentContractId(input.cwd);
154
+ if (resolution?.status === "none")
155
+ throw new FlowError("EMPTY_PARAM", "no active contract found");
156
+ const contractId = input.contractId ?? resolution.contractId;
157
+ const renewed = await renewContract({ cwd: input.cwd, contractId, ...{ actor: ACTOR_IDENTITY, nowMs: Date.now() } });
158
+ const currentBranch = await getCurrentBranch(input.cwd);
159
+ const target = await resolveDefaultBranch(input.cwd);
160
+ const diff = renewed.sealed?.incrementalDiff ?? "nothing sealed";
161
+ const summary = renewed.alreadyFresh
162
+ ? `Contract ${contractId} is already fresh; nothing touched.`
163
+ : `Contract ${contractId} renewed onto ${renewed.newBase}.`;
164
+ let responsePath;
165
+ try {
166
+ responsePath = await persistResponseHistory({
167
+ cwd: input.cwd,
168
+ tool: "renew",
169
+ title: contractId,
170
+ branch: currentBranch,
171
+ commit: renewed.ledgerHead,
172
+ oldBase: renewed.oldBase,
173
+ newBase: renewed.newBase,
174
+ oldHead: renewed.oldHead,
175
+ newHead: renewed.newHead,
176
+ ledgerCommit: renewed.ledgerHead,
177
+ summary,
178
+ diff,
179
+ ...(renewed.sealed ? { sealed: renewed.sealed } : {}),
180
+ });
181
+ }
182
+ catch {
183
+ responsePath = undefined;
184
+ }
185
+ return {
186
+ status: "success",
187
+ ...renewed,
188
+ currentBranch,
189
+ baseBranch: target?.branch ?? currentBranch,
190
+ diff,
191
+ sealed: renewed.sealed ?? undefined,
192
+ summary,
193
+ responsePath,
194
+ };
195
+ }