trackly-cli 0.17.1 → 0.18.1

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.
@@ -29,6 +29,13 @@ const APPLY_SUBMISSION_EVIDENCE_TYPES = APPLY_CONTRACT.constants.applySubmission
29
29
  const APPLY_SUBMISSION_EVIDENCE_SOURCES = APPLY_CONTRACT.constants.applySubmissionEvidenceSources;
30
30
  const APPLY_EXECUTION_RECOVERY_ELIGIBILITY_CODES = APPLY_CONTRACT.constants.applyExecutionRecoveryEligibilityCodes;
31
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;
38
+ const APPLY_ACCESS_DEFERMENT_MAX_ACTIVE = 20;
32
39
  const APPLY_BATCH_MAX_MEMBERS = 100;
33
40
  const APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST = 20;
34
41
  const APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT = 25;
@@ -146,12 +153,446 @@ const applyExecutionDispositionSchema = z.object({
146
153
  probeOnlyNoDraft: z.boolean().optional(),
147
154
  browserSurface: z.enum(APPLY_BROWSER_SURFACES),
148
155
  }).strict();
156
+ const accessKnowledgeSchema = z.object({
157
+ observedAccess: z.object({
158
+ classification: z.enum(APPLY_OBSERVED_ACCESS_CLASSIFICATIONS),
159
+ detailCode: z.string().regex(SAFE_OBSERVATION_CODE).nullable(),
160
+ wallStage: z.enum(APPLY_EXECUTION_ACCESS_CLASSIFICATIONS).nullable(),
161
+ matchedScope: z.enum(APPLY_ACCESS_MATCHED_SCOPES),
162
+ source: z.enum(APPLY_ACCESS_KNOWLEDGE_SOURCES),
163
+ lastConfirmedAt: z.string().datetime().nullable(),
164
+ freshUntil: z.string().datetime().nullable(),
165
+ freshness: z.enum(APPLY_ACCESS_FRESHNESS_STATES),
166
+ evidenceCount: z.number().int().min(0),
167
+ contradictory: z.boolean(),
168
+ }).strict(),
169
+ userPreference: z.object({
170
+ defermentId: z.number().int().min(1),
171
+ scope: z.enum(APPLY_ACCESS_DEFERMENT_SCOPES),
172
+ createdAt: z.string().datetime(),
173
+ persistsUntilCleared: z.literal(true),
174
+ }).strict().nullable(),
175
+ effectiveSchedulingEffect: z.enum(APPLY_SCHEDULING_EFFECTS),
176
+ rationaleCode: z.string().regex(SAFE_OBSERVATION_CODE),
177
+ knowledgeRevision: z.number().int().min(1),
178
+ evaluatedAt: z.string().datetime(),
179
+ freshLiveProbeRequired: z.literal(true),
180
+ }).strict();
181
+ const OPTIONAL_WAVE_DISPLAY_FIELDS = [
182
+ 'jobTitle', 'companyName', 'provider', 'requisitionUrl',
183
+ ];
184
+ function validateOptionalWaveDisplay(member, context) {
185
+ const present = OPTIONAL_WAVE_DISPLAY_FIELDS.filter((field) => member[field] !== undefined);
186
+ if (present.length > 0 && present.length < OPTIONAL_WAVE_DISPLAY_FIELDS.length) {
187
+ context.addIssue({
188
+ code: z.ZodIssueCode.custom,
189
+ path: [present[0]],
190
+ message: 'wave member display identity fields must be complete when projected',
191
+ });
192
+ }
193
+ }
194
+ const proposedWaveMemberSchema = z.object({
195
+ jobId: z.number().int().min(1),
196
+ // The local CLI projection is intentionally compact: the deployed backend
197
+ // returns only the stable job identity and scheduling receipt here. Rich
198
+ // approval metadata lives in accessProposal.members. The optional identity
199
+ // fields keep the client forward-compatible with the coordinated full
200
+ // identity projection without trusting them over the rich receipt.
201
+ // Protocol 3.8.0 compact receipts omit memberPosition. When a coordinated
202
+ // backend projects it, validate and compare it independently of display
203
+ // identity fields; never reject the deployed compact shape for its absence.
204
+ memberPosition: z.number().int().min(0).optional(),
205
+ jobTitle: z.string().min(1).refine((value) => Array.from(value).length <= 300, {
206
+ message: 'jobTitle must contain at most 300 Unicode code points',
207
+ }).optional(),
208
+ companyName: z.string().min(1).refine((value) => Array.from(value).length <= 300, {
209
+ message: 'companyName must contain at most 300 Unicode code points',
210
+ }).optional(),
211
+ provider: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
212
+ requisitionUrl: z.string().url().max(2048).refine((value) => value.startsWith('https://'), {
213
+ message: 'requisitionUrl must use HTTPS',
214
+ }).optional(),
215
+ accessKnowledge: accessKnowledgeSchema,
216
+ }).strict().superRefine(validateOptionalWaveDisplay);
217
+ const blockedJobDefermentSchema = z.object({
218
+ jobId: z.number().int().min(1),
219
+ defermentId: z.number().int().min(1),
220
+ scope: z.enum(APPLY_ACCESS_DEFERMENT_SCOPES),
221
+ }).strict();
222
+ const richProposedWaveMemberSchema = z.object({
223
+ jobId: z.number().int().min(1),
224
+ memberPosition: z.number().int().min(0),
225
+ jobTitle: z.string().min(1).refine((value) => Array.from(value).length <= 300, {
226
+ message: 'jobTitle must contain at most 300 Unicode code points',
227
+ }).optional(),
228
+ companyName: z.string().min(1).refine((value) => Array.from(value).length <= 300, {
229
+ message: 'companyName must contain at most 300 Unicode code points',
230
+ }).optional(),
231
+ provider: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
232
+ requisitionUrl: z.string().url().max(2048).refine((value) => value.startsWith('https://'), {
233
+ message: 'requisitionUrl must use HTTPS',
234
+ }).optional(),
235
+ accessKnowledge: accessKnowledgeSchema,
236
+ rationaleCode: z.string().regex(SAFE_OBSERVATION_CODE),
237
+ receiptHash: z.string().regex(/^[a-f0-9]{64}$/),
238
+ }).strict().superRefine(validateOptionalWaveDisplay);
239
+ const accessProposalSchema = z.object({
240
+ proposalId: z.number().int().min(1),
241
+ approvalHash: z.string().regex(/^[a-f0-9]{64}$/),
242
+ rationaleCode: z.string().regex(SAFE_OBSERVATION_CODE),
243
+ knowledgeRevision: z.number().int().min(1),
244
+ evaluatedAt: z.string().datetime(),
245
+ availableCandidateCount: z.number().int().min(0),
246
+ deferredCandidateCount: z.number().int().min(0),
247
+ blockedJobDeferments: z.array(blockedJobDefermentSchema)
248
+ .max(APPLY_EXECUTION_MAX_TARGET)
249
+ .refine((values) => (
250
+ new Set(values.map(({ jobId, defermentId }) => `${jobId}:${defermentId}`)).size
251
+ === values.length
252
+ ), { message: 'blockedJobDeferments must contain unique job/deferment pairs' })
253
+ .optional(),
254
+ members: z.array(richProposedWaveMemberSchema).max(APPLY_EXECUTION_MAX_TARGET),
255
+ }).strict().superRefine((proposal, context) => {
256
+ const allDeferred = proposal.rationaleCode === 'all_candidates_user_deferred';
257
+ const recoveryBlocked = proposal.rationaleCode === 'recovery_blocked_by_user_deferment';
258
+ if (proposal.members.length === 0
259
+ && ((!allDeferred && !recoveryBlocked)
260
+ || (allDeferred
261
+ && (proposal.availableCandidateCount !== 0 || proposal.deferredCandidateCount < 1))
262
+ || (recoveryBlocked && proposal.deferredCandidateCount < 1))) {
263
+ context.addIssue({
264
+ code: z.ZodIssueCode.custom,
265
+ path: ['members'],
266
+ message: 'empty accessProposal members require no available candidates and at least one deferred candidate',
267
+ });
268
+ }
269
+ if (proposal.members.length > 0 && (allDeferred || recoveryBlocked)) {
270
+ context.addIssue({
271
+ code: z.ZodIssueCode.custom,
272
+ path: ['members'],
273
+ message: 'terminal deferment rationales require an empty accessProposal members array',
274
+ });
275
+ }
276
+ // The initial 3.8.0 backend response omitted this optional mapping. When
277
+ // it is present, the mapping is validated and cached so the agent can offer
278
+ // an exact clear action; when it is absent, the proposal remains a safe
279
+ // stop/expiry state until a refreshed backend receipt supplies IDs.
280
+ if (new Set(proposal.members.map(({ jobId }) => jobId)).size !== proposal.members.length) {
281
+ context.addIssue({
282
+ code: z.ZodIssueCode.custom,
283
+ path: ['members'],
284
+ message: 'accessProposal job IDs must be unique',
285
+ });
286
+ }
287
+ if (proposal.members.some(({ memberPosition }, index) => memberPosition !== index)) {
288
+ context.addIssue({
289
+ code: z.ZodIssueCode.custom,
290
+ path: ['members'],
291
+ message: 'accessProposal members must retain exact contiguous order',
292
+ });
293
+ }
294
+ });
295
+ const executionProgressSchema = z.object({
296
+ target: z.number().int().min(1),
297
+ achievementCount: z.number().int().min(0),
298
+ completed: z.number().int().min(0),
299
+ durablyReviewReady: z.number().int().min(0),
300
+ submitted: z.number().int().min(0),
301
+ reservedReviewSlots: z.number().int().min(0),
302
+ currentlyFilling: z.number().int().min(0),
303
+ awaitingAnswer: z.number().int().min(0),
304
+ authParked: z.number().int().min(0),
305
+ excluded: z.number().int().min(0),
306
+ conflicted: z.number().int().min(0),
307
+ attempted: z.number().int().min(0),
308
+ remainingCandidates: z.number().int().min(0),
309
+ availableCandidateCount: z.number().int().min(0),
310
+ deferredCandidateCount: z.number().int().min(0),
311
+ queueExhausted: z.boolean(),
312
+ targetReached: z.boolean(),
313
+ nextAction: z.enum([
314
+ 'continue_current_wave', 'advance', 'access_review', 'answer_required',
315
+ 'manual_review', 'complete', 'none',
316
+ ]),
317
+ historicalProjection: z.object({
318
+ achievementCount: z.number().int().min(0),
319
+ completed: z.number().int().min(0),
320
+ }).strict(),
321
+ currentProjection: z.object({
322
+ durablyReviewReady: z.number().int().min(0),
323
+ submitted: z.number().int().min(0),
324
+ }).strict(),
325
+ }).strict();
326
+ const compatibleExecutionProgressSchema = executionProgressSchema.extend({
327
+ achievementCount: z.number().int().min(0).optional(),
328
+ completed: z.number().int().min(0).optional(),
329
+ availableCandidateCount: z.number().int().min(0).optional(),
330
+ deferredCandidateCount: z.number().int().min(0).optional(),
331
+ historicalProjection: z.object({
332
+ achievementCount: z.number().int().min(0),
333
+ completed: z.number().int().min(0),
334
+ }).strict().optional(),
335
+ currentProjection: z.object({
336
+ durablyReviewReady: z.number().int().min(0),
337
+ submitted: z.number().int().min(0),
338
+ }).strict().optional(),
339
+ }).strict();
340
+ function canonicalizeAccessKnowledge(value) {
341
+ if (Array.isArray(value)) return value.map(canonicalizeAccessKnowledge);
342
+ if (value && typeof value === 'object') {
343
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [
344
+ key,
345
+ canonicalizeAccessKnowledge(value[key]),
346
+ ]));
347
+ }
348
+ return value;
349
+ }
350
+ function accessKnowledgeEqual(left, right) {
351
+ return JSON.stringify(canonicalizeAccessKnowledge(left))
352
+ === JSON.stringify(canonicalizeAccessKnowledge(right));
353
+ }
354
+ function validateMatchingProposal(response, context) {
355
+ const simpleIds = response.proposedWave.map(({ jobId }) => jobId);
356
+ const richIds = response.accessProposal.members.map(({ jobId }) => jobId);
357
+ if (new Set(simpleIds).size !== simpleIds.length) {
358
+ context.addIssue({
359
+ code: z.ZodIssueCode.custom,
360
+ path: ['proposedWave'],
361
+ message: 'proposedWave job IDs must be unique',
362
+ });
363
+ }
364
+ if (simpleIds.length !== richIds.length
365
+ || simpleIds.some((jobId, index) => jobId !== richIds[index])) {
366
+ context.addIssue({
367
+ code: z.ZodIssueCode.custom,
368
+ path: ['accessProposal', 'members'],
369
+ message: 'accessProposal must bind the exact ordered proposedWave job IDs',
370
+ });
371
+ }
372
+ if (response.proposedWave.some((member, index) => {
373
+ const rich = response.accessProposal.members[index];
374
+ return !rich
375
+ || (member.memberPosition !== undefined && member.memberPosition !== rich.memberPosition)
376
+ || (member.jobTitle !== undefined && member.jobTitle !== rich.jobTitle)
377
+ || (member.companyName !== undefined && member.companyName !== rich.companyName)
378
+ || (member.provider !== undefined && member.provider !== rich.provider)
379
+ || (member.requisitionUrl !== undefined && member.requisitionUrl !== rich.requisitionUrl)
380
+ || !accessKnowledgeEqual(member.accessKnowledge, rich.accessKnowledge)
381
+ || rich.rationaleCode !== rich.accessKnowledge.rationaleCode;
382
+ })) {
383
+ context.addIssue({
384
+ code: z.ZodIssueCode.custom,
385
+ path: ['accessProposal', 'members'],
386
+ message: 'accessProposal must bind the exact displayed frozen identities and access knowledge',
387
+ });
388
+ }
389
+ if (response.progress?.nextAction === 'access_review'
390
+ && response.progress.availableCandidateCount !== undefined
391
+ && response.progress.availableCandidateCount
392
+ !== response.accessProposal.availableCandidateCount) {
393
+ context.addIssue({
394
+ code: z.ZodIssueCode.custom,
395
+ path: ['progress', 'availableCandidateCount'],
396
+ message: 'progress availableCandidateCount must match accessProposal',
397
+ });
398
+ }
399
+ if (response.progress?.nextAction === 'access_review'
400
+ && response.progress.deferredCandidateCount !== undefined
401
+ && response.progress.deferredCandidateCount
402
+ !== response.accessProposal.deferredCandidateCount) {
403
+ context.addIssue({
404
+ code: z.ZodIssueCode.custom,
405
+ path: ['progress', 'deferredCandidateCount'],
406
+ message: 'progress deferredCandidateCount must match accessProposal',
407
+ });
408
+ }
409
+ }
410
+ const proposedWaveResponseSchema = z.object({
411
+ success: z.literal(true),
412
+ executionId: z.number().int().min(1),
413
+ createdWave: z.boolean(),
414
+ batchId: z.number().int().min(1).optional(),
415
+ revision: z.number().int().min(1),
416
+ proposedWave: z.array(proposedWaveMemberSchema).max(APPLY_EXECUTION_MAX_TARGET),
417
+ accessProposal: accessProposalSchema,
418
+ progress: executionProgressSchema,
419
+ replay: z.boolean().optional(),
420
+ }).strict().superRefine(validateMatchingProposal);
421
+ const executionWaveSchema = z.object({
422
+ batchId: z.number().int().min(1),
423
+ waveOrder: z.number().int().min(0),
424
+ }).strict();
425
+ const applyExecutionSchema = z.object({
426
+ id: z.number().int().min(1),
427
+ userId: z.number().int().min(1),
428
+ mode: z.enum(['complete_next_n_accessible', 'recover_exact_members']),
429
+ targetCount: z.number().int().min(1).max(APPLY_EXECUTION_MAX_TARGET),
430
+ orderingVersion: z.number().int().min(1),
431
+ queueSnapshotAt: z.string().datetime(),
432
+ originalSnapshotHash: z.string().regex(/^[a-f0-9]{64}$/),
433
+ status: z.enum(['running', 'target_reached', 'exhausted_partial', 'stopped', 'closed', 'expired']),
434
+ revision: z.number().int().min(1),
435
+ expiresAt: z.string().datetime(),
436
+ recoverableUntil: z.string().datetime(),
437
+ sourceExecutionId: z.number().int().min(1).nullable(),
438
+ sourceSnapshotHash: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
439
+ achievementLedgerEnabled: z.boolean().optional(),
440
+ currentWave: executionWaveSchema.nullable(),
441
+ unresolvedWaves: z.array(executionWaveSchema).max(APPLY_EXECUTION_MAX_TARGET),
442
+ }).strict();
443
+ const getExecutionAccessReviewResponseSchema = z.object({
444
+ success: z.literal(true),
445
+ execution: applyExecutionSchema,
446
+ progress: executionProgressSchema,
447
+ proposedWave: z.array(proposedWaveMemberSchema).max(APPLY_EXECUTION_MAX_TARGET),
448
+ accessProposal: accessProposalSchema,
449
+ }).strict().superRefine(validateMatchingProposal);
450
+ const activeExecutionAccessReviewResponseSchema = getExecutionAccessReviewResponseSchema.innerType().extend({
451
+ enabled: z.boolean().optional(),
452
+ active: z.boolean().optional(),
453
+ preserved: z.boolean().optional(),
454
+ }).strict().superRefine(validateMatchingProposal).superRefine(rejectInactiveExecution);
455
+ const startExecutionAccessReviewResponseSchema = z.object({
456
+ success: z.literal(true),
457
+ replay: z.boolean().optional(),
458
+ execution: applyExecutionSchema,
459
+ candidateCount: z.number().int().min(0).optional(),
460
+ progress: executionProgressSchema,
461
+ proposedWave: z.array(proposedWaveMemberSchema).max(APPLY_EXECUTION_MAX_TARGET),
462
+ accessProposal: accessProposalSchema,
463
+ }).strict().superRefine(validateMatchingProposal);
464
+ const startExecutionOrdinaryResponseSchema = z.object({
465
+ success: z.literal(true),
466
+ replay: z.boolean().optional(),
467
+ execution: applyExecutionSchema,
468
+ candidateCount: z.number().int().min(0).optional(),
469
+ progress: compatibleExecutionProgressSchema,
470
+ }).strict();
471
+ const startExecutionResponseSchema = z.union([
472
+ startExecutionAccessReviewResponseSchema,
473
+ startExecutionOrdinaryResponseSchema,
474
+ ]);
475
+ function rejectAccessReviewWithoutProposal(response, context) {
476
+ if (response.progress.nextAction === 'access_review'
477
+ && (response.proposedWave === undefined || response.accessProposal === undefined)) {
478
+ context.addIssue({
479
+ code: z.ZodIssueCode.custom,
480
+ path: ['progress', 'nextAction'],
481
+ message: 'access_review responses must include proposedWave and accessProposal',
482
+ });
483
+ }
484
+ }
485
+ function rejectInactiveExecution(response, context) {
486
+ // A preserved terminal execution is intentionally returned with
487
+ // `active: false` for read-only reconciliation. Only reject the
488
+ // contradictory combination where that same response still asks the
489
+ // caller to perform more work. Preserved terminals are valid only with
490
+ // nextAction `none`.
491
+ if (
492
+ response.active === false
493
+ && response.execution !== undefined
494
+ && response.progress?.nextAction !== 'none'
495
+ ) {
496
+ context.addIssue({
497
+ code: z.ZodIssueCode.custom,
498
+ path: ['active'],
499
+ message: 'active=false terminal responses must use nextAction none',
500
+ });
501
+ }
502
+ }
503
+ const advanceExecutionResponseSchema = z.object({
504
+ success: z.literal(true),
505
+ executionId: z.number().int().min(1),
506
+ createdWave: z.boolean(),
507
+ batchId: z.number().int().min(1).optional(),
508
+ revision: z.number().int().min(1),
509
+ progress: compatibleExecutionProgressSchema,
510
+ replay: z.boolean().optional(),
511
+ }).strict().superRefine(rejectAccessReviewWithoutProposal);
512
+ const getExecutionResponseObjectSchema = z.object({
513
+ success: z.literal(true),
514
+ execution: applyExecutionSchema,
515
+ progress: compatibleExecutionProgressSchema,
516
+ }).strict();
517
+ const getExecutionResponseSchema = getExecutionResponseObjectSchema.superRefine(rejectAccessReviewWithoutProposal);
518
+ const activeExecutionOrdinaryResponseSchema = getExecutionResponseObjectSchema.extend({
519
+ enabled: z.boolean().optional(),
520
+ active: z.boolean().optional(),
521
+ preserved: z.boolean().optional(),
522
+ }).strict().superRefine(rejectAccessReviewWithoutProposal).superRefine(rejectInactiveExecution);
523
+ const activeExecutionInactiveResponseSchema = z.object({
524
+ success: z.literal(true),
525
+ enabled: z.boolean().optional(),
526
+ active: z.literal(false),
527
+ preserved: z.boolean().optional(),
528
+ }).strict().superRefine(rejectInactiveExecution);
529
+ const activeExecutionEnvelopeSchema = getExecutionResponseObjectSchema.extend({
530
+ enabled: z.boolean().optional(),
531
+ active: z.boolean().optional(),
532
+ preserved: z.boolean().optional(),
533
+ }).strict().superRefine(rejectInactiveExecution);
534
+ const activeExecutionResponseSchema = z.union([
535
+ activeExecutionInactiveResponseSchema,
536
+ activeExecutionOrdinaryResponseSchema,
537
+ activeExecutionAccessReviewResponseSchema,
538
+ ]);
539
+ const accessDefermentSchema = z.object({
540
+ id: z.number().int().min(1),
541
+ jobId: z.number().int().min(1),
542
+ scope: z.enum(APPLY_ACCESS_DEFERMENT_SCOPES),
543
+ createdAt: z.string().datetime(),
544
+ persistsUntilCleared: z.literal(true),
545
+ }).strict();
546
+ const clearedAccessDefermentSchema = accessDefermentSchema.extend({
547
+ clearedAt: z.string().datetime(),
548
+ persistsUntilCleared: z.literal(false),
549
+ }).strict();
550
+ const accessDefermentListResponseSchema = z.object({
551
+ success: z.literal(true),
552
+ deferments: z.array(accessDefermentSchema).max(APPLY_ACCESS_DEFERMENT_MAX_ACTIVE),
553
+ }).strict().refine(({ deferments }) => (
554
+ new Set(deferments.map(({ id }) => id)).size === deferments.length
555
+ ), { message: 'deferment IDs must be unique' });
556
+ const accessDefermentMutationResponseSchema = z.object({
557
+ success: z.literal(true),
558
+ replay: z.boolean(),
559
+ deferment: accessDefermentSchema,
560
+ }).strict();
561
+ const accessDefermentClearResponseSchema = z.object({
562
+ success: z.literal(true),
563
+ replay: z.boolean(),
564
+ deferment: clearedAccessDefermentSchema,
565
+ }).strict();
149
566
  const recoverableCandidateSchema = z.object({
150
567
  candidateId: z.number().int().min(1),
151
568
  jobId: z.number().int().min(1),
152
569
  queuePosition: z.number().int().min(0),
153
570
  eligibilityCode: z.enum(APPLY_EXECUTION_RECOVERY_ELIGIBILITY_CODES),
571
+ accessKnowledge: accessKnowledgeSchema.optional(),
154
572
  }).strict();
