trackly-cli 0.17.0 → 0.18.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.
@@ -18,7 +18,6 @@ const APPLY_BROWSER_SURFACES = APPLY_CONTRACT.constants.applyBrowserSurfaces;
18
18
  const APPLY_EXECUTION_ACCESS_CLASSIFICATIONS = APPLY_CONTRACT.constants.applyAccessClassifications;
19
19
  const APPLY_EXECUTION_DISPOSITION_SOURCES = APPLY_CONTRACT.constants.applyExecutionDispositionSources;
20
20
  const APPLY_SCENARIO_CODES = APPLY_CONTRACT.constants.applyScenarioCodes;
21
- const APPLY_CHECKPOINT_ACTION_CODES = APPLY_CONTRACT.constants.applyCheckpointActionCodes;
22
21
  const APPLY_EXECUTION_MAX_TARGET = APPLY_CONTRACT.constants.applyExecutionMaxTarget;
23
22
  const APPLY_EXECUTION_STOP_REASON_CODES = APPLY_CONTRACT.constants.applyExecutionStopReasonCodes;
24
23
  const FIXED_APPLY_BATCH_CANCEL_REASON_CODES = APPLY_CONTRACT.constants.fixedApplyBatchCancelReasonCodes;
@@ -30,13 +29,107 @@ const APPLY_SUBMISSION_EVIDENCE_TYPES = APPLY_CONTRACT.constants.applySubmission
30
29
  const APPLY_SUBMISSION_EVIDENCE_SOURCES = APPLY_CONTRACT.constants.applySubmissionEvidenceSources;
31
30
  const APPLY_EXECUTION_RECOVERY_ELIGIBILITY_CODES = APPLY_CONTRACT.constants.applyExecutionRecoveryEligibilityCodes;
32
31
  const APPLY_HANDOFF_RECONCILIATION_CLASSIFICATION_CODES = APPLY_CONTRACT.constants.applyHandoffReconciliationClassifications;
32
+ const APPLY_OBSERVED_ACCESS_CLASSIFICATIONS = APPLY_CONTRACT.constants.applyObservedAccessClassifications;
33
+ const APPLY_ACCESS_KNOWLEDGE_SOURCES = APPLY_CONTRACT.constants.applyAccessKnowledgeSources;
34
+ const APPLY_ACCESS_FRESHNESS_STATES = APPLY_CONTRACT.constants.applyAccessFreshnessStates;
35
+ const APPLY_SCHEDULING_EFFECTS = APPLY_CONTRACT.constants.applySchedulingEffects;
36
+ const APPLY_ACCESS_DEFERMENT_SCOPES = APPLY_CONTRACT.constants.applyAccessDefermentScopes;
37
+ const APPLY_ACCESS_MATCHED_SCOPES = APPLY_CONTRACT.constants.applyAccessMatchedScopes;
33
38
  const APPLY_BATCH_MAX_MEMBERS = 100;
34
39
  const APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST = 20;
35
40
  const APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT = 25;
36
41
  const APPLY_BATCH_MAX_BULK_MUTATIONS = 20;
37
42
 
43
+ const APPLY_CHECKPOINT_CONTINUATION_BY_ACTION = APPLY_CONTRACT.constants
44
+ .applyCheckpointContinuationByAction;
45
+ const APPLY_CHECKPOINT_LIFECYCLE_BY_ACTION = APPLY_CONTRACT.constants
46
+ .applyCheckpointLifecycleByAction;
47
+ const APPLY_CHECKPOINT_QUESTION_PACKET_BY_ACTION = APPLY_CONTRACT.constants
48
+ .applyCheckpointQuestionPacketByAction;
49
+ const APPLY_CHECKPOINT_ACTION_CODES = APPLY_CONTRACT.constants.applyCheckpointActionCodes;
50
+ const applyCheckpointActionVariant = (actionCode) => z.object({
51
+ actionCode: z.literal(actionCode),
52
+ continuationAllowed: z.literal(APPLY_CHECKPOINT_CONTINUATION_BY_ACTION[actionCode]),
53
+ fieldFingerprint: APPLY_CHECKPOINT_QUESTION_PACKET_BY_ACTION[actionCode]
54
+ ? z.string().regex(/^[a-f0-9]{64}$/)
55
+ : z.string().regex(/^[a-f0-9]{64}$/).optional(),
56
+ });
57
+ const applyCheckpointActionSchema = z.discriminatedUnion(
58
+ 'actionCode',
59
+ APPLY_CHECKPOINT_ACTION_CODES.map(applyCheckpointActionVariant),
60
+ );
61
+
38
62
  const SAFE_OBSERVATION_CODE = /^[a-z0-9][a-z0-9_:-]{0,99}$/;
39
63
  const SAFE_IDEMPOTENCY_KEY = /^[\x20-\x7e]+$/;
64
+ const applyCheckpointSchema = z.object({
65
+ memberId: z.number().int().min(1),
66
+ runId: z.number().int().min(1),
67
+ expectedMemberVersion: z.number().int().min(1),
68
+ expectedInspectionEpoch: z.number().int().min(0),
69
+ inspectionEpoch: z.number().int().min(0),
70
+ packetPhase: z.enum(APPLY_CHECKPOINT_PACKET_PHASES).optional(),
71
+ knownFieldsCommitted: z.boolean(),
72
+ resolvedActionIds: z.array(z.string().regex(/^[1-9][0-9]*$/))
73
+ .max(APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT).optional(),
74
+ idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
75
+ actions: z.array(applyCheckpointActionSchema)
76
+ .min(1).max(APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT),
77
+ }).superRefine((checkpoint, context) => {
78
+ if (checkpoint.inspectionEpoch !== checkpoint.expectedInspectionEpoch) {
79
+ context.addIssue({
80
+ code: z.ZodIssueCode.custom,
81
+ path: ['inspectionEpoch'],
82
+ message: 'inspectionEpoch must equal expectedInspectionEpoch',
83
+ });
84
+ }
85
+ if (checkpoint.resolvedActionIds
86
+ && new Set(checkpoint.resolvedActionIds).size !== checkpoint.resolvedActionIds.length) {
87
+ context.addIssue({
88
+ code: z.ZodIssueCode.custom,
89
+ path: ['resolvedActionIds'],
90
+ message: 'resolvedActionIds must be unique',
91
+ });
92
+ }
93
+ const actionCodes = checkpoint.actions.map(({ actionCode }) => actionCode);
94
+ if (new Set(actionCodes.map((code) => APPLY_CHECKPOINT_LIFECYCLE_BY_ACTION[code])).size !== 1) {
95
+ context.addIssue({
96
+ code: z.ZodIssueCode.custom,
97
+ path: ['actions'],
98
+ message: 'Actions in one inspection checkpoint must share one lifecycle',
99
+ });
100
+ }
101
+ const hasQuestions = actionCodes.some((code) => APPLY_CHECKPOINT_QUESTION_PACKET_BY_ACTION[code]);
102
+ if (hasQuestions && checkpoint.packetPhase === undefined) {
103
+ context.addIssue({
104
+ code: z.ZodIssueCode.custom,
105
+ path: ['packetPhase'],
106
+ message: 'Question checkpoints require a packet phase',
107
+ });
108
+ }
109
+ if (hasQuestions && !checkpoint.knownFieldsCommitted) {
110
+ context.addIssue({
111
+ code: z.ZodIssueCode.custom,
112
+ path: ['knownFieldsCommitted'],
113
+ message: 'Question checkpoints require committed known fields',
114
+ });
115
+ }
116
+ if (!hasQuestions && checkpoint.packetPhase !== undefined) {
117
+ context.addIssue({
118
+ code: z.ZodIssueCode.custom,
119
+ path: ['packetPhase'],
120
+ message: 'packetPhase is only valid for grouped questions',
121
+ });
122
+ }
123
+ const reviewReady = actionCodes.includes('captcha/at_submit')
124
+ || actionCodes.includes('review/manual_submit');
125
+ if (reviewReady && !checkpoint.knownFieldsCommitted) {
126
+ context.addIssue({
127
+ code: z.ZodIssueCode.custom,
128
+ path: ['knownFieldsCommitted'],
129
+ message: 'Review checkpoints require all known fields to be committed',
130
+ });
131
+ }
132
+ });
40
133
  const iso3166Alpha2Schema = z.string().regex(/^[A-Za-z]{2}$/).refine(isIso3166Alpha2, {
41
134
  message: 'Expected an ISO 3166-1 alpha-2 country code',
42
135
  });
