trackly-cli 0.17.1 → 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.
- package/README.md +7 -4
- package/contracts/trackly-apply-contract-history.json +6 -0
- package/contracts/trackly-apply-tools.json +32 -3
- package/docs/solutions/integration-issues/backend-owned-sensitive-answer-deletion-guard.md +88 -0
- package/docs/trackly-tools.md +10 -1
- package/lib/agent.js +2 -2
- package/mcp/apply-tools.js +486 -20
- package/mcp/server.js +96 -11
- package/npm-shrinkwrap.json +22 -21
- package/package.json +6 -4
- package/server.json +2 -2
- package/skills/trackly-apply/SKILL.md +5 -4
- package/skills/trackly-apply/references/access-probe.md +5 -0
- package/skills/trackly-apply/references/batch-orchestration.md +58 -3
package/mcp/apply-tools.js
CHANGED
|
@@ -29,6 +29,12 @@ 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;
|
|
32
38
|
const APPLY_BATCH_MAX_MEMBERS = 100;
|
|
33
39
|
const APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST = 20;
|
|
34
40
|
const APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT = 25;
|
|
@@ -146,12 +152,290 @@ const applyExecutionDispositionSchema = z.object({
|
|
|
146
152
|
probeOnlyNoDraft: z.boolean().optional(),
|
|
147
153
|
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
148
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();
|
|
149
409
|
const recoverableCandidateSchema = z.object({
|
|
150
410
|
candidateId: z.number().int().min(1),
|
|
151
411
|
jobId: z.number().int().min(1),
|
|
152
412
|
queuePosition: z.number().int().min(0),
|
|
153
413
|
eligibilityCode: z.enum(APPLY_EXECUTION_RECOVERY_ELIGIBILITY_CODES),
|
|
414
|
+
accessKnowledge: accessKnowledgeSchema.optional(),
|
|
154
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
|
+
}
|
|
155
439
|
const recoverableExecutionSourceSchema = z.object({
|
|
156
440
|
sourceExecutionId: z.number().int().min(1),
|
|
157
441
|
sourceSnapshotHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
@@ -350,7 +634,7 @@ const startApplyRunSchema = z.object({
|
|
|
350
634
|
}
|
|
351
635
|
});
|
|
352
636
|
|
|
353
|
-
const APPLY_RELIABILITY_PROMPT = 'Protocol 3.
|
|
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.';
|
|
354
638
|
|
|
355
639
|
function registerApplyTools(
|
|
356
640
|
server,
|
|
@@ -375,6 +659,39 @@ function registerApplyTools(
|
|
|
375
659
|
const discoveredRecoverableSources = new Map();
|
|
376
660
|
const discoveredHandoffBindings = new Map();
|
|
377
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
|
+
}
|
|
378
695
|
server.tool(
|
|
379
696
|
'trackly_get_apply_queue',
|
|
380
697
|
'Get the deterministic queue of jobs the user already approved by saving as check later. Do not rescore or veto these jobs.',
|
|
@@ -491,27 +808,55 @@ function registerApplyTools(
|
|
|
491
808
|
target: z.number().int().min(1).max(APPLY_EXECUTION_MAX_TARGET),
|
|
492
809
|
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
493
810
|
},
|
|
494
|
-
wrapTool(async ({ idempotencyKey, ...body }) =>
|
|
495
|
-
|
|
496
|
-
|
|
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')
|
|
497
823
|
);
|
|
498
824
|
|
|
499
825
|
server.tool(
|
|
500
826
|
'trackly_get_active_apply_execution',
|
|
501
827
|
'Recover the active Apply execution before recovering or creating a legacy fixed batch.',
|
|
502
828
|
{},
|
|
503
|
-
wrapTool(async () =>
|
|
504
|
-
'GET', '/api/jobscout/apply/executions/active'
|
|
505
|
-
|
|
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')
|
|
506
841
|
);
|
|
507
842
|
|
|
508
843
|
server.tool(
|
|
509
844
|
'trackly_get_apply_execution',
|
|
510
845
|
'Read the authoritative execution state, latest current-wave identity, and aggregate progress funnel.',
|
|
511
846
|
{ executionId: z.number().int().min(1) },
|
|
512
|
-
wrapTool(async ({ executionId }) =>
|
|
513
|
-
|
|
514
|
-
|
|
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')
|
|
515
860
|
);
|
|
516
861
|
|
|
517
862
|
server.tool(
|
|
@@ -704,16 +1049,62 @@ function registerApplyTools(
|
|
|
704
1049
|
|
|
705
1050
|
server.tool(
|
|
706
1051
|
'trackly_advance_apply_execution',
|
|
707
|
-
'Advance an execution transactionally for the current browser surface.
|
|
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.',
|
|
708
1053
|
{
|
|
709
1054
|
executionId: z.number().int().min(1),
|
|
710
1055
|
expectedRevision: z.number().int().min(1),
|
|
711
1056
|
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
712
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(),
|
|
713
1065
|
},
|
|
714
|
-
wrapTool(async ({ executionId, idempotencyKey, ...body }) =>
|
|
715
|
-
|
|
716
|
-
|
|
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')
|
|
717
1108
|
);
|
|
718
1109
|
|
|
719
1110
|
server.tool(
|
|
@@ -739,9 +1130,84 @@ function registerApplyTools(
|
|
|
739
1130
|
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
740
1131
|
reasonCode: z.enum(APPLY_EXECUTION_STOP_REASON_CODES).optional(),
|
|
741
1132
|
},
|
|
742
|
-
wrapTool(async ({ executionId, idempotencyKey, ...body }) =>
|
|
743
|
-
|
|
744
|
-
|
|
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')
|
|
745
1211
|
);
|
|
746
1212
|
|
|
747
1213
|
server.tool(
|
|
@@ -1271,7 +1737,7 @@ function registerApplyTools(
|
|
|
1271
1737
|
content: { type: 'text', text: APPLY_RELIABILITY_PROMPT },
|
|
1272
1738
|
}, {
|
|
1273
1739
|
role: 'user',
|
|
1274
|
-
content: { type: 'text', text: 'Before generating questions or filling controls, run the skill 4.
|
|
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.' },
|
|
1275
1741
|
}, {
|
|
1276
1742
|
role: 'user',
|
|
1277
1743
|
content: { type: 'text', text: 'Only active=true identifies resumable execution work. Active=false and preserved=true is terminal read-only reconciliation evidence.' },
|
|
@@ -1279,13 +1745,13 @@ function registerApplyTools(
|
|
|
1279
1745
|
role: 'user',
|
|
1280
1746
|
content: {
|
|
1281
1747
|
type: 'text',
|
|
1282
|
-
text: 'Protocol 3.
|
|
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.',
|
|
1283
1749
|
},
|
|
1284
1750
|
}, {
|
|
1285
1751
|
role: 'user',
|
|
1286
1752
|
content: {
|
|
1287
1753
|
type: 'text',
|
|
1288
|
-
text: 'Legacy protocol compatibility gate: require the fetched compatibleSkillMinimumVersion or newer for all new work and use protocol 3.
|
|
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.',
|
|
1289
1755
|
},
|
|
1290
1756
|
}, {
|
|
1291
1757
|
role: 'user',
|