@astrosheep/keiyaku 2.9.7 → 2.9.8

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 (52) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/harness/event-persistence.js +7 -5
  3. package/build/agents/harness/events.js +3 -2
  4. package/build/agents/providers/codex-app-server/adapter.js +6 -1
  5. package/build/agents/providers/codex-app-server/session.js +8 -7
  6. package/build/agents/selector.js +12 -1
  7. package/build/cli/commands/akuma/view/handler.js +3 -11
  8. package/build/cli/commands/contract/amend/handler.js +1 -1
  9. package/build/cli/commands/contract/amend/meta.js +4 -4
  10. package/build/cli/commands/projection/status/handler.js +5 -4
  11. package/build/cli/commands/projection/status/meta.js +2 -2
  12. package/build/cli/commands/task/add/meta.js +9 -1
  13. package/build/cli/commands/task/shared.js +2 -1
  14. package/build/cli/completion.js +8 -0
  15. package/build/cli/render/kanshi.js +2 -2
  16. package/build/cli/render/path-prefix-compaction.js +119 -76
  17. package/build/cli/render/projection-activity.js +10 -1
  18. package/build/cli/render/shared.js +8 -7
  19. package/build/cli/render/status.js +8 -5
  20. package/build/cli/render/wait.js +1 -1
  21. package/build/config/settings/disease.js +4 -4
  22. package/build/config/settings/loader.js +44 -21
  23. package/build/core/addressing.js +40 -9
  24. package/build/core/amend.js +21 -5
  25. package/build/core/call/context.js +19 -3
  26. package/build/core/call/execution.js +43 -20
  27. package/build/core/ledger-batch.js +194 -0
  28. package/build/core/projection/generation/database.js +22 -0
  29. package/build/core/projection/generation/projection-generation-execution.js +45 -37
  30. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  31. package/build/core/projection/generation/projection-generation-process.js +3 -1
  32. package/build/core/projection/generation/projection-generation-runner.js +76 -40
  33. package/build/core/projection/generation/projection-generation-runtime.js +82 -19
  34. package/build/core/projection/generation/store.js +17 -1
  35. package/build/core/projection/generation/transitions.js +89 -12
  36. package/build/core/projection/index.js +3 -3
  37. package/build/core/projection/projection-kill.js +22 -10
  38. package/build/core/projection/projection-runner-lock.js +177 -37
  39. package/build/core/projection/projection-status.js +143 -55
  40. package/build/core/projection/projection-wake.js +171 -72
  41. package/build/core/status/board.js +42 -4
  42. package/build/core/status/drift.js +21 -5
  43. package/build/core/status/ledger-batch.js +1 -158
  44. package/build/core/task/task-git-runtime.js +8 -10
  45. package/build/core/task/task-git-store.js +5 -3
  46. package/build/core/worktree-path.js +39 -25
  47. package/build/flow-error.js +1 -1
  48. package/build/generated/version.js +2 -2
  49. package/build/git/refs.js +47 -1
  50. package/package.json +1 -1
  51. package/skills/keiyaku-akuma/SKILL.md +18 -0
  52. package/skills/keiyaku-workflow/SKILL.md +68 -13
@@ -1,11 +1,10 @@
1
1
  import { FlowError } from "../../flow-error.js";
2
- import { SETTINGS_FILE } from "../../keiyaku.js";
3
2
  export function findSettingsDiseasesForKnob(diseases, knob) {
4
- return diseases.filter((disease) => disease.knob === knob);
3
+ return diseases.filter((disease) => disease.knob === knob || disease.knob === undefined);
5
4
  }
6
5
  export function formatSettingsDiseases(diseases) {
7
6
  return diseases.map((disease) => {
8
- const where = disease.knob ? `${disease.source}:${disease.knob}` : `${disease.source}:(root)`;
7
+ const where = disease.knob ? `${disease.coordinate}:${disease.knob}` : `${disease.coordinate}:(root)`;
9
8
  return `${where}: ${disease.reason}`;
10
9
  });
11
10
  }