@@ -59,12 +152,290 @@ const applyExecutionDispositionSchema = z.object({
59
152
  probeOnlyNoDraft: z.boolean().optional(),
60
153
  browserSurface: z.enum(APPLY_BROWSER_SURFACES),
61
154
  }).strict();
155
+ const accessKnowledgeSchema = z.object({
156
+ observedAccess: z.object({
157
+ classification: z.enum(APPLY_OBSERVED_ACCESS_CLASSIFICATIONS),
158
+ detailCode: z.string().regex(SAFE_OBSERVATION_CODE).nullable(),
159
+ wallStage: z.enum(APPLY_EXECUTION_ACCESS_CLASSIFICATIONS).nullable(),
160
+ matchedScope: z.enum(APPLY_ACCESS_MATCHED_SCOPES),
161
+ source: z.enum(APPLY_ACCESS_KNOWLEDGE_SOURCES),
162
+ lastConfirmedAt: z.string().datetime().nullable(),
163
+ freshUntil: z.string().datetime().nullable(),
164
+ freshness: z.enum(APPLY_ACCESS_FRESHNESS_STATES),
165
+ evidenceCount: z.number().int().min(0),
166
+ contradictory: z.boolean(),
167
+ }).strict(),
168
+ userPreference: z.object({
169
+ defermentId: z.number().int().min(1),
170
+ scope: z.enum(APPLY_ACCESS_DEFERMENT_SCOPES),
171
+ createdAt: z.string().datetime(),
172
+ persistsUntilCleared: z.literal(true),
173
+ }).strict().nullable(),
174
+ effectiveSchedulingEffect: z.enum(APPLY_SCHEDULING_EFFECTS),
175
+ rationaleCode: z.string().regex(SAFE_OBSERVATION_CODE),
176
+ knowledgeRevision: z.number().int().min(1),
177
+ evaluatedAt: z.string().datetime(),
178
+ freshLiveProbeRequired: z.literal(true),
179
+ }).strict();
180
+ const proposedWaveMemberSchema = z.object({
181
+ jobId: z.number().int().min(1),
182
+ memberPosition: z.number().int().min(0),
183
+ jobTitle: z.string().min(1).refine((value) => Array.from(value).length <= 300, {
184
+ message: 'jobTitle must contain at most 300 Unicode code points',
185
+ }),
186
+ companyName: z.string().min(1).refine((value) => Array.from(value).length <= 300, {
187
+ message: 'companyName must contain at most 300 Unicode code points',
188
+ }),
189
+ provider: z.string().regex(SAFE_OBSERVATION_CODE),
190
+ requisitionUrl: z.string().url().max(2048).refine((value) => value.startsWith('https://'), {
191
+ message: 'requisitionUrl must use HTTPS',
192
+ }),
193
+ accessKnowledge: accessKnowledgeSchema,
194
+ }).strict();
195
+ const blockedJobDefermentSchema = z.object({
196
+ jobId: z.number().int().min(1),
197
+ defermentId: z.number().int().min(1),
198
+ scope: z.enum(APPLY_ACCESS_DEFERMENT_SCOPES),
199
+ }).strict();
200
+ const richProposedWaveMemberSchema = proposedWaveMemberSchema.extend({
201
+ rationaleCode: z.string().regex(SAFE_OBSERVATION_CODE),
202
+ receiptHash: z.string().regex(/^[a-f0-9]{64}$/),
203
+ }).strict();
204
+ const accessProposalSchema = z.object({
205
+ proposalId: z.number().int().min(1),
206
+ approvalHash: z.string().regex(/^[a-f0-9]{64}$/),
207
+ rationaleCode: z.string().regex(SAFE_OBSERVATION_CODE),
208
+ knowledgeRevision: z.number().int().min(1),
209
+ evaluatedAt: z.string().datetime(),
210
+ availableCandidateCount: z.number().int().min(0),
211
+ deferredCandidateCount: z.number().int().min(0),
212
+ blockedJobDeferments: z.array(blockedJobDefermentSchema)
213
+ .max(APPLY_EXECUTION_MAX_TARGET)
214
+ .refine((values) => (
215
+ new Set(values.map(({ jobId, defermentId }) => `${jobId}:${defermentId}`)).size
216
+ === values.length
217
+ ), { message: 'blockedJobDeferments must contain unique job/deferment pairs' })
218
+ .optional(),
219
+ members: z.array(richProposedWaveMemberSchema).max(APPLY_EXECUTION_MAX_TARGET),
220
+ }).strict().superRefine((proposal, context) => {
221
+ if (new Set(proposal.members.map(({ jobId }) => jobId)).size !== proposal.members.length) {
222
+ context.addIssue({
223
+ code: z.ZodIssueCode.custom,
224
+ path: ['members'],
225
+ message: 'accessProposal job IDs must be unique',
226
+ });
227
+ }
228
+ if (proposal.members.some(({ memberPosition }, index) => memberPosition !== index)) {
229
+ context.addIssue({
230
+ code: z.ZodIssueCode.custom,
231
+ path: ['members'],
232
+ message: 'accessProposal members must retain exact contiguous order',
233
+ });
234
+ }
235
+ });
236
+ const executionProgressSchema = z.object({
237
+ target: z.number().int().min(1),
238
+ achievementCount: z.number().int().min(0),
239
+ completed: z.number().int().min(0),
240
+ durablyReviewReady: z.number().int().min(0),
241
+ submitted: z.number().int().min(0),
242
+ reservedReviewSlots: z.number().int().min(0),
243
+ currentlyFilling: z.number().int().min(0),
244
+ awaitingAnswer: z.number().int().min(0),
245
+ authParked: z.number().int().min(0),
246
+ excluded: z.number().int().min(0),
247
+ conflicted: z.number().int().min(0),
248
+ attempted: z.number().int().min(0),
249
+ remainingCandidates: z.number().int().min(0),
250
+ availableCandidateCount: z.number().int().min(0),
251
+ deferredCandidateCount: z.number().int().min(0),
252
+ queueExhausted: z.boolean(),
253
+ targetReached: z.boolean(),
254
+ nextAction: z.enum([
255
+ 'continue_current_wave', 'advance', 'access_review', 'answer_required',
256
+ 'manual_review', 'complete', 'none',
257
+ ]),
258
+ historicalProjection: z.object({
259
+ achievementCount: z.number().int().min(0),
260
+ completed: z.number().int().min(0),
261
+ }).strict(),
262
+ currentProjection: z.object({
263
+ durablyReviewReady: z.number().int().min(0),
264
+ submitted: z.number().int().min(0),
265
+ }).strict(),
266
+ }).strict();
267
+ const compatibleExecutionProgressSchema = executionProgressSchema.extend({
268
+ achievementCount: z.number().int().min(0).optional(),
269
+ completed: z.number().int().min(0).optional(),
270
+ availableCandidateCount: z.number().int().min(0).optional(),
271
+ deferredCandidateCount: z.number().int().min(0).optional(),
272
+ historicalProjection: z.object({
273
+ achievementCount: z.number().int().min(0),
274
+ completed: z.number().int().min(0),
275
+ }).strict().optional(),
276
+ currentProjection: z.object({
277
+ durablyReviewReady: z.number().int().min(0),
278
+ submitted: z.number().int().min(0),
279
+ }).strict().optional(),
280
+ }).strict();
281
+ function validateMatchingProposal(response, context) {
282
+ const simpleIds = response.proposedWave.map(({ jobId }) => jobId);
283
+ const richIds = response.accessProposal.members.map(({ jobId }) => jobId);
284
+ if (new Set(simpleIds).size !== simpleIds.length) {
285
+ context.addIssue({
286
+ code: z.ZodIssueCode.custom,
287
+ path: ['proposedWave'],
288
+ message: 'proposedWave job IDs must be unique',
289
+ });
290
+ }
291
+ if (simpleIds.length !== richIds.length
292
+ || simpleIds.some((jobId, index) => jobId !== richIds[index])) {
293
+ context.addIssue({
294
+ code: z.ZodIssueCode.custom,
295
+ path: ['accessProposal', 'members'],
296
+ message: 'accessProposal must bind the exact ordered proposedWave job IDs',
297
+ });
298
+ }
299
+ if (response.proposedWave.some((member, index) => {
300
+ const rich = response.accessProposal.members[index];
301
+ return !rich
302
+ || member.memberPosition !== rich.memberPosition
303
+ || member.jobTitle !== rich.jobTitle
304
+ || member.companyName !== rich.companyName
305
+ || member.provider !== rich.provider
306
+ || member.requisitionUrl !== rich.requisitionUrl
307
+ || JSON.stringify(member.accessKnowledge) !== JSON.stringify(rich.accessKnowledge)
308
+ || rich.rationaleCode !== rich.accessKnowledge.rationaleCode;
309
+ })) {
310
+ context.addIssue({
311
+ code: z.ZodIssueCode.custom,
312
+ path: ['accessProposal', 'members'],
313
+ message: 'accessProposal must bind the exact displayed frozen identities and access knowledge',
314
+ });
315
+ }
316
+ }
317
+ const proposedWaveResponseSchema = z.object({
318
+ success: z.literal(true),
319
+ executionId: z.number().int().min(1),
320
+ createdWave: z.boolean(),
321
+ batchId: z.number().int().min(1).optional(),
322
+ revision: z.number().int().min(1),
323
+ proposedWave: z.array(proposedWaveMemberSchema).max(APPLY_EXECUTION_MAX_TARGET),
324
+ accessProposal: accessProposalSchema,
325
+ progress: executionProgressSchema,
326
+ replay: z.boolean().optional(),
327
+ }).strict().superRefine(validateMatchingProposal);
328
+ const executionWaveSchema = z.object({
329
+ batchId: z.number().int().min(1),
330
+ waveOrder: z.number().int().min(0),
331
+ }).strict();
332
+ const applyExecutionSchema = z.object({
333
+ id: z.number().int().min(1),
334
+ userId: z.number().int().min(1),
335
+ mode: z.enum(['complete_next_n_accessible', 'recover_exact_members']),
336
+ targetCount: z.number().int().min(1).max(APPLY_EXECUTION_MAX_TARGET),
337
+ orderingVersion: z.number().int().min(1),
338
+ queueSnapshotAt: z.string().datetime(),
339
+ originalSnapshotHash: z.string().regex(/^[a-f0-9]{64}$/),
340
+ status: z.enum(['running', 'target_reached', 'exhausted_partial', 'stopped', 'closed', 'expired']),
341
+ revision: z.number().int().min(1),
342
+ expiresAt: z.string().datetime(),
343
+ recoverableUntil: z.string().datetime(),
344
+ sourceExecutionId: z.number().int().min(1).nullable(),
345
+ sourceSnapshotHash: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
346
+ achievementLedgerEnabled: z.boolean().optional(),
347
+ currentWave: executionWaveSchema.nullable(),
348
+ unresolvedWaves: z.array(executionWaveSchema).max(APPLY_EXECUTION_MAX_TARGET),
349
+ }).strict();
350
+ const getExecutionAccessReviewResponseSchema = z.object({
351
+ success: z.literal(true),
352
+ execution: applyExecutionSchema,
353
+ progress: executionProgressSchema,
354
+ proposedWave: z.array(proposedWaveMemberSchema).max(APPLY_EXECUTION_MAX_TARGET),
355
+ accessProposal: accessProposalSchema,
356
+ }).strict().superRefine(validateMatchingProposal);
357
+ const activeExecutionAccessReviewResponseSchema = getExecutionAccessReviewResponseSchema.innerType().extend({
358
+ active: z.boolean().optional(),
359
+ preserved: z.boolean().optional(),
360
+ }).strict().superRefine(validateMatchingProposal);
361
+ const startExecutionAccessReviewResponseSchema = z.object({
362
+ success: z.literal(true),
363
+ execution: applyExecutionSchema,
364
+ progress: executionProgressSchema,
365
+ proposedWave: z.array(proposedWaveMemberSchema).max(APPLY_EXECUTION_MAX_TARGET),
366
+ accessProposal: accessProposalSchema,
367
+ }).strict().superRefine(validateMatchingProposal);
368
+ const advanceExecutionResponseSchema = z.object({
369
+ success: z.literal(true),
370
+ executionId: z.number().int().min(1),
371
+ createdWave: z.boolean(),
372
+ batchId: z.number().int().min(1).optional(),
373
+ revision: z.number().int().min(1),
374
+ progress: compatibleExecutionProgressSchema,
375
+ replay: z.boolean().optional(),
376
+ }).strict();
377
+ const getExecutionResponseSchema = z.object({
378
+ success: z.literal(true),
379
+ execution: applyExecutionSchema,
380
+ progress: compatibleExecutionProgressSchema,
381
+ }).strict();
382
+ const accessDefermentSchema = z.object({
383
+ id: z.number().int().min(1),
384
+ jobId: z.number().int().min(1),
385
+ scope: z.enum(APPLY_ACCESS_DEFERMENT_SCOPES),
386
+ createdAt: z.string().datetime(),
387
+ persistsUntilCleared: z.literal(true),
388
+ }).strict();
389
+ const clearedAccessDefermentSchema = accessDefermentSchema.extend({
390
+ clearedAt: z.string().datetime(),
391
+ persistsUntilCleared: z.literal(false),
392
+ }).strict();
393
+ const accessDefermentListResponseSchema = z.object({
394
+ success: z.literal(true),
395
+ deferments: z.array(accessDefermentSchema).max(APPLY_EXECUTION_MAX_TARGET),
396
+ }).strict().refine(({ deferments }) => (
397
+ new Set(deferments.map(({ id }) => id)).size === deferments.length
398
+ ), { message: 'deferment IDs must be unique' });
399
+ const accessDefermentMutationResponseSchema = z.object({
400
+ success: z.literal(true),
401
+ replay: z.boolean(),
402
+ deferment: accessDefermentSchema,
403
+ }).strict();
404
+ const accessDefermentClearResponseSchema = z.object({
405
+ success: z.literal(true),
406
+ replay: z.boolean(),
407
+ deferment: clearedAccessDefermentSchema,
408
+ }).strict();
62
409
  const recoverableCandidateSchema = z.object({
63
410
  candidateId: z.number().int().min(1),
64
411
  jobId: z.number().int().min(1),
65
412
  queuePosition: z.number().int().min(0),
66
413
  eligibilityCode: z.enum(APPLY_EXECUTION_RECOVERY_ELIGIBILITY_CODES),
414
+ accessKnowledge: accessKnowledgeSchema.optional(),
67
415
  }).strict();
