trackly-cli 0.14.1 → 0.15.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/contracts/trackly-apply-contract-history.json +6 -0
- package/contracts/trackly-apply-tools.json +9 -4
- package/docs/plans/2026-08-05-001-trackly-apply-durable-recovery-reconciliation-plan.md +14 -12
- package/docs/solutions/integration-issues/durable-exact-recovery-after-browser-state-loss.md +126 -0
- package/lib/agent.js +1 -1
- package/mcp/analytics.js +3 -1
- package/mcp/apply-tools.js +47 -9
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/trackly-apply/SKILL.md +6 -4
- package/skills/trackly-apply/references/answer-compounding.md +30 -4
- package/skills/trackly-apply/references/answer-resolution.md +123 -0
- package/skills/trackly-apply/references/application-writing.md +7 -0
- package/skills/trackly-apply/references/batch-orchestration.md +26 -0
- package/skills/trackly-apply/references/form-integrity.md +5 -2
|
@@ -17,5 +17,11 @@
|
|
|
17
17
|
},
|
|
18
18
|
"3.7.1": {
|
|
19
19
|
"contractSha256": "0eb582fcac72804347c5da53d214a67908f657f600536db7b42167350bb3f3ab"
|
|
20
|
+
},
|
|
21
|
+
"3.7.2": {
|
|
22
|
+
"contractSha256": "058dc698ff1b0722f48b9f244773c0f09286046540609c31f001fe8604b06093"
|
|
23
|
+
},
|
|
24
|
+
"3.7.3": {
|
|
25
|
+
"contractSha256": "d37e7ec0a17d3dba399f12dd7281c51536ca92bbb21125ca09288d1678f34c14"
|
|
20
26
|
}
|
|
21
27
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "3.7.
|
|
2
|
+
"contractVersion": "3.7.3",
|
|
3
3
|
"constants": {
|
|
4
4
|
"applyExecutionMaxTarget": 20,
|
|
5
5
|
"applyScenarioCodes": ["browser_reclaim", "resume_upload", "resume_parser_recheck", "semantic_boolean_commit", "custom_select_commit", "multi_step_navigation", "free_text_voice", "required_error_sweep", "final_consent", "handoff_reclaim", "critical_contact_integrity", "manual_submit_boundary", "job_identity_match"],
|
|
@@ -23,11 +23,16 @@
|
|
|
23
23
|
"applyUploadStages": ["semantic_control_discovered", "file_chooser_armed", "file_chooser_opened", "set_files_succeeded", "user_facing_filename_committed", "parser_fields_rechecked"],
|
|
24
24
|
"applyUploadFailureCodes": ["upload_capability_unavailable", "upload_stage_missing", "upload_stage_out_of_order", "upload_stage_duplicate", "control_ambiguous", "chooser_timeout", "chooser_open_without_commit", "file_missing", "file_expired", "set_files_failed", "filename_uncommitted", "unexpected_file_navigation", "parser_unsettled", "parser_field_regression"]
|
|
25
25
|
},
|
|
26
|
+
"toolInputInvariants": {
|
|
27
|
+
"trackly_get_apply_execution_snapshot": {
|
|
28
|
+
"officeProjectionMemberIdsMustExistInMemberIds": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
26
31
|
"tools": {
|
|
27
32
|
"trackly_get_apply_queue": "{limit:z.number().int().min(1).max(100).optional(),cursor:z.string().min(1).max(2048).optional()}",
|
|
28
|
-
"trackly_get_application_profile": "{includeSensitive:z.boolean().optional(),provider:z.string().max(100).optional(),companyId:z.string().max(100).optional(),jurisdiction:iso3166Alpha2Schema.optional()}",
|
|
33
|
+
"trackly_get_application_profile": "{includeSensitive:z.boolean().optional(),provider:z.string().max(100).optional(),companyId:z.string().max(100).optional(),jurisdiction:iso3166Alpha2Schema.optional(),office:officeScopeSchema.optional()}",
|
|
29
34
|
"trackly_get_profile_onboarding": "{}",
|
|
30
|
-
"trackly_update_application_profile": "{expectedRevision:z.number().int().min(1),source:z.enum(['web','ios','macos','codex','claude','mcp']).optional(),changes:z.array(z.discriminatedUnion('scope',[z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('global'),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('provider'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('company'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('jurisdiction'),scopeValue:iso3166Alpha2Schema,questionLabel:z.string().max(1000).optional()})])).max(100).optional(),education:z.array(z.object({school:z.string().min(1).max(500),degree:z.string().max(500).nullable().optional(),fieldOfStudy:z.string().max(500).nullable().optional(),gpa:z.string().max(50).nullable().optional(),startDate:z.string().max(50).nullable().optional(),endDate:z.string().max(50).nullable().optional()})).max(20).optional(),confirmProfile:z.boolean().optional(),sensitiveStorageConsent:z.boolean().optional(),sensitiveRevocationConfirmToken:z.string().regex(/^[a-f0-9]{64}$/).optional()}",
|
|
35
|
+
"trackly_update_application_profile": "{expectedRevision:z.number().int().min(1),source:z.enum(['web','ios','macos','codex','claude','mcp']).optional(),changes:z.array(z.discriminatedUnion('scope',[z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('global'),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('provider'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('company'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('jurisdiction'),scopeValue:iso3166Alpha2Schema,questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('office'),scopeValue:officeScopeSchema,questionLabel:z.string().max(1000).optional()})])).max(100).optional(),education:z.array(z.object({school:z.string().min(1).max(500),degree:z.string().max(500).nullable().optional(),fieldOfStudy:z.string().max(500).nullable().optional(),gpa:z.string().max(50).nullable().optional(),startDate:z.string().max(50).nullable().optional(),endDate:z.string().max(50).nullable().optional()})).max(20).optional(),confirmProfile:z.boolean().optional(),sensitiveStorageConsent:z.boolean().optional(),sensitiveRevocationConfirmToken:z.string().regex(/^[a-f0-9]{64}$/).optional()}",
|
|
31
36
|
"trackly_start_apply_execution": "{mode:z.literal('complete_next_n_accessible'),target:z.number().int().min(1).max(APPLY_EXECUTION_MAX_TARGET),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
32
37
|
"trackly_get_active_apply_execution": "{}",
|
|
33
38
|
"trackly_get_apply_execution": "{executionId:z.number().int().min(1)}",
|
|
@@ -35,7 +40,7 @@
|
|
|
35
40
|
"trackly_recover_exact_apply_members": "{sourceExecutionId:z.number().int().min(1),sourceSnapshotHash:z.string().regex(/^[a-f0-9]{64}$/),candidateIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values).size===values.length,{message:'candidateIdsmustbeunique'}),explicitExactSetConfirmation:z.literal(true),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
36
41
|
"trackly_list_apply_review_handoffs": "{executionId:z.number().int().min(1)}",
|
|
37
42
|
"trackly_claim_apply_review_handoff": "{handoffId:z.number().int().min(1),members:z.array(z.object({memberId:z.number().int().min(1),classification:z.enum(APPLY_HANDOFF_RECONCILIATION_CLASSIFICATION_CODES)}).strict()).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values.map(({memberId})=>memberId)).size===values.length,{message:'membersmustcontainuniquememberIdvalues'}),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
38
|
-
"trackly_get_apply_execution_snapshot": "{executionId:z.number().int().min(1),memberIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET),profileKeys:z.array(z.string().min(1).max(200)).max(100).optional(),browserSurface:z.enum(APPLY_BROWSER_SURFACES)}",
|
|
43
|
+
"trackly_get_apply_execution_snapshot": "{executionId:z.number().int().min(1),memberIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values).size===values.length,{message:'memberIdsmustbeunique'}),profileKeys:z.array(z.string().min(1).max(200)).max(100).refine((values)=>newSet(values).size===values.length,{message:'profileKeysmustbeunique'}).optional(),officeProjections:z.array(z.object({memberId:z.number().int().min(1),office:officeScopeSchema,profileKeys:z.array(z.string().min(1).max(200)).min(1).max(100).refine((values)=>newSet(values).size===values.length,{message:'officeprofileKeysmustbeunique'})}).strict()).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values.map(({memberId})=>memberId)).size===values.length,{message:'officeProjectionsmustcontainuniquememberIdvalues'}).optional(),browserSurface:z.enum(APPLY_BROWSER_SURFACES)}",
|
|
39
44
|
"trackly_resume_parked_apply_member": "{executionId:z.number().int().min(1),memberId:z.number().int().min(1),expectedRevision:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),explicitUserResume:z.literal(true),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
40
45
|
"trackly_approve_apply_execution_resume": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),originalSnapshotHash:z.string().regex(/^[a-f0-9]{64}$/),profileRevision:z.number().int().min(1),resumeId:z.number().int().min(1),resumeSha256:z.string().regex(/^[a-f0-9]{64}$/),resumeFilename:z.string().min(1).max(255),resumeSizeBytes:z.number().int().min(1),expiresAt:z.string().datetime(),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
41
46
|
"trackly_advance_apply_execution": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
@@ -49,9 +49,9 @@ None of these receipts implies another.
|
|
|
49
49
|
- R1. Trackly must persist one cumulative completion credit per execution candidate after the candidate first reaches durable manual review or a valid submitted-state repair.
|
|
50
50
|
- R2. A credited candidate must continue counting after manual submission; submitting it must not create replacement capacity.
|
|
51
51
|
- R3. Execution status, raw cumulative achievement count, target-capped `completed`, current ready/submitted counts, reservations, `targetReached`, and `nextAction` must come from one transactionally consistent state.
|
|
52
|
-
- R4. Trackly must recover
|
|
53
|
-
- R5. Exact recovery must assert the requested source membership, report each member's eligibility, create no substitutes, reuse valid run identity, and require a fresh browser binding and inspection epoch.
|
|
54
|
-
- R6. Exact recovery must reject foreign-user, revoked, changed-requisition, duplicate, and concurrently owned members. Already-applied members are reconciliation-only and inactive
|
|
52
|
+
- R4. Trackly must recover the exact confirmed set from one prior terminal or expired execution only when every confirmed member is eligible, without changing queue recency or selecting newer jobs; otherwise it must create no recovery execution.
|
|
53
|
+
- R5. Exact recovery must assert the requested source membership, evaluate and report each member's eligibility without partial creation, create no substitutes, reuse valid run identity, and require a fresh browser binding and inspection epoch. The KTD3 addendum below supersedes the original partial-eligibility model: any ineligible confirmed member rejects the whole set and creates no recovery execution.
|
|
54
|
+
- R6. Exact recovery must reject foreign-user, revoked, changed-requisition, duplicate, and concurrently owned members. Already-applied members are reconciliation-only, not recovery-eligible; including one in the confirmed set must fail the request atomically. Reconcile that member through its review-handoff/outcome path, and recover any still-eligible sibling only through a separately confirmed exact set. An inactive member likewise makes the confirmed exact set ineligible. Authentication, account creation, OTP, CAPTCHA, and other runtime access barriers are not pre-creation eligibility facts; discover and persist them afterward as typed, non-counting live dispositions.
|
|
55
55
|
- R7. Tab restoration, form restoration, and mutation authority must be exposed as separate value-free recovery facts.
|
|
56
56
|
|
|
57
57
|
**Review and submission reconciliation**
|
|
@@ -84,8 +84,8 @@ None of these receipts implies another.
|
|
|
84
84
|
- F1. Exact interrupted-member recovery
|
|
85
85
|
- **Trigger:** A prior execution expired or ended after tabs or form state were lost.
|
|
86
86
|
- **Actors:** User, Trackly backend, MCP client, browser harness.
|
|
87
|
-
- **Steps:** The client lists bounded backend-owned recoverable source summaries when local identity is absent; the user confirms the exact candidate set; the client requests those source candidates; the backend validates the immutable source set and
|
|
88
|
-
- **Outcome:**
|
|
87
|
+
- **Steps:** The client lists bounded backend-owned recoverable source summaries when local identity is absent; the user confirms the exact candidate set; the client requests those source candidates; before creation, the backend validates the immutable source set and confirms that every requested candidate is eligible; only then does the backend create one exact recovery execution; the harness verifies returned membership; the harness obtains fresh browser and mutation receipts; the harness rehydrates only blank or agent-owned controls.
|
|
88
|
+
- **Outcome:** Either the full confirmed set becomes mutable or no recovery execution is created. Queue ordering is unchanged.
|
|
89
89
|
- **Covered by:** R4-R7, R18, R22-R23.
|
|
90
90
|
|
|
91
91
|
- F2. Cumulative two-job completion
|
|
@@ -132,15 +132,15 @@ None of these receipts implies another.
|
|
|
132
132
|
|
|
133
133
|
- AE3. **Exact recovery with newer jobs present**
|
|
134
134
|
- **Covers:** R4-R7.
|
|
135
|
-
- **Given:** Two expired source members and newer Check Later jobs exist.
|
|
135
|
+
- **Given:** Two eligible expired source members and newer Check Later jobs exist.
|
|
136
136
|
- **When:** The user requests recovery of those two source members.
|
|
137
|
-
- **Then:**
|
|
137
|
+
- **Then:** Both requested jobs, and no others, enter the recovery execution; queue timestamps remain unchanged.
|
|
138
138
|
|
|
139
139
|
- AE4. **Mixed exact recovery set**
|
|
140
140
|
- **Covers:** R5-R7, R13.
|
|
141
141
|
- **Given:** One source member is applied, one is recoverable, and one is inactive.
|
|
142
142
|
- **When:** Recovery is requested for all three.
|
|
143
|
-
- **Then:** Identity assertion
|
|
143
|
+
- **Then:** Identity assertion and per-member eligibility evaluation cover all three, the request fails atomically, and no recovery execution or replacement is created.
|
|
144
144
|
|
|
145
145
|
- AE5. **Ashby same-route success**
|
|
146
146
|
- **Covers:** R9-R12, R21.
|
|
@@ -209,7 +209,9 @@ None of these receipts implies another.
|
|
|
209
209
|
|
|
210
210
|
- KTD1. **Persist completion credit in an append-only achievement ledger.** (session-settled: user-approved — chosen over concurrent unsubmitted inventory: once a job is ready or submitted it remains part of the requested total.) Create one value-free credit per `(execution_id, user_id, frozen_job_id)`, constrained to reviewed sources such as `review_ready` and `submitted_repair`. Insert the credit in the same transaction as the durable outcome. Expose `achievementCount` as the raw unique effective-credit count and compute target-facing `completed = LEAST(achievementCount, target)`. Credits are the sole target-achievement input; current ready/submitted counts remain operator projections. Do not add mutable columns to immutable execution candidates. A credit later proven to originate from invalid evidence may be neutralized only by an append-only compensating revocation through an audited internal repair operation; later submission or lifecycle change is never a revocation reason. Governs R1-R3.
|
|
211
211
|
- KTD2. **Characterize the live state sequence before changing accounting.** Current unit tests already cover `ready + submitted`, so implementation begins with a Postgres-backed checkpoint, certification, bulk-outcome, submission, and progress-read reproduction. A passing unit calculator test is not sufficient evidence. Governs R1-R3, R8.
|
|
212
|
-
- KTD3. **Model exact recovery as a new execution mode over stable source candidates.**
|
|
212
|
+
- KTD3. **Model exact recovery as a new execution mode over stable source candidates.** The original August 5 decision used `recover_exact_members` with `sourceExecutionId`, exact execution-candidate IDs proven inside the source snapshot hash, optimistic revision, and idempotency key. It persisted `source_execution_id`, `source_candidate_id`, and the attempt-specific `source_member_id` when present; identity assertion was atomic, eligibility could be partial (superseded—see the shipped-contract addendum immediately below), and the server created no substitutes. Governs R4-R7.
|
|
213
|
+
|
|
214
|
+
**Shipped-contract addendum (2026-08-13):** Backend implementation and adversarial review superseded partial eligibility with an atomic all-or-nothing eligibility check. If any confirmed candidate is no longer eligible, the server creates no recovery execution and no substitutes. AE4 and U4 below reflect this shipped contract while the original decision remains recorded here.
|
|
213
215
|
- KTD4. **Restrict initial recovery to one source execution.** This keeps authorization, ordering, and historical semantics bounded. Arbitrary job IDs or cross-execution recovery remain deferred. Governs R4-R6.
|
|
214
216
|
- KTD5. **Reuse the existing evidence and outcome repair path behind a typed local detection boundary.** Provider recognition produces a local `DetectedSubmission` containing provider strategy version, handoff/member identity, inspection epoch, bounded evidence type, confidence, and a value-free fingerprint. The fingerprint is a canonical digest only of provider strategy version, bounded evidence type, backend-owned handoff/member identity, and inspection epoch; DOM text, field values, URLs, paths, filenames, and raw browser identifiers are prohibited inputs. Group reconciliation yields one of `detected`, `user_confirmed`, `unresolved`, or `contradictory` per member. Only the existing typed evidence/outcome path writes backend state; page text stays local. Governs R10-R12, R21.
|
|
215
217
|
- KTD6. **Create a first-class execution-owned review-handoff receipt.** Truth attestations remain batch-scoped certification evidence, but they cannot safely own a visible handoff spanning replacement waves. Persist an ordered set of `{batchId, memberId, runId, memberVersion, inspectionEpoch}`, its hash, and lifecycle `active | partially_reconciled | resolved | superseded | expired`. Creating a new receipt may supersede an older unresolved receipt only when the new ordered set contains every unresolved older member; otherwise both remain addressable and an unqualified group phrase is ambiguous. The first group-language reconciliation atomically claims the receipt under the outcome request idempotency key and stores the exact per-member result; retries return that result. Later confirmation must name the receipt and its unresolved members. A receipt becomes partially reconciled after a strict subset reaches durable submission, resolved after all members do, and expired only after its bounded handoff window. Group phrases resolve to an explicit receipt ID or to the sole unambiguous active/partially reconciled receipt. Governs R9-R10.
|
|
@@ -333,9 +335,9 @@ The backend owns durable identity, membership, milestones, and reconciliation. T
|
|
|
333
335
|
- **Dependencies:** U2-U3.
|
|
334
336
|
- **Repositories:** `close-ai`.
|
|
335
337
|
- **Files:** `src/services/application-profile/apply-execution-contract.ts`, `src/services/application-profile/batch-service.ts`, `src/services/application-profile/service.ts`, `src/routes/trackly-apply.ts`, `src/mcp/server.ts`, execution service/route/MCP contract tests.
|
|
336
|
-
- **Approach:** Add an authenticated discovery read returning only the user's recoverable execution IDs, snapshot hashes, stable candidate IDs, bounded lifecycle/eligibility codes, and expiry. It must not return profile values, browser data, or local paths. After explicit exact-set confirmation, create recovery with source execution, stable execution-candidate list, source snapshot hash, and idempotency key. Validate ownership and no competing mutable execution under the documented lock order.
|
|
337
|
-
- **Test scenarios:** Complete local context loss followed by discovery; cross-user discovery rejection; explicit exact-set confirmation; newer queue candidates present; saved timestamps unchanged; mixed eligible/applied/inactive set; duplicate IDs; changed requisition; foreign user; concurrent identical recovery; concurrent different recovery; old evidence rejected; second interruption; recovery immediately before and after operational expiry; recovery after daily purge but before `recoverable_until`; rejection after retention expiry.
|
|
338
|
-
- **Verification:** AE3-AE4 pass and
|
|
338
|
+
- **Approach:** Add an authenticated discovery read returning only the user's recoverable execution IDs, snapshot hashes, stable candidate IDs, bounded lifecycle/eligibility codes, and expiry. It must not return profile values, browser data, or local paths. After explicit exact-set confirmation, create recovery with source execution, stable execution-candidate list, source snapshot hash, and idempotency key. Validate ownership and no competing mutable execution under the documented lock order. Evaluate eligibility for the entire asserted source set before creation; if any confirmed candidate is ineligible, fail atomically without creating an execution or substituting another candidate. When all are eligible, preserve source execution/candidate/member lineage, create one immutable non-replenishing recovery execution for the exact confirmed set, and require fresh epochs and browser bindings. Separate operational expiry from a bounded recovery-retention boundary so the nightly purge cannot erase eligible identity mid-window.
|
|
339
|
+
- **Test scenarios:** Complete local context loss followed by discovery; cross-user discovery rejection; explicit exact-set confirmation; newer queue candidates present; saved timestamps unchanged; wholly eligible set; mixed eligible/applied/inactive set rejected with no execution; duplicate IDs; changed requisition; foreign user; concurrent identical recovery; concurrent different recovery; old evidence rejected; second interruption; recovery immediately before and after operational expiry; recovery after daily purge but before `recoverable_until`; rejection after retention expiry.
|
|
340
|
+
- **Verification:** AE3-AE4 pass; AE4 explicitly proves that an ineligible confirmed member yields zero recovery executions, and every execution created by an eligible case has a response membership hash that exactly matches the full asserted source set.
|
|
339
341
|
|
|
340
342
|
### U5. Publish protocol and MCP recovery parity
|
|
341
343
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Durable exact recovery after browser state loss
|
|
3
|
+
date: 2026-08-13
|
|
4
|
+
category: integration-issues
|
|
5
|
+
module: Trackly Apply durable recovery
|
|
6
|
+
problem_type: integration_issue
|
|
7
|
+
component: assistant
|
|
8
|
+
symptoms:
|
|
9
|
+
- Restored browser tabs had lost unsaved application-form values after a browser or laptop restart
|
|
10
|
+
- A Zoox submission attempt reached ERR_FILE_NOT_FOUND without evidence identifying the failed upload stage or cause
|
|
11
|
+
- Earlier browser observations could appear valid even though they belonged to an obsolete inspection epoch
|
|
12
|
+
- Authentication and pre-form challenge pages risked being mistaken for completed accessible applications
|
|
13
|
+
root_cause: missing_workflow_step
|
|
14
|
+
resolution_type: code_fix
|
|
15
|
+
severity: high
|
|
16
|
+
related_components: [service_object, tooling, development_workflow]
|
|
17
|
+
tags: [trackly-apply, exact-recovery, browser-restart, inspection-epoch, manual-submit, durable-handoff]
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Durable exact recovery after browser state loss
|
|
21
|
+
|
|
22
|
+
## Problem
|
|
23
|
+
|
|
24
|
+
Trackly Apply previously had no complete recovery contract for the gap between durable execution state and ephemeral browser state. A laptop restart, browser relaunch, renderer crash, lost automation connection, or missing local upload artifact could leave a server execution intact while destroying the tab handle, DOM snapshot, unsaved employer-form values, field provenance, or upload handle that had authorized the next mutation.
|
|
25
|
+
|
|
26
|
+
The shipped recovery contract separates those layers. Exact recovery restores only a user-confirmed, server-retained candidate lineage; the agent must independently restore the browser surface, revalidate or reconstruct the form, and reacquire current mutation authority before writing anything.
|
|
27
|
+
|
|
28
|
+
## Symptoms
|
|
29
|
+
|
|
30
|
+
- Session evidence from the August 4–5 Apply run: after Chrome relaunched, previously filled Ontic and Lila forms appeared empty even though their tabs had been restored. The tab list survived, but the unsaved application DOM did not.
|
|
31
|
+
- Session evidence from the same run: a Zoox submission attempt navigated to Chrome's `ERR_FILE_NOT_FOUND`. The failed upload stage and underlying cause were not captured, so the cause remains unknown.
|
|
32
|
+
- A tab could remain visible to the user while disappearing from the automation controller, or the controller could retain a stale handle after navigation or a renderer restart. Application identity must not be inferred from window position, a transient tab number, or title text alone (`skills/trackly-apply/references/browser-lifecycle.md`, opening identity rules).
|
|
33
|
+
- Earlier field checks, upload evidence, and review receipts could look plausible after recovery while belonging to an obsolete inspection epoch (`skills/trackly-apply/references/batch-orchestration.md`, “Exact recovery after local context loss”).
|
|
34
|
+
- Authentication, account creation, OTP, and pre-form CAPTCHA pages could be mistakenly counted toward the review-ready target even though no safely accessible form existed. These classifications consume no completed slot (`skills/trackly-apply/references/batch-orchestration.md`, “Parent execution and child waves”).
|
|
35
|
+
|
|
36
|
+
## What Didn't Work
|
|
37
|
+
|
|
38
|
+
### Choosing external Chrome or the in-app browser as the persistence strategy
|
|
39
|
+
|
|
40
|
+
Neither browser is a durable store. External Chrome can restore tabs without restoring page-local draft state; an in-app renderer or its automation connection can also restart or lose its controller inventory. Changing browser hosts may improve control availability, but it cannot make DOM state, tab handles, file chooser handles, or local upload paths authoritative.
|
|
41
|
+
|
|
42
|
+
### Treating tab presence as proof of saved form state
|
|
43
|
+
|
|
44
|
+
A restored URL proves only that a page can be reopened. It does not prove that the employer saved the draft, that the same requisition is loaded, that user edits survived, or that Trackly still authorizes mutation. Recovery therefore requires three separate proofs: tab restoration, form-state restoration or safe reconstruction, and current mutation authority (`skills/trackly-apply/references/browser-lifecycle.md`, “Missing-tab recovery”).
|
|
45
|
+
|
|
46
|
+
### Reusing cached resume paths
|
|
47
|
+
|
|
48
|
+
Regardless of the unclassified Zoox failure's cause, an earlier local path cannot be reused as attachment evidence. A prepared artifact may expire, move, or be deleted while the browser still displays an old reference. The supported sequence verifies the prepared resume immediately before attachment, checks the employer-facing committed filename, and rechecks parser-sensitive fields (`skills/trackly-apply/references/browser-upload.md`, “Capability gate”).
|
|
49
|
+
|
|
50
|
+
### Using generic advancement or replacement after context loss
|
|
51
|
+
|
|
52
|
+
Generic “continue with the next job” behavior is wrong for exact recovery because it can substitute newly saved or similar jobs for the user's original applications. Fixed membership may not be replaced, rescored, or expanded (`skills/trackly-apply/references/batch-orchestration.md`, “Freeze before browser work”). Recovery must use one retained source snapshot and exactly the candidates the user confirmed.
|
|
53
|
+
|
|
54
|
+
### Refilling from profile data without provenance
|
|
55
|
+
|
|
56
|
+
Blind autofill can overwrite a correction the user made after the agent's last observation. After ledger loss, every unexplained non-empty field becomes `unknown_external_change` and must be preserved; only safely empty or still agent-owned fields may be filled (`skills/trackly-apply/references/browser-lifecycle.md`, “Local tab ledger”).
|
|
57
|
+
|
|
58
|
+
## Solution
|
|
59
|
+
|
|
60
|
+
The coordinated durable recovery work shipped through [trackly-cli PR #100](https://github.com/trackly-app/trackly-cli/pull/100), which enforces exact-set discovery and response validation in the CLI/MCP/skill layer, and [close-ai PR #1391](https://github.com/trackly-app/close-ai/pull/1391), whose backend implementation owns atomic all-or-nothing recovery creation.
|
|
61
|
+
|
|
62
|
+
### 1. Discover a bounded, value-free recovery menu
|
|
63
|
+
|
|
64
|
+
Before discovery, call `trackly_get_apply_protocol` and require the advertised CLI, skill, MCP-contract, rollout, and browser capabilities to be compatible with exact recovery. If they are incompatible or exact recovery is disabled, stop without mutation; retained older work may use only the legacy recovery path explicitly published by that protocol (`skills/trackly-apply/SKILL.md`, “Preflight”). Then call `trackly_get_active_apply_execution`. When it returns `active: true`, reclaim that execution and its unresolved waves instead of listing or creating terminal exact recovery. Only when it proves there is no active execution may the compatible server's retained terminal lineage be listed with `trackly_list_recoverable_apply_executions`. The response exposes stable source execution IDs, snapshot hashes, candidate IDs, job IDs, queue positions, and eligibility codes; duplicate source and candidate identities are rejected (`mcp/apply-tools.js`, `recoverableCandidateSchema` and `recoverableExecutionsResponseSchema`). Resolve each job through Trackly and present the exact company, role, requisition identity when available, and source execution. Never reconstruct identity from chat, tab order, page copy, or search results (`skills/trackly-apply/references/batch-orchestration.md`, “Exact recovery after local context loss”).
|
|
65
|
+
|
|
66
|
+
### 2. Require explicit confirmation of one exact set
|
|
67
|
+
|
|
68
|
+
Call `trackly_recover_exact_apply_members` only with one discovered source execution, that source's snapshot hash, unique confirmed candidate IDs, `explicitExactSetConfirmation: true`, and a fresh idempotency key. Before the write, the CLI rejects an undiscovered source, changed snapshot hash, or candidate outside the latest discovery menu (`mcp/apply-tools.js`, `trackly_recover_exact_apply_members` registration).
|
|
69
|
+
|
|
70
|
+
Recovery is all-or-nothing at the client boundary. The response validator requires `assertedCandidateIds`, `eligibleCandidateIds`, and every eligibility row to equal the requested set exactly, without duplicates or non-recoverable results (`mcp/apply-tools.js`, `validateExactRecoveryResponse`). This prevents partial recovery, substitution, and replenishment. Frozen order and membership remain server-owned.
|
|
71
|
+
|
|
72
|
+
### 3. Restore browser state and authority independently
|
|
73
|
+
|
|
74
|
+
Recovery creation does not grant a usable batch lease. First consume the recovery response's authoritative `progress` and obey `progress.nextAction`; stop or perform the specified non-browser action when the directive is blocking or terminal, and claim browser work only when that directive makes it actionable. Then consume every actionable entry in the recovered execution's `unresolvedWaves` in ascending `waveOrder`; `currentWave` is only the latest scheduling identity, never the complete recovery set. For each actionable unresolved wave, page only its exact linked batch with `trackly_get_apply_batch`, then acquire or renew ownership with `trackly_claim_apply_batch` using the latest revision. Use only the member/version/epoch/lease binding returned by that claim (`skills/trackly-apply/SKILL.md`, “Start every run”; `skills/trackly-apply/references/batch-orchestration.md`, “Backend tool sequence”).
|
|
75
|
+
|
|
76
|
+
Branch on each claimed recovered member's run binding. If `runId` is absent, call `trackly_start_apply_run` with that exact current binding. Stop without browser binding or private-data mutation when the returned run has a non-null `executionBlocker`; otherwise call `trackly_bind_apply_surface` for the returned run with `initial_binding` and its current batch/member/version/epoch/lease plus browser-binding inputs. If `runId` already exists, first verify that run's published protocol remains compatible with the negotiated exact-recovery contract, then reuse it and, after the same non-null `executionBlocker` stop gate, call `trackly_bind_apply_surface` with `recovery_binding` for every recovered surface, including a still-visible, reopened, or reclaimed exact tab. Never start a replacement run merely because browser control was interrupted. The returned member version and inspection epoch are authoritative, and prior-epoch evidence is invalid (`skills/trackly-apply/SKILL.md`, “Start every run”; `skills/trackly-apply/references/browser-lifecycle.md`, “Reconciliation”). Execute the run's published origin policy, then revalidate HTTPS origin, employer, role, requisition, job identity, semantic controls, and ATS tenant only when that policy makes a separate tenant applicable; `trackly_employer_source_exact_origin` does not require one. Reinventory the entire form because a draft must never be assumed to have survived a crash or handoff (`skills/trackly-apply/references/browser-lifecycle.md`, “Missing-tab recovery”).
|
|
77
|
+
|
|
78
|
+
The durable operator receipt must separately report whether the exact tab was restored, whether employer form state was restored or safely reconstructed, and whether current mutation authority was reacquired. It must also carry completed work, unresolved members, the user's exact next action, the exact resume phrase or observable event that unblocks it, the agent's next action, each browser surface, and the authoritative execution funnel. A missing original tab may be reopened from the exact backend URL; mutation requires the resulting form to be restored or safely reconstructed and current authority to be reacquired, while the tab-restoration fact remains independently reported.
|
|
79
|
+
|
|
80
|
+
Before each claimed batch's first recovered form mutation, offer that exact batch's optional receipt-deduplication check once, keyed by its immutable batch ID and membership hash. Declining or lacking an approved inbox connector never blocks browser work. If the user opts in and a receipt matches, resolve its exact requisition and submission evidence through the documented confirmation and outcome path before mutating that affected member; never infer a duplicate from employer name alone or let the optional check expand the frozen set (`skills/trackly-apply/SKILL.md`, “Start every run”).
|
|
81
|
+
|
|
82
|
+
Before any recovered form write, prove the adapter can preserve every target tab through either a usable session finalizer with a complete controller-owned plus user-owned inventory union, or a documented per-tab durable-handoff primitive with a verifiable persistence receipt. If neither preservation path works end to end, stop before private-data mutation. Then report a committed same-run `browser_ready` observation for every current run that will proceed, using its exact current batch, member, inspection epoch, browser surface, and browser-binding hash. Stop the affected run without private-data mutation if Trackly does not durably accept that readiness attestation; this gate applies whether or not the form has a Resume/CV control (`skills/trackly-apply/SKILL.md`, “Start every run”; `skills/trackly-apply/references/browser-lifecycle.md`, “Preserving browser state at turn end”).
|
|
83
|
+
|
|
84
|
+
### 4. Park access barriers before form mutation
|
|
85
|
+
|
|
86
|
+
Classify the entire current wave with the typed value-free set: `accessible`, `authentication_required`, `account_creation_required`, `otp_required`, `captcha_before_form`, `captcha_at_submit`, `manual_only`, or `unknown_unobservable` (`skills/trackly-apply/references/batch-orchestration.md`, “Parent execution and child waves”). Record every live result through `trackly_record_apply_execution_dispositions` with the exact current-wave job, batch, member, run, member-version, inspection-epoch, and browser-surface binding. Do not begin provenance inventory, resume preparation, attachment, or any other form mutation for an accessible sibling until the complete current probe set is durably accepted. Authentication, account creation, OTP, and pre-form CAPTCHA consume no target slot. A submit-time CAPTCHA can remain review-ready because the user owns the final action; the agent never solves, stores, or bypasses it (`skills/trackly-apply/references/batch-orchestration.md`, “Challenge placement”).
|
|
87
|
+
|
|
88
|
+
### 5. Preserve user-owned and unknown values
|
|
89
|
+
|
|
90
|
+
Take a fresh semantic inventory before the first write. Maintain provenance by execution, run, inspection epoch, and semantic field fingerprint. Preserve `user_edited` values byte-for-byte. If the ledger was lost, classify every unknown non-empty value as `unknown_external_change` instead of refilling (`skills/trackly-apply/references/browser-lifecycle.md`, “Local tab ledger”). Resume parsing, rerendering, recovery, and final validation cannot overwrite those values (`skills/trackly-apply/SKILL.md`, “Fill the form”).
|
|
91
|
+
|
|
92
|
+
### 6. Reverify the exact resume at attachment time
|
|
93
|
+
|
|
94
|
+
Before inspecting attachment controls, require the active browser adapter to advertise semantic control discovery; fail closed without inspecting or guessing when that capability is absent. Use it to discover whether the form has a real semantic Resume/CV attachment control. If it does not, skip the entire resume approval and upload sequence. For each current recovered run with a real Resume/CV control, call `trackly_prepare_resume` with that exact run ID, browser surface, and browser-binding hash, and let the user inspect the prepared file plus its exact resume ID, SHA-256, filename, size, and expiry. Reuse execution-scoped resume approval only when Trackly then proves the same prepared resume hash, profile revision, snapshot lineage, and an unexpired approval; otherwise obtain fresh approval for that verified prepared artifact and recovered execution. Bind confirmation to the current run and SHA-256. Then require chooser arming, file attachment, committed-filename inspection, and parser-field recheck. If approval, run-bound preparation and confirmation, or any capability is missing, fail closed and hand the upload to the user before opening a chooser. Arm and prove the discovered control's chooser, run `trackly_verify_prepared_resume` with the current prepare-issued proof, and immediately attach only that verified artifact. Verify the visible employer-facing filename and recheck every field the parser may have changed. Pass the six ordered value-free stage outcomes to `trackly_validate_apply_resume_upload`; stop or hand off unless it returns `safeToClaimAttachment: true` (`skills/trackly-apply/references/browser-upload.md`, “Capability gate”). An earlier approval proves which content the user authorized; it does not prove which bytes the current employer form received.
|
|
95
|
+
|
|
96
|
+
### 7. Stop at manual review
|
|
97
|
+
|
|
98
|
+
Submit remains a hard human boundary. The skill forbids the agent from clicking Submit even when the user previously approved submission (`skills/trackly-apply/SKILL.md`, “Non-negotiable rules”). When a recovered form passes every review gate, checkpoint it with `review/manual_submit` and its exact current epoch, then obtain final truth certification for the exact complete ready subset with the correct `resumeDependency` branch: use `approved` plus the exact approved resume identity when any certified member depends on an attachment, or `not_applicable` with no resume ID/hash when no certified member has a Resume/CV control. Record literal `outcome: review_ready`, and verify every returned run is `awaiting_manual_submit` before presenting the handoff. Focus or reveal each exact review tab and prove current adapter-visible presentation or a current tab-bound handoff receipt; inventory membership alone is not visibility proof. If presentation cannot be proven, use the visibility-unverified handoff and do not instruct the user to Submit (`skills/trackly-apply/SKILL.md`, “Review handoff”; `skills/trackly-apply/references/browser-lifecycle.md`, “Visible review tabs”). After any grouped human submission statement, first call `trackly_list_apply_review_handoffs` and resolve the statement to the named receipt or sole active receipt; if more than one receipt could match, ask instead of guessing. For a fresh receipt, classify every member in its exact set and call `trackly_claim_apply_review_handoff` once. For a `partially_reconciled` receipt, preserve its immutable stored claim and classifications, skip every member already reconciled, and never issue a fresh claim for that receipt. In either branch, write submission evidence and outcomes only for unreconciled members classified `detected` or `user_confirmed`; ask the user once about `unresolved` members, and preserve `contradictory` members without a success write. Refetch every affected member and require both member lifecycle `submitted` and job state `applied_confirmed`; on conflict, preserve the tab, refresh once, and replay only the documented idempotent evidence/outcome sequence. If either durable state remains unproven, report the reconciliation defect and leave the tab open without claiming completion (`skills/trackly-apply/SKILL.md`, “Review handoff”; `skills/trackly-apply/references/review-handoff.md`, “Group submission reconciliation”).
|
|
99
|
+
|
|
100
|
+
At browser-turn end, execute—not merely advertise—the preservation path proven before mutation. For the session-finalizer path, immediately before finalization call `trackly_validate_apply_tab_keep_set` with the expected set, keep set, and complete controller-owned plus user-owned inventories; continue only when it returns `safeToFinalize: true`, then call the one finalizer with that validated keep set. Otherwise perform and verify the documented durable handoff for every live application tab. Require the finalizer result or every per-tab persistence receipt before reporting the recovered review handoff complete (`skills/trackly-apply/SKILL.md`, “Non-negotiable rules”; `skills/trackly-apply/references/browser-lifecycle.md`, “Preserving browser state at turn end”).
|
|
101
|
+
|
|
102
|
+
## Why This Works
|
|
103
|
+
|
|
104
|
+
The solution places authority in records that survive a browser crash: the server-retained source execution, immutable snapshot hash, exact candidate identities, member versions, inspection epochs, and idempotent mutation receipts. Browser state remains evidence, never authority by itself.
|
|
105
|
+
|
|
106
|
+
Set equality closes substitution. Discovery establishes the eligible menu, explicit confirmation binds user intent to it, the pre-write cache rejects undiscovered candidates, and post-write validation requires every candidate collection to equal the request. A partially eligible response cannot be rationalized into a “close enough” recovery.
|
|
107
|
+
|
|
108
|
+
Inspection epochs close stale-evidence reuse. Field provenance protects user corrections. Point-of-use resume verification distinguishes authorized content from the file actually committed to the employer form. Parking access barriers and preserving manual Submit prevent recovery pressure from weakening authentication, anti-bot, or consent boundaries.
|
|
109
|
+
|
|
110
|
+
## Prevention
|
|
111
|
+
|
|
112
|
+
- Model browser tabs, DOM snapshots, control handles, chooser handles, and local artifact paths as leases or observations, never durable state.
|
|
113
|
+
- Bind every exact-recovery member's sanctioned surface: use `initial_binding` only for a newly started run, and use `recovery_binding` whenever recovery reuses an existing run—even when its exact tab is still visible. After ordinary navigation or rerender outside recovery, retain the current binding and revalidate identity, form state, and the semantic DOM before mutation.
|
|
114
|
+
- Keep tab restored, form restored/reconstructed, and mutation authority reacquired as separate assertions in code, tests, and operator output.
|
|
115
|
+
- Preserve exact recovery as a dedicated operation; never route it through normal advancement, refill, replacement, or “next candidate” logic.
|
|
116
|
+
- Assert set equality and uniqueness on both sides of the mutation. Cover duplicate discovery identities, undiscovered candidates, changed snapshot hashes, partial eligible sets, substitutions, and non-recoverable rows.
|
|
117
|
+
- Treat every unexplained non-empty field as protected after provenance loss.
|
|
118
|
+
- Verify resume identity immediately before attachment, verify the committed filename immediately afterward, and re-snapshot parser-sensitive fields.
|
|
119
|
+
- Keep authentication, account creation, OTP, and pre-form CAPTCHA parked and value-free; never count or auto-resume them.
|
|
120
|
+
- Keep “Never Submit” in behavioral tests and handoff templates.
|
|
121
|
+
|
|
122
|
+
## Related Issues
|
|
123
|
+
|
|
124
|
+
- [Trackly CLI PR #100](https://github.com/trackly-app/trackly-cli/pull/100) — CLI, MCP, and skill-side durable exact recovery contract.
|
|
125
|
+
- [Close AI PR #1391](https://github.com/trackly-app/close-ai/pull/1391) — backend durable recovery lineage and exact-member control plane.
|
|
126
|
+
- [Durable recovery implementation plan](../../plans/2026-08-05-001-trackly-apply-durable-recovery-reconciliation-plan.md) — historical design record aligned with the released all-or-nothing recovery contract.
|
package/lib/agent.js
CHANGED
|
@@ -8,7 +8,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
8
8
|
const { apiRequest, downloadFile, getConfigPaths, hasAuth, maintenanceOutput } = require('./client');
|
|
9
9
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
10
10
|
const { contractVersion: MCP_CONTRACT_VERSION } = require('../contracts/trackly-apply-tools.json');
|
|
11
|
-
const SKILL_VERSION = '4.
|
|
11
|
+
const SKILL_VERSION = '4.6.0';
|
|
12
12
|
const CLI_USER_AGENT = `trackly-cli/${PACKAGE_VERSION}`;
|
|
13
13
|
const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
|
|
14
14
|
|
package/mcp/analytics.js
CHANGED
|
@@ -633,6 +633,7 @@ function addOptionalContextToTools(server) {
|
|
|
633
633
|
|
|
634
634
|
function runtimeEventProperties(env = process.env) {
|
|
635
635
|
return {
|
|
636
|
+
$mcp_source: 'local',
|
|
636
637
|
channel: 'mcp',
|
|
637
638
|
contract_version: 3,
|
|
638
639
|
environment: env.TRACKLY_MCP_ANALYTICS_ENVIRONMENT || 'production',
|
|
@@ -797,6 +798,7 @@ function configureMcpAnalytics(server, options = {}) {
|
|
|
797
798
|
const anonymousDistinctId = `mcp-anon-${randomUUID()}`;
|
|
798
799
|
relay = (options.createRelay || createBackendRelay)({ fetch: options.fetch });
|
|
799
800
|
const eventProperties = runtimeEventProperties(env);
|
|
801
|
+
const { $mcp_source: _mcpSource, ...sdkEventProperties } = eventProperties;
|
|
800
802
|
addOptionalContextToTools(server);
|
|
801
803
|
const sdkFailureWarnings = [];
|
|
802
804
|
const analytics = sdk.instrument(server, relay, {
|
|
@@ -818,7 +820,7 @@ function configureMcpAnalytics(server, options = {}) {
|
|
|
818
820
|
...eventProperties,
|
|
819
821
|
},
|
|
820
822
|
}),
|
|
821
|
-
eventProperties: () =>
|
|
823
|
+
eventProperties: () => sdkEventProperties,
|
|
822
824
|
logger: (message) => {
|
|
823
825
|
if (SDK_FAILURE_WARNING.test(String(message))) {
|
|
824
826
|
sdkFailureWarnings.push('sdk_setup_warning');
|
package/mcp/apply-tools.js
CHANGED
|
@@ -40,6 +40,7 @@ const SAFE_IDEMPOTENCY_KEY = /^[\x20-\x7e]+$/;
|
|
|
40
40
|
const iso3166Alpha2Schema = z.string().regex(/^[A-Za-z]{2}$/).refine(isIso3166Alpha2, {
|
|
41
41
|
message: 'Expected an ISO 3166-1 alpha-2 country code',
|
|
42
42
|
});
|
|
43
|
+
const officeScopeSchema = z.string().regex(/^[1-9]\d*:[A-Za-z0-9][A-Za-z0-9._-]{0,150}$/);
|
|
43
44
|
const opaqueTabIdSchema = z.union([
|
|
44
45
|
z.number().int().safe(),
|
|
45
46
|
z.string().min(1).max(512).refine((value) => value.trim().length > 0, {
|
|
@@ -262,7 +263,7 @@ const startApplyRunSchema = z.object({
|
|
|
262
263
|
}
|
|
263
264
|
});
|
|
264
265
|
|
|
265
|
-
const APPLY_RELIABILITY_PROMPT = 'Protocol 3.6 / skill 4.
|
|
266
|
+
const APPLY_RELIABILITY_PROMPT = 'Protocol 3.6 / skill 4.6.0 reliability gate: recover active work first. 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.';
|
|
266
267
|
|
|
267
268
|
function registerApplyTools(
|
|
268
269
|
server,
|
|
@@ -311,13 +312,19 @@ function registerApplyTools(
|
|
|
311
312
|
provider: z.string().max(100).optional(),
|
|
312
313
|
companyId: z.string().max(100).optional(),
|
|
313
314
|
jurisdiction: iso3166Alpha2Schema.optional(),
|
|
315
|
+
office: officeScopeSchema.optional(),
|
|
314
316
|
},
|
|
315
|
-
wrapTool(async ({ includeSensitive, provider, companyId, jurisdiction }) => {
|
|
317
|
+
wrapTool(async ({ includeSensitive, provider, companyId, jurisdiction, office }) => {
|
|
318
|
+
const normalizedCompanyId = companyId?.trim();
|
|
319
|
+
if (office && (!normalizedCompanyId || !office.startsWith(`${normalizedCompanyId}:`))) {
|
|
320
|
+
throw new Error('Office scope must match the requested companyId');
|
|
321
|
+
}
|
|
316
322
|
const qs = new URLSearchParams();
|
|
317
323
|
if (includeSensitive) qs.set('includeSensitive', 'true');
|
|
318
324
|
if (provider) qs.set('provider', provider);
|
|
319
|
-
if (
|
|
325
|
+
if (normalizedCompanyId) qs.set('companyId', normalizedCompanyId);
|
|
320
326
|
if (jurisdiction) qs.set('jurisdiction', jurisdiction);
|
|
327
|
+
if (office) qs.set('office', office);
|
|
321
328
|
return applyApiRequest('GET', `/api/jobscout/application-profile?${qs.toString()}`, null, false, false, MCP_USER_AGENT);
|
|
322
329
|
}, 'Failed to fetch application profile')
|
|
323
330
|
);
|
|
@@ -361,6 +368,11 @@ function registerApplyTools(
|
|
|
361
368
|
value: z.any().optional(), scope: z.literal('jurisdiction'), scopeValue: iso3166Alpha2Schema,
|
|
362
369
|
questionLabel: z.string().max(1000).optional(),
|
|
363
370
|
}),
|
|
371
|
+
z.object({
|
|
372
|
+
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
373
|
+
value: z.any().optional(), scope: z.literal('office'), scopeValue: officeScopeSchema,
|
|
374
|
+
questionLabel: z.string().max(1000).optional(),
|
|
375
|
+
}),
|
|
364
376
|
])).max(100).optional(),
|
|
365
377
|
education: z.array(z.object({
|
|
366
378
|
school: z.string().min(1).max(500),
|
|
@@ -535,13 +547,36 @@ function registerApplyTools(
|
|
|
535
547
|
'Fetch a compact, bounded projection for one Apply execution. Request only the current members and profile keys needed for the visible form. The response owns mutability, allowed operations, milestones, lease timing, and progress.',
|
|
536
548
|
{
|
|
537
549
|
executionId: z.number().int().min(1),
|
|
538
|
-
memberIds: z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET)
|
|
539
|
-
|
|
550
|
+
memberIds: z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET)
|
|
551
|
+
.refine((values) => new Set(values).size === values.length, {
|
|
552
|
+
message: 'memberIds must be unique',
|
|
553
|
+
}),
|
|
554
|
+
profileKeys: z.array(z.string().min(1).max(200)).max(100)
|
|
555
|
+
.refine((values) => new Set(values).size === values.length, {
|
|
556
|
+
message: 'profileKeys must be unique',
|
|
557
|
+
}).optional(),
|
|
558
|
+
officeProjections: z.array(z.object({
|
|
559
|
+
memberId: z.number().int().min(1),
|
|
560
|
+
office: officeScopeSchema,
|
|
561
|
+
profileKeys: z.array(z.string().min(1).max(200)).min(1).max(100)
|
|
562
|
+
.refine((values) => new Set(values).size === values.length, {
|
|
563
|
+
message: 'office profileKeys must be unique',
|
|
564
|
+
}),
|
|
565
|
+
}).strict()).max(APPLY_EXECUTION_MAX_TARGET)
|
|
566
|
+
.refine((values) => new Set(values.map(({ memberId }) => memberId)).size === values.length, {
|
|
567
|
+
message: 'officeProjections must contain unique memberId values',
|
|
568
|
+
}).optional(),
|
|
540
569
|
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
541
570
|
},
|
|
542
|
-
wrapTool(async ({ executionId, ...body }) =>
|
|
543
|
-
|
|
544
|
-
|
|
571
|
+
wrapTool(async ({ executionId, ...body }) => {
|
|
572
|
+
const memberIds = new Set(body.memberIds);
|
|
573
|
+
if (body.officeProjections?.some(({ memberId }) => !memberIds.has(memberId))) {
|
|
574
|
+
throw new Error('Every office projection memberId must exist in memberIds.');
|
|
575
|
+
}
|
|
576
|
+
return applyControlRequest(
|
|
577
|
+
'POST', `/api/jobscout/apply/executions/${executionId}/snapshot`, body,
|
|
578
|
+
);
|
|
579
|
+
}, 'Failed to fetch compact apply execution snapshot')
|
|
545
580
|
);
|
|
546
581
|
|
|
547
582
|
server.tool(
|
|
@@ -1162,6 +1197,9 @@ function registerApplyTools(
|
|
|
1162
1197
|
messages: [{
|
|
1163
1198
|
role: 'user',
|
|
1164
1199
|
content: { type: 'text', text: APPLY_RELIABILITY_PROMPT },
|
|
1200
|
+
}, {
|
|
1201
|
+
role: 'user',
|
|
1202
|
+
content: { type: 'text', text: 'Before generating questions or filling controls, run the skill 4.6 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.' },
|
|
1165
1203
|
}, {
|
|
1166
1204
|
role: 'user',
|
|
1167
1205
|
content: { type: 'text', text: 'Only active=true identifies resumable execution work. Active=false and preserved=true is terminal read-only reconciliation evidence.' },
|
|
@@ -1169,7 +1207,7 @@ function registerApplyTools(
|
|
|
1169
1207
|
role: 'user',
|
|
1170
1208
|
content: {
|
|
1171
1209
|
type: 'text',
|
|
1172
|
-
text: 'Protocol 3.6.0 reliability gate for new work: require MCP contract 3.7.
|
|
1210
|
+
text: 'Protocol 3.6.0 reliability gate for new work: require MCP contract 3.7.3 and skill 4.6.0. After complete local context loss, list bounded recovery candidates, obtain explicit confirmation of the exact set, and recover only that set. Treat tab recovery, form-state recovery, and mutation authority as independent. List active handoff receipts for the execution before resolving grouped submission statements; use the named receipt or the sole returned active receipt, classify every member, and claim that receipt before recording outcomes. Validate tab keep sets and resume upload stages locally. Never send raw browser values or click Submit.',
|
|
1173
1211
|
},
|
|
1174
1212
|
}, {
|
|
1175
1213
|
role: 'user',
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "trackly-cli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.15.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "1.30.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"mcpName": "io.github.trackly-app/trackly",
|
|
5
5
|
"description": "AI job search CLI + hosted MCP server with OAuth. 170K+ jobs, 3,800+ companies, 40+ ATS. Works with Claude, ChatGPT, Cursor, Windsurf, Codex via hosted streamable-http or local stdio.",
|
|
6
6
|
"bin": {
|
package/server.json
CHANGED
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
]
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
|
-
"version": "0.
|
|
20
|
+
"version": "0.15.0",
|
|
21
21
|
"packages": [
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "trackly-cli",
|
|
25
|
-
"version": "0.
|
|
25
|
+
"version": "0.15.0",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"runtimeArguments": [
|
|
28
28
|
{
|
|
@@ -22,12 +22,13 @@ Use Trackly as the source of truth for profile answers, documents, queue decisio
|
|
|
22
22
|
|
|
23
23
|
Before selecting or resuming work, read
|
|
24
24
|
[references/batch-orchestration.md](references/batch-orchestration.md) and
|
|
25
|
-
[references/browser-lifecycle.md](references/browser-lifecycle.md).
|
|
25
|
+
[references/browser-lifecycle.md](references/browser-lifecycle.md). Before generating an employer-form question packet, also read and run
|
|
26
|
+
[references/answer-resolution.md](references/answer-resolution.md). These
|
|
26
27
|
references define the server-frozen batch, grouped-question, tab-reconciliation,
|
|
27
28
|
and missing-surface recovery contract; they are mandatory even for a one-job batch.
|
|
28
29
|
Read [references/legal-clarifications.md](references/legal-clarifications.md) when a form asks for a legal, regulatory, consent, or employer-relationship decision.
|
|
29
30
|
|
|
30
|
-
1. Call `trackly_get_apply_protocol`. Skill 4.
|
|
31
|
+
1. Call `trackly_get_apply_protocol`. Skill 4.6.0 requires protocol 3.6.0 or newer for new reliability work. Protocol 3.2 remains valid only for an already-active explicit legacy single run; an already-active explicit 3.2 single run may finish through its legacy path. Preserve the protocol's documented recovery paths for already-active older work. Require `compatibleSkillMajor: 4` and `compatibleSkillMinimumVersion` no newer than this installed skill. Reject an older or incompatible version for new work and report that the backend must finish updating or `trackly agent setup` must update the skill. Protocol 3.2 added exact-origin trust for jobs Trackly ingested directly from employer careers sources; do not recreate the retired ownership-timestamp gate in the client.
|
|
31
32
|
2. Call `trackly_get_profile_onboarding` or fetch both the profile schema and application profile. When present, render `schema.screens` in ascending `order` as one grouped question packet per screen. Within a screen, preserve category `order` and then field `order`; use each field's user-facing `rationale` when the user asks why it is needed. Ask only unknown or confirmation-needed fields and honor `consistencyRules` before submitting profile changes, resolving contradictions with the user instead of guessing. When `schema.screens` is absent on a legacy backend, fall back to the existing category-based onboarding behavior. Treat `completeness.percent` as required onboarding readiness only. Use `coverage.missingReusableKeys` to explain reusable optional gaps, while `coverage.contextualKeys` are intentionally asked only on the relevant employer form. Do not claim that 100% required completeness answers every possible application question.
|
|
32
33
|
3. Save answers with `trackly_update_application_profile`:
|
|
33
34
|
- Use `answered`, `intentionally_blank`, `declined`, or `unknown` exactly.
|
|
@@ -48,6 +49,7 @@ Read [references/legal-clarifications.md](references/legal-clarifications.md) wh
|
|
|
48
49
|
- After full local context loss with no active execution, call `trackly_list_recoverable_apply_executions`. For every distinct returned `jobId`, call `trackly_get_job`, bind that Trackly-owned job record to the returned `candidateId`, and show the user its company, role, requisition identity when available, and source-execution identity; do not identify candidates from chat, tab order, search results, or page copy. A missing or mismatched Trackly job record blocks confirmation. Require explicit confirmation of one exact candidate set, then call `trackly_recover_exact_apply_members` with that source execution, snapshot hash, and only those candidate IDs. Verify `assertedCandidateIds`, `eligibleCandidateIds`, and the eligibility candidate IDs each contain exactly the confirmed set with no duplicates. Never add substitutes, newly saved jobs, or merely similar roles.
|
|
49
50
|
- Exact recovery restores server lineage only. Treat tab restored, form state restored, and mutation authority restored as three independent receipts. Bind every recovered surface with a fresh recovery binding and inspection epoch, revalidate the full form, and preserve every user-edited or unknown non-empty value before any write. Earlier-epoch browser, field, upload, integrity, and submission evidence cannot authorize the recovered form.
|
|
50
51
|
- Every compact snapshot request must contain a non-empty list of only the current member IDs. Never request an empty or inferred all-members projection.
|
|
52
|
+
- When a visible form asks an office-scoped profile question and the exact employer office is known, request that field only through the matching member's `officeProjections` entry using the canonical `companyId:office-identity` scope. Consume only the returned `memberOfficeProfiles` entry for that same member and exact office; never move an office answer into the global profile map or reuse it for another member, employer, or office. A recovered member may reuse the answer only when Trackly proves its frozen company identity. If a legacy snapshot cannot prove that identity, preserve the form and treat the answer as unknown instead of refilling from chat or current mutable job metadata.
|
|
51
53
|
- Recover every entry in `execution.unresolvedWaves` in ascending `waveOrder`; `currentWave` identifies only the latest scheduling wave, not the complete recovery set.
|
|
52
54
|
- Immediately consume the start response's authoritative `progress` and `nextAction`; do not issue a speculative advance first.
|
|
53
55
|
- When that authoritative `nextAction` requests another wave, call `trackly_advance_apply_execution` with the actual current `browserSurface`, latest execution revision, and a fresh idempotency key.
|
|
@@ -71,7 +73,7 @@ Read [references/legal-clarifications.md](references/legal-clarifications.md) wh
|
|
|
71
73
|
- For each fixed batch member, preserve an explicit job ID -> application run ID -> browser tab mapping. Complete the full start -> conditional resume preparation/confirmation/verification when an upload control exists -> form completion -> `review_ready` lifecycle for every member. A review-ready run does not block the next member. Never submit any of them.
|
|
72
74
|
6. When the selected job or current fixed batch member has no `runId`, call `trackly_start_apply_run` with the complete `batchId`, `memberId`, `expectedMemberVersion`, `expectedInspectionEpoch`, and `leaseToken` returned by Trackly. This is the sanctioned idempotent lookup for a recovered member whose `runId` is absent; call it with the exact recovered binding and Trackly will reuse the bound active run. When the member already has a `runId`, do not call `trackly_start_apply_run` in this or any subsequent start step: reuse that exact run and bind or recover its browser surface through `trackly_bind_apply_surface` as required. Never create a replacement run because browser control was interrupted.
|
|
73
75
|
- If a bound start returns a transport failure, a non-access HTTP 5xx response, or an error explicitly marked `retryable: true`, preserve the frozen member and browser state. Refetch the active batch, renew its lease, and retry the same complete binding once. Classify the retry response independently with these same rules: route canonical `maintenance_mode` or legacy `planned_maintenance` directly through **Resume after maintenance**; surface controlled-access/request errors marked `retryable: false` and every other HTTP 4xx response unchanged; only a second transport failure, non-access HTTP 5xx response, or explicitly retryable error becomes `backend_run_start_unavailable`. Route maintenance on either attempt without consuming or relabeling the retry, and never relabel a permanent retry response as an outage. For `backend_run_start_unavailable`, continue other members and report a Trackly control-plane failure. Do not call `trackly_checkpoint_apply_batch` for this condition because start failure has not produced the required run ID. The unchanged frozen member is the durable resume point. Never switch that frozen member to an unbound legacy run or blame the employer form.
|
|
74
|
-
- Require `run.protocolVersion` to be 3.1.0 or newer and to share protocol major 3 with the fetched protocol. A new reliability execution member requires 3.6.0 or newer; already-active older work may use only the recovery path published by its fetched protocol. Never continue a pre-evidence 3.0.x run under skill 4.
|
|
76
|
+
- Require `run.protocolVersion` to be 3.1.0 or newer and to share protocol major 3 with the fetched protocol. A new reliability execution member requires 3.6.0 or newer; already-active older work may use only the recovery path published by its fetched protocol. Never continue a pre-evidence 3.0.x run under skill 4.6.0. Preserve that run instead of starting a replacement, record it `blocked` with a value-free protocol-upgrade reason when possible, and tell the user the saved job can be retried only after the stale run is cleared through Trackly's supported lifecycle. Stop and refetch the protocol and active execution or batch if support level, execution mode, provider, required scenarios, authorized origin policy, member version, or inspection epoch changes after run creation.
|
|
75
77
|
- In guided mode, inspect the page before preparing any resume bytes. Confirm the employer, role, HTTPS origin, reachable review path, semantic controls, whether an attachment control exists, and absence of a credential, verification, CAPTCHA, or submit-only wall. A missing file input is not itself a blocker; skip the resume path when the application has no attachment control. Any other failed precondition is an execution blocker, not permission to improvise.
|
|
76
78
|
7. Pass the browser readiness gate before preparing resume bytes:
|
|
77
79
|
- Use semantic browser control through Codex in-app browser controls, Chrome MCP/extension browser control, or Claude in Chrome.
|
|
@@ -112,7 +114,7 @@ Follow this order:
|
|
|
112
114
|
1. Open the application in the controlled browser context and confirm the employer, role, ATS host, and HTTPS URL. Before entering private data, require the visible company and role to match the run binding and, when the stored URL exposes one, require the requisition identifier to match. When `job_identity_match` is required, immediately report its value-free committed `scenario_coverage` proof after this check passes; never include company, role, URL, requisition, or page text in the observation. On exact-origin fallback, revalidate the frozen company, role, and available requisition identity after every navigation or redirect and before entering any additional private data. Parse and normalize the URL, then require the exact origin to equal an `originPolicy.authorizedOrigins` entry or the normalized hostname to satisfy `host === allowedDomain` or `host.endsWith("." + allowedDomain)` for an allowed ATS suffix or verified company domain. Never use substring, display-text, logo, or suffix-without-a-dot matching. When `verification` is `trackly_employer_source_exact_origin`, accept only the exact listed origin; never convert it into a hostname suffix or carry it across a redirect or iframe origin change. For every other vendor-hosted ATS policy, require both `originPolicy.tenantRule` and `originPolicy.verifiedAtsTenant` to be non-null; stop before private data entry if either is missing. Execute the backend-owned declarative tenant rule exactly, including its extraction, exact-host-depth, locale, percent-decoding, normalization, and fail-closed semantics, then require the normalized result to equal `originPolicy.verifiedAtsTenant`; never invent or reinterpret a strategy token. Revalidate both origin and tenant after every redirect and apply the same policy to every iframe that receives private data. Stop on any unmatched origin or tenant, malformed percent encoding, missing required rule, or rule shape the client cannot execute exactly.
|
|
113
115
|
2. Inspect the whole form and identify required fields, semantic controls, consent controls, document inputs, and multi-step sections.
|
|
114
116
|
3. When an attachment control exists, only after the exact-hash visual confirmation and a successful immediate pre-attach `trackly_verify_prepared_resume` check, upload the prepared resume before autofill when parsing may overwrite contact fields. Do not change the file between verification and attachment. Verify that the filename chip exactly matches the prepared resume’s user-facing filename and contains no internal cache identifier. Stop and replace the attachment if it does not. When no attachment control exists, skip resume preparation and upload without treating that absence as an error.
|
|
115
|
-
4.
|
|
117
|
+
4. Run the deterministic resolver from [references/answer-resolution.md](references/answer-resolution.md) against the visible controls, then fill typed fields from the resolved Trackly profile. Every answer must be classified as `exact_profile`, `safe_derivation`, `supported_draft`, `missing_fact`, `live_consent`, or `forbidden_inference` before use. Clear parser-filled data when the canonical state is intentionally blank. Before writing any non-empty control, apply the local field-ownership gate from [references/form-integrity.md](references/form-integrity.md). Preserve `user_edited` and `unknown_external_change` values byte-for-byte unless the user explicitly requests a rewrite; never let resume parsing, rerendering, recovery, or the final sweep overwrite them.
|
|
116
118
|
5. Use real UI clicks for React/native selects, radios, and checkboxes. Resolve boolean values by their exact semantic label (`true` to Yes, `false` to No), never by option order, index, proximity, or a stale prior selection. After every selection, compare the committed value with the canonical Trackly value. If the field is required or had a validation error before selection, verify that the required-field error disappeared. An optional control with no validation error passes when its committed value is correct. Treat any value mismatch or applicable stale error as a failed field and correct it before continuing.
|
|
117
119
|
6. Recheck email and phone through both browser DOM state and macOS accessibility state. Require exact values and reject duplicate/concatenated values.
|
|
118
120
|
7. Complete every visible field whose canonical answer is known, including optional fields, previous-employer/title history, education rows and dates, links, relocation, communication preferences, and opportunity source. Before review, compare the live field inventory with the resolved Trackly profile and prove that no known visible answer was silently omitted. Group only genuinely unknown contextual fields into the question packet.
|
|
@@ -8,14 +8,20 @@ questions and piecemeal profile writes.
|
|
|
8
8
|
|
|
9
9
|
1. Collect all newly supplied answers from the current question packet.
|
|
10
10
|
2. Fetch the current profile schema and the smallest relevant profile
|
|
11
|
-
projection once. Include `provider`, `company`, and `
|
|
12
|
-
context is known and required.
|
|
11
|
+
projection once. Include `provider`, `company`, `jurisdiction`, and `office`
|
|
12
|
+
when that context is known and required. An office scope value is the exact
|
|
13
|
+
backend company ID plus a stable office identity, for example
|
|
14
|
+
`42:waltham-ma`; never derive it from a display name alone. Corporate-family reuse is unavailable until
|
|
13
15
|
Trackly has an authoritative company-family registry; never derive a family
|
|
14
16
|
identity from names, logos, page text, parent-company knowledge, or search.
|
|
15
17
|
3. Map each answer to an exposed canonical key and permitted scope. Never call
|
|
16
18
|
a field missing merely because it was absent from the compact execution
|
|
17
19
|
snapshot; contextual fields require a targeted profile fetch.
|
|
18
|
-
4.
|
|
20
|
+
4. Validate the expected input type before mapping or writing. “Whole number”
|
|
21
|
+
requires a number, and “Yes or No” requires a boolean. A mismatch remains
|
|
22
|
+
unresolved and returns to the grouped packet; never coerce a boolean into
|
|
23
|
+
years, a proficiency label into a language level, or vice versa.
|
|
24
|
+
5. Classify every answer as exactly one of:
|
|
19
25
|
- `saved`: a canonical field exists and this run changed it;
|
|
20
26
|
- `already_matched`: the canonical field already contains the same state and
|
|
21
27
|
value at the resolved scope;
|
|
@@ -23,9 +29,13 @@ questions and piecemeal profile writes.
|
|
|
23
29
|
live schema audit;
|
|
24
30
|
- `run_only_contextual`: the answer is intentionally per-run, such as the
|
|
25
31
|
final truthfulness certification, and must not enter the reusable profile.
|
|
26
|
-
|
|
32
|
+
6. Ask scope only when the user's wording and the schema do not determine it.
|
|
27
33
|
“Always” means global only for fields whose schema permits global scope.
|
|
28
34
|
|
|
35
|
+
Compound a reusable gap only when a visible form requires it. Defer optional
|
|
36
|
+
profile gaps that no current visible application needs; do not turn the full
|
|
37
|
+
schema into an onboarding questionnaire.
|
|
38
|
+
|
|
29
39
|
## One write and one verification
|
|
30
40
|
|
|
31
41
|
- Send all `saved` changes in one `trackly_update_application_profile` call
|
|
@@ -65,6 +75,10 @@ claim that Trackly learned it.
|
|
|
65
75
|
|
|
66
76
|
- Country-specific work authorization:
|
|
67
77
|
`authorization.legally_authorized_by_country` at `jurisdiction` scope.
|
|
78
|
+
- Employer-office commute willingness and cadence:
|
|
79
|
+
`location.commute_willing`, `location.commute_days_per_week`, and
|
|
80
|
+
`location.commute_commitment` at exact `office` scope. Never reuse an answer
|
|
81
|
+
for another office of the same company.
|
|
68
82
|
- Prior interview with the employer:
|
|
69
83
|
`employment.previously_interviewed_at_employer` at `company` scope.
|
|
70
84
|
- Employment, contracting, consulting, temporary work, or similar engagement
|
|
@@ -83,6 +97,18 @@ claim that Trackly learned it.
|
|
|
83
97
|
the backend can prove a match. A write receipt or refetch verifies only the
|
|
84
98
|
company scope and `questionFingerprint`; a redacted, unknown, or absent
|
|
85
99
|
reusable value is expected and must not block truth certification or review.
|
|
100
|
+
- Named applicant privacy-notice routing:
|
|
101
|
+
`consent.named_applicant_privacy_notice_acknowledgement_policy` is a global
|
|
102
|
+
routing policy, not consent to unseen text. `acknowledge_named_notice_only`
|
|
103
|
+
permits only a visible, unbundled named applicant notice; `ask` remains a
|
|
104
|
+
live question. Marketing, retention, arbitration, background-check,
|
|
105
|
+
recording, AI, and data-sharing choices stay separate.
|
|
106
|
+
- Interview recording or transcription consent and workplace-policy
|
|
107
|
+
acknowledgement: use `consent.interview_recording` and
|
|
108
|
+
`consent.workplace_policy_acknowledgement` only at exact company scope with
|
|
109
|
+
the current policy question or version as `questionLabel`. Both are
|
|
110
|
+
`run_only_contextual`: retain the fingerprint for audit, ask again, and never
|
|
111
|
+
require a reusable returned value during the verification refetch.
|
|
86
112
|
- Consumer hardware, IoT, or retail experience level and supporting summary:
|
|
87
113
|
the two global `employment.consumer_hardware_iot_retail_experience_*` fields.
|
|
88
114
|
- Residential city and EEO sexual orientation remain their existing canonical
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Deterministic answer resolution
|
|
2
|
+
|
|
3
|
+
Run this resolver after reading the current application-profile revision and
|
|
4
|
+
inventorying a visible employer form, but before generating that form's
|
|
5
|
+
question packet or writing an answer. Profile onboarding that happens before
|
|
6
|
+
an employer form exists uses the profile schema's field metadata instead.
|
|
7
|
+
This resolver reduces questions without weakening truthfulness.
|
|
8
|
+
|
|
9
|
+
## Resolution pipeline
|
|
10
|
+
|
|
11
|
+
For each visible question:
|
|
12
|
+
|
|
13
|
+
1. Normalize its label, help text, expected input type, choices, employer scope,
|
|
14
|
+
jurisdiction, and whether it is required.
|
|
15
|
+
2. Map it to one exact current schema key. Do not map by a single keyword.
|
|
16
|
+
3. Resolve one authority class:
|
|
17
|
+
- `exact_profile`: an answered canonical key with matching scope and type.
|
|
18
|
+
- `safe_derivation`: a deterministic derivation allowed below.
|
|
19
|
+
- `supported_draft`: free text whose every factual claim has evidence.
|
|
20
|
+
- `missing_fact`: no authoritative value exists; ask at point of use.
|
|
21
|
+
- `live_consent`: a notice, acknowledgement, certification, or choice that
|
|
22
|
+
must be made against the current form.
|
|
23
|
+
- `forbidden_inference`: a legal, identity, relationship, or specialized
|
|
24
|
+
claim that cannot be inferred.
|
|
25
|
+
4. Validate the expected input type before translating or committing a value.
|
|
26
|
+
5. Fill only the first three classes. Put only visible `missing_fact`,
|
|
27
|
+
`live_consent`, and user-resolvable `forbidden_inference` items in one
|
|
28
|
+
grouped question packet with expected type and scope.
|
|
29
|
+
|
|
30
|
+
Keep this value-free local provenance for each resolved control: profile
|
|
31
|
+
revision, schema key, scope, authority class, derivation rule when any,
|
|
32
|
+
expected type, and committed-control receipt. Never send the answer value in
|
|
33
|
+
an observation.
|
|
34
|
+
|
|
35
|
+
## Source precedence
|
|
36
|
+
|
|
37
|
+
The current profile revision is the only reusable answer authority. A
|
|
38
|
+
transcript or conversation, screenshot, parser output, browser autocomplete,
|
|
39
|
+
or cached value is never authority. Resume parsing is a suggestion only: it may
|
|
40
|
+
help identify a schema key, but the parser never becomes authority and never
|
|
41
|
+
overwrites an answered profile value or a user-edited control.
|
|
42
|
+
|
|
43
|
+
Contact details and addresses must resolve from exact current profile keys.
|
|
44
|
+
Verify their live committed state even when the browser initially displays a
|
|
45
|
+
masked or formatted value.
|
|
46
|
+
|
|
47
|
+
For an ordinary application name, use the canonical first and last name and
|
|
48
|
+
reconcile parser-created casing. A government-ID full legal name is different:
|
|
49
|
+
use it only when the form explicitly asks for a government-ID or legal-document
|
|
50
|
+
name and the dedicated restricted profile field is explicitly answered. Never
|
|
51
|
+
derive it from ordinary name fields.
|
|
52
|
+
|
|
53
|
+
Employment status outranks legacy employer columns. When status is
|
|
54
|
+
`not_employed`, current company and current title are non-authoritative even if
|
|
55
|
+
legacy values remain populated. For a current-employer control, commit boolean
|
|
56
|
+
No, an exact “Not currently employed” option, or an intentionally blank optional
|
|
57
|
+
text control according to the visible control semantics. Ask only when a
|
|
58
|
+
required control offers no truthful non-employed representation. Answer
|
|
59
|
+
prior-employer questions from
|
|
60
|
+
`employment.most_recent_company` and `employment.most_recent_title`, and leave current-employer
|
|
61
|
+
questions unresolved unless the schema supplies an applicable authoritative
|
|
62
|
+
answer. `student`, `other`, or unknown status fails closed when applicability
|
|
63
|
+
is ambiguous.
|
|
64
|
+
|
|
65
|
+
## Safe experience derivation
|
|
66
|
+
|
|
67
|
+
Read the wording semantically:
|
|
68
|
+
|
|
69
|
+
- A minimum qualification/profile range asks whether experience meets the
|
|
70
|
+
lower threshold. A profile value of 7 years answers “2–4 years?” as Yes when
|
|
71
|
+
the choices mean “at least 2 years” or a qualification bracket.
|
|
72
|
+
- An exact bounded band asks whether the value lies inside both bounds. With 7
|
|
73
|
+
years, “exactly between 2 and 4 years?” is No.
|
|
74
|
+
- An explicit disqualifying maximum is a ceiling. With 7 years, “no more than 4 years?” is No.
|
|
75
|
+
|
|
76
|
+
Never infer specialized experience from total product-management tenure.
|
|
77
|
+
Financial-infrastructure years, growth-focused software PM years, enterprise
|
|
78
|
+
SaaS PM years, health-tech experience, and named platform/tool experience need
|
|
79
|
+
their own schema keys or an explicit user answer.
|
|
80
|
+
|
|
81
|
+
Type must match the control. Returning Yes for “financial-infrastructure
|
|
82
|
+
years” is a type mismatch; ask for a whole number. Returning C2 for SQL
|
|
83
|
+
proficiency is a type mismatch when the field expects a proficiency enum.
|
|
84
|
+
English C2 is valid when the English field expects a CEFR level. Never coerce a
|
|
85
|
+
type mismatch into a plausible-looking answer.
|
|
86
|
+
|
|
87
|
+
## Employer relationships and legal boundaries
|
|
88
|
+
|
|
89
|
+
A direct prior-employment No may be derived only under the complete-history
|
|
90
|
+
rule in the main skill. It never proves no contractor, affiliate, subsidiary,
|
|
91
|
+
acquisition, family, friendship, referral, or other relationship. Those need
|
|
92
|
+
an exact matching scoped field or a user answer.
|
|
93
|
+
|
|
94
|
+
Region authorization is separate from country authorization. A region may be
|
|
95
|
+
answered Yes only when it is explicitly present in
|
|
96
|
+
`authorization.legally_authorized_regions`; absence is unknown, never No, and a
|
|
97
|
+
region answer must not be stored under a country jurisdiction.
|
|
98
|
+
|
|
99
|
+
Commute willingness and days-per-week commitments are employer-office facts,
|
|
100
|
+
not company-wide facts. Resolve them only from an exact `office` scope whose
|
|
101
|
+
identity combines the backend company ID and the specific office; another
|
|
102
|
+
office at the same employer remains unknown.
|
|
103
|
+
|
|
104
|
+
A named applicant privacy notice may be acknowledged only as the current
|
|
105
|
+
notice policy allows and with its current fingerprint. Marketing, data
|
|
106
|
+
retention, arbitration, background check, and interview recording are separate
|
|
107
|
+
choices. Onsite willingness does not prove that a workplace policy was read or
|
|
108
|
+
accepted. Truthfulness certification is always per-run and never reusable.
|
|
109
|
+
|
|
110
|
+
## Supported writing
|
|
111
|
+
|
|
112
|
+
For a useful optional motivation or experience prompt, `supported_draft` means
|
|
113
|
+
fill it when every factual claim is supported. Build from canonical facts and
|
|
114
|
+
evidence claims, state explicit gaps instead of inventing bridge facts, match
|
|
115
|
+
the saved voice rules, lint, commit, and reread. The user's final truth review
|
|
116
|
+
still governs the complete review-ready subset.
|
|
117
|
+
|
|
118
|
+
Ask the user only for a genuinely new fact, subjective choice, live consent,
|
|
119
|
+
or unsupported claim that the visible application requires. A visible employer
|
|
120
|
+
accuracy checkbox is a live consent resolved before the review-ready
|
|
121
|
+
checkpoint; the separate Trackly truth certification is requested only
|
|
122
|
+
afterward for the exact durable review-ready subset. Defer reusable
|
|
123
|
+
gaps that no current visible form needs.
|
|
@@ -40,4 +40,11 @@ Before entering the response:
|
|
|
40
40
|
|
|
41
41
|
When `writing.optional_question_policy` permits it, answer a strategically useful optional motivation, experience, product, or role-overlap question when every fact is supported. Optional does not mean skip. Leave demographic, consent, legal, compensation, and employer-relationship questions unanswered when their canonical value is unknown. Group those unknowns into the consolidated question packet.
|
|
42
42
|
|
|
43
|
+
A supported strategically useful optional prompt must not be left blank. Draft
|
|
44
|
+
it from canonical evidence claims, expose any explicit gaps instead of
|
|
45
|
+
inventing bridge facts, match the user's voice, pass deterministic lint, enter
|
|
46
|
+
the committed text, and reread it from the live control. Ask only when a new
|
|
47
|
+
fact or subjective choice is actually required; the final truth review remains
|
|
48
|
+
the user's approval boundary.
|
|
49
|
+
|
|
43
50
|
Before entering any answer, supply value-free claim fingerprints and evidence-reference codes to the local linter. It returns only a draft hash, length, policy, and violation codes. It never echoes or sends the draft to Trackly.
|
|
@@ -116,6 +116,16 @@ With protocol 3.5 or newer and the compact-snapshot capability enabled, prefer `
|
|
|
116
116
|
|
|
117
117
|
Every compact snapshot request must contain a non-empty list of current member IDs. Never use an empty member projection as shorthand for all members.
|
|
118
118
|
|
|
119
|
+
For an office-scoped question visible on one member, add an `officeProjections`
|
|
120
|
+
entry containing only that member ID, its canonical `companyId:office-identity`,
|
|
121
|
+
and the exact office-only keys needed by the form. Read the answer only from the
|
|
122
|
+
matching `memberOfficeProfiles` entry. Never copy it into the shared profile
|
|
123
|
+
projection or reuse it for a sibling member, another office, or another
|
|
124
|
+
employer. Recovery does not weaken this binding: Trackly must prove the
|
|
125
|
+
candidate's frozen company identity through its source lineage. A legacy
|
|
126
|
+
candidate without that immutable identity stays unknown and must be confirmed
|
|
127
|
+
from the visible form/user rather than inferred from current job metadata.
|
|
128
|
+
|
|
119
129
|
The execution freezes one original recent-first queue snapshot and ordering
|
|
120
130
|
version. Newly saved jobs wait for the next execution. Every continuation is a
|
|
121
131
|
new immutable child batch linked in wave order; never append, replace, reorder,
|
|
@@ -300,6 +310,22 @@ evidence cannot satisfy the current gate.
|
|
|
300
310
|
|
|
301
311
|
## First pass
|
|
302
312
|
|
|
313
|
+
Accessible-first is a hard scheduler invariant. While any accessible candidate remains, a known authentication-gated, account-creation, OTP, or pre-form
|
|
314
|
+
CAPTCHA candidate must not be opened. When access is unknown, perform only a
|
|
315
|
+
minimal non-mutating probe. If a gate appears, never start a draft or enter
|
|
316
|
+
private data: record the typed disposition, park it, and continue accessible
|
|
317
|
+
work. The backend disposition must release its reservation or capacity and
|
|
318
|
+
increment the authoritative `authParked` funnel count before replacement
|
|
319
|
+
scheduling. If that durable transition conflicts, preserve the surface and
|
|
320
|
+
surface the conflict; never invent released capacity locally.
|
|
321
|
+
|
|
322
|
+
An authoritative already-parked member is not first-pass browser work and
|
|
323
|
+
needs no duplicate disposition. A server access hint is not authoritative: if
|
|
324
|
+
accessible candidates remain, defer that hinted member; after accessible work
|
|
325
|
+
is exhausted, perform the minimal live probe required by the public
|
|
326
|
+
disposition contract. “Process every frozen member” below means every currently
|
|
327
|
+
actionable, non-parked member subject to this deferral rule.
|
|
328
|
+
|
|
303
329
|
Process every frozen member even when another needs user input:
|
|
304
330
|
|
|
305
331
|
1. Create or reuse its bound application run.
|
|
@@ -60,8 +60,11 @@ agent-written fingerprint.
|
|
|
60
60
|
|
|
61
61
|
## Contact-field integrity
|
|
62
62
|
|
|
63
|
-
-
|
|
64
|
-
-
|
|
63
|
+
- Resolve email, phone, name, and address from exact canonical keys in the current profile revision. Transcript or conversation text, screenshots, parser output, autocomplete, and cached values are never reusable authority.
|
|
64
|
+
- Compare email and phone to the exact Trackly values after resume parsing and autofill. Reconcile parser-altered ordinary name casing to canonical first/last-name casing; never use that rule for a government-ID name.
|
|
65
|
+
- Read both DOM input state and macOS accessibility state. A static HTML `value` attribute is not truth or proof of the live committed control state; require rendered live state, framework/native state, or accessibility state after events settle.
|
|
66
|
+
- For a masked phone control, compare its normalized live digits and country code to the canonical phone rather than requiring identical formatting. Prove an implicit country code from live control metadata, a committed country selector, or framework/native state; ten visible digits alone do not prove it.
|
|
67
|
+
- For address autocomplete, choose the exact offered option and prove that option committed through the control's semantic state; displayed typed text alone is insufficient.
|
|
65
68
|
- Reject duplicate or concatenated values, autofill overlays, placeholder-only text, and values present only in a custom wrapper.
|
|
66
69
|
- Recheck when navigation, resume parsing, or a correction banner rerenders the form.
|
|
67
70
|
- Before review, inventory every email, phone, country-code, and required contact control. Pass `critical_contact_integrity` only when all present canonical values are exact and no required contact field was omitted. A form with no contact control passes only after the whole-form inventory confirms none exists or is required.
|