gestalt-mobile 0.35.4 → 0.35.5

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.
@@ -152,13 +152,13 @@ const autopilotPlanText = (childState = 'TODO') => `#+TITLE: Autopilot fixture
152
152
  const completedAutopilotPlanText = () => autopilotPlanText('DONE')
153
153
  .replace('* WIP [#A] Parent', '* DONE [#A] Parent')
154
154
  .replace(':REVIEW_STATUS: UNREVIEWED', ':REVIEW_STATUS: REVIEWED');
155
- async function installAutopilotPlan(app, sessionId, workspacePath, name, childState = 'TODO') {
155
+ async function installAutopilotPlan(app, sessionId, workspacePath, name, childState = 'TODO', reason = 'supervision-start') {
156
156
  const planPath = join(workspacePath, `${name}.org`);
157
157
  await writeFile(planPath, autopilotPlanText(childState));
158
158
  await writeFile(planStatusFilePath(planStatusDirectoryPath(workspacePath, sessionId), planPath), JSON.stringify({
159
159
  schemaVersion: 1,
160
160
  planPath,
161
- reason: 'supervision-start',
161
+ reason,
162
162
  updatedAt: new Date().toISOString(),
163
163
  }));
164
164
  await expect
@@ -2242,6 +2242,37 @@ describe('production composition', () => {
2242
2242
  });
2243
2243
  });
2244
2244
  describeCompositionConcern('lifecycle', () => {
2245
+ it('retains an authored plan without starting supervision', async () => {
2246
+ const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
2247
+ const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
2248
+ ownTemporaryPaths(root, dataDir);
2249
+ const workspacePath = join(root, 'workspace');
2250
+ await mkdir(workspacePath);
2251
+ const handles = [];
2252
+ const app = await composeAuthorizedApp({
2253
+ root,
2254
+ dataDir,
2255
+ relyingParty,
2256
+ installedCodexVersion: 'codex-cli 0.144.3',
2257
+ startAppServers: true,
2258
+ launchAppServer: liveAppServer(handles),
2259
+ profiles: {
2260
+ list: async () => [],
2261
+ require: async () => ({
2262
+ name: 'default',
2263
+ state: 'ok',
2264
+ status: 'ready',
2265
+ }),
2266
+ },
2267
+ });
2268
+ const sessionId = await createComposedSession(app);
2269
+ await installAutopilotPlan(app, sessionId, workspacePath, 'authored-only', 'TODO', 'authoring-start');
2270
+ expect((await app.inject(`/api/sessions/${sessionId}`)).json()).toMatchObject({
2271
+ autopilot: { enabled: false, state: 'disabled' },
2272
+ });
2273
+ expect(handles.flatMap((handle) => handle.requests.filter((request) => request.method === 'turn/start'))).toHaveLength(0);
2274
+ await app.close();
2275
+ });
2245
2276
  it('rejects a wait lease when automatic continuation is unavailable', async () => {
2246
2277
  const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
2247
2278
  const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
@@ -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 = 'v9';
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. 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. 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 = 'v10';
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.';
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) {
@@ -201,9 +201,10 @@ function measurementFor(heading) {
201
201
  if (value === undefined)
202
202
  continue;
203
203
  if (property.endsWith('_AT')) {
204
- if (!isUtcIsoInstant(value))
204
+ const instant = normalizeUtcIsoInstant(value);
205
+ if (!instant)
205
206
  return null;
206
- measurement[field] = value;
207
+ measurement[field] = instant;
207
208
  continue;
208
209
  }
209
210
  if (!/^\d+$/.test(value))
@@ -215,14 +216,38 @@ function measurementFor(heading) {
215
216
  }
216
217
  return Object.keys(measurement).length === 0 ? undefined : measurement;
217
218
  }
218
- function isUtcIsoInstant(value) {
219
- if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value))
220
- return false;
219
+ function normalizeUtcIsoInstant(value) {
220
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3}))?(Z|[+-]\d{2}:\d{2})$/.exec(value);
221
+ if (!match)
222
+ return null;
223
+ const [, yearValue, monthValue, dayValue, hourValue, minuteValue, secondValue, , zone] = match;
224
+ const [year, month, day, hour, minute, second] = [
225
+ yearValue,
226
+ monthValue,
227
+ dayValue,
228
+ hourValue,
229
+ minuteValue,
230
+ secondValue,
231
+ ].map(Number);
232
+ const offset = zone === 'Z' ? null : zone.slice(1).split(':').map(Number);
233
+ if (year < 1 ||
234
+ month < 1 ||
235
+ month > 12 ||
236
+ day < 1 ||
237
+ day > new Date(Date.UTC(year, month, 0)).getUTCDate() ||
238
+ hour > 23 ||
239
+ minute > 59 ||
240
+ second > 59 ||
241
+ (offset && (offset[0] > 23 || offset[1] > 59)))
242
+ return null;
221
243
  const instant = new Date(value);
222
244
  if (Number.isNaN(instant.getTime()))
223
- return false;
224
- const canonical = instant.toISOString();
225
- return value === canonical || value === canonical.replace('.000Z', 'Z');
245
+ return null;
246
+ if (zone === 'Z') {
247
+ const canonical = instant.toISOString();
248
+ return value === canonical || value === canonical.replace('.000Z', 'Z') ? value : null;
249
+ }
250
+ return instant.toISOString().replace('.000Z', 'Z');
226
251
  }
227
252
  function descriptionFor(heading) {
228
253
  const fields = heading.level === 1 ? ['Effort', 'Goal', 'Notes'] : ['Why', 'Change', 'Tests', 'Done when'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt-mobile",
3
- "version": "0.35.4",
3
+ "version": "0.35.5",
4
4
  "description": "Mobile-first web relay for durable Codex development sessions",
5
5
  "keywords": [
6
6
  "codex",