416
+
417
+ function validateProposedWaveResponse(response) {
418
+ if (
419
+ !response
420
+ || typeof response !== 'object'
421
+ || Array.isArray(response)
422
+ ) {
423
+ return advanceExecutionResponseSchema.parse(response);
424
+ }
425
+ const hasSimpleProposal = response.proposedWave !== undefined;
426
+ const hasRichProposal = response.accessProposal !== undefined;
427
+ if (!hasSimpleProposal && !hasRichProposal) return advanceExecutionResponseSchema.parse(response);
428
+ return proposedWaveResponseSchema.parse(response);
429
+ }
430
+ function validateGetExecutionResponse(response) {
431
+ if (!response || typeof response !== 'object' || Array.isArray(response)) {
432
+ return getExecutionResponseSchema.parse(response);
433
+ }
434
+ if (response.proposedWave === undefined && response.accessProposal === undefined) {
435
+ return getExecutionResponseSchema.parse(response);
436
+ }
437
+ return getExecutionAccessReviewResponseSchema.parse(response);
438
+ }
68
439
  const recoverableExecutionSourceSchema = z.object({
69
440
  sourceExecutionId: z.number().int().min(1),
70
441
  sourceSnapshotHash: z.string().regex(/^[a-f0-9]{64}$/),
@@ -263,7 +634,7 @@ const startApplyRunSchema = z.object({
263
634
  }
264
635
  });
265
636
 
266
- const APPLY_RELIABILITY_PROMPT = 'Protocol 3.6 / skill 4.7.0 reliability gate: recover active work first. Retain the latest explicit target as hard, prove genuine applicant fields before counting access, obtain approval for the exact accessible jobs before form mutation, fill all deterministic fields before one true-gap question packet, and validate each value-free phase checkpoint. After full local context loss, list recoverable executions, show only stable job identity, obtain explicit confirmation of the exact candidate set, and call exact-member recovery without substitutions. Treat recovered tab presence, form state, and mutation authority as three separate facts; reacquire a fresh browser binding and inspection epoch before mutation. Before resolving broad submission statements, list active review handoffs for the execution; use only an explicit receipt or the sole returned active receipt, classify every member as detected, user_confirmed, unresolved, or contradictory, and claim that exact handoff before writing outcomes. Use provider-specific positive success evidence; an unchanged URL or title is never negative evidence. Validate an exact expected browser keep set locally before finalization. For resume uploads, negotiate the browser surface capabilities, identify the semantic control, arm the chooser before clicking, attach the immediately verified file, prove the user-facing filename committed, and recheck parser-modified fields. Use compact snapshots and server-provided mutability. Preserve user-edited and unknown non-empty fields. Never reopen parked work without explicit user resumption. Never click Submit.';
637
+ const APPLY_RELIABILITY_PROMPT = 'Protocol 3.7 / skill 4.8.0 reliability gate: recover active work first. Consume the exact proposedWave and frozen accessKnowledge receipts before opening any browser. Rank OPEN ahead of neutral VARIES/UNKNOWN, then fresh ACCOUNT WALL; never auto-select an active user deferment. Historical OPEN never authorizes fill_form; freshLiveProbeRequired remains true. When nextAction is access_review, display the exact accessProposal and approvalHash, then obtain explicit approval for those exact jobIds and call the hash-bound approval continuation; if access is deferred, wait for probe, clear-deferment, stop, or expiry. Do not open a browser or report the queue exhausted. Retain the latest explicit target as hard, prove genuine applicant fields before counting access, obtain approval for the exact accessible jobs before form mutation, fill all deterministic fields before one true-gap question packet, and validate each value-free phase checkpoint. Every checkpoint action must use its canonical continuationAllowed value; review/manual_submit, captcha/at_submit, trust/origin_mismatch, and observability/unverifiable_state require false. Fail closed on a rejected checkpoint. After full local context loss, list recoverable executions, show only stable job identity, obtain explicit confirmation of the exact candidate set, and call exact-member recovery without substitutions. An active personal deferment blocks exact recovery until cleared. Treat recovered tab presence, form state, and mutation authority as three separate facts; reacquire a fresh browser binding and inspection epoch before mutation. Before resolving broad submission statements, list active review handoffs for the execution; use only an explicit receipt or the sole returned active receipt, classify every member as detected, user_confirmed, unresolved, or contradictory, and claim that exact handoff before writing outcomes. Use provider-specific positive success evidence; an unchanged URL or title is never negative evidence. Validate an exact expected browser keep set locally before finalization. For resume uploads, negotiate the browser surface capabilities, identify the semantic control, arm the chooser before clicking, attach the immediately verified file, prove the user-facing filename committed, and recheck parser-modified fields. Use compact snapshots and server-provided mutability. Preserve user-edited and unknown non-empty fields. Never reopen parked work without explicit user resumption. Never click Submit.';
267
638
 
268
639
  function registerApplyTools(
269
640
  server,
@@ -288,6 +659,39 @@ function registerApplyTools(
288
659
  const discoveredRecoverableSources = new Map();
289
660
  const discoveredHandoffBindings = new Map();
290
661
  const discoveredHandoffIdsByExecution = new Map();
662
+ const discoveredDefermentIds = new Set();
663
+ const clearedDefermentReplayKeys = new Map();
664
+ const MAX_CLEARED_DEFERMENT_REPLAYS = 64;
665
+ let clearedDefermentReplayCount = 0;
666
+ function rememberClearedDefermentReplay(defermentId, idempotencyKey) {
667
+ if (!clearedDefermentReplayKeys.has(defermentId)) clearedDefermentReplayKeys.set(defermentId, new Set());
668
+ const keys = clearedDefermentReplayKeys.get(defermentId);
669
+ if (keys.has(idempotencyKey)) return;
670
+ keys.add(idempotencyKey);
671
+ clearedDefermentReplayCount += 1;
672
+ while (clearedDefermentReplayCount > MAX_CLEARED_DEFERMENT_REPLAYS) {
673
+ const [oldestId, oldestKeys] = clearedDefermentReplayKeys.entries().next().value;
674
+ const oldestKey = oldestKeys.values().next().value;
675
+ oldestKeys.delete(oldestKey);
676
+ clearedDefermentReplayCount -= 1;
677
+ if (!oldestKeys.size) clearedDefermentReplayKeys.delete(oldestId);
678
+ }
679
+ }
680
+ const pendingAccessProposalByExecution = new Map();
681
+ const replayableAccessApprovalByExecution = new Map();
682
+ function rememberAccessProposal(executionId, revision, browserSurface, response) {
683
+ if (response.progress?.nextAction !== 'access_review') {
684
+ pendingAccessProposalByExecution.delete(executionId);
685
+ return response;
686
+ }
687
+ pendingAccessProposalByExecution.set(executionId, {
688
+ revision,
689
+ browserSurface,
690
+ approvalHash: response.accessProposal.approvalHash,
691
+ jobIds: response.accessProposal.members.map(({ jobId }) => jobId),
692
+ });
693
+ return response;
694
+ }
291
695
  server.tool(
292
696
  'trackly_get_apply_queue',
293
697
  'Get the deterministic queue of jobs the user already approved by saving as check later. Do not rescore or veto these jobs.',
@@ -404,27 +808,55 @@ function registerApplyTools(
404
808
  target: z.number().int().min(1).max(APPLY_EXECUTION_MAX_TARGET),
405
809
  idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
406
810
  },
407
- wrapTool(async ({ idempotencyKey, ...body }) => applyControlRequest(
408
- 'POST', '/api/jobscout/apply/executions', body, idempotencyKey,
409
- ), 'Failed to start apply execution')
811
+ wrapTool(async ({ idempotencyKey, ...body }) => {
812
+ const rawResponse = await applyControlRequest(
813
+ 'POST', '/api/jobscout/apply/executions', body, idempotencyKey,
814
+ );
815
+ const response = rawResponse?.proposedWave !== undefined || rawResponse?.accessProposal !== undefined
816
+ ? startExecutionAccessReviewResponseSchema.parse(rawResponse)
817
+ : rawResponse;
818
+ if (response?.proposedWave !== undefined && response?.execution?.id !== undefined) {
819
+ rememberAccessProposal(response.execution.id, response.execution.revision, null, response);
820
+ }
821
+ return response;
822
+ }, 'Failed to start apply execution')
410
823
  );
411
824
 
412
825
  server.tool(
413
826
  'trackly_get_active_apply_execution',
414
827
  'Recover the active Apply execution before recovering or creating a legacy fixed batch.',
415
828
  {},
416
- wrapTool(async () => applyControlRequest(
417
- 'GET', '/api/jobscout/apply/executions/active',
418
- ), 'Failed to recover active apply execution')
829
+ wrapTool(async () => {
830
+ const rawResponse = await applyControlRequest('GET', '/api/jobscout/apply/executions/active');
831
+ const response = rawResponse?.execution !== undefined || rawResponse?.progress !== undefined
832
+ ? (rawResponse?.active !== undefined || rawResponse?.preserved !== undefined
833
+ ? activeExecutionAccessReviewResponseSchema.parse(rawResponse)
834
+ : validateGetExecutionResponse(rawResponse))
835
+ : rawResponse;
836
+ if (response?.execution?.id && response.proposedWave !== undefined) {
837
+ rememberAccessProposal(response.execution.id, response.execution.revision, null, response);
838
+ }
839
+ return response;
840
+ }, 'Failed to recover active apply execution')
419
841
  );
420
842
 
421
843
  server.tool(
422
844
  'trackly_get_apply_execution',
423
845
  'Read the authoritative execution state, latest current-wave identity, and aggregate progress funnel.',
424
846
  { executionId: z.number().int().min(1) },
425
- wrapTool(async ({ executionId }) => applyControlRequest(
426
- 'GET', `/api/jobscout/apply/executions/${executionId}`,
427
- ), 'Failed to fetch apply execution')
847
+ wrapTool(async ({ executionId }) => {
848
+ const response = validateGetExecutionResponse(await applyControlRequest(
849
+ 'GET', `/api/jobscout/apply/executions/${executionId}`,
850
+ ));
851
+ if (response.execution !== undefined && response.execution?.id !== executionId) {
852
+ throw new Error('Apply execution response does not match the requested execution id.');
853
+ }
854
+ if (response.proposedWave === undefined) {
855
+ pendingAccessProposalByExecution.delete(executionId);
856
+ return response;
857
+ }
858
+ return rememberAccessProposal(executionId, response.execution.revision, null, response);
859
+ }, 'Failed to fetch apply execution')
428
860
  );
429
861
 
430
862
  server.tool(
@@ -617,16 +1049,62 @@ function registerApplyTools(
617
1049
 
618
1050
  server.tool(
619
1051
  'trackly_advance_apply_execution',
620
- 'Advance an execution transactionally for the current browser surface. The backend creates at most one immutable continuation wave and never exceeds the requested review-ready target. A same-key replay returns current authoritative progress and the current execution revision.',
1052
+ 'Advance an execution transactionally for the current browser surface. Returns the immutable proposedWave with frozen accessKnowledge receipts and never opens a browser. Same-key replay returns the same members, order, and rationale. Optional hash-bound accessReviewApproval probes exact deferred job IDs only after personal deferments are cleared.',
621
1053
  {
622
1054
  executionId: z.number().int().min(1),
623
1055
  expectedRevision: z.number().int().min(1),
624
1056
  browserSurface: z.enum(APPLY_BROWSER_SURFACES),
625
1057
  idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
1058
+ accessReviewApproval: z.object({
1059
+ jobIds: z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET)
1060
+ .refine((values) => new Set(values).size === values.length, {
1061
+ message: 'jobIds must be unique',
1062
+ }),
1063
+ approvalHash: z.string().regex(/^[a-f0-9]{64}$/),
1064
+ }).strict().optional(),
626
1065
  },
627
- wrapTool(async ({ executionId, idempotencyKey, ...body }) => applyControlRequest(
628
- 'POST', `/api/jobscout/apply/executions/${executionId}/advance`, body, idempotencyKey,
629
- ), 'Failed to advance apply execution')
1066
+ wrapTool(async ({ executionId, idempotencyKey, ...body }) => {
1067
+ if (body.accessReviewApproval) {
1068
+ const pending = pendingAccessProposalByExecution.get(executionId);
1069
+ const replayable = replayableAccessApprovalByExecution.get(executionId);
1070
+ const approvedIds = body.accessReviewApproval.jobIds;
1071
+ const matchesProposal = (proposal) => proposal
1072
+ && proposal.revision === body.expectedRevision
1073
+ && (proposal.browserSurface === null || proposal.browserSurface === body.browserSurface)
1074
+ && proposal.approvalHash === body.accessReviewApproval.approvalHash
1075
+ && proposal.jobIds.length === approvedIds.length
1076
+ && proposal.jobIds.every((jobId, index) => jobId === approvedIds[index]);
1077
+ const matchesReplay = matchesProposal(replayable)
1078
+ && replayable.idempotencyKey === idempotencyKey;
1079
+ if (!matchesProposal(pending) && !matchesReplay) {
1080
+ throw new Error(
1081
+ 'Access review approval must match the exact returned proposal, revision, browser surface, ordered job IDs, and approval hash.',
1082
+ );
1083
+ }
1084
+ }
1085
+ const response = validateProposedWaveResponse(await applyControlRequest(
1086
+ 'POST', `/api/jobscout/apply/executions/${executionId}/advance`, body, idempotencyKey,
1087
+ ));
1088
+ if (response.executionId !== undefined && response.executionId !== executionId) {
1089
+ throw new Error('Apply execution response does not match the requested execution id.');
1090
+ }
1091
+ if (body.accessReviewApproval) {
1092
+ pendingAccessProposalByExecution.delete(executionId);
1093
+ replayableAccessApprovalByExecution.set(executionId, {
1094
+ revision: body.expectedRevision,
1095
+ browserSurface: body.browserSurface,
1096
+ approvalHash: body.accessReviewApproval.approvalHash,
1097
+ jobIds: [...body.accessReviewApproval.jobIds],
1098
+ idempotencyKey,
1099
+ });
1100
+ } else if (response.proposedWave !== undefined) {
1101
+ rememberAccessProposal(executionId, response.revision, body.browserSurface, response);
1102
+ } else {
1103
+ pendingAccessProposalByExecution.delete(executionId);
1104
+ replayableAccessApprovalByExecution.delete(executionId);
1105
+ }
1106
+ return response;
1107
+ }, 'Failed to advance apply execution')
630
1108
  );
631
1109
 
632
1110
  server.tool(
@@ -652,9 +1130,84 @@ function registerApplyTools(
652
1130
  idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
653
1131
  reasonCode: z.enum(APPLY_EXECUTION_STOP_REASON_CODES).optional(),
654
1132
  },
655
- wrapTool(async ({ executionId, idempotencyKey, ...body }) => applyControlRequest(
656
- 'POST', `/api/jobscout/apply/executions/${executionId}/stop`, body, idempotencyKey,
657
- ), 'Failed to stop apply execution')
1133
+ wrapTool(async ({ executionId, idempotencyKey, ...body }) => {
1134
+ const response = await applyControlRequest(
1135
+ 'POST', `/api/jobscout/apply/executions/${executionId}/stop`, body, idempotencyKey,
1136
+ );
1137
+ pendingAccessProposalByExecution.delete(executionId);
1138
+ replayableAccessApprovalByExecution.delete(executionId);
1139
+ return response;
1140
+ }, 'Failed to stop apply execution')
1141
+ );
1142
+
1143
+ server.tool(
1144
+ 'trackly_list_apply_access_deferments',
1145
+ 'List the current user\'s persistent Apply access deferments. Returns only job, company, or provider scope identities; never URLs, provider names, or chat text.',
1146
+ {},
1147
+ wrapTool(async () => {
1148
+ const response = accessDefermentListResponseSchema.parse(await applyControlRequest(
1149
+ 'GET', '/api/jobscout/apply/access-deferments',
1150
+ ));
1151
+ discoveredDefermentIds.clear();
1152
+ for (const deferment of response.deferments) {
1153
+ discoveredDefermentIds.add(deferment.id);
1154
+ }
1155
+ return response;
1156
+ }, 'Failed to list apply access deferments')
1157
+ );
1158
+
1159
+ server.tool(
1160
+ 'trackly_defer_apply_access',
1161
+ 'Persist an explicit user deferment for one Trackly job or its derived company or provider scope. Provider scope blocks that provider across companies, proposal approval, recovery, and replay until cleared. The server derives company, provider, tenant, origin, and route from jobId; never submit a provider name, URL, or free text.',
1162
+ {
1163
+ jobId: z.number().int().min(1),
1164
+ scope: z.enum(APPLY_ACCESS_DEFERMENT_SCOPES),
1165
+ idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
1166
+ },
1167
+ wrapTool(async ({ idempotencyKey, ...body }) => {
1168
+ const response = accessDefermentMutationResponseSchema.parse(await applyControlRequest(
1169
+ 'POST', '/api/jobscout/apply/access-deferments', body, idempotencyKey,
1170
+ ));
1171
+ if (
1172
+ response.deferment.jobId !== body.jobId
1173
+ || response.deferment.scope !== body.scope
1174
+ ) {
1175
+ throw new Error('Access deferment response does not match the requested job and scope.');
1176
+ }
1177
+ discoveredDefermentIds.add(response.deferment.id);
1178
+ return response;
1179
+ }, 'Failed to defer apply access')
1180
+ );
1181
+
1182
+ server.tool(
1183
+ 'trackly_clear_apply_access_deferment',
1184
+ 'Clear one explicit user deferment previously listed or created in this session. An exact same-session retry remains idempotent, including for provider-wide user policy.',
1185
+ {
1186
+ defermentId: z.number().int().min(1),
1187
+ idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
1188
+ },
1189
+ wrapTool(async ({ defermentId, idempotencyKey }) => {
1190
+ if (!discoveredDefermentIds.has(defermentId)) {
1191
+ if (clearedDefermentReplayKeys.get(defermentId)?.has(idempotencyKey)) {
1192
+ const replayResponse = accessDefermentClearResponseSchema.parse(await applyControlRequest(
1193
+ 'POST', `/api/jobscout/apply/access-deferments/${defermentId}/clear`, {}, idempotencyKey,
1194
+ ));
1195
+ if (replayResponse.deferment.id !== defermentId) {
1196
+ throw new Error('Access deferment response does not match the requested deferment id.');
1197
+ }
1198
+ return replayResponse;
1199
+ }
1200
+ throw new Error('Clear must use a deferment id from the latest list or defer response.');
1201
+ }
1202
+ const response = accessDefermentClearResponseSchema.parse(await applyControlRequest(
1203
+ 'POST', `/api/jobscout/apply/access-deferments/${defermentId}/clear`, {}, idempotencyKey,
1204
+ ));
1205
+ if (response.deferment.id !== defermentId) {
1206
+ throw new Error('Access deferment response does not match the requested deferment id.');
1207
+ }
1208
+ rememberClearedDefermentReplay(defermentId, idempotencyKey);
1209
+ return response;
1210
+ }, 'Failed to clear apply access deferment')
658
1211
  );
659
1212
 
660
1213
  server.tool(
@@ -773,25 +1326,10 @@ function registerApplyTools(
773
1326
  {
774
1327
  batchId: z.number().int().min(1),
775
1328
  leaseToken: z.string().min(1).max(1024),
776
- checkpoints: z.array(z.object({
777
- memberId: z.number().int().min(1),
778
- runId: z.number().int().min(1),
779
- expectedMemberVersion: z.number().int().min(1),
780
- expectedInspectionEpoch: z.number().int().min(0),
781
- inspectionEpoch: z.number().int().min(0),
782
- packetPhase: z.enum(APPLY_CHECKPOINT_PACKET_PHASES).optional(),
783
- knownFieldsCommitted: z.boolean(),
784
- resolvedActionIds: z.array(z.string().regex(/^[1-9][0-9]*$/))
785
- .max(APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT).optional(),
786
- idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
787
- actions: z.array(z.object({
788
- actionCode: z.enum(APPLY_CHECKPOINT_ACTION_CODES),
789
- continuationAllowed: z.boolean(),
790
- fieldFingerprint: z.string().regex(/^[a-f0-9]{64}$/).optional(),
791
- })).min(1).max(APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT),
792
- })).min(1).max(APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST),
1329
+ checkpoints: z.array(applyCheckpointSchema)
1330
+ .min(1).max(APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST),
793
1331
  },
794
- wrapTool(async ({ batchId, ...body }) => apiRequest(
1332
+ wrapTool(async ({ batchId, ...body }) => applyApiRequest(
795
1333
  'POST',
796
1334
  `/api/jobscout/apply/batches/${batchId}/checkpoints`,
797
1335
  body,
@@ -1199,7 +1737,7 @@ function registerApplyTools(
1199
1737
  content: { type: 'text', text: APPLY_RELIABILITY_PROMPT },
1200
1738
  }, {
1201
1739
  role: 'user',
1202
- content: { type: 'text', text: 'Before generating questions or filling controls, run the skill 4.7.0 deterministic answer resolver. Classify every visible answer as exact_profile, safe_derivation, supported_draft, missing_fact, live_consent, or forbidden_inference; validate the control type; fill only the first three; and ask only currently visible unresolved needs. Treat the current profile revision as reusable authority, never transcript, screenshot, parser, autocomplete, or cached values. Accessible-first is a hard scheduler invariant: park known authentication, account-creation, OTP, and pre-form-CAPTCHA work without starting a draft while an accessible candidate remains.' },
1740
+ content: { type: 'text', text: 'Before generating questions or filling controls, run the skill 4.8.0 deterministic answer resolver. Classify every visible answer as exact_profile, safe_derivation, supported_draft, missing_fact, live_consent, or forbidden_inference; validate the control type; fill only the first three; and ask only currently visible unresolved needs. Treat the current profile revision as reusable authority, never transcript, screenshot, parser, autocomplete, or cached values. Accessible-first is a hard scheduler invariant: park known authentication, account-creation, OTP, and pre-form-CAPTCHA work without starting a draft while an accessible candidate remains. Curated OPEN and recent accessibility never change allowedOperations to fill_form.' },
1203
1741
  }, {
1204
1742
  role: 'user',
1205
1743
  content: { type: 'text', text: 'Only active=true identifies resumable execution work. Active=false and preserved=true is terminal read-only reconciliation evidence.' },
@@ -1207,13 +1745,13 @@ function registerApplyTools(
1207
1745
  role: 'user',
1208
1746
  content: {
1209
1747
  type: 'text',
1210
- text: 'Protocol 3.6.0 reliability gate for new work: require MCP contract 3.7.3 and skill 4.7.0. After complete local context loss, list bounded recovery candidates, obtain explicit confirmation of the exact set, and recover only that set. Treat tab recovery, form-state recovery, and mutation authority as independent. List active handoff receipts for the execution before resolving grouped submission statements; use the named receipt or the sole returned active receipt, classify every member, and claim that receipt before recording outcomes. Validate tab keep sets and resume upload stages locally. Never send raw browser values or click Submit.',
1748
+ text: 'Protocol 3.7.0 reliability gate for new work: require MCP contract 3.8.1 and skill 4.8.0. Consume the exact proposedWave with frozen accessKnowledge before opening any browser; same-key advance replay returns identical member IDs, order, and rationale. When nextAction is access_review, display the deferred proposal and do not report exhaustion. Defer or clear only through jobId-scoped tools; provider scope persists server-derived provider exclusions across companies, approval, recovery, and replay. Never submit provider names, URLs, or raw chat. After complete local context loss, list bounded recovery candidates, obtain explicit confirmation of the exact set, and recover only that set. An active personal deferment blocks exact recovery until cleared. Treat tab recovery, form-state recovery, and mutation authority as independent. List active handoff receipts for the execution before resolving grouped submission statements; use the named receipt or the sole returned active receipt, classify every member, and claim that receipt before recording outcomes. Validate tab keep sets and resume upload stages locally. Never send raw browser values or click Submit.',
1211
1749
  },
1212
1750
  }, {
1213
1751
  role: 'user',
1214
1752
  content: {
1215
1753
  type: 'text',
1216
- text: 'Legacy protocol compatibility gate: require the fetched compatibleSkillMinimumVersion or newer for all new work and use protocol 3.6.0 as specified above. Read the Apply protocol first. Only protocol 3.5 or newer with the compact-snapshot capability may call trackly_get_apply_execution_snapshot or the parked-member resume and execution-resume approval tools. An already-active protocol 3.4 execution is read-only legacy recovery: use only its published get or stop tools and never mutate its browser forms. Only when the fetched protocol is 3.4 or newer call trackly_get_active_apply_execution before legacy batch recovery, including when accessible execution is disabled. For protocol 3.3, skip the execution endpoint and recover the already-active immutable fixed batch directly; protocol 3.2 remains valid only for an already-active explicit legacy single run. A disabled rollout may preserve an active execution: recover it read-only and use only get or stop tools until the capability is enabled; never start, advance, or record dispositions while disabled. If disabled and no execution is active, use the legacy fixed-batch path. Recover every entry in execution.unresolvedWaves in ascending waveOrder; an older unresolved wave remains part of recovery after a replacement wave exists, and execution.currentWave is only the latest scheduling identity, never the complete recovery set. For “fill/apply to the next N,” recover or start one complete_next_n_accessible execution with target 1–20 and follow only the server nextAction and authoritative funnel. If the requested N differs from the active target, explain the mismatch, obtain explicit confirmation, stop the old execution with reason target_changed, refetch its terminal state, then start the new target. If an immutable fixed batch is active when the user requests complete_next_n_accessible, explain the incompatible mode and summarize any review-ready, submitted, or unresolved work before browser mutation. Resume that exact fixed batch when the user chooses to finish it. If the user instead says to start fresh, leave, replace, discard, or otherwise abandon the old batch, treat that statement as explicit cancellation confirmation: refetch the latest batch revision, call trackly_cancel_apply_batch with reason user_requested_restart and a fresh idempotency key, refetch until no active fixed batch remains, preserve every existing browser tab without mutation, and start the requested accessible execution in the same turn. Never wait for batch expiry and never create a scheduled continuation merely to escape an obsolete batch. If cancellation reports submission_in_progress, preserve everything and stop for the user; do not cancel or start replacement work. If the user asks to stop, call trackly_stop_apply_execution with reason user_requested and refetch its terminal state. Continue immutable child waves from the original recent-first snapshot until the authoritative backend funnel says targetReached, the queue is exhausted, or the user stops. Treat achievementCount and target-capped completed as the cumulative target authority. durablyReviewReady and submitted are current operator projections only; never add them together locally to reconstruct completion. Accessible drafts awaiting answers and forms currently being filled occupy target slots; authentication, account creation, OTP, pre-form CAPTCHA, exclusions, manual-only, conflicts, and revocations do not. Record only typed value-free live-probe dispositions. Advance only when no current-wave member remains unclassified queued or inspecting. Never calculate replacements or progress locally. For an explicit “inspect the next N records” request, use the existing fixed immutable batch and never replenish it; if a different accessible execution is active, confirm the intent change with the user, stop that execution with reason target_changed, refetch its terminal state, then recover or create the fixed batch. A cache hint may prioritize a live minimal non-mutating probe but never authorizes private-data entry or replaces that probe. After a redirect or contradictory observation, report only the fresh live disposition with its exact binding and let the backend invalidate its own hint. Preserve every user-edited or unknown non-empty field through the local provenance ledger. Never submit.',
1754
+ text: 'Legacy protocol compatibility gate: require the fetched compatibleSkillMinimumVersion or newer for all new work and use protocol 3.7.0 as specified above. Read the Apply protocol first. Only protocol 3.5 or newer with the compact-snapshot capability may call trackly_get_apply_execution_snapshot or the parked-member resume and execution-resume approval tools. An already-active protocol 3.4 execution is read-only legacy recovery: use only its published get or stop tools and never mutate its browser forms. Only when the fetched protocol is 3.4 or newer call trackly_get_active_apply_execution before legacy batch recovery, including when accessible execution is disabled. For protocol 3.3, skip the execution endpoint and recover the already-active immutable fixed batch directly; protocol 3.2 remains valid only for an already-active explicit legacy single run. A disabled rollout may preserve an active execution: recover it read-only and use only get or stop tools until the capability is enabled; never start, advance, or record dispositions while disabled. If disabled and no execution is active, use the legacy fixed-batch path. Recover every entry in execution.unresolvedWaves in ascending waveOrder; an older unresolved wave remains part of recovery after a replacement wave exists, and execution.currentWave is only the latest scheduling identity, never the complete recovery set. For “fill/apply to the next N,” recover or start one complete_next_n_accessible execution with target 1–20 and follow only the server nextAction and authoritative funnel. If the requested N differs from the active target, explain the mismatch, obtain explicit confirmation, stop the old execution with reason target_changed, refetch its terminal state, then start the new target. If an immutable fixed batch is active when the user requests complete_next_n_accessible, explain the incompatible mode and summarize any review-ready, submitted, or unresolved work before browser mutation. Resume that exact fixed batch when the user chooses to finish it. If the user instead says to start fresh, leave, replace, discard, or otherwise abandon the old batch, treat that statement as explicit cancellation confirmation: refetch the latest batch revision, call trackly_cancel_apply_batch with reason user_requested_restart and a fresh idempotency key, refetch until no active fixed batch remains, preserve every existing browser tab without mutation, and start the requested accessible execution in the same turn. Never wait for batch expiry and never create a scheduled continuation merely to escape an obsolete batch. If cancellation reports submission_in_progress, preserve everything and stop for the user; do not cancel or start replacement work. If the user asks to stop, call trackly_stop_apply_execution with reason user_requested and refetch its terminal state. Continue immutable child waves from the original recent-first snapshot until the authoritative backend funnel says targetReached, the queue is exhausted, or the user stops. Treat achievementCount and target-capped completed as the cumulative target authority. durablyReviewReady and submitted are current operator projections only; never add them together locally to reconstruct completion. Accessible drafts awaiting answers and forms currently being filled occupy target slots; authentication, account creation, OTP, pre-form CAPTCHA, exclusions, manual-only, conflicts, and revocations do not. Record only typed value-free live-probe dispositions. Advance only when no current-wave member remains unclassified queued or inspecting. Never calculate replacements or progress locally. For an explicit “inspect the next N records” request, use the existing fixed immutable batch and never replenish it; if a different accessible execution is active, confirm the intent change with the user, stop that execution with reason target_changed, refetch its terminal state, then recover or create the fixed batch. A cache hint may prioritize a live minimal non-mutating probe but never authorizes private-data entry or replaces that probe. After a redirect or contradictory observation, report only the fresh live disposition with its exact binding and let the backend invalidate its own hint. Preserve every user-edited or unknown non-empty field through the local provenance ledger. Never submit.',
1217
1755
  },
1218
1756
  }, {
1219
1757
  role: 'user',