@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
@@ -0,0 +1,194 @@
1
+ import { wrapGitError, runGitProcess } from "../git/core.js";
2
+ import { GitStreamingBatchFramingError, GitStreamingBatchResponseEndedError, openGitStreamingBatch, } from "../git/streaming-batch.js";
3
+ import { parseLedgerEntryCommitMessage } from "./entry.js";
4
+ import { ACTIVE_LEDGER_REF_PREFIX, activeLedgerRef, } from "./ledger.js";
5
+ const MAX_LEDGER_COMMIT_BYTES = 1024 * 1024;
6
+ async function runLedgerGit(cwd, args) {
7
+ const result = await runGitProcess(cwd, args);
8
+ if (result.error || result.status !== 0) {
9
+ throw wrapGitError(args.join(" "), {
10
+ message: result.error?.message
11
+ ?? `git exited with status ${result.status ?? "unknown"}`,
12
+ stderr: result.stderr,
13
+ stdout: result.stdout,
14
+ }, cwd);
15
+ }
16
+ return Buffer.from(result.stdout, "utf8");
17
+ }
18
+ async function queryBatchObject(batch, expression) {
19
+ const record = await batch.request(expression, {
20
+ maxContentBytes: MAX_LEDGER_COMMIT_BYTES,
21
+ });
22
+ if (record.kind === "missing")
23
+ return null;
24
+ if (record.kind === "ambiguous") {
25
+ throw new Error(`[git cat-file ${expression}] object is ambiguous`);
26
+ }
27
+ return {
28
+ oid: record.record.oid,
29
+ type: record.record.type,
30
+ content: record.record.content,
31
+ };
32
+ }
33
+ function parseCommitObject(object) {
34
+ if (object.type !== "commit") {
35
+ throw new Error(`git cat-file expected commit ${object.oid}, received ${object.type}`);
36
+ }
37
+ const content = object.content.toString("utf8");
38
+ const messageStart = content.indexOf("\n\n");
39
+ if (messageStart < 0)
40
+ throw new Error(`git commit ${object.oid} has no message separator`);
41
+ const parents = [];
42
+ for (const line of content.slice(0, messageStart).split("\n")) {
43
+ if (!line.startsWith("parent "))
44
+ continue;
45
+ const parent = line.slice("parent ".length);
46
+ if (!/^[0-9a-f]{40,64}$/.test(parent)) {
47
+ throw new Error(`git commit ${object.oid} has an invalid parent header`);
48
+ }
49
+ parents.push(parent);
50
+ }
51
+ return { oid: object.oid, parents, message: content.slice(messageStart + 2) };
52
+ }
53
+ function parseLedgerRefs(output) {
54
+ return output
55
+ .toString("utf8")
56
+ .split(/\r?\n/)
57
+ .filter((ref) => ref.startsWith(ACTIVE_LEDGER_REF_PREFIX))
58
+ .map((ref) => ({ contractId: ref.slice(ACTIVE_LEDGER_REF_PREFIX.length), ref }));
59
+ }
60
+ async function readLedgerFromBatch(batch, ref, commits) {
61
+ const headObject = await queryBatchObject(batch, `${ref.ref}^{commit}`);
62
+ if (!headObject)
63
+ return null;
64
+ const head = parseCommitObject(headObject);
65
+ commits.set(head.oid, Promise.resolve(head));
66
+ const ordered = [];
67
+ const visited = new Set();
68
+ const visiting = new Set();
69
+ const visit = async (oid) => {
70
+ if (visited.has(oid))
71
+ return;
72
+ if (visiting.has(oid))
73
+ throw new Error(`git ledger ancestry contains a cycle at ${oid}`);
74
+ visiting.add(oid);
75
+ let pending = commits.get(oid);
76
+ if (!pending) {
77
+ pending = queryBatchObject(batch, oid).then((object) => {
78
+ if (!object)
79
+ throw new Error(`[git cat-file ${oid}] object is missing`);
80
+ return parseCommitObject(object);
81
+ });
82
+ commits.set(oid, pending);
83
+ }
84
+ const commit = await pending;
85
+ for (const parent of commit.parents)
86
+ await visit(parent);
87
+ visiting.delete(oid);
88
+ visited.add(oid);
89
+ ordered.push(commit);
90
+ };
91
+ await visit(head.oid);
92
+ return {
93
+ contractId: ref.contractId,
94
+ ref: ref.ref,
95
+ head: head.oid,
96
+ entries: ordered.map((commit) => parseLedgerEntryCommitMessage(commit.message)),
97
+ };
98
+ }
99
+ async function closeBatch(cwd, batch) {
100
+ const close = await batch.close();
101
+ if (!close.aborted && (close.timedOut || close.signal || close.code !== 0)) {
102
+ throw wrapGitError("cat-file --batch", {
103
+ message: close.timedOut
104
+ ? "git timed out"
105
+ : close.signal
106
+ ? `git exited from signal ${close.signal}`
107
+ : `git exited with status ${close.code ?? "unknown"}`,
108
+ stderr: close.stderr.toString("utf8"),
109
+ }, cwd);
110
+ }
111
+ }
112
+ async function readLedgerRefs(cwd, refs) {
113
+ if (refs.length === 0)
114
+ return [];
115
+ const batch = openGitStreamingBatch(cwd);
116
+ const commits = new Map();
117
+ const results = [];
118
+ let readError;
119
+ try {
120
+ for (let index = 0; index < refs.length; index += 1) {
121
+ const ref = refs[index];
122
+ try {
123
+ const ledger = await readLedgerFromBatch(batch, ref, commits);
124
+ if (ledger)
125
+ results.push(ledger);
126
+ else
127
+ results.push({ id: ref.contractId, error: new Error(`ledger ref disappeared: ${ref.ref}`) });
128
+ }
129
+ catch (error) {
130
+ const diagnostic = error instanceof Error ? error : new Error(String(error));
131
+ results.push({ id: ref.contractId, error: diagnostic });
132
+ if (error instanceof GitStreamingBatchFramingError
133
+ || error instanceof GitStreamingBatchResponseEndedError) {
134
+ const isolationReason = error instanceof GitStreamingBatchResponseEndedError
135
+ ? "response ended"
136
+ : "malformed frame";
137
+ for (const pending of refs.slice(index + 1)) {
138
+ results.push({
139
+ id: pending.contractId,
140
+ error: new Error(`git cat-file batch terminated after ${isolationReason}: ${diagnostic.message}`),
141
+ });
142
+ }
143
+ break;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ catch (error) {
149
+ readError = error;
150
+ }
151
+ try {
152
+ await closeBatch(cwd, batch);
153
+ }
154
+ catch (error) {
155
+ if (!readError)
156
+ readError = error;
157
+ }
158
+ if (readError)
159
+ throw readError;
160
+ return results;
161
+ }
162
+ /** Read every ledger with two Git processes regardless of ledger or entry count. */
163
+ export async function readAllLedgerSnapshots(cwd) {
164
+ const refOutput = await runLedgerGit(cwd, [
165
+ "for-each-ref",
166
+ "--sort=refname",
167
+ "--format=%(refname)",
168
+ ACTIVE_LEDGER_REF_PREFIX,
169
+ ]);
170
+ return readLedgerRefs(cwd, parseLedgerRefs(refOutput));
171
+ }
172
+ /** Read one exact ledger with one Git process and without enumerating other refs. */
173
+ export async function readLedgerSnapshot(cwd, contractId) {
174
+ const ref = activeLedgerRef(contractId);
175
+ const batch = openGitStreamingBatch(cwd);
176
+ let ledger = null;
177
+ let readError;
178
+ try {
179
+ ledger = await readLedgerFromBatch(batch, { contractId, ref }, new Map());
180
+ }
181
+ catch (error) {
182
+ readError = error;
183
+ }
184
+ try {
185
+ await closeBatch(cwd, batch);
186
+ }
187
+ catch (error) {
188
+ if (!readError)
189
+ readError = error;
190
+ }
191
+ if (readError)
192
+ throw readError;
193
+ return ledger;
194
+ }
@@ -295,6 +295,8 @@ export function writeTransition(database, readOnly, run) {
295
295
  if (readOnly) {
296
296
  throw new ProjectionGenerationStoreError("projection generation store is read-only");
297
297
  }
298
+ if (database.isTransaction)
299
+ return run();
298
300
  database.exec("BEGIN IMMEDIATE");
299
301
  try {
300
302
  const result = run();
@@ -306,6 +308,26 @@ export function writeTransition(database, readOnly, run) {
306
308
  throw error;
307
309
  }
308
310
  }
311
+ export function beginGenerationWriteTransaction(database, readOnly) {
312
+ assertDatabaseOpen(database);
313
+ if (readOnly)
314
+ throw new ProjectionGenerationStoreError("projection generation store is read-only");
315
+ if (database.isTransaction)
316
+ throw new ProjectionGenerationStoreError("projection generation write transaction is already open");
317
+ database.exec("BEGIN IMMEDIATE");
318
+ }
319
+ export function commitGenerationWriteTransaction(database) {
320
+ assertDatabaseOpen(database);
321
+ if (!database.isTransaction)
322
+ throw new ProjectionGenerationStoreError("projection generation write transaction is not open");
323
+ database.exec("COMMIT");
324
+ }
325
+ export function rollbackGenerationWriteTransaction(database) {
326
+ assertDatabaseOpen(database);
327
+ if (!database.isTransaction)
328
+ return;
329
+ rollbackQuietly(database);
330
+ }
309
331
  export function closeDatabase(database) {
310
332
  if (closedDatabases.has(database))
311
333
  return;
@@ -136,7 +136,7 @@ async function completionFacts(projectionDirectory, executionId, inputs, outcome
136
136
  * Construct the public Projection once and expose a provider-neutral execution
137
137
  * handle. Provider adapters and harness public types are unchanged.
138
138
  */
139
- export function executeProjectionGeneration(projectionDirectory, launch, providerLoaderPorts = {}) {
139
+ export function prepareProjectionGenerationExecution(projectionDirectory, launch, providerLoaderPorts = {}) {
140
140
  const inputs = parseLaunchInputs(launch);
141
141
  initializeConfig(process.env, inputs.cwd);
142
142
  const executionId = launch.executionId;
@@ -160,43 +160,51 @@ export function executeProjectionGeneration(projectionDirectory, launch, provide
160
160
  executionId,
161
161
  initialTellIds,
162
162
  });
163
- let projection;
164
- try {
165
- projection = inputs.session
166
- ? revive({
167
- ...terms,
168
- session: inputs.session,
169
- resumePath: inputs.resumePath ?? projectionDirectory,
170
- }, { observer: observerController.observer })
171
- : call(terms, { observer: observerController.observer });
172
- }
173
- catch (error) {
174
- void observerController.close();
175
- throw error;
176
- }
177
- const outcome = projection.outcome.then(async (result) => {
178
- try {
163
+ return {
164
+ start() {
165
+ let projection;
166
+ try {
167
+ projection = inputs.session
168
+ ? revive({
169
+ ...terms,
170
+ session: inputs.session,
171
+ resumePath: inputs.resumePath ?? projectionDirectory,
172
+ }, { observer: observerController.observer })
173
+ : call(terms, { observer: observerController.observer });
174
+ }
175
+ catch (error) {
176
+ void observerController.close();
177
+ throw error;
178
+ }
179
+ const outcome = projection.outcome.then(async (result) => {
180
+ try {
181
+ return {
182
+ verdict: result.status === "completed"
183
+ ? "completed"
184
+ : result.status === "dismissed"
185
+ ? "dismissed"
186
+ : result.status === "interrupted"
187
+ ? "interrupted"
188
+ : "failed",
189
+ facts: await completionFacts(projectionDirectory, executionId, inputs, result),
190
+ };
191
+ }
192
+ finally {
193
+ await observerController.close();
194
+ }
195
+ }, async (error) => {
196
+ await observerController.close();
197
+ throw error;
198
+ });
179
199
  return {
180
- verdict: result.status === "completed"
181
- ? "completed"
182
- : result.status === "dismissed"
183
- ? "dismissed"
184
- : result.status === "interrupted"
185
- ? "interrupted"
186
- : "failed",
187
- facts: await completionFacts(projectionDirectory, executionId, inputs, result),
200
+ events: projection.events,
201
+ outcome,
202
+ abort: (mode) => projection.abort(mode),
188
203
  };
189
- }
190
- finally {
191
- await observerController.close();
192
- }
193
- }, async (error) => {
194
- await observerController.close();
195
- throw error;
196
- });
197
- return {
198
- events: projection.events,
199
- outcome,
200
- abort: (mode) => projection.abort(mode),
204
+ },
205
+ dispose: () => observerController.close(),
201
206
  };
202
207
  }
208
+ export function executeProjectionGeneration(projectionDirectory, launch, providerLoaderPorts = {}) {
209
+ return prepareProjectionGenerationExecution(projectionDirectory, launch, providerLoaderPorts).start();
210
+ }
@@ -1,37 +1,179 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
2
  import { openProjectionGenerationStore } from "./store.js";
3
3
  import { spawnProjectionGenerationRunner, } from "./projection-generation-process.js";
4
+ import { acquireProjectionRunnerLock, observeProjectionRunnerLock, } from "../projection-runner-lock.js";
4
5
  export const PROJECTION_ADOPTION_TIMEOUT_MS = 4_000;
5
6
  export const PROJECTION_ADOPTION_POLL_MS = 20;
6
7
  function errorDetail(error) {
7
8
  return error instanceof Error ? error.message : String(error);
8
9
  }
9
- function recordLaunchFailure(projectionDirectory, executionId, detail, stage, terminalAt) {
10
+ function recordDeadStartup(projectionDirectory, executionId, detail, stage, terminalAt) {
10
11
  const store = openProjectionGenerationStore(projectionDirectory);
11
12
  try {
12
- return store.verdictIfOpen({
13
+ return store.settleStartupFailure({
13
14
  executionId,
14
- verdict: "launch-failed",
15
- facts: { stage, detail, ...(terminalAt ? { terminalAt } : {}) },
15
+ releasedLockEvidence: { stage, detail },
16
+ diagnostic: { stage, detail },
17
+ terminalAt,
16
18
  }).status === "committed";
17
19
  }
18
20
  finally {
19
21
  store.close();
20
22
  }
21
23
  }
24
+ function observeBootstrapMessages(child, executionId) {
25
+ const seen = new Set();
26
+ const waiters = new Map();
27
+ let failure;
28
+ const onMessage = (value) => {
29
+ const message = value;
30
+ if (message.executionId !== executionId)
31
+ return;
32
+ if (message.kind !== "leash-inherited" && message.kind !== "leash-acquired")
33
+ return;
34
+ seen.add(message.kind);
35
+ const waiter = waiters.get(message.kind);
36
+ if (waiter) {
37
+ waiter.resolve();
38
+ waiters.delete(message.kind);
39
+ }
40
+ };
41
+ const onError = (error) => {
42
+ failure = error;
43
+ for (const waiter of waiters.values())
44
+ waiter.reject(error);
45
+ waiters.clear();
46
+ };
47
+ const onExit = () => onError(new Error("runner exited during bootstrap"));
48
+ child.on("message", onMessage);
49
+ child.on("error", onError);
50
+ child.on("exit", onExit);
51
+ return {
52
+ waitFor(kind) {
53
+ if (seen.has(kind))
54
+ return Promise.resolve();
55
+ if (failure)
56
+ return Promise.reject(failure);
57
+ return new Promise((resolve, reject) => {
58
+ const timer = setTimeout(() => {
59
+ waiters.delete(kind);
60
+ reject(failure ?? new Error(`runner did not report ${kind} before startup timeout`));
61
+ }, PROJECTION_ADOPTION_TIMEOUT_MS);
62
+ waiters.set(kind, {
63
+ resolve: () => {
64
+ clearTimeout(timer);
65
+ waiters.delete(kind);
66
+ resolve();
67
+ },
68
+ reject: (error) => {
69
+ clearTimeout(timer);
70
+ waiters.delete(kind);
71
+ reject(error);
72
+ },
73
+ });
74
+ });
75
+ },
76
+ close() {
77
+ child.off("message", onMessage);
78
+ child.off("error", onError);
79
+ child.off("exit", onExit);
80
+ },
81
+ };
82
+ }
83
+ function sendBootstrapCommand(child, command) {
84
+ return new Promise((resolve, reject) => {
85
+ if (!child.send || !child.connected) {
86
+ reject(new Error("runner bootstrap channel is unavailable"));
87
+ return;
88
+ }
89
+ child.send(command, (error) => error ? reject(error) : resolve());
90
+ });
91
+ }
22
92
  /** Spawn one committed launch and return only after its exact adoption row. */
23
93
  export async function startProjectionGeneration(projectionDirectory, executionId, dependencies = {}) {
24
94
  const now = dependencies.now ?? Date.now;
25
95
  const sleep = dependencies.sleep ?? ((milliseconds) => delay(milliseconds));
96
+ const launcherAcquiredOwnership = dependencies.launchOwnership === undefined;
97
+ let launchOwnership;
98
+ try {
99
+ launchOwnership = dependencies.launchOwnership
100
+ ?? acquireProjectionRunnerLock(projectionDirectory);
101
+ }
102
+ catch (error) {
103
+ if (observeProjectionRunnerLock(projectionDirectory).state === "held") {
104
+ return { status: "pending" };
105
+ }
106
+ return {
107
+ status: "failed",
108
+ cause: "adoption",
109
+ detail: `runner construction failed: ${errorDetail(error)}`,
110
+ };
111
+ }
112
+ let launchCommitted = false;
113
+ let childPromoted = false;
26
114
  let child;
27
115
  try {
28
116
  child = spawnProjectionGenerationRunner(projectionDirectory, executionId, dependencies);
29
117
  }
30
118
  catch (error) {
31
119
  const detail = `runner spawn failed: ${errorDetail(error)}`;
32
- recordLaunchFailure(projectionDirectory, executionId, detail, "spawn", new Date(now()).toISOString());
120
+ recordDeadStartup(projectionDirectory, executionId, detail, "spawn", new Date(now()).toISOString());
121
+ launchOwnership.close();
33
122
  return { status: "failed", cause: "spawn", detail };
34
123
  }
124
+ const bootstrap = observeBootstrapMessages(child, executionId);
125
+ try {
126
+ await bootstrap.waitFor("leash-inherited");
127
+ if (dependencies.commitLaunch && !dependencies.commitLaunch()) {
128
+ await sendBootstrapCommand(child, { kind: "launch-rejected", executionId });
129
+ if (launcherAcquiredOwnership)
130
+ launchOwnership.close();
131
+ return { status: "failed", cause: "adoption", detail: "generation launch transaction was rejected" };
132
+ }
133
+ launchCommitted = true;
134
+ launchOwnership.releaseReservationForTransfer();
135
+ await sendBootstrapCommand(child, { kind: "promote-leash", executionId });
136
+ await bootstrap.waitFor("leash-acquired");
137
+ childPromoted = true;
138
+ await sendBootstrapCommand(child, { kind: "launch-committed", executionId });
139
+ try {
140
+ launchOwnership.close();
141
+ }
142
+ catch {
143
+ // The promoted child owns lifecycle truth; parent guard cleanup is diagnostic only.
144
+ }
145
+ }
146
+ catch (error) {
147
+ void sendBootstrapCommand(child, { kind: "launch-rejected", executionId }).catch(() => { });
148
+ if (childPromoted) {
149
+ try {
150
+ launchOwnership.close();
151
+ }
152
+ catch { }
153
+ return { status: "pending", child };
154
+ }
155
+ if (!launchCommitted) {
156
+ launchOwnership.close();
157
+ return { status: "failed", cause: "adoption", detail: `runner construction failed: ${errorDetail(error)}` };
158
+ }
159
+ try {
160
+ launchOwnership.reclaimReservation();
161
+ }
162
+ catch {
163
+ launchOwnership.close();
164
+ return { status: "pending", child };
165
+ }
166
+ try {
167
+ recordDeadStartup(projectionDirectory, executionId, `runner construction failed: ${errorDetail(error)}`, "adoption", new Date(now()).toISOString());
168
+ }
169
+ finally {
170
+ launchOwnership.close();
171
+ }
172
+ return { status: "failed", cause: "adoption", detail: `runner construction failed: ${errorDetail(error)}` };
173
+ }
174
+ finally {
175
+ bootstrap.close();
176
+ }
35
177
  const deadline = now() + PROJECTION_ADOPTION_TIMEOUT_MS;
36
178
  for (;;) {
37
179
  const observedAt = now();
@@ -74,19 +216,5 @@ export async function startProjectionGeneration(projectionDirectory, executionId
74
216
  finally {
75
217
  store.close();
76
218
  }
77
- const detail = "runner did not commit adoption before startup timeout";
78
- if (!recordLaunchFailure(projectionDirectory, executionId, detail, "adoption", new Date(now()).toISOString())) {
79
- const raced = openProjectionGenerationStore(projectionDirectory);
80
- try {
81
- const current = raced.readCurrentGeneration();
82
- if (current?.launch.executionId === executionId && current.adoption) {
83
- return { status: "adopted", child };
84
- }
85
- }
86
- finally {
87
- raced.close();
88
- }
89
- }
90
- child.kill("SIGTERM");
91
- return { status: "failed", cause: "adoption", detail };
219
+ return { status: "pending", child };
92
220
  }
@@ -17,6 +17,7 @@ export function projectionGenerationRunnerArgv(projectionDirectory, executionId)
17
17
  executionId,
18
18
  ];
19
19
  }
20
+ export const PROJECTION_RUNNER_BOOTSTRAP_ENV = "KEIYAKU_PROJECTION_RUNNER_BOOTSTRAP";
20
21
  export function spawnProjectionGenerationRunner(projectionDirectory, executionId, dependencies = {}) {
21
22
  const spawn = dependencies.spawn ?? nodeSpawn;
22
23
  const openSync = dependencies.openSync ?? fs.openSync;
@@ -30,7 +31,8 @@ export function spawnProjectionGenerationRunner(projectionDirectory, executionId
30
31
  try {
31
32
  const child = spawn(command, args, {
32
33
  detached: true,
33
- stdio: ["ignore", logFd, logFd],
34
+ stdio: ["ignore", logFd, logFd, "ipc"],
35
+ env: { ...process.env, [PROJECTION_RUNNER_BOOTSTRAP_ENV]: "1" },
34
36
  });
35
37
  if (child.pid === undefined) {
36
38
  throw new Error("projection runner did not expose a process id");