trackly-cli 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 | 55 local MCP tools
38
+ 3,800+ companies | 170K+ jobs | 40+ ATS types | CLI + MCP | 58 local MCP tools
39
39
 
40
40
  ## CLI Commands
41
41
 
@@ -189,7 +189,10 @@ Then use natural language in any of these clients:
189
189
  | trackly_get_apply_execution_snapshot | Fetch a compact bounded projection for current execution members and required profile keys |
190
190
  | trackly_resume_parked_apply_member | Explicitly resume one parked member for a fresh non-mutating access probe |
191
191
  | trackly_approve_apply_execution_resume | Approve one exact resume identity for an unchanged execution snapshot |
192
- | trackly_advance_apply_execution | Transactionally create the next eligible immutable wave |
192
+ | trackly_advance_apply_execution | Transactionally create the next eligible immutable wave with frozen accessKnowledge |
193
+ | trackly_list_apply_access_deferments | List persistent user access deferments |
194
+ | trackly_defer_apply_access | Persist a job or company deferment from a Trackly jobId |
195
+ | trackly_clear_apply_access_deferment | Clear one discovered user access deferment |
193
196
  | trackly_record_apply_execution_dispositions | Record typed, value-free access classifications |
194
197
  | trackly_stop_apply_execution | Stop an execution without changing saved-job state |
195
198
  | trackly_create_apply_batch | Freeze an exact recent-first approved batch |
@@ -282,7 +285,7 @@ trackly config --base-url http://127.0.0.1:3000 # Point at a different backend
282
285
  | Job search + filters | Yes | Yes | Yes |
283
286
  | Apply/save/dismiss | Yes | Yes | Yes |
284
287
  | AI-powered search | Yes (trackly ask) | Yes | Yes |
285
- | MCP integration | Yes (55 local tools) | -- | -- |
288
+ | MCP integration | Yes (58 local tools) | -- | -- |
286
289
  | Browser required | No | Yes | No |
287
290
  | Best for | Terminal + AI agents | Visual browsing | Custom integrations |
288
291
 
