trackly-cli 0.14.2 → 0.16.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 +2 -1
- package/bin/trackly +41 -3
- package/contracts/trackly-apply-contract-history.json +6 -0
- package/contracts/trackly-apply-tools.json +9 -4
- package/docs/trackly-tools.md +1 -0
- package/lib/agent.js +1 -1
- package/mcp/analytics.js +3 -3
- package/mcp/apply-tools.js +47 -9
- package/mcp/server.js +19 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/trackly-apply/SKILL.md +6 -4
- package/skills/trackly-apply/references/answer-compounding.md +30 -4
- package/skills/trackly-apply/references/answer-resolution.md +123 -0
- package/skills/trackly-apply/references/application-writing.md +7 -0
- package/skills/trackly-apply/references/batch-orchestration.md +26 -0
- package/skills/trackly-apply/references/form-integrity.md +5 -2
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ trackly jobs --region us # Filter by region (us, non_us, all, or a
|
|
|
46
46
|
trackly jobs --job-type internship # Filter by employment type (full_time, internship, all)
|
|
47
47
|
trackly jobs --work-arrangement hybrid,remote # Filter by work arrangement independently
|
|
48
48
|
trackly jobs --function product # Filter by function
|
|
49
|
+
trackly jobs --sponsorship exclude_no # Visa sponsorship filter (all, exclude_no, only_yes)
|
|
49
50
|
trackly jobs --company 243 # Filter by company ID
|
|
50
51
|
trackly job 1234 # Get job details
|
|
51
52
|
trackly jobs 1234 # Alias for job details
|
|
@@ -161,7 +162,7 @@ Then use natural language in any of these clients:
|
|
|
161
162
|
|
|
162
163
|
| Tool | Description |
|
|
163
164
|
|------|-------------|
|
|
164
|
-
| trackly_search_jobs | Search and filter jobs by function, company, region, employment type, work arrangement, and status |
|
|
165
|
+
| trackly_search_jobs | Search and filter jobs by function, company, region, employment type, work arrangement, visa sponsorship, and status |
|
|
165
166
|
| trackly_get_job | Get full details for a specific job |
|
|
166
167
|
| trackly_search_companies | Semantic company search |
|
|
167
168
|
| trackly_list_companies | List all tracked companies |
|
package/bin/trackly
CHANGED
|
@@ -39,6 +39,7 @@ const VALUE_FLAGS = new Set([
|
|
|
39
39
|
'region',
|
|
40
40
|
'search',
|
|
41
41
|
'sort',
|
|
42
|
+
'sponsorship',
|
|
42
43
|
'status',
|
|
43
44
|
'url',
|
|
44
45
|
'window-days',
|
|
@@ -62,7 +63,7 @@ const COMMAND_FLAGS = {
|
|
|
62
63
|
login: [],
|
|
63
64
|
logout: [],
|
|
64
65
|
whoami: [],
|
|
65
|
-
jobs: ['function', 'company', 'region', 'job-type', 'work-arrangement', 'remote', 'status', 'sort', 'limit', 'offset', 'keywords', 'location', 'modality'],
|
|
66
|
+
jobs: ['function', 'company', 'region', 'job-type', 'work-arrangement', 'remote', 'status', 'sort', 'sponsorship', 'limit', 'offset', 'keywords', 'location', 'modality'],
|
|
66
67
|
job: [],
|
|
67
68
|
companies: ['limit', 'offset'],
|
|
68
69
|
search: [],
|
|
@@ -94,6 +95,15 @@ const STATUS_LABELS = {
|
|
|
94
95
|
dismissed: 'dismissed',
|
|
95
96
|
};
|
|
96
97
|
const WORK_ARRANGEMENT_VALUES = ['remote', 'hybrid', 'in_person', 'unspecified'];
|
|
98
|
+
// Discovery-only visa sponsorship filter (close-ai PR #1578). Mirrors the
|
|
99
|
+
// hosted MCP enum exactly. Absent or 'all' applies no filter.
|
|
100
|
+
const SPONSORSHIP_VALUES = ['all', 'exclude_no', 'only_yes'];
|
|
101
|
+
// Job-detail `sponsorshipStatus` values → user-facing copy. Anything else
|
|
102
|
+
// (including null/'unknown') reads "Not mentioned" — never "unknown".
|
|
103
|
+
const SPONSORSHIP_STATUS_LABELS = {
|
|
104
|
+
yes: 'Sponsors visa',
|
|
105
|
+
no: 'No sponsorship',
|
|
106
|
+
};
|
|
97
107
|
const JOB_FUNCTIONS = [
|
|
98
108
|
'product', 'engineering', 'design', 'data', 'marketing', 'sales', 'partnerships',
|
|
99
109
|
'finance', 'strategy', 'operations', 'people', 'legal', 'support', 'other',
|
|
@@ -324,7 +334,7 @@ function hasJobListFilters(args) {
|
|
|
324
334
|
// (which then raises the migration error) instead of cmdJob(42) silently ignoring
|
|
325
335
|
// the filter. See PR #13 Cursor Bugbot finding.
|
|
326
336
|
const deprecated = Object.keys(DEPRECATED_JOB_FLAGS);
|
|
327
|
-
const active = ['function', 'company', 'keywords', 'limit', 'region', 'job-type', 'work-arrangement', 'remote', 'offset', 'sort', 'status'];
|
|
337
|
+
const active = ['function', 'company', 'keywords', 'limit', 'region', 'job-type', 'work-arrangement', 'remote', 'offset', 'sort', 'sponsorship', 'status'];
|
|
328
338
|
return [...active, ...deprecated].some((key) => args[key] !== undefined);
|
|
329
339
|
}
|
|
330
340
|
|
|
@@ -598,6 +608,17 @@ async function cmdJobs(args) {
|
|
|
598
608
|
}
|
|
599
609
|
}
|
|
600
610
|
|
|
611
|
+
let sponsorship;
|
|
612
|
+
if (args.sponsorship !== undefined) {
|
|
613
|
+
if (typeof args.sponsorship !== 'string' || !args.sponsorship.trim()) {
|
|
614
|
+
failNow(`--sponsorship requires a value. Use: ${SPONSORSHIP_VALUES.join(', ')}.`);
|
|
615
|
+
}
|
|
616
|
+
sponsorship = args.sponsorship.trim();
|
|
617
|
+
if (!SPONSORSHIP_VALUES.includes(sponsorship)) {
|
|
618
|
+
failNow(`Invalid --sponsorship value "${sponsorship}". Use: ${SPONSORSHIP_VALUES.join(', ')}.`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
601
622
|
const params = new URLSearchParams();
|
|
602
623
|
if (args.function) params.set('jobFunction', args.function);
|
|
603
624
|
if (args.company) params.set('companyId', args.company);
|
|
@@ -607,6 +628,13 @@ async function cmdJobs(args) {
|
|
|
607
628
|
if (args.remote) params.set('usStates', 'REMOTE');
|
|
608
629
|
if (args.status) params.set('status', args.status);
|
|
609
630
|
if (args.sort) params.set('sort', args.sort);
|
|
631
|
+
if (sponsorship) {
|
|
632
|
+
params.set('sponsorship', sponsorship);
|
|
633
|
+
// The backend honors the filter only on the literal status=new discovery
|
|
634
|
+
// view — without this default, `--sponsorship exclude_no` alone would be
|
|
635
|
+
// a silent no-op. Mirrors the MCP server's behavior.
|
|
636
|
+
if (sponsorship !== 'all' && !args.status) params.set('status', 'new');
|
|
637
|
+
}
|
|
610
638
|
if (args.limit) params.set('limit', args.limit);
|
|
611
639
|
if (args.offset) params.set('offset', args.offset);
|
|
612
640
|
if (args.keywords) params.set('search', args.keywords);
|
|
@@ -638,6 +666,12 @@ async function cmdJob(args) {
|
|
|
638
666
|
if (job.location) console.log(`Location: ${job.location}`);
|
|
639
667
|
if (job.jobUrl) console.log(`URL: ${color('dim', job.jobUrl)}`);
|
|
640
668
|
if (job.postedAt) console.log(`Posted: ${job.postedAt}`);
|
|
669
|
+
// Field presence, not truthiness: a returned null/'unknown' status is a real
|
|
670
|
+
// "Not mentioned" answer from the backend. Only an older backend that omits
|
|
671
|
+
// the key entirely should render no Sponsorship line at all.
|
|
672
|
+
if (job && typeof job === 'object' && 'sponsorshipStatus' in job) {
|
|
673
|
+
console.log(`Sponsorship: ${SPONSORSHIP_STATUS_LABELS[job.sponsorshipStatus] || 'Not mentioned'}`);
|
|
674
|
+
}
|
|
641
675
|
if (typeof job.description === 'string' && job.description.length > 0) {
|
|
642
676
|
console.log();
|
|
643
677
|
console.log(color('bold', 'Description:'));
|
|
@@ -1277,7 +1311,7 @@ ${color('bold', 'Commands:')}
|
|
|
1277
1311
|
${color('cyan', 'login')} Authenticate via Google OAuth
|
|
1278
1312
|
${color('cyan', 'logout')} Clear credentials
|
|
1279
1313
|
${color('cyan', 'whoami')} Show current user
|
|
1280
|
-
${color('cyan', 'jobs')} List jobs (--function, --company <id>, --region <tag>, --job-type <type>, --work-arrangement <values>, --remote, --status, --sort, --limit, --offset, --keywords)
|
|
1314
|
+
${color('cyan', 'jobs')} List jobs (--function, --company <id>, --region <tag>, --job-type <type>, --work-arrangement <values>, --remote, --status, --sort, --sponsorship, --limit, --offset, --keywords)
|
|
1281
1315
|
${color('cyan', 'job <id>')} Get full job detail
|
|
1282
1316
|
${color('cyan', 'jobs <id>')} Alias for job <id>
|
|
1283
1317
|
${color('cyan', 'companies')} List companies
|
|
@@ -1333,6 +1367,10 @@ ${color('bold', 'Filter values:')}
|
|
|
1333
1367
|
--status new, applied_confirmed, check_later, not_interested, all
|
|
1334
1368
|
(filters by YOUR application pipeline state)
|
|
1335
1369
|
--sort newest (default), match (highest match score first, requires resume)
|
|
1370
|
+
--sponsorship all (default), exclude_no, only_yes
|
|
1371
|
+
exclude_no hides jobs that explicitly state they do not sponsor
|
|
1372
|
+
(~13% of active US jobs); only_yes keeps only jobs that explicitly
|
|
1373
|
+
state they sponsor (~2%). Most postings never state a policy.
|
|
1336
1374
|
|
|
1337
1375
|
${color('bold', 'MCP Setup:')}
|
|
1338
1376
|
Claude Code: claude mcp add --scope user trackly -- trackly mcp
|
|
@@ -17,5 +17,11 @@
|
|
|
17
17
|
},
|
|
18
18
|
"3.7.1": {
|
|
19
19
|
"contractSha256": "0eb582fcac72804347c5da53d214a67908f657f600536db7b42167350bb3f3ab"
|
|
20
|
+
},
|
|
21
|
+
"3.7.2": {
|
|
22
|
+
"contractSha256": "058dc698ff1b0722f48b9f244773c0f09286046540609c31f001fe8604b06093"
|
|
23
|
+
},
|
|
24
|
+
"3.7.3": {
|
|
25
|
+
"contractSha256": "d37e7ec0a17d3dba399f12dd7281c51536ca92bbb21125ca09288d1678f34c14"
|
|
20
26
|
}
|
|
21
27
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "3.7.
|
|
2
|
+
"contractVersion": "3.7.3",
|
|
3
3
|
"constants": {
|
|
4
4
|
"applyExecutionMaxTarget": 20,
|
|
5
5
|
"applyScenarioCodes": ["browser_reclaim", "resume_upload", "resume_parser_recheck", "semantic_boolean_commit", "custom_select_commit", "multi_step_navigation", "free_text_voice", "required_error_sweep", "final_consent", "handoff_reclaim", "critical_contact_integrity", "manual_submit_boundary", "job_identity_match"],
|
|
@@ -23,11 +23,16 @@
|
|
|
23
23
|
"applyUploadStages": ["semantic_control_discovered", "file_chooser_armed", "file_chooser_opened", "set_files_succeeded", "user_facing_filename_committed", "parser_fields_rechecked"],
|
|
24
24
|
"applyUploadFailureCodes": ["upload_capability_unavailable", "upload_stage_missing", "upload_stage_out_of_order", "upload_stage_duplicate", "control_ambiguous", "chooser_timeout", "chooser_open_without_commit", "file_missing", "file_expired", "set_files_failed", "filename_uncommitted", "unexpected_file_navigation", "parser_unsettled", "parser_field_regression"]
|
|
25
25
|
},
|
|
26
|
+
"toolInputInvariants": {
|
|
27
|
+
"trackly_get_apply_execution_snapshot": {
|
|
28
|
+
"officeProjectionMemberIdsMustExistInMemberIds": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
26
31
|
"tools": {
|
|
27
32
|
"trackly_get_apply_queue": "{limit:z.number().int().min(1).max(100).optional(),cursor:z.string().min(1).max(2048).optional()}",
|
|
28
|
-
"trackly_get_application_profile": "{includeSensitive:z.boolean().optional(),provider:z.string().max(100).optional(),companyId:z.string().max(100).optional(),jurisdiction:iso3166Alpha2Schema.optional()}",
|
|
33
|
+
"trackly_get_application_profile": "{includeSensitive:z.boolean().optional(),provider:z.string().max(100).optional(),companyId:z.string().max(100).optional(),jurisdiction:iso3166Alpha2Schema.optional(),office:officeScopeSchema.optional()}",
|
|
29
34
|
"trackly_get_profile_onboarding": "{}",
|
|
30
|
-
"trackly_update_application_profile": "{expectedRevision:z.number().int().min(1),source:z.enum(['web','ios','macos','codex','claude','mcp']).optional(),changes:z.array(z.discriminatedUnion('scope',[z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('global'),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('provider'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('company'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('jurisdiction'),scopeValue:iso3166Alpha2Schema,questionLabel:z.string().max(1000).optional()})])).max(100).optional(),education:z.array(z.object({school:z.string().min(1).max(500),degree:z.string().max(500).nullable().optional(),fieldOfStudy:z.string().max(500).nullable().optional(),gpa:z.string().max(50).nullable().optional(),startDate:z.string().max(50).nullable().optional(),endDate:z.string().max(50).nullable().optional()})).max(20).optional(),confirmProfile:z.boolean().optional(),sensitiveStorageConsent:z.boolean().optional(),sensitiveRevocationConfirmToken:z.string().regex(/^[a-f0-9]{64}$/).optional()}",
|
|
35
|
+
"trackly_update_application_profile": "{expectedRevision:z.number().int().min(1),source:z.enum(['web','ios','macos','codex','claude','mcp']).optional(),changes:z.array(z.discriminatedUnion('scope',[z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('global'),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('provider'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('company'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('jurisdiction'),scopeValue:iso3166Alpha2Schema,questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('office'),scopeValue:officeScopeSchema,questionLabel:z.string().max(1000).optional()})])).max(100).optional(),education:z.array(z.object({school:z.string().min(1).max(500),degree:z.string().max(500).nullable().optional(),fieldOfStudy:z.string().max(500).nullable().optional(),gpa:z.string().max(50).nullable().optional(),startDate:z.string().max(50).nullable().optional(),endDate:z.string().max(50).nullable().optional()})).max(20).optional(),confirmProfile:z.boolean().optional(),sensitiveStorageConsent:z.boolean().optional(),sensitiveRevocationConfirmToken:z.string().regex(/^[a-f0-9]{64}$/).optional()}",
|
|
31
36
|
"trackly_start_apply_execution": "{mode:z.literal('complete_next_n_accessible'),target:z.number().int().min(1).max(APPLY_EXECUTION_MAX_TARGET),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
32
37
|
"trackly_get_active_apply_execution": "{}",
|
|
33
38
|
"trackly_get_apply_execution": "{executionId:z.number().int().min(1)}",
|
|
@@ -35,7 +40,7 @@
|
|
|
35
40
|
"trackly_recover_exact_apply_members": "{sourceExecutionId:z.number().int().min(1),sourceSnapshotHash:z.string().regex(/^[a-f0-9]{64}$/),candidateIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values).size===values.length,{message:'candidateIdsmustbeunique'}),explicitExactSetConfirmation:z.literal(true),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
36
41
|
"trackly_list_apply_review_handoffs": "{executionId:z.number().int().min(1)}",
|
|
37
42
|
"trackly_claim_apply_review_handoff": "{handoffId:z.number().int().min(1),members:z.array(z.object({memberId:z.number().int().min(1),classification:z.enum(APPLY_HANDOFF_RECONCILIATION_CLASSIFICATION_CODES)}).strict()).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values.map(({memberId})=>memberId)).size===values.length,{message:'membersmustcontainuniquememberIdvalues'}),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
38
|
-
"trackly_get_apply_execution_snapshot": "{executionId:z.number().int().min(1),memberIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET),profileKeys:z.array(z.string().min(1).max(200)).max(100).optional(),browserSurface:z.enum(APPLY_BROWSER_SURFACES)}",
|
|
43
|
+
"trackly_get_apply_execution_snapshot": "{executionId:z.number().int().min(1),memberIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values).size===values.length,{message:'memberIdsmustbeunique'}),profileKeys:z.array(z.string().min(1).max(200)).max(100).refine((values)=>newSet(values).size===values.length,{message:'profileKeysmustbeunique'}).optional(),officeProjections:z.array(z.object({memberId:z.number().int().min(1),office:officeScopeSchema,profileKeys:z.array(z.string().min(1).max(200)).min(1).max(100).refine((values)=>newSet(values).size===values.length,{message:'officeprofileKeysmustbeunique'})}).strict()).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values.map(({memberId})=>memberId)).size===values.length,{message:'officeProjectionsmustcontainuniquememberIdvalues'}).optional(),browserSurface:z.enum(APPLY_BROWSER_SURFACES)}",
|
|
39
44
|
"trackly_resume_parked_apply_member": "{executionId:z.number().int().min(1),memberId:z.number().int().min(1),expectedRevision:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),explicitUserResume:z.literal(true),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
40
45
|
"trackly_approve_apply_execution_resume": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),originalSnapshotHash:z.string().regex(/^[a-f0-9]{64}$/),profileRevision:z.number().int().min(1),resumeId:z.number().int().min(1),resumeSha256:z.string().regex(/^[a-f0-9]{64}$/),resumeFilename:z.string().min(1).max(255),resumeSizeBytes:z.number().int().min(1),expiresAt:z.string().datetime(),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
41
46
|
"trackly_advance_apply_execution": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
package/docs/trackly-tools.md
CHANGED
|
@@ -83,6 +83,7 @@ API-key creation attempts.
|
|
|
83
83
|
- `remote` (boolean): filter to remote jobs only. Maps to `usStates=REMOTE`.
|
|
84
84
|
- `status`: your application pipeline state. Values: `new`, `applied_confirmed`, `check_later`, `not_interested`, `all`.
|
|
85
85
|
- `sort`: `newest` (default) or `match` (highest match score first; requires a resume on file). Backend rejects the deprecated values `oldest` and `company` with HTTP 400.
|
|
86
|
+
- `sponsorship`: `all` (default), `exclude_no`, `only_yes`. `exclude_no` hides jobs with a reliable explicit "does not sponsor" statement (~13% of active US jobs); `only_yes` keeps only jobs with a reliable explicit "sponsors" statement (~2%). Most postings never state a policy either way, so `only_yes` is a very narrow slice.
|
|
86
87
|
- `keywords`, `companyId`, `limit`, `offset`.
|
|
87
88
|
- **trackly_get_job** — Get full job details by ID
|
|
88
89
|
- **trackly_search_companies** — Semantic company search
|
package/lib/agent.js
CHANGED
|
@@ -8,7 +8,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
8
8
|
const { apiRequest, downloadFile, getConfigPaths, hasAuth, maintenanceOutput } = require('./client');
|
|
9
9
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
10
10
|
const { contractVersion: MCP_CONTRACT_VERSION } = require('../contracts/trackly-apply-tools.json');
|
|
11
|
-
const SKILL_VERSION = '4.
|
|
11
|
+
const SKILL_VERSION = '4.6.0';
|
|
12
12
|
const CLI_USER_AGENT = `trackly-cli/${PACKAGE_VERSION}`;
|
|
13
13
|
const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
|
|
14
14
|
|
package/mcp/analytics.js
CHANGED
|
@@ -111,11 +111,11 @@ const SAFE_ENUM_VALUES = new Set([
|
|
|
111
111
|
'unspecified', 'us', 'non_us', 'europe', 'latam', 'middle_east', 'asia', 'africa',
|
|
112
112
|
'canada', 'oceania', 'unknown', 'product', 'engineering', 'design', 'data',
|
|
113
113
|
'marketing', 'sales', 'partnerships', 'finance', 'strategy', 'operations', 'people',
|
|
114
|
-
'legal', 'support', 'other', 'newest', 'match',
|
|
114
|
+
'legal', 'support', 'other', 'newest', 'match', 'exclude_no', 'only_yes',
|
|
115
115
|
]);
|
|
116
116
|
const SAFE_ENUM_KEYS = new Set([
|
|
117
117
|
'status', 'stage', 'function', 'jobfunction', 'jobmodality', 'workarrangement',
|
|
118
|
-
'region', 'regiontag', 'locationfilter', 'sort',
|
|
118
|
+
'region', 'regiontag', 'locationfilter', 'sort', 'sponsorship',
|
|
119
119
|
]);
|
|
120
120
|
const SDK_FAILURE_WARNING = /Warning: (?:Failed to instrument server|Failed to setup tool call instrumentation|No PostHog client passed)/i;
|
|
121
121
|
|
|
@@ -474,7 +474,7 @@ function projectMcpParameters(toolName, parameters) {
|
|
|
474
474
|
const allowed = {
|
|
475
475
|
trackly_search_jobs: new Set([
|
|
476
476
|
'function', 'companyId', 'locationFilter', 'jobModality', 'workArrangements',
|
|
477
|
-
'remote', 'status', 'sort', 'limit', 'offset', 'keywords',
|
|
477
|
+
'remote', 'status', 'sort', 'sponsorship', 'limit', 'offset', 'keywords',
|
|
478
478
|
]),
|
|
479
479
|
trackly_get_job: new Set(['id']),
|
|
480
480
|
trackly_search_companies: new Set(['query', 'limit']),
|
package/mcp/apply-tools.js
CHANGED
|
@@ -40,6 +40,7 @@ const SAFE_IDEMPOTENCY_KEY = /^[\x20-\x7e]+$/;
|
|
|
40
40
|
const iso3166Alpha2Schema = z.string().regex(/^[A-Za-z]{2}$/).refine(isIso3166Alpha2, {
|
|
41
41
|
message: 'Expected an ISO 3166-1 alpha-2 country code',
|
|
42
42
|
});
|
|
43
|
+
const officeScopeSchema = z.string().regex(/^[1-9]\d*:[A-Za-z0-9][A-Za-z0-9._-]{0,150}$/);
|
|
43
44
|
const opaqueTabIdSchema = z.union([
|
|
44
45
|
z.number().int().safe(),
|
|
45
46
|
z.string().min(1).max(512).refine((value) => value.trim().length > 0, {
|
|
@@ -262,7 +263,7 @@ const startApplyRunSchema = z.object({
|
|
|
262
263
|
}
|
|
263
264
|
});
|
|
264
265
|
|
|
265
|
-
const APPLY_RELIABILITY_PROMPT = 'Protocol 3.6 / skill 4.
|
|
266
|
+
const APPLY_RELIABILITY_PROMPT = 'Protocol 3.6 / skill 4.6.0 reliability gate: recover active work first. After full local context loss, list recoverable executions, show only stable job identity, obtain explicit confirmation of the exact candidate set, and call exact-member recovery without substitutions. Treat recovered tab presence, form state, and mutation authority as three separate facts; reacquire a fresh browser binding and inspection epoch before mutation. Before resolving broad submission statements, list active review handoffs for the execution; use only an explicit receipt or the sole returned active receipt, classify every member as detected, user_confirmed, unresolved, or contradictory, and claim that exact handoff before writing outcomes. Use provider-specific positive success evidence; an unchanged URL or title is never negative evidence. Validate an exact expected browser keep set locally before finalization. For resume uploads, negotiate the browser surface capabilities, identify the semantic control, arm the chooser before clicking, attach the immediately verified file, prove the user-facing filename committed, and recheck parser-modified fields. Use compact snapshots and server-provided mutability. Preserve user-edited and unknown non-empty fields. Never reopen parked work without explicit user resumption. Never click Submit.';
|
|
266
267
|
|
|
267
268
|
function registerApplyTools(
|
|
268
269
|
server,
|
|
@@ -311,13 +312,19 @@ function registerApplyTools(
|
|
|
311
312
|
provider: z.string().max(100).optional(),
|
|
312
313
|
companyId: z.string().max(100).optional(),
|
|
313
314
|
jurisdiction: iso3166Alpha2Schema.optional(),
|
|
315
|
+
office: officeScopeSchema.optional(),
|
|
314
316
|
},
|
|
315
|
-
wrapTool(async ({ includeSensitive, provider, companyId, jurisdiction }) => {
|
|
317
|
+
wrapTool(async ({ includeSensitive, provider, companyId, jurisdiction, office }) => {
|
|
318
|
+
const normalizedCompanyId = companyId?.trim();
|
|
319
|
+
if (office && (!normalizedCompanyId || !office.startsWith(`${normalizedCompanyId}:`))) {
|
|
320
|
+
throw new Error('Office scope must match the requested companyId');
|
|
321
|
+
}
|
|
316
322
|
const qs = new URLSearchParams();
|
|
317
323
|
if (includeSensitive) qs.set('includeSensitive', 'true');
|
|
318
324
|
if (provider) qs.set('provider', provider);
|
|
319
|
-
if (
|
|
325
|
+
if (normalizedCompanyId) qs.set('companyId', normalizedCompanyId);
|
|
320
326
|
if (jurisdiction) qs.set('jurisdiction', jurisdiction);
|
|
327
|
+
if (office) qs.set('office', office);
|
|
321
328
|
return applyApiRequest('GET', `/api/jobscout/application-profile?${qs.toString()}`, null, false, false, MCP_USER_AGENT);
|
|
322
329
|
}, 'Failed to fetch application profile')
|
|
323
330
|
);
|
|
@@ -361,6 +368,11 @@ function registerApplyTools(
|
|
|
361
368
|
value: z.any().optional(), scope: z.literal('jurisdiction'), scopeValue: iso3166Alpha2Schema,
|
|
362
369
|
questionLabel: z.string().max(1000).optional(),
|
|
363
370
|
}),
|
|
371
|
+
z.object({
|
|
372
|
+
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
373
|
+
value: z.any().optional(), scope: z.literal('office'), scopeValue: officeScopeSchema,
|
|
374
|
+
questionLabel: z.string().max(1000).optional(),
|
|
375
|
+
}),
|
|
364
376
|
])).max(100).optional(),
|
|
365
377
|
education: z.array(z.object({
|
|
366
378
|
school: z.string().min(1).max(500),
|
|
@@ -535,13 +547,36 @@ function registerApplyTools(
|
|
|
535
547
|
'Fetch a compact, bounded projection for one Apply execution. Request only the current members and profile keys needed for the visible form. The response owns mutability, allowed operations, milestones, lease timing, and progress.',
|
|
536
548
|
{
|
|
537
549
|
executionId: z.number().int().min(1),
|
|
538
|
-
memberIds: z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET)
|
|
539
|
-
|
|
550
|
+
memberIds: z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET)
|
|
551
|
+
.refine((values) => new Set(values).size === values.length, {
|
|
552
|
+
message: 'memberIds must be unique',
|
|
553
|
+
}),
|
|
554
|
+
profileKeys: z.array(z.string().min(1).max(200)).max(100)
|
|
555
|
+
.refine((values) => new Set(values).size === values.length, {
|
|
556
|
+
message: 'profileKeys must be unique',
|
|
557
|
+
}).optional(),
|
|
558
|
+
officeProjections: z.array(z.object({
|
|
559
|
+
memberId: z.number().int().min(1),
|
|
560
|
+
office: officeScopeSchema,
|
|
561
|
+
profileKeys: z.array(z.string().min(1).max(200)).min(1).max(100)
|
|
562
|
+
.refine((values) => new Set(values).size === values.length, {
|
|
563
|
+
message: 'office profileKeys must be unique',
|
|
564
|
+
}),
|
|
565
|
+
}).strict()).max(APPLY_EXECUTION_MAX_TARGET)
|
|
566
|
+
.refine((values) => new Set(values.map(({ memberId }) => memberId)).size === values.length, {
|
|
567
|
+
message: 'officeProjections must contain unique memberId values',
|
|
568
|
+
}).optional(),
|
|
540
569
|
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
541
570
|
},
|
|
542
|
-
wrapTool(async ({ executionId, ...body }) =>
|
|
543
|
-
|
|
544
|
-
|
|
571
|
+
wrapTool(async ({ executionId, ...body }) => {
|
|
572
|
+
const memberIds = new Set(body.memberIds);
|
|
573
|
+
if (body.officeProjections?.some(({ memberId }) => !memberIds.has(memberId))) {
|
|
574
|
+
throw new Error('Every office projection memberId must exist in memberIds.');
|
|
575
|
+
}
|
|
576
|
+
return applyControlRequest(
|
|
577
|
+
'POST', `/api/jobscout/apply/executions/${executionId}/snapshot`, body,
|
|
578
|
+
);
|
|
579
|
+
}, 'Failed to fetch compact apply execution snapshot')
|
|
545
580
|
);
|
|
546
581
|
|
|
547
582
|
server.tool(
|
|
@@ -1162,6 +1197,9 @@ function registerApplyTools(
|
|
|
1162
1197
|
messages: [{
|
|
1163
1198
|
role: 'user',
|
|
1164
1199
|
content: { type: 'text', text: APPLY_RELIABILITY_PROMPT },
|
|
1200
|
+
}, {
|
|
1201
|
+
role: 'user',
|
|
1202
|
+
content: { type: 'text', text: 'Before generating questions or filling controls, run the skill 4.6 deterministic answer resolver. Classify every visible answer as exact_profile, safe_derivation, supported_draft, missing_fact, live_consent, or forbidden_inference; validate the control type; fill only the first three; and ask only currently visible unresolved needs. Treat the current profile revision as reusable authority, never transcript, screenshot, parser, autocomplete, or cached values. Accessible-first is a hard scheduler invariant: park known authentication, account-creation, OTP, and pre-form-CAPTCHA work without starting a draft while an accessible candidate remains.' },
|
|
1165
1203
|
}, {
|
|
1166
1204
|
role: 'user',
|
|
1167
1205
|
content: { type: 'text', text: 'Only active=true identifies resumable execution work. Active=false and preserved=true is terminal read-only reconciliation evidence.' },
|
|
@@ -1169,7 +1207,7 @@ function registerApplyTools(
|
|
|
1169
1207
|
role: 'user',
|
|
1170
1208
|
content: {
|
|
1171
1209
|
type: 'text',
|
|
1172
|
-
text: 'Protocol 3.6.0 reliability gate for new work: require MCP contract 3.7.
|
|
1210
|
+
text: 'Protocol 3.6.0 reliability gate for new work: require MCP contract 3.7.3 and skill 4.6.0. After complete local context loss, list bounded recovery candidates, obtain explicit confirmation of the exact set, and recover only that set. Treat tab recovery, form-state recovery, and mutation authority as independent. List active handoff receipts for the execution before resolving grouped submission statements; use the named receipt or the sole returned active receipt, classify every member, and claim that receipt before recording outcomes. Validate tab keep sets and resume upload stages locally. Never send raw browser values or click Submit.',
|
|
1173
1211
|
},
|
|
1174
1212
|
}, {
|
|
1175
1213
|
role: 'user',
|
package/mcp/server.js
CHANGED
|
@@ -67,6 +67,12 @@ const MCP_MISSING_DESTINATION_VALUES = [
|
|
|
67
67
|
'trackly', 'external_site', 'file', 'email', 'calendar', 'crm', 'browser', 'other',
|
|
68
68
|
];
|
|
69
69
|
|
|
70
|
+
// `sponsorship` enum matches the backend's discovery-only filter (close-ai
|
|
71
|
+
// PR #1578). Absent or 'all' applies no filter; only ~2% of active US jobs
|
|
72
|
+
// carry an explicit "sponsors" statement and ~13% an explicit "does not
|
|
73
|
+
// sponsor" one, so the remaining ~85% never state a policy either way.
|
|
74
|
+
const SPONSORSHIP_VALUES = ['all', 'exclude_no', 'only_yes'];
|
|
75
|
+
|
|
70
76
|
// `sort` enum matches backend handler at `jobscout.ts:3053` — NOT the pre-fix
|
|
71
77
|
// `newest|oldest|company` (backend rejects oldest/company with HTTP 400).
|
|
72
78
|
const SORT_VALUES = ['newest', 'match'];
|
|
@@ -254,6 +260,9 @@ function createServer() {
|
|
|
254
260
|
'Filter by YOUR application pipeline state. Not a generic job-posting status. Values: ' + STATUS_VALUES.join(', ')
|
|
255
261
|
),
|
|
256
262
|
sort: z.enum(SORT_VALUES).optional().describe('Sort order: newest (default) or match (highest match score first; requires resume). Backend rejects legacy oldest/company with HTTP 400.'),
|
|
263
|
+
sponsorship: z.enum(SPONSORSHIP_VALUES).optional().describe(
|
|
264
|
+
"Visa sponsorship filter. 'exclude_no' hides jobs with a reliable explicit \"does not sponsor\" statement (~13% of active US jobs); 'only_yes' keeps only jobs with a reliable explicit \"sponsors\" statement (~2% — most postings never state a policy either way). Default 'all' shows every job. Applies only to the discovery view: when you pass a filtering mode without `status`, status defaults to 'new' (the backend ignores the filter on tracked/pipeline views)."
|
|
265
|
+
),
|
|
257
266
|
limit: z.number().max(50).optional().describe('Max results (default 20, max 50)'),
|
|
258
267
|
offset: z.number().min(0).optional().describe('Pagination offset'),
|
|
259
268
|
keywords: z.string().max(500).optional().describe('Keyword search in title, company, or description'),
|
|
@@ -281,6 +290,16 @@ function createServer() {
|
|
|
281
290
|
if (params.remote === true) qs.set('usStates', 'REMOTE');
|
|
282
291
|
if (params.status !== undefined) qs.set('status', params.status);
|
|
283
292
|
if (params.sort !== undefined) qs.set('sort', params.sort);
|
|
293
|
+
if (params.sponsorship !== undefined) {
|
|
294
|
+
qs.set('sponsorship', String(params.sponsorship));
|
|
295
|
+
// The backend applies the filter only on the literal status=new
|
|
296
|
+
// discovery view; without this default an agent passing a filtering
|
|
297
|
+
// mode alone would get silently unfiltered results. Mirrors
|
|
298
|
+
// close-ai src/mcp/server.ts.
|
|
299
|
+
if (params.sponsorship !== 'all' && params.status === undefined) {
|
|
300
|
+
qs.set('status', 'new');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
284
303
|
if (params.limit !== undefined) qs.set('limit', String(params.limit));
|
|
285
304
|
if (params.offset !== undefined) qs.set('offset', String(params.offset));
|
|
286
305
|
if (params.keywords !== undefined) qs.set('search', params.keywords);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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.16.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.16.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.16.0",
|
|
21
21
|
"packages": [
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "trackly-cli",
|
|
25
|
-
"version": "0.
|
|
25
|
+
"version": "0.16.0",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"runtimeArguments": [
|
|
28
28
|
{
|
|
@@ -22,12 +22,13 @@ Use Trackly as the source of truth for profile answers, documents, queue decisio
|
|
|
22
22
|
|
|
23
23
|
Before selecting or resuming work, read
|
|
24
24
|
[references/batch-orchestration.md](references/batch-orchestration.md) and
|
|
25
|
-
[references/browser-lifecycle.md](references/browser-lifecycle.md).
|
|
25
|
+
[references/browser-lifecycle.md](references/browser-lifecycle.md). Before generating an employer-form question packet, also read and run
|
|
26
|
+
[references/answer-resolution.md](references/answer-resolution.md). These
|
|
26
27
|
references define the server-frozen batch, grouped-question, tab-reconciliation,
|
|
27
28
|
and missing-surface recovery contract; they are mandatory even for a one-job batch.
|
|
28
29
|
Read [references/legal-clarifications.md](references/legal-clarifications.md) when a form asks for a legal, regulatory, consent, or employer-relationship decision.
|
|
29
30
|
|
|
30
|
-
1. Call `trackly_get_apply_protocol`. Skill 4.
|
|
31
|
+
1. Call `trackly_get_apply_protocol`. Skill 4.6.0 requires protocol 3.6.0 or newer for new reliability work. Protocol 3.2 remains valid only for an already-active explicit legacy single run; an already-active explicit 3.2 single run may finish through its legacy path. Preserve the protocol's documented recovery paths for already-active older work. Require `compatibleSkillMajor: 4` and `compatibleSkillMinimumVersion` no newer than this installed skill. Reject an older or incompatible version for new work and report that the backend must finish updating or `trackly agent setup` must update the skill. Protocol 3.2 added exact-origin trust for jobs Trackly ingested directly from employer careers sources; do not recreate the retired ownership-timestamp gate in the client.
|
|
31
32
|
2. Call `trackly_get_profile_onboarding` or fetch both the profile schema and application profile. When present, render `schema.screens` in ascending `order` as one grouped question packet per screen. Within a screen, preserve category `order` and then field `order`; use each field's user-facing `rationale` when the user asks why it is needed. Ask only unknown or confirmation-needed fields and honor `consistencyRules` before submitting profile changes, resolving contradictions with the user instead of guessing. When `schema.screens` is absent on a legacy backend, fall back to the existing category-based onboarding behavior. Treat `completeness.percent` as required onboarding readiness only. Use `coverage.missingReusableKeys` to explain reusable optional gaps, while `coverage.contextualKeys` are intentionally asked only on the relevant employer form. Do not claim that 100% required completeness answers every possible application question.
|
|
32
33
|
3. Save answers with `trackly_update_application_profile`:
|
|
33
34
|
- Use `answered`, `intentionally_blank`, `declined`, or `unknown` exactly.
|
|
@@ -48,6 +49,7 @@ Read [references/legal-clarifications.md](references/legal-clarifications.md) wh
|
|
|
48
49
|
- After full local context loss with no active execution, call `trackly_list_recoverable_apply_executions`. For every distinct returned `jobId`, call `trackly_get_job`, bind that Trackly-owned job record to the returned `candidateId`, and show the user its company, role, requisition identity when available, and source-execution identity; do not identify candidates from chat, tab order, search results, or page copy. A missing or mismatched Trackly job record blocks confirmation. Require explicit confirmation of one exact candidate set, then call `trackly_recover_exact_apply_members` with that source execution, snapshot hash, and only those candidate IDs. Verify `assertedCandidateIds`, `eligibleCandidateIds`, and the eligibility candidate IDs each contain exactly the confirmed set with no duplicates. Never add substitutes, newly saved jobs, or merely similar roles.
|
|
49
50
|
- Exact recovery restores server lineage only. Treat tab restored, form state restored, and mutation authority restored as three independent receipts. Bind every recovered surface with a fresh recovery binding and inspection epoch, revalidate the full form, and preserve every user-edited or unknown non-empty value before any write. Earlier-epoch browser, field, upload, integrity, and submission evidence cannot authorize the recovered form.
|
|
50
51
|
- Every compact snapshot request must contain a non-empty list of only the current member IDs. Never request an empty or inferred all-members projection.
|
|
52
|
+
- When a visible form asks an office-scoped profile question and the exact employer office is known, request that field only through the matching member's `officeProjections` entry using the canonical `companyId:office-identity` scope. Consume only the returned `memberOfficeProfiles` entry for that same member and exact office; never move an office answer into the global profile map or reuse it for another member, employer, or office. A recovered member may reuse the answer only when Trackly proves its frozen company identity. If a legacy snapshot cannot prove that identity, preserve the form and treat the answer as unknown instead of refilling from chat or current mutable job metadata.
|
|
51
53
|
- Recover every entry in `execution.unresolvedWaves` in ascending `waveOrder`; `currentWave` identifies only the latest scheduling wave, not the complete recovery set.
|
|
52
54
|
- Immediately consume the start response's authoritative `progress` and `nextAction`; do not issue a speculative advance first.
|
|
53
55
|
- When that authoritative `nextAction` requests another wave, call `trackly_advance_apply_execution` with the actual current `browserSurface`, latest execution revision, and a fresh idempotency key.
|
|
@@ -71,7 +73,7 @@ Read [references/legal-clarifications.md](references/legal-clarifications.md) wh
|
|
|
71
73
|
- For each fixed batch member, preserve an explicit job ID -> application run ID -> browser tab mapping. Complete the full start -> conditional resume preparation/confirmation/verification when an upload control exists -> form completion -> `review_ready` lifecycle for every member. A review-ready run does not block the next member. Never submit any of them.
|
|
72
74
|
6. When the selected job or current fixed batch member has no `runId`, call `trackly_start_apply_run` with the complete `batchId`, `memberId`, `expectedMemberVersion`, `expectedInspectionEpoch`, and `leaseToken` returned by Trackly. This is the sanctioned idempotent lookup for a recovered member whose `runId` is absent; call it with the exact recovered binding and Trackly will reuse the bound active run. When the member already has a `runId`, do not call `trackly_start_apply_run` in this or any subsequent start step: reuse that exact run and bind or recover its browser surface through `trackly_bind_apply_surface` as required. Never create a replacement run because browser control was interrupted.
|
|
73
75
|
- If a bound start returns a transport failure, a non-access HTTP 5xx response, or an error explicitly marked `retryable: true`, preserve the frozen member and browser state. Refetch the active batch, renew its lease, and retry the same complete binding once. Classify the retry response independently with these same rules: route canonical `maintenance_mode` or legacy `planned_maintenance` directly through **Resume after maintenance**; surface controlled-access/request errors marked `retryable: false` and every other HTTP 4xx response unchanged; only a second transport failure, non-access HTTP 5xx response, or explicitly retryable error becomes `backend_run_start_unavailable`. Route maintenance on either attempt without consuming or relabeling the retry, and never relabel a permanent retry response as an outage. For `backend_run_start_unavailable`, continue other members and report a Trackly control-plane failure. Do not call `trackly_checkpoint_apply_batch` for this condition because start failure has not produced the required run ID. The unchanged frozen member is the durable resume point. Never switch that frozen member to an unbound legacy run or blame the employer form.
|
|
74
|
-
- Require `run.protocolVersion` to be 3.1.0 or newer and to share protocol major 3 with the fetched protocol. A new reliability execution member requires 3.6.0 or newer; already-active older work may use only the recovery path published by its fetched protocol. Never continue a pre-evidence 3.0.x run under skill 4.
|
|
76
|
+
- Require `run.protocolVersion` to be 3.1.0 or newer and to share protocol major 3 with the fetched protocol. A new reliability execution member requires 3.6.0 or newer; already-active older work may use only the recovery path published by its fetched protocol. Never continue a pre-evidence 3.0.x run under skill 4.6.0. Preserve that run instead of starting a replacement, record it `blocked` with a value-free protocol-upgrade reason when possible, and tell the user the saved job can be retried only after the stale run is cleared through Trackly's supported lifecycle. Stop and refetch the protocol and active execution or batch if support level, execution mode, provider, required scenarios, authorized origin policy, member version, or inspection epoch changes after run creation.
|
|
75
77
|
- In guided mode, inspect the page before preparing any resume bytes. Confirm the employer, role, HTTPS origin, reachable review path, semantic controls, whether an attachment control exists, and absence of a credential, verification, CAPTCHA, or submit-only wall. A missing file input is not itself a blocker; skip the resume path when the application has no attachment control. Any other failed precondition is an execution blocker, not permission to improvise.
|
|
76
78
|
7. Pass the browser readiness gate before preparing resume bytes:
|
|
77
79
|
- Use semantic browser control through Codex in-app browser controls, Chrome MCP/extension browser control, or Claude in Chrome.
|
|
@@ -112,7 +114,7 @@ Follow this order:
|
|
|
112
114
|
1. Open the application in the controlled browser context and confirm the employer, role, ATS host, and HTTPS URL. Before entering private data, require the visible company and role to match the run binding and, when the stored URL exposes one, require the requisition identifier to match. When `job_identity_match` is required, immediately report its value-free committed `scenario_coverage` proof after this check passes; never include company, role, URL, requisition, or page text in the observation. On exact-origin fallback, revalidate the frozen company, role, and available requisition identity after every navigation or redirect and before entering any additional private data. Parse and normalize the URL, then require the exact origin to equal an `originPolicy.authorizedOrigins` entry or the normalized hostname to satisfy `host === allowedDomain` or `host.endsWith("." + allowedDomain)` for an allowed ATS suffix or verified company domain. Never use substring, display-text, logo, or suffix-without-a-dot matching. When `verification` is `trackly_employer_source_exact_origin`, accept only the exact listed origin; never convert it into a hostname suffix or carry it across a redirect or iframe origin change. For every other vendor-hosted ATS policy, require both `originPolicy.tenantRule` and `originPolicy.verifiedAtsTenant` to be non-null; stop before private data entry if either is missing. Execute the backend-owned declarative tenant rule exactly, including its extraction, exact-host-depth, locale, percent-decoding, normalization, and fail-closed semantics, then require the normalized result to equal `originPolicy.verifiedAtsTenant`; never invent or reinterpret a strategy token. Revalidate both origin and tenant after every redirect and apply the same policy to every iframe that receives private data. Stop on any unmatched origin or tenant, malformed percent encoding, missing required rule, or rule shape the client cannot execute exactly.
|
|
113
115
|
2. Inspect the whole form and identify required fields, semantic controls, consent controls, document inputs, and multi-step sections.
|
|
114
116
|
3. When an attachment control exists, only after the exact-hash visual confirmation and a successful immediate pre-attach `trackly_verify_prepared_resume` check, upload the prepared resume before autofill when parsing may overwrite contact fields. Do not change the file between verification and attachment. Verify that the filename chip exactly matches the prepared resume’s user-facing filename and contains no internal cache identifier. Stop and replace the attachment if it does not. When no attachment control exists, skip resume preparation and upload without treating that absence as an error.
|
|
115
|
-
4.
|
|
117
|
+
4. Run the deterministic resolver from [references/answer-resolution.md](references/answer-resolution.md) against the visible controls, then fill typed fields from the resolved Trackly profile. Every answer must be classified as `exact_profile`, `safe_derivation`, `supported_draft`, `missing_fact`, `live_consent`, or `forbidden_inference` before use. Clear parser-filled data when the canonical state is intentionally blank. Before writing any non-empty control, apply the local field-ownership gate from [references/form-integrity.md](references/form-integrity.md). Preserve `user_edited` and `unknown_external_change` values byte-for-byte unless the user explicitly requests a rewrite; never let resume parsing, rerendering, recovery, or the final sweep overwrite them.
|
|
116
118
|
5. Use real UI clicks for React/native selects, radios, and checkboxes. Resolve boolean values by their exact semantic label (`true` to Yes, `false` to No), never by option order, index, proximity, or a stale prior selection. After every selection, compare the committed value with the canonical Trackly value. If the field is required or had a validation error before selection, verify that the required-field error disappeared. An optional control with no validation error passes when its committed value is correct. Treat any value mismatch or applicable stale error as a failed field and correct it before continuing.
|
|
117
119
|
6. Recheck email and phone through both browser DOM state and macOS accessibility state. Require exact values and reject duplicate/concatenated values.
|
|
118
120
|
7. Complete every visible field whose canonical answer is known, including optional fields, previous-employer/title history, education rows and dates, links, relocation, communication preferences, and opportunity source. Before review, compare the live field inventory with the resolved Trackly profile and prove that no known visible answer was silently omitted. Group only genuinely unknown contextual fields into the question packet.
|
|
@@ -8,14 +8,20 @@ questions and piecemeal profile writes.
|
|
|
8
8
|
|
|
9
9
|
1. Collect all newly supplied answers from the current question packet.
|
|
10
10
|
2. Fetch the current profile schema and the smallest relevant profile
|
|
11
|
-
projection once. Include `provider`, `company`, and `
|
|
12
|
-
context is known and required.
|
|
11
|
+
projection once. Include `provider`, `company`, `jurisdiction`, and `office`
|
|
12
|
+
when that context is known and required. An office scope value is the exact
|
|
13
|
+
backend company ID plus a stable office identity, for example
|
|
14
|
+
`42:waltham-ma`; never derive it from a display name alone. Corporate-family reuse is unavailable until
|
|
13
15
|
Trackly has an authoritative company-family registry; never derive a family
|
|
14
16
|
identity from names, logos, page text, parent-company knowledge, or search.
|
|
15
17
|
3. Map each answer to an exposed canonical key and permitted scope. Never call
|
|
16
18
|
a field missing merely because it was absent from the compact execution
|
|
17
19
|
snapshot; contextual fields require a targeted profile fetch.
|
|
18
|
-
4.
|
|
20
|
+
4. Validate the expected input type before mapping or writing. “Whole number”
|
|
21
|
+
requires a number, and “Yes or No” requires a boolean. A mismatch remains
|
|
22
|
+
unresolved and returns to the grouped packet; never coerce a boolean into
|
|
23
|
+
years, a proficiency label into a language level, or vice versa.
|
|
24
|
+
5. Classify every answer as exactly one of:
|
|
19
25
|
- `saved`: a canonical field exists and this run changed it;
|
|
20
26
|
- `already_matched`: the canonical field already contains the same state and
|
|
21
27
|
value at the resolved scope;
|
|
@@ -23,9 +29,13 @@ questions and piecemeal profile writes.
|
|
|
23
29
|
live schema audit;
|
|
24
30
|
- `run_only_contextual`: the answer is intentionally per-run, such as the
|
|
25
31
|
final truthfulness certification, and must not enter the reusable profile.
|
|
26
|
-
|
|
32
|
+
6. Ask scope only when the user's wording and the schema do not determine it.
|
|
27
33
|
“Always” means global only for fields whose schema permits global scope.
|
|
28
34
|
|
|
35
|
+
Compound a reusable gap only when a visible form requires it. Defer optional
|
|
36
|
+
profile gaps that no current visible application needs; do not turn the full
|
|
37
|
+
schema into an onboarding questionnaire.
|
|
38
|
+
|
|
29
39
|
## One write and one verification
|
|
30
40
|
|
|
31
41
|
- Send all `saved` changes in one `trackly_update_application_profile` call
|
|
@@ -65,6 +75,10 @@ claim that Trackly learned it.
|
|
|
65
75
|
|
|
66
76
|
- Country-specific work authorization:
|
|
67
77
|
`authorization.legally_authorized_by_country` at `jurisdiction` scope.
|
|
78
|
+
- Employer-office commute willingness and cadence:
|
|
79
|
+
`location.commute_willing`, `location.commute_days_per_week`, and
|
|
80
|
+
`location.commute_commitment` at exact `office` scope. Never reuse an answer
|
|
81
|
+
for another office of the same company.
|
|
68
82
|
- Prior interview with the employer:
|
|
69
83
|
`employment.previously_interviewed_at_employer` at `company` scope.
|
|
70
84
|
- Employment, contracting, consulting, temporary work, or similar engagement
|
|
@@ -83,6 +97,18 @@ claim that Trackly learned it.
|
|
|
83
97
|
the backend can prove a match. A write receipt or refetch verifies only the
|
|
84
98
|
company scope and `questionFingerprint`; a redacted, unknown, or absent
|
|
85
99
|
reusable value is expected and must not block truth certification or review.
|
|
100
|
+
- Named applicant privacy-notice routing:
|
|
101
|
+
`consent.named_applicant_privacy_notice_acknowledgement_policy` is a global
|
|
102
|
+
routing policy, not consent to unseen text. `acknowledge_named_notice_only`
|
|
103
|
+
permits only a visible, unbundled named applicant notice; `ask` remains a
|
|
104
|
+
live question. Marketing, retention, arbitration, background-check,
|
|
105
|
+
recording, AI, and data-sharing choices stay separate.
|
|
106
|
+
- Interview recording or transcription consent and workplace-policy
|
|
107
|
+
acknowledgement: use `consent.interview_recording` and
|
|
108
|
+
`consent.workplace_policy_acknowledgement` only at exact company scope with
|
|
109
|
+
the current policy question or version as `questionLabel`. Both are
|
|
110
|
+
`run_only_contextual`: retain the fingerprint for audit, ask again, and never
|
|
111
|
+
require a reusable returned value during the verification refetch.
|
|
86
112
|
- Consumer hardware, IoT, or retail experience level and supporting summary:
|
|
87
113
|
the two global `employment.consumer_hardware_iot_retail_experience_*` fields.
|
|
88
114
|
- Residential city and EEO sexual orientation remain their existing canonical
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Deterministic answer resolution
|
|
2
|
+
|
|
3
|
+
Run this resolver after reading the current application-profile revision and
|
|
4
|
+
inventorying a visible employer form, but before generating that form's
|
|
5
|
+
question packet or writing an answer. Profile onboarding that happens before
|
|
6
|
+
an employer form exists uses the profile schema's field metadata instead.
|
|
7
|
+
This resolver reduces questions without weakening truthfulness.
|
|
8
|
+
|
|
9
|
+
## Resolution pipeline
|
|
10
|
+
|
|
11
|
+
For each visible question:
|
|
12
|
+
|
|
13
|
+
1. Normalize its label, help text, expected input type, choices, employer scope,
|
|
14
|
+
jurisdiction, and whether it is required.
|
|
15
|
+
2. Map it to one exact current schema key. Do not map by a single keyword.
|
|
16
|
+
3. Resolve one authority class:
|
|
17
|
+
- `exact_profile`: an answered canonical key with matching scope and type.
|
|
18
|
+
- `safe_derivation`: a deterministic derivation allowed below.
|
|
19
|
+
- `supported_draft`: free text whose every factual claim has evidence.
|
|
20
|
+
- `missing_fact`: no authoritative value exists; ask at point of use.
|
|
21
|
+
- `live_consent`: a notice, acknowledgement, certification, or choice that
|
|
22
|
+
must be made against the current form.
|
|
23
|
+
- `forbidden_inference`: a legal, identity, relationship, or specialized
|
|
24
|
+
claim that cannot be inferred.
|
|
25
|
+
4. Validate the expected input type before translating or committing a value.
|
|
26
|
+
5. Fill only the first three classes. Put only visible `missing_fact`,
|
|
27
|
+
`live_consent`, and user-resolvable `forbidden_inference` items in one
|
|
28
|
+
grouped question packet with expected type and scope.
|
|
29
|
+
|
|
30
|
+
Keep this value-free local provenance for each resolved control: profile
|
|
31
|
+
revision, schema key, scope, authority class, derivation rule when any,
|
|
32
|
+
expected type, and committed-control receipt. Never send the answer value in
|
|
33
|
+
an observation.
|
|
34
|
+
|
|
35
|
+
## Source precedence
|
|
36
|
+
|
|
37
|
+
The current profile revision is the only reusable answer authority. A
|
|
38
|
+
transcript or conversation, screenshot, parser output, browser autocomplete,
|
|
39
|
+
or cached value is never authority. Resume parsing is a suggestion only: it may
|
|
40
|
+
help identify a schema key, but the parser never becomes authority and never
|
|
41
|
+
overwrites an answered profile value or a user-edited control.
|
|
42
|
+
|
|
43
|
+
Contact details and addresses must resolve from exact current profile keys.
|
|
44
|
+
Verify their live committed state even when the browser initially displays a
|
|
45
|
+
masked or formatted value.
|
|
46
|
+
|
|
47
|
+
For an ordinary application name, use the canonical first and last name and
|
|
48
|
+
reconcile parser-created casing. A government-ID full legal name is different:
|
|
49
|
+
use it only when the form explicitly asks for a government-ID or legal-document
|
|
50
|
+
name and the dedicated restricted profile field is explicitly answered. Never
|
|
51
|
+
derive it from ordinary name fields.
|
|
52
|
+
|
|
53
|
+
Employment status outranks legacy employer columns. When status is
|
|
54
|
+
`not_employed`, current company and current title are non-authoritative even if
|
|
55
|
+
legacy values remain populated. For a current-employer control, commit boolean
|
|
56
|
+
No, an exact “Not currently employed” option, or an intentionally blank optional
|
|
57
|
+
text control according to the visible control semantics. Ask only when a
|
|
58
|
+
required control offers no truthful non-employed representation. Answer
|
|
59
|
+
prior-employer questions from
|
|
60
|
+
`employment.most_recent_company` and `employment.most_recent_title`, and leave current-employer
|
|
61
|
+
questions unresolved unless the schema supplies an applicable authoritative
|
|
62
|
+
answer. `student`, `other`, or unknown status fails closed when applicability
|
|
63
|
+
is ambiguous.
|
|
64
|
+
|
|
65
|
+
## Safe experience derivation
|
|
66
|
+
|
|
67
|
+
Read the wording semantically:
|
|
68
|
+
|
|
69
|
+
- A minimum qualification/profile range asks whether experience meets the
|
|
70
|
+
lower threshold. A profile value of 7 years answers “2–4 years?” as Yes when
|
|
71
|
+
the choices mean “at least 2 years” or a qualification bracket.
|
|
72
|
+
- An exact bounded band asks whether the value lies inside both bounds. With 7
|
|
73
|
+
years, “exactly between 2 and 4 years?” is No.
|
|
74
|
+
- An explicit disqualifying maximum is a ceiling. With 7 years, “no more than 4 years?” is No.
|
|
75
|
+
|
|
76
|
+
Never infer specialized experience from total product-management tenure.
|
|
77
|
+
Financial-infrastructure years, growth-focused software PM years, enterprise
|
|
78
|
+
SaaS PM years, health-tech experience, and named platform/tool experience need
|
|
79
|
+
their own schema keys or an explicit user answer.
|
|
80
|
+
|
|
81
|
+
Type must match the control. Returning Yes for “financial-infrastructure
|
|
82
|
+
years” is a type mismatch; ask for a whole number. Returning C2 for SQL
|
|
83
|
+
proficiency is a type mismatch when the field expects a proficiency enum.
|
|
84
|
+
English C2 is valid when the English field expects a CEFR level. Never coerce a
|
|
85
|
+
type mismatch into a plausible-looking answer.
|
|
86
|
+
|
|
87
|
+
## Employer relationships and legal boundaries
|
|
88
|
+
|
|
89
|
+
A direct prior-employment No may be derived only under the complete-history
|
|
90
|
+
rule in the main skill. It never proves no contractor, affiliate, subsidiary,
|
|
91
|
+
acquisition, family, friendship, referral, or other relationship. Those need
|
|
92
|
+
an exact matching scoped field or a user answer.
|
|
93
|
+
|
|
94
|
+
Region authorization is separate from country authorization. A region may be
|
|
95
|
+
answered Yes only when it is explicitly present in
|
|
96
|
+
`authorization.legally_authorized_regions`; absence is unknown, never No, and a
|
|
97
|
+
region answer must not be stored under a country jurisdiction.
|
|
98
|
+
|
|
99
|
+
Commute willingness and days-per-week commitments are employer-office facts,
|
|
100
|
+
not company-wide facts. Resolve them only from an exact `office` scope whose
|
|
101
|
+
identity combines the backend company ID and the specific office; another
|
|
102
|
+
office at the same employer remains unknown.
|
|
103
|
+
|
|
104
|
+
A named applicant privacy notice may be acknowledged only as the current
|
|
105
|
+
notice policy allows and with its current fingerprint. Marketing, data
|
|
106
|
+
retention, arbitration, background check, and interview recording are separate
|
|
107
|
+
choices. Onsite willingness does not prove that a workplace policy was read or
|
|
108
|
+
accepted. Truthfulness certification is always per-run and never reusable.
|
|
109
|
+
|
|
110
|
+
## Supported writing
|
|
111
|
+
|
|
112
|
+
For a useful optional motivation or experience prompt, `supported_draft` means
|
|
113
|
+
fill it when every factual claim is supported. Build from canonical facts and
|
|
114
|
+
evidence claims, state explicit gaps instead of inventing bridge facts, match
|
|
115
|
+
the saved voice rules, lint, commit, and reread. The user's final truth review
|
|
116
|
+
still governs the complete review-ready subset.
|
|
117
|
+
|
|
118
|
+
Ask the user only for a genuinely new fact, subjective choice, live consent,
|
|
119
|
+
or unsupported claim that the visible application requires. A visible employer
|
|
120
|
+
accuracy checkbox is a live consent resolved before the review-ready
|
|
121
|
+
checkpoint; the separate Trackly truth certification is requested only
|
|
122
|
+
afterward for the exact durable review-ready subset. Defer reusable
|
|
123
|
+
gaps that no current visible form needs.
|
|
@@ -40,4 +40,11 @@ Before entering the response:
|
|
|
40
40
|
|
|
41
41
|
When `writing.optional_question_policy` permits it, answer a strategically useful optional motivation, experience, product, or role-overlap question when every fact is supported. Optional does not mean skip. Leave demographic, consent, legal, compensation, and employer-relationship questions unanswered when their canonical value is unknown. Group those unknowns into the consolidated question packet.
|
|
42
42
|
|
|
43
|
+
A supported strategically useful optional prompt must not be left blank. Draft
|
|
44
|
+
it from canonical evidence claims, expose any explicit gaps instead of
|
|
45
|
+
inventing bridge facts, match the user's voice, pass deterministic lint, enter
|
|
46
|
+
the committed text, and reread it from the live control. Ask only when a new
|
|
47
|
+
fact or subjective choice is actually required; the final truth review remains
|
|
48
|
+
the user's approval boundary.
|
|
49
|
+
|
|
43
50
|
Before entering any answer, supply value-free claim fingerprints and evidence-reference codes to the local linter. It returns only a draft hash, length, policy, and violation codes. It never echoes or sends the draft to Trackly.
|
|
@@ -116,6 +116,16 @@ With protocol 3.5 or newer and the compact-snapshot capability enabled, prefer `
|
|
|
116
116
|
|
|
117
117
|
Every compact snapshot request must contain a non-empty list of current member IDs. Never use an empty member projection as shorthand for all members.
|
|
118
118
|
|
|
119
|
+
For an office-scoped question visible on one member, add an `officeProjections`
|
|
120
|
+
entry containing only that member ID, its canonical `companyId:office-identity`,
|
|
121
|
+
and the exact office-only keys needed by the form. Read the answer only from the
|
|
122
|
+
matching `memberOfficeProfiles` entry. Never copy it into the shared profile
|
|
123
|
+
projection or reuse it for a sibling member, another office, or another
|
|
124
|
+
employer. Recovery does not weaken this binding: Trackly must prove the
|
|
125
|
+
candidate's frozen company identity through its source lineage. A legacy
|
|
126
|
+
candidate without that immutable identity stays unknown and must be confirmed
|
|
127
|
+
from the visible form/user rather than inferred from current job metadata.
|
|
128
|
+
|
|
119
129
|
The execution freezes one original recent-first queue snapshot and ordering
|
|
120
130
|
version. Newly saved jobs wait for the next execution. Every continuation is a
|
|
121
131
|
new immutable child batch linked in wave order; never append, replace, reorder,
|
|
@@ -300,6 +310,22 @@ evidence cannot satisfy the current gate.
|
|
|
300
310
|
|
|
301
311
|
## First pass
|
|
302
312
|
|
|
313
|
+
Accessible-first is a hard scheduler invariant. While any accessible candidate remains, a known authentication-gated, account-creation, OTP, or pre-form
|
|
314
|
+
CAPTCHA candidate must not be opened. When access is unknown, perform only a
|
|
315
|
+
minimal non-mutating probe. If a gate appears, never start a draft or enter
|
|
316
|
+
private data: record the typed disposition, park it, and continue accessible
|
|
317
|
+
work. The backend disposition must release its reservation or capacity and
|
|
318
|
+
increment the authoritative `authParked` funnel count before replacement
|
|
319
|
+
scheduling. If that durable transition conflicts, preserve the surface and
|
|
320
|
+
surface the conflict; never invent released capacity locally.
|
|
321
|
+
|
|
322
|
+
An authoritative already-parked member is not first-pass browser work and
|
|
323
|
+
needs no duplicate disposition. A server access hint is not authoritative: if
|
|
324
|
+
accessible candidates remain, defer that hinted member; after accessible work
|
|
325
|
+
is exhausted, perform the minimal live probe required by the public
|
|
326
|
+
disposition contract. “Process every frozen member” below means every currently
|
|
327
|
+
actionable, non-parked member subject to this deferral rule.
|
|
328
|
+
|
|
303
329
|
Process every frozen member even when another needs user input:
|
|
304
330
|
|
|
305
331
|
1. Create or reuse its bound application run.
|
|
@@ -60,8 +60,11 @@ agent-written fingerprint.
|
|
|
60
60
|
|
|
61
61
|
## Contact-field integrity
|
|
62
62
|
|
|
63
|
-
-
|
|
64
|
-
-
|
|
63
|
+
- Resolve email, phone, name, and address from exact canonical keys in the current profile revision. Transcript or conversation text, screenshots, parser output, autocomplete, and cached values are never reusable authority.
|
|
64
|
+
- Compare email and phone to the exact Trackly values after resume parsing and autofill. Reconcile parser-altered ordinary name casing to canonical first/last-name casing; never use that rule for a government-ID name.
|
|
65
|
+
- Read both DOM input state and macOS accessibility state. A static HTML `value` attribute is not truth or proof of the live committed control state; require rendered live state, framework/native state, or accessibility state after events settle.
|
|
66
|
+
- For a masked phone control, compare its normalized live digits and country code to the canonical phone rather than requiring identical formatting. Prove an implicit country code from live control metadata, a committed country selector, or framework/native state; ten visible digits alone do not prove it.
|
|
67
|
+
- For address autocomplete, choose the exact offered option and prove that option committed through the control's semantic state; displayed typed text alone is insufficient.
|
|
65
68
|
- Reject duplicate or concatenated values, autofill overlays, placeholder-only text, and values present only in a custom wrapper.
|
|
66
69
|
- Recheck when navigation, resume parsing, or a correction banner rerenders the form.
|
|
67
70
|
- Before review, inventory every email, phone, country-code, and required contact control. Pass `critical_contact_integrity` only when all present canonical values are exact and no required contact field was omitted. A form with no contact control passes only after the whole-form inventory confirms none exists or is required.
|