trackly-cli 0.13.4 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/contracts/trackly-apply-contract-history.json +8 -1
- package/contracts/trackly-apply-tools.json +15 -4
- package/docs/trackly-tools.md +11 -0
- package/lib/agent.js +2 -2
- package/lib/apply-tab-set.js +124 -0
- package/lib/apply-upload.js +66 -0
- package/mcp/apply-tools.js +301 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/trackly-apply/SKILL.md +12 -4
- package/skills/trackly-apply/references/batch-orchestration.md +34 -0
- package/skills/trackly-apply/references/browser-lifecycle.md +14 -0
- package/skills/trackly-apply/references/browser-upload.md +40 -8
- package/skills/trackly-apply/references/review-handoff.md +87 -0
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ trackly jobs --function product
|
|
|
35
35
|
|
|
36
36
|
## At a Glance
|
|
37
37
|
|
|
38
|
-
3,800+ companies | 170K+ jobs | 40+ ATS types | CLI + MCP |
|
|
38
|
+
3,800+ companies | 170K+ jobs | 40+ ATS types | CLI + MCP | 54 local MCP tools
|
|
39
39
|
|
|
40
40
|
## CLI Commands
|
|
41
41
|
|
|
@@ -181,6 +181,10 @@ Then use natural language in any of these clients:
|
|
|
181
181
|
| trackly_start_apply_execution | Start a target-counted accessible Apply execution |
|
|
182
182
|
| trackly_get_active_apply_execution | Recover the active execution before legacy batch recovery |
|
|
183
183
|
| trackly_get_apply_execution | Read the authoritative progress funnel and immutable child waves |
|
|
184
|
+
| trackly_list_recoverable_apply_executions | List bounded exact-member recovery candidates after context loss |
|
|
185
|
+
| trackly_recover_exact_apply_members | Recover only one explicitly confirmed candidate set without substitutes |
|
|
186
|
+
| trackly_list_apply_review_handoffs | Rediscover active, value-free review-handoff receipts for one execution |
|
|
187
|
+
| trackly_claim_apply_review_handoff | Claim and classify one exact review-handoff group before reconciliation |
|
|
184
188
|
| trackly_get_apply_execution_snapshot | Fetch a compact bounded projection for current execution members and required profile keys |
|
|
185
189
|
| trackly_resume_parked_apply_member | Explicitly resume one parked member for a fresh non-mutating access probe |
|
|
186
190
|
| trackly_approve_apply_execution_resume | Approve one exact resume identity for an unchanged execution snapshot |
|
|
@@ -204,6 +208,8 @@ Then use natural language in any of these clients:
|
|
|
204
208
|
| trackly_report_apply_observation | Send redacted ATS mechanics feedback |
|
|
205
209
|
| trackly_lint_application_text | Locally lint application writing and return only value-free violations plus a draft hash |
|
|
206
210
|
| trackly_diagnose_local_path | Locally diagnose the exact implicated filesystem path without deleting files |
|
|
211
|
+
| trackly_validate_apply_tab_keep_set | Locally validate an exact expected/keep tab set against complete caller-supplied inventories without controlling the browser or sending tab IDs remotely |
|
|
212
|
+
| trackly_validate_apply_resume_upload | Locally validate adapter capability and ordered value-free attachment proof stages |
|
|
207
213
|
| trackly_report_apply_observations | Bulk-send up to 20 leased, batch-bound redacted observations |
|
|
208
214
|
| trackly_record_application_outcome | Record review or confirmed submission outcome |
|
|
209
215
|
| trackly_record_application_outcomes | Bulk-record up to 20 leased batch outcomes with per-member conflicts |
|
|
@@ -274,7 +280,7 @@ trackly config --base-url http://127.0.0.1:3000 # Point at a different backend
|
|
|
274
280
|
| Job search + filters | Yes | Yes | Yes |
|
|
275
281
|
| Apply/save/dismiss | Yes | Yes | Yes |
|
|
276
282
|
| AI-powered search | Yes (trackly ask) | Yes | Yes |
|
|
277
|
-
| MCP integration | Yes (
|
|
283
|
+
| MCP integration | Yes (54 local tools) | -- | -- |
|
|
278
284
|
| Browser required | No | Yes | No |
|
|
279
285
|
| Best for | Terminal + AI agents | Visual browsing | Custom integrations |
|
|
280
286
|
|
|
@@ -288,7 +294,7 @@ Install trackly-cli (`npm install -g trackly-cli`), authenticate with `trackly l
|
|
|
288
294
|
|
|
289
295
|
**What MCP servers exist for job searching?**
|
|
290
296
|
|
|
291
|
-
trackly-cli includes a built-in MCP server with
|
|
297
|
+
trackly-cli includes a built-in MCP server with 54 tools for job search, company lookup, discovery preferences, application tracking, accessible execution and frozen-batch orchestration, profile onboarding, beta evidence, and manual-submit form preparation. Run `trackly mcp` or use `trackly agent setup --client claude`.
|
|
292
298
|
|
|
293
299
|
**How do I use Claude Code for job hunting?**
|
|
294
300
|
|
|
@@ -6,9 +6,16 @@
|
|
|
6
6
|
"contractSha256": "8c52daa8b62095a6431d5b954505079c3f7c802e5c5ae39efa8ea7e988dfbc7a"
|
|
7
7
|
},
|
|
8
8
|
"3.6.2": {
|
|
9
|
-
"contractSha256": "83e107ad7ae2abdc42cdacb9848d8e3bf345951ed7b8f42d5cd9b5e203528bf2"
|
|
9
|
+
"contractSha256": "83e107ad7ae2abdc42cdacb9848d8e3bf345951ed7b8f42d5cd9b5e203528bf2",
|
|
10
|
+
"toolsSha256": "d9d24da4b79a4cc65aec589debd2b9072e41b309fd6bf6a65e95dd4b5bef3c08"
|
|
10
11
|
},
|
|
11
12
|
"3.6.3": {
|
|
12
13
|
"contractSha256": "93a8e55196c14f04afb923711997eacdc26dbdeae50b253b906d547abd346e30"
|
|
14
|
+
},
|
|
15
|
+
"3.7.0": {
|
|
16
|
+
"toolsSha256": "8380e2a9fabef4da18ea731f55c8994aef5a4738bb6e7daef9be4ce6ebfec64f"
|
|
17
|
+
},
|
|
18
|
+
"3.7.1": {
|
|
19
|
+
"contractSha256": "0eb582fcac72804347c5da53d214a67908f657f600536db7b42167350bb3f3ab"
|
|
13
20
|
}
|
|
14
21
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "3.
|
|
2
|
+
"contractVersion": "3.7.1",
|
|
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"],
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
"applySurfaceEvidenceTypes": ["surface_inventory_reconciled", "surface_missing", "surface_close_attempted", "surface_close_receipt", "surface_post_close_absent", "surface_close_failed"],
|
|
17
17
|
"applySurfaceOwnershipStates": ["controller_owned", "user_owned", "controller_user_union", "unresolved"],
|
|
18
18
|
"applySubmissionEvidenceTypes": ["request_accepted", "confirmation_detected", "provider_receipt_detected"],
|
|
19
|
-
"applySubmissionEvidenceSources": ["submission_request", "success_page", "user_confirmation", "provider_receipt", "post_submit_receipt"]
|
|
19
|
+
"applySubmissionEvidenceSources": ["submission_request", "success_page", "user_confirmation", "provider_receipt", "post_submit_receipt"],
|
|
20
|
+
"applyPositiveSubmissionStrategyCodes": ["greenhouse_confirmation_redirect", "greenhouse_semantic_success", "ashby_semantic_success_banner", "ashby_submitted_state", "lever_confirmation_redirect", "lever_semantic_confirmation"],
|
|
21
|
+
"applyExecutionRecoveryEligibilityCodes": ["recoverable", "already_applied", "inactive", "revoked", "changed_requisition", "concurrently_owned", "lineage_unavailable", "retention_expired"],
|
|
22
|
+
"applyHandoffReconciliationClassifications": ["detected", "user_confirmed", "unresolved", "contradictory"],
|
|
23
|
+
"applyUploadStages": ["semantic_control_discovered", "file_chooser_armed", "file_chooser_opened", "set_files_succeeded", "user_facing_filename_committed", "parser_fields_rechecked"],
|
|
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"]
|
|
20
25
|
},
|
|
21
26
|
"tools": {
|
|
22
27
|
"trackly_get_apply_queue": "{limit:z.number().int().min(1).max(100).optional(),cursor:z.string().min(1).max(2048).optional()}",
|
|
@@ -26,6 +31,10 @@
|
|
|
26
31
|
"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)}",
|
|
27
32
|
"trackly_get_active_apply_execution": "{}",
|
|
28
33
|
"trackly_get_apply_execution": "{executionId:z.number().int().min(1)}",
|
|
34
|
+
"trackly_list_recoverable_apply_executions": "{}",
|
|
35
|
+
"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
|
+
"trackly_list_apply_review_handoffs": "{executionId:z.number().int().min(1)}",
|
|
37
|
+
"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)}",
|
|
29
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)}",
|
|
30
39
|
"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)}",
|
|
31
40
|
"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)}",
|
|
@@ -48,11 +57,13 @@
|
|
|
48
57
|
"trackly_get_apply_protocol": "{}",
|
|
49
58
|
"trackly_report_apply_observation": "{runId:z.number().int().min(1),batchId:z.number().int().min(1).optional(),memberId:z.number().int().min(1).optional(),inspectionEpoch:z.number().int().min(0).optional(),leaseToken:z.string().min(1).max(1024).optional(),provider:z.string().regex(SAFE_OBSERVATION_CODE),fieldLabel:z.string().min(1).max(1000),observationType:z.string().regex(SAFE_OBSERVATION_CODE),resolutionCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),metadata:z.object({controlType:z.string().regex(SAFE_OBSERVATION_CODE).optional(),required:z.boolean().optional(),errorCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),committed:z.boolean(),scenarioCode:z.enum(APPLY_SCENARIO_CODES),browserSurface:z.enum(APPLY_BROWSER_SURFACES),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/).optional(),resumedAfterHandoff:z.boolean().optional()})}",
|
|
50
59
|
"trackly_report_apply_observations": "{observations:z.array(z.object({runId:z.number().int().min(1),batchId:z.number().int().min(1),memberId:z.number().int().min(1),inspectionEpoch:z.number().int().min(0),leaseToken:z.string().min(1).max(1024),provider:z.string().regex(SAFE_OBSERVATION_CODE),fieldLabel:z.string().min(1).max(1000),observationType:z.string().regex(SAFE_OBSERVATION_CODE),resolutionCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),metadata:z.object({controlType:z.string().regex(SAFE_OBSERVATION_CODE).optional(),required:z.boolean().optional(),errorCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),committed:z.boolean(),scenarioCode:z.enum(APPLY_SCENARIO_CODES),browserSurface:z.enum(APPLY_BROWSER_SURFACES),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/).optional(),resumedAfterHandoff:z.boolean().optional()})})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS)}",
|
|
51
|
-
"trackly_record_application_outcome": "{runId:z.number().int().min(1),batchId:z.number().int().min(1).optional(),memberId:z.number().int().min(1).optional(),inspectionEpoch:z.number().int().min(0).optional(),leaseToken:z.string().min(1).max(1024).optional(),outcome:z.enum(['review_ready','submitted','failed','blocked']),confirmation:z.enum(['user_confirmation','success_page']).optional()}",
|
|
52
|
-
"trackly_record_application_outcomes": "{outcomes:z.array(z.object({runId:z.number().int().min(1),batchId:z.number().int().min(1),memberId:z.number().int().min(1),inspectionEpoch:z.number().int().min(0),leaseToken:z.string().min(1).max(1024),outcome:z.enum(['review_ready','submitted','failed','blocked']),confirmation:z.enum(['user_confirmation','success_page']).optional()})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS)}",
|
|
60
|
+
"trackly_record_application_outcome": "{runId:z.number().int().min(1),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),batchId:z.number().int().min(1).optional(),memberId:z.number().int().min(1).optional(),inspectionEpoch:z.number().int().min(0).optional(),leaseToken:z.string().min(1).max(1024).optional(),outcome:z.enum(['review_ready','submitted','failed','blocked']),confirmation:z.enum(['user_confirmation','success_page']).optional()}",
|
|
61
|
+
"trackly_record_application_outcomes": "{idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),outcomes:z.array(z.object({runId:z.number().int().min(1),batchId:z.number().int().min(1),memberId:z.number().int().min(1),inspectionEpoch:z.number().int().min(0),leaseToken:z.string().min(1).max(1024),outcome:z.enum(['review_ready','submitted','failed','blocked']),confirmation:z.enum(['user_confirmation','success_page']).optional()})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS).refine((values)=>newSet(values.map(({runId})=>runId)).size===values.length,{message:'outcomesmustcontainuniquerunIdvalues'})}",
|
|
53
62
|
"trackly_prepare_resume": "{runId:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/)}",
|
|
54
63
|
"trackly_lint_application_text": "{text:z.string().max(20000),emDashPolicy:z.enum(['forbid','allow_if_voice_sample','allow']).optional(),voiceSampleAllowsEmDash:z.boolean().optional(),prohibitedPhrases:z.array(z.string().min(1).max(200)).max(50).optional(),minLength:z.number().int().min(0).max(20000).optional(),maxLength:z.number().int().min(0).max(20000).optional(),claims:z.array(z.object({claimFingerprint:z.string().regex(/^[a-f0-9]{64}$/),evidenceRefs:z.array(z.string().regex(SAFE_OBSERVATION_CODE)).max(20)}).strict()).max(100),claimsComplete:z.literal(true)}",
|
|
55
64
|
"trackly_diagnose_local_path": "{exactPath:z.string().min(1).max(4096),originalErrno:z.string().regex(ERRNO_PATTERN).optional()}",
|
|
65
|
+
"trackly_validate_apply_tab_keep_set": "{expectedTabIds:z.array(opaqueTabIdSchema).min(1).max(100),keepTabIds:z.array(opaqueTabIdSchema).max(100),controllerInventory:z.object({complete:z.boolean(),tabIds:z.array(opaqueTabIdSchema).max(1000)}).strict(),userInventory:z.object({complete:z.boolean(),tabIds:z.array(opaqueTabIdSchema).max(1000)}).strict()}",
|
|
66
|
+
"trackly_validate_apply_resume_upload": "{capabilities:z.object({semanticControlDiscovery:z.boolean(),chooserArming:z.boolean(),fileAttachment:z.boolean(),committedFilenameInspection:z.boolean(),parserFieldRecheck:z.boolean()}).strict(),events:z.array(z.object({stage:z.enum(APPLY_UPLOAD_STAGES),outcome:z.enum(['passed','failed']),failureCode:z.enum(APPLY_UPLOAD_FAILURE_CODES).optional()}).strict()).max(APPLY_UPLOAD_STAGES.length)}",
|
|
56
67
|
"trackly_verify_prepared_resume": {
|
|
57
68
|
"local": "{runId:z.number().int().min(1),resumeId:z.number().int().min(1),confirmationId:z.string().min(1).max(200),exactLocalPath:z.string().min(1).max(4096),sha256:z.string().regex(/^[a-f0-9]{64}$/i),sizeBytes:z.number().int().min(1),expiresAt:z.string().datetime()}",
|
|
58
69
|
"hosted": "{runId:z.number().int().min(1),confirmationId:z.string().min(1).max(200)}"
|
package/docs/trackly-tools.md
CHANGED
|
@@ -106,6 +106,10 @@ API-key creation attempts.
|
|
|
106
106
|
- **trackly_start_apply_execution** — Start a server-owned `complete_next_n_accessible` execution with a target from 1–20, then consume the returned authoritative `progress` and `nextAction` before doing browser work.
|
|
107
107
|
- **trackly_get_active_apply_execution** — Recover the active execution before legacy batch recovery.
|
|
108
108
|
- **trackly_get_apply_execution** — Read an execution's latest current-wave identity and authoritative aggregate progress funnel.
|
|
109
|
+
- **trackly_list_recoverable_apply_executions** — List a bounded, value-free menu of exact recovery candidates after local context loss.
|
|
110
|
+
- **trackly_recover_exact_apply_members** — Recover only the explicitly confirmed candidates from one retained source snapshot, with no substitutions.
|
|
111
|
+
- **trackly_list_apply_review_handoffs** — Rediscover active, nonexpired review-handoff receipts for one execution without browser or profile values.
|
|
112
|
+
- **trackly_claim_apply_review_handoff** — Claim one exact handoff receipt and classify every member before grouped submission reconciliation.
|
|
109
113
|
- **trackly_get_apply_execution_snapshot**: Fetch a compact bounded projection of current members, requested profile keys, mutability, allowed operations, milestones, lease timing, and the authoritative funnel.
|
|
110
114
|
- **trackly_resume_parked_apply_member**: Resume one parked member only after explicit user instruction; a fresh non-mutating access probe remains required before form mutation.
|
|
111
115
|
- **trackly_approve_apply_execution_resume**: Approve one exact resume identity for an execution's unchanged original snapshot while preserving immediate per-run local verification before upload.
|
|
@@ -134,9 +138,16 @@ API-key creation attempts.
|
|
|
134
138
|
- **trackly_verify_prepared_resume** — Local MCP only: immediately before attachment, recompute the user-confirmed resume hash and size, validate the exact path/run/expiration, and lock the file read-only. Any mismatch requires a fresh preview and confirmation.
|
|
135
139
|
- **trackly_lint_application_text**: Local MCP only: require an explicitly complete claim-reference packet, then return a draft hash, length, and stable value-free writing violations without sending or echoing application text to Trackly.
|
|
136
140
|
- **trackly_diagnose_local_path**: Local MCP only: measure the exact implicated path's filesystem, inode, quota observability, and write-probe result without deleting user files or claiming a global disk cause.
|
|
141
|
+
- **trackly_validate_apply_tab_keep_set**: Local MCP only: fail closed unless a caller-supplied nonempty expected tab set exactly matches the keep set and is covered by complete controller and user inventories. This pure helper allows unrelated tabs, performs no browser action, and never sends tab IDs to Trackly.
|
|
142
|
+
- **trackly_validate_apply_resume_upload**: Local MCP only: validate the browser adapter's capabilities and ordered, value-free attachment proof stages without opening a chooser, reading a path, controlling a browser, or sending form data to Trackly.
|
|
137
143
|
|
|
138
144
|
Apply contract v3 intentionally gives this verifier different local and hosted schemas: local MCP receives the full proof needed to inspect the private file, while hosted MCP accepts only run and confirmation identifiers and returns the manual/local-agent requirement. Local paths are never sent remotely. Resume fingerprints are sent only to authenticated Trackly resume approval and truth-certification endpoints, never observations or employer forms. Version 3.1 also records universal value-free evidence for critical-contact integrity and the manual-submit boundary. Version 3.2 authorizes the exact stored HTTPS origin for jobs Trackly ingested from employer careers sources, without granting redirect, iframe, or hostname-suffix privileges. Version 3.3.1 adds active-batch recovery, epoch-bound observations/outcomes, and truth certification for forms with no resume control. Version 3.4 adds server-owned accessible executions above immutable child batches, typed access dispositions, and an authoritative target-completion funnel.
|
|
139
145
|
|
|
146
|
+
Version 3.7 adds public exact-member recovery and scoped review-handoff claims,
|
|
147
|
+
plus local tab-finalizer and resume-upload proof validators. Recovery candidates
|
|
148
|
+
are bounded and value-free. The local helpers perform no browser actions and
|
|
149
|
+
send no tab IDs, filenames, paths, page text, or form values to Trackly.
|
|
150
|
+
|
|
140
151
|
### Maintenance behavior
|
|
141
152
|
|
|
142
153
|
REST, refresh, download, CLI, and local/hosted MCP surfaces use canonical `code: "maintenance_mode"`; older `planned_maintenance` responses are accepted only as a compatibility alias. Structured errors retain HTTP and service status, retry time, estimated return, and request ID without clearing valid credentials. Maintenance is resumable rather than retryable: wait for the advertised window, refetch the Apply protocol and profile, then resume the existing `agent_browser` run. Never create a duplicate run or click Submit.
|
package/lib/agent.js
CHANGED
|
@@ -8,13 +8,13 @@ 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.5.0';
|
|
12
12
|
const CLI_USER_AGENT = `trackly-cli/${PACKAGE_VERSION}`;
|
|
13
13
|
const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
|
|
14
14
|
|
|
15
15
|
const SKILL_NAME = 'trackly-apply';
|
|
16
16
|
const SKILL_MAJOR = Number(SKILL_VERSION.split('.')[0]);
|
|
17
|
-
const MIN_APPLY_PROTOCOL_VERSION = '3.
|
|
17
|
+
const MIN_APPLY_PROTOCOL_VERSION = '3.6.0';
|
|
18
18
|
const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
19
19
|
const MANAGED_FILE = '.trackly-managed.json';
|
|
20
20
|
const SKILL_MANIFEST_VERSION = 1;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const APPLY_TAB_SET_FAILURE_CODES = Object.freeze({
|
|
4
|
+
CONTROLLER_INVENTORY_INCOMPLETE: 'controller_inventory_incomplete',
|
|
5
|
+
USER_INVENTORY_INCOMPLETE: 'user_inventory_incomplete',
|
|
6
|
+
EXPECTED_SET_EMPTY: 'expected_set_empty',
|
|
7
|
+
KEEP_SET_EMPTY_WITH_EXPECTED_TABS: 'keep_set_empty_with_expected_tabs',
|
|
8
|
+
DUPLICATE_EXPECTED_TAB_ID: 'duplicate_expected_tab_id',
|
|
9
|
+
DUPLICATE_KEEP_TAB_ID: 'duplicate_keep_tab_id',
|
|
10
|
+
DUPLICATE_CONTROLLER_TAB_ID: 'duplicate_controller_tab_id',
|
|
11
|
+
DUPLICATE_USER_TAB_ID: 'duplicate_user_tab_id',
|
|
12
|
+
AMBIGUOUS_CROSS_TYPE_TAB_ID: 'ambiguous_cross_type_tab_id',
|
|
13
|
+
EXPECTED_KEEP_CARDINALITY_MISMATCH: 'expected_keep_cardinality_mismatch',
|
|
14
|
+
EXPECTED_KEEP_SET_MISMATCH: 'expected_keep_set_mismatch',
|
|
15
|
+
EXPECTED_TAB_MISSING_FROM_INVENTORY_UNION: 'expected_tab_missing_from_inventory_union',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function canonicalizeTabId(tabId) {
|
|
19
|
+
if (typeof tabId === 'number') {
|
|
20
|
+
if (!Number.isSafeInteger(tabId)) throw new TypeError('Tab IDs supplied as numbers must be safe integers.');
|
|
21
|
+
return String(tabId);
|
|
22
|
+
}
|
|
23
|
+
if (typeof tabId !== 'string' || tabId.trim().length === 0) {
|
|
24
|
+
throw new TypeError('Tab IDs must be safe integers or nonblank opaque strings.');
|
|
25
|
+
}
|
|
26
|
+
if (tabId.length > 512) throw new RangeError('Opaque tab IDs must not exceed 512 characters.');
|
|
27
|
+
return tabId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function canonicalizeTabIds(tabIds, { label, maxItems }) {
|
|
31
|
+
if (!Array.isArray(tabIds)) throw new TypeError('Tab ID collections must be arrays.');
|
|
32
|
+
if (tabIds.length > maxItems) {
|
|
33
|
+
throw new RangeError(`${label} must not contain more than ${maxItems} tab IDs.`);
|
|
34
|
+
}
|
|
35
|
+
return tabIds.map(canonicalizeTabId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasDuplicates(tabIds) {
|
|
39
|
+
return new Set(tabIds).size !== tabIds.length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasCrossTypeCollision(collections) {
|
|
43
|
+
const typesByCanonicalId = new Map();
|
|
44
|
+
for (const tabId of collections.flat()) {
|
|
45
|
+
const canonical = canonicalizeTabId(tabId);
|
|
46
|
+
const types = typesByCanonicalId.get(canonical) ?? new Set();
|
|
47
|
+
types.add(typeof tabId);
|
|
48
|
+
typesByCanonicalId.set(canonical, types);
|
|
49
|
+
}
|
|
50
|
+
return [...typesByCanonicalId.values()].some((types) => types.size > 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sameSet(left, right) {
|
|
54
|
+
if (left.length !== right.length) return false;
|
|
55
|
+
const rightSet = new Set(right);
|
|
56
|
+
return left.every((tabId) => rightSet.has(tabId));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateApplyTabKeepSet({
|
|
60
|
+
expectedTabIds,
|
|
61
|
+
keepTabIds,
|
|
62
|
+
controllerInventory,
|
|
63
|
+
userInventory,
|
|
64
|
+
}) {
|
|
65
|
+
const expected = canonicalizeTabIds(expectedTabIds, { label: 'expectedTabIds', maxItems: 100 });
|
|
66
|
+
const keep = canonicalizeTabIds(keepTabIds, { label: 'keepTabIds', maxItems: 100 });
|
|
67
|
+
const controller = canonicalizeTabIds(controllerInventory?.tabIds, {
|
|
68
|
+
label: 'controllerInventory.tabIds',
|
|
69
|
+
maxItems: 1000,
|
|
70
|
+
});
|
|
71
|
+
const user = canonicalizeTabIds(userInventory?.tabIds, {
|
|
72
|
+
label: 'userInventory.tabIds',
|
|
73
|
+
maxItems: 1000,
|
|
74
|
+
});
|
|
75
|
+
const failureCodes = [];
|
|
76
|
+
const addFailure = (code) => {
|
|
77
|
+
if (!failureCodes.includes(code)) failureCodes.push(code);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (controllerInventory?.complete !== true) {
|
|
81
|
+
addFailure(APPLY_TAB_SET_FAILURE_CODES.CONTROLLER_INVENTORY_INCOMPLETE);
|
|
82
|
+
}
|
|
83
|
+
if (userInventory?.complete !== true) {
|
|
84
|
+
addFailure(APPLY_TAB_SET_FAILURE_CODES.USER_INVENTORY_INCOMPLETE);
|
|
85
|
+
}
|
|
86
|
+
if (expected.length === 0) addFailure(APPLY_TAB_SET_FAILURE_CODES.EXPECTED_SET_EMPTY);
|
|
87
|
+
if (expected.length > 0 && keep.length === 0) {
|
|
88
|
+
addFailure(APPLY_TAB_SET_FAILURE_CODES.KEEP_SET_EMPTY_WITH_EXPECTED_TABS);
|
|
89
|
+
}
|
|
90
|
+
if (hasDuplicates(expected)) addFailure(APPLY_TAB_SET_FAILURE_CODES.DUPLICATE_EXPECTED_TAB_ID);
|
|
91
|
+
if (hasDuplicates(keep)) addFailure(APPLY_TAB_SET_FAILURE_CODES.DUPLICATE_KEEP_TAB_ID);
|
|
92
|
+
if (hasDuplicates(controller)) addFailure(APPLY_TAB_SET_FAILURE_CODES.DUPLICATE_CONTROLLER_TAB_ID);
|
|
93
|
+
if (hasDuplicates(user)) addFailure(APPLY_TAB_SET_FAILURE_CODES.DUPLICATE_USER_TAB_ID);
|
|
94
|
+
if (hasCrossTypeCollision([
|
|
95
|
+
expectedTabIds, keepTabIds, controllerInventory.tabIds, userInventory.tabIds,
|
|
96
|
+
])) addFailure(APPLY_TAB_SET_FAILURE_CODES.AMBIGUOUS_CROSS_TYPE_TAB_ID);
|
|
97
|
+
if (expected.length !== keep.length) {
|
|
98
|
+
addFailure(APPLY_TAB_SET_FAILURE_CODES.EXPECTED_KEEP_CARDINALITY_MISMATCH);
|
|
99
|
+
}
|
|
100
|
+
if (!sameSet(expected, keep)) addFailure(APPLY_TAB_SET_FAILURE_CODES.EXPECTED_KEEP_SET_MISMATCH);
|
|
101
|
+
|
|
102
|
+
const inventoryUnion = new Set([...controller, ...user]);
|
|
103
|
+
if (expected.some((tabId) => !inventoryUnion.has(tabId))) {
|
|
104
|
+
addFailure(APPLY_TAB_SET_FAILURE_CODES.EXPECTED_TAB_MISSING_FROM_INVENTORY_UNION);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const safeToFinalize = failureCodes.length === 0;
|
|
108
|
+
return {
|
|
109
|
+
safeToFinalize,
|
|
110
|
+
failureCodes,
|
|
111
|
+
expectedCount: expected.length,
|
|
112
|
+
keepCount: keep.length,
|
|
113
|
+
controllerInventoryCount: controller.length,
|
|
114
|
+
userInventoryCount: user.length,
|
|
115
|
+
inventoryUnionCount: inventoryUnion.size,
|
|
116
|
+
canonicalKeepTabIds: safeToFinalize ? keep : [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
APPLY_TAB_SET_FAILURE_CODES,
|
|
122
|
+
canonicalizeTabId,
|
|
123
|
+
validateApplyTabKeepSet,
|
|
124
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const APPLY_CONTRACT = require('../contracts/trackly-apply-tools.json');
|
|
4
|
+
|
|
5
|
+
const APPLY_UPLOAD_STAGES = Object.freeze([...APPLY_CONTRACT.constants.applyUploadStages]);
|
|
6
|
+
const APPLY_UPLOAD_FAILURE_CODES = Object.freeze([
|
|
7
|
+
...APPLY_CONTRACT.constants.applyUploadFailureCodes,
|
|
8
|
+
]);
|
|
9
|
+
const APPLY_UPLOAD_CAPABILITIES = Object.freeze([
|
|
10
|
+
'semanticControlDiscovery',
|
|
11
|
+
'chooserArming',
|
|
12
|
+
'fileAttachment',
|
|
13
|
+
'committedFilenameInspection',
|
|
14
|
+
'parserFieldRecheck',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function validateApplyResumeUpload({ capabilities, events }) {
|
|
18
|
+
const failureCodes = [];
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
let expectedIndex = 0;
|
|
21
|
+
let lastPassedStage = null;
|
|
22
|
+
|
|
23
|
+
for (const capability of APPLY_UPLOAD_CAPABILITIES) {
|
|
24
|
+
if (capabilities[capability] !== true) failureCodes.push('upload_capability_unavailable');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const event of events) {
|
|
28
|
+
if (seen.has(event.stage)) {
|
|
29
|
+
failureCodes.push('upload_stage_duplicate');
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
seen.add(event.stage);
|
|
33
|
+
const index = APPLY_UPLOAD_STAGES.indexOf(event.stage);
|
|
34
|
+
const isExpectedStage = index === expectedIndex;
|
|
35
|
+
if (!isExpectedStage) failureCodes.push('upload_stage_out_of_order');
|
|
36
|
+
|
|
37
|
+
const hasFailureCode = Boolean(event.failureCode);
|
|
38
|
+
if (hasFailureCode) {
|
|
39
|
+
failureCodes.push(event.failureCode);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (event.outcome === 'failed' || hasFailureCode) {
|
|
43
|
+
if (!event.failureCode) failureCodes.push('upload_stage_missing');
|
|
44
|
+
} else if (isExpectedStage) {
|
|
45
|
+
expectedIndex += 1;
|
|
46
|
+
lastPassedStage = event.stage;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (expectedIndex !== APPLY_UPLOAD_STAGES.length) failureCodes.push('upload_stage_missing');
|
|
51
|
+
const uniqueFailureCodes = [...new Set(failureCodes)];
|
|
52
|
+
return {
|
|
53
|
+
safeToClaimAttachment: uniqueFailureCodes.length === 0,
|
|
54
|
+
failureCodes: uniqueFailureCodes,
|
|
55
|
+
lastPassedStage,
|
|
56
|
+
completedStageCount: expectedIndex,
|
|
57
|
+
requiredStageCount: APPLY_UPLOAD_STAGES.length,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
APPLY_UPLOAD_CAPABILITIES,
|
|
63
|
+
APPLY_UPLOAD_FAILURE_CODES,
|
|
64
|
+
APPLY_UPLOAD_STAGES,
|
|
65
|
+
validateApplyResumeUpload,
|
|
66
|
+
};
|
package/mcp/apply-tools.js
CHANGED
|
@@ -5,6 +5,12 @@ const { apiRequest } = require('../lib/client');
|
|
|
5
5
|
const { prepareResume, verifyPreparedResume } = require('../lib/agent');
|
|
6
6
|
const { lintApplicationText } = require('../lib/application-text');
|
|
7
7
|
const { diagnoseLocalPath, ERRNO_PATTERN } = require('../lib/path-diagnostics');
|
|
8
|
+
const { validateApplyTabKeepSet } = require('../lib/apply-tab-set');
|
|
9
|
+
const {
|
|
10
|
+
APPLY_UPLOAD_FAILURE_CODES,
|
|
11
|
+
APPLY_UPLOAD_STAGES,
|
|
12
|
+
validateApplyResumeUpload,
|
|
13
|
+
} = require('../lib/apply-upload');
|
|
8
14
|
const { isIso3166Alpha2 } = require('../lib/iso-country-codes');
|
|
9
15
|
const APPLY_CONTRACT = require('../contracts/trackly-apply-tools.json');
|
|
10
16
|
|
|
@@ -22,6 +28,8 @@ const APPLY_SURFACE_EVIDENCE_TYPES = APPLY_CONTRACT.constants.applySurfaceEviden
|
|
|
22
28
|
const APPLY_SURFACE_OWNERSHIP_STATES = APPLY_CONTRACT.constants.applySurfaceOwnershipStates;
|
|
23
29
|
const APPLY_SUBMISSION_EVIDENCE_TYPES = APPLY_CONTRACT.constants.applySubmissionEvidenceTypes;
|
|
24
30
|
const APPLY_SUBMISSION_EVIDENCE_SOURCES = APPLY_CONTRACT.constants.applySubmissionEvidenceSources;
|
|
31
|
+
const APPLY_EXECUTION_RECOVERY_ELIGIBILITY_CODES = APPLY_CONTRACT.constants.applyExecutionRecoveryEligibilityCodes;
|
|
32
|
+
const APPLY_HANDOFF_RECONCILIATION_CLASSIFICATION_CODES = APPLY_CONTRACT.constants.applyHandoffReconciliationClassifications;
|
|
25
33
|
const APPLY_BATCH_MAX_MEMBERS = 100;
|
|
26
34
|
const APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST = 20;
|
|
27
35
|
const APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT = 25;
|
|
@@ -32,6 +40,12 @@ const SAFE_IDEMPOTENCY_KEY = /^[\x20-\x7e]+$/;
|
|
|
32
40
|
const iso3166Alpha2Schema = z.string().regex(/^[A-Za-z]{2}$/).refine(isIso3166Alpha2, {
|
|
33
41
|
message: 'Expected an ISO 3166-1 alpha-2 country code',
|
|
34
42
|
});
|
|
43
|
+
const opaqueTabIdSchema = z.union([
|
|
44
|
+
z.number().int().safe(),
|
|
45
|
+
z.string().min(1).max(512).refine((value) => value.trim().length > 0, {
|
|
46
|
+
message: 'Expected a nonblank opaque tab ID',
|
|
47
|
+
}),
|
|
48
|
+
]);
|
|
35
49
|
const applyExecutionDispositionSchema = z.object({
|
|
36
50
|
jobId: z.number().int().min(1),
|
|
37
51
|
classification: z.enum(APPLY_EXECUTION_ACCESS_CLASSIFICATIONS),
|
|
@@ -44,6 +58,133 @@ const applyExecutionDispositionSchema = z.object({
|
|
|
44
58
|
probeOnlyNoDraft: z.boolean().optional(),
|
|
45
59
|
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
46
60
|
}).strict();
|
|
61
|
+
const recoverableCandidateSchema = z.object({
|
|
62
|
+
candidateId: z.number().int().min(1),
|
|
63
|
+
jobId: z.number().int().min(1),
|
|
64
|
+
queuePosition: z.number().int().min(0),
|
|
65
|
+
eligibilityCode: z.enum(APPLY_EXECUTION_RECOVERY_ELIGIBILITY_CODES),
|
|
66
|
+
}).strict();
|
|
67
|
+
const recoverableExecutionSourceSchema = z.object({
|
|
68
|
+
sourceExecutionId: z.number().int().min(1),
|
|
69
|
+
sourceSnapshotHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
70
|
+
recoverableUntil: z.string().datetime(),
|
|
71
|
+
candidates: z.array(recoverableCandidateSchema).max(APPLY_EXECUTION_MAX_TARGET),
|
|
72
|
+
}).strict().refine(({ candidates }) => (
|
|
73
|
+
new Set(candidates.map(({ candidateId }) => candidateId)).size === candidates.length
|
|
74
|
+
), { message: 'recoverable source candidate IDs must be unique' });
|
|
75
|
+
const recoverableExecutionsResponseSchema = z.object({
|
|
76
|
+
success: z.literal(true),
|
|
77
|
+
sources: z.array(recoverableExecutionSourceSchema).max(APPLY_EXECUTION_MAX_TARGET),
|
|
78
|
+
}).strict().refine(({ sources }) => (
|
|
79
|
+
new Set(sources.map(({ sourceExecutionId }) => sourceExecutionId)).size === sources.length
|
|
80
|
+
), { message: 'recoverable source execution IDs must be unique' });
|
|
81
|
+
// Discovery responses are strict and value-free so unexpected fields never enter agent context.
|
|
82
|
+
// Mutation responses permit additive top-level metadata, but keep safety-critical nested rows strict.
|
|
83
|
+
const exactRecoveryResponseSchema = z.object({
|
|
84
|
+
success: z.literal(true),
|
|
85
|
+
replay: z.boolean(),
|
|
86
|
+
execution: z.object({
|
|
87
|
+
id: z.number().int().min(1),
|
|
88
|
+
mode: z.literal('recover_exact_members'),
|
|
89
|
+
}).passthrough(),
|
|
90
|
+
assertedCandidateIds: z.array(z.number().int().min(1)).max(APPLY_EXECUTION_MAX_TARGET),
|
|
91
|
+
eligibleCandidateIds: z.array(z.number().int().min(1)).max(APPLY_EXECUTION_MAX_TARGET),
|
|
92
|
+
eligibility: z.array(recoverableCandidateSchema).max(APPLY_EXECUTION_MAX_TARGET),
|
|
93
|
+
}).passthrough();
|
|
94
|
+
|
|
95
|
+
function validateExactRecoveryResponse(input, response) {
|
|
96
|
+
const requested = [...input.candidateIds].sort((a, b) => a - b);
|
|
97
|
+
const asserted = [...response.assertedCandidateIds].sort((a, b) => a - b);
|
|
98
|
+
const eligible = [...response.eligibleCandidateIds].sort((a, b) => a - b);
|
|
99
|
+
const eligibilityIds = response.eligibility
|
|
100
|
+
.map(({ candidateId }) => candidateId)
|
|
101
|
+
.sort((a, b) => a - b);
|
|
102
|
+
const matchesRequested = (values) => (
|
|
103
|
+
values.length === requested.length
|
|
104
|
+
&& values.every((value, index) => value === requested[index])
|
|
105
|
+
);
|
|
106
|
+
if (
|
|
107
|
+
new Set(response.assertedCandidateIds).size !== response.assertedCandidateIds.length
|
|
108
|
+
|| new Set(response.eligibleCandidateIds).size !== response.eligibleCandidateIds.length
|
|
109
|
+
|| new Set(eligibilityIds).size !== eligibilityIds.length
|
|
110
|
+
|| !matchesRequested(asserted)
|
|
111
|
+
|| !matchesRequested(eligible)
|
|
112
|
+
|| !matchesRequested(eligibilityIds)
|
|
113
|
+
|| response.eligibility.some(({ eligibilityCode }) => eligibilityCode !== 'recoverable')
|
|
114
|
+
) {
|
|
115
|
+
throw new Error('Exact recovery response does not match the requested candidate set.');
|
|
116
|
+
}
|
|
117
|
+
return response;
|
|
118
|
+
}
|
|
119
|
+
const handoffClaimMemberSchema = z.object({
|
|
120
|
+
memberId: z.number().int().min(1),
|
|
121
|
+
classification: z.enum(APPLY_HANDOFF_RECONCILIATION_CLASSIFICATION_CODES),
|
|
122
|
+
}).strict();
|
|
123
|
+
const handoffMemberSchema = z.object({
|
|
124
|
+
handoffId: z.number().int().min(1),
|
|
125
|
+
ordinal: z.number().int().min(0),
|
|
126
|
+
batchId: z.number().int().min(1),
|
|
127
|
+
memberId: z.number().int().min(1),
|
|
128
|
+
runId: z.number().int().min(1),
|
|
129
|
+
memberVersion: z.number().int().min(1),
|
|
130
|
+
inspectionEpoch: z.number().int().min(0),
|
|
131
|
+
reconciliationClassification: z.enum(APPLY_HANDOFF_RECONCILIATION_CLASSIFICATION_CODES).nullable(),
|
|
132
|
+
reconciliationResultStatus: z.string().max(100).nullable(),
|
|
133
|
+
}).strict();
|
|
134
|
+
const handoffListResponseSchema = z.object({
|
|
135
|
+
success: z.literal(true),
|
|
136
|
+
executionId: z.number().int().min(1),
|
|
137
|
+
handoffs: z.array(z.object({
|
|
138
|
+
id: z.number().int().min(1),
|
|
139
|
+
executionId: z.number().int().min(1),
|
|
140
|
+
orderedMemberSetHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
141
|
+
generation: z.number().int().min(1),
|
|
142
|
+
status: z.enum(['active', 'partially_reconciled']),
|
|
143
|
+
claimedAt: z.string().datetime().nullable(),
|
|
144
|
+
expiresAt: z.string().datetime(),
|
|
145
|
+
members: z.array(handoffMemberSchema).min(1).max(APPLY_EXECUTION_MAX_TARGET),
|
|
146
|
+
}).strict()).max(APPLY_EXECUTION_MAX_TARGET),
|
|
147
|
+
}).strict();
|
|
148
|
+
|
|
149
|
+
function validateHandoffListResponse(requestedExecutionId, response) {
|
|
150
|
+
const handoffIds = response.handoffs.map(({ id }) => id);
|
|
151
|
+
if (
|
|
152
|
+
response.executionId !== requestedExecutionId
|
|
153
|
+
|| new Set(handoffIds).size !== handoffIds.length
|
|
154
|
+
|| response.handoffs.some((handoff) => (
|
|
155
|
+
handoff.executionId !== requestedExecutionId
|
|
156
|
+
|| new Set(handoff.members.map(({ memberId }) => memberId)).size !== handoff.members.length
|
|
157
|
+
|| handoff.members.some((member) => member.handoffId !== handoff.id)
|
|
158
|
+
))
|
|
159
|
+
) {
|
|
160
|
+
throw new Error('Review handoff response does not match the requested execution and receipt bindings.');
|
|
161
|
+
}
|
|
162
|
+
return response;
|
|
163
|
+
}
|
|
164
|
+
const handoffClaimResponseSchema = z.object({
|
|
165
|
+
success: z.literal(true),
|
|
166
|
+
handoffId: z.number().int().min(1),
|
|
167
|
+
executionId: z.number().int().min(1),
|
|
168
|
+
orderedMemberSetHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
169
|
+
members: z.array(handoffClaimMemberSchema).min(1).max(APPLY_EXECUTION_MAX_TARGET),
|
|
170
|
+
transition: z.literal('claimed'),
|
|
171
|
+
}).passthrough();
|
|
172
|
+
|
|
173
|
+
function validateHandoffClaimResponse(request, response) {
|
|
174
|
+
const requestedMembers = request.members.map(({ memberId, classification }) => `${memberId}:${classification}`);
|
|
175
|
+
const returnedMembers = response.members.map(({ memberId, classification }) => `${memberId}:${classification}`);
|
|
176
|
+
if (
|
|
177
|
+
response.handoffId !== request.handoffId
|
|
178
|
+
|| response.executionId !== request.executionId
|
|
179
|
+
|| response.orderedMemberSetHash !== request.orderedMemberSetHash
|
|
180
|
+
|| new Set(returnedMembers).size !== returnedMembers.length
|
|
181
|
+
|| requestedMembers.length !== returnedMembers.length
|
|
182
|
+
|| !requestedMembers.every((member) => returnedMembers.includes(member))
|
|
183
|
+
) {
|
|
184
|
+
throw new Error('Handoff claim response does not match the requested handoff and member classifications.');
|
|
185
|
+
}
|
|
186
|
+
return response;
|
|
187
|
+
}
|
|
47
188
|
|
|
48
189
|
const truthCertificationCommon = {
|
|
49
190
|
batchId: z.number().int().min(1),
|
|
@@ -121,7 +262,7 @@ const startApplyRunSchema = z.object({
|
|
|
121
262
|
}
|
|
122
263
|
});
|
|
123
264
|
|
|
124
|
-
const APPLY_RELIABILITY_PROMPT = 'Protocol 3.
|
|
265
|
+
const APPLY_RELIABILITY_PROMPT = 'Protocol 3.6 / skill 4.5.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.';
|
|
125
266
|
|
|
126
267
|
function registerApplyTools(
|
|
127
268
|
server,
|
|
@@ -143,6 +284,8 @@ function registerApplyTools(
|
|
|
143
284
|
idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : undefined,
|
|
144
285
|
)
|
|
145
286
|
);
|
|
287
|
+
const discoveredHandoffBindings = new Map();
|
|
288
|
+
const discoveredHandoffIdsByExecution = new Map();
|
|
146
289
|
server.tool(
|
|
147
290
|
'trackly_get_apply_queue',
|
|
148
291
|
'Get the deterministic queue of jobs the user already approved by saving as check later. Do not rescore or veto these jobs.',
|
|
@@ -271,6 +414,101 @@ function registerApplyTools(
|
|
|
271
414
|
), 'Failed to fetch apply execution')
|
|
272
415
|
);
|
|
273
416
|
|
|
417
|
+
server.tool(
|
|
418
|
+
'trackly_list_recoverable_apply_executions',
|
|
419
|
+
'List bounded, value-free exact-member recovery candidates after local context loss. Show the stable job identities to the user and obtain explicit confirmation before recovery; never infer or substitute candidates.',
|
|
420
|
+
{},
|
|
421
|
+
wrapTool(async () => recoverableExecutionsResponseSchema.parse(await applyControlRequest(
|
|
422
|
+
'GET', '/api/jobscout/apply/executions/recoverable',
|
|
423
|
+
)), 'Failed to list recoverable apply executions')
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
server.tool(
|
|
427
|
+
'trackly_recover_exact_apply_members',
|
|
428
|
+
'Create an exact-member recovery execution from one confirmed source snapshot. The asserted candidate set is immutable and replacements are forbidden.',
|
|
429
|
+
{
|
|
430
|
+
sourceExecutionId: z.number().int().min(1),
|
|
431
|
+
sourceSnapshotHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
432
|
+
candidateIds: z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET)
|
|
433
|
+
.refine((values) => new Set(values).size === values.length, {
|
|
434
|
+
message: 'candidateIds must be unique',
|
|
435
|
+
}),
|
|
436
|
+
explicitExactSetConfirmation: z.literal(true),
|
|
437
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
438
|
+
},
|
|
439
|
+
wrapTool(async ({ idempotencyKey, ...input }) => validateExactRecoveryResponse(
|
|
440
|
+
input,
|
|
441
|
+
exactRecoveryResponseSchema.parse(await applyControlRequest('POST', '/api/jobscout/apply/executions/recover', {
|
|
442
|
+
mode: 'recover_exact_members',
|
|
443
|
+
...input,
|
|
444
|
+
}, idempotencyKey)),
|
|
445
|
+
), 'Failed to recover exact apply members')
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
server.tool(
|
|
449
|
+
'trackly_list_apply_review_handoffs',
|
|
450
|
+
'List active, nonexpired review-handoff receipts for one execution after context loss. Returns only stable IDs, lifecycle metadata, and member bindings; never browser values, URLs, or local paths.',
|
|
451
|
+
{ executionId: z.number().int().min(1) },
|
|
452
|
+
wrapTool(async ({ executionId }) => {
|
|
453
|
+
const response = validateHandoffListResponse(
|
|
454
|
+
executionId,
|
|
455
|
+
handoffListResponseSchema.parse(await applyControlRequest(
|
|
456
|
+
'GET', `/api/jobscout/apply/executions/${executionId}/review-handoffs`,
|
|
457
|
+
)),
|
|
458
|
+
);
|
|
459
|
+
for (const handoffId of discoveredHandoffIdsByExecution.get(executionId) || []) {
|
|
460
|
+
discoveredHandoffBindings.delete(handoffId);
|
|
461
|
+
}
|
|
462
|
+
const discoveredHandoffIds = new Set();
|
|
463
|
+
for (const handoff of response.handoffs) {
|
|
464
|
+
discoveredHandoffBindings.set(handoff.id, {
|
|
465
|
+
executionId: handoff.executionId,
|
|
466
|
+
orderedMemberSetHash: handoff.orderedMemberSetHash,
|
|
467
|
+
memberIds: handoff.members.map(({ memberId }) => memberId),
|
|
468
|
+
});
|
|
469
|
+
discoveredHandoffIds.add(handoff.id);
|
|
470
|
+
}
|
|
471
|
+
discoveredHandoffIdsByExecution.set(executionId, discoveredHandoffIds);
|
|
472
|
+
return response;
|
|
473
|
+
}, 'Failed to list apply review handoffs')
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
server.tool(
|
|
477
|
+
'trackly_claim_apply_review_handoff',
|
|
478
|
+
'Claim one exact, unambiguous review-handoff group and classify every member before grouped submission reconciliation. Never use this tool to infer submission or replace success-page evidence or explicit user confirmation.',
|
|
479
|
+
{
|
|
480
|
+
handoffId: z.number().int().min(1),
|
|
481
|
+
members: z.array(z.object({
|
|
482
|
+
memberId: z.number().int().min(1),
|
|
483
|
+
classification: z.enum(APPLY_HANDOFF_RECONCILIATION_CLASSIFICATION_CODES),
|
|
484
|
+
}).strict()).min(1).max(APPLY_EXECUTION_MAX_TARGET)
|
|
485
|
+
.refine((values) => new Set(values.map(({ memberId }) => memberId)).size === values.length, {
|
|
486
|
+
message: 'members must contain unique memberId values',
|
|
487
|
+
}),
|
|
488
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
489
|
+
},
|
|
490
|
+
wrapTool(async ({ handoffId, idempotencyKey, members }) => {
|
|
491
|
+
const binding = discoveredHandoffBindings.get(handoffId);
|
|
492
|
+
if (!binding) {
|
|
493
|
+
throw new Error('Review handoff must be selected from the latest discovery response.');
|
|
494
|
+
}
|
|
495
|
+
const { memberIds, ...receiptBinding } = binding;
|
|
496
|
+
const requestedMemberIds = new Set(members.map(({ memberId }) => memberId));
|
|
497
|
+
if (
|
|
498
|
+
requestedMemberIds.size !== memberIds.length
|
|
499
|
+
|| memberIds.some((memberId) => !requestedMemberIds.has(memberId))
|
|
500
|
+
) {
|
|
501
|
+
throw new Error('Handoff claim must classify every discovered member exactly once.');
|
|
502
|
+
}
|
|
503
|
+
return validateHandoffClaimResponse(
|
|
504
|
+
{ handoffId, members, ...receiptBinding },
|
|
505
|
+
handoffClaimResponseSchema.parse(await applyControlRequest(
|
|
506
|
+
'POST', `/api/jobscout/apply/review-handoffs/${handoffId}/claim`, { members }, idempotencyKey,
|
|
507
|
+
)),
|
|
508
|
+
);
|
|
509
|
+
}, 'Failed to claim apply review handoff')
|
|
510
|
+
);
|
|
511
|
+
|
|
274
512
|
server.tool(
|
|
275
513
|
'trackly_get_apply_execution_snapshot',
|
|
276
514
|
'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.',
|
|
@@ -757,6 +995,7 @@ function registerApplyTools(
|
|
|
757
995
|
'Record review readiness or a user-confirmed outcome. Before handoff use literal outcome=review_ready and verify awaiting_manual_submit. Mark submitted with literal outcome=submitted only after a success page or explicit user confirmation.',
|
|
758
996
|
{
|
|
759
997
|
runId: z.number().int().min(1),
|
|
998
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
760
999
|
batchId: z.number().int().min(1).optional(),
|
|
761
1000
|
memberId: z.number().int().min(1).optional(),
|
|
762
1001
|
inspectionEpoch: z.number().int().min(0).optional(),
|
|
@@ -764,13 +1003,19 @@ function registerApplyTools(
|
|
|
764
1003
|
outcome: z.enum(['review_ready', 'submitted', 'failed', 'blocked']),
|
|
765
1004
|
confirmation: z.enum(['user_confirmation', 'success_page']).optional(),
|
|
766
1005
|
},
|
|
767
|
-
wrapTool(async ({ runId, ...body }) =>
|
|
1006
|
+
wrapTool(async ({ runId, idempotencyKey, ...body }) => applyControlRequest(
|
|
1007
|
+
'POST',
|
|
1008
|
+
`/api/jobscout/apply/runs/${runId}/outcome`,
|
|
1009
|
+
body,
|
|
1010
|
+
idempotencyKey
|
|
1011
|
+
), 'Failed to record application outcome')
|
|
768
1012
|
);
|
|
769
1013
|
|
|
770
1014
|
server.tool(
|
|
771
1015
|
'trackly_record_application_outcomes',
|
|
772
1016
|
'Bulk-record up to 20 leased, batch-bound review or user-confirmed outcomes. Before handoff every item uses literal outcome=review_ready and every recorded run must return awaiting_manual_submit. After manual confirmation use literal outcome=submitted. Each member returns recorded or a stable conflict without hiding sibling results.',
|
|
773
1017
|
{
|
|
1018
|
+
idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
|
|
774
1019
|
outcomes: z.array(z.object({
|
|
775
1020
|
runId: z.number().int().min(1),
|
|
776
1021
|
batchId: z.number().int().min(1),
|
|
@@ -779,16 +1024,17 @@ function registerApplyTools(
|
|
|
779
1024
|
leaseToken: z.string().min(1).max(1024),
|
|
780
1025
|
outcome: z.enum(['review_ready', 'submitted', 'failed', 'blocked']),
|
|
781
1026
|
confirmation: z.enum(['user_confirmation', 'success_page']).optional(),
|
|
782
|
-
})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS)
|
|
1027
|
+
})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS).refine(
|
|
1028
|
+
(values) => new Set(values.map(({ runId }) => runId)).size === values.length,
|
|
1029
|
+
{ message: 'outcomes must contain unique runId values' }
|
|
1030
|
+
),
|
|
783
1031
|
},
|
|
784
1032
|
wrapTool(
|
|
785
|
-
async (params) =>
|
|
1033
|
+
async ({ idempotencyKey, ...params }) => applyControlRequest(
|
|
786
1034
|
'POST',
|
|
787
1035
|
'/api/jobscout/apply/outcomes/bulk',
|
|
788
1036
|
params,
|
|
789
|
-
|
|
790
|
-
false,
|
|
791
|
-
MCP_USER_AGENT
|
|
1037
|
+
idempotencyKey
|
|
792
1038
|
),
|
|
793
1039
|
'Failed to record bulk application outcomes'
|
|
794
1040
|
)
|
|
@@ -835,6 +1081,44 @@ function registerApplyTools(
|
|
|
835
1081
|
wrapTool(async ({ exactPath, originalErrno }) => diagnoseLocalPath(exactPath, { originalErrno }), 'Local path diagnosis failed')
|
|
836
1082
|
);
|
|
837
1083
|
|
|
1084
|
+
server.tool(
|
|
1085
|
+
'trackly_validate_apply_tab_keep_set',
|
|
1086
|
+
'Locally validate a caller-supplied session-finalizer keep set against complete controller and user tab inventories. This pure helper never enumerates, focuses, closes, hands off, or finalizes browser tabs and never sends tab IDs to Trackly.',
|
|
1087
|
+
{
|
|
1088
|
+
expectedTabIds: z.array(opaqueTabIdSchema).min(1).max(100),
|
|
1089
|
+
keepTabIds: z.array(opaqueTabIdSchema).max(100),
|
|
1090
|
+
controllerInventory: z.object({
|
|
1091
|
+
complete: z.boolean(),
|
|
1092
|
+
tabIds: z.array(opaqueTabIdSchema).max(1000),
|
|
1093
|
+
}).strict(),
|
|
1094
|
+
userInventory: z.object({
|
|
1095
|
+
complete: z.boolean(),
|
|
1096
|
+
tabIds: z.array(opaqueTabIdSchema).max(1000),
|
|
1097
|
+
}).strict(),
|
|
1098
|
+
},
|
|
1099
|
+
wrapTool(async (params) => validateApplyTabKeepSet(params), 'Apply tab keep-set validation failed')
|
|
1100
|
+
);
|
|
1101
|
+
|
|
1102
|
+
server.tool(
|
|
1103
|
+
'trackly_validate_apply_resume_upload',
|
|
1104
|
+
'Locally validate the browser adapter capabilities and ordered, value-free proof stages required to claim one resume attachment. This helper does not open a chooser, read a local path, control the browser, or send values to Trackly.',
|
|
1105
|
+
{
|
|
1106
|
+
capabilities: z.object({
|
|
1107
|
+
semanticControlDiscovery: z.boolean(),
|
|
1108
|
+
chooserArming: z.boolean(),
|
|
1109
|
+
fileAttachment: z.boolean(),
|
|
1110
|
+
committedFilenameInspection: z.boolean(),
|
|
1111
|
+
parserFieldRecheck: z.boolean(),
|
|
1112
|
+
}).strict(),
|
|
1113
|
+
events: z.array(z.object({
|
|
1114
|
+
stage: z.enum(APPLY_UPLOAD_STAGES),
|
|
1115
|
+
outcome: z.enum(['passed', 'failed']),
|
|
1116
|
+
failureCode: z.enum(APPLY_UPLOAD_FAILURE_CODES).optional(),
|
|
1117
|
+
}).strict()).max(APPLY_UPLOAD_STAGES.length),
|
|
1118
|
+
},
|
|
1119
|
+
wrapTool(async (params) => validateApplyResumeUpload(params), 'Apply resume upload validation failed')
|
|
1120
|
+
);
|
|
1121
|
+
|
|
838
1122
|
server.tool(
|
|
839
1123
|
'trackly_verify_prepared_resume',
|
|
840
1124
|
'Immediately before attachment, recompute the prepared resume fingerprint, validate its run and expiration, and lock the confirmed file read-only.',
|
|
@@ -857,11 +1141,20 @@ function registerApplyTools(
|
|
|
857
1141
|
messages: [{
|
|
858
1142
|
role: 'user',
|
|
859
1143
|
content: { type: 'text', text: APPLY_RELIABILITY_PROMPT },
|
|
1144
|
+
}, {
|
|
1145
|
+
role: 'user',
|
|
1146
|
+
content: { type: 'text', text: 'Only active=true identifies resumable execution work. Active=false and preserved=true is terminal read-only reconciliation evidence.' },
|
|
1147
|
+
}, {
|
|
1148
|
+
role: 'user',
|
|
1149
|
+
content: {
|
|
1150
|
+
type: 'text',
|
|
1151
|
+
text: 'Protocol 3.6.0 reliability gate for new work: require MCP contract 3.7.1 and skill 4.5.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.',
|
|
1152
|
+
},
|
|
860
1153
|
}, {
|
|
861
1154
|
role: 'user',
|
|
862
1155
|
content: {
|
|
863
1156
|
type: 'text',
|
|
864
|
-
text: '
|
|
1157
|
+
text: 'Legacy protocol compatibility gate: require the fetched compatibleSkillMinimumVersion or newer for all new work and use protocol 3.6.0 as specified above. Read the Apply protocol first. Only protocol 3.5 or newer with the compact-snapshot capability may call trackly_get_apply_execution_snapshot or the parked-member resume and execution-resume approval tools. An already-active protocol 3.4 execution is read-only legacy recovery: use only its published get or stop tools and never mutate its browser forms. Only when the fetched protocol is 3.4 or newer call trackly_get_active_apply_execution before legacy batch recovery, including when accessible execution is disabled. For protocol 3.3, skip the execution endpoint and recover the already-active immutable fixed batch directly; protocol 3.2 remains valid only for an already-active explicit legacy single run. A disabled rollout may preserve an active execution: recover it read-only and use only get or stop tools until the capability is enabled; never start, advance, or record dispositions while disabled. If disabled and no execution is active, use the legacy fixed-batch path. Recover every entry in execution.unresolvedWaves in ascending waveOrder; an older unresolved wave remains part of recovery after a replacement wave exists, and execution.currentWave is only the latest scheduling identity, never the complete recovery set. For “fill/apply to the next N,” recover or start one complete_next_n_accessible execution with target 1–20 and follow only the server nextAction and authoritative funnel. If the requested N differs from the active target, explain the mismatch, obtain explicit confirmation, stop the old execution with reason target_changed, refetch its terminal state, then start the new target. If an immutable fixed batch is active when the user requests complete_next_n_accessible, explain the incompatible mode and summarize any review-ready, submitted, or unresolved work before browser mutation. Resume that exact fixed batch when the user chooses to finish it. If the user instead says to start fresh, leave, replace, discard, or otherwise abandon the old batch, treat that statement as explicit cancellation confirmation: refetch the latest batch revision, call trackly_cancel_apply_batch with reason user_requested_restart and a fresh idempotency key, refetch until no active fixed batch remains, preserve every existing browser tab without mutation, and start the requested accessible execution in the same turn. Never wait for batch expiry and never create a scheduled continuation merely to escape an obsolete batch. If cancellation reports submission_in_progress, preserve everything and stop for the user; do not cancel or start replacement work. If the user asks to stop, call trackly_stop_apply_execution with reason user_requested and refetch its terminal state. Continue immutable child waves from the original recent-first snapshot until the authoritative backend funnel says targetReached, the queue is exhausted, or the user stops. Treat achievementCount and target-capped completed as the cumulative target authority. durablyReviewReady and submitted are current operator projections only; never add them together locally to reconstruct completion. Accessible drafts awaiting answers and forms currently being filled occupy target slots; authentication, account creation, OTP, pre-form CAPTCHA, exclusions, manual-only, conflicts, and revocations do not. Record only typed value-free live-probe dispositions. Advance only when no current-wave member remains unclassified queued or inspecting. Never calculate replacements or progress locally. For an explicit “inspect the next N records” request, use the existing fixed immutable batch and never replenish it; if a different accessible execution is active, confirm the intent change with the user, stop that execution with reason target_changed, refetch its terminal state, then recover or create the fixed batch. A cache hint may prioritize a live minimal non-mutating probe but never authorizes private-data entry or replaces that probe. After a redirect or contradictory observation, report only the fresh live disposition with its exact binding and let the backend invalidate its own hint. Preserve every user-edited or unknown non-empty field through the local provenance ledger. Never submit.',
|
|
865
1158
|
},
|
|
866
1159
|
}, {
|
|
867
1160
|
role: 'user',
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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.14.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.14.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.14.0",
|
|
21
21
|
"packages": [
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "trackly-cli",
|
|
25
|
-
"version": "0.
|
|
25
|
+
"version": "0.14.0",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"runtimeArguments": [
|
|
28
28
|
{
|
|
@@ -27,7 +27,7 @@ references define the server-frozen batch, grouped-question, tab-reconciliation,
|
|
|
27
27
|
and missing-surface recovery contract; they are mandatory even for a one-job batch.
|
|
28
28
|
Read [references/legal-clarifications.md](references/legal-clarifications.md) when a form asks for a legal, regulatory, consent, or employer-relationship decision.
|
|
29
29
|
|
|
30
|
-
1. Call `trackly_get_apply_protocol`. Skill 4.
|
|
30
|
+
1. Call `trackly_get_apply_protocol`. Skill 4.5.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
31
|
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
32
|
3. Save answers with `trackly_update_application_profile`:
|
|
33
33
|
- Use `answered`, `intentionally_blank`, `declined`, or `unknown` exactly.
|
|
@@ -44,7 +44,9 @@ Read [references/legal-clarifications.md](references/legal-clarifications.md) wh
|
|
|
44
44
|
- Use `writing.em_dash_policy` and `writing.optional_question_policy` when the fetched schema exposes them. An unanswered em-dash policy fails closed to `forbid` for form writing.
|
|
45
45
|
- Infer a direct prior-employer answer of No only when the profile explicitly marks history complete, the target and backend-confirmed aliases are absent, and the question is limited to direct employment. Ask for subsidiary, affiliate, acquisition, contractor, or otherwise ambiguous relationships.
|
|
46
46
|
4. Require the one-time profile confirmation, complete education entries, and default-resume metadata before browser work. Do not prepare or upload the resume when the form has no attachment control.
|
|
47
|
-
5. For every protocol 3.4 or newer request, call `trackly_get_active_apply_execution` before legacy recovery. Resume execution work only when the response says `active: true`. A response with `preserved: true` and `active: false` exposes a stopped, closed, or otherwise terminal execution only for read-only reconciliation. Never fetch its compact snapshot, continue its wave, mutate its members, or interpret historical `currentlyFilling` or reservation counts as current work; require `progress.nextAction: none`. Only with protocol 3.5 or newer and an enabled compact-snapshot capability may an active recovery or new execution call `trackly_get_apply_execution_snapshot`, using only the current member IDs, profile keys needed by the visible forms, and actual browser surface. An already-active protocol 3.4 execution is read-only legacy recovery: use only its published get or stop operations, never call the 3.5-only snapshot, resume, approval, advance, or disposition tools, and never mutate its browser forms. For protocol 3.5 or newer, treat snapshot `mutable` and `allowedOperations` values as authoritative. Never mutate or reopen authentication, account-creation, OTP, pre-form-CAPTCHA, or manual-only members. Only an explicit user request may call `trackly_resume_parked_apply_member`; the returned member still requires a fresh non-mutating probe before private data or form mutation. Resume unresolved waves in ascending order only for `active: true` and obey the server `nextAction` and funnel. Never reconstruct progress or choose replacements locally.
|
|
47
|
+
5. For every protocol 3.4 or newer request, call `trackly_get_active_apply_execution` before legacy recovery. Resume execution work only when the response says `active: true`. A response with `preserved: true` and `active: false` exposes a stopped, closed, or otherwise terminal execution only for read-only reconciliation. Never fetch its compact snapshot, continue its wave, mutate its members, or interpret historical `currentlyFilling` or reservation counts as current work; require `progress.nextAction: none`. Only with protocol 3.5 or newer and an enabled compact-snapshot capability may an active recovery or new execution call `trackly_get_apply_execution_snapshot`, using only the current member IDs, profile keys needed by the visible forms, and actual browser surface. An already-active protocol 3.4 execution is read-only legacy recovery: use only its published get or stop operations, never call the 3.5-only snapshot, resume, approval, advance, or disposition tools, and never mutate its browser forms. For protocol 3.5 or newer, treat snapshot `mutable` and `allowedOperations` values as authoritative. Never mutate or reopen authentication, account-creation, OTP, pre-form-CAPTCHA, or manual-only members. Only an explicit user request may call `trackly_resume_parked_apply_member`; the returned member still requires a fresh non-mutating probe before private data or form mutation. Resume unresolved waves in ascending order only for `active: true` and obey the server `nextAction` and funnel. Never reconstruct progress or choose replacements locally. Treat backend `achievementCount`, target-capped `completed`, `targetReached`, and `nextAction` as authoritative. `durablyReviewReady` and `submitted` remain current operator projections and must never be added together by the client to reconstruct cumulative completion. Let the backend own reservations, capacity, attempted-job deduplication, immutable waves, and advancement. Continue until `targetReached`, `queueExhausted`, a blocking `nextAction`, or the user stops.
|
|
48
|
+
- 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
|
+
- 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.
|
|
48
50
|
- 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.
|
|
49
51
|
- Recover every entry in `execution.unresolvedWaves` in ascending `waveOrder`; `currentWave` identifies only the latest scheduling wave, not the complete recovery set.
|
|
50
52
|
- Immediately consume the start response's authoritative `progress` and `nextAction`; do not issue a speculative advance first.
|
|
@@ -69,7 +71,7 @@ Read [references/legal-clarifications.md](references/legal-clarifications.md) wh
|
|
|
69
71
|
- 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.
|
|
70
72
|
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.
|
|
71
73
|
- 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.
|
|
72
|
-
- 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.
|
|
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.5.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.
|
|
73
75
|
- 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.
|
|
74
76
|
7. Pass the browser readiness gate before preparing resume bytes:
|
|
75
77
|
- Use semantic browser control through Codex in-app browser controls, Chrome MCP/extension browser control, or Claude in Chrome.
|
|
@@ -79,7 +81,7 @@ Read [references/legal-clarifications.md](references/legal-clarifications.md) wh
|
|
|
79
81
|
- After a handoff, context resume, or browser-control interruption, reclaim and re-verify every mapped tab before continuing.
|
|
80
82
|
- Report `observationType: browser_ready` for the current run with the exact current `batchId`, `memberId`, and `inspectionEpoch`, plus `scenarioCode: browser_reclaim`, the allowed `browserSurface`, `committed: true`, and that `browserBindingHash`. Do not call `trackly_prepare_resume` until this same-run attestation succeeds. When preparation is permitted, bind it to the exact run ID, browser surface, and browser binding hash. Accessibility may provide an independent verification signal, but coordinate-only clicking is forbidden for form completion.
|
|
81
83
|
- If the semantic browser bridge is unavailable, preserve every existing run and tab mapping, record the blocker when possible, and stop before any upload or form mutation.
|
|
82
|
-
8. If and only if the application offers or requires a resume attachment, read [references/browser-upload.md](references/browser-upload.md). For an accessible execution, call `trackly_approve_apply_execution_resume` once after the user approves the exact unchanged resume and original snapshot. Reuse only that content approval within the unchanged execution. For every run, still call `trackly_prepare_resume` with the exact run ID, browser surface, and binding hash and immediately verify the exact path, hash, size, and expiration before upload. If hosted MCP reports local preparation unavailable, use local Trackly MCP or manual upload. If no attachment control exists, skip steps 8–11 and do not report `resume_upload` as exercised.
|
|
84
|
+
8. If and only if the application offers or requires a resume attachment, read [references/browser-upload.md](references/browser-upload.md). For an accessible execution, call `trackly_approve_apply_execution_resume` once after the user approves the exact unchanged resume and original snapshot. Reuse only that content approval within the unchanged execution. For every run, still call `trackly_prepare_resume` with the exact run ID, browser surface, and binding hash and immediately verify the exact path, hash, size, and expiration before upload. Require the browser upload capability gate and the local `trackly_validate_apply_resume_upload` stage proof before claiming attachment success. If hosted MCP reports local preparation unavailable, use local Trackly MCP or manual upload. If no attachment control exists, skip steps 8–11 and do not report `resume_upload` as exercised.
|
|
83
85
|
9. Preserve the user’s filename returned by `trackly_prepare_resume`. Internal cache identifiers belong only in private parent directories and must never appear in the employer-facing upload filename.
|
|
84
86
|
10. Before any upload, let the user inspect the exact prepared file returned by `trackly_prepare_resume`:
|
|
85
87
|
- Prefer an inline visual preview. Otherwise open the exact local file in Quick Look or Preview.app.
|
|
@@ -138,6 +140,12 @@ Every required scenario and both universal evidence scenarios must have correspo
|
|
|
138
140
|
|
|
139
141
|
## Review handoff
|
|
140
142
|
|
|
143
|
+
After context loss, call `trackly_list_apply_review_handoffs` for the execution
|
|
144
|
+
before interpreting any grouped submission statement. Use the named receipt or
|
|
145
|
+
the sole active receipt returned by Trackly; if multiple receipts remain, ask
|
|
146
|
+
the user to identify the intended group. Never guess a receipt from tabs or
|
|
147
|
+
chat history.
|
|
148
|
+
|
|
141
149
|
After every conditional field is settled and all other review gates pass, bulk checkpoint every ready member with `review/manual_submit`, the current unchanged inspection epoch in both epoch fields, and any `resolvedActionIds`. Use the returned member versions and epochs; this checkpoint resolves the listed human actions and moves the durable member lifecycle to `review_ready` without invalidating current-epoch browser evidence or an unchanged resume-content approval. Do not wait for `needs_input` members: whenever at least one member is durably `review_ready`, ask for one explicit final truthfulness confirmation for the exact complete subset that is currently `review_ready`. If any form in that certified subset used a resume attachment, certify with `resumeDependency: approved` and the exact approved resume identity. If no form in the subset exposes a resume control, certify with `resumeDependency: not_applicable` and omit the resume ID and hash; do not invent an upload requirement. Then call `trackly_record_application_outcomes` once with every member in that certified subset using its exact current `runId`, `batchId`, `memberId`, `inspectionEpoch`, and `leaseToken`; every item must use the literal `outcome: review_ready`. Verify every recorded run returns `awaiting_manual_submit` before showing the review handoff. A member conflict does not invalidate successful siblings: preserve its tab, refresh that member once, and surface the returned stable conflict code instead of blindly replaying a different transition. A member that becomes ready later requires a fresh certification for the then-current complete `review_ready` subset; never reuse an older certification for it. Use singular `trackly_record_application_outcome` only for a legacy single run or an isolated post-review transition. Use the normal review block in [references/review-handoff.md](references/review-handoff.md) only after documented visibility proof for the exact review tab succeeds. If visibility is unverified, preserve the tab, use the separate visibility-unverified block, and do not tell the user to submit until the tab is reclaimed and visibly proven. For a single run, keep the browser on the final review state and stop. For a frozen batch, preserve every review-ready tab, hand off each certified review-ready subset without waiting on unrelated human actions, and continue the same lifecycle for the remaining mapped members. Members with unresolved actions stay frozen and resumable. Never submit any member. End with job ID -> run ID -> browser tab -> ATS -> status plus the actual scenario coverage for each run.
|
|
142
150
|
|
|
143
151
|
After the user submits manually:
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Batch orchestration
|
|
2
2
|
|
|
3
3
|
Protocol 3.4 adds a server-owned Apply execution above immutable child batches.
|
|
4
|
+
Protocol 3.6 adds exact-member recovery after local context loss and scoped
|
|
5
|
+
review-handoff reconciliation.
|
|
6
|
+
After context loss, rediscover active handoff receipts with
|
|
7
|
+
`trackly_list_apply_review_handoffs`; never reconstruct or guess a handoff ID
|
|
8
|
+
from chat history, tab order, or application similarity.
|
|
4
9
|
The execution is the target-completion ledger; each child batch remains the
|
|
5
10
|
auditable browser-work unit. Protocol 3.3 fixed batches and protocol 3.2 single
|
|
6
11
|
runs remain recovery-only compatibility paths.
|
|
@@ -57,6 +62,35 @@ its top-level `response.batchId`. Never guess across those response shapes. If
|
|
|
57
62
|
the applicable field is null or `unresolvedWaves` is empty, follow
|
|
58
63
|
`response.progress.nextAction` rather than guessing a prior batch.
|
|
59
64
|
|
|
65
|
+
## Exact recovery after local context loss
|
|
66
|
+
|
|
67
|
+
When no active execution exists but unfinished lineage may still be retained,
|
|
68
|
+
call `trackly_list_recoverable_apply_executions`. The returned source snapshot
|
|
69
|
+
and candidates are the complete bounded recovery menu. Resolve every distinct
|
|
70
|
+
returned `jobId` through `trackly_get_job`, bind that Trackly-owned record to
|
|
71
|
+
its `candidateId`, and present its company, role, requisition identity when
|
|
72
|
+
available, and source execution to the user. A missing or mismatched Trackly
|
|
73
|
+
job record blocks confirmation. Require explicit confirmation of the exact
|
|
74
|
+
candidate set. Never infer the set from browser tabs, conversation memory,
|
|
75
|
+
search results, page copy, employer similarity, or queue position.
|
|
76
|
+
|
|
77
|
+
Call `trackly_recover_exact_apply_members` with one source execution, its exact
|
|
78
|
+
snapshot hash, the confirmed candidate IDs, and a fresh idempotency key. The
|
|
79
|
+
response may never replace candidates. Verify that `assertedCandidateIds`,
|
|
80
|
+
`eligibleCandidateIds`, and the eligibility candidate IDs each contain exactly
|
|
81
|
+
the user-confirmed set with no duplicates before browser work.
|
|
82
|
+
|
|
83
|
+
Recovery has three independent proof dimensions:
|
|
84
|
+
|
|
85
|
+
1. the exact browser tab was restored;
|
|
86
|
+
2. the employer form state was restored or safely reconstructed; and
|
|
87
|
+
3. Trackly granted current mutation authority.
|
|
88
|
+
|
|
89
|
+
Never let one imply another. Rebind each recovered surface, accept the new
|
|
90
|
+
inspection epoch, inventory the form, preserve every user-edited and unknown
|
|
91
|
+
non-empty field, and rerun required integrity evidence. All prior-epoch
|
|
92
|
+
browser, upload, field, review, and submission evidence is stale.
|
|
93
|
+
|
|
60
94
|
## Replace an obsolete fixed batch
|
|
61
95
|
|
|
62
96
|
An active legacy fixed batch and a new accessible execution are mutually
|
|
@@ -92,6 +92,13 @@ Before resuming, handing off, or closing a tab:
|
|
|
92
92
|
5. Reclaim a matching tab only after employer, role, requisition, HTTPS origin,
|
|
93
93
|
ATS tenant when applicable, run ID, and browser binding all revalidate.
|
|
94
94
|
|
|
95
|
+
Immediately before a session-finalizer call, pass the complete expected and
|
|
96
|
+
keep sets plus complete controller and user inventories through
|
|
97
|
+
`trackly_validate_apply_tab_keep_set`. Continue only when it returns
|
|
98
|
+
`safeToFinalize: true`. This helper is a local validator, not a browser
|
|
99
|
+
controller: it does not discover, focus, close, hand off, or finalize tabs.
|
|
100
|
+
Unrelated tabs may remain in the inventories. Raw tab IDs stay local.
|
|
101
|
+
|
|
95
102
|
For every initial tab and every recovery, call `trackly_bind_apply_surface`
|
|
96
103
|
with the existing frozen member and run. Use `initial_binding` for the first
|
|
97
104
|
surface and `recovery_binding` after a missing tab or handoff. The returned URL
|
|
@@ -253,6 +260,13 @@ When an incomplete member's tab is missing:
|
|
|
253
260
|
revalidate the exact requisition before recording any outcome and never
|
|
254
261
|
activate Submit again.
|
|
255
262
|
|
|
263
|
+
When protocol 3.6 exact-member recovery created the current execution, prove
|
|
264
|
+
three facts separately in the operator receipt: whether the tab was restored,
|
|
265
|
+
whether the employer form state was restored or reconstructed, and whether
|
|
266
|
+
current mutation authority was reacquired. A recovered tab alone never grants
|
|
267
|
+
write authority. A recovered server member alone never proves the draft or tab
|
|
268
|
+
survived.
|
|
269
|
+
|
|
256
270
|
If the recovered origin or identity differs, semantic controls are
|
|
257
271
|
unobservable, or access is blocked by credentials or a pre-form challenge,
|
|
258
272
|
stop private-data mutation and create the appropriate human action. Continue
|
|
@@ -1,11 +1,43 @@
|
|
|
1
|
-
# Browser upload
|
|
1
|
+
# Browser upload
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Resume approval proves the content the user authorized. Attachment proof proves
|
|
4
|
+
that the browser committed that exact file. Neither fact substitutes for the
|
|
5
|
+
other.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
2. Open its file chooser through the browser adapter.
|
|
7
|
-
3. Use the adapter's advertised `setFiles` capability with the immediately verified local path.
|
|
8
|
-
4. Verify the employer-facing filename exactly matches the prepared user-facing filename and contains no cache identifier.
|
|
9
|
-
5. Recheck contact and parser-modified fields after the upload settles.
|
|
7
|
+
## Capability gate
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
Before opening a file chooser, require the active browser adapter to advertise
|
|
10
|
+
all five capabilities: semantic control discovery, chooser arming, file
|
|
11
|
+
attachment, committed-filename inspection, and parser-field recheck. If any is
|
|
12
|
+
missing, fail closed before opening the chooser and hand the upload to the user. Never
|
|
13
|
+
navigate to a `file://` URL or paste a local path into an ordinary text field.
|
|
14
|
+
|
|
15
|
+
Perform these stages in exact order:
|
|
16
|
+
|
|
17
|
+
1. identify the exact semantic Resume or CV control;
|
|
18
|
+
2. arm the file chooser before clicking that control;
|
|
19
|
+
3. prove the chooser opened;
|
|
20
|
+
4. run `trackly_verify_prepared_resume`, then immediately attach that verified
|
|
21
|
+
file through the adapter's documented file-setting primitive;
|
|
22
|
+
5. verify the employer-facing filename committed; and
|
|
23
|
+
6. recheck every contact, employment, education, and other field that resume
|
|
24
|
+
parsing may have changed.
|
|
25
|
+
|
|
26
|
+
Pass only the value-free stage outcomes to
|
|
27
|
+
`trackly_validate_apply_resume_upload`. Do not pass a filename, local path,
|
|
28
|
+
resume hash, form value, URL, page text, or tab identifier. Claim attachment
|
|
29
|
+
success only when it returns `safeToClaimAttachment: true`.
|
|
30
|
+
|
|
31
|
+
Stable failure codes include capability unavailable, ambiguous control,
|
|
32
|
+
chooser timeout, chooser opened without a commit, missing or expired file,
|
|
33
|
+
file-setting failure, uncommitted filename, unexpected file navigation,
|
|
34
|
+
unsettled parser, and parser-field regression. A failure is a handoff or retry
|
|
35
|
+
decision, never permission to guess another control or weaken verification.
|
|
36
|
+
|
|
37
|
+
Snapshot field provenance immediately before upload. Mark parser changes as
|
|
38
|
+
`parser_filled`; preserve a later user edit byte-for-byte. After ledger loss,
|
|
39
|
+
preserve every unknown non-empty value. Never let parser rechecks or the final
|
|
40
|
+
required-field sweep overwrite user-owned or unknown external values.
|
|
41
|
+
|
|
42
|
+
The employer-facing filename must remain the user's confirmed filename.
|
|
43
|
+
Random cache identifiers may appear only in private parent directories.
|
|
@@ -15,6 +15,53 @@ Funnel: target=; ready=; submitted=; filling=; awaitingAnswer=; authParked=; exc
|
|
|
15
15
|
|
|
16
16
|
Use the compact execution snapshot as authority. Give a bounded next-update estimate, not an invented overall completion time.
|
|
17
17
|
|
|
18
|
+
## Deterministic operator receipts
|
|
19
|
+
|
|
20
|
+
Every handoff names the browser surface and authoritative funnel. Use the
|
|
21
|
+
matching template; do not improvise internal routing language.
|
|
22
|
+
|
|
23
|
+
### Recovery
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
Recovery in progress — not submitted
|
|
27
|
+
Completed: exact Trackly source and user-confirmed candidate set recovered
|
|
28
|
+
Tab restored: yes/no
|
|
29
|
+
Form state restored: yes/no
|
|
30
|
+
Current mutation authority: yes/no
|
|
31
|
+
Unresolved:
|
|
32
|
+
Your next action: wait / inspect / answer
|
|
33
|
+
Resume: exact approved resume / no attachment / manual upload required
|
|
34
|
+
Browser surface:
|
|
35
|
+
Funnel:
|
|
36
|
+
My next action:
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Upload requires the user
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
Application preserved — resume upload requires you
|
|
43
|
+
Completed:
|
|
44
|
+
Unresolved: browser surface cannot prove the required attachment stages
|
|
45
|
+
Your next action: attach the exact approved resume and confirm the committed filename
|
|
46
|
+
Resume: exact approved resume
|
|
47
|
+
Browser surface:
|
|
48
|
+
Funnel:
|
|
49
|
+
My next action: recheck parser-modified fields, finish integrity sweep, stop at review
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Submission reconciled
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
Submission confirmed and reconciled
|
|
56
|
+
Completed: success evidence recorded; member=submitted; job=applied_confirmed
|
|
57
|
+
Unresolved: tab cleanup pending / none
|
|
58
|
+
Your next action: none
|
|
59
|
+
Resume: exact approved resume / no attachment
|
|
60
|
+
Browser surface:
|
|
61
|
+
Funnel:
|
|
62
|
+
My next action: verify authorized tab disposition only
|
|
63
|
+
```
|
|
64
|
+
|
|
18
65
|
## Approval scope receipt
|
|
19
66
|
|
|
20
67
|
Before persisting a broad statement such as “always,” show which separate categories it will update: personal facts, consent choices, writing preferences, resume approval, and truthfulness certification. Never let approval of one category authorize another. Consolidate genuinely unknown questions into one packet after filling everything already known. Explain legal terms in plain language using the protocol glossary; do not expose internal action codes or routing labels.
|
|
@@ -141,5 +188,45 @@ open for manual submission, and list remaining human actions separately.
|
|
|
141
188
|
Members that become ready later require a new certification for the exact
|
|
142
189
|
then-current `review_ready` subset.
|
|
143
190
|
|
|
191
|
+
## Group submission reconciliation
|
|
192
|
+
|
|
193
|
+
A broad statement such as “I submitted all of them” applies only to one
|
|
194
|
+
explicit review-handoff receipt. Call `trackly_list_apply_review_handoffs` for
|
|
195
|
+
the execution, then resolve it to the receipt the user names or to the sole
|
|
196
|
+
active receipt returned by Trackly. If multiple receipts could
|
|
197
|
+
match, ask the user to choose; never fan the statement across executions or
|
|
198
|
+
tabs.
|
|
199
|
+
|
|
200
|
+
Inspect every member in the selected receipt and classify exactly one of:
|
|
201
|
+
`detected`, `user_confirmed`, `unresolved`, or `contradictory`. Use
|
|
202
|
+
provider-positive evidence only:
|
|
203
|
+
|
|
204
|
+
- Greenhouse: a same-requisition thank-you page, confirmation route, or
|
|
205
|
+
semantic success state;
|
|
206
|
+
- Ashby: a same-requisition explicit application-success banner or page,
|
|
207
|
+
including when the route does not change; and
|
|
208
|
+
- Lever: a same-requisition confirmation redirect or semantic success state.
|
|
209
|
+
|
|
210
|
+
An unchanged URL or page title is never evidence that submission failed. Build
|
|
211
|
+
any local `DetectedSubmission` only from a versioned provider strategy, the
|
|
212
|
+
handoff/member identity, current inspection epoch, evidence type, confidence,
|
|
213
|
+
and a value-free fingerprint derived from those identifiers. Never hash DOM
|
|
214
|
+
text, URLs, local paths, or raw tab IDs into that record.
|
|
215
|
+
|
|
216
|
+
If a rediscovered receipt is `partially_reconciled`, preserve every member that
|
|
217
|
+
already has a stored reconciliation result and inspect only members whose
|
|
218
|
+
stored result remains `unresolved`. Do not replay the original claim with a
|
|
219
|
+
partial member list: the claim receipt is an immutable exact-set idempotency
|
|
220
|
+
record. Record later positive evidence and submitted outcomes through the
|
|
221
|
+
normal per-member outcome path, naming the receipt and only its unresolved
|
|
222
|
+
members in any follow-up confirmation.
|
|
223
|
+
|
|
224
|
+
Call `trackly_claim_apply_review_handoff` with the exact receipt and a
|
|
225
|
+
classification for every member. Write submission evidence and outcomes only
|
|
226
|
+
for `detected` and `user_confirmed`. Ask one consolidated question containing
|
|
227
|
+
only the `unresolved` members. Preserve every `contradictory` member and its
|
|
228
|
+
tab without an outcome. Refetch and require member `submitted` plus job
|
|
229
|
+
`applied_confirmed` before any authorized closure.
|
|
230
|
+
|
|
144
231
|
Do not include restricted answers in chat unless needed for the user’s review.
|
|
145
232
|
Never include credentials, an OTP, or a CAPTCHA response.
|