@@ -20,11 +19,12 @@ export function assertSettingsKnobUsable(loaded, knob) {
20
19
  const hits = findSettingsDiseasesForKnob(loaded.diseases, knob);
21
20
  if (hits.length === 0)
22
21
  return;
23
- throw new FlowError("INVALID_SETTINGS", `Invalid settings in ${SETTINGS_FILE} (diseased knob '${knob}').`, {
22
+ throw new FlowError("INVALID_SETTINGS", `Invalid settings for diseased knob '${knob}'.`, {
24
23
  facts: {
25
24
  kind: "invalid_settings",
26
25
  diseases: hits.map((disease) => ({
27
26
  source: disease.source,
27
+ coordinate: disease.coordinate,
28
28
  ...(disease.knob !== undefined ? { knob: disease.knob } : {}),
29
29
  reason: disease.reason,
30
30
  })),
@@ -58,11 +58,11 @@ function formatProviderInstanceIssues(name, error) {
58
58
  })
59
59
  .join("; ");
60
60
  }
61
- function collectProviderCandidates(source, rawProviders) {
61
+ function collectProviderCandidates(settingsSource, providerSource, rawProviders) {
62
62
  if (!isPlainObject(rawProviders)) {
63
63
  return {
64
64
  candidates: [],
65
- disease: { source: source === "project" ? "local" : "global", knob: "providers", reason: "providers: must be a JSON object" },
65
+ disease: { ...settingsSource, knob: "providers", reason: "providers: must be a JSON object" },
66
66
  };
67
67
  }
68
68
  const candidates = [];
@@ -71,7 +71,8 @@ function collectProviderCandidates(source, rawProviders) {
71
71
  if (!nameResult.success) {
72
72
  candidates.push({
73
73
  name,
74
- source,
74
+ source: providerSource,
75
+ coordinate: settingsSource.coordinate,
75
76
  reason: formatProviderInstanceIssues(name, nameResult.error),
76
77
  });
77
78
  continue;
@@ -80,7 +81,8 @@ function collectProviderCandidates(source, rawProviders) {
80
81
  if (!instanceResult.success) {
81
82
  candidates.push({
82
83
  name,
83
- source,
84
+ source: providerSource,
85
+ coordinate: settingsSource.coordinate,
84
86
  reason: formatProviderInstanceIssues(name, instanceResult.error),
85
87
  });
86
88
  continue;
@@ -93,7 +95,7 @@ function collectProviderCandidates(source, rawProviders) {
93
95
  "writableRoots" in instance
94
96
  ? instance.writableRoots
95
97
  : undefined;
96
- if (writableRoots !== undefined && source === "global") {
98
+ if (writableRoots !== undefined && providerSource === "global") {
97
99
  let relativeIndex;
98
100
  for (let i = 0; i < writableRoots.length; i++) {
99
101
  if (!path.posix.isAbsolute(writableRoots[i])) {
@@ -104,13 +106,14 @@ function collectProviderCandidates(source, rawProviders) {
104
106
  if (relativeIndex !== undefined) {
105
107
  candidates.push({
106
108
  name,
107
- source,
109
+ source: providerSource,
110
+ coordinate: settingsSource.coordinate,
108
111
  reason: `providers.${name}.writableRoots.${relativeIndex}: writableRoots item must be an absolute path for global provider instances`,
109
112
  });
110
113
  continue;
111
114
  }
112
115
  }
113
- candidates.push({ name, source, instance: instanceResult.data });
116
+ candidates.push({ name, source: providerSource, coordinate: settingsSource.coordinate, instance: instanceResult.data });
114
117
  }
115
118
  return { candidates };
116
119
  }
@@ -126,21 +129,23 @@ function mergeProviderCandidates(catalog, candidates) {
126
129
  }
127
130
  return { selected, shadows };
128
131
  }
129
- function collectSourceDiseases(source, providerSource, read) {
132
+ function collectSourceDiseases(settingsSource, providerSource, read) {
130
133
  if (!read.found) {
131
- return { partial: {}, diseases: [], providerCandidates: [] };
134
+ return { settingsSource, partial: {}, diseases: [], providerCandidates: [] };
132
135
  }
133
136
  if (read.parseError !== undefined) {
134
137
  return {
138
+ settingsSource,
135
139
  partial: {},
136
- diseases: [{ source, reason: `settings root is unreadable (${read.parseError})` }],
140
+ diseases: [{ ...settingsSource, reason: `settings root is unreadable (${read.parseError})` }],
137
141
  providerCandidates: [],
138
142
  };
139
143
  }
140
144
  if (!isPlainObject(read.raw)) {
141
145
  return {
146
+ settingsSource,
142
147
  partial: {},
143
- diseases: [{ source, reason: "settings root must be a JSON object" }],
148
+ diseases: [{ ...settingsSource, reason: "settings root must be a JSON object" }],
144
149
  providerCandidates: [],
145
150
  };
146
151
  }
@@ -151,7 +156,7 @@ function collectSourceDiseases(source, providerSource, read) {
151
156
  for (const key of Object.keys(raw)) {
152
157
  if (!KNOWN_SETTINGS_ROOT_KEYS.includes(key)) {
153
158
  diseases.push({
154
- source,
159
+ ...settingsSource,
155
160
  knob: key,
156
161
  reason: key === "agents"
157
162
  ? 'settings key "agents" has been removed; define profiles in .keiyaku/akuma/<name>.md'
@@ -160,7 +165,7 @@ function collectSourceDiseases(source, providerSource, read) {
160
165
  }
161
166
  }
162
167
  if ("providers" in raw) {
163
- const collectedProviders = collectProviderCandidates(providerSource, raw.providers);
168
+ const collectedProviders = collectProviderCandidates(settingsSource, providerSource, raw.providers);
164
169
  providerCandidates = collectedProviders.candidates;
165
170
  if (collectedProviders.disease !== undefined)
166
171
  diseases.push(collectedProviders.disease);
@@ -170,9 +175,9 @@ function collectSourceDiseases(source, providerSource, read) {
170
175
  continue;
171
176
  // worktreeBootstrap is project-layer only. Global/user presence is a named
172
177
  // disease and never contributes execution authority.
173
- if (knob === "worktreeBootstrap" && source === "global") {
178
+ if (knob === "worktreeBootstrap" && settingsSource.source === "global") {
174
179
  diseases.push({
175
- source,
180
+ ...settingsSource,
176
181
  knob,
177
182
  reason: "worktreeBootstrap is project-only and never supplies a value from global settings",
178
183
  });
@@ -182,7 +187,7 @@ function collectSourceDiseases(source, providerSource, read) {
182
187
  const result = schema.safeParse(raw[knob]);
183
188
  if (!result.success) {
184
189
  diseases.push({
185
- source,
190
+ ...settingsSource,
186
191
  knob,
187
192
  reason: formatZodIssues(result.error),
188
193
  });
@@ -192,7 +197,7 @@ function collectSourceDiseases(source, providerSource, read) {
192
197
  continue;
193
198
  partial[knob] = result.data;
194
199
  }
195
- return { partial, diseases, providerCandidates };
200
+ return { settingsSource, partial, diseases, providerCandidates };
196
201
  }
197
202
  function mergeSettingsKnobs(base, override) {
198
203
  // worktreeBootstrap is whole-value project authority; never field-merge it.
@@ -217,9 +222,21 @@ function mergeSettingsKnobs(base, override) {
217
222
  merged.worktreeBootstrap = bootstrap;
218
223
  return merged;
219
224
  }
225
+ function collectKnobSources(global, local) {
226
+ const sources = {};
227
+ for (const knob of KNOWN_SETTINGS_KNOBS) {
228
+ if (global.partial[knob] !== undefined)
229
+ sources[knob] = global.settingsSource;
230
+ if (local.partial[knob] !== undefined)
231
+ sources[knob] = local.settingsSource;
232
+ }
233
+ return sources;
234
+ }
220
235
  export async function loadKeiyakuSettings(cwd) {
221
236
  const localSettingsPath = path.join(cwd, SETTINGS_FILE);
222
237
  const globalSettingsPath = path.join(getKeiyakuHome(), path.basename(SETTINGS_FILE));
238
+ const globalSource = { source: "global", coordinate: globalSettingsPath };
239
+ const localSource = { source: "local", coordinate: localSettingsPath };
223
240
  let globalRead;
224
241
  let localRead;
225
242
  try {
@@ -233,12 +250,13 @@ export async function loadKeiyakuSettings(cwd) {
233
250
  const reason = error instanceof Error ? error.message : String(error);
234
251
  return {
235
252
  knobs: null,
236
- diseases: [{ source: "local", reason: `settings root is unreadable (${reason})` }],
253
+ diseases: [{ ...localSource, reason: `settings root is unreadable (${reason})` }],
254
+ knobSources: {},
237
255
  providerInstances: builtinProviderInstanceCatalog(),
238
256
  };
239
257
  }
240
- const globalCollected = collectSourceDiseases("global", "global", globalRead);
241
- const localCollected = collectSourceDiseases("local", "project", localRead);
258
+ const globalCollected = collectSourceDiseases(globalSource, "global", globalRead);
259
+ const localCollected = collectSourceDiseases(localSource, "project", localRead);
242
260
  const diseases = [...globalCollected.diseases, ...localCollected.diseases];
243
261
  let providerInstances = builtinProviderInstanceCatalog();
244
262
  providerInstances = mergeProviderCandidates(providerInstances, globalCollected.providerCandidates);
@@ -248,5 +266,10 @@ export async function loadKeiyakuSettings(cwd) {
248
266
  knobs = mergeSettingsKnobs(globalCollected.partial, localCollected.partial);
249
267
  }
250
268
  // Empty object still means "settings files were present"; callers treat null as absent files.
251
- return { knobs, diseases, providerInstances };
269
+ return {
270
+ knobs,
271
+ diseases,
272
+ knobSources: collectKnobSources(globalCollected, localCollected),
273
+ providerInstances,
274
+ };
252
275
  }
@@ -1,8 +1,8 @@
1
1
  import * as path from "node:path";
2
2
  import { FlowError } from "../flow-error.js";
3
3
  import { errorContainsAnyPattern, NOT_GIT_REPOSITORY_PATTERNS } from "../git/core.js";
4
- import { commissionSlug, formatCommissionCoordinate } from "./ids.js";
5
- import { listContractIds, readLedger } from "./ledger.js";
4
+ import { commissionSlug, formatCommissionCoordinate, isCommissionId } from "./ids.js";
5
+ import { readAllLedgerSnapshots, readLedgerSnapshot } from "./ledger-batch.js";
6
6
  import { isResponseArtifactId } from "./response-artifact-id.js";
7
7
  import { activeContractBoundByEngineWorktree, stableRepoRoot } from "./worktree-path.js";
8
8
  import { assertNoRenewRecoverySession } from "./renew-session.js";
@@ -115,12 +115,8 @@ export class AddressResolutionError extends FlowError {
115
115
  function candidatePlace(candidate) {
116
116
  return candidate.bind.data.workspace === "worktree" ? candidate.bind.data.place ?? null : null;
117
117
  }
118
- async function commissionAddressCandidates(cwd) {
119
- const ids = await listContractIds(cwd);
120
- const ledgers = await Promise.all(ids.map(async (contractId) => readLedger(cwd, contractId)));
118
+ function commissionAddressCandidatesFromLedgers(ledgers) {
121
119
  return ledgers.flatMap((ledger) => {
122
- if (!ledger)
123
- return [];
124
120
  const bind = ledger.entries.find((entry) => entry.kind === "bind");
125
121
  if (!bind || bind.contract !== ledger.contractId)
126
122
  return [];
@@ -133,6 +129,17 @@ async function commissionAddressCandidates(cwd) {
133
129
  }];
134
130
  });
135
131
  }
132
+ async function commissionAddressCandidates(cwd, exactContractId) {
133
+ if (exactContractId) {
134
+ const ledger = await readLedgerSnapshot(cwd, exactContractId);
135
+ return ledger ? commissionAddressCandidatesFromLedgers([ledger]) : [];
136
+ }
137
+ const reads = await readAllLedgerSnapshots(cwd);
138
+ const failed = reads.find((read) => "error" in read);
139
+ if (failed && "error" in failed)
140
+ throw failed.error;
141
+ return commissionAddressCandidatesFromLedgers(reads.filter((read) => !("error" in read)));
142
+ }
136
143
  function matchCommissionAddress(query, candidates) {
137
144
  return candidates.flatMap((candidate) => {
138
145
  const matches = [];
@@ -221,7 +228,7 @@ export async function resolveContractAddress(cwd, raw, source = raw?.startsWith(
221
228
  actual: "artifact",
222
229
  });
223
230
  }
224
- const candidates = await commissionAddressCandidates(repo.stableRoot);
231
+ const candidates = await commissionAddressCandidates(repo.stableRoot, isCommissionId(query) ? query : undefined);
225
232
  const matches = matchCommissionAddress(query, candidates);
226
233
  const attempt = {
227
234
  repo,
@@ -260,7 +267,7 @@ export async function resolveContractAddress(cwd, raw, source = raw?.startsWith(
260
267
  try {
261
268
  const effectiveRepo = await stableRepoRoot(workdir);
262
269
  if (effectiveRepo === repo.stableRoot) {
263
- boundContractId = await activeContractBoundByEngineWorktree(workdir);
270
+ boundContractId = await activeContractBoundByEngineWorktree(workdir, effectiveRepo);
264
271
  }
265
272
  }
266
273
  catch (error) {
@@ -305,6 +312,30 @@ export async function resolveContractAddress(cwd, raw, source = raw?.startsWith(
305
312
  ? "no active commission found; pass @place, @slug, full @commission-id, or --contract <addr>"
306
313
  : "multiple active commissions found; pass @place, @slug, full @commission-id, or --contract <addr>", attempt);
307
314
  }
315
+ /**
316
+ * `call` may inherit only the active commission proven by the selected
317
+ * engine-owned worktree. Unlike existing-contract resolution, this never
318
+ * considers sole-active cardinality.
319
+ */
320
+ export async function resolveCallWorktreeAddress(cwd, effectiveDirectory = cwd) {
321
+ const repo = await resolvedRepository(cwd);
322
+ const workdir = path.resolve(effectiveDirectory);
323
+ let contractId;
324
+ try {
325
+ const effectiveRepo = await stableRepoRoot(workdir);
326
+ if (effectiveRepo !== repo.stableRoot)
327
+ return undefined;
328
+ contractId = await activeContractBoundByEngineWorktree(workdir, effectiveRepo);
329
+ }
330
+ catch (error) {
331
+ if (!errorContainsAnyPattern(error, NOT_GIT_REPOSITORY_PATTERNS))
332
+ throw error;
333
+ return undefined;
334
+ }
335
+ if (!contractId)
336
+ return undefined;
337
+ return await resolveContractAddress(repo.stableRoot, contractId, "worktree-binding", workdir);
338
+ }
308
339
  export async function resolveExistingContractAddress(cwd, raw, source = raw?.startsWith("@") ? "at-address" : "explicit-flag", effectiveDirectory = cwd) {
309
340
  const address = await resolveContractAddress(cwd, raw, source, effectiveDirectory);
310
341
  try {
@@ -5,7 +5,7 @@ import { refreshContractView } from "./contract-view.js";
5
5
  import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
6
6
  import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
7
7
  import { resolveDefaultBranch } from "./status/drift.js";
8
- import { markdownSourceSections, normalizeScopePattern, validateScopeFragmentLines } from "./scope.js";
8
+ import { markdownSourceSections, normalizeScopePattern, ScopePatternError, validateScopeFragmentLines } from "./scope.js";
9
9
  import { normalizeSectionTitle } from "./markdown/titles.js";
10
10
  import { resolveContractDeliveryCwd } from "./worktree-path.js";
11
11
  function amendScopeHeading(name) {
@@ -44,9 +44,25 @@ function scopePatterns(markdown, section) {
44
44
  emptyMessage: `amend input has an empty ${amendScopeHeading(section.name)} section`,
45
45
  });
46
46
  }
47
- export function buildAmendInput(markdown, cwd, contractId, contractAddressSource) {
47
+ function normalizeAmendScopePatterns(patterns) {
48
+ try {
49
+ return patterns.map((pattern) => normalizeScopePattern(pattern));
50
+ }
51
+ catch (error) {
52
+ if (error instanceof ScopePatternError) {
53
+ throw new FlowError("INVALID_KEIYAKU_DRAFT", error.message);
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+ export function buildAmendInput(markdown, cwd, contractId, contractAddressSource, appendScope) {
48
59
  const { sections, prose } = parseAmendScopeSections(markdown);
49
- const append = sections.append ? scopePatterns(markdown, sections.append) : [];
60
+ if (sections.append && appendScope && appendScope.length > 0) {
61
+ throw new FlowError("INVALID_KEIYAKU_DRAFT", "amend input may use either --append-scope or ## Scope Append, not both");
62
+ }
63
+ const append = sections.append
64
+ ? scopePatterns(markdown, sections.append)
65
+ : normalizeAmendScopePatterns(appendScope ?? []);
50
66
  if (!prose.trim()) {
51
67
  throw new FlowError("EMPTY_PARAM", "amendment cannot be empty");
52
68
  }
@@ -55,7 +71,7 @@ export function buildAmendInput(markdown, cwd, contractId, contractAddressSource
55
71
  ...(contractId ? { contractId } : {}),
56
72
  ...(contractAddressSource ? { contractAddressSource } : {}),
57
73
  amendment: prose,
58
- ...(sections.append ? { scopeDelta: { add: append } } : {}),
74
+ ...(append.length > 0 ? { scopeDelta: { add: append } } : {}),
59
75
  };
60
76
  }
61
77
  function requireAmendment(value) {
@@ -76,7 +92,7 @@ function normalizeScopeDelta(scopeDelta) {
76
92
  if (!scopeDelta)
77
93
  return undefined;
78
94
  // Scope patterns are gitignore text; do not requireText/trim, or escaped trailing spaces die.
79
- const add = scopeDelta.add.map((pattern) => normalizeScopePattern(pattern));
95
+ const add = normalizeAmendScopePatterns(scopeDelta.add);
80
96
  if (add.length === 0)
81
97
  return undefined;
82
98
  return { add };
@@ -2,7 +2,7 @@ import * as fs from "fs/promises";
2
2
  import { FlowError } from "../../flow-error.js";
3
3
  import { assertGitObservation, observeGitRepository } from "../../git/branches.js";
4
4
  import { createGit, wrapGitError } from "../../git/core.js";
5
- import { AddressResolutionError, buildResolutionAttempt, buildUncommissionedAddress, buildUncommissionedRepositoryAddress, resolveContractAddress, withResolvedAddress, } from "../addressing.js";
5
+ import { AddressResolutionError, buildResolutionAttempt, buildUncommissionedAddress, buildUncommissionedRepositoryAddress, resolveContractAddress, resolveCallWorktreeAddress, withResolvedAddress, } from "../addressing.js";
6
6
  import { normalizeFilesystemCoordinate } from "../../fs/path-coordinate.js";
7
7
  import { findOpenArcView } from "../arc.js";
8
8
  import { readLedger } from "../ledger.js";
@@ -139,7 +139,24 @@ export async function readCallKeiyakuContext(executionCwd, input) {
139
139
  },
140
140
  }));
141
141
  }
142
- if (input.bare || !input.contractId) {
142
+ if (input.bare) {
143
+ return {
144
+ activeKeiyaku: false,
145
+ effectiveCwd: executionCwd,
146
+ historyCwd: ledgerCwd,
147
+ profileCwd: ledgerCwd,
148
+ projectionCwd: ledgerCwd,
149
+ address: buildUncommissionedRepositoryAddress({
150
+ stableRoot: ledgerCwd,
151
+ workdir: executionCwd,
152
+ binding: "bare",
153
+ }),
154
+ };
155
+ }
156
+ const address = input.contractId
157
+ ? await resolveContractAddress(ledgerCwd, input.contractId, input.contractAddressSource)
158
+ : await resolveCallWorktreeAddress(ledgerCwd, executionCwd);
159
+ if (!address) {
143
160
  return {
144
161
  activeKeiyaku: false,
145
162
  effectiveCwd: executionCwd,
@@ -153,7 +170,6 @@ export async function readCallKeiyakuContext(executionCwd, input) {
153
170
  }),
154
171
  };
155
172
  }
156
- const address = await resolveContractAddress(ledgerCwd, input.contractId, input.contractAddressSource);
157
173
  return await withResolvedAddress(address, async () => {
158
174
  const contractId = address.binding.commissionId;
159
175
  const ledger = await readLedger(ledgerCwd, contractId);
@@ -7,7 +7,7 @@ import { assertProjectionAlias, moveProjectionAlias } from "../projection/index.
7
7
  import { projectionDir } from "../projection/index.js";
8
8
  import { createProjectionExecutionId } from "../projection/index.js";
9
9
  import { PROJECTION_ADOPTION_TIMEOUT_MS, startProjectionGeneration } from "../projection/index.js";
10
- import { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, } from "../projection/index.js";
10
+ import { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, acquireProjectionRunnerLock, } from "../projection/index.js";
11
11
  import { observeProjectionLife } from "../projection/index.js";
12
12
  import { mintProjection, mintRepositoryProjection } from "../projection/index.js";
13
13
  import { waitForProjection } from "../projection/index.js";
@@ -60,13 +60,29 @@ export function outcomeFromGeneration(current) {
60
60
  return restoreOutcome(record);
61
61
  }
62
62
  export async function executeSubagent(agentName, prompt, cwd, options) {
63
- const { projection: projectionCoordinate, launchSnapshot } = await buildCallConfig(agentName, prompt, cwd, options);
63
+ const { projection: projectionCoordinate, launchSnapshot, launchFacts } = await buildCallConfig(agentName, prompt, cwd, options);
64
64
  if (!projectionCoordinate) {
65
65
  throw new FlowError("INTERNAL_STATE", "durable execution requires a projection coordinate");
66
66
  }
67
- const launched = await startProjectionGeneration(projectionCoordinate.dir, projectionCoordinate.executionId);
68
- if (launched.status === "failed") {
69
- throw new FlowError("SUBAGENT_EXEC_ERROR", launched.detail);
67
+ const launchOwnership = acquireProjectionRunnerLock(projectionCoordinate.dir);
68
+ const launched = await startProjectionGeneration(projectionCoordinate.dir, projectionCoordinate.executionId, {
69
+ commitLaunch: () => {
70
+ const store = openProjectionGenerationStore(projectionCoordinate.dir);
71
+ try {
72
+ return store.launchIfSettled({ executionId: projectionCoordinate.executionId, facts: launchFacts }).status === "committed";
73
+ }
74
+ finally {
75
+ store.close();
76
+ }
77
+ },
78
+ launchOwnership,
79
+ });
80
+ launchOwnership.close();
81
+ if (launched.status !== "adopted") {
82
+ const detail = launched.status === "pending"
83
+ ? "runner retained startup ownership without adopting before the bounded launch wait"
84
+ : launched.detail;
85
+ throw new FlowError("SUBAGENT_EXEC_ERROR", detail);
70
86
  }
71
87
  const joined = await waitForProjection({
72
88
  projectionDirectory: projectionCoordinate.dir,
@@ -111,13 +127,28 @@ export async function executeSubagent(agentName, prompt, cwd, options) {
111
127
  throw new FlowError("INTERNAL_STATE", "Unhandled subagent outcome state.");
112
128
  }
113
129
  export async function launchSubagentGeneration(agentName, prompt, cwd, options) {
114
- const { projection, launchSnapshot } = await buildCallConfig(agentName, prompt, cwd, options);
130
+ const { projection, launchSnapshot, launchFacts } = await buildCallConfig(agentName, prompt, cwd, options);
115
131
  if (!projection) {
116
132
  throw new FlowError("INTERNAL_STATE", "durable launch requires a projection coordinate");
117
133
  }
118
- const launched = await startProjectionGeneration(projection.dir, projection.executionId);
119
- if (launched.status === "failed") {
120
- throw new FlowError("SUBAGENT_EXEC_ERROR", launched.detail);
134
+ const launchOwnership = acquireProjectionRunnerLock(projection.dir);
135
+ const launched = await startProjectionGeneration(projection.dir, projection.executionId, {
136
+ commitLaunch: () => {
137
+ const store = openProjectionGenerationStore(projection.dir);
138
+ try {
139
+ return store.launchIfSettled({ executionId: projection.executionId, facts: launchFacts }).status === "committed";
140
+ }
141
+ finally {
142
+ store.close();
143
+ }
144
+ },
145
+ launchOwnership,
146
+ });
147
+ launchOwnership.close();
148
+ if (launched.status !== "adopted") {
149
+ throw new FlowError("SUBAGENT_EXEC_ERROR", launched.status === "pending"
150
+ ? "runner retained startup ownership without adopting before the bounded launch wait"
151
+ : launched.detail);
121
152
  }
122
153
  return { projection, launchSnapshot };
123
154
  }
@@ -173,16 +204,6 @@ export async function buildCallConfig(agentName, prompt, cwd, options) {
173
204
  } : {}),
174
205
  },
175
206
  }));
176
- const store = openProjectionGenerationStore(minted.dir);
177
- try {
178
- const launch = store.launchIfSettled({ executionId, facts: launchFacts });
179
- if (launch.status !== "committed") {
180
- throw new FlowError("INTERNAL_STATE", `initial generation launch rejected: ${launch.reason}`);
181
- }
182
- }
183
- finally {
184
- store.close();
185
- }
186
207
  const previousAliasTarget = options.alias === undefined
187
208
  ? undefined
188
209
  : await moveProjectionAlias(options.projectionCwd ?? cwd, assertProjectionAlias(options.alias), minted.id);
@@ -202,6 +223,7 @@ export async function buildCallConfig(agentName, prompt, cwd, options) {
202
223
  id: minted.id,
203
224
  dir: minted.dir,
204
225
  executionId,
226
+ launchFacts,
205
227
  ...(options.alias ? { alias: options.alias } : {}),
206
228
  ...(previousAliasTargetStillRunning ? { previousAliasTarget: previousAliasTargetStillRunning } : {}),
207
229
  };
@@ -209,9 +231,10 @@ export async function buildCallConfig(agentName, prompt, cwd, options) {
209
231
  // Every public launch has one durable projection object. Repository-free
210
232
  // authority changes the storage owner, not whether the object exists; mint
211
233
  // failure is therefore a launch failure rather than an in-process fallback.
212
- const projectionCoordinate = await publishProjection();
234
+ const { launchFacts, ...projectionCoordinate } = await publishProjection();
213
235
  return {
214
236
  projection: projectionCoordinate,
215
237
  launchSnapshot,
238
+ launchFacts,
216
239
  };
217
240
  }