@astrosheep/keiyaku 1.0.2 → 2.8.1

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.
@@ -1,11 +1,98 @@
1
- import { resolveProjectionPrefix } from "../core/projection-core.js";
2
- import { FlowError } from "../flow-error.js";
3
- export function resolveProjectionAddress(cwd, prefix, contractId) {
4
- const resolved = resolveProjectionPrefix(cwd, prefix, contractId);
5
- if (resolved.status === "resolved")
6
- return resolved.id;
7
- if (resolved.status === "none") {
8
- throw new FlowError("EMPTY_PARAM", `projection not resolved${contractId ? ` within @${contractId}` : ""}: ${prefix}`);
9
- }
10
- throw new FlowError("EMPTY_PARAM", `projection address is ambiguous${contractId ? ` within @${contractId}` : ""}: ${prefix}; candidates:\n${resolved.candidates.join("\n")}`);
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { AddressResolutionError, buildObjectResolutionAttempt, } from "../core/addressing.js";
4
+ import { resolveProjectionCoordinate } from "../core/projection-coordinate.js";
5
+ import { isProjectionId, projectionDir } from "../core/projection-core.js";
6
+ import { isProjectionAlias, readProjectionAlias } from "../core/projection-alias.js";
7
+ import { readProjectionIdentity } from "../core/projection-identity.js";
8
+ import { isResponseArtifactId } from "../core/response-artifact-id.js";
9
+ import { isErrnoException } from "../errno.js";
10
+ function isRealDirectory(directory) {
11
+ try {
12
+ return fs.lstatSync(directory).isDirectory();
13
+ }
14
+ catch (error) {
15
+ if (isErrnoException(error) && error.code === "ENOENT")
16
+ return false;
17
+ throw error;
18
+ }
19
+ }
20
+ export async function resolveProjectionAddress(input) {
21
+ const workdir = path.resolve(input.cwd);
22
+ // Scoped lookup reuses the commission carrier's repository root: zero Git probes.
23
+ // Unscoped lookup performs exactly one coordinate probe.
24
+ let repo;
25
+ let physicalRoot;
26
+ let attempt;
27
+ if (input.scope) {
28
+ repo = input.scope.repo;
29
+ physicalRoot = input.scope.repo.stableRoot;
30
+ attempt = buildObjectResolutionAttempt({
31
+ repo,
32
+ workdir,
33
+ expected: "projection",
34
+ supplied: input.address,
35
+ scope: {
36
+ commissionId: input.scope.binding.commissionId,
37
+ asTyped: input.scope.spelling.asTyped,
38
+ },
39
+ inference: ["commission-scoped projection lookup"],
40
+ });
41
+ }
42
+ else {
43
+ const coordinate = resolveProjectionCoordinate(input.cwd);
44
+ repo = coordinate.authority.kind === "repository"
45
+ ? { kind: "repository", stableRoot: coordinate.authority.stableRoot }
46
+ : { kind: "user" };
47
+ physicalRoot = coordinate.physicalRoot;
48
+ attempt = buildObjectResolutionAttempt({
49
+ repo,
50
+ workdir,
51
+ expected: "projection",
52
+ supplied: input.address,
53
+ inference: ["projection directory lookup"],
54
+ });
55
+ }
56
+ if (isResponseArtifactId(input.address)) {
57
+ throw new AddressResolutionError("EMPTY_PARAM", `artifact ${input.address} is a response artifact; ${input.verb} expects a projection id or alias`, { ...attempt, actual: "artifact" });
58
+ }
59
+ const suffix = input.scope
60
+ ? `within ${input.scope.spelling.asTyped}`
61
+ : repo.kind === "repository" ? `in repository ${repo.stableRoot}` : `in workdir ${workdir}`;
62
+ let id;
63
+ let alias;
64
+ if (input.address.includes("/")) {
65
+ if (!isProjectionId(input.address))
66
+ throw new AddressResolutionError("EMPTY_PARAM", `projection ${input.address} has no match ${suffix}`, attempt);
67
+ id = input.address;
68
+ }
69
+ else {
70
+ if (!isProjectionAlias(input.address))
71
+ throw new AddressResolutionError("EMPTY_PARAM", `projection alias ${input.address} has no match ${suffix}`, attempt);
72
+ const target = readProjectionAlias(physicalRoot, input.address);
73
+ if (!target)
74
+ throw new AddressResolutionError("EMPTY_PARAM", `projection alias ${input.address} has no match ${suffix}`, attempt);
75
+ id = target;
76
+ alias = input.address;
77
+ }
78
+ const directory = projectionDir(physicalRoot, id);
79
+ if (!isRealDirectory(directory)) {
80
+ const subject = alias ? `projection alias ${alias} points to missing projection ${id}` : `projection ${id}`;
81
+ throw new AddressResolutionError("EMPTY_PARAM", `${subject} has no match ${suffix}`, attempt);
82
+ }
83
+ let identity;
84
+ try {
85
+ identity = readProjectionIdentity(directory);
86
+ }
87
+ catch (error) {
88
+ if (error instanceof Error && /ENOENT|no such file/i.test(error.message)) {
89
+ const subject = alias ? `projection alias ${alias} points to missing projection ${id}` : `projection ${id}`;
90
+ throw new AddressResolutionError("EMPTY_PARAM", `${subject} has no match ${suffix}`, attempt);
91
+ }
92
+ throw error;
93
+ }
94
+ if (input.scope && (identity.binding.kind !== "commission" || identity.binding.commissionId !== input.scope.binding.commissionId)) {
95
+ throw new AddressResolutionError("EMPTY_PARAM", `projection ${id} has no match ${suffix}`, attempt);
96
+ }
97
+ return { id, ...(alias ? { alias } : {}) };
11
98
  }
@@ -25,7 +25,7 @@ export function renderResolvedAddressReceipt(address, facts = {}) {
25
25
  `repo ${address.repo.stableRoot}`,
26
26
  `workdir ${address.workdir}`,
27
27
  facts.artifactId ? `artifact ${facts.artifactId}` : undefined,
28
- facts.projectionId ? `projection ${facts.projectionId}` : undefined,
28
+ facts.projectionId ? `projection ${facts.projectionId}${facts.projectionAlias ? ` (${facts.projectionAlias})` : ""}` : undefined,
29
29
  ].filter((part) => part !== undefined).join(" · ");
30
30
  }