@@ -296,7 +299,7 @@ Install trackly-cli (`npm install -g trackly-cli`), authenticate with `trackly l
296
299
 
297
300
  **What MCP servers exist for job searching?**
298
301
 
299
- trackly-cli includes a built-in MCP server with 55 tools: the complete Trackly
302
+ trackly-cli includes a built-in MCP server with 58 tools: the complete Trackly
300
303
  job-search and application set plus `get_more_tools`, which lets agents report
301
304
  a missing capability. Run `trackly mcp` or use
302
305
  `trackly agent setup --client claude`.
package/bin/trackly CHANGED
@@ -15,7 +15,7 @@ const {
15
15
  removeCredentials,
16
16
  saveConfig,
17
17
  } = require('../lib/client');
18
- const { color, isJSON, outputJobs, outputCompanies, outputStats, outputContacts, outputReferralCampaign, outputNetworkBrief, outputCompanyBrief, outputCompanyWorkspace, outputError } = require('../lib/formatters');
18
+ const { color, isJSON, jobDateDetailLines, outputJobs, outputCompanies, outputStats, outputContacts, outputReferralCampaign, outputNetworkBrief, outputCompanyBrief, outputCompanyWorkspace, outputError } = require('../lib/formatters');
19
19
  const http = require('http');
20
20
  const crypto = require('crypto');
21
21
  const { spawnSync } = require('child_process');
@@ -665,7 +665,7 @@ async function cmdJob(args) {
665
665
  console.log(`Company: ${color('blue', job.companyName || (job.company && job.company.name) || '')}`);
666
666
  if (job.location) console.log(`Location: ${job.location}`);
667
667
  if (job.jobUrl) console.log(`URL: ${color('dim', job.jobUrl)}`);
668
- if (job.postedAt) console.log(`Posted: ${job.postedAt}`);
668
+ for (const line of jobDateDetailLines(job)) console.log(line);
669
669
  // Field presence, not truthiness: a returned null/'unknown' status is a real
670
670
  // "Not mentioned" answer from the backend. Only an older backend that omits
671
671
  // the key entirely should render no Sponsorship line at all.
@@ -23,5 +23,20 @@
23
23
  },
24
24
  "3.7.3": {
25
25
  "contractSha256": "d37e7ec0a17d3dba399f12dd7281c51536ca92bbb21125ca09288d1678f34c14"
26
+ },
27
+ "3.7.4": {
28
+ "contractSha256": "b9da194f5d2a02afda339b271a2c5ed8e78b538b3d95ba8df54b14408bf49d8f"
29
+ },
30
+ "3.7.5": {
31
+ "contractSha256": "13997fd0496035bfa2db1d8ba9b52ff326f880e8bea3231a3c9a7fec26ddc1a7"
32
+ },
33
+ "3.7.6": {
34
+ "contractSha256": "259f4c0b52739956fced3936e36792b27960dcad820659994f3e9107453a5844"
35
+ },
36
+ "3.8.0": {
37
+ "contractSha256": "5404c2f210b584d4f1327d34e9753530c56a9dd963ea9524081d0d00b229df2e"
38
+ },
39
+ "3.8.1": {
40
+ "contractSha256": "49028b8a8e1d75a8db6e54dd91001195f8a2e50b1f0ff6554750bd243dde4c6e"
26
41
  }
27
42
  }
@@ -1,15 +1,20 @@
1
1
  {
2
- "contractVersion": "3.7.3",
2
+ "contractVersion": "3.8.1",
3
3
  "constants": {
4
+ "applyExecutionProtocolVersion": "trackly-apply-execution/v3",
5
+ "applyAccessKnowledgeOrderingVersion": 3,
4
6
  "applyExecutionMaxTarget": 20,
5
7
  "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"],
6
8
  "applyBrowserSurfaces": ["codex_in_app", "chrome_extension", "claude_in_chrome"],
7
9
  "applyCheckpointActionCodes": ["answer/unknown", "auth/sign_in", "auth/account_creation", "auth/otp", "captcha/before_form", "captcha/at_submit", "artifact/upload_required", "legal/decision_required", "consent/decision_required", "review/manual_submit", "trust/origin_mismatch", "observability/unverifiable_state"],
10
+ "applyCheckpointContinuationByAction": {"answer/unknown": true, "auth/sign_in": true, "auth/account_creation": true, "auth/otp": true, "captcha/before_form": true, "captcha/at_submit": false, "artifact/upload_required": true, "legal/decision_required": true, "consent/decision_required": true, "review/manual_submit": false, "trust/origin_mismatch": false, "observability/unverifiable_state": false},
11
+ "applyCheckpointLifecycleByAction": {"answer/unknown": "needs_input", "auth/sign_in": "needs_input", "auth/account_creation": "needs_input", "auth/otp": "needs_input", "captcha/before_form": "needs_input", "captcha/at_submit": "review_ready", "artifact/upload_required": "needs_input", "legal/decision_required": "needs_input", "consent/decision_required": "needs_input", "review/manual_submit": "review_ready", "trust/origin_mismatch": "blocked", "observability/unverifiable_state": "blocked"},
12
+ "applyCheckpointQuestionPacketByAction": {"answer/unknown": true, "auth/sign_in": false, "auth/account_creation": false, "auth/otp": false, "captcha/before_form": false, "captcha/at_submit": false, "artifact/upload_required": false, "legal/decision_required": true, "consent/decision_required": true, "review/manual_submit": false, "trust/origin_mismatch": false, "observability/unverifiable_state": false},
8
13
  "applyAccessClassifications": ["accessible", "authentication_required", "account_creation_required", "otp_required", "captcha_before_form", "captcha_at_submit", "manual_only", "unknown_unobservable"],
9
14
  "applyExecutionDispositionSources": ["live_probe"],
10
15
  "applyProbeCleanupPreferences": ["never", "submitted_only", "submitted_and_probe_blockers"],
11
16
  "applyExecutionStopReasonCodes": ["user_requested", "target_changed", "session_ended", "execution_restarted", "operator_stop"],
12
- "applyBatchConflictCodes": ["state_changed", "lease_unavailable", "idempotency_key_reused", "fixed_batch_not_found", "execution_child_batch", "fixed_batch_not_active", "fixed_batch_revision_changed", "submission_in_progress", "attestation_dependencies_changed", "legacy_fixed_batch_active", "dedicated_recovery_required"],
17
+ "applyBatchConflictCodes": ["state_changed", "lease_unavailable", "idempotency_key_reused", "fixed_batch_not_found", "execution_child_batch", "fixed_batch_not_active", "fixed_batch_revision_changed", "submission_in_progress", "attestation_dependencies_changed", "legacy_fixed_batch_active", "dedicated_recovery_required", "recovery_migration_not_ready", "access_deferment_limit_reached", "access_deferments_active"],
13
18
  "fixedApplyBatchCancelReasonCodes": ["user_requested_restart"],
14
19
  "applyCheckpointPacketPhases": ["first_pass", "delta"],
15
20
  "applySurfaceBindingReasons": ["initial_binding", "recovery_binding"],
@@ -20,14 +25,48 @@
20
25
  "applyPositiveSubmissionStrategyCodes": ["greenhouse_confirmation_redirect", "greenhouse_semantic_success", "ashby_semantic_success_banner", "ashby_submitted_state", "lever_confirmation_redirect", "lever_semantic_confirmation"],
21
26
  "applyExecutionRecoveryEligibilityCodes": ["recoverable", "already_applied", "inactive", "revoked", "changed_requisition", "concurrently_owned", "lineage_unavailable", "retention_expired"],
22
27
  "applyHandoffReconciliationClassifications": ["detected", "user_confirmed", "unresolved", "contradictory"],
28
+ "applyObservedAccessClassifications": ["open", "account_wall", "varies", "unknown"],
29
+ "applyAccessKnowledgeSources": ["curated_audit", "live_probe", "none"],
30
+ "applyAccessFreshnessStates": ["fresh", "stale", "unknown"],
31
+ "applySchedulingEffects": ["prefer", "neutral", "defer_while_alternatives_exist", "defer_until_cleared"],
32
+ "applyAccessDefermentScopes": ["job", "company", "provider"],
33
+ "applyAccessMatchedScopes": ["job", "company", "provider_tenant", "origin", "route", "ats_default"],
23
34
  "applyUploadStages": ["semantic_control_discovered", "file_chooser_armed", "file_chooser_opened", "set_files_succeeded", "user_facing_filename_committed", "parser_fields_rechecked"],
24
35
  "applyUploadFailureCodes": ["upload_capability_unavailable", "upload_stage_missing", "upload_stage_out_of_order", "upload_stage_duplicate", "control_ambiguous", "chooser_timeout", "chooser_open_without_commit", "file_missing", "file_expired", "set_files_failed", "filename_uncommitted", "unexpected_file_navigation", "parser_unsettled", "parser_field_regression"]
25
36
  },
26
37
  "toolInputInvariants": {
38
+ "trackly_checkpoint_apply_batch": {
39
+ "questionAndNonQuestionActionsMutuallyExclusiveForNewCheckpoints": true,
40
+ "exactReplayOfPreviouslyAcceptedPayloadPreserved": true,
41
+ "preFormAuthenticationWallsRejectedAfterExactReplayLookup": true
42
+ },
27
43
  "trackly_get_apply_execution_snapshot": {
28
44
  "officeProjectionMemberIdsMustExistInMemberIds": true
45
+ },
46
+ "trackly_advance_apply_execution": {
47
+ "accessReviewApprovalJobIdsMustBeUnique": true,
48
+ "accessReviewApprovalRequiresClearedPersonalDeferment": true,
49
+ "sameKeyReplayReturnsIdenticalProposedWaveAndRationale": true,
50
+ "proposedWaveIncludesFrozenIdentity": true,
51
+ "approvalReceiptBindsFrozenIdentity": true
52
+ },
53
+ "trackly_defer_apply_access": {
54
+ "serverDerivesIdentityFromJobId": true,
55
+ "agentsCannotSubmitUrlsOrRawChat": true,
56
+ "providerScopeAppliesAcrossCompanies": true
57
+ },
58
+ "trackly_clear_apply_access_deferment": {
59
+ "clearRequiresDiscoveredDefermentId": true,
60
+ "idempotentReplayPreserved": true,
61
+ "clearReceiptIncludesClearedAt": true,
62
+ "clearReceiptPersistsUntilClearedFalse": true
29
63
  }
30
64
  },
65
+ "schemaDigests": {
66
+ "applyCheckpointActionVariant": "db6b69924b791b3e50c4c7464519fa1a786c7b39ed86c3cc9482e18c211553d2",
67
+ "applyCheckpointActionSchema": "99baf25b27ae4bd41183bf47f8e1eb7522e1d471156f6090a784362d5b248e95",
68
+ "applyCheckpointSchema": "101c5d7606cb902e89e677773fa45fbbdc9b38de99b452d641806bac90004274"
69
+ },
31
70
  "tools": {
32
71
  "trackly_get_apply_queue": "{limit:z.number().int().min(1).max(100).optional(),cursor:z.string().min(1).max(2048).optional()}",
33
72
  "trackly_get_application_profile": "{includeSensitive:z.boolean().optional(),provider:z.string().max(100).optional(),companyId:z.string().max(100).optional(),jurisdiction:iso3166Alpha2Schema.optional(),office:officeScopeSchema.optional()}",
@@ -43,7 +82,10 @@
43
82
  "trackly_get_apply_execution_snapshot": "{executionId:z.number().int().min(1),memberIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values).size===values.length,{message:'memberIdsmustbeunique'}),profileKeys:z.array(z.string().min(1).max(200)).max(100).refine((values)=>newSet(values).size===values.length,{message:'profileKeysmustbeunique'}).optional(),officeProjections:z.array(z.object({memberId:z.number().int().min(1),office:officeScopeSchema,profileKeys:z.array(z.string().min(1).max(200)).min(1).max(100).refine((values)=>newSet(values).size===values.length,{message:'officeprofileKeysmustbeunique'})}).strict()).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values.map(({memberId})=>memberId)).size===values.length,{message:'officeProjectionsmustcontainuniquememberIdvalues'}).optional(),browserSurface:z.enum(APPLY_BROWSER_SURFACES)}",
44
83
  "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)}",
45
84
  "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)}",
46
- "trackly_advance_apply_execution": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
85
+ "trackly_advance_apply_execution": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),accessReviewApproval:z.object({jobIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values).size===values.length,{message:'jobIdsmustbeunique'}),approvalHash:z.string().regex(/^[a-f0-9]{64}$/)}).strict().optional()}",
86
+ "trackly_list_apply_access_deferments": "{}",
87
+ "trackly_defer_apply_access": "{jobId:z.number().int().min(1),scope:z.enum(APPLY_ACCESS_DEFERMENT_SCOPES),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
88
+ "trackly_clear_apply_access_deferment": "{defermentId:z.number().int().min(1),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
47
89
  "trackly_record_apply_execution_dispositions": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),dispositions:z.array(applyExecutionDispositionSchema).min(1).max(APPLY_EXECUTION_MAX_TARGET)}",
48
90
  "trackly_stop_apply_execution": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),reasonCode:z.enum(APPLY_EXECUTION_STOP_REASON_CODES).optional()}",
49
91
  "trackly_create_apply_batch": "{limit:z.number().int().min(1).max(100),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
@@ -51,7 +93,7 @@
51
93
  "trackly_get_active_apply_batch": "{limit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),cursor:z.string().min(1).max(2048).optional(),actionLimit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),actionCursor:z.string().min(1).max(2048).optional()}",
52
94
  "trackly_get_apply_batch": "{batchId:z.number().int().min(1),limit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),cursor:z.string().min(1).max(2048).optional(),actionLimit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),actionCursor:z.string().min(1).max(2048).optional()}",
53
95
  "trackly_claim_apply_batch": "{batchId:z.number().int().min(1),expectedRevision:z.number().int().min(1),leaseOwner:z.string().min(1).max(1024),leaseToken:z.string().min(1).max(1024),leaseDurationMs:z.number().int().min(15000).max(300000)}",
54
- "trackly_checkpoint_apply_batch": "{batchId:z.number().int().min(1),leaseToken:z.string().min(1).max(1024),checkpoints:z.array(z.object({memberId:z.number().int().min(1),runId:z.number().int().min(1),expectedMemberVersion:z.number().int().min(1),expectedInspectionEpoch:z.number().int().min(0),inspectionEpoch:z.number().int().min(0),packetPhase:z.enum(APPLY_CHECKPOINT_PACKET_PHASES).optional(),knownFieldsCommitted:z.boolean(),resolvedActionIds:z.array(z.string().regex(/^[1-9][0-9]*$/)).max(APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT).optional(),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),actions:z.array(z.object({actionCode:z.enum(APPLY_CHECKPOINT_ACTION_CODES),continuationAllowed:z.boolean(),fieldFingerprint:z.string().regex(/^[a-f0-9]{64}$/).optional()})).min(1).max(APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT)})).min(1).max(APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST)}",
96
+ "trackly_checkpoint_apply_batch": "{batchId:z.number().int().min(1),leaseToken:z.string().min(1).max(1024),checkpoints:z.array(applyCheckpointSchema).min(1).max(APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST)}",
55
97
  "trackly_bind_apply_surface": "{batchId:z.number().int().min(1),memberId:z.number().int().min(1),runId:z.number().int().min(1),expectedMemberVersion:z.number().int().min(1),expectedInspectionEpoch:z.number().int().min(0),leaseToken:z.string().min(1).max(1024),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/),browserSurface:z.enum(APPLY_BROWSER_SURFACES),adapterCode:z.string().regex(SAFE_OBSERVATION_CODE),bindingReason:z.enum(APPLY_SURFACE_BINDING_REASONS),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
56
98
  "trackly_record_apply_surface_evidence": "{batchId:z.number().int().min(1),memberId:z.number().int().min(1),runId:z.number().int().min(1),expectedMemberVersion:z.number().int().min(1),expectedInspectionEpoch:z.number().int().min(1),leaseToken:z.string().min(1).max(1024),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/),browserSurface:z.enum(APPLY_BROWSER_SURFACES),adapterCode:z.string().regex(SAFE_OBSERVATION_CODE),ownershipState:z.enum(APPLY_SURFACE_OWNERSHIP_STATES),completeInventory:z.boolean(),evidenceType:z.enum(APPLY_SURFACE_EVIDENCE_TYPES),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
57
99
  "trackly_record_apply_submission_evidence": "{batchId:z.number().int().min(1),memberId:z.number().int().min(1),runId:z.number().int().min(1),expectedMemberVersion:z.number().int().min(1),expectedInspectionEpoch:z.number().int().min(1),leaseToken:z.string().min(1).max(1024),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/),evidenceType:z.enum(APPLY_SUBMISSION_EVIDENCE_TYPES),evidenceSource:z.enum(APPLY_SUBMISSION_EVIDENCE_SOURCES),evidenceFingerprint:z.string().regex(/^[a-f0-9]{64}$/),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
@@ -0,0 +1,88 @@
1
+ ---
2
+ title: Keep destructive confirmation guards at the mutation boundary
3
+ date: 2026-08-28
4
+ category: integration-issues
5
+ module: Trackly Apply sensitive revocation
6
+ problem_type: security_issue
7
+ component: service_layer
8
+ symptoms:
9
+ - A stale trackly-cli 0.10.0 MCP server could revoke sensitive-storage consent without presenting the new confirmation challenge
10
+ - The backend accepted an unconfirmed destructive request because confirmation was enforced only by caller-side tool handlers
11
+ - The bypass deleted 33 encrypted application-profile answers before a recovery archive existed
12
+ root_cause: missing_validation
13
+ resolution_type: code_fix
14
+ severity: critical
15
+ related_components: [assistant, api_layer]
16
+ tags: [trackly-apply, sensitive-revocation, destructive-action, version-skew, backend-authority, mcp, data-loss]
17
+ ---
18
+
19
+ # Keep destructive confirmation guards at the mutation boundary
20
+
21
+ ## Problem
22
+
23
+ Sensitive-answer deletion originally used a two-step confirmation guard in the hosted and local MCP callers. That protected only clients running the new code. During verification, a stale local MCP server running trackly-cli 0.10.0 sent an unconfirmed revocation request directly to the backend, which accepted it and permanently deleted 33 encrypted answers before a recovery archive existed.
24
+
25
+ This was a trust-boundary failure, not merely a missed client upgrade. Any old, alternate, or defective caller could omit a caller-owned safety check while still reaching the destructive service operation.
26
+
27
+ ## Symptoms
28
+
29
+ - A destructive request from an older installed client bypassed the newly shipped caller-side guard. The incident transcript records the resulting loss of 33 encrypted answers; [trackly-cli PR #90](https://github.com/trackly-app/trackly-cli/pull/90) independently records stale CLI 0.10.0 as the failure mode.
30
+ - The superseded implementation in [trackly-cli PR #89](https://github.com/trackly-app/trackly-cli/pull/89) computed the affected-key inventory and confirmation token in the client before sending the mutation.
31
+ - Review found that a deterministic caller-computed token was an interaction speed bump, not a server-enforced consent boundary: an automated caller could derive or echo it without proving that a person saw the affected keys.
32
+
33
+ ## What Didn't Work
34
+
35
+ The first implementation mirrored safety logic into local and hosted MCP handlers. The client recognized a revocation, fetched a profile, computed a challenge, and withheld the API request until the supplied token matched.
36
+
37
+ That implementation could be thoroughly tested and still fail globally. A previously installed client had neither the preflight read nor the challenge branch, and the backend could not distinguish its unconfirmed request from an approved one. Duplicating the guard also forced callers to mirror the deletion predicate, token rules, revision behavior, and error shape. Those copies could drift independently from the mutation they were meant to protect.
38
+
39
+ Adding more checks to every caller would not close the version-skew hole. Compatibility checks may make obsolete clients inconvenient to use, but they are not an authorization boundary for a reachable backend endpoint.
40
+
41
+ ## Solution
42
+
43
+ Move the mandatory challenge-and-confirm invariant to the backend operation that performs the deletion, then keep MCP callers as thin protocol adapters. The backend implementation is outside this repository; internal/private [close-ai PR #1296](https://github.com/trackly-app/close-ai/pull/1296) and public [trackly-cli PR #90](https://github.com/trackly-app/trackly-cli/pull/90) record the service-authoritative design.
44
+
45
+ The current CLI publishes `sensitiveRevocationConfirmToken` in the tool schema and forwards the complete request to the application-profile endpoint:
46
+
47
+ ```js
48
+ wrapTool(async (params) => {
49
+ return applyApiRequest(
50
+ 'PATCH',
51
+ '/api/jobscout/application-profile',
52
+ params,
53
+ false,
54
+ false,
55
+ MCP_USER_AGENT,
56
+ );
57
+ }, 'Failed to update application profile')
58
+ ```
59
+
60
+ The handler does not compute, strip, or validate its own copy of the guard (`mcp/apply-tools.js`). The MCP error mapper preserves the service's structured HTTP 409 challenge (`mcp/server.js`). The regression test in `test/mcp-schema.test.js` verifies that the CLI publishes the token field, relays the service challenge, forwards the confirmed token, and leaves ordinary opt-in as a single service round trip.
61
+
62
+ PRs #89 and #90 are both merged into the default branch. PR #89 is important history because it demonstrates why the caller-owned design was insufficient; PR #90 is the corrective CLI design.
63
+
64
+ ## Why This Works
65
+
66
+ Every backend-classified agent caller, including stale CLI and MCP versions, must cross the backend mutation boundary. A stale agent client may have a worse experience or be unable to complete a newer flow, but it cannot make the backend omit its own destructive-operation check. First-party UI clients intentionally retain their own single-call confirmation flows.
67
+
68
+ The separation also removes semantic duplication. The backend owns the authoritative deletion decision, confirmation validity, revision binding, archive behavior, and mutation. MCP owns discoverability and transport: publish the token field, explain the two-step interaction, preserve the full request, and relay the service response.
69
+
70
+ Internal/private [close-ai issue #1314](https://github.com/trackly-app/close-ai/issues/1314) remains administratively open, but current backend main appears to satisfy its scoped-inventory acceptance criteria through merged internal/private [close-ai PR #1369](https://github.com/trackly-app/close-ai/pull/1369): the service inventories sensitive and restricted rows across scopes, serializes scope identity into `affectedKeys`, and uses the same user-and-sensitivity predicate for archival and deletion. Close the issue only after confirming the fix is deployed and behaviorally verified; an open tracker alone is not evidence of a current functional gap.
71
+
72
+ ## Prevention
73
+
74
+ - Treat destructive confirmation as a backend invariant and enforce it in the authoritative operation or transaction that changes the data.
75
+ - Keep first-party dialogs and MCP challenge descriptions for user understanding, but never count them as the only protection.
76
+ - Make callers forward the full confirmed request, and test the exact service-bound body.
77
+ - Relay authoritative conflict payloads without regenerating the decision in the caller.
78
+ - Test across the real trust boundary. A caller-only test proves the newest caller behaves correctly; it cannot prove an older caller is safe.
79
+ - During coordinated releases, deploy and verify the server guard before removing caller-side defense in depth.
80
+ - For every destructive control, ask what happens when the request comes from last month's client. If safety depends on that client running new code, the invariant is in the wrong layer.
81
+
82
+ ## Related Issues
83
+
84
+ - [trackly-cli PR #89](https://github.com/trackly-app/trackly-cli/pull/89) — initial caller-owned guard, retained as the superseded attempt
85
+ - [trackly-cli PR #90](https://github.com/trackly-app/trackly-cli/pull/90) — thin caller relay and service-authoritative correction
86
+ - Internal/private [close-ai PR #1296](https://github.com/trackly-app/close-ai/pull/1296) — backend enforcement recorded by the coordinated fix
87
+ - Internal/private [close-ai issue #1314](https://github.com/trackly-app/close-ai/issues/1314) and [close-ai PR #1369](https://github.com/trackly-app/close-ai/pull/1369) — tracker still open; current main contains the scoped-inventory fix pending deployment/behavior confirmation
88
+ - [Durable exact recovery after browser state loss](durable-exact-recovery-after-browser-state-loss.md) — related Trackly Apply authority-boundary learning
@@ -115,7 +115,10 @@ API-key creation attempts.
115
115
  - **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.
116
116
  - **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.
117
117
  - **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.
118
- - **trackly_advance_apply_execution** — Transactionally select the next wave from the execution's original recent-first snapshot for the declared `browserSurface`. Same-key retries return current authoritative progress and the current execution revision.
118
+ - **trackly_advance_apply_execution** — Transactionally select the next wave from the execution's original recent-first snapshot for the declared `browserSurface`. Returns the immutable `proposedWave` with each member's frozen order, job ID, company, title, provider, requisition URL, and value-free `accessKnowledge` receipt. Same-key retries return identical identity, order, and rationale. Optional hash-bound `accessReviewApproval` probes exact deferred job IDs after personal deferments are cleared.
119
+ - **trackly_list_apply_access_deferments** — List the current user's persistent job/company/provider access deferments. Returns only scope identities; never provider names, URLs, or raw text.
120
+ - **trackly_defer_apply_access** — Persist an explicit user deferment for one Trackly `jobId` at `job`, `company`, or `provider` scope. The server derives company, provider, tenant, origin, and route; provider scope applies across companies, recovery, and replay.
121
+ - **trackly_clear_apply_access_deferment** — Clear one deferment listed or created in the current MCP session. Exact same-session replay is idempotent, including for provider-wide user policy; after restart, refresh the active list instead of replaying an undiscovered cleared ID.
119
122
  - **trackly_record_apply_execution_dispositions** — Record typed, value-free live-probe classifications for the current wave. Every item requires `jobId`, one allowed `classification`, `source: 'live_probe'`, and the exact `batchId`, `memberId`, `runId`, `expectedMemberVersion`, `expectedInspectionEpoch`, and `browserSurface`; cache/static scheduling records are server-owned and cannot be submitted through MCP.
120
123
  - **trackly_stop_apply_execution** — Stop an execution with optimistic revision and idempotency guards.
121
124
  - **trackly_create_apply_batch** — Freeze an exact recent-first set of approved jobs with an idempotency key.
@@ -145,6 +148,12 @@ API-key creation attempts.
145
148
 
146
149
  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.
147
150
 
151
+ Version 3.8.1 adds access-aware scheduling receipts, `nextAction: access_review`,
152
+ hash-bound deferred-probe approval, and job/company/provider deferment tools.
153
+ Provider scope is derived from a stable job anchor and applies across companies,
154
+ approval, recovery, and replay. Historical OPEN never authorizes form filling;
155
+ every selected job still requires a fresh non-mutating live probe.
156
+
148
157
  Version 3.7 adds public exact-member recovery and scoped review-handoff claims,
149
158
  plus local tab-finalizer and resume-upload proof validators. Recovery candidates
150
159
  are bounded and value-free. The local helpers perform no browser actions and
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.7.0';
11
+ const SKILL_VERSION = '4.8.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.6.0';
17
+ const MIN_APPLY_PROTOCOL_VERSION = '3.7.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;
package/lib/formatters.js CHANGED
@@ -44,6 +44,107 @@ function formatFundingLine(company) {
44
44
  return parts.length > 0 ? parts.join(' | ') : null;
45
45
  }
46
46
 
47
+ function validCalendarDay(value) {
48
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
49
+ const parsed = new Date(`${value}T00:00:00.000Z`);
50
+ return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value
51
+ ? null
52
+ : value;
53
+ }
54
+
55
+ function isoCalendarDay(value) {
56
+ if (typeof value !== 'string' || value.trim() === '') return null;
57
+ const normalized = value.trim();
58
+ const sourceDay = validCalendarDay(normalized.slice(0, 10));
59
+ if (!sourceDay) return null;
60
+ const parsed = new Date(normalized);
61
+ return Number.isNaN(parsed.getTime()) ? null : sourceDay;
62
+ }
63
+
64
+ function sourceCalendarDay(job, kind) {
65
+ const meta = kind === 'posted'
66
+ ? job && job.sourceDateMeta && job.sourceDateMeta.original
67
+ : kind === 'reposted'
68
+ ? job && job.sourceDateMeta && job.sourceDateMeta.repost
69
+ : null;
70
+ return meta && meta.precision === 'day'
71
+ ? validCalendarDay(meta.calendarDay)
72
+ : null;
73
+ }
74
+
75
+ function resolveJobDateBundle(job = {}) {
76
+ const projectedKind = ['posted', 'reposted', 'found'].includes(job.displayDateKind)
77
+ ? job.displayDateKind
78
+ : null;
79
+ const projectedDay = projectedKind
80
+ ? sourceCalendarDay(job, projectedKind) || isoCalendarDay(job.displayDate)
81
+ : null;
82
+ const originalDay = sourceCalendarDay(job, 'posted') || isoCalendarDay(job.originalPostedAt);
83
+ const repostDay = sourceCalendarDay(job, 'reposted') || isoCalendarDay(job.repostedAt)
84
+ || (projectedKind === 'reposted' ? projectedDay : null);
85
+ const legacyPostedDay = isoCalendarDay(job.postedAt || job.posted_at);
86
+ const foundDay = isoCalendarDay(job.firstSeenAt || job.first_seen_at);
87
+ const presentation = projectedKind && projectedDay
88
+ ? {
89
+ label: { posted: 'Posted', reposted: 'Reposted', found: 'Found' }[projectedKind],
90
+ value: projectedDay,
91
+ }
92
+ : legacyPostedDay
93
+ ? { label: 'Posted', value: legacyPostedDay }
94
+ : { label: 'Found', value: foundDay || 'unknown' };
95
+ return {
96
+ projectedKind,
97
+ projectedDay,
98
+ originalDay,
99
+ repostDay,
100
+ legacyPostedDay,
101
+ foundDay,
102
+ presentation,
103
+ };
104
+ }
105
+
106
+ function jobDatePresentation(job = {}) {
107
+ return resolveJobDateBundle(job).presentation;
108
+ }
109
+
110
+ function jobDateDetailLines(job = {}) {
111
+ const lines = [];
112
+ const {
113
+ projectedKind,
114
+ projectedDay,
115
+ originalDay,
116
+ repostDay,
117
+ legacyPostedDay,
118
+ foundDay,
119
+ presentation,
120
+ } = resolveJobDateBundle(job);
121
+ if (originalDay) lines.push(`Posted: ${originalDay}`);
122
+ else if (repostDay && legacyPostedDay) lines.push(`Posted: ${legacyPostedDay}`);
123
+ else if (repostDay) lines.push('Original posting date unavailable');
124
+ else if (projectedKind === 'posted' && projectedDay) lines.push(`Posted: ${projectedDay}`);
125
+ else if ((!projectedKind || !projectedDay) && legacyPostedDay) {
126
+ lines.push(`Posted: ${legacyPostedDay}`);
127
+ }
128
+ if (foundDay) lines.push(`Found: ${foundDay}`);
129
+ else if (lines.length === 0 && !originalDay && !repostDay) {
130
+ if (presentation.value !== 'unknown') {
131
+ lines.push(`${presentation.label}: ${presentation.value}`);
132
+ }
133
+ }
134
+ if (repostDay) lines.push(`Reposted: ${repostDay}`);
135
+ const requirement = sourceRequirement(job);
136
+ if (requirement) lines.push(`Req: ${requirement}`);
137
+ return lines;
138
+ }
139
+
140
+ function sourceRequirement(job = {}) {
141
+ const reference = job.sourceReference;
142
+ return reference && reference.label === 'Req' && reference.disambiguatesTitle === true
143
+ && typeof reference.value === 'string' && reference.value.trim()
144
+ ? reference.value.trim()
145
+ : null;
146
+ }
147
+
47
148
  function outputJobs(jobs) {
48
149
  if (isJSON()) return console.log(JSON.stringify(jobs, null, 2));
49
150
  if (!jobs || jobs.length === 0) {
@@ -57,7 +158,10 @@ function outputJobs(jobs) {
57
158
  console.log(` ${color('blue', companyName)} | ${location}`);
58
159
  const fundingLine = formatFundingLine(job.company);
59
160
  if (fundingLine) console.log(` ${color('magenta', fundingLine)}`);
60
- console.log(` ID: ${color('dim', job.id)} | Posted: ${color('dim', job.postedAt || job.firstSeenAt || 'unknown')}`);
161
+ const date = jobDatePresentation(job);
162
+ const requirement = sourceRequirement(job);
163
+ const requirementText = requirement ? ` | Req: ${color('dim', requirement)}` : '';
164
+ console.log(` ID: ${color('dim', job.id)} | ${date.label}: ${color('dim', date.value)}${requirementText}`);
61
165
  if (job.jobUrl) console.log(` ${color('dim', job.jobUrl)}`);
62
166
  console.log();
63
167
  }
@@ -330,4 +434,4 @@ function outputError(msg) {
330
434
  console.error(color('red', `Error: ${msg}`, process.stderr));
331
435
  }
332
436
 
333
- module.exports = { color, shouldColor, isJSON, outputJobs, outputCompanies, outputStats, outputContacts, outputReferralCampaign, outputNetworkBrief, outputCompanyBrief, outputCompanyWorkspace, outputError };
437
+ module.exports = { color, shouldColor, isJSON, jobDatePresentation, jobDateDetailLines, outputJobs, outputCompanies, outputStats, outputContacts, outputReferralCampaign, outputNetworkBrief, outputCompanyBrief, outputCompanyWorkspace, outputError };