gestalt-mobile 0.32.1 → 0.34.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,8 +16,8 @@ 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-CxAG4Fmv.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-CcWoGWyP.css">
19
+ <script type="module" crossorigin src="/assets/index-nivA6-l4.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-CPQBJDsg.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="app"></div>
@@ -28,6 +28,30 @@ Options:
28
28
  --skills list List saved global skill profiles without starting the server
29
29
  --help Show this help
30
30
  --version Show the installed version`;
31
+ function managedVersion(value) {
32
+ return value && /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,99}$/.test(value) ? value : null;
33
+ }
34
+ export function componentVersions(mobileVersion, codexVersion, environment = process.env) {
35
+ return [
36
+ {
37
+ id: 'gestalt',
38
+ label: 'Gestalt manager',
39
+ version: managedVersion(environment.GESTALT_MANAGER_VERSION),
40
+ },
41
+ { id: 'gestalt-mobile', label: 'Gestalt Mobile', version: mobileVersion },
42
+ {
43
+ id: 'gestalt-agents',
44
+ label: 'Gestalt Agents',
45
+ version: managedVersion(environment.GESTALT_AGENTS_VERSION),
46
+ },
47
+ {
48
+ id: 'context-mode',
49
+ label: 'Context Mode',
50
+ version: managedVersion(environment.GESTALT_CONTEXT_MODE_VERSION),
51
+ },
52
+ { id: 'codex', label: 'Codex CLI', version: codexVersion },
53
+ ];
54
+ }
31
55
  export async function runStartupDoctor(cwd) {
32
56
  if (process.env.GESTALT_STARTUP_DIAGNOSTICS_DONE === '1')
33
57
  return { ok: true, report: '' };
@@ -180,6 +204,8 @@ export async function runCli(dependencies = {}) {
180
204
  stderr.write(diagnosis.report);
181
205
  if (!diagnosis.ok)
182
206
  stderr.write('WARNING: Startup diagnostics reported a problem; Gestalt Mobile will continue so it can show recovery controls.\n');
207
+ const installedCodexVersion = await (dependencies.probeCodexVersion ?? probeCodexVersion)();
208
+ const mobileVersion = await packageVersion(moduleUrl);
183
209
  const app = await (dependencies.compose ?? composeRelayApp)({
184
210
  root: config.root,
185
211
  dataDir: config.dataDir,
@@ -187,7 +213,8 @@ export async function runCli(dependencies = {}) {
187
213
  passkeyAuthEnabled: config.passkeyAuthEnabled,
188
214
  staticDir: packagedClientDir(moduleUrl),
189
215
  profiles: new LauncherProfileCatalog(),
190
- installedCodexVersion: await (dependencies.probeCodexVersion ?? probeCodexVersion)(),
216
+ installedCodexVersion,
217
+ componentVersions: componentVersions(mobileVersion, installedCodexVersion, dependencies.environment),
191
218
  startAppServers: true,
192
219
  homeDirectory: dependencies.homeDirectory,
193
220
  explicitSkillProfile,
@@ -711,6 +711,7 @@ export async function composeRelayApp(options) {
711
711
  };
712
712
  }),
713
713
  },
714
+ versions: options.componentVersions,
714
715
  protocolCompatible: protocol.compatible,
715
716
  },
716
717
  sessionRoutes: {
@@ -3,7 +3,7 @@
3
3
  * Designed by Denis Roio <jaromil@dyne.org>
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
- /** Public roster projection: never expose process, thread, task-path, or host metrics identifiers. */
6
+ /** Public roster projection: expose bounded context use, never raw host metrics or identifiers. */
7
7
  export function toAgentActivityDto(snapshot) {
8
8
  return {
9
9
  sessionId: snapshot.sessionId,
@@ -19,6 +19,9 @@ export function toAgentActivityDto(snapshot) {
19
19
  ? { continuationGeneration: child.continuationGeneration }
20
20
  : {}),
21
21
  ...(child.outcome ? { outcome: child.outcome } : {}),
22
+ ...(child.contextUsedPercent !== undefined
23
+ ? { contextUsedPercent: child.contextUsedPercent }
24
+ : {}),
22
25
  ownedProcesses: (child.ownedProcesses ?? []).slice(0, 64).map((process) => ({
23
26
  state: process.state,
24
27
  ownership: process.ownership,
@@ -40,6 +40,12 @@ export function projectAgentActivity(current, fact) {
40
40
  const child = current.subagents.find((candidate) => candidate.threadId === fact.threadId);
41
41
  if (!child)
42
42
  return current;
43
+ if (fact.kind === 'contextUsage' && validPercent(fact.contextUsedPercent)) {
44
+ const subagents = Object.freeze(current.subagents.map((candidate) => candidate.id === child.id
45
+ ? Object.freeze({ ...candidate, contextUsedPercent: fact.contextUsedPercent })
46
+ : candidate));
47
+ return Object.freeze({ ...current, subagents });
48
+ }
43
49
  const childFact = { ...fact };
44
50
  delete childFact.threadId;
45
51
  return projectAgentActivity(current, {
@@ -53,7 +59,9 @@ export function projectAgentActivity(current, fact) {
53
59
  const root = {
54
60
  ...current.root,
55
61
  observedAt,
56
- lastActivityAt: later(current.root.lastActivityAt, fact.occurredAt),
62
+ lastActivityAt: fact.kind === 'contextUsage'
63
+ ? current.root.lastActivityAt
64
+ : later(current.root.lastActivityAt, fact.occurredAt),
57
65
  };
58
66
  const children = new Map(current.subagents.map((child) => [child.id, child]));
59
67
  if (fact.kind === 'turnStarted')
@@ -75,6 +83,8 @@ export function projectAgentActivity(current, fact) {
75
83
  Object.assign(root, { state: 'disconnected', reason: 'processExited' });
76
84
  if (fact.kind === 'threadStatus')
77
85
  applyStatus(root, fact.status);
86
+ if (fact.kind === 'contextUsage' && validPercent(fact.contextUsedPercent))
87
+ root.contextUsedPercent = fact.contextUsedPercent;
78
88
  if (fact.kind === 'collaboration') {
79
89
  // Older servers may omit experimental child metadata. That is a capability
80
90
  // downgrade, not evidence that the root process disappeared.
@@ -117,6 +127,11 @@ export function projectAgentActivity(current, fact) {
117
127
  ? { canonicalTaskName: 'final_review' }
118
128
  : {}),
119
129
  ...(outcome ? { outcome } : {}),
130
+ ...(validPercent(fact.contextUsedPercent)
131
+ ? { contextUsedPercent: fact.contextUsedPercent }
132
+ : validPercent(before?.contextUsedPercent)
133
+ ? { contextUsedPercent: before.contextUsedPercent }
134
+ : {}),
120
135
  ...(fact.childOwnedProcesses
121
136
  ? { ownedProcesses: Object.freeze([...fact.childOwnedProcesses]) }
122
137
  : before?.ownedProcesses
@@ -224,6 +239,9 @@ function aggregate(children) {
224
239
  function validTimestamp(value) {
225
240
  return !Number.isNaN(Date.parse(value));
226
241
  }
242
+ function validPercent(value) {
243
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 100;
244
+ }
227
245
  function later(left, right) {
228
246
  return Date.parse(right) > Date.parse(left) ? right : left;
229
247
  }
@@ -235,12 +253,17 @@ function semantic(snapshot) {
235
253
  sessionId: snapshot.sessionId,
236
254
  rootThreadId: snapshot.rootThreadId,
237
255
  confidence: snapshot.confidence,
238
- root: { state: snapshot.root.state, reason: snapshot.root.reason },
256
+ root: {
257
+ state: snapshot.root.state,
258
+ reason: snapshot.root.reason,
259
+ contextUsedPercent: snapshot.root.contextUsedPercent,
260
+ },
239
261
  subagents: snapshot.subagents.map((child) => ({
240
262
  id: child.id,
241
263
  threadId: child.threadId,
242
264
  nickname: child.nickname,
243
265
  role: child.role,
266
+ contextUsedPercent: child.contextUsedPercent,
244
267
  model: child.model,
245
268
  taskPath: child.taskPath,
246
269
  canonicalTaskName: child.canonicalTaskName,
@@ -3,8 +3,8 @@
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 = 'v6';
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. For a subagent dedicated to that position, use the collaboration-safe task_name l<a> or l<a>_<b> and refer to it by its canonical L label. If the active L1 is still WIP and its executor stopped or is idle after a partial report or L2 checkpoint, call followup_task on that same executor before sending any response. Treat a status question as an interruption: answer briefly, then perform that continuation in the same turn. Invoke gestalt_org_plan_attention only for a decision-table blocker. When the Autopilot probe is active, register gestalt_autopilot_wait_lease with an observable wake condition, 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 = 'v7';
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. For a subagent dedicated to that position, use the collaboration-safe task_name l<a> or l<a>_<b> and refer to it by its canonical L label. If the active L1 is still WIP and its executor stopped or is idle after a partial report or L2 checkpoint, call followup_task on that same executor before sending any response. Treat a status question as an interruption: answer briefly, then perform that 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. When the Autopilot probe is active, register gestalt_autopilot_wait_lease with an observable wake condition, 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, take the next legal L2 action, and report only 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) {
@@ -462,16 +462,20 @@ export class AutopilotCoordinator {
462
462
  validStructuredBlock(state.blocking);
463
463
  if (checkpointBoundary && state?.checkpoints) {
464
464
  const occurredAt = this.deps.now();
465
+ const pendingKind = state.checkpoints.pendingKind ??
466
+ (state.checkpoints.terminalReviewAccepted ? 'terminalReviewAccepted' : 'l1Accepted');
465
467
  this.persist({
466
468
  ...state,
467
- checkpoints: { ...state.checkpoints, pendingTurnId: null },
469
+ checkpoints: { ...state.checkpoints, pendingTurnId: null, pendingKind: null },
468
470
  updatedAt: occurredAt,
469
471
  }, undefined, [
470
472
  {
471
473
  sessionId,
472
- type: state.checkpoints.terminalReviewAccepted
473
- ? 'org-plan.terminal-review-reported'
474
- : 'org-plan.milestone-reported',
474
+ type: pendingKind === 'l2Completed'
475
+ ? 'org-plan.step-reported'
476
+ : pendingKind === 'terminalReviewAccepted'
477
+ ? 'org-plan.terminal-review-reported'
478
+ : 'org-plan.milestone-reported',
475
479
  payload: { turnId: state.checkpoints.pendingTurnId },
476
480
  occurredAt,
477
481
  },
@@ -503,32 +507,47 @@ export class AutopilotCoordinator {
503
507
  const previous = prior.checkpoints;
504
508
  if (previous && previous.planIdentity !== retained.identity)
505
509
  return false;
506
- const reported = previous?.reportedL1Ids ?? [];
510
+ const reportedL1Ids = previous?.reportedL1Ids ?? [];
511
+ const reportedL2Ids = previous?.reportedL2Ids ?? [];
507
512
  const key = createHash('sha256').update(JSON.stringify(checkpoint)).digest('hex');
508
513
  const acceptedKeys = previous?.acceptedKeys ?? [];
509
514
  if (acceptedKeys.includes(key))
510
515
  return true;
511
- if (checkpoint.kind === 'l1Accepted' && reported.includes(checkpoint.l1Id))
516
+ const l2Key = checkpoint.kind === 'l2Completed' ? JSON.stringify([checkpoint.l1Id, checkpoint.l2Id]) : null;
517
+ if (l2Key && reportedL2Ids.includes(l2Key))
518
+ return false;
519
+ if (checkpoint.kind === 'l1Accepted' && reportedL1Ids.includes(checkpoint.l1Id))
512
520
  return false;
513
521
  if (checkpoint.kind === 'terminalReviewAccepted' && previous?.terminalReviewAccepted)
514
522
  return false;
515
523
  const checkpoints = {
516
524
  protocolVersion: 1,
517
525
  planIdentity: retained.identity,
518
- reportedL1Ids: checkpoint.kind === 'l1Accepted' ? [...reported, checkpoint.l1Id] : reported,
526
+ reportedL2Ids: l2Key ? [...reportedL2Ids, l2Key] : reportedL2Ids,
527
+ reportedL1Ids: checkpoint.kind === 'l1Accepted' ? [...reportedL1Ids, checkpoint.l1Id] : reportedL1Ids,
519
528
  acceptedKeys: [...acceptedKeys, key],
520
529
  pendingTurnId: turnId,
530
+ pendingKind: checkpoint.kind,
521
531
  terminalReviewAccepted: checkpoint.kind === 'terminalReviewAccepted' || previous?.terminalReviewAccepted === true,
522
532
  };
523
533
  this.persist({ ...prior, checkpoints, updatedAt: occurredAt }, undefined, [
524
534
  {
525
535
  sessionId,
526
- type: checkpoint.kind === 'l1Accepted'
527
- ? 'org-plan.milestone-checkpointed'
528
- : 'org-plan.terminal-review-checkpointed',
529
- payload: checkpoint.kind === 'l1Accepted'
530
- ? { l1Id: checkpoint.l1Id, position: checkpoint.position, turnId }
531
- : { turnId },
536
+ type: checkpoint.kind === 'l2Completed'
537
+ ? 'org-plan.step-checkpointed'
538
+ : checkpoint.kind === 'l1Accepted'
539
+ ? 'org-plan.milestone-checkpointed'
540
+ : 'org-plan.terminal-review-checkpointed',
541
+ payload: checkpoint.kind === 'l2Completed'
542
+ ? {
543
+ l1Id: checkpoint.l1Id,
544
+ l2Id: checkpoint.l2Id,
545
+ position: checkpoint.position,
546
+ turnId,
547
+ }
548
+ : checkpoint.kind === 'l1Accepted'
549
+ ? { l1Id: checkpoint.l1Id, position: checkpoint.position, turnId }
550
+ : { turnId },
532
551
  occurredAt,
533
552
  },
534
553
  ]);
@@ -152,6 +152,13 @@ function parseCheckpoints(value) {
152
152
  const planIdentity = boundedText(value.planIdentity);
153
153
  if (value.protocolVersion !== 1 || !planIdentity)
154
154
  return undefined;
155
+ const rawReportedL2Ids = value.reportedL2Ids ?? [];
156
+ if (!Array.isArray(rawReportedL2Ids) || rawReportedL2Ids.length > 512)
157
+ return undefined;
158
+ const reportedL2Ids = rawReportedL2Ids.map((id) => boundedText(id)).filter(Boolean);
159
+ if (reportedL2Ids.length !== rawReportedL2Ids.length ||
160
+ new Set(reportedL2Ids).size !== reportedL2Ids.length)
161
+ return undefined;
155
162
  if (!Array.isArray(value.reportedL1Ids) || value.reportedL1Ids.length > 128)
156
163
  return undefined;
157
164
  const reportedL1Ids = value.reportedL1Ids
@@ -160,7 +167,7 @@ function parseCheckpoints(value) {
160
167
  if (reportedL1Ids.length !== value.reportedL1Ids.length ||
161
168
  new Set(reportedL1Ids).size !== reportedL1Ids.length)
162
169
  return undefined;
163
- if (!Array.isArray(value.acceptedKeys) || value.acceptedKeys.length > 128)
170
+ if (!Array.isArray(value.acceptedKeys) || value.acceptedKeys.length > 768)
164
171
  return undefined;
165
172
  const acceptedKeys = value.acceptedKeys
166
173
  .map((key) => boundedText(key))
@@ -173,12 +180,24 @@ function parseCheckpoints(value) {
173
180
  return undefined;
174
181
  if (typeof value.terminalReviewAccepted !== 'boolean')
175
182
  return undefined;
183
+ const pendingKind = value.pendingKind === undefined
184
+ ? pendingTurnId
185
+ ? value.terminalReviewAccepted
186
+ ? 'terminalReviewAccepted'
187
+ : 'l1Accepted'
188
+ : null
189
+ : value.pendingKind;
190
+ if (pendingKind !== null &&
191
+ !['l2Completed', 'l1Accepted', 'terminalReviewAccepted'].includes(String(pendingKind)))
192
+ return undefined;
176
193
  return {
177
194
  protocolVersion: 1,
178
195
  planIdentity,
196
+ reportedL2Ids,
179
197
  reportedL1Ids,
180
198
  acceptedKeys,
181
199
  pendingTurnId,
200
+ pendingKind: pendingKind,
182
201
  terminalReviewAccepted: value.terminalReviewAccepted,
183
202
  };
184
203
  }
@@ -14,6 +14,7 @@ export async function getBootstrap(deps) {
14
14
  profiles,
15
15
  models,
16
16
  sessions: deps.sessions.list(),
17
+ versions: deps.versions ?? [],
17
18
  capabilities: {
18
19
  approvals: true,
19
20
  userInput: true,
@@ -13,6 +13,14 @@ export function validOrgPlanCheckpoint(input) {
13
13
  if (!l1)
14
14
  return false;
15
15
  const position = `L${plan.steps.indexOf(l1) + 1}`;
16
+ if (checkpoint.kind === 'l2Completed') {
17
+ const l2 = l1.children.find((step) => step.id === checkpoint.l2Id);
18
+ if (!l2)
19
+ return false;
20
+ return (checkpoint.position === `${position}.${l1.children.indexOf(l2) + 1}` &&
21
+ l2.state === 'DONE' &&
22
+ l1.reviewStatus === 'UNREVIEWED');
23
+ }
16
24
  return (checkpoint.position === position &&
17
25
  l1.state === 'DONE' &&
18
26
  l1.reviewStatus === 'REVIEWED' &&
@@ -68,6 +68,29 @@ export function decodeAgentActivityFacts(sessionId, occurredAt, input) {
68
68
  },
69
69
  ]
70
70
  : [];
71
+ if (input.method === 'thread/tokenUsage/updated') {
72
+ const usage = record(params.tokenUsage);
73
+ const last = record(usage?.last);
74
+ const totalTokens = last?.totalTokens;
75
+ const modelContextWindow = usage?.modelContextWindow;
76
+ if (!id(params.threadId) ||
77
+ typeof totalTokens !== 'number' ||
78
+ !Number.isFinite(totalTokens) ||
79
+ totalTokens < 0 ||
80
+ typeof modelContextWindow !== 'number' ||
81
+ !Number.isFinite(modelContextWindow) ||
82
+ modelContextWindow <= 0)
83
+ return [];
84
+ return [
85
+ {
86
+ sessionId,
87
+ occurredAt,
88
+ kind: 'contextUsage',
89
+ threadId: id(params.threadId),
90
+ contextUsedPercent: Math.min(100, Math.max(0, Math.round((totalTokens / modelContextWindow) * 100))),
91
+ },
92
+ ];
93
+ }
71
94
  if (input.method === 'item/started' || input.method === 'item/completed') {
72
95
  const item = record(params.item);
73
96
  if (item?.type !== 'collabAgentToolCall' && item?.type !== 'collabToolCall')
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright (C) 2026 Dyne.org foundation
3
+ * Designed by Denis Roio <jaromil@dyne.org>
4
+ * SPDX-License-Identifier: AGPL-3.0-or-later
5
+ */
6
+ export {};
@@ -10,17 +10,19 @@ const record = (value) => typeof value === 'object' && value !== null && !Array.
10
10
  export const gestaltOrgPlanCheckpointDynamicTool = {
11
11
  type: 'function',
12
12
  name: GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME,
13
- description: 'Record a validated supervised Org Plan report boundary. This is not review authority and must only be called by the active root after the Org Plan helper has recorded the matching accepted state.',
13
+ description: 'Record a validated supervised Org Plan report boundary. This is not review authority and must only be called by the active root after the Org Plan helper has recorded the matching state.',
14
14
  inputSchema: {
15
15
  type: 'object',
16
16
  additionalProperties: false,
17
- required: ['version', 'kind', 'planIdentity', 'verdict'],
17
+ required: ['version', 'kind', 'planIdentity'],
18
18
  properties: {
19
19
  version: { const: 1 },
20
- kind: { type: 'string', enum: ['l1Accepted', 'terminalReviewAccepted'] },
20
+ kind: { type: 'string', enum: ['l2Completed', 'l1Accepted', 'terminalReviewAccepted'] },
21
21
  planIdentity: { type: 'string', minLength: 1, maxLength: 128 },
22
22
  l1Id: { type: 'string', minLength: 1, maxLength: 128 },
23
- position: { type: 'string', pattern: '^L[1-9][0-9]*$', maxLength: 32 },
23
+ l2Id: { type: 'string', minLength: 1, maxLength: 128 },
24
+ position: { type: 'string', pattern: '^L[1-9][0-9]*(\\.[1-9][0-9]*)?$', maxLength: 32 },
25
+ status: { const: 'DONE' },
24
26
  verdict: { const: 'ACCEPT' },
25
27
  commit: {
26
28
  oneOf: [
@@ -43,24 +45,86 @@ export const gestaltOrgPlanCheckpointDynamicTool = {
43
45
  ],
44
46
  },
45
47
  findings: { type: 'string', minLength: 1, maxLength: 600 },
48
+ changes: { type: 'string', minLength: 1, maxLength: 600 },
49
+ files: { type: 'string', minLength: 1, maxLength: 600 },
46
50
  tests: { type: 'string', minLength: 1, maxLength: 600 },
47
51
  },
48
52
  oneOf: [
49
- { properties: { kind: { const: 'l1Accepted' } }, required: ['l1Id', 'position', 'commit'] },
53
+ {
54
+ properties: { kind: { const: 'l2Completed' } },
55
+ required: ['l1Id', 'l2Id', 'position', 'status', 'changes', 'files', 'tests'],
56
+ not: {
57
+ anyOf: [{ required: ['verdict'] }, { required: ['commit'] }, { required: ['findings'] }],
58
+ },
59
+ },
60
+ {
61
+ properties: { kind: { const: 'l1Accepted' } },
62
+ required: ['l1Id', 'position', 'verdict', 'commit'],
63
+ not: {
64
+ anyOf: [
65
+ { required: ['l2Id'] },
66
+ { required: ['status'] },
67
+ { required: ['changes'] },
68
+ { required: ['files'] },
69
+ ],
70
+ },
71
+ },
50
72
  {
51
73
  properties: { kind: { const: 'terminalReviewAccepted' } },
74
+ required: ['verdict'],
52
75
  not: {
53
- anyOf: [{ required: ['l1Id'] }, { required: ['position'] }, { required: ['commit'] }],
76
+ anyOf: [
77
+ { required: ['l1Id'] },
78
+ { required: ['l2Id'] },
79
+ { required: ['position'] },
80
+ { required: ['status'] },
81
+ { required: ['commit'] },
82
+ { required: ['changes'] },
83
+ { required: ['files'] },
84
+ ],
54
85
  },
55
86
  },
56
87
  ],
57
88
  },
58
89
  };
59
90
  export function parseOrgPlanCheckpoint(value) {
60
- if (!record(value) ||
61
- value.version !== 1 ||
62
- !bounded(value.planIdentity, 128) ||
63
- value.verdict !== 'ACCEPT')
91
+ if (!record(value) || value.version !== 1 || !bounded(value.planIdentity, 128))
92
+ return null;
93
+ if (value.kind === 'l2Completed') {
94
+ if (!bounded(value.l1Id, 128) ||
95
+ !bounded(value.l2Id, 128) ||
96
+ !isL2Position(value.position) ||
97
+ value.status !== 'DONE' ||
98
+ !bounded(value.changes, 600) ||
99
+ !bounded(value.files, 600) ||
100
+ !bounded(value.tests, 600) ||
101
+ Object.keys(value).some((key) => ![
102
+ 'version',
103
+ 'kind',
104
+ 'planIdentity',
105
+ 'l1Id',
106
+ 'l2Id',
107
+ 'position',
108
+ 'status',
109
+ 'changes',
110
+ 'files',
111
+ 'tests',
112
+ ].includes(key)))
113
+ return null;
114
+ return {
115
+ version: 1,
116
+ kind: 'l2Completed',
117
+ planIdentity: value.planIdentity,
118
+ l1Id: value.l1Id,
119
+ l2Id: value.l2Id,
120
+ position: value.position,
121
+ status: 'DONE',
122
+ changes: value.changes,
123
+ files: value.files,
124
+ tests: value.tests,
125
+ };
126
+ }
127
+ if (value.verdict !== 'ACCEPT')
64
128
  return null;
65
129
  if (value.findings !== undefined && !bounded(value.findings, 600))
66
130
  return null;
@@ -118,6 +182,9 @@ export function toOrgPlanCheckpointToolResponse() {
118
182
  function isPosition(value) {
119
183
  return typeof value === 'string' && /^L[1-9][0-9]*$/.test(value);
120
184
  }
185
+ function isL2Position(value) {
186
+ return typeof value === 'string' && /^L[1-9][0-9]*\.[1-9][0-9]*$/.test(value);
187
+ }
121
188
  function parseCommit(value) {
122
189
  if (value.kind === 'notRequired' && Object.keys(value).length === 1)
123
190
  return { kind: 'notRequired' };
@@ -6,6 +6,7 @@
6
6
  "supervision-start",
7
7
  "wait-lease-tool",
8
8
  "checkpoint-tool",
9
+ "l2-report-boundary",
9
10
  "agent-capacity-recovery",
10
11
  "controller-status",
11
12
  "canonical-agent-identity",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt-mobile",
3
- "version": "0.32.1",
3
+ "version": "0.34.0",
4
4
  "description": "Mobile-first web relay for durable Codex development sessions",
5
5
  "keywords": [
6
6
  "codex",