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.
@@ -17,10 +17,15 @@ import { threadPlanName } from './thread-plan-name.js';
17
17
  import { WriterAcquisitionError, } from '../../features/sessions/application/writer-acquisition.js';
18
18
  import { isCodexThreadWriterBusy, isMissingCodexThreadRollout } from './json-rpc-client.js';
19
19
  import { resolvedServerRequestId } from './server-request.js';
20
+ /** Sticky thread config keeps skill paths out of the app-server process argv. */
21
+ export function threadSkillConfig(skillsConfig) {
22
+ return skillsConfig === undefined ? {} : { config: { skills: { config: skillsConfig } } };
23
+ }
20
24
  /** One private owner for every resource acquired for a live Codex child. */
21
25
  class SessionResource {
22
26
  sessionId;
23
27
  process;
28
+ skillsConfig;
24
29
  planStatusLease;
25
30
  planMeasurementToken;
26
31
  unregister;
@@ -39,9 +44,10 @@ class SessionResource {
39
44
  completedProcessActions = new Map();
40
45
  turnThreads = new Map();
41
46
  childThreads = new Set();
42
- constructor(sessionId, process, planStatusLease, planMeasurementToken, unregister, onDisposed) {
47
+ constructor(sessionId, process, skillsConfig, planStatusLease, planMeasurementToken, unregister, onDisposed) {
43
48
  this.sessionId = sessionId;
44
49
  this.process = process;
50
+ this.skillsConfig = skillsConfig;
45
51
  this.planStatusLease = planStatusLease;
46
52
  this.planMeasurementToken = planMeasurementToken;
47
53
  this.unregister = unregister;
@@ -117,7 +123,7 @@ export class CodexSessionRuntime {
117
123
  clientInfo: { name: 'gestalt-mobile', version: '0.1.0' },
118
124
  capabilities: { experimentalApi: true },
119
125
  });
120
- const startedThreadId = await this.startThread(resource.process, session);
126
+ const startedThreadId = await this.startThread(resource.process, session, resource.skillsConfig);
121
127
  resource.threadId = startedThreadId;
122
128
  this.sessions.set(session.id, resource);
123
129
  await this.writePendingThreadName(session.id);
@@ -671,6 +677,7 @@ export class CodexSessionRuntime {
671
677
  await resource.process.rpc.request('thread/resume', {
672
678
  threadId: session.threadId,
673
679
  cwd: session.workspacePath,
680
+ ...threadSkillConfig(resource.skillsConfig),
674
681
  ...(session.executionPolicy?.approvalPolicy
675
682
  ? { approvalPolicy: session.executionPolicy.approvalPolicy }
676
683
  : {}),
@@ -692,7 +699,7 @@ export class CodexSessionRuntime {
692
699
  catch (error) {
693
700
  if (!canRebindMissingRollout(session, error))
694
701
  throw error;
695
- const replacementThreadId = await this.startThread(resource.process, session);
702
+ const replacementThreadId = await this.startThread(resource.process, session, resource.skillsConfig);
696
703
  result = rebindMissingRollout(session, error, replacementThreadId, now);
697
704
  }
698
705
  resource.threadId = result.session.threadId;
@@ -760,12 +767,13 @@ export class CodexSessionRuntime {
760
767
  reject(new Error('CODEX_SERVER_REQUEST_UNSUPPORTED'));
761
768
  });
762
769
  }
763
- async startThread(process, session) {
764
- return decodeThreadStart(await process.rpc.request('thread/start', this.threadStartParams(session)));
770
+ async startThread(process, session, skillsConfig) {
771
+ return decodeThreadStart(await process.rpc.request('thread/start', this.threadStartParams(session, skillsConfig)));
765
772
  }
766
- threadStartParams(session) {
773
+ threadStartParams(session, skillsConfig) {
767
774
  return {
768
775
  cwd: session.workspacePath,
776
+ ...threadSkillConfig(skillsConfig),
769
777
  approvalPolicy: session.executionPolicy?.approvalPolicy ?? 'on-request',
770
778
  dynamicTools: [
771
779
  gestaltQuizDynamicTool,
@@ -792,10 +800,10 @@ export class CodexSessionRuntime {
792
800
  : undefined;
793
801
  try {
794
802
  const token = randomUUID();
803
+ const skillsConfig = await this.resolveSkills?.(session);
795
804
  const process = this.launch({
796
805
  profile: session.profile,
797
806
  cwd: session.workspacePath,
798
- skillsConfig: await this.resolveSkills?.(session),
799
807
  ...(lease || this.planMeasurementBaseUrl
800
808
  ? {
801
809
  environment: {
@@ -812,7 +820,7 @@ export class CodexSessionRuntime {
812
820
  }
813
821
  : {}),
814
822
  });
815
- const resource = new SessionResource(session.id, process, lease, token, [], () => {
823
+ const resource = new SessionResource(session.id, process, skillsConfig, lease, token, [], () => {
816
824
  if (this.sessions.get(session.id) === resource)
817
825
  this.sessions.delete(session.id);
818
826
  });
@@ -41,6 +41,12 @@ export const gestaltOrgPlanAttentionDynamicTool = {
41
41
  summary: { type: 'string', minLength: 1, maxLength: 600 },
42
42
  requestedAction: { type: 'string', minLength: 1, maxLength: 600 },
43
43
  resumeCondition: { type: 'string', enum: orgPlanAttentionResumeConditions },
44
+ executorReplacement: {
45
+ type: 'object',
46
+ additionalProperties: false,
47
+ required: ['canonicalTaskName'],
48
+ properties: { canonicalTaskName: { type: 'string', pattern: '^l[1-9][0-9]*$' } },
49
+ },
44
50
  },
45
51
  oneOf: orgPlanAttentionReasons.map((reason) => ({
46
52
  properties: {
@@ -66,19 +72,31 @@ export function parseOrgPlanAttention(value) {
66
72
  requestedAction: `Satisfy the ${value.resumeCondition} resume condition, then resume or disable Autopilot.`,
67
73
  resumeCondition: value.resumeCondition,
68
74
  };
69
- if (keys.length !== 4)
75
+ if (keys.length !== 4 && keys.length !== 5)
76
+ return null;
77
+ if (keys.some((key) => ![
78
+ 'reason',
79
+ 'summary',
80
+ 'requestedAction',
81
+ 'resumeCondition',
82
+ 'executorReplacement',
83
+ ].includes(key)))
70
84
  return null;
85
+ const executorReplacement = parseExecutorReplacement(value.executorReplacement);
71
86
  if (!isReason(value.reason) ||
72
87
  !isBoundedText(value.summary, 600) ||
73
88
  !isBoundedText(value.requestedAction, 600) ||
74
89
  !isResumeCondition(value.resumeCondition) ||
75
- orgPlanAttentionResumeConditionByReason[value.reason] !== value.resumeCondition)
90
+ orgPlanAttentionResumeConditionByReason[value.reason] !== value.resumeCondition ||
91
+ (value.executorReplacement !== undefined && !executorReplacement) ||
92
+ (executorReplacement && value.reason !== 'permissionRequired'))
76
93
  return null;
77
94
  return {
78
95
  reason: value.reason,
79
96
  summary: value.summary,
80
97
  requestedAction: value.requestedAction,
81
98
  resumeCondition: value.resumeCondition,
99
+ ...(executorReplacement ? { executorReplacement } : {}),
82
100
  };
83
101
  }
84
102
  export function parseOrgPlanAttentionResponse(value) {
@@ -108,21 +126,32 @@ export function toOrgPlanAttentionAcknowledgement(acknowledgement = { accepted:
108
126
  };
109
127
  }
110
128
  export function isOrgPlanAttentionToolResponse(value) {
129
+ return parseOrgPlanAttentionToolResponse(value) !== null;
130
+ }
131
+ export function parseOrgPlanAttentionToolResponse(value) {
111
132
  if (!isRecord(value) || value.success !== true || !Array.isArray(value.contentItems))
112
- return false;
133
+ return null;
113
134
  const item = value.contentItems[0];
114
135
  if (value.contentItems.length !== 1 ||
115
136
  !isRecord(item) ||
116
137
  item.type !== 'inputText' ||
117
138
  typeof item.text !== 'string')
118
- return false;
139
+ return null;
119
140
  try {
120
- return parseOrgPlanAttentionResponse(JSON.parse(item.text)) !== null;
141
+ return parseOrgPlanAttentionResponse(JSON.parse(item.text));
121
142
  }
122
143
  catch {
123
- return false;
144
+ return null;
124
145
  }
125
146
  }
147
+ function parseExecutorReplacement(value) {
148
+ if (!isRecord(value) || Object.keys(value).length !== 1)
149
+ return null;
150
+ return typeof value.canonicalTaskName === 'string' &&
151
+ /^l[1-9][0-9]*$/.test(value.canonicalTaskName)
152
+ ? { canonicalTaskName: value.canonicalTaskName }
153
+ : null;
154
+ }
126
155
  function isReason(value) {
127
156
  return (typeof value === 'string' && orgPlanAttentionReasons.includes(value));
128
157
  }
@@ -10,81 +10,14 @@ 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 state.',
13
+ description: 'Record a validated supervised Org Plan report boundary. Send only the boundary kind; Mobile derives and validates all details from authoritative control state.',
14
14
  inputSchema: {
15
15
  type: 'object',
16
16
  additionalProperties: false,
17
- required: ['version', 'kind', 'planIdentity'],
17
+ required: ['kind'],
18
18
  properties: {
19
- version: { const: 1 },
20
19
  kind: { type: 'string', enum: ['l2Completed', 'l1Accepted', 'terminalReviewAccepted'] },
21
- planIdentity: { type: 'string', minLength: 1, maxLength: 128 },
22
- l1Id: { type: 'string', minLength: 1, maxLength: 128 },
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' },
26
- verdict: { const: 'ACCEPT' },
27
- commit: {
28
- oneOf: [
29
- {
30
- type: 'object',
31
- additionalProperties: false,
32
- required: ['kind', 'subject', 'shortHash'],
33
- properties: {
34
- kind: { const: 'created' },
35
- subject: { type: 'string', minLength: 1, maxLength: 160 },
36
- shortHash: { type: 'string', pattern: '^[0-9a-f]{7,16}$' },
37
- },
38
- },
39
- {
40
- type: 'object',
41
- additionalProperties: false,
42
- required: ['kind'],
43
- properties: { kind: { const: 'notRequired' } },
44
- },
45
- ],
46
- },
47
- findings: { type: 'string', minLength: 1, maxLength: 600 },
48
- changes: { type: 'string', minLength: 1, maxLength: 600 },
49
- files: { type: 'string', minLength: 1, maxLength: 600 },
50
- tests: { type: 'string', minLength: 1, maxLength: 600 },
51
20
  },
52
- oneOf: [
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
- },
72
- {
73
- properties: { kind: { const: 'terminalReviewAccepted' } },
74
- required: ['verdict'],
75
- not: {
76
- anyOf: [
77
- { required: ['l1Id'] },
78
- { required: ['l2Id'] },
79
- { required: ['position'] },
80
- { required: ['status'] },
81
- { required: ['commit'] },
82
- { required: ['changes'] },
83
- { required: ['files'] },
84
- ],
85
- },
86
- },
87
- ],
88
21
  },
89
22
  };
90
23
  export function parseOrgPlanCheckpoint(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt-mobile",
3
- "version": "0.41.2",
3
+ "version": "0.42.0",
4
4
  "description": "Mobile-first web relay for durable Codex development sessions",
5
5
  "keywords": [
6
6
  "codex",