@astrosheep/keiyaku 2.9.6 → 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 (87) 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/harness/outcome.js +10 -0
  5. package/build/agents/providers/codex-app-server/adapter.js +6 -1
  6. package/build/agents/providers/codex-app-server/session.js +8 -7
  7. package/build/agents/selector.js +12 -1
  8. package/build/cli/commands/akuma/view/handler.js +3 -11
  9. package/build/cli/commands/contract/amend/handler.js +1 -1
  10. package/build/cli/commands/contract/amend/meta.js +4 -4
  11. package/build/cli/commands/contract/bind/handler.js +10 -4
  12. package/build/cli/commands/contract/bind/meta.js +6 -6
  13. package/build/cli/commands/contract/petition/handler.js +16 -3
  14. package/build/cli/commands/contract/petition/meta.js +4 -4
  15. package/build/cli/commands/metadata.js +3 -2
  16. package/build/cli/commands/projection/status/handler.js +5 -4
  17. package/build/cli/commands/projection/status/meta.js +2 -2
  18. package/build/cli/commands/projection/tell/handler.js +10 -2
  19. package/build/cli/commands/projection/tell/meta.js +3 -3
  20. package/build/cli/commands/task/add/meta.js +9 -1
  21. package/build/cli/commands/task/catalog.js +2 -0
  22. package/build/cli/commands/task/log/handler.js +12 -0
  23. package/build/cli/commands/task/log/meta.js +8 -0
  24. package/build/cli/commands/task/shared.js +2 -1
  25. package/build/cli/completion.js +8 -0
  26. package/build/cli/flags.js +8 -0
  27. package/build/cli/index.js +10 -6
  28. package/build/cli/parse-flags.js +6 -0
  29. package/build/cli/parse-metadata.js +1 -1
  30. package/build/cli/render/kanshi.js +2 -2
  31. package/build/cli/render/line-width.js +33 -0
  32. package/build/cli/render/path-prefix-compaction.js +131 -0
  33. package/build/cli/render/petition.js +4 -0
  34. package/build/cli/render/projection-activity.js +103 -21
  35. package/build/cli/render/shared.js +38 -15
  36. package/build/cli/render/status.js +8 -5
  37. package/build/cli/render/success-response.js +2 -0
  38. package/build/cli/render/tool-presentation.js +3 -3
  39. package/build/cli/render/wait.js +68 -48
  40. package/build/cli/subagent-guard.js +3 -0
  41. package/build/cli/types.js +1 -1
  42. package/build/config/settings/disease.js +4 -4
  43. package/build/config/settings/loader.js +44 -21
  44. package/build/core/addressing.js +40 -9
  45. package/build/core/amend.js +21 -5
  46. package/build/core/bind.js +111 -6
  47. package/build/core/call/context.js +19 -3
  48. package/build/core/call/execution.js +43 -20
  49. package/build/core/draft.js +1 -1
  50. package/build/core/ledger-batch.js +194 -0
  51. package/build/core/projection/generation/database.js +43 -2
  52. package/build/core/projection/generation/model.js +24 -3
  53. package/build/core/projection/generation/projection-generation-continuation.js +75 -10
  54. package/build/core/projection/generation/projection-generation-execution.js +46 -37
  55. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  56. package/build/core/projection/generation/projection-generation-process.js +3 -1
  57. package/build/core/projection/generation/projection-generation-runner.js +145 -66
  58. package/build/core/projection/generation/projection-generation-runtime.js +93 -11
  59. package/build/core/projection/generation/store.js +26 -1
  60. package/build/core/projection/generation/transitions.js +193 -14
  61. package/build/core/projection/index.js +5 -5
  62. package/build/core/projection/projection-core.js +1 -1
  63. package/build/core/projection/projection-kill.js +53 -10
  64. package/build/core/projection/projection-life-protocol.js +10 -0
  65. package/build/core/projection/projection-runner-lock.js +177 -37
  66. package/build/core/projection/projection-status.js +143 -55
  67. package/build/core/projection/projection-wait.js +53 -15
  68. package/build/core/projection/projection-wake.js +177 -46
  69. package/build/core/projection/tell/database.js +18 -0
  70. package/build/core/projection/tell/model.js +1 -0
  71. package/build/core/projection/tell/store.js +102 -55
  72. package/build/core/registry.js +82 -69
  73. package/build/core/scope.js +9 -9
  74. package/build/core/status/board.js +42 -4
  75. package/build/core/status/drift.js +21 -5
  76. package/build/core/status/ledger-batch.js +1 -158
  77. package/build/core/task/index.js +2 -2
  78. package/build/core/task/task-contract.js +18 -0
  79. package/build/core/task/task-git-runtime.js +8 -10
  80. package/build/core/task/task-git-store.js +55 -3
  81. package/build/core/worktree-path.js +39 -25
  82. package/build/flow-error.js +1 -1
  83. package/build/generated/version.js +2 -2
  84. package/build/git/refs.js +47 -1
  85. package/package.json +1 -1
  86. package/skills/keiyaku-akuma/SKILL.md +28 -0
  87. package/skills/keiyaku-workflow/SKILL.md +142 -18
@@ -1,22 +1,87 @@
1
1
  import { ProjectionStateError } from "../../atomic-publish.js";
2
2
  import { applySnapshotEffort, parseResolvedAkumaLaunchSnapshot, } from "../../../agents/launch-snapshot/model.js";
3
- import { listTellIds, readTellOriginal, readTellSubmitted, } from "../projection-core.js";
4
- export function snapshotPendingTells(projectionDirectory) {
5
- const pending = ["inbox", "inflight", "submitted"]
6
- .flatMap((layer) => listTellIds(projectionDirectory, layer)
7
- .map((tellId) => ({ layer, tellId })))
8
- .sort((left, right) => left.tellId.localeCompare(right.tellId));
3
+ import { listTellMetadataFromDatabase, readPendingTellWindowFromDatabase, } from "../tell/store.js";
4
+ import { PENDING_PROJECTION_TELL_STATES } from "../tell/model.js";
5
+ import { readValidatedHistory, withGenerationReadSnapshot } from "./database.js";
6
+ function retainedFenceFor(current) {
7
+ return current?.verdict?.facts.state === "launch-failed"
8
+ ? current.launch.facts.tellFence ?? []
9
+ : [];
10
+ }
11
+ function interruptedForReplay(current) {
12
+ return current?.verdict?.facts.state === "interrupted";
13
+ }
14
+ function snapshotTells(database, layers, admit) {
15
+ const pending = listTellMetadataFromDatabase(database, layers)
16
+ .filter((item) => admit?.({
17
+ layer: item.state,
18
+ tellId: item.tellId,
19
+ ...(item.executionId ? { executionId: item.executionId } : {}),
20
+ }) ?? true);
9
21
  const effort = pending.reduce((selected, item) => {
10
- const tell = item.layer === "submitted"
11
- ? readTellSubmitted(projectionDirectory, item.tellId)
12
- : readTellOriginal(projectionDirectory, item.layer, item.tellId);
13
- return tell.effort ?? selected;
22
+ return item.effort ?? selected;
14
23
  }, undefined);
15
24
  return {
16
25
  tellIds: pending.map((item) => item.tellId),
17
26
  ...(effort ? { effort } : {}),
18
27
  };
19
28
  }
29
+ /** Ordinary completed-generation handoff retains only tells not yet delivered. */
30
+ export function snapshotPendingTells(projectionDirectory) {
31
+ return withGenerationReadSnapshot(projectionDirectory, (database) => snapshotTells(database, PENDING_PROJECTION_TELL_STATES));
32
+ }
33
+ /** Interrupted generations replay pending mail and their own delivered tells. */
34
+ export function snapshotInterruptTells(projectionDirectory, executionId) {
35
+ return withGenerationReadSnapshot(projectionDirectory, (database) => snapshotTells(database, [...PENDING_PROJECTION_TELL_STATES, "delivered"], (item) => item.layer !== "delivered" || item.executionId === executionId));
36
+ }
37
+ /** Retry ordinary pending mail plus exactly the durable fence of a failed launch. */
38
+ export function snapshotRetainedSuccessorTells(projectionDirectory, retainedFence) {
39
+ const retained = new Set(retainedFence);
40
+ return withGenerationReadSnapshot(projectionDirectory, (database) => snapshotTells(database, [...PENDING_PROJECTION_TELL_STATES, "delivered"], ({ layer, tellId }) => layer !== "delivered" || retained.has(tellId)));
41
+ }
42
+ /** Build the durable successor fence only after continuation is admitted. */
43
+ export function snapshotProjectionContinuationTells(projectionDirectory, current) {
44
+ const snapshot = readProjectionGenerationContinuationSnapshot(projectionDirectory, 0);
45
+ if (snapshot.current?.launch.executionId !== current?.launch.executionId) {
46
+ throw new ProjectionStateError("projection generation changed while selecting continuation tells");
47
+ }
48
+ return snapshot.successor;
49
+ }
50
+ function readContinuationFromDatabase(database, current, limit = 10) {
51
+ const tells = readPendingTellWindowFromDatabase(database, limit);
52
+ const retainedFence = retainedFenceFor(current);
53
+ const successor = interruptedForReplay(current)
54
+ ? snapshotTells(database, [...PENDING_PROJECTION_TELL_STATES, "delivered"], (item) => item.layer !== "delivered" || item.executionId === current.launch.executionId)
55
+ : retainedFence.length > 0
56
+ ? snapshotTells(database, [...PENDING_PROJECTION_TELL_STATES, "delivered"], ({ layer, tellId }) => layer !== "delivered" || retainedFence.includes(tellId))
57
+ : snapshotTells(database, PENDING_PROJECTION_TELL_STATES);
58
+ return {
59
+ tells,
60
+ successor,
61
+ requiresWake: successor.tellIds.length > 0,
62
+ };
63
+ }
64
+ /** One heart snapshot owns generation selection and all continuation facts. */
65
+ export function readProjectionGenerationContinuationSnapshot(projectionDirectory, limit = 10) {
66
+ return withGenerationReadSnapshot(projectionDirectory, (database) => {
67
+ const { records, current } = readValidatedHistory(database);
68
+ const continuation = readContinuationFromDatabase(database, current, limit);
69
+ return {
70
+ records,
71
+ current,
72
+ generationSequence: records.at(-1)?.seq ?? 0,
73
+ ...continuation,
74
+ };
75
+ });
76
+ }
77
+ /** One continuation eligibility fact shared by tell, wait, and wake. */
78
+ export function readProjectionContinuation(projectionDirectory, current, limit = 10) {
79
+ const snapshot = readProjectionGenerationContinuationSnapshot(projectionDirectory, limit);
80
+ if (snapshot.current?.launch.executionId !== current?.launch.executionId) {
81
+ throw new ProjectionStateError("projection generation changed while reading continuation");
82
+ }
83
+ return snapshot;
84
+ }
20
85
  export function applyContinuationEffort(inputs, effort) {
21
86
  if (!effort)
22
87
  return inputs;
@@ -2,7 +2,7 @@ import { initializeConfig } from "../../../config/env.js";
2
2
  import { buildCallTerms } from "../../../agents/call-terms.js";
3
3
  import { parseResolvedAkumaLaunchSnapshot } from "../../../agents/launch-snapshot/model.js";
4
4
  import { revive, call, } from "../../../agents/harness/index.js";
5
- import { claimFencedTells, demoteSubmittedTellsForReplay, readTellOriginal, readProjectionEvents, } from "../projection-core.js";
5
+ import { claimFencedTells, readTellOriginal, readProjectionEvents, } from "../projection-core.js";
6
6
  import { createProjectionExecutionObserver, renderProjectionTellFrame, } from "../projection-execution-observer.js";
7
7
  import { readProjectionIdentity } from "../projection-identity.js";
8
8
  import { selectValidStoredAgentEvents } from "../../stored-agent-event.js";
@@ -136,11 +136,10 @@ 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;
143
- demoteSubmittedTellsForReplay(projectionDirectory, executionId);
144
143
  const initialTellIds = claimFencedTells(projectionDirectory, launch.facts.tellFence ?? [], executionId);
145
144
  const tellPrompt = initialTellIds.map((tellId) => {
146
145
  const tell = readTellOriginal(projectionDirectory, "inflight", tellId);
@@ -161,41 +160,51 @@ export function executeProjectionGeneration(projectionDirectory, launch, provide
161
160
  executionId,
162
161
  initialTellIds,
163
162
  });
164
- let projection;
165
- try {
166
- projection = inputs.session
167
- ? revive({
168
- ...terms,
169
- session: inputs.session,
170
- resumePath: inputs.resumePath ?? projectionDirectory,
171
- }, { observer: observerController.observer })
172
- : call(terms, { observer: observerController.observer });
173
- }
174
- catch (error) {
175
- void observerController.close();
176
- throw error;
177
- }
178
- const outcome = projection.outcome.then(async (result) => {
179
- 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
+ });
180
199
  return {
181
- verdict: result.status === "completed"
182
- ? "completed"
183
- : result.status === "dismissed"
184
- ? "dismissed"
185
- : "failed",
186
- facts: await completionFacts(projectionDirectory, executionId, inputs, result),
200
+ events: projection.events,
201
+ outcome,
202
+ abort: (mode) => projection.abort(mode),
187
203
  };
188
- }
189
- finally {
190
- await observerController.close();
191
- }
192
- }, async (error) => {
193
- await observerController.close();
194
- throw error;
195
- });
196
- return {
197
- events: projection.events,
198
- outcome,
199
- abort: (mode) => projection.abort(mode),
204
+ },
205
+ dispose: () => observerController.close(),
200
206
  };
201
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");