31
31
  const repository = address.repo.kind === "repository"
@@ -37,7 +37,7 @@ export function renderResolvedAddressReceipt(address, facts = {}) {
37
37
  repository,
38
38
  `workdir ${address.workdir}`,
39
39
  facts.artifactId ? `artifact ${facts.artifactId}` : undefined,
40
- facts.projectionId ? `projection ${facts.projectionId}` : undefined,
40
+ facts.projectionId ? `projection ${facts.projectionId}${facts.projectionAlias ? ` (${facts.projectionAlias})` : ""}` : undefined,
41
41
  ].filter((part) => part !== undefined).join(" · ");
42
42
  }
43
43
  return [
@@ -46,7 +46,7 @@ export function renderResolvedAddressReceipt(address, facts = {}) {
46
46
  `repo ${address.repo.stableRoot}`,
47
47
  `target ${address.target.branch}`,
48
48
  facts.artifactId ? `artifact ${facts.artifactId}` : undefined,
49
- facts.projectionId ? `projection ${facts.projectionId}` : undefined,
49
+ facts.projectionId ? `projection ${facts.projectionId}${facts.projectionAlias ? ` (${facts.projectionAlias})` : ""}` : undefined,
50
50
  `via ${sourceLabel(address)}`,
51
51
  ].filter((part) => part !== undefined).join(" · ");
52
52
  }
@@ -61,7 +61,16 @@ function renderAttemptCandidate(candidate) {
61
61
  candidate.matchedBy ? `matched by ${candidate.matchedBy}` : undefined,
62
62
  ].filter((part) => part !== undefined).join(" · ");
63
63
  }