573
+
574
+ function validateProposedWaveResponse(response) {
575
+ if (
576
+ !response
577
+ || typeof response !== 'object'
578
+ || Array.isArray(response)
579
+ ) {
580
+ return advanceExecutionResponseSchema.parse(response);
581
+ }
582
+ const hasSimpleProposal = response.proposedWave !== undefined;
583
+ const hasRichProposal = response.accessProposal !== undefined;
584
+ if (!hasSimpleProposal && !hasRichProposal) return advanceExecutionResponseSchema.parse(response);
585
+ return proposedWaveResponseSchema.parse(response);
586
+ }
587
+ function validateGetExecutionResponse(response) {
588
+ if (!response || typeof response !== 'object' || Array.isArray(response)) {
589
+ return getExecutionResponseSchema.parse(response);
590
+ }
591
+ if (response.proposedWave === undefined && response.accessProposal === undefined) {
592
+ return getExecutionResponseSchema.parse(response);
593
+ }
594
+ return getExecutionAccessReviewResponseSchema.parse(response);
595
+ }
155
596
  const recoverableExecutionSourceSchema = z.object({
156
597
  sourceExecutionId: z.number().int().min(1),
157
598
  sourceSnapshotHash: z.string().regex(/^[a-f0-9]{64}$/),
@@ -350,7 +791,7 @@ const startApplyRunSchema = z.object({
350
791
  }
351
792
  });
352
793
 
353
- const APPLY_RELIABILITY_PROMPT = 'Protocol 3.6 / skill 4.7.1 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. 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. 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.';
794
+ 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, including ordinary OPEN or neutral proposals, display the exact accessProposal and accessProposal.approvalHash. If the proposal has nonempty members, obtain explicit approval for those exact ordered jobIds and call the hash-bound approval continuation. If the proposal has zero members because all remaining candidates are deferred or exact recovery is blocked by a user deferment, surface the deferred count and stable deferment IDs, offer clear-deferment only for explicitly returned IDs, stop, or expiry, and never send an empty approval. List or create deferments only at job, company, or provider scope through the jobId-bound tools; provider scope applies across companies until explicitly cleared. 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.';
354
795
 
355
796
  function registerApplyTools(
356
797
  server,
@@ -375,6 +816,50 @@ function registerApplyTools(
375
816
  const discoveredRecoverableSources = new Map();
376
817
  const discoveredHandoffBindings = new Map();
377
818
  const discoveredHandoffIdsByExecution = new Map();
819
+ const discoveredDefermentIds = new Set();
820
+ const clearedDefermentReplayKeys = new Map();
821
+ const MAX_CLEARED_DEFERMENT_REPLAYS = 64;
822
+ let clearedDefermentReplayCount = 0;
823
+ function rememberClearedDefermentReplay(defermentId, idempotencyKey) {
824
+ if (!clearedDefermentReplayKeys.has(defermentId)) clearedDefermentReplayKeys.set(defermentId, new Set());
825
+ const keys = clearedDefermentReplayKeys.get(defermentId);
826
+ if (keys.has(idempotencyKey)) return;
827
+ keys.add(idempotencyKey);
828
+ clearedDefermentReplayCount += 1;
829
+ while (clearedDefermentReplayCount > MAX_CLEARED_DEFERMENT_REPLAYS) {
830
+ const [oldestId, oldestKeys] = clearedDefermentReplayKeys.entries().next().value;
831
+ const oldestKey = oldestKeys.values().next().value;
832
+ oldestKeys.delete(oldestKey);
833
+ clearedDefermentReplayCount -= 1;
834
+ if (!oldestKeys.size) clearedDefermentReplayKeys.delete(oldestId);
835
+ }
836
+ }
837
+ const pendingAccessProposalByExecution = new Map();
838
+ const replayableAccessApprovalByExecution = new Map();
839
+ const MAX_ACCESS_PROPOSAL_BINDINGS = 64;
840
+ function setBoundedAccessProposalBinding(bindings, executionId, value) {
841
+ bindings.delete(executionId);
842
+ bindings.set(executionId, value);
843
+ while (bindings.size > MAX_ACCESS_PROPOSAL_BINDINGS) {
844
+ bindings.delete(bindings.keys().next().value);
845
+ }
846
+ }
847
+ function rememberAccessProposal(executionId, revision, browserSurface, response) {
848
+ if (response.progress?.nextAction !== 'access_review') {
849
+ pendingAccessProposalByExecution.delete(executionId);
850
+ return response;
851
+ }
852
+ setBoundedAccessProposalBinding(pendingAccessProposalByExecution, executionId, {
853
+ revision,
854
+ browserSurface,
855
+ approvalHash: response.accessProposal.approvalHash,
856
+ jobIds: response.accessProposal.members.map(({ jobId }) => jobId),
857
+ });
858
+ for (const deferment of response.accessProposal.blockedJobDeferments ?? []) {
859
+ discoveredDefermentIds.add(deferment.defermentId);
860
+ }
861
+ return response;
862
+ }
378
863
  server.tool(
379
864
  'trackly_get_apply_queue',
380
865
  'Get the deterministic queue of jobs the user already approved by saving as check later. Do not rescore or veto these jobs.',
@@ -491,27 +976,114 @@ function registerApplyTools(
491
976
  target: z.number().int().min(1).max(APPLY_EXECUTION_MAX_TARGET),
492
977
  idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
493
978
  },
494
- wrapTool(async ({ idempotencyKey, ...body }) => applyControlRequest(
495
- 'POST', '/api/jobscout/apply/executions', body, idempotencyKey,
496
- ), 'Failed to start apply execution')
979
+ wrapTool(async ({ idempotencyKey, ...body }) => {
980
+ const rawResponse = await applyControlRequest(
981
+ 'POST', '/api/jobscout/apply/executions', body, idempotencyKey,
982
+ );
983
+ let response = startExecutionResponseSchema.parse(rawResponse);
984
+ // An idempotent start replay may return the existing pending execution
985
+ // and its access_review progress without inlining the proposal. Hydrate
986
+ // the same strict detail response used by active recovery before the
987
+ // start result is exposed or cached.
988
+ if (response.progress.nextAction === 'access_review'
989
+ && response.proposedWave === undefined) {
990
+ const hydrated = validateGetExecutionResponse(await applyControlRequest(
991
+ 'GET', `/api/jobscout/apply/executions/${response.execution.id}`,
992
+ ));
993
+ if (hydrated.execution?.id !== response.execution.id
994
+ || hydrated.execution.revision !== response.execution.revision
995
+ || hydrated.progress.nextAction !== 'access_review'
996
+ || hydrated.proposedWave === undefined) {
997
+ throw new Error('Trackly start access review did not include its proposal.');
998
+ }
999
+ response = startExecutionAccessReviewResponseSchema.parse({
1000
+ ...response,
1001
+ execution: hydrated.execution,
1002
+ progress: hydrated.progress,
1003
+ proposedWave: hydrated.proposedWave,
1004
+ accessProposal: hydrated.accessProposal,
1005
+ });
1006
+ }
1007
+ if (response?.proposedWave !== undefined && response?.execution?.id !== undefined) {
1008
+ rememberAccessProposal(response.execution.id, response.execution.revision, null, response);
1009
+ } else if (response?.execution?.id !== undefined) {
1010
+ // A replay may legitimately return ordinary progress after the
1011
+ // access-review wave was consumed. Do not retain a prior approval
1012
+ // receipt that could authorize a later request for the same execution.
1013
+ pendingAccessProposalByExecution.delete(response.execution.id);
1014
+ replayableAccessApprovalByExecution.delete(response.execution.id);
1015
+ }
1016
+ return response;
1017
+ }, 'Failed to start apply execution')
497
1018
  );
498
1019
 
499
1020
  server.tool(
500
1021
  'trackly_get_active_apply_execution',
501
1022
  'Recover the active Apply execution before recovering or creating a legacy fixed batch.',
502
1023
  {},
503
- wrapTool(async () => applyControlRequest(
504
- 'GET', '/api/jobscout/apply/executions/active',
505
- ), 'Failed to recover active apply execution')
1024
+ wrapTool(async () => {
1025
+ const rawResponse = await applyControlRequest('GET', '/api/jobscout/apply/executions/active');
1026
+ const pendingProposal = rawResponse?.execution !== undefined
1027
+ && rawResponse?.progress?.nextAction === 'access_review'
1028
+ && rawResponse?.proposedWave === undefined;
1029
+ let response = pendingProposal
1030
+ ? activeExecutionEnvelopeSchema.parse(rawResponse)
1031
+ : activeExecutionResponseSchema.parse(rawResponse);
1032
+ // The deployed active endpoint reports the authoritative execution and
1033
+ // progress but does not inline a pending access proposal. Hydrate that
1034
+ // proposal from the strict execution endpoint before exposing an
1035
+ // access_review response, otherwise restart/recovery would stall on a
1036
+ // false missing-proposal error.
1037
+ if (
1038
+ response.progress?.nextAction === 'access_review'
1039
+ && response.execution?.id !== undefined
1040
+ && response.proposedWave === undefined
1041
+ ) {
1042
+ const hydrated = validateGetExecutionResponse(await applyControlRequest(
1043
+ 'GET', `/api/jobscout/apply/executions/${response.execution.id}`,
1044
+ ));
1045
+ if (hydrated.execution?.id !== response.execution.id
1046
+ || hydrated.execution.revision !== response.execution.revision
1047
+ || hydrated.progress.nextAction !== 'access_review'
1048
+ || hydrated.proposedWave === undefined) {
1049
+ throw new Error('Trackly active execution access review did not include its proposal.');
1050
+ }
1051
+ response = activeExecutionResponseSchema.parse({
1052
+ ...response,
1053
+ ...hydrated,
1054
+ enabled: response.enabled,
1055
+ active: response.active,
1056
+ preserved: response.preserved,
1057
+ });
1058
+ }
1059
+ if (response?.execution?.id && response.proposedWave !== undefined) {
1060
+ rememberAccessProposal(response.execution.id, response.execution.revision, null, response);
1061
+ } else if (response?.execution?.id) {
1062
+ pendingAccessProposalByExecution.delete(response.execution.id);
1063
+ replayableAccessApprovalByExecution.delete(response.execution.id);
1064
+ }
1065
+ return response;
1066
+ }, 'Failed to recover active apply execution')
506
1067
  );
507
1068
 
508
1069
  server.tool(
509
1070
  'trackly_get_apply_execution',
510
1071
  'Read the authoritative execution state, latest current-wave identity, and aggregate progress funnel.',
511
1072
  { executionId: z.number().int().min(1) },
512
- wrapTool(async ({ executionId }) => applyControlRequest(
513
- 'GET', `/api/jobscout/apply/executions/${executionId}`,
514
- ), 'Failed to fetch apply execution')
1073
+ wrapTool(async ({ executionId }) => {
1074
+ const response = validateGetExecutionResponse(await applyControlRequest(
1075
+ 'GET', `/api/jobscout/apply/executions/${executionId}`,
1076
+ ));
1077
+ if (response.execution !== undefined && response.execution?.id !== executionId) {
1078
+ throw new Error('Apply execution response does not match the requested execution id.');
1079
+ }
1080
+ if (response.proposedWave === undefined) {
1081
+ pendingAccessProposalByExecution.delete(executionId);
1082
+ replayableAccessApprovalByExecution.delete(executionId);
1083
+ return response;
1084
+ }
1085
+ return rememberAccessProposal(executionId, response.execution.revision, null, response);
1086
+ }, 'Failed to fetch apply execution')
515
1087
  );
516
1088
 
517
1089
  server.tool(
@@ -704,16 +1276,65 @@ function registerApplyTools(
704
1276
 
705
1277
  server.tool(
706
1278
  'trackly_advance_apply_execution',
707
- '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.',
1279
+ '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 the exact proposed job IDs only after personal deferments are cleared.',
708
1280
  {
709
1281
  executionId: z.number().int().min(1),
710
1282
  expectedRevision: z.number().int().min(1),
711
1283
  browserSurface: z.enum(APPLY_BROWSER_SURFACES),
712
1284
  idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
1285
+ accessReviewApproval: z.object({
1286
+ jobIds: z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET)
1287
+ .refine((values) => new Set(values).size === values.length, {
1288
+ message: 'jobIds must be unique',
1289
+ }),
1290
+ approvalHash: z.string().regex(/^[a-f0-9]{64}$/),
1291
+ }).strict().optional(),
713
1292
  },
714
- wrapTool(async ({ executionId, idempotencyKey, ...body }) => applyControlRequest(
715
- 'POST', `/api/jobscout/apply/executions/${executionId}/advance`, body, idempotencyKey,
716
- ), 'Failed to advance apply execution')
1293
+ wrapTool(async ({ executionId, idempotencyKey, ...body }) => {
1294
+ if (body.accessReviewApproval) {
1295
+ const pending = pendingAccessProposalByExecution.get(executionId);
1296
+ const replayable = replayableAccessApprovalByExecution.get(executionId);
1297
+ const approvedIds = body.accessReviewApproval.jobIds;
1298
+ const matchesProposal = (proposal) => proposal
1299
+ && proposal.revision === body.expectedRevision
1300
+ && (proposal.browserSurface === null || proposal.browserSurface === body.browserSurface)
1301
+ && proposal.approvalHash === body.accessReviewApproval.approvalHash
1302
+ && proposal.jobIds.length === approvedIds.length
1303
+ && proposal.jobIds.every((jobId, index) => jobId === approvedIds[index]);
1304
+ const matchesReplay = matchesProposal(replayable)
1305
+ && replayable.idempotencyKey === idempotencyKey;
1306
+ if (!matchesProposal(pending) && !matchesReplay) {
1307
+ throw new Error(
1308
+ 'Access review approval must match the exact returned proposal, revision, browser surface, ordered job IDs, and approval hash.',
1309
+ );
1310
+ }
1311
+ }
1312
+ const response = validateProposedWaveResponse(await applyControlRequest(
1313
+ 'POST', `/api/jobscout/apply/executions/${executionId}/advance`, body, idempotencyKey,
1314
+ ));
1315
+ if (response.executionId !== undefined && response.executionId !== executionId) {
1316
+ throw new Error('Apply execution response does not match the requested execution id.');
1317
+ }
1318
+ if (body.accessReviewApproval) {
1319
+ pendingAccessProposalByExecution.delete(executionId);
1320
+ setBoundedAccessProposalBinding(replayableAccessApprovalByExecution, executionId, {
1321
+ revision: body.expectedRevision,
1322
+ browserSurface: body.browserSurface,
1323
+ approvalHash: body.accessReviewApproval.approvalHash,
1324
+ jobIds: [...body.accessReviewApproval.jobIds],
1325
+ idempotencyKey,
1326
+ });
1327
+ if (response.proposedWave !== undefined && response.progress?.nextAction === 'access_review') {
1328
+ rememberAccessProposal(executionId, response.revision, body.browserSurface, response);
1329
+ }
1330
+ } else if (response.proposedWave !== undefined) {
1331
+ rememberAccessProposal(executionId, response.revision, body.browserSurface, response);
1332
+ } else {
1333
+ pendingAccessProposalByExecution.delete(executionId);
1334
+ replayableAccessApprovalByExecution.delete(executionId);
1335
+ }
1336
+ return response;
1337
+ }, 'Failed to advance apply execution')
717
1338
  );
718
1339
 
719
1340
  server.tool(
@@ -739,9 +1360,85 @@ function registerApplyTools(
739
1360
  idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
740
1361
  reasonCode: z.enum(APPLY_EXECUTION_STOP_REASON_CODES).optional(),
741
1362
  },
742
- wrapTool(async ({ executionId, idempotencyKey, ...body }) => applyControlRequest(
743
- 'POST', `/api/jobscout/apply/executions/${executionId}/stop`, body, idempotencyKey,
744
- ), 'Failed to stop apply execution')
1363
+ wrapTool(async ({ executionId, idempotencyKey, ...body }) => {
1364
+ const response = await applyControlRequest(
1365
+ 'POST', `/api/jobscout/apply/executions/${executionId}/stop`, body, idempotencyKey,
1366
+ );
1367
+ pendingAccessProposalByExecution.delete(executionId);
1368
+ replayableAccessApprovalByExecution.delete(executionId);
1369
+ return response;
1370
+ }, 'Failed to stop apply execution')
1371
+ );
1372
+
1373
+ server.tool(
1374
+ 'trackly_list_apply_access_deferments',
1375
+ 'List the current user\'s persistent Apply access deferments. Returns only job, company, or provider scope identities; never URLs, provider names, or chat text.',
1376
+ {},
1377
+ wrapTool(async () => {
1378
+ const response = accessDefermentListResponseSchema.parse(await applyControlRequest(
1379
+ 'GET', '/api/jobscout/apply/access-deferments',
1380
+ ));
1381
+ discoveredDefermentIds.clear();
1382
+ for (const deferment of response.deferments) {
1383
+ discoveredDefermentIds.add(deferment.id);
1384
+ }
1385
+ return response;
1386
+ }, 'Failed to list apply access deferments')
1387
+ );
1388
+
1389
+ server.tool(
1390
+ 'trackly_defer_apply_access',
1391
+ 'Persist an explicit user deferment for one Trackly job or its derived company or provider scope. Provider scope applies across companies until explicitly cleared. The server derives company, provider, tenant, origin, and route from jobId; never submit a provider name, URL, or free text.',
1392
+ {
1393
+ jobId: z.number().int().min(1),
1394
+ scope: z.enum(APPLY_ACCESS_DEFERMENT_SCOPES),
1395
+ idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
1396
+ },
1397
+ wrapTool(async ({ idempotencyKey, ...body }) => {
1398
+ const response = accessDefermentMutationResponseSchema.parse(await applyControlRequest(
1399
+ 'POST', '/api/jobscout/apply/access-deferments', body, idempotencyKey,
1400
+ ));
1401
+ if (
1402
+ response.deferment.jobId !== body.jobId
1403
+ || response.deferment.scope !== body.scope
1404
+ ) {
1405
+ throw new Error('Access deferment response does not match the requested job and scope.');
1406
+ }
1407
+ discoveredDefermentIds.add(response.deferment.id);
1408
+ return response;
1409
+ }, 'Failed to defer apply access')
1410
+ );
1411
+
1412
+ server.tool(
1413
+ 'trackly_clear_apply_access_deferment',
1414
+ 'Clear one explicit user deferment previously listed or created in this session. An exact same-session retry remains idempotent.',
1415
+ {
1416
+ defermentId: z.number().int().min(1),
1417
+ idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
1418
+ },
1419
+ wrapTool(async ({ defermentId, idempotencyKey }) => {
1420
+ if (!discoveredDefermentIds.has(defermentId)) {
1421
+ if (clearedDefermentReplayKeys.get(defermentId)?.has(idempotencyKey)) {
1422
+ const replayResponse = accessDefermentClearResponseSchema.parse(await applyControlRequest(
1423
+ 'POST', `/api/jobscout/apply/access-deferments/${defermentId}/clear`, {}, idempotencyKey,
1424
+ ));
1425
+ if (replayResponse.deferment.id !== defermentId) {
1426
+ throw new Error('Access deferment response does not match the requested deferment id.');
1427
+ }
1428
+ return replayResponse;
1429
+ }
1430
+ throw new Error('Clear must use a deferment id from the latest list or defer response.');
1431
+ }
1432
+ const response = accessDefermentClearResponseSchema.parse(await applyControlRequest(
1433
+ 'POST', `/api/jobscout/apply/access-deferments/${defermentId}/clear`, {}, idempotencyKey,
1434
+ ));
1435
+ if (response.deferment.id !== defermentId) {
1436
+ throw new Error('Access deferment response does not match the requested deferment id.');
1437
+ }
1438
+ rememberClearedDefermentReplay(defermentId, idempotencyKey);
1439
+ discoveredDefermentIds.delete(defermentId);
1440
+ return response;
1441
+ }, 'Failed to clear apply access deferment')
745
1442
  );
746
1443
 
747
1444
  server.tool(
@@ -1271,7 +1968,7 @@ function registerApplyTools(
1271
1968
  content: { type: 'text', text: APPLY_RELIABILITY_PROMPT },
1272
1969
  }, {
1273
1970
  role: 'user',
1274
- content: { type: 'text', text: 'Before generating questions or filling controls, run the skill 4.7.1 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.' },
1971
+ 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.' },
1275
1972
  }, {
1276
1973
  role: 'user',
1277
1974
  content: { type: 'text', text: 'Only active=true identifies resumable execution work. Active=false and preserved=true is terminal read-only reconciliation evidence.' },
@@ -1279,13 +1976,13 @@ function registerApplyTools(
1279
1976
  role: 'user',
1280
1977
  content: {
1281
1978
  type: 'text',
1282
- text: 'Protocol 3.6.0 reliability gate for new work: require MCP contract 3.7.6 and skill 4.7.1. 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.',
1979
+ 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, including ordinary OPEN or neutral proposals, display the exact access-review receipt and do not report exhaustion. For a nonempty proposal, obtain explicit approval for the unchanged ordered job IDs and server approval hash before probing. For an all-deferred or recovery-blocked proposal with zero members, show the deferred count and stable job/company/provider deferment IDs, offer clear-deferment only for explicitly returned IDs, stop, or expiry, and never send an empty approval. Defer or clear only through jobId-scoped job, company, or provider tools; provider scope applies across companies until explicitly cleared. 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.',
1283
1980
  },
1284
1981
  }, {
1285
1982
  role: 'user',
1286
1983
  content: {
1287
1984
  type: 'text',
1288
- 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.',
1985
+ 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.',
1289
1986
  },
1290
1987
  }, {
1291
1988
  role: 'user',