gestalt-mobile 0.41.2 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
16
16
  <link rel="icon" href="/icons/gestalt-mobile-192.png" />
17
17
  <link rel="apple-touch-icon" href="/icons/gestalt-mobile-180.png" />
18
18
  <title>Gestalt Mobile</title>
19
- <script type="module" crossorigin src="/assets/index-9uZwieND.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-BRly-q56.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-D57mr3B1.css">
21
21
  </head>
22
22
  <body>
@@ -15,8 +15,11 @@ import { CliUsageError, parseConfig } from './config.js';
15
15
  import { LauncherProfileCatalog } from './platform/catalog/launcher-profile-catalog.js';
16
16
  import { FilesystemSkillProfileStore } from './platform/skills/filesystem-skill-profile-store.js';
17
17
  import { loadPwaIcon } from './platform/pwa/load-pwa-icon.js';
18
+ import { exportControlPlaneTrace, formatControlPlaneTrace, } from './features/control-plane-trace/export-trace.js';
19
+ import { relayStatePath } from './platform/persistence/state-path.js';
18
20
  const runFile = promisify(execFile);
19
21
  export const usage = `Usage: gestalt-mobile [options]
22
+ gestalt-mobile trace <session-id> [--json] [--cwd <path>] [--data-dir <path>]
20
23
 
21
24
  Options:
22
25
  --cwd <path> Workspace root (default: current directory)
@@ -125,6 +128,33 @@ export function installShutdownHandlers(app, signalSource = process) {
125
128
  return shutdown;
126
129
  }
127
130
  function parseInvocation(args, cwd) {
131
+ if (args[0] === 'trace') {
132
+ const sessionId = args[1];
133
+ if (!sessionId || sessionId.startsWith('--'))
134
+ throw new CliUsageError('trace requires a session ID');
135
+ let json = false;
136
+ let root = cwd;
137
+ let dataDir;
138
+ for (let index = 2; index < args.length; index += 1) {
139
+ const option = args[index];
140
+ if (option === '--json') {
141
+ if (json)
142
+ throw new CliUsageError('Duplicate option: --json');
143
+ json = true;
144
+ continue;
145
+ }
146
+ if (option !== '--cwd' && option !== '--data-dir')
147
+ throw new CliUsageError(`Unknown trace option: ${option}`);
148
+ const value = args[++index];
149
+ if (!value || value.startsWith('--'))
150
+ throw new CliUsageError(`Missing value for ${option}`);
151
+ if (option === '--cwd')
152
+ root = resolve(cwd, value);
153
+ else
154
+ dataDir = resolve(cwd, value);
155
+ }
156
+ return { command: 'trace', sessionId, json, root, ...(dataDir ? { dataDir } : {}) };
157
+ }
128
158
  if (args.includes('--help')) {
129
159
  if (args.length !== 1)
130
160
  throw new CliUsageError('--help cannot be combined with other arguments');
@@ -187,6 +217,23 @@ export async function runCli(dependencies = {}) {
187
217
  stdout.write(`${await packageVersion(moduleUrl)}\n`);
188
218
  return 0;
189
219
  }
220
+ if (invocation.command === 'trace') {
221
+ const stateHome = dependencies.environment?.XDG_STATE_HOME ??
222
+ process.env.XDG_STATE_HOME ??
223
+ resolve(dependencies.homeDirectory ?? homedir(), '.local/state');
224
+ const databasePath = invocation.dataDir
225
+ ? resolve(invocation.dataDir, 'relay.sqlite')
226
+ : relayStatePath(invocation.root, stateHome);
227
+ try {
228
+ const trace = (dependencies.exportTrace ?? exportControlPlaneTrace)(databasePath, invocation.sessionId);
229
+ stdout.write(invocation.json ? `${JSON.stringify(trace, null, 2)}\n` : formatControlPlaneTrace(trace));
230
+ return 0;
231
+ }
232
+ catch (error) {
233
+ stderr.write(`Unable to export control-plane trace: ${error instanceof Error ? error.message : 'unknown error'}\n`);
234
+ return 1;
235
+ }
236
+ }
190
237
  const skillProfiles = new FilesystemSkillProfileStore(dependencies.homeDirectory ?? homedir());
191
238
  if (invocation.command === 'list')
192
239
  return listSkillProfiles(skillProfiles, stdout, stderr);
@@ -12,7 +12,7 @@ import { join } from 'node:path';
12
12
  import { describe, expect, it, onTestFinished, vi } from 'vitest';
13
13
  import WebSocket from 'ws';
14
14
  import { composeRelayApp } from './composition.js';
15
- import { AUTOPILOT_CONTINUATION_PROMPT } from './features/autopilot/application/policy.js';
15
+ import { autopilotExecutorLaunchPrompt } from './features/autopilot/application/policy.js';
16
16
  import { CodexJsonRpcError } from './platform/codex/json-rpc-client.js';
17
17
  import { SqliteAuthorizationStore } from './platform/auth/sqlite-authorization-store.js';
18
18
  import { authorizationSessionId, authorizedDeviceId, localOwnerId, webAuthnCredentialId, } from './features/auth/domain/identifiers.js';
@@ -497,7 +497,12 @@ describe('production composition', () => {
497
497
  input: [
498
498
  {
499
499
  type: 'text',
500
- text: `${AUTOPILOT_CONTINUATION_PROMPT} Launch task_name l1 for canonical L1; retain the canonical label in status and review output.`,
500
+ text: autopilotExecutorLaunchPrompt({
501
+ canonicalTaskName: 'l1',
502
+ canonicalPosition: 'L1',
503
+ generation: 1,
504
+ taskName: 'l1',
505
+ }),
501
506
  text_elements: [],
502
507
  },
503
508
  ],
@@ -1040,6 +1045,16 @@ describe('production composition', () => {
1040
1045
  await expect
1041
1046
  .poll(async () => (await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json().activeTurnId)
1042
1047
  .toBeNull();
1048
+ await expect
1049
+ .poll(() => {
1050
+ const database = new DatabaseSync(join(fixture.dataDir, 'relay.sqlite'));
1051
+ const scheduled = database
1052
+ .prepare("SELECT count(*) AS count FROM session_events WHERE session_id = ? AND type = 'autopilot.continuation-scheduled'")
1053
+ .get(fixture.sessionId);
1054
+ database.close();
1055
+ return scheduled.count;
1056
+ })
1057
+ .toBeGreaterThan(0);
1043
1058
  const startedTurns = () => fixture.handles
1044
1059
  .flatMap((candidate) => candidate.requests)
1045
1060
  .filter((request) => request.method === 'turn/start').length;
@@ -1455,7 +1470,7 @@ describe('production composition', () => {
1455
1470
  .filter((request) => request.method === 'turn/start')).toHaveLength(startsAfterFinal);
1456
1471
  await fixture.app.close();
1457
1472
  });
1458
- it('production attention request requires explicit re-enable and a complete plan transitions to completed', async () => {
1473
+ it('production attention approval resumes Autopilot and a complete plan transitions to completed', async () => {
1459
1474
  const fixture = await createProductionAutopilotFixture();
1460
1475
  const handle = fixture.handles.find((candidate) => candidate.request);
1461
1476
  expect((await fixture.app.inject({
@@ -1474,15 +1489,6 @@ describe('production composition', () => {
1474
1489
  url: `/api/sessions/${fixture.sessionId}/attention/810/resolve`,
1475
1490
  payload: { operationKey: 'resume-attention', action: 'resume' },
1476
1491
  })).statusCode).toBe(202);
1477
- await new Promise((resolve) => setTimeout(resolve, 25));
1478
- expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
1479
- autopilot: { state: 'attentionRequired', enabled: false },
1480
- });
1481
- expect((await fixture.app.inject({
1482
- method: 'PUT',
1483
- url: `/api/sessions/${fixture.sessionId}/autopilot`,
1484
- payload: { enabled: true },
1485
- })).statusCode).toBe(200);
1486
1492
  await vi.waitFor(async () => expect((await fixture.app.inject(`/api/sessions/${fixture.sessionId}`)).json()).toMatchObject({
1487
1493
  autopilot: { enabled: true, state: expect.stringMatching(/monitoring|backoff/) },
1488
1494
  }));
@@ -1615,8 +1621,14 @@ describe('production composition', () => {
1615
1621
  handle.notify(completion);
1616
1622
  handle.notify(completion);
1617
1623
  await vi.waitFor(() => expect(timers.filter((timer) => !timer.cancelled && !timer.fired)).toHaveLength(1));
1618
- await runNextTimer();
1619
- await runNextTimer();
1624
+ for (let attempts = 0; attempts < 3; attempts += 1) {
1625
+ const pending = timers.find((timer) => !timer.cancelled && !timer.fired);
1626
+ if (!pending)
1627
+ break;
1628
+ pending.fired = true;
1629
+ pending.callback();
1630
+ await Promise.resolve();
1631
+ }
1620
1632
  await vi.waitFor(() => expect(fixture.handles
1621
1633
  .flatMap((candidate) => candidate.calls)
1622
1634
  .filter((call) => call === 'turn/start')).toHaveLength(2), { timeout: 3_500 });
@@ -1770,7 +1782,7 @@ describe('production composition', () => {
1770
1782
  // rejected child continuation; it must never target the dead child.
1771
1783
  expect((starts[3]?.params).threadId).not.toBe('child-1');
1772
1784
  expect((starts[3]?.params).input?.[0]?.text).toContain('Launch task_name l1_g2 for canonical L1');
1773
- expect((starts[3]?.params).input?.[0]?.text).toContain('explicit model selected by the supervisor');
1785
+ expect((starts[3]?.params).input?.[0]?.text).toContain('agent_type org-plan-executor and reasoning_effort high');
1774
1786
  // Capacity recovery is allowed only for the active root control. It
1775
1787
  // recycles that writer but must retain the one durable g2 handoff.
1776
1788
  const rootHandle = fixture.handles.find((candidate) => candidate.requests.some((request) => request.method === 'turn/start' &&
@@ -66,7 +66,7 @@ import { AutopilotCoordinator } from './features/autopilot/application/service.j
66
66
  import { deriveSessionStatus } from './features/sessions/session-status.js';
67
67
  import { AUTOPILOT_CONTINUATION_PROMPT, AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT, autopilotExecutorLaunchPrompt, defaultAutopilotPolicy, } from './features/autopilot/application/policy.js';
68
68
  import { createRelyingPartyConfig } from './config.js';
69
- import { parseOrgPlanAttention, toOrgPlanAttentionAcknowledgement, } from '../shared/contracts/org-plan-attention.js';
69
+ import { parseOrgPlanAttention, parseOrgPlanAttentionToolResponse, toOrgPlanAttentionAcknowledgement, } from '../shared/contracts/org-plan-attention.js';
70
70
  import { autopilotWaitLeaseToolResponse, } from '../shared/contracts/autopilot-wait-lease.js';
71
71
  import { agentCapacityRecoveryToolResponse } from '../shared/contracts/agent-capacity-recovery.js';
72
72
  import { pwaIconUrl } from './features/pwa/register-routes.js';
@@ -137,7 +137,14 @@ export async function composeRelayApp(options) {
137
137
  };
138
138
  options.onAttentionTransitions?.(attentionTransitions);
139
139
  const activity = new AgentActivityRegistry((snapshot, occurredAt) => {
140
- events.publish(journal.append(snapshot.sessionId, 'agent.activity.updated', toAgentActivityDto(snapshot), occurredAt));
140
+ events.publish(journal.append(snapshot.sessionId, 'agent.activity.updated', {
141
+ ...toAgentActivityDto(snapshot),
142
+ ...(autopilotStore.find(snapshot.sessionId)?.checkpoints?.activeHandoffId
143
+ ? {
144
+ traceId: autopilotStore.find(snapshot.sessionId).checkpoints.activeHandoffId,
145
+ }
146
+ : {}),
147
+ }, occurredAt));
141
148
  publishSessionStatus(snapshot.sessionId, occurredAt);
142
149
  notifyAutopilotActivity(snapshot.sessionId);
143
150
  }, {
@@ -400,8 +407,6 @@ export async function composeRelayApp(options) {
400
407
  explicit: session.effectiveSkillSelection.skills,
401
408
  }).skillsConfig;
402
409
  const project = await skillProfiles.readWorkspaceDefault(session.workspacePath);
403
- if (!options.explicitSkillProfile && !project)
404
- return undefined;
405
410
  return compileSkillOverride({
406
411
  discovered: catalog.skills,
407
412
  explicit: options.explicitSkillProfile?.skills,
@@ -498,7 +503,13 @@ export async function composeRelayApp(options) {
498
503
  if (!interactions.resolve(sessionId, requestId, occurredAt, 'failed'))
499
504
  return false;
500
505
  autopilot.checkpointHandoffFailed(sessionId, turnId);
501
- events.publish(journal.append(sessionId, 'org-plan.checkpoint-handoff-failed', { requestId, reason: 'checkpointHandoffFailed' }, occurredAt));
506
+ events.publish(journal.append(sessionId, 'org-plan.checkpoint-handoff-failed', {
507
+ requestId,
508
+ reason: 'checkpointHandoffFailed',
509
+ ...(autopilotStore.find(sessionId)?.checkpoints?.activeHandoffId
510
+ ? { traceId: autopilotStore.find(sessionId).checkpoints.activeHandoffId }
511
+ : {}),
512
+ }, occurredAt));
502
513
  publishAttentionSettlement(sessionId, requestId, occurredAt, 'failed');
503
514
  // Neither this timeout nor a failed transport may schedule continuation.
504
515
  // If a successful response is already blocked in the writable stream, drop
@@ -698,9 +709,16 @@ export async function composeRelayApp(options) {
698
709
  const rootCommandId = resolvedOrigin.kind === 'root' ? completedCommandId(notification) : null;
699
710
  if (rootCommandId)
700
711
  autopilot.rootProcessCompleted(sessionId, rootCommandId);
712
+ const refreshChildTopology = notification.method === 'item/completed' &&
713
+ activityFacts.some((fact) => fact.kind === 'collaboration' &&
714
+ (fact.collaborationAction === 'spawn_agent' ||
715
+ fact.collaborationAction === 'resume_agent' ||
716
+ fact.collaborationAction === 'close_agent'));
701
717
  if (!normalized) {
702
718
  for (const activityFact of activityFacts)
703
719
  activity.observe(activityFact);
720
+ if (refreshChildTopology)
721
+ void activity.refresh(sessionId);
704
722
  return;
705
723
  }
706
724
  let completedSession;
@@ -719,6 +737,19 @@ export async function composeRelayApp(options) {
719
737
  // activeTurnId while the completion fact makes the root appear idle.
720
738
  for (const activityFact of activityFacts)
721
739
  activity.observe(activityFact);
740
+ // Re-evaluate once both halves of the safe continuation boundary are
741
+ // observable. turnCompleted() releases the durable checkpoint before
742
+ // these facts project the idle root; its first evaluation can therefore
743
+ // still see the pre-completion activity snapshot. This second evaluation
744
+ // is idempotently fenced by the durable control row.
745
+ if (completedSession)
746
+ autopilot.evaluate(sessionId);
747
+ // Lifecycle notifications are fast transition evidence, while
748
+ // thread/list is the bounded topology authority. Reconcile after the
749
+ // mutation so canonical identity and every retained child reach both
750
+ // the GUI and Autopilot even when the notification is sparse.
751
+ if (refreshChildTopology)
752
+ void activity.refresh(sessionId);
722
753
  events.publish(journal.append(sessionId, normalized.type, normalized.payload, normalized.occurredAt));
723
754
  if (completedSession)
724
755
  events.publish(journal.append(sessionId, 'session.updated', completedSession, occurredAt));
@@ -1433,6 +1464,10 @@ export async function composeRelayApp(options) {
1433
1464
  const interaction = interactions.find(sessionId, requestId);
1434
1465
  if (!interaction || interaction.kind !== 'orgPlanAttention')
1435
1466
  return { kind: 'noActive' };
1467
+ const decision = parseOrgPlanAttentionToolResponse(response);
1468
+ const attention = parseOrgPlanAttention(interaction.payload);
1469
+ if (!decision || !attention)
1470
+ return { kind: 'staleOperation' };
1436
1471
  const claim = interactions.claimOperation(sessionId, requestId, operationKey);
1437
1472
  if (claim === 'resolved') {
1438
1473
  const resolved = interactions.resolved(sessionId, requestId);
@@ -1448,12 +1483,23 @@ export async function composeRelayApp(options) {
1448
1483
  if (acknowledged) {
1449
1484
  if (!interactions.beginDelivery(sessionId, requestId, operationKey))
1450
1485
  return { kind: 'staleOperation' };
1486
+ if (decision.action === 'resume' && attention.executorReplacement) {
1487
+ const authorization = autopilot.authorizeExecutorReplacement(sessionId, attention.executorReplacement.canonicalTaskName);
1488
+ if (!authorization.accepted) {
1489
+ interactions.retryDelivery(sessionId, requestId, operationKey);
1490
+ return { kind: 'replacementRejected' };
1491
+ }
1492
+ }
1451
1493
  const resolvedAt = new Date().toISOString();
1452
1494
  if (!interactions.settleOperation(sessionId, requestId, operationKey, resolvedAt, 'answered'))
1453
1495
  return { kind: 'staleOperation' };
1454
1496
  const accepted = { kind: 'accepted', resolvedAt };
1455
1497
  idempotency.put(scope, operationKey, 202, JSON.stringify({ kind: 'replayed', resolvedAt }));
1456
1498
  publishAttentionSettlement(sessionId, requestId, resolvedAt, 'answered');
1499
+ if (decision.action === 'disableAutopilot')
1500
+ autopilot.disable(sessionId);
1501
+ else
1502
+ autopilot.enable(sessionId);
1457
1503
  return accepted;
1458
1504
  }
1459
1505
  // A durable capability belongs to the session/thread, not the
@@ -1465,6 +1511,13 @@ export async function composeRelayApp(options) {
1465
1511
  return { kind: 'writerUnavailable' };
1466
1512
  if (!interactions.beginDelivery(sessionId, requestId, operationKey))
1467
1513
  return { kind: 'staleOperation' };
1514
+ if (decision.action === 'resume' && attention.executorReplacement) {
1515
+ const authorization = autopilot.authorizeExecutorReplacement(sessionId, attention.executorReplacement.canonicalTaskName);
1516
+ if (!authorization.accepted) {
1517
+ interactions.retryDelivery(sessionId, requestId, operationKey);
1518
+ return { kind: 'replacementRejected' };
1519
+ }
1520
+ }
1468
1521
  const writer = runtime.attentionWriterState(sessionId, requestId);
1469
1522
  if (writer === 'unavailable') {
1470
1523
  interactions.retryDelivery(sessionId, requestId, operationKey);
@@ -1488,6 +1541,10 @@ export async function composeRelayApp(options) {
1488
1541
  const accepted = { kind: 'accepted', resolvedAt };
1489
1542
  idempotency.put(scope, operationKey, 202, JSON.stringify({ kind: 'replayed', resolvedAt }));
1490
1543
  publishAttentionSettlement(sessionId, requestId, resolvedAt, 'answered');
1544
+ if (decision.action === 'disableAutopilot')
1545
+ autopilot.disable(sessionId);
1546
+ else
1547
+ autopilot.enable(sessionId);
1491
1548
  return accepted;
1492
1549
  })();
1493
1550
  attentionResolutionOperations.set(key, operation);
@@ -196,6 +196,14 @@ function childOutcome(status, action, previous) {
196
196
  return 'cancelled';
197
197
  if (status === 'completed' || status === 'idle')
198
198
  return 'partial';
199
+ // A follow-up reopens the same physical child. Do not keep presenting its
200
+ // previous boundary completion while its new turn is running.
201
+ if (action === 'resume_agent' ||
202
+ status === 'working' ||
203
+ status === 'active' ||
204
+ status === 'running' ||
205
+ status === 'pendingInit')
206
+ return undefined;
199
207
  return previous;
200
208
  }
201
209
  function childState(status, action, previous = 'working') {
@@ -3,15 +3,15 @@
3
3
  * Designed by Denis Roio <jaromil@dyne.org>
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
- export const AUTOPILOT_PROMPT_VERSION = 'v11';
7
- export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Refer to every L1 as L<a> and each nested L2 as L<a>.<b>, using one-based positions. Spawn exactly one executor per L1 and pass the exact literal task_name l<a>: l1 for L1, l2 for L2, and so on. Never create an L2-specific task name or append a title, role, nickname, plan name, or generated label. A validated DONE L2 and an accepted L1 are mandatory answer boundaries. The corresponding gestalt_org_plan_checkpoint call must be the last tool call of that root turn: immediately emit exactly one boundary final and end the turn, without followup_task, review, executor launch, or later milestone work. Roll up commentary, files, verification, and commands since the previous boundary into that one answer; all later milestone activity belongs to a new root turn. On this later Autopilot turn, resume the same executor after an L2 boundary or launch the next executor after an accepted-L1 boundary. Treat a status question as an interruption: answer briefly, then perform the applicable continuation in the same turn. Before sending any blocker response or yielding because progress cannot continue safely, call gestalt_org_plan_attention for the matching decision-table blocker; never rely on blocker prose as the signal. Do not call it for routine progress or a recoverable failure. Before yielding for known long work, call gestalt_autopilot_wait_lease version 2 with relevant observable wake conditions and a bounded maxWaitMs; it is one episode, so reassess before registering another in a later turn. Yield only when its response contains accepted:true; accepted:false means no continuation was registered and requires same-turn supervision. For GitHub PR CI, start an owned gh pr checks --watch process and lease processExited plus processResultAvailable while it runs. When the Autopilot probe is active, register the compatible wait lease, declare genuine attention, or immediately do actionable work; never acknowledge waiting in prose. Do not send a status-only response.';
6
+ export const AUTOPILOT_PROMPT_VERSION = 'v13';
7
+ export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Refer to every L1 as L<a> and each nested L2 as L<a>.<b>, using one-based positions. Spawn exactly one executor per L1 and pass the exact literal task_name l<a>: l1 for L1, l2 for L2, and so on. Never create an L2-specific task name or append a title, role, nickname, plan name, or generated label. A validated DONE L2 and an accepted L1 are mandatory answer boundaries. The corresponding gestalt_org_plan_checkpoint call must be the last tool call of that root turn: immediately emit exactly one boundary final and end the turn, without followup_task, review, executor launch, or later milestone work. An accepted L1 always ends the root turn with a chat answer: never continue into the next L1 or emit post-acceptance documentation as commentary. Roll up commentary, files, verification, and commands since the previous boundary into that one answer; all later milestone activity belongs to a new root turn. On this later Autopilot turn, resume the same executor after an L2 boundary or launch exactly one fresh canonical executor for the next L1 after an accepted-L1 boundary. Treat a status question as an interruption: answer briefly, then perform the applicable continuation in the same turn. Before sending any blocker response or yielding because progress cannot continue safely, call gestalt_org_plan_attention for the matching decision-table blocker; never rely on blocker prose as the signal. When explicit human permission is required for a physical executor replacement, include executorReplacement with the exact canonicalTaskName; Mobile alone computes and persists the physical generation. Do not call attention for routine progress or a recoverable failure. Before yielding for known long work, call gestalt_autopilot_wait_lease version 2 with relevant observable wake conditions and a bounded maxWaitMs; it is one episode, so reassess before registering another in a later turn. Yield only when its response contains accepted:true; accepted:false means no continuation was registered and requires same-turn supervision. For GitHub PR CI, start an owned gh pr checks --watch process and lease processExited plus processResultAvailable while it runs. When the Autopilot probe is active, register the compatible wait lease, declare genuine attention, or immediately do actionable work; never acknowledge waiting in prose. Do not send a status-only response.';
8
8
  export const AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT = 'Continue the same assigned Org L1 from its durable state. A prior turn ending did not complete the objective. Consume any supplied process result and take the next legal L2 action. Whenever an L2 reaches DONE, return its concise structured evidence and end the executor turn so the root can publish that mandatory answer boundary. Also report at the L1 review boundary or through structured attention.';
9
9
  /** Builds a physical launch instruction while keeping durable L1 identity canonical. */
10
10
  export function autopilotExecutorLaunchPrompt(identity) {
11
- const launch = `Launch task_name ${identity.taskName} for canonical ${identity.canonicalPosition}; retain the canonical label in status and review output.`;
11
+ const launch = `Launch task_name ${identity.taskName} for canonical ${identity.canonicalPosition} with agent_type org-plan-executor and reasoning_effort high; the profile selects gpt-5.6-terra by default, so do not override its model. Retain the canonical label in status and review output.`;
12
12
  if (identity.generation === 1)
13
13
  return `${AUTOPILOT_CONTINUATION_PROMPT} ${launch}`;
14
- return `${AUTOPILOT_CONTINUATION_PROMPT} ${launch} This exact replacement generation ${identity.generation} is durably authorized by Mobile; do not infer another generation from this prompt or the roster; the durable ${identity.canonicalTaskName} slot may remain reserved. Do not reuse that task_name or attempt to change an existing agent's model in place. Interrupt the previous physical executor if it is still running, then spawn ${identity.taskName} with agent_type worker and an explicit model selected by the supervisor. Transfer sole ${identity.canonicalPosition} ownership and continue from its durable Org Plan and worktree state.`;
14
+ return `${AUTOPILOT_CONTINUATION_PROMPT} ${launch} This exact replacement generation ${identity.generation} is durably authorized by Mobile; do not infer another generation from this prompt or the roster; the durable ${identity.canonicalTaskName} slot may remain reserved. Do not reuse that task_name or attempt to change an existing agent's model in place. Interrupt the previous physical executor if it is still running, then spawn the authorized ${identity.taskName}. Transfer sole ${identity.canonicalPosition} ownership and continue from its durable Org Plan and worktree state.`;
15
15
  }
16
16
  export const defaultAutopilotPolicy = Object.freeze({
17
17
  quiescenceMs: 1_000,