64
+ function isCommissionResolutionAttempt(attempt) {
65
+ return attempt.expected === "commission";
66
+ }
64
67
  export function renderResolutionAttemptPreamble(attempt) {
68
+ if (!isCommissionResolutionAttempt(attempt)) {
69
+ return [
70
+ attempt.repo.kind === "repository" ? `At repo ${attempt.repo.stableRoot}` : "At no repository",
71
+ attempt.scope ? `scope ${attempt.scope.asTyped}` : undefined,
72
+ ].filter((part) => part !== undefined).join(" · ");
73
+ }
65
74
  const parts = [
66
75
  attempt.repo.kind === "repository" ? `At repo ${attempt.repo.stableRoot}` : "At no repository",
67
76
  attempt.spelling ? `address ${attempt.spelling.asTyped}` : undefined,
@@ -2,10 +2,12 @@ import { assembleResponse, buildPayload, buildSection, formatMaybe, formatWarnin
2
2
  import { WARNING_SECTION_TITLE } from "./response-style.js";
3
3
  import { CONTENT_SECTION_TITLE, formatPersistenceSummary, formatCallCompletionHeading, formatCallNextAction, INFO_AKUMA_MAX_CHARS, projectionRejoinHintLines, } from "./shared.js";
4
4
  import { carryAddress, prependAddressReceipt, renderResolvedAddressReceipt } from "./address.js";
5
- export function renderAdoptedLaunchReceipt(address, projectionId) {
5
+ export function renderAdoptedLaunchReceipt(address, projectionId, alias, previousAliasTarget) {
6
6
  return [
7
- renderResolvedAddressReceipt(address, { projectionId }),
8
- ...projectionRejoinHintLines(projectionId),
7
+ renderResolvedAddressReceipt(address, { projectionId, ...(alias ? { projectionAlias: alias } : {}) }),
8
+ ...(alias ? [`alias ${alias} → ${projectionId}`] : []),
9
+ ...(alias && previousAliasTarget ? [`(moved from ${previousAliasTarget}, still running)`] : []),
10
+ ...projectionRejoinHintLines(alias ?? projectionId),
9
11
  ].join("\n");
10
12
  }
11
13
  export function buildCallResponse(result, input) {
@@ -15,8 +17,11 @@ export function buildCallResponse(result, input) {
15
17
  const { session: _session, evidenceCwd: _evidenceCwd, address: _address, foregroundWait: _foregroundWait, ...responseData } = result;
16
18
  const output = {
17
19
  text: input.addressPlacement === "stdout"
18
- ? renderAdoptedLaunchReceipt(result.address, result.projection.id)
19
- : projectionRejoinHintLines(result.projection.id).join("\n"),
20
+ ? renderAdoptedLaunchReceipt(result.address, result.projection.id, result.projection.alias, result.projection.previousAliasTarget)
21
+ : [
22
+ ...(result.projection.alias ? [`alias ${result.projection.alias} → ${result.projection.id}`] : []),
23
+ ...projectionRejoinHintLines(result.projection.alias ?? result.projection.id),
24
+ ].join("\n"),
20
25
  payload: buildPayload(responseData),
21
26
  };
22
27
  switch (input.addressPlacement) {
@@ -135,14 +135,14 @@ export function projectionRejoinHintLines(projectionId) {
135
135
  }
136
136
  export function formatCallNextAction(input) {
137
137
  if (input.projection?.phase === "active") {
138
- return projectionRejoinHintLines(input.projection.id).join("\n");
138
+ return projectionRejoinHintLines(input.projection.alias ?? input.projection.id).join("\n");
139
139
  }
140
140
  if (input.projection?.phase === "done") {
141
141
  return [
142
142
  "The akuma is back.",
143
143
  ...(input.artifactId ? [`record: ${input.artifactId}`] : []),
144
144
  "talk to it:",
145
- ` » keiyaku tell ${input.projection.id} "your words"`,
145
+ ` » keiyaku tell ${input.projection.alias ?? input.projection.id} "your words"`,
146
146
  ].join("\n");
147
147
  }
148
148
  return "⟶ keiyaku status";
@@ -15,6 +15,16 @@ function renderProjectionDiagnostics(row) {
15
15
  return "";
16
16
  return ` · diag ${row.diagnostics.map((diagnostic) => diagnostic.detail).join("; ")}`;
17
17
  }
18
+ function renderProjectionOmission(board) {
19
+ if (!board.omitted)
20
+ return undefined;
21
+ const counts = ["failed", "dead", "done", "dismissed"]
22
+ .flatMap((state) => {
23
+ const count = board.omitted?.byState[state] ?? 0;
24
+ return count > 0 ? [`${count} ${state}`] : [];
25
+ });
26
+ return ` · +${board.omitted.total} not shown (${counts.join(" · ")}) — keiyaku status --all`;
27
+ }
18
28
  function renderProjectionAge(row) {
19
29
  if (row.state === "done")
20
30
  return "done";
@@ -63,18 +73,19 @@ function renderProjectionRow(row) {
63
73
  : row.state === "startup-timeout"
64
74
  ? "minting"
65
75
  : row.state);
76
+ const identity = row.alias ? `${row.id} (${row.alias})` : row.id;
66
77
  let heading;
67
78
  if (row.state === "startup-timeout" || row.state === "unknown") {
68
79
  const akuma = (row.akuma ?? "(unknown)").padEnd(18);
69
- heading = ` ${glyph} ${row.id} ${akuma} ${renderProjectionAge(row)}`.trimEnd();
80
+ heading = ` ${glyph} ${identity} ${akuma} ${renderProjectionAge(row)}${renderProjectionDiagnostics(row)}`.trimEnd();
70
81
  }
71
82
  else if (row.state === "minting") {
72
- heading = ` ${glyph} ${row.id.padEnd(18)} (minting)${renderProjectionDiagnostics(row)}`.trimEnd();
83
+ heading = ` ${glyph} ${identity.padEnd(18)} (minting)${renderProjectionDiagnostics(row)}`.trimEnd();
73
84
  }
74
85
  else {
75
86
  const age = renderProjectionAge(row).padEnd(9);
76
87
  const akuma = (row.akuma ?? "(unknown)").padEnd(18);
77
- heading = ` ${glyph} ${row.id} ${akuma} ${age}${renderProjectionTellCounts(row)}${renderProjectionDiagnostics(row)}`.trimEnd();
88
+ heading = ` ${glyph} ${identity} ${akuma} ${age}${renderProjectionTellCounts(row)}${renderProjectionDiagnostics(row)}`.trimEnd();
78
89
  }
79
90
  const activity = renderProjectionActivityLine(row);
80
91
  return activity ? [heading, activity] : [heading];
@@ -82,7 +93,11 @@ function renderProjectionRow(row) {
82
93
  export function renderProjectionStatusSection(board) {
83
94
  if (!board || board.rows.length === 0)
84
95
  return "";
85
- return `${STATUS_PROJECTION_SECTION_TITLE}\n${board.rows.flatMap(renderProjectionRow).join("\n")}\n`;
96
+ const lines = board.rows.flatMap(renderProjectionRow);
97
+ const omission = renderProjectionOmission(board);
98
+ if (omission)
99
+ lines.push(omission);
100
+ return `${STATUS_PROJECTION_SECTION_TITLE}\n${lines.join("\n")}\n`;
86
101
  }
87
102
  function renderClaimReconciliationSection(recon) {
88
103
  if (!recon)
@@ -3,7 +3,7 @@ import { displayColumns, resolveLineColumns, truncateColumns } from "./line-widt
3
3
  import { projectionRejoinHintLines, textResponse } from "./shared.js";
4
4
  const WAIT_HEADER_MAX_COLUMNS = 60;
5
5
  function shortAddress(projectionId) {
6
- return /-([0-9a-f]{8})$/.exec(projectionId)?.[1] ?? projectionId;
6
+ return /\/([0-9a-f]{8})$/.exec(projectionId)?.[1] ?? projectionId;
7
7
  }
8
8
  function phaseState(phase) {
9
9
  switch (phase) {
@@ -42,7 +42,7 @@ function waitStateText(snapshot) {
42
42
  }
43
43
  }
44
44
  function projectionIdentityParts(projectionId) {
45
- const match = /^(.*)-([0-9a-f]{8})$/.exec(projectionId);
45
+ const match = /^(.*)\/([0-9a-f]{8})$/.exec(projectionId);
46
46
  return match ? { slug: match[1], shortId: match[2] } : { slug: "", shortId: projectionId };
47
47
  }
48
48
  function waitHeader(projectionId, akuma, snapshot, outputColumns) {
@@ -215,7 +215,7 @@ export function buildWaitResponse(projectionId, result, options = {}) {
215
215
  if (rendered.lines.length === 0 && state === "running") {
216
216
  lines.push(" not a word out of it yet — pulse is steady");
217
217
  }
218
- const nextActions = nextActionLines(projectionId, result, state);
218
+ const nextActions = nextActionLines(options.alias ?? projectionId, result, state);
219
219
  if (rendered.hasMutationFiles && nextActions.length > 0)
220
220
  lines.push("");
221
221
  lines.push(...nextActions);
@@ -0,0 +1,39 @@
1
+ const EMPTY_OWNERSHIP = Object.freeze({
2
+ access: Object.freeze([]),
3
+ network: Object.freeze([]),
4
+ webSearch: Object.freeze([]),
5
+ });
6
+ function owns(config, key) {
7
+ return Object.prototype.hasOwnProperty.call(config, key);
8
+ }
9
+ export function nativePolicyOwnership(instanceName, instance) {
10
+ if (instance.kind !== "codex-sdk" && instance.kind !== "codex-app-server") {
11
+ return EMPTY_OWNERSHIP;
12
+ }
13
+ const config = instance.config;
14
+ if (!config)
15
+ return EMPTY_OWNERSHIP;
16
+ const access = [];
17
+ const network = [];
18
+ const webSearch = [];
19
+ const add = (target, key) => {
20
+ target.push({ path: `providers.${instanceName}.config.${key}`, value: config[key] });
21
+ };
22
+ if (owns(config, "sandbox_mode"))
23
+ add(access, "sandbox_mode");
24
+ if (owns(config, "approval_policy"))
25
+ add(access, "approval_policy");
26
+ for (const key of Object.keys(config)) {
27
+ if (key !== "sandbox_workspace_write" && !key.startsWith("sandbox_workspace_write."))
28
+ continue;
29
+ add(access, key);
30
+ add(network, key);
31
+ }
32
+ if (owns(config, "web_search"))
33
+ add(webSearch, "web_search");
34
+ return Object.freeze({
35
+ access: Object.freeze(access),
36
+ network: Object.freeze(network),
37
+ webSearch: Object.freeze(webSearch),
38
+ });
39
+ }
@@ -41,19 +41,9 @@ const CODEX_SDK_FORBIDDEN_THREAD_OPTIONS_KEYS = [
41
41
  const FORBIDDEN_CONFIG_KEYS = [
42
42
  "model",
43
43
  "model_reasoning_effort",
44
- "sandbox_mode",
45
- "sandbox_workspace_write",
46
- "web_search",
47
- "approval_policy",
48
44
  ];
49
45
  function isConfigKeyForbidden(key) {
50
- // Exact match for model, model_reasoning_effort, sandbox_mode, web_search, approval_policy
51
- if (FORBIDDEN_CONFIG_KEYS.includes(key))
52
- return true;
53
- // sandbox_workspace_write also rejects dotted subkeys from adapter-native TOML flattening
54
- if (key.startsWith("sandbox_workspace_write."))
55
- return true;
56
- return false;
46
+ return FORBIDDEN_CONFIG_KEYS.includes(key);
57
47
  }
58
48
  const claudeSettingSourceSchema = z.enum(CLAUDE_SETTING_SOURCES);
59
49
  const writableRootItemSchema = z.string().superRefine((value, ctx) => {
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import { FlowError } from "../flow-error.js";
3
3
  import { listContractIds, readLedger } from "./ledger.js";
4
+ import { isResponseArtifactId } from "./response-artifact-id.js";
4
5
  import { stableRepoRoot } from "./worktree-path.js";
5
6
  import { deriveContractState, isTerminalState, } from "./status.js";
6
7
  function normalizeAddressQuery(raw) {
@@ -66,10 +67,39 @@ export function buildResolutionAttempt(input) {
66
67
  workdir: path.resolve(input.workdir),
67
68
  expected: "commission",
68
69
  ...(input.spelling ? { spelling: input.spelling } : {}),
70
+ ...(input.actual !== undefined ? { actual: input.actual } : {}),
69
71
  candidates: [...(input.candidates ?? [])],
70
72
  inference: [...(input.inference ?? [])],
71
73
  };
72
74
  }
75
+ export function buildObjectResolutionAttempt(input) {
76
+ return {
77
+ repo: input.repo,
78
+ workdir: path.resolve(input.workdir),
79
+ expected: input.expected,
80
+ supplied: input.supplied,
81
+ ...(input.actual !== undefined ? { actual: input.actual } : {}),
82
+ ...(input.scope ? { scope: input.scope } : {}),
83
+ candidates: [...(input.candidates ?? [])],
84
+ inference: [...(input.inference ?? [])],
85
+ };
86
+ }
87
+ export function buildArtifactResolutionAttempt(input) {
88
+ return {
89
+ ...buildObjectResolutionAttempt({
90
+ ...input,
91
+ expected: "artifact",
92
+ }),
93
+ expected: "artifact",
94
+ };
95
+ }
96
+ function commissionSelectorAsTyped(query, source) {
97
+ if (source === "at-address")
98
+ return `@${query}`;
99
+ if (source === "explicit-flag")
100
+ return `--contract ${query}`;
101
+ return query;
102
+ }
73
103
  export class AddressResolutionError extends FlowError {
74
104
  attempt;
75
105
  constructor(code, message, attempt, options) {
@@ -162,63 +192,77 @@ export async function resolveUncommissionedRepositoryAddress(input) {
162
192
  export async function resolveContractAddress(cwd, raw, source = raw?.startsWith("@") ? "at-address" : "explicit-flag") {
163
193
  const workdir = path.resolve(cwd);
164
194
  const repo = await resolvedRepository(cwd);
165
- const candidates = await commissionAddressCandidates(repo.stableRoot);
166
- if (raw === undefined) {
167
- const operable = candidates.filter((candidate) => candidate.active);
195
+ if (raw !== undefined) {
196
+ const query = normalizeAddressQuery(raw);
197
+ const spelling = { asTyped: commissionSelectorAsTyped(query, source), source };
198
+ // Lexical artifact fence: no ledger enumeration for rsp_<ULID>.
199
+ if (isResponseArtifactId(query)) {
200
+ throw new AddressResolutionError("EMPTY_PARAM", `artifact ${query} is a response artifact; expected a commission address`, {
201
+ repo,
202
+ workdir,
203
+ expected: "commission",
204
+ spelling,
205
+ candidates: [],
206
+ inference: [],
207
+ actual: "artifact",
208
+ });
209
+ }
210
+ const candidates = await commissionAddressCandidates(repo.stableRoot);
211
+ const matches = matchCommissionAddress(query, candidates);
168
212
  const attempt = {
169
213
  repo,
170
214
  workdir,
171
215
  expected: "commission",
172
- candidates: attemptCandidates(operable),
173
- inference: ["consulted active commission ledgers"],
216
+ spelling,
217
+ candidates: matches.map((candidate) => ({
218
+ commissionId: candidate.contractId,
219
+ matchedBy: candidate.spelling,
220
+ place: candidate.bind.data.place ?? null,
221
+ name: candidate.bind.data.name,
222
+ objective: candidate.bind.data.objective,
223
+ lifecycle: candidate.lifecycle,
224
+ target: { branch: candidate.bind.data.target },
225
+ })),
226
+ inference: [],
174
227
  };
175
- if (operable.length === 1) {
176
- const candidate = operable[0];
228
+ if (matches.length === 1) {
177
229
  return buildResolvedAddress({
178
230
  repo,
179
231
  workdir,
180
- candidate,
181
- matchedBy: "id",
182
- asTyped: `@${candidate.contractId}`,
183
- source: "sole-active",
184
- inference: attempt.inference,
232
+ candidate: matches[0],
233
+ matchedBy: matches[0].spelling,
234
+ asTyped: attempt.spelling.asTyped,
235
+ source,
236
+ inference: [],
185
237
  });
186
238
  }
187
- throw new AddressResolutionError(operable.length === 0 ? "EMPTY_PARAM" : "CONTRACT_AMBIGUOUS", operable.length === 0
188
- ? "no active commission found; pass @place, @slug, full @commission-id, or --contract <addr>"
189
- : "multiple active commissions found; pass @place, @slug, full @commission-id, or --contract <addr>", attempt);
239
+ if (matches.length === 0) {
240
+ throw new AddressResolutionError("EMPTY_PARAM", `unknown contract address @${query}`, attempt);
241
+ }
242
+ throw new AddressResolutionError("CONTRACT_AMBIGUOUS", `contract address @${query} is ambiguous: ${attempt.candidates.map((candidate) => formatAmbiguousCandidate(query, candidate)).join("; ")}; use a full @commission-id`, attempt);
190
243
  }
191
- const query = normalizeAddressQuery(raw);
192
- const matches = matchCommissionAddress(query, candidates);
244
+ const candidates = await commissionAddressCandidates(repo.stableRoot);
245
+ const operable = candidates.filter((candidate) => candidate.active);
193
246
  const attempt = {
194
247
  repo,
195
248
  workdir,
196
249
  expected: "commission",
197
- spelling: { asTyped: source === "at-address" ? `@${query}` : query, source },
198
- candidates: matches.map((candidate) => ({
199
- commissionId: candidate.contractId,
200
- matchedBy: candidate.spelling,
201
- place: candidate.bind.data.place ?? null,
202
- name: candidate.bind.data.name,
203
- objective: candidate.bind.data.objective,
204
- lifecycle: candidate.lifecycle,
205
- target: { branch: candidate.bind.data.target },
206
- })),
207
- inference: [],
250
+ candidates: attemptCandidates(operable),
251
+ inference: ["consulted active commission ledgers"],
208
252
  };
209
- if (matches.length === 1) {
253
+ if (operable.length === 1) {
254
+ const candidate = operable[0];
210
255
  return buildResolvedAddress({
211
256
  repo,
212
257
  workdir,
213
- candidate: matches[0],
214
- matchedBy: matches[0].spelling,
215
- asTyped: attempt.spelling.asTyped,
216
- source,
217
- inference: [],
258
+ candidate,
259
+ matchedBy: "id",
260
+ asTyped: `@${candidate.contractId}`,
261
+ source: "sole-active",
262
+ inference: attempt.inference,
218
263
  });
219
264
  }
220
- if (matches.length === 0) {
221
- throw new AddressResolutionError("EMPTY_PARAM", `unknown contract address @${query}`, attempt);
222
- }
223
- throw new AddressResolutionError("CONTRACT_AMBIGUOUS", `contract address @${query} is ambiguous: ${attempt.candidates.map((candidate) => formatAmbiguousCandidate(query, candidate)).join("; ")}; use a full @commission-id`, attempt);
265
+ throw new AddressResolutionError(operable.length === 0 ? "EMPTY_PARAM" : "CONTRACT_AMBIGUOUS", operable.length === 0
266
+ ? "no active commission found; pass @place, @slug, full @commission-id, or --contract <addr>"
267
+ : "multiple active commissions found; pass @place, @slug, full @commission-id, or --contract <addr>", attempt);
224
268
  }
@@ -11,6 +11,8 @@ import { buildCallTermsV2 } from "../agents/call-terms_v2.js";
11
11
  import { applySnapshotEffort, resolveAkumaLaunchSnapshot, } from "../agents/launch-snapshot_v2.js";
12
12
  import { restoreOutcome } from "../agents/harness/index.js";
13
13
  import { mintProjection, mintRepositoryProjection } from "./projection-mint.js";
14
+ import { assertProjectionAlias, moveProjectionAlias } from "./projection-alias.js";
15
+ import { projectionDir } from "./projection-core.js";
14
16
  import { waitForProjection } from "./projection-wait.js";
15
17
  import { requireResumableArtifact } from "./transcripts.js";
16
18
  import { appendDebugBlock } from "../telemetry/debug-log.js";
@@ -125,7 +127,7 @@ async function readCallKeiyakuContext(executionCwd, input) {
125
127
  if (input.bare && input.contractId) {
126
128
  const asTyped = input.contractAddressSource === "at-address"
127
129
  ? `@${input.contractId}`
128
- : input.contractId;
130
+ : `--contract ${input.contractId}`;
129
131
  throw new AddressResolutionError("EMPTY_PARAM", `commission address ${asTyped} cannot be combined with --bare`, buildResolutionAttempt({
130
132
  repo: { kind: "user" },
131
133
  workdir: executionCwd,
@@ -138,7 +140,7 @@ async function readCallKeiyakuContext(executionCwd, input) {
138
140
  if (input.contractId) {
139
141
  const asTyped = input.contractAddressSource === "at-address"
140
142
  ? `@${input.contractId}`
141
- : input.contractId;
143
+ : `--contract ${input.contractId}`;
142
144
  throw new AddressResolutionError("EMPTY_PARAM", `${asTyped} is a commission address, but ${ledgerCandidate} has no repository ledger; contract addressing requires a ledger`, buildResolutionAttempt({
143
145
  repo: { kind: "user" },
144
146
  workdir: executionCwd,
@@ -190,7 +192,7 @@ async function readCallKeiyakuContext(executionCwd, input) {
190
192
  });
191
193
  const asTyped = input.contractAddressSource === "at-address"
192
194
  ? `@${input.contractId}`
193
- : input.contractId;
195
+ : `--contract ${input.contractId}`;
194
196
  throw new AddressResolutionError("EMPTY_PARAM", `commission address ${asTyped} cannot be combined with --bare`, buildResolutionAttempt({
195
197
  repo: repositoryAddress.repo,
196
198
  workdir: repositoryAddress.workdir,
@@ -240,6 +242,7 @@ async function readCallKeiyakuContext(executionCwd, input) {
240
242
  catch (error) {
241
243
  if (error instanceof AddressResolutionError
242
244
  && error.code === "EMPTY_PARAM"
245
+ && error.attempt.expected === "commission"
243
246
  && error.attempt.candidates.length === 0) {
244
247
  return {
245
248
  activeKeiyaku: false,
@@ -414,7 +417,7 @@ async function buildCallConfig(agentName, prompt, cwd, options) {
414
417
  idleTimeoutMs: options.idleTimeoutMs,
415
418
  });
416
419
  // Mint projection directory for supervisor pump.
417
- const slug = launchSnapshot.akuma.name.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
420
+ const slug = launchSnapshot.akuma.name;
418
421
  const publishProjection = async () => {
419
422
  const mintInput = {
420
423
  cwd: options.projectionCwd ?? cwd,
@@ -462,7 +465,29 @@ async function buildCallConfig(agentName, prompt, cwd, options) {
462
465
  finally {
463
466
  store.close();
464
467
  }
465
- return { id: minted.id, dir: minted.dir, executionId };
468
+ const previousAliasTarget = options.alias === undefined
469
+ ? undefined
470
+ : moveProjectionAlias(options.projectionCwd ?? cwd, assertProjectionAlias(options.alias), minted.id);
471
+ const previousAliasTargetStillRunning = previousAliasTarget === undefined
472
+ ? undefined
473
+ : (() => {
474
+ try {
475
+ const phase = observeProjectionLife(projectionDir(options.projectionCwd ?? cwd, previousAliasTarget), { nowMs: Date.now(), startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS }).phase;
476
+ return phase === "minting" || phase === "active" || phase === "quiescent"
477
+ ? previousAliasTarget
478
+ : undefined;
479
+ }
480
+ catch {
481
+ return undefined;
482
+ }
483
+ })();
484
+ return {
485
+ id: minted.id,
486
+ dir: minted.dir,
487
+ executionId,
488
+ ...(options.alias ? { alias: options.alias } : {}),
489
+ ...(previousAliasTargetStillRunning ? { previousAliasTarget: previousAliasTargetStillRunning } : {}),
490
+ };
466
491
  };
467
492
  // Every public launch has one durable projection object. Repository-free
468
493
  // authority changes the storage owner, not whether the object exists; mint
@@ -565,6 +590,7 @@ export async function runCall(input) {
565
590
  binding: keiyakuContext.address.binding,
566
591
  title,
567
592
  context: input.context,
593
+ ...(input.mode === "call" && input.alias !== undefined ? { alias: assertProjectionAlias(input.alias) } : {}),
568
594
  };
569
595
  if ((input.mode ?? "call") === "call") {
570
596
  const launched = await launchSubagentGeneration(launchSnapshot.akuma.name, prompt, executionCwd, executionOptions);
@@ -574,6 +600,8 @@ export async function runCall(input) {
574
600
  nowMs: Date.now(),
575
601
  startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
576
602
  }).phase,
603
+ ...(launched.projection.alias ? { alias: launched.projection.alias } : {}),
604
+ ...(launched.projection.previousAliasTarget ? { previousAliasTarget: launched.projection.previousAliasTarget } : {}),
577
605
  };
578
606
  input.onLaunch?.({ address: keiyakuContext.address, projection: adoptedProjection });
579
607
  if (input.detach) {