@astrosheep/keiyaku 2.8.5 → 2.9.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.
- package/README.md +35 -15
- package/build/.tsbuildinfo +1 -1
- package/build/agents/{call-terms_v2.js → call-terms.js} +12 -8
- package/build/agents/harness/control-types.js +1 -0
- package/build/agents/harness/execution-handle.js +23 -3
- package/build/agents/harness/index.js +6 -607
- package/build/agents/harness/outcome.js +141 -0
- package/build/agents/harness/projection.js +310 -0
- package/build/agents/harness/router.js +201 -0
- package/build/agents/harness/runtime.js +2 -1
- package/build/agents/{launch-snapshot_v2.js → launch-snapshot/model.js} +69 -74
- package/build/agents/launch-snapshot/resolve.js +72 -0
- package/build/agents/opencode-sdk.js +24 -3
- package/build/agents/providers/{claude-agent-sdk.js → claude-agent-sdk/adapter.js} +46 -368
- package/build/agents/providers/claude-agent-sdk/events.js +218 -0
- package/build/agents/providers/claude-agent-sdk/session.js +158 -0
- package/build/agents/providers/codex-app-server/adapter.js +347 -0
- package/build/agents/providers/codex-app-server/events.js +352 -0
- package/build/agents/providers/codex-app-server/session.js +266 -0
- package/build/agents/providers/codex-sdk.js +23 -5
- package/build/agents/providers/opencode-sdk/adapter.js +344 -0
- package/build/agents/providers/opencode-sdk/events.js +354 -0
- package/build/agents/providers/opencode-sdk/session.js +180 -0
- package/build/agents/providers/pi/adapter.js +269 -0
- package/build/agents/providers/pi/checkpoint.js +41 -0
- package/build/agents/providers/pi/events.js +282 -0
- package/build/agents/{selector_v2.js → selector.js} +8 -6
- package/build/cli/commands/akuma/akuma/handler.js +4 -0
- package/build/cli/commands/akuma/akuma/meta.js +16 -0
- package/build/cli/commands/akuma/catalog.js +8 -0
- package/build/cli/commands/akuma/list/handler.js +15 -0
- package/build/cli/commands/akuma/list/meta-ls.js +9 -0
- package/build/cli/commands/akuma/view/handler.js +34 -0
- package/build/cli/commands/akuma/view/meta.js +8 -0
- package/build/cli/commands/akuma.js +1 -1
- package/build/cli/commands/contract/amend/handler.js +10 -0
- package/build/cli/commands/contract/amend/meta.js +12 -0
- package/build/cli/commands/contract/arc/handler.js +9 -0
- package/build/cli/commands/contract/arc/meta.js +26 -0
- package/build/cli/commands/contract/bind/handler.js +23 -0
- package/build/cli/commands/contract/bind/meta.js +30 -0
- package/build/cli/commands/contract/catalog.js +16 -0
- package/build/cli/commands/contract/forfeit/handler.js +13 -0
- package/build/cli/commands/contract/forfeit/meta.js +12 -0
- package/build/cli/commands/contract/log/handler.js +12 -0
- package/build/cli/commands/contract/log/meta.js +12 -0
- package/build/cli/commands/contract/petition/handler.js +26 -0
- package/build/cli/commands/contract/petition/meta.js +23 -0
- package/build/cli/commands/contract/renew/handler.js +10 -0
- package/build/cli/commands/contract/renew/meta.js +17 -0
- package/build/cli/commands/projection/call/handler.js +39 -0
- package/build/cli/commands/projection/call/meta.js +9 -0
- package/build/cli/commands/projection/catalog.js +14 -0
- package/build/cli/commands/projection/kill/handler.js +35 -0
- package/build/cli/commands/projection/kill/meta.js +11 -0
- package/build/cli/commands/projection/revive/handler.js +53 -0
- package/build/cli/commands/projection/revive/meta.js +9 -0
- package/build/cli/commands/projection/status/handler.js +23 -0
- package/build/cli/commands/projection/status/meta.js +9 -0
- package/build/cli/commands/projection/tell/handler.js +34 -0
- package/build/cli/commands/projection/tell/meta.js +9 -0
- package/build/cli/commands/projection/wait/handler.js +21 -0
- package/build/cli/commands/projection/wait/meta.js +11 -0
- package/build/cli/commands/registry.js +100 -0
- package/build/cli/commands/shared.js +71 -0
- package/build/cli/commands/skills/catalog.js +6 -0
- package/build/cli/commands/skills/install/handler.js +10 -0
- package/build/cli/commands/skills/install/meta.js +9 -0
- package/build/cli/commands/skills/skills/handler.js +4 -0
- package/build/cli/commands/skills/skills/meta.js +10 -0
- package/build/cli/commands/system/catalog.js +8 -0
- package/build/cli/commands/system/completion/handler.js +10 -0
- package/build/cli/commands/system/completion/meta.js +9 -0
- package/build/cli/commands/system/dump-env/handler.js +3 -0
- package/build/cli/commands/system/dump-env/meta.js +10 -0
- package/build/cli/commands/system/guide/handler.js +4 -0
- package/build/cli/commands/system/guide/meta.js +9 -0
- package/build/cli/commands/task/add/handler.js +21 -0
- package/build/cli/commands/task/add/meta.js +8 -0
- package/build/cli/commands/task/catalog.js +22 -0
- package/build/cli/commands/task/doctor/handler.js +4 -0
- package/build/cli/commands/task/doctor/meta.js +8 -0
- package/build/cli/commands/task/done/handler.js +17 -0
- package/build/cli/commands/task/done/meta.js +8 -0
- package/build/cli/commands/task/drop/handler.js +6 -0
- package/build/cli/commands/task/drop/meta.js +8 -0
- package/build/cli/commands/task/ls/handler.js +13 -0
- package/build/cli/commands/task/ls/meta.js +8 -0
- package/build/cli/commands/task/shared.js +131 -0
- package/build/cli/commands/task/start/handler.js +6 -0
- package/build/cli/commands/task/start/meta.js +8 -0
- package/build/cli/commands/task/stop/handler.js +17 -0
- package/build/cli/commands/task/stop/meta.js +8 -0
- package/build/cli/commands/task/task/handler.js +5 -0
- package/build/cli/commands/task/task/meta.js +11 -0
- package/build/cli/commands/task/update/handler.js +18 -0
- package/build/cli/commands/task/update/meta.js +8 -0
- package/build/cli/commands/task/view/handler.js +14 -0
- package/build/cli/commands/task/view/meta.js +8 -0
- package/build/cli/completion.js +26 -27
- package/build/cli/flags.js +79 -2
- package/build/cli/help.js +27 -49
- package/build/cli/index.js +121 -374
- package/build/cli/output-stream-error-policy.js +17 -0
- package/build/cli/parse.js +159 -72
- package/build/cli/projection-address.js +5 -0
- package/build/cli/render/arc.js +1 -1
- package/build/cli/render/errors.js +4 -3
- package/build/cli/render/format.js +14 -0
- package/build/cli/render/kanshi.js +106 -0
- package/build/cli/render/petition.js +24 -1
- package/build/cli/render/projection-activity.js +5 -1
- package/build/cli/render/shared.js +2 -2
- package/build/cli/render/status.js +79 -13
- package/build/cli/render/task.js +113 -0
- package/build/cli/render/wait.js +23 -3
- package/build/cli/types.js +1 -1
- package/build/config/{akuma-loader_v2.js → akuma-loader.js} +2 -2
- package/build/config/env.js +9 -4
- package/build/config/settings/disease.js +26 -0
- package/build/config/settings/knobs.js +8 -0
- package/build/config/settings/loader.js +244 -0
- package/build/config/settings/schema.js +276 -0
- package/build/core/addressing.js +1 -1
- package/build/core/amend.js +3 -3
- package/build/core/arc.js +5 -4
- package/build/core/bind-workspace.js +123 -0
- package/build/core/bind.js +75 -155
- package/build/core/call/call.js +224 -0
- package/build/core/call/context.js +238 -0
- package/build/core/call/execution.js +217 -0
- package/build/core/call/prompt.js +42 -0
- package/build/core/call/reference.js +24 -0
- package/build/core/call-persist.js +1 -1
- package/build/core/claim-delivery.js +247 -0
- package/build/core/claim.js +39 -277
- package/build/core/context.js +2 -1
- package/build/core/{render.js → contract-view.js} +12 -140
- package/build/core/draft.js +107 -9
- package/build/core/entry.js +9 -1
- package/build/core/execution-coordinate.js +4 -20
- package/build/core/execution-pact.js +16 -31
- package/build/core/forfeit.js +35 -22
- package/build/core/hints.js +2 -2
- package/build/core/log.js +2 -2
- package/build/core/petition-claim-gates.js +8 -6
- package/build/core/petition-preview.js +97 -0
- package/build/core/petition-run.js +3 -2
- package/build/core/petition.js +39 -26
- package/build/core/projection/generation/database.js +306 -0
- package/build/core/projection/generation/identity.js +84 -0
- package/build/core/projection/generation/model.js +201 -0
- package/build/core/projection/generation/store.js +73 -0
- package/build/core/projection/generation/transitions.js +258 -0
- package/build/core/projection/heart.js +0 -307
- package/build/core/projection/tell/database.js +109 -0
- package/build/core/projection/tell/model.js +97 -0
- package/build/core/{projection-tells.js → projection/tell/store.js} +3 -158
- package/build/core/projection-activity.js +23 -2
- package/build/core/projection-core.js +2 -1
- package/build/core/projection-execution-observer.js +259 -0
- package/build/core/projection-generation-continuation.js +1 -1
- package/build/core/projection-generation-execution.js +90 -20
- package/build/core/projection-generation-launcher.js +1 -1
- package/build/core/projection-generation-runner.js +198 -4
- package/build/core/projection-identity.js +1 -1
- package/build/core/projection-kill.js +50 -17
- package/build/core/projection-life-observer.js +1 -1
- package/build/core/projection-life-protocol.js +10 -0
- package/build/core/projection-mint.js +1 -1
- package/build/core/projection-status.js +45 -32
- package/build/core/projection-wait.js +12 -2
- package/build/core/projection-wake.js +5 -2
- package/build/core/{queue_v2.js → queue.js} +2 -1
- package/build/core/registry.js +1 -1
- package/build/core/renew.js +7 -7
- package/build/core/response-artifact-catalog-sqlite.js +112 -0
- package/build/core/response-artifact-catalog.js +7 -0
- package/build/core/seal.js +44 -16
- package/build/core/status/board.js +286 -0
- package/build/core/status/drift.js +99 -0
- package/build/core/status/lifecycle.js +84 -0
- package/build/core/status/reconciliation.js +131 -0
- package/build/core/stored-agent-event.js +28 -4
- package/build/core/target-ref.js +10 -0
- package/build/core/task/board.js +175 -0
- package/build/core/task/commands.js +211 -0
- package/build/core/task/document.js +189 -0
- package/build/core/task/model.js +1 -0
- package/build/core/task/settlement-git.js +207 -0
- package/build/core/task/settlement-policy.js +82 -0
- package/build/core/task/source-board.js +84 -0
- package/build/core/task/source-model.js +1 -0
- package/build/core/task/validation.js +44 -0
- package/build/core/task-contract.js +224 -0
- package/build/core/task-doctor.js +14 -0
- package/build/core/task-git-actor.js +38 -0
- package/build/core/task-git-runtime.js +34 -0
- package/build/core/task-git-store.js +375 -0
- package/build/core/task-repository.js +1 -0
- package/build/core/task-store-repository.js +86 -0
- package/build/core/task-store.js +1 -0
- package/build/core/task-worktree.js +51 -0
- package/build/core/task.js +4 -0
- package/build/core/transcripts.js +138 -56
- package/build/core/validation.js +17 -0
- package/build/core/verdict.js +2 -2
- package/build/core/worktree-path.js +24 -6
- package/build/flow-error.js +3 -2
- package/build/git/commits.js +3 -0
- package/build/git/diff/parsers.js +0 -94
- package/build/index.js +2 -0
- package/build/keiyaku-constants.js +2 -0
- package/build/keiyaku.js +4 -3
- package/package.json +3 -2
- package/skills/keiyaku/SKILL.md +1 -1
- package/skills/keiyaku-akuma/SKILL.md +11 -6
- package/build/agents/harness/pump.js +0 -158
- package/build/agents/providers/codex-app-server.js +0 -864
- package/build/agents/providers/opencode-sdk.js +0 -363
- package/build/agents/providers/pi.js +0 -557
- package/build/config/settings.js +0 -533
- package/build/core/call.js +0 -686
- package/build/core/projection/generation.js +0 -412
- package/build/core/projection/identity.js +0 -81
- package/build/core/projection/mint.js +0 -174
- package/build/core/projection-generation-store.js +0 -707
- package/build/core/status.js +0 -539
|
@@ -1,15 +1,50 @@
|
|
|
1
|
-
import { formatSettingsDiseases } from "../../config/settings.js";
|
|
2
|
-
import {
|
|
1
|
+
import { formatSettingsDiseases } from "../../config/settings/disease.js";
|
|
2
|
+
import { shortCommitHash } from "../../git/commits.js";
|
|
3
|
+
import { formatAgeMs } from "./format.js";
|
|
4
|
+
import { renderKanshiBoard } from "./kanshi.js";
|
|
3
5
|
import { renderProjectionActivitySummary, } from "./projection-activity.js";
|
|
4
6
|
export const STATUS_DISEASES_SECTION_TITLE = "Diseases";
|
|
5
7
|
export const STATUS_PROJECTION_SECTION_TITLE = "akuma";
|
|
6
8
|
export const STATUS_CLAIM_RECEIPTS_SECTION_TITLE = "claim receipts";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
function projectionGlyph(state) {
|
|
10
|
+
switch (state) {
|
|
11
|
+
case "out":
|
|
12
|
+
return "●";
|
|
13
|
+
case "lost":
|
|
14
|
+
return "○";
|
|
15
|
+
case "done":
|
|
16
|
+
return "◎";
|
|
17
|
+
case "dismissed":
|
|
18
|
+
return "×";
|
|
19
|
+
case "dead":
|
|
20
|
+
return "✕";
|
|
21
|
+
case "minting":
|
|
22
|
+
return "…";
|
|
23
|
+
case "startup-timeout":
|
|
24
|
+
return "!";
|
|
25
|
+
case "unknown":
|
|
26
|
+
return "?";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function renderSettingsDiseasesSection(diseases, taskDiseases = []) {
|
|
30
|
+
if (diseases.length === 0 && taskDiseases.length === 0)
|
|
9
31
|
return "";
|
|
10
32
|
const lines = formatSettingsDiseases(diseases).map((line) => `- ${line}`);
|
|
33
|
+
lines.push(...taskDiseases.map((line) => `- ${line}`));
|
|
11
34
|
return `${STATUS_DISEASES_SECTION_TITLE}\n${lines.join("\n")}\n`;
|
|
12
35
|
}
|
|
36
|
+
function renderTaskStatusSection(tasks) {
|
|
37
|
+
if (!tasks || tasks.inProgress + tasks.ready + tasks.blocked === 0)
|
|
38
|
+
return "";
|
|
39
|
+
const segments = [
|
|
40
|
+
tasks.inProgress > 0 ? `${tasks.inProgress} in_progress` : undefined,
|
|
41
|
+
tasks.ready > 0 ? `${tasks.ready} ready` : undefined,
|
|
42
|
+
tasks.blocked > 0 ? `${tasks.blocked} blocked` : undefined,
|
|
43
|
+
].filter((segment) => segment !== undefined);
|
|
44
|
+
const lines = [`tasks · ${segments.join(" / ")}`];
|
|
45
|
+
lines.push(...tasks.topTasks.map((task) => ` ${task.id.slice(0, 9)} · P${task.pri} · ${task.state === "in_progress" ? "in_progress" : "ready"} · ${task.title}`));
|
|
46
|
+
return `${lines.join("\n")}\n`;
|
|
47
|
+
}
|
|
13
48
|
function renderProjectionDiagnostics(row) {
|
|
14
49
|
if (row.diagnostics.length === 0)
|
|
15
50
|
return "";
|
|
@@ -18,7 +53,7 @@ function renderProjectionDiagnostics(row) {
|
|
|
18
53
|
function renderProjectionOmission(board) {
|
|
19
54
|
if (!board.omitted)
|
|
20
55
|
return undefined;
|
|
21
|
-
const counts = ["failed", "dead", "done", "dismissed"]
|
|
56
|
+
const counts = ["failed", "dead", "done", "killed", "dismissed"]
|
|
22
57
|
.flatMap((state) => {
|
|
23
58
|
const count = board.omitted?.byState[state] ?? 0;
|
|
24
59
|
return count > 0 ? [`${count} ${state}`] : [];
|
|
@@ -28,6 +63,8 @@ function renderProjectionOmission(board) {
|
|
|
28
63
|
function renderProjectionAge(row) {
|
|
29
64
|
if (row.state === "done")
|
|
30
65
|
return "done";
|
|
66
|
+
if (row.state === "killed")
|
|
67
|
+
return "killed";
|
|
31
68
|
if (row.state === "dismissed")
|
|
32
69
|
return "dismissed";
|
|
33
70
|
if (row.state === "failed")
|
|
@@ -50,6 +87,7 @@ function projectionActivityState(row) {
|
|
|
50
87
|
case "failed":
|
|
51
88
|
case "dead": return "dead";
|
|
52
89
|
case "lost": return "lost";
|
|
90
|
+
case "killed":
|
|
53
91
|
case "dismissed": return "dismissed";
|
|
54
92
|
case "minting":
|
|
55
93
|
case "out": return "running";
|
|
@@ -68,11 +106,13 @@ function renderProjectionActivityLine(row) {
|
|
|
68
106
|
});
|
|
69
107
|
}
|
|
70
108
|
function renderProjectionRow(row) {
|
|
71
|
-
const glyph =
|
|
72
|
-
? "
|
|
73
|
-
: row.state === "
|
|
74
|
-
? "
|
|
75
|
-
: row.state
|
|
109
|
+
const glyph = row.state === "killed"
|
|
110
|
+
? "×"
|
|
111
|
+
: projectionGlyph(row.state === "failed"
|
|
112
|
+
? "lost"
|
|
113
|
+
: row.state === "startup-timeout"
|
|
114
|
+
? "minting"
|
|
115
|
+
: row.state);
|
|
76
116
|
const identity = row.alias ? `${row.id} (${row.alias})` : row.id;
|
|
77
117
|
let heading;
|
|
78
118
|
if (row.state === "startup-timeout" || row.state === "unknown") {
|
|
@@ -90,6 +130,25 @@ function renderProjectionRow(row) {
|
|
|
90
130
|
const activity = renderProjectionActivityLine(row);
|
|
91
131
|
return activity ? [heading, activity] : [heading];
|
|
92
132
|
}
|
|
133
|
+
function isTerminalProjectionStatus(row) {
|
|
134
|
+
return row.state === "done"
|
|
135
|
+
|| row.state === "killed"
|
|
136
|
+
|| row.state === "dismissed"
|
|
137
|
+
|| row.state === "failed"
|
|
138
|
+
|| row.state === "dead";
|
|
139
|
+
}
|
|
140
|
+
export function renderProjectionFilteredStatus(board, projectionId) {
|
|
141
|
+
const head = board.defaultHead ? shortCommitHash(board.defaultHead) : "(no HEAD)";
|
|
142
|
+
const heading = `現世 ${head} · projection ${projectionId}`;
|
|
143
|
+
const row = board.projections?.rows.find((candidate) => candidate.id === projectionId);
|
|
144
|
+
if (!row)
|
|
145
|
+
return `${heading}\nno projections on projection ${projectionId}\n`;
|
|
146
|
+
const lines = [heading, "", ...renderProjectionRow(row), ""];
|
|
147
|
+
if (!isTerminalProjectionStatus(row))
|
|
148
|
+
lines.push(`» keiyaku tell ${projectionId} "..."`);
|
|
149
|
+
lines.push(`» keiyaku wait ${projectionId}`);
|
|
150
|
+
return `${lines.join("\n")}\n`;
|
|
151
|
+
}
|
|
93
152
|
export function renderProjectionStatusSection(board) {
|
|
94
153
|
if (!board || board.rows.length === 0)
|
|
95
154
|
return "";
|
|
@@ -116,17 +175,24 @@ export function renderStatusBoard(board) {
|
|
|
116
175
|
const boardText = renderKanshiBoard(board);
|
|
117
176
|
const projectionText = renderProjectionStatusSection(board.projections);
|
|
118
177
|
const receiptsText = renderClaimReconciliationSection(board.claimReconciliation);
|
|
119
|
-
const
|
|
120
|
-
|
|
178
|
+
const taskText = renderTaskStatusSection(board.tasks);
|
|
179
|
+
const taskDiseaseText = [
|
|
180
|
+
...(board.tasks?.diseases ?? []).map((disease) => disease.message),
|
|
181
|
+
...(board.tasks?.diagnostic ? [board.tasks.diagnostic] : []),
|
|
182
|
+
];
|
|
183
|
+
const diseasesText = renderSettingsDiseasesSection(board.diseases, taskDiseaseText);
|
|
184
|
+
if (!projectionText && !receiptsText && !taskText && diseasesText === "")
|
|
121
185
|
return boardText;
|
|
122
186
|
const sections = [boardText.trimEnd()];
|
|
123
|
-
if (board.targetFilter !== null) {
|
|
187
|
+
if (board.targetFilter !== null && (projectionText || receiptsText || diseasesText)) {
|
|
124
188
|
sections.push("repository-wide — not filtered by target");
|
|
125
189
|
}
|
|
126
190
|
if (projectionText)
|
|
127
191
|
sections.push(projectionText.trimEnd());
|
|
128
192
|
if (receiptsText)
|
|
129
193
|
sections.push(receiptsText.trimEnd());
|
|
194
|
+
if (taskText)
|
|
195
|
+
sections.push(taskText.trimEnd());
|
|
130
196
|
if (diseasesText)
|
|
131
197
|
sections.push(diseasesText.trimEnd());
|
|
132
198
|
return `${sections.join("\n\n")}\n`;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export function shortTaskId(id) {
|
|
2
|
+
return id.slice(0, 9);
|
|
3
|
+
}
|
|
4
|
+
export function shortContractId(contractId) {
|
|
5
|
+
const separator = contractId.indexOf(".");
|
|
6
|
+
return separator > 0 ? contractId.slice(0, separator) : contractId;
|
|
7
|
+
}
|
|
8
|
+
export function deriveTaskDisplay(task) {
|
|
9
|
+
if (task.state === "done")
|
|
10
|
+
return "done";
|
|
11
|
+
if (task.state === "drop")
|
|
12
|
+
return "dropped";
|
|
13
|
+
if (task.contractId !== undefined)
|
|
14
|
+
return `bound(${shortContractId(task.contractId)})`;
|
|
15
|
+
if (task.state === "in_progress")
|
|
16
|
+
return "in_progress";
|
|
17
|
+
return task.blocked ? "blocked" : "ready";
|
|
18
|
+
}
|
|
19
|
+
function renderTaskRowWithId(task, id) {
|
|
20
|
+
return `${id} · P${task.pri} · ${deriveTaskDisplay(task)} · ${task.title}`;
|
|
21
|
+
}
|
|
22
|
+
export function renderTaskListRow(task) {
|
|
23
|
+
return renderTaskRowWithId(task, shortTaskId(task.id));
|
|
24
|
+
}
|
|
25
|
+
export function renderTaskMutationRow(task) {
|
|
26
|
+
return renderTaskRowWithId(task, task.id);
|
|
27
|
+
}
|
|
28
|
+
export function renderTaskRow(task, completeId = false) {
|
|
29
|
+
return completeId ? renderTaskMutationRow(task) : renderTaskListRow(task);
|
|
30
|
+
}
|
|
31
|
+
function displayOrder(display) {
|
|
32
|
+
if (display === "in_progress")
|
|
33
|
+
return 0;
|
|
34
|
+
if (display === "ready")
|
|
35
|
+
return 1;
|
|
36
|
+
if (display === "blocked")
|
|
37
|
+
return 2;
|
|
38
|
+
if (display.startsWith("bound("))
|
|
39
|
+
return 3;
|
|
40
|
+
return display === "done" ? 4 : 5;
|
|
41
|
+
}
|
|
42
|
+
function compareText(left, right) {
|
|
43
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
44
|
+
}
|
|
45
|
+
function compareTasks(left, right) {
|
|
46
|
+
const displayDifference = displayOrder(deriveTaskDisplay(left)) - displayOrder(deriveTaskDisplay(right));
|
|
47
|
+
if (displayDifference !== 0)
|
|
48
|
+
return displayDifference;
|
|
49
|
+
if (left.pri !== right.pri)
|
|
50
|
+
return left.pri - right.pri;
|
|
51
|
+
const createdDifference = compareText(left.createdAt, right.createdAt);
|
|
52
|
+
return createdDifference || compareText(left.id, right.id);
|
|
53
|
+
}
|
|
54
|
+
function selected(task, selection) {
|
|
55
|
+
const display = deriveTaskDisplay(task);
|
|
56
|
+
const terminal = display === "done" || display === "dropped";
|
|
57
|
+
if (selection === "done")
|
|
58
|
+
return terminal;
|
|
59
|
+
if (selection === "default")
|
|
60
|
+
return !terminal;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
export function groupAndSortTasks(tasks, selection = "default") {
|
|
64
|
+
return tasks.filter((task) => selected(task, selection)).sort(compareTasks);
|
|
65
|
+
}
|
|
66
|
+
export function renderTaskList(tasks, options = {}) {
|
|
67
|
+
return groupAndSortTasks(tasks, options.selection).map(renderTaskListRow).join("\n");
|
|
68
|
+
}
|
|
69
|
+
function renderIds(ids) {
|
|
70
|
+
return ids.length === 0 ? "(none)" : ids.map(shortTaskId).join(", ");
|
|
71
|
+
}
|
|
72
|
+
export function renderTaskShow(task) {
|
|
73
|
+
const released = new Set(task.releasedNeeds);
|
|
74
|
+
const needs = task.needs.length === 0
|
|
75
|
+
? "(none)"
|
|
76
|
+
: task.needs
|
|
77
|
+
.map((id) => `${shortTaskId(id)}${released.has(id) ? " (dropped)" : ""}`)
|
|
78
|
+
.join(", ");
|
|
79
|
+
return [
|
|
80
|
+
`${shortTaskId(task.id)} · ${task.title}`,
|
|
81
|
+
`state: ${deriveTaskDisplay(task)}`,
|
|
82
|
+
`priority: ${task.pri}`,
|
|
83
|
+
`needs: ${needs}`,
|
|
84
|
+
`blocks: ${renderIds(task.blocks)}`,
|
|
85
|
+
`parent: ${task.parent === null ? "(none)" : shortTaskId(task.parent)}`,
|
|
86
|
+
`from: ${renderIds(task.from)}`,
|
|
87
|
+
`created: ${task.createdAt} by ${task.creator}`,
|
|
88
|
+
"",
|
|
89
|
+
task.body,
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
export function taskJson(task) {
|
|
93
|
+
return {
|
|
94
|
+
id: task.id,
|
|
95
|
+
title: task.title,
|
|
96
|
+
state: task.state,
|
|
97
|
+
display: deriveTaskDisplay(task),
|
|
98
|
+
priority: task.pri,
|
|
99
|
+
needs: [...task.needs],
|
|
100
|
+
blocks: [...task.blocks],
|
|
101
|
+
parent: task.parent,
|
|
102
|
+
from: [...task.from],
|
|
103
|
+
contractId: task.contractId ?? null,
|
|
104
|
+
createdAt: task.createdAt,
|
|
105
|
+
creator: task.creator,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function renderTaskShowJson(task) {
|
|
109
|
+
return `${JSON.stringify({ ...taskJson(task), body: task.body })}\n`;
|
|
110
|
+
}
|
|
111
|
+
export function renderTaskListJson(tasks, selection = "default") {
|
|
112
|
+
return `${JSON.stringify(groupAndSortTasks(tasks, selection).map(taskJson))}\n`;
|
|
113
|
+
}
|
package/build/cli/render/wait.js
CHANGED
|
@@ -10,6 +10,7 @@ function phaseState(phase) {
|
|
|
10
10
|
case "done": return "returned";
|
|
11
11
|
case "failed": return "dead";
|
|
12
12
|
case "lost": return "lost";
|
|
13
|
+
case "killed":
|
|
13
14
|
case "dismissed": return "dismissed";
|
|
14
15
|
case "startup-timeout": return "stalled";
|
|
15
16
|
case "unknown": return "unknown";
|
|
@@ -35,6 +36,8 @@ function waitStateText(snapshot) {
|
|
|
35
36
|
return `returned · lived ${formatTimelineDuration(snapshot.durationMs)}`;
|
|
36
37
|
case "failed":
|
|
37
38
|
return `dead · lived ${formatTimelineDuration(snapshot.durationMs)}`;
|
|
39
|
+
case "killed":
|
|
40
|
+
return "killed";
|
|
38
41
|
case "dismissed":
|
|
39
42
|
return "dismissed";
|
|
40
43
|
case "lost":
|
|
@@ -68,7 +71,7 @@ function terminalDiagnostic(result) {
|
|
|
68
71
|
}
|
|
69
72
|
function ledgerHint(projectionId, snapshot) {
|
|
70
73
|
const file = snapshot.eventLedgerFile ?? "events.jsonl";
|
|
71
|
-
return
|
|
74
|
+
return `· ledger: .keiyaku/projection/${projectionId}/${file}`;
|
|
72
75
|
}
|
|
73
76
|
function recordReviveLines(artifactId) {
|
|
74
77
|
return [
|
|
@@ -98,6 +101,17 @@ function nextActionLines(projectionId, result, state) {
|
|
|
98
101
|
: ["The akuma dropped dead mid-turn.", "no record survived — there is nothing to revive."];
|
|
99
102
|
}
|
|
100
103
|
if (result.state === "terminal" && result.verdict.state === "failed") {
|
|
104
|
+
if (snapshot.failureCode === "SUBAGENT_PROVIDER_UNAVAILABLE") {
|
|
105
|
+
return snapshot.sourceArtifactId
|
|
106
|
+
? [
|
|
107
|
+
"× provider unavailable — no turn accepted",
|
|
108
|
+
`» nothing was consumed · retry from the same artifact: keiyaku revive ${snapshot.sourceArtifactId}`,
|
|
109
|
+
]
|
|
110
|
+
: [
|
|
111
|
+
"× provider unavailable — no turn accepted",
|
|
112
|
+
"» nothing was consumed · safe to call again",
|
|
113
|
+
];
|
|
114
|
+
}
|
|
101
115
|
return [
|
|
102
116
|
"The akuma failed and stopped.",
|
|
103
117
|
...(snapshot.artifactId ? [`record: ${snapshot.artifactId}`] : []),
|
|
@@ -117,7 +131,7 @@ function nextActionLines(projectionId, result, state) {
|
|
|
117
131
|
];
|
|
118
132
|
}
|
|
119
133
|
if (state === "stalled" || state === "unknown") {
|
|
120
|
-
return [ledgerHint(projectionId, snapshot), "
|
|
134
|
+
return [ledgerHint(projectionId, snapshot), "» keiyaku status"];
|
|
121
135
|
}
|
|
122
136
|
return [];
|
|
123
137
|
}
|
|
@@ -186,6 +200,12 @@ export function buildWaitResponse(projectionId, result, options = {}) {
|
|
|
186
200
|
}
|
|
187
201
|
const snapshot = result.snapshot;
|
|
188
202
|
const maxColumns = options.maxColumns ?? resolveLineColumns();
|
|
203
|
+
if (result.state === "terminal" && result.verdict.state === "killed") {
|
|
204
|
+
return {
|
|
205
|
+
...textResponse(`× killed ${projectionId}`),
|
|
206
|
+
exitCode: 1,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
189
209
|
if (snapshot.phase === "startup-timeout") {
|
|
190
210
|
return startupTimeoutResponse(projectionId, result.pact.akuma);
|
|
191
211
|
}
|
|
@@ -216,7 +236,7 @@ export function buildWaitResponse(projectionId, result, options = {}) {
|
|
|
216
236
|
lines.push(" not a word out of it yet — pulse is steady");
|
|
217
237
|
}
|
|
218
238
|
const nextActions = nextActionLines(options.alias ?? projectionId, result, state);
|
|
219
|
-
if (
|
|
239
|
+
if (nextActions.length > 0)
|
|
220
240
|
lines.push("");
|
|
221
241
|
lines.push(...nextActions);
|
|
222
242
|
const response = textResponse(lines.join("\n"));
|
package/build/cli/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const TASK_SUBCOMMANDS = ["add", "view", "ls", "start", "stop", "done", "drop", "update", "doctor"];
|
|
@@ -5,7 +5,7 @@ import { parseToAST } from "../core/markdown/parser.js";
|
|
|
5
5
|
import { isGitRepo } from "../git/branches.js";
|
|
6
6
|
import { BUILTIN_AGENT_PROFILES } from "../keiyaku.js";
|
|
7
7
|
import { getKeiyakuHome } from "./env.js";
|
|
8
|
-
import { agentNameSchema, providerInstanceNameSchema } from "./settings.js";
|
|
8
|
+
import { agentNameSchema, providerInstanceNameSchema } from "./settings/schema.js";
|
|
9
9
|
const nonblankString = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
|
|
10
10
|
const effortOptionsSchema = z.array(nonblankString).superRefine((values, ctx) => {
|
|
11
11
|
if (new Set(values).size !== values.length) {
|
|
@@ -187,7 +187,7 @@ function foldLayer(selected, shadows, allCandidates, source, layer) {
|
|
|
187
187
|
selected[candidate.name] = candidate;
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
export async function
|
|
190
|
+
export async function loadAkumaCatalog(cwd) {
|
|
191
191
|
const candidates = [];
|
|
192
192
|
const selected = {};
|
|
193
193
|
const shadows = [];
|
package/build/config/env.js
CHANGED
|
@@ -4,12 +4,14 @@ import * as path from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { isErrnoException } from "../errno.js";
|
|
6
6
|
import { ENV_KEYS, getEnvDefaultValue } from "./env-keys.js";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { configureResponseArtifactCatalog } from "../core/response-artifact-catalog.js";
|
|
8
|
+
import { createSqliteResponseArtifactCatalog } from "../core/response-artifact-catalog-sqlite.js";
|
|
9
|
+
function readEnvHome(env = process.env) {
|
|
10
|
+
const value = env.KEIYAKU_HOME?.trim();
|
|
9
11
|
return value && value.length > 0 ? value : undefined;
|
|
10
12
|
}
|
|
11
|
-
export function getKeiyakuHome() {
|
|
12
|
-
return readEnvHome() ?? path.join(os.homedir(), ".keiyaku");
|
|
13
|
+
export function getKeiyakuHome(env = process.env) {
|
|
14
|
+
return readEnvHome(env) ?? path.join(os.homedir(), ".keiyaku");
|
|
13
15
|
}
|
|
14
16
|
const DOTENV_FILE = ".env";
|
|
15
17
|
function decodeQuotedValue(raw) {
|
|
@@ -260,6 +262,8 @@ export function initializeConfig(env = process.env, cwd = process.cwd()) {
|
|
|
260
262
|
runtimeEnv: snapshotEnv({ ...dotenvEnv, ...env }),
|
|
261
263
|
});
|
|
262
264
|
activeConfig = runtimeConfig;
|
|
265
|
+
const homeEnv = { ...process.env, ...env };
|
|
266
|
+
configureResponseArtifactCatalog(createSqliteResponseArtifactCatalog({ keiyakuHome: getKeiyakuHome(homeEnv) }));
|
|
263
267
|
return runtimeConfig;
|
|
264
268
|
}
|
|
265
269
|
export function getConfig() {
|
|
@@ -270,4 +274,5 @@ export function getConfig() {
|
|
|
270
274
|
}
|
|
271
275
|
export function resetConfigForTests() {
|
|
272
276
|
activeConfig = undefined;
|
|
277
|
+
configureResponseArtifactCatalog(undefined);
|
|
273
278
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FlowError } from "../../flow-error.js";
|
|
2
|
+
import { SETTINGS_FILE } from "../../keiyaku.js";
|
|
3
|
+
export function findSettingsDiseasesForKnob(diseases, knob) {
|
|
4
|
+
return diseases.filter((disease) => disease.knob === knob);
|
|
5
|
+
}
|
|
6
|
+
export function formatSettingsDiseases(diseases) {
|
|
7
|
+
return diseases.map((disease) => {
|
|
8
|
+
const where = disease.knob ? `${disease.source}:${disease.knob}` : `${disease.source}:(root)`;
|
|
9
|
+
return `${where}: ${disease.reason}`;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Reject when a named knob is diseased and was not admitted into knobs.
|
|
14
|
+
* A healthy value from another source still wins; only failed-and-unset knobs reject.
|
|
15
|
+
*/
|
|
16
|
+
export function assertSettingsKnobUsable(loaded, knob) {
|
|
17
|
+
const knobs = loaded.knobs;
|
|
18
|
+
if (knobs !== null && knobs[knob] !== undefined)
|
|
19
|
+
return;
|
|
20
|
+
const hits = findSettingsDiseasesForKnob(loaded.diseases, knob);
|
|
21
|
+
if (hits.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
throw new FlowError("INVALID_SETTINGS", `Invalid settings in ${SETTINGS_FILE} (diseased knob '${knob}').`, {
|
|
24
|
+
hints: formatSettingsDiseases(hits),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const KNOWN_SETTINGS_KNOBS = ["default", "reviewer", "gate", "defaultBranch", "worktreeBootstrap"];
|
|
2
|
+
export const KNOWN_SETTINGS_ROOT_KEYS = [...KNOWN_SETTINGS_KNOBS, "providers"];
|
|
3
|
+
export function resolveDefaultAgentName(settings) {
|
|
4
|
+
return settings?.default ?? "akuma-codex";
|
|
5
|
+
}
|
|
6
|
+
export function resolveReviewerAgentName(settings) {
|
|
7
|
+
return settings?.reviewer ?? resolveDefaultAgentName(settings);
|
|
8
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { isErrnoException } from "../../errno.js";
|
|
4
|
+
import { SETTINGS_FILE } from "../../keiyaku.js";
|
|
5
|
+
import { getKeiyakuHome } from "../env.js";
|
|
6
|
+
import { KNOWN_SETTINGS_KNOBS, KNOWN_SETTINGS_ROOT_KEYS } from "./knobs.js";
|
|
7
|
+
import { BUILTIN_PROVIDER_INSTANCE_CANDIDATES, knobSchemas, providerInstanceNameSchema, providerInstanceSchema, } from "./schema.js";
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function deepMerge(base, override) {
|
|
12
|
+
if (!isPlainObject(base) || !isPlainObject(override)) {
|
|
13
|
+
return override;
|
|
14
|
+
}
|
|
15
|
+
const merged = { ...base };
|
|
16
|
+
for (const [key, value] of Object.entries(override)) {
|
|
17
|
+
merged[key] = key in merged ? deepMerge(merged[key], value) : value;
|
|
18
|
+
}
|
|
19
|
+
return merged;
|
|
20
|
+
}
|
|
21
|
+
function formatZodIssues(error) {
|
|
22
|
+
return error.issues
|
|
23
|
+
.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
24
|
+
.join("; ");
|
|
25
|
+
}
|
|
26
|
+
async function readSettingsSource(filePath) {
|
|
27
|
+
try {
|
|
28
|
+
const text = await fs.readFile(filePath, "utf-8");
|
|
29
|
+
try {
|
|
30
|
+
return { found: true, raw: JSON.parse(text) };
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const reason = error instanceof SyntaxError
|
|
34
|
+
? `invalid JSON: ${error.message}`
|
|
35
|
+
: error instanceof Error
|
|
36
|
+
? error.message
|
|
37
|
+
: String(error);
|
|
38
|
+
return { found: true, raw: undefined, parseError: reason };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
43
|
+
return { found: false };
|
|
44
|
+
}
|
|
45
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
46
|
+
return { found: true, raw: undefined, parseError: reason };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function builtinProviderInstanceCatalog() {
|
|
50
|
+
return { selected: { ...BUILTIN_PROVIDER_INSTANCE_CANDIDATES }, shadows: [] };
|
|
51
|
+
}
|
|
52
|
+
function formatProviderInstanceIssues(name, error) {
|
|
53
|
+
return error.issues
|
|
54
|
+
.map((issue) => {
|
|
55
|
+
const suffix = issue.path.length === 0 ? "" : `.${issue.path.join(".")}`;
|
|
56
|
+
return `providers.${name}${suffix}: ${issue.message}`;
|
|
57
|
+
})
|
|
58
|
+
.join("; ");
|
|
59
|
+
}
|
|
60
|
+
function collectProviderCandidates(source, rawProviders) {
|
|
61
|
+
if (!isPlainObject(rawProviders)) {
|
|
62
|
+
return {
|
|
63
|
+
candidates: [],
|
|
64
|
+
disease: { source: source === "project" ? "local" : "global", knob: "providers", reason: "providers: must be a JSON object" },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const candidates = [];
|
|
68
|
+
for (const [name, rawInstance] of Object.entries(rawProviders)) {
|
|
69
|
+
const nameResult = providerInstanceNameSchema.safeParse(name);
|
|
70
|
+
if (!nameResult.success) {
|
|
71
|
+
candidates.push({
|
|
72
|
+
name,
|
|
73
|
+
source,
|
|
74
|
+
reason: formatProviderInstanceIssues(name, nameResult.error),
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const instanceResult = providerInstanceSchema.safeParse(rawInstance);
|
|
79
|
+
if (!instanceResult.success) {
|
|
80
|
+
candidates.push({
|
|
81
|
+
name,
|
|
82
|
+
source,
|
|
83
|
+
reason: formatProviderInstanceIssues(name, instanceResult.error),
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const instance = instanceResult.data;
|
|
88
|
+
if (instance.kind === "codex-app-server" && instance.writableRoots !== undefined && source === "global") {
|
|
89
|
+
let relativeIndex;
|
|
90
|
+
for (let i = 0; i < instance.writableRoots.length; i++) {
|
|
91
|
+
if (!path.posix.isAbsolute(instance.writableRoots[i])) {
|
|
92
|
+
relativeIndex = i;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (relativeIndex !== undefined) {
|
|
97
|
+
candidates.push({
|
|
98
|
+
name,
|
|
99
|
+
source,
|
|
100
|
+
reason: `providers.${name}.writableRoots.${relativeIndex}: writableRoots item must be an absolute path for global provider instances`,
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
candidates.push({ name, source, instance: instanceResult.data });
|
|
106
|
+
}
|
|
107
|
+
return { candidates };
|
|
108
|
+
}
|
|
109
|
+
function mergeProviderCandidates(catalog, candidates) {
|
|
110
|
+
const selected = { ...catalog.selected };
|
|
111
|
+
const shadows = [...catalog.shadows];
|
|
112
|
+
for (const candidate of candidates) {
|
|
113
|
+
const prior = selected[candidate.name];
|
|
114
|
+
if (prior !== undefined) {
|
|
115
|
+
shadows.push({ name: candidate.name, source: prior.source, shadowedBy: candidate.source });
|
|
116
|
+
}
|
|
117
|
+
selected[candidate.name] = candidate;
|
|
118
|
+
}
|
|
119
|
+
return { selected, shadows };
|
|
120
|
+
}
|
|
121
|
+
function collectSourceDiseases(source, providerSource, read) {
|
|
122
|
+
if (!read.found) {
|
|
123
|
+
return { partial: {}, diseases: [], providerCandidates: [] };
|
|
124
|
+
}
|
|
125
|
+
if (read.parseError !== undefined) {
|
|
126
|
+
return {
|
|
127
|
+
partial: {},
|
|
128
|
+
diseases: [{ source, reason: `settings root is unreadable (${read.parseError})` }],
|
|
129
|
+
providerCandidates: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (!isPlainObject(read.raw)) {
|
|
133
|
+
return {
|
|
134
|
+
partial: {},
|
|
135
|
+
diseases: [{ source, reason: "settings root must be a JSON object" }],
|
|
136
|
+
providerCandidates: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const diseases = [];
|
|
140
|
+
const partial = {};
|
|
141
|
+
let providerCandidates = [];
|
|
142
|
+
const raw = read.raw;
|
|
143
|
+
for (const key of Object.keys(raw)) {
|
|
144
|
+
if (!KNOWN_SETTINGS_ROOT_KEYS.includes(key)) {
|
|
145
|
+
diseases.push({
|
|
146
|
+
source,
|
|
147
|
+
knob: key,
|
|
148
|
+
reason: key === "agents"
|
|
149
|
+
? 'settings key "agents" has been removed; define profiles in .keiyaku/akuma/<name>.md'
|
|
150
|
+
: `unknown settings key "${key}"`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if ("providers" in raw) {
|
|
155
|
+
const collectedProviders = collectProviderCandidates(providerSource, raw.providers);
|
|
156
|
+
providerCandidates = collectedProviders.candidates;
|
|
157
|
+
if (collectedProviders.disease !== undefined)
|
|
158
|
+
diseases.push(collectedProviders.disease);
|
|
159
|
+
}
|
|
160
|
+
for (const knob of KNOWN_SETTINGS_KNOBS) {
|
|
161
|
+
if (!(knob in raw))
|
|
162
|
+
continue;
|
|
163
|
+
// worktreeBootstrap is project-layer only. Global/user presence is a named
|
|
164
|
+
// disease and never contributes execution authority.
|
|
165
|
+
if (knob === "worktreeBootstrap" && source === "global") {
|
|
166
|
+
diseases.push({
|
|
167
|
+
source,
|
|
168
|
+
knob,
|
|
169
|
+
reason: "worktreeBootstrap is project-only and never supplies a value from global settings",
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const schema = knobSchemas[knob];
|
|
174
|
+
const result = schema.safeParse(raw[knob]);
|
|
175
|
+
if (!result.success) {
|
|
176
|
+
diseases.push({
|
|
177
|
+
source,
|
|
178
|
+
knob,
|
|
179
|
+
reason: formatZodIssues(result.error),
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (result.data === undefined)
|
|
184
|
+
continue;
|
|
185
|
+
partial[knob] = result.data;
|
|
186
|
+
}
|
|
187
|
+
return { partial, diseases, providerCandidates };
|
|
188
|
+
}
|
|
189
|
+
function mergeSettingsKnobs(base, override) {
|
|
190
|
+
// worktreeBootstrap is whole-value project authority; never field-merge it.
|
|
191
|
+
const { worktreeBootstrap: baseBootstrap, ...baseRest } = base;
|
|
192
|
+
const { worktreeBootstrap: overrideBootstrap, ...overrideRest } = override;
|
|
193
|
+
const mergedRaw = deepMerge(baseRest, overrideRest);
|
|
194
|
+
if (!isPlainObject(mergedRaw)) {
|
|
195
|
+
return overrideBootstrap !== undefined
|
|
196
|
+
? { ...override, worktreeBootstrap: overrideBootstrap }
|
|
197
|
+
: { ...override };
|
|
198
|
+
}
|
|
199
|
+
const merged = {};
|
|
200
|
+
for (const knob of KNOWN_SETTINGS_KNOBS) {
|
|
201
|
+
if (knob === "worktreeBootstrap")
|
|
202
|
+
continue;
|
|
203
|
+
if (knob in mergedRaw) {
|
|
204
|
+
merged[knob] = mergedRaw[knob];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const bootstrap = overrideBootstrap ?? baseBootstrap;
|
|
208
|
+
if (bootstrap !== undefined)
|
|
209
|
+
merged.worktreeBootstrap = bootstrap;
|
|
210
|
+
return merged;
|
|
211
|
+
}
|
|
212
|
+
export async function loadKeiyakuSettings(cwd) {
|
|
213
|
+
const localSettingsPath = path.join(cwd, SETTINGS_FILE);
|
|
214
|
+
const globalSettingsPath = path.join(getKeiyakuHome(), path.basename(SETTINGS_FILE));
|
|
215
|
+
let globalRead;
|
|
216
|
+
let localRead;
|
|
217
|
+
try {
|
|
218
|
+
[globalRead, localRead] = await Promise.all([
|
|
219
|
+
readSettingsSource(globalSettingsPath),
|
|
220
|
+
readSettingsSource(localSettingsPath),
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
// loadKeiyakuSettings never throws; unexpected I/O becomes a local root disease.
|
|
225
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
226
|
+
return {
|
|
227
|
+
knobs: null,
|
|
228
|
+
diseases: [{ source: "local", reason: `settings root is unreadable (${reason})` }],
|
|
229
|
+
providerInstances: builtinProviderInstanceCatalog(),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const globalCollected = collectSourceDiseases("global", "global", globalRead);
|
|
233
|
+
const localCollected = collectSourceDiseases("local", "project", localRead);
|
|
234
|
+
const diseases = [...globalCollected.diseases, ...localCollected.diseases];
|
|
235
|
+
let providerInstances = builtinProviderInstanceCatalog();
|
|
236
|
+
providerInstances = mergeProviderCandidates(providerInstances, globalCollected.providerCandidates);
|
|
237
|
+
providerInstances = mergeProviderCandidates(providerInstances, localCollected.providerCandidates);
|
|
238
|
+
let knobs = null;
|
|
239
|
+
if (globalRead.found || localRead.found) {
|
|
240
|
+
knobs = mergeSettingsKnobs(globalCollected.partial, localCollected.partial);
|
|
241
|
+
}
|
|
242
|
+
// Empty object still means "settings files were present"; callers treat null as absent files.
|
|
243
|
+
return { knobs, diseases, providerInstances };
|
|
244
|
+
}
|