trackly-cli 0.7.2 → 0.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "mcpName": "io.github.trackly-app/trackly",
5
5
  "description": "AI job search CLI + hosted MCP server with OAuth. 128K+ jobs, 1,900+ companies, 40+ ATS. Works with Claude, ChatGPT, Cursor, Windsurf, Codex via hosted streamable-http or local stdio.",
6
6
  "bin": {
@@ -8,7 +8,9 @@
8
8
  },
9
9
  "scripts": {
10
10
  "test": "node --test",
11
- "test:hosted-contract": "node scripts/verify-hosted-contract.js"
11
+ "test:contract-fixture": "node --test test/apply-contract.test.js",
12
+ "test:hosted-contract": "node scripts/verify-hosted-contract.js",
13
+ "security:audit": "node scripts/verify-audit-exceptions.js"
12
14
  },
13
15
  "files": [
14
16
  "bin/",
@@ -17,6 +19,9 @@
17
19
  "skills/",
18
20
  "contracts/",
19
21
  "docs/",
22
+ "scripts/verify-audit-exceptions.js",
23
+ "security/audit-exceptions.json",
24
+ "npm-shrinkwrap.json",
20
25
  "server.json",
21
26
  "README.md",
22
27
  "LICENSE"
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const { spawnSync } = require('node:child_process');
8
+
9
+ const DEFAULT_POLICY_PATH = path.join(
10
+ __dirname,
11
+ '..',
12
+ 'security',
13
+ 'audit-exceptions.json',
14
+ );
15
+ const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';
16
+
17
+ function selectAdvisoryFields(advisory) {
18
+ return {
19
+ source: advisory.source,
20
+ name: advisory.name,
21
+ dependency: advisory.dependency,
22
+ title: advisory.title,
23
+ url: advisory.url,
24
+ severity: advisory.severity,
25
+ cwe: advisory.cwe,
26
+ cvss: advisory.cvss,
27
+ range: advisory.range,
28
+ };
29
+ }
30
+
31
+ function selectVulnerabilityFields(vulnerability) {
32
+ return {
33
+ name: vulnerability.name,
34
+ severity: vulnerability.severity,
35
+ isDirect: vulnerability.isDirect,
36
+ via: vulnerability.via.map((entry) => (
37
+ typeof entry === 'string' ? entry : selectAdvisoryFields(entry)
38
+ )),
39
+ effects: vulnerability.effects,
40
+ range: vulnerability.range,
41
+ nodes: vulnerability.nodes,
42
+ };
43
+ }
44
+
45
+ function normalizedAuditShape(report) {
46
+ const vulnerabilities = {};
47
+ for (const packageName of Object.keys(report.vulnerabilities || {}).sort()) {
48
+ vulnerabilities[packageName] = selectVulnerabilityFields(
49
+ report.vulnerabilities[packageName],
50
+ );
51
+ }
52
+
53
+ return {
54
+ vulnerabilityCounts: report.metadata?.vulnerabilities,
55
+ vulnerabilities,
56
+ };
57
+ }
58
+
59
+ function cleanAuditResult(report) {
60
+ const counts = report.metadata?.vulnerabilities;
61
+ const vulnerabilities = report.vulnerabilities || {};
62
+ const zeroCounts = counts
63
+ && ['info', 'low', 'moderate', 'high', 'critical', 'total']
64
+ .every((key) => counts[key] === 0);
65
+ return zeroCounts && Object.keys(vulnerabilities).length === 0;
66
+ }
67
+
68
+ function validatePolicy(policy) {
69
+ assert.equal(policy?.schemaVersion, 1);
70
+ assert.ok(Array.isArray(policy.exceptions));
71
+ assert.equal(
72
+ policy.exceptions.length,
73
+ 1,
74
+ 'Exactly one temporary npm audit exception is permitted',
75
+ );
76
+ const exception = policy.exceptions[0];
77
+ assert.equal(exception.advisory, 'GHSA-frvp-7c67-39w9');
78
+ assert.equal(exception.package, '@hono/node-server');
79
+ assert.equal(exception.dependencyOf, '@modelcontextprotocol/sdk');
80
+ assert.equal(exception.severity, 'moderate');
81
+ assert.equal(exception.vulnerableRange, '<2.0.5');
82
+ assert.equal(
83
+ exception.upstream,
84
+ 'https://github.com/modelcontextprotocol/typescript-sdk/issues/2531',
85
+ );
86
+ assert.ok(exception.expectedAuditShape);
87
+ return exception;
88
+ }
89
+
90
+ function validateAuditReport(report, policy) {
91
+ if (
92
+ report
93
+ && typeof report === 'object'
94
+ && (
95
+ (report.error && typeof report.error === 'object')
96
+ || typeof report.message === 'string'
97
+ )
98
+ ) {
99
+ const detail = [
100
+ report.message,
101
+ report.error?.summary,
102
+ report.error?.detail,
103
+ ].find((value) => typeof value === 'string' && value.trim());
104
+ throw new Error(
105
+ `npm audit did not return an advisory report${detail ? `: ${detail.trim()}` : ''}`,
106
+ );
107
+ }
108
+ assert.equal(report?.auditReportVersion, 2, 'Unsupported npm audit report version');
109
+ const exception = validatePolicy(policy);
110
+
111
+ if (cleanAuditResult(report)) {
112
+ return {
113
+ allowedAdvisories: [],
114
+ propagatedVulnerabilities: [],
115
+ };
116
+ }
117
+
118
+ try {
119
+ assert.deepEqual(normalizedAuditShape(report), exception.expectedAuditShape);
120
+ } catch (error) {
121
+ const wrapped = new Error(
122
+ 'npm audit report does not match the temporary exception. '
123
+ + 'Review every new advisory, severity, affected range, and dependency-path change.',
124
+ );
125
+ wrapped.cause = error;
126
+ throw wrapped;
127
+ }
128
+
129
+ return {
130
+ allowedAdvisories: [exception.advisory],
131
+ propagatedVulnerabilities: Object.keys(report.vulnerabilities).sort(),
132
+ };
133
+ }
134
+
135
+ function loadPolicy(policyPath = DEFAULT_POLICY_PATH) {
136
+ return JSON.parse(fs.readFileSync(policyPath, 'utf8'));
137
+ }
138
+
139
+ function runAudit() {
140
+ const result = spawnSync(NPM, ['audit', '--json', '--audit-level=moderate'], {
141
+ cwd: path.join(__dirname, '..'),
142
+ encoding: 'utf8',
143
+ maxBuffer: 10 * 1024 * 1024,
144
+ });
145
+
146
+ if (result.error) {
147
+ throw result.error;
148
+ }
149
+ if (!result.stdout.trim()) {
150
+ throw new Error(`npm audit returned no JSON. stderr: ${result.stderr.trim()}`);
151
+ }
152
+
153
+ let report;
154
+ try {
155
+ report = JSON.parse(result.stdout);
156
+ } catch (error) {
157
+ throw new Error(
158
+ `npm audit returned invalid JSON. stderr: ${result.stderr.trim()}`,
159
+ { cause: error },
160
+ );
161
+ }
162
+
163
+ return validateAuditReport(report, loadPolicy());
164
+ }
165
+
166
+ if (require.main === module) {
167
+ try {
168
+ const result = runAudit();
169
+ if (result.allowedAdvisories.length === 0) {
170
+ console.log('npm audit is clean; no temporary exceptions were used.');
171
+ } else {
172
+ console.log(
173
+ `npm audit matches the single temporary exception: ${result.allowedAdvisories.join(', ')}`,
174
+ );
175
+ }
176
+ } catch (error) {
177
+ console.error(error.message);
178
+ if (error.cause?.message) {
179
+ console.error(error.cause.message);
180
+ }
181
+ process.exitCode = 1;
182
+ }
183
+ }
184
+
185
+ module.exports = {
186
+ loadPolicy,
187
+ normalizedAuditShape,
188
+ runAudit,
189
+ validateAuditReport,
190
+ };
@@ -0,0 +1,70 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "exceptions": [
4
+ {
5
+ "advisory": "GHSA-frvp-7c67-39w9",
6
+ "source": 1124006,
7
+ "package": "@hono/node-server",
8
+ "dependencyOf": "@modelcontextprotocol/sdk",
9
+ "severity": "moderate",
10
+ "vulnerableRange": "<2.0.5",
11
+ "upstream": "https://github.com/modelcontextprotocol/typescript-sdk/issues/2531",
12
+ "rationale": "Trackly starts the MCP SDK only through its stdio transport. It does not import @hono/node-server, Streamable HTTP, or serveStatic, so the Windows static-file path traversal is unreachable. Automated stdio isolation and packed-consumer tests enforce that boundary until the SDK removes or upgrades this dependency.",
13
+ "expectedAuditShape": {
14
+ "vulnerabilityCounts": {
15
+ "info": 0,
16
+ "low": 0,
17
+ "moderate": 2,
18
+ "high": 0,
19
+ "critical": 0,
20
+ "total": 2
21
+ },
22
+ "vulnerabilities": {
23
+ "@hono/node-server": {
24
+ "name": "@hono/node-server",
25
+ "severity": "moderate",
26
+ "isDirect": false,
27
+ "via": [
28
+ {
29
+ "source": 1124006,
30
+ "name": "@hono/node-server",
31
+ "dependency": "@hono/node-server",
32
+ "title": "Node.js Adapter for Hono: Path traversal in `serve-static` on Windows via encoded backslash (`%5C`)",
33
+ "url": "https://github.com/advisories/GHSA-frvp-7c67-39w9",
34
+ "severity": "moderate",
35
+ "cwe": [
36
+ "CWE-22"
37
+ ],
38
+ "cvss": {
39
+ "score": 5.9,
40
+ "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N"
41
+ },
42
+ "range": "<2.0.5"
43
+ }
44
+ ],
45
+ "effects": [
46
+ "@modelcontextprotocol/sdk"
47
+ ],
48
+ "range": "<2.0.5",
49
+ "nodes": [
50
+ "node_modules/@hono/node-server"
51
+ ]
52
+ },
53
+ "@modelcontextprotocol/sdk": {
54
+ "name": "@modelcontextprotocol/sdk",
55
+ "severity": "moderate",
56
+ "isDirect": true,
57
+ "via": [
58
+ "@hono/node-server"
59
+ ],
60
+ "effects": [],
61
+ "range": ">=1.25.0",
62
+ "nodes": [
63
+ "node_modules/@modelcontextprotocol/sdk"
64
+ ]
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ]
70
+ }
package/server.json CHANGED
@@ -17,12 +17,12 @@
17
17
  ]
18
18
  }
19
19
  ],
20
- "version": "0.7.2",
20
+ "version": "0.8.1",
21
21
  "packages": [
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "trackly-cli",
25
- "version": "0.7.2",
25
+ "version": "0.8.1",
26
26
  "runtimeHint": "npx",
27
27
  "runtimeArguments": [
28
28
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: trackly-apply
3
- description: Fill user-approved jobs from a Trackly apply/check-later queue through a supported ATS in a real browser, using the user’s profile and resume from Trackly, then stop for manual review and submission. Use for requests such as “apply to my queue,” “fill the next application,” “Trackly apply,” or “prepare job 123 for review” in Codex or Claude Code on macOS.
3
+ description: Fill the next user-approved jobs from a Trackly apply/check-later queue through a supported ATS in a real browser, using the user’s profile and resume from Trackly, then stop for manual review and submission. Use for requests such as “apply to my queue,” “fill the next application,” or “Trackly apply” in Codex or Claude Code on macOS.
4
4
  ---
5
5
 
6
6
  # Trackly Apply
@@ -19,7 +19,13 @@ Use Trackly as the source of truth for profile answers, documents, queue decisio
19
19
 
20
20
  ## Start every run
21
21
 
22
- 1. Call `trackly_get_apply_protocol`. Skill 4.1 requires protocol major 3 (version 3.1.0 or newer) and `compatibleSkillMajor: 4`. Reject an older or incompatible version and report that the backend must finish updating or `trackly agent setup` must update the skill. This gate prevents a pre-evidence skill or run from being reused under the stronger cross-ATS contract. Protocol 3.2 adds exact-origin trust for jobs Trackly ingested directly from employer careers sources; do not recreate the retired ownership-timestamp gate in the client.
22
+ Before selecting or resuming work, read
23
+ [references/batch-orchestration.md](references/batch-orchestration.md) and
24
+ [references/browser-lifecycle.md](references/browser-lifecycle.md). Those
25
+ references define the server-frozen batch, grouped-question, tab-reconciliation,
26
+ and missing-surface recovery contract; they are mandatory even for a one-job batch.
27
+
28
+ 1. Call `trackly_get_apply_protocol`. Skill 4.2.1 requires protocol major 3 and protocol 3.3.1 or newer for frozen-batch execution; protocol 3.2 remains valid for the explicit legacy single-run workflow. Require `compatibleSkillMajor: 4` and `compatibleSkillMinimumVersion` no newer than this installed skill. Reject an older or incompatible version 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.
23
29
  2. Call `trackly_get_profile_onboarding` or fetch both the profile schema and application profile. 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.
24
30
  3. Save answers with `trackly_update_application_profile`:
25
31
  - Use `answered`, `intentionally_blank`, `declined`, or `unknown` exactly.
@@ -34,14 +40,14 @@ Use Trackly as the source of truth for profile answers, documents, queue decisio
34
40
  - Use `consent.future_opportunity_retention` only at company scope because one employer's optional retention choice is not consent for another employer.
35
41
  - Treat an accuracy or truthfulness certification as a live per-run attestation. Never save that attestation to the reusable profile; ask and verify it on every application run.
36
42
  4. Require the one-time profile confirmation, complete education entries, and default-resume metadata before browser work. Do not prepare or upload the resume when the form has no attachment control.
37
- 5. Call `trackly_get_apply_queue`. Select deterministically unless the user names a job. Do not replace the queue call with a fresh job search.
38
- - Use the queue item's backend-owned `atsCapability`, `originPolicy`, and `executionBlocker`. Never infer a stronger support claim from the page design or the LLM's familiarity with a provider. Required scenarios are backend-enforced gates; possible scenarios are reported only when actually exercised.
39
- - Stop before `trackly_start_apply_run` whenever `executionBlocker` is non-null. `standard` runs use the provider playbook. `guided` runs are allowed only while semantic browser state remains observable, `originPolicy.authorized` is true, and every published stop condition is obeyed. `manual_only` items stop without starting or mutating a run.
43
+ 5. For every protocol 3.3.1 request, call `trackly_get_active_apply_batch` before creating anything. When it returns a batch, page and resume that exact batch, including every non-null existing member `runId`; never replace it because chat context, a tab, or the local ledger was lost. Only when it returns no active batch may you call `trackly_create_apply_batch` once with the requested count and a fresh idempotency key. Then page only that batch with `trackly_get_apply_batch` and claim it with `trackly_claim_apply_batch`. The returned server membership and order are authoritative. Do not call `trackly_get_apply_queue` first to construct a client-side candidate list; reserve that tool for previewing the queue or resuming a legacy protocol 3.2 single run.
44
+ - Use the frozen member's `staticExclusionCode` as the pre-run blocker. Never infer a stronger support claim from the page design or the LLM's familiarity with a provider. For a member without a static exclusion, call `trackly_start_apply_run`; its returned backend-owned `atsCapability`, `originPolicy`, `executionBlocker`, and required scenarios are authoritative for browser work.
45
+ - Stop after `trackly_start_apply_run` whenever its `executionBlocker` is non-null. `standard` runs use the provider playbook. `guided` runs are allowed only while semantic browser state remains observable, `originPolicy.authorized` is true, and every published stop condition is obeyed. `manual_only` items stop without browser or private-data mutation.
40
46
  - When `originPolicy.verification` is `trackly_employer_source_exact_origin`, Trackly has authorized the exact stored HTTPS origin from its direct employer-careers ingestion. Do not demand a separate ownership timestamp, ATS tenant, or company-domain suffix. This authorization never extends to another origin, hostname suffix, redirect, or iframe. The queue/run will require `job_identity_match`; pass it only after the browser visibly confirms the frozen company, role, and available requisition identity. Revalidate that identity after every navigation or redirect and before entering any additional private data.
41
- - If the user requests the next `N` jobs, freeze the deterministic ordered set of exactly `N` job IDs before starting any run. Do not replace, rescore, or expand that approved batch.
47
+ - If the user requests the next `N` jobs, freeze up to `N` available saved jobs before starting any run. Preserve inactive, insecure, or protocol-declared manual-only members with their static exclusion reason; do not replenish, replace, rescore, or expand the batch. If fewer than `N` saved jobs exist, report the frozen count rather than searching for substitutes.
42
48
  - 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.
43
- 6. Call `trackly_start_apply_run` for the selected job or current fixed batch member. Reuse an active run returned by Trackly; never create a replacement run because browser control was interrupted.
44
- - Require `run.protocolVersion` to be 3.1.0 or newer and to share protocol major 3 with the fetched protocol. Never continue a pre-evidence 3.0.x run under skill 4.1. 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. Compare `run.atsCapability` and `run.originPolicy` with the queue values. Stop and refetch the protocol and queue if support level, execution mode, provider, required scenarios, or authorized origin policy changed between preflight and run creation.
49
+ 6. Call `trackly_start_apply_run` for the selected job or current fixed batch member with the complete `batchId`, `memberId`, `expectedMemberVersion`, `expectedInspectionEpoch`, and `leaseToken` returned by Trackly. This call is also the sanctioned idempotent lookup when a recovered member has no `runId`; call it with the exact recovered binding and Trackly will reuse the bound active run. Never create a replacement run because browser control was interrupted.
50
+ - Require `run.protocolVersion` to be 3.1.0 or newer and to share protocol major 3 with the fetched protocol. A new batch member requires 3.3.1 or newer; an explicit 3.2 single run may start or finish through its legacy path. Never continue a pre-evidence 3.0.x run under skill 4.2.1. 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 batch if support level, execution mode, provider, required scenarios, authorized origin policy, member version, or inspection epoch changes after run creation.
45
51
  - 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.
46
52
  7. Pass the browser readiness gate before preparing resume bytes:
47
53
  - Use semantic browser control through Codex in-app browser controls, Chrome MCP/extension browser control, or Claude in Chrome.
@@ -49,25 +55,25 @@ Use Trackly as the source of truth for profile answers, documents, queue decisio
49
55
  - Bind each tab to the exact employer, role, ATS, requisition URL, job ID, and run ID. A window position or ephemeral tab number alone is not identity.
50
56
  - Build a value-free browser binding from those normalized keys plus the semantic browser surface and stable controller tab identity. Compute its lowercase SHA-256; never send the raw URL, title, employer, role, or tab text as observation metadata.
51
57
  - After a handoff, context resume, or browser-control interruption, reclaim and re-verify every mapped tab before continuing.
52
- - Report `observationType: browser_ready` for the current run with `scenarioCode: browser_reclaim`, the allowed `browserSurface`, `committed: true`, and that `browserBindingHash`. Do not call `trackly_prepare_resume` until this same-run attestation succeeds. Accessibility may provide an independent verification signal, but coordinate-only clicking is forbidden for form completion.
58
+ - Report `observationType: browser_ready` for the current run with the exact current `batchId`, `memberId`, and `inspectionEpoch`, plus `scenarioCode: browser_reclaim`, the allowed `browserSurface`, `committed: true`, and that `browserBindingHash`. Do not call `trackly_prepare_resume` until this same-run attestation succeeds. Accessibility may provide an independent verification signal, but coordinate-only clicking is forbidden for form completion.
53
59
  - If the semantic browser bridge is unavailable, preserve every existing run and tab mapping, record the blocker when possible, and stop before any upload or form mutation.
54
60
  8. If and only if the application offers or requires a resume attachment, call `trackly_prepare_resume` with that exact application run ID, browser surface, and browser binding hash. If hosted MCP reports it unavailable, tell the user that local Trackly MCP or manual upload is required. If no attachment control exists, skip steps 8–11 and do not report `resume_upload` as exercised.
55
61
  9. Preserve the user’s filename returned by `trackly_prepare_resume`. Internal cache identifiers belong only in private parent directories and must never appear in the employer-facing upload filename.
56
62
  10. Before any upload, let the user inspect the exact prepared file returned by `trackly_prepare_resume`:
57
63
  - Prefer an inline visual preview. Otherwise open the exact local file in Quick Look or Preview.app.
58
- - Show a compact proof block with source (`Trackly default resume`), exact local path, user-facing filename, file size, SHA-256 fingerprint, application run, and expiration.
64
+ - Show a compact local proof block with source (`Trackly default resume`), exact local path, user-facing filename, file size, SHA-256 fingerprint, application run, and expiration. The exact path is local-only. The SHA-256 may be sent only to Trackly's authenticated resume approval, prepared-resume verification, and truth-certification tools; never send either value in observations, logs, application answers, analytics, or employer form fields.
59
65
  - Ask for explicit confirmation to use that resume. Bind confirmation to the exact SHA-256 and current application run. For an explicitly approved batch of `N` runs, the user may authorize the same confirmed SHA-256 only for the frozen job/run/tab set; still show and verify each member's exact path, size, hash, run ID, and expiration. Stop if any hash differs, a run is missing, or a run falls outside the frozen batch. Outside that batch, a different hash or run requires new confirmation.
60
66
  - Always provide `confirmation.verification.exactLocalPath` so the user can independently verify the file. Never describe the prepared cache path as the original upload source.
61
67
  - If an original local source path is known from the current session, identify it separately. Do not store original device paths in Trackly.
62
68
  - A generic profile page is not proof of the prepared file. Use an app or web deep link only when the current protocol supplies an authenticated exact-resume viewer tied to the same SHA-256.
63
69
  - If no exact preview method works, stop and ask the user to inspect the file manually.
64
- 11. After the user confirms and immediately before attachment, call local `trackly_verify_prepared_resume` with the confirmed run ID, confirmation ID, exact path, SHA-256, size, and expiration. Continue only when it returns `verified: true` for exactly those values. The verifier validates the signed prepare-issued proof, recomputes the file hash, and locks it read-only. If it is unavailable, expired, missing, or mismatched, stop; prepare and visually confirm a fresh copy or require manual upload. Never send a local path or fingerprint to the hosted verifier.
70
+ 11. After the user confirms and immediately before attachment, call local `trackly_verify_prepared_resume` with the confirmed run ID, resume ID, confirmation ID, exact path, SHA-256, size, and expiration. Continue only when it returns `verified: true` for exactly those values. The verifier validates the signed prepare-issued proof, recomputes the file hash, and locks it read-only. If it is unavailable, expired, missing, or mismatched, stop; prepare and visually confirm a fresh copy or require manual upload. Never send a local path or fingerprint to the hosted verifier.
65
71
 
66
72
  ## Resume after maintenance
67
73
 
68
74
  If any REST or MCP tool returns canonical `maintenance_mode` (or the legacy `planned_maintenance` compatibility alias):
69
75
 
70
- 1. Retain the current `agent_browser` run ID, selected job, and browser context. Do not call `trackly_start_apply_run` again.
76
+ 1. Retain the current `agent_browser` run ID, selected job, and browser context. Do not blindly call `trackly_start_apply_run` while maintenance is active. After recovery, call it only when the active frozen member omits `runId`, using the exact current member/version/epoch/lease binding so Trackly performs the sanctioned idempotent lookup.
71
77
  2. Stop issuing mutations and wait for the advertised retry window or estimated return time. Do not loop or blindly retry.
72
78
  3. After maintenance clears, refetch `trackly_get_apply_protocol` and the application profile/onboarding state before taking another action.
73
79
  4. Resume the existing run from the observable browser state, re-verify fields that may have rerendered, and continue toward manual review.
@@ -91,7 +97,7 @@ Follow this order:
91
97
  9. For a free-text application response, read [references/application-writing.md](references/application-writing.md). Calibrate from the user's Trackly writing fields, use only supported profile and role facts, and run the built-in voice and anti-slop gate before entering the response. Do not require a separate writing or humanizer skill.
92
98
  10. Run the full integrity gate, including the final consent checkbox, every visible error, all steps, and any correction banner.
93
99
 
94
- When the user corrects an answer, immediately save the appropriate scope with `trackly_update_application_profile` and report only a redacted mechanics observation through `trackly_report_apply_observation`. Never promote one user’s value into a global default. For `generic_web_form`, never save provider-scoped answers; use company scope for form-specific answers.
100
+ When the user corrects an answer, immediately save the appropriate scope with `trackly_update_application_profile` and report only a redacted mechanics observation. For a frozen batch, collect current-epoch evidence locally and send it through `trackly_report_apply_observations` in one bounded bulk call; use `trackly_report_apply_observation` only for a legacy single run or an isolated follow-up. For every protocol 3.3 observation, include the exact current `batchId`, `memberId`, and `inspectionEpoch`; stale-epoch evidence must fail closed and be recreated only after reclaiming the current surface. Never promote one user’s value into a global default. For `generic_web_form`, never save provider-scoped answers; use company scope for form-specific answers.
95
101
 
96
102
  For every run, track only scenarios actually exercised, except the two universal review proofs below. Attest `browser_reclaim` once with the same-run `observationType: browser_ready`, exact binding hash, browser surface, and `metadata.committed: true`; do not send a duplicate `scenario_coverage` row for it. Before `review_ready`, report every other exercised scenario with `observationType: scenario_coverage`, the stable scenario code, browser surface, `metadata.committed: true` for `passed` or `corrected`, and whether the tab was resumed after handoff.
97
103
 
@@ -105,12 +111,13 @@ Every required scenario and both universal evidence scenarios must have correspo
105
111
 
106
112
  ## Review handoff
107
113
 
108
- Call `trackly_record_application_outcome` with `review_ready`, then provide the review block defined in [references/review-handoff.md](references/review-handoff.md). For a single run, keep the browser on the final review state and stop. For a frozen batch, preserve the current review-ready tab and continue the same lifecycle for the next mapped batch member; stop only after every frozen member is review-ready, then provide one review block per run. Never submit any member. End with job ID -> run ID -> browser tab -> ATS -> status plus the actual scenario coverage for each run.
114
+ After every conditional field is settled and all other review gates pass, bulk checkpoint every ready member with `review/manual_submit`, the current unchanged inspection epoch in both epoch fields, and any `resolvedActionIds`. Use the returned member versions and epochs; this checkpoint resolves the listed human actions and moves the durable member lifecycle to `review_ready` without invalidating current-epoch browser evidence or an unchanged resume-content approval. Do not wait for `needs_input` members: whenever at least one member is durably `review_ready`, ask for one explicit final truthfulness confirmation for the exact complete subset that is currently `review_ready`. If any form in that certified subset used a resume attachment, certify with `resumeDependency: approved` and the exact approved resume identity. If no form in the subset exposes a resume control, certify with `resumeDependency: not_applicable` and omit the resume ID and hash; do not invent an upload requirement. Then call `trackly_record_application_outcomes` once with every member in that certified subset using its exact current `runId`, `batchId`, `memberId`, `inspectionEpoch`, and `leaseToken`; refresh only members returned as conflicts. A member that becomes ready later requires a fresh certification for the then-current complete `review_ready` subset; never reuse an older certification for it. Use singular `trackly_record_application_outcome` only for a legacy single run or an isolated post-review transition. Provide the review block defined in [references/review-handoff.md](references/review-handoff.md). For a single run, keep the browser on the final review state and stop. For a frozen batch, preserve every review-ready tab, hand off each certified review-ready subset without waiting on unrelated human actions, and continue the same lifecycle for the remaining mapped members. Members with unresolved actions stay frozen and resumable. Never submit any member. End with job ID -> run ID -> browser tab -> ATS -> status plus the actual scenario coverage for each run.
109
115
 
110
116
  After the user submits manually:
111
117
 
112
- - If a success page is visible, record `submitted` with a short non-sensitive confirmation signal.
113
- - If the user explicitly confirms submission, record `submitted` with `user_confirmed`.
118
+ - If a success page is visible, reclaim the current batch lease if needed, first record current-epoch `confirmation_detected` evidence with source `success_page` through `trackly_record_apply_submission_evidence`, then record `submitted` with the exact current batch/member/epoch/lease binding and confirmation `success_page`.
119
+ - If the user explicitly confirms submission, reclaim the current batch lease if needed, first record current-epoch `confirmation_detected` evidence with source `user_confirmation` through `trackly_record_apply_submission_evidence`, then record `submitted` with the exact current batch/member/epoch/lease binding and confirmation `user_confirmation`.
120
+ - A provider receipt is optional orthogonal evidence. Record it as `provider_receipt_detected`; never use `provider_receipt` as the outcome confirmation or as a substitute for a success page or explicit user confirmation.
114
121
  - If neither exists, do not move the job to applied.
115
122
  - Treat a contradictory ATS response such as “already applied” as provisional until the exact requisition URL settles. Do not click Submit again. Preserve the page, confirm the job/requisition identifier is unchanged, and re-read the final route state after the UI and network activity settle. A later explicit success state on that same requisition overrides the provisional error and must be recorded as `submitted`; otherwise record the run as blocked without marking the job applied.
116
123
 
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Trackly Apply"
3
3
  short_description: "Fill queued job applications for manual review"
4
- default_prompt: "Use $trackly-apply to fill my next approved Trackly job application and stop before Submit."
4
+ default_prompt: "Use $trackly-apply. Resume my active frozen batch, or create a one-job batch from my next approved Trackly job. Keep the browser visible, verify every committed field, and stop before Submit."
@@ -0,0 +1,187 @@
1
+ # Batch orchestration
2
+
3
+ Use the backend batch contract for every new protocol 3.3 run, including a
4
+ one-job request. The batch is an execution ledger, not a fit-ranking exercise.
5
+ Protocol 3.2 is retained for the explicit legacy single-run workflow.
6
+
7
+ ## Freeze before browser work
8
+
9
+ Create one server-frozen batch before opening or mutating application forms.
10
+ Use the backend order exactly: active jobs first, then `savedAt` descending,
11
+ then job ID ascending. Never replace, rescore, or expand frozen membership
12
+ because a form is difficult or another job looks easier. A job saved after the
13
+ snapshot belongs only to a future batch.
14
+
15
+ Static planning keeps an inactive posting, insecure URL, or protocol-declared
16
+ manual-only job in frozen membership with a non-executable exclusion reason.
17
+ Never replenish or replace it with another queued job. Credentials, CAPTCHA
18
+ placement, attachment controls, semantic observability, and review reachability
19
+ are browser findings; they do not change membership. If the user removes or
20
+ discards a frozen member, stop before its next private-data mutation.
21
+
22
+ Read large batches through opaque continuation tokens. Do not recreate ordering
23
+ or pagination in the prompt.
24
+
25
+ ## Backend tool sequence
26
+
27
+ For every protocol 3.3.1 request:
28
+
29
+ 1. call `trackly_get_active_apply_batch` before any create call;
30
+ 2. resume the returned active batch, or, only when none exists, call
31
+ `trackly_create_apply_batch` once with the requested size and a fresh
32
+ idempotency key;
33
+ 3. page only that batch with `trackly_get_apply_batch`;
34
+ 4. acquire or renew ownership with `trackly_claim_apply_batch`;
35
+ 5. start or reuse each run with the complete batch/member/lease binding; and
36
+ 6. send browser findings in groups of at most 20 through
37
+ `trackly_checkpoint_apply_batch`.
38
+
39
+ Do not recreate a batch after maintenance. Refetch the existing batch, reclaim
40
+ its lease with the latest revision, and resume its existing run bindings.
41
+
42
+ ### Request budget
43
+
44
+ For a new 20-member batch, keep recovery, planning, initial binding, evidence,
45
+ two bulk checkpoints, resume approval, truth certification, outcomes, and the
46
+ final batch refresh within 52 non-resume MCP/HTTP requests: one active-batch
47
+ lookup, one create, one page, one claim, 20 run starts, 20 surface bindings, one
48
+ `trackly_report_apply_observations` call before resume preparation, a second
49
+ bulk observation call for final scenario coverage, two bulk checkpoints, one
50
+ resume approval, one truth certification, one
51
+ `trackly_record_application_outcomes` call, and one final refresh. An existing
52
+ active batch omits the create call. Do not replace bulk observations,
53
+ checkpoints, or outcomes with per-member requests. Resume download and exact
54
+ local verification are excluded from this count.
55
+
56
+ ## Ownership and replay safety
57
+
58
+ Acquire the renewable lease before browser mutation. Each mutation supplies
59
+ only the guards exposed by its tool schema: lease token when the schema exposes
60
+ it, optimistic version when the schema exposes it, inspection epoch when the
61
+ schema exposes it, and idempotency key when the schema exposes it. Never invent
62
+ or attach an uncontracted guard.
63
+
64
+ Renew before expiry and stop mutation if ownership is lost. For operations
65
+ documented as idempotent, a same-key, same-payload replay returns the original
66
+ result. A same key with a different payload is a conflict (`409`), never
67
+ permission to retry under a new run. Do not blindly retry a mutation without an
68
+ idempotency key after an ambiguous response; recover through the documented
69
+ read or lookup operation first. Reclaiming a browser tab increments the
70
+ inspection epoch; earlier-epoch review, attachment, certification, or close
71
+ evidence cannot satisfy the current gate.
72
+
73
+ ## First pass
74
+
75
+ Process every frozen member even when another needs user input:
76
+
77
+ 1. Create or reuse its bound application run.
78
+ 2. Reclaim or open its exact stored requisition URL and validate origin and job
79
+ identity.
80
+ 3. Inventory the accessible form.
81
+ 4. Fill and verify every known field before checkpointing an unknown.
82
+ 5. Record typed human actions and continue to the next frozen member.
83
+
84
+ Recoverable actions keep the run in `needs_input`. Group the first interruption
85
+ into one grouped first-pass packet organized by company, role, run, and action
86
+ type. After answers reveal conditional fields, send a delta packet containing
87
+ only newly revealed items.
88
+
89
+ Action records contain only value-free type, stage, blocking scope,
90
+ continuation flags, status, and redacted fingerprints. Credentials, OTPs,
91
+ CAPTCHA answers, raw question text, and private answer values never enter
92
+ Trackly observations.
93
+
94
+ Each checkpoint includes the expected member version, unchanged current
95
+ inspection epoch in both epoch fields, lease token, one to 25 typed actions, the shared
96
+ known-fields-committed flag, optional packet phase, and its own idempotency key.
97
+ Each action carries its continuation flag and optional redacted field
98
+ fingerprint. All actions in one checkpoint share one member lifecycle. The
99
+ member version advances once, but only a browser bind or reclaim may advance the
100
+ inspection epoch. Never add raw labels, options, answers, or page text to this
101
+ packet.
102
+
103
+ Use `packetPhase: first_pass` while inventorying the frozen set. Use
104
+ `packetPhase: delta` only for a conditional question or action that became
105
+ visible after the first grouped packet. A per-member conflict does not cancel
106
+ successful siblings; refresh only the conflicted member before retrying it.
107
+ When the user has completed or answered a prior human action, include its exact
108
+ server-returned ID in `resolvedActionIds` on the next checkpoint for that same
109
+ member and current inspection epoch. Never infer, fabricate, or resolve an
110
+ action from another member; an unknown or stale action ID must conflict.
111
+
112
+ ## Challenge placement
113
+
114
+ An access CAPTCHA or verification wall that hides the semantic form must be
115
+ recorded before private data is entered. Create a human action and continue the
116
+ batch.
117
+
118
+ A submit-time CAPTCHA beside an otherwise complete final form stays
119
+ `review_ready` with an `at_submit` human action because the user owns Submit.
120
+ Never solve, store, or bypass the challenge. Reserve terminal `blocked` for an
121
+ unrecoverable trust or observability failure, not for an answer the user can
122
+ provide.
123
+
124
+ ## Resume approval and truth
125
+
126
+ Resume approval is early and content-bound. It covers only immutable batch/run
127
+ membership, default-resume identity, exact content hash, user-facing filename,
128
+ size, profile revision, and expiry. Each upload still needs its own immediate
129
+ per-run local path proof for the exact bytes.
130
+
131
+ Exact local paths are user-visible local proof only and never leave the
132
+ machine. Content hashes may be sent only to authenticated Trackly resume
133
+ approval, prepared-resume verification, and truth-certification endpoints.
134
+ Never send either value to observations, application answers, analytics, logs,
135
+ or employer form fields.
136
+
137
+ Prepare the resume for every run that exposes a real Resume or CV control. Show
138
+ one consolidated proof with every run/path plus the shared resume identity,
139
+ filename, size, and SHA-256. After explicit user approval, call
140
+ `trackly_approve_apply_batch_resume` for the complete current run set. Reuse that
141
+ content approval only while every returned immutable dependency remains exact.
142
+ Ordinary checkpoints may advance member versions without invalidating approval
143
+ for unchanged resume bytes and run membership. Immediately before each
144
+ attachment, call `trackly_verify_prepared_resume` with that run's resume ID and
145
+ signed local proof.
146
+
147
+ Truth certification is late. Ask only after final answers and any conditional
148
+ wording are known. Bind it to the run set, answer snapshot, wording
149
+ fingerprints, profile revision, inspection epochs, and expiry. When at least
150
+ one form used a resume attachment, use `resumeDependency: approved` and bind
151
+ the exact approved resume identity. When no form in the batch exposes a resume
152
+ control, use `resumeDependency: not_applicable` with no resume ID or hash. It
153
+ is ephemeral evidence and never a reusable profile answer.
154
+
155
+ After all conditional questions and certification wording are visible, show one
156
+ final truthfulness prompt. Only after explicit user confirmation, compute the
157
+ value-free answer-snapshot and wording fingerprints and call
158
+ `trackly_certify_apply_batch_truth` for the exact complete subset that is
159
+ currently `review_ready`. A `needs_input` member does not block certification
160
+ or handoff of ready siblings. Never certify an arbitrary subset. When another
161
+ member becomes ready later, obtain a fresh certification for the then-current
162
+ complete `review_ready` subset. Never place the certification or its wording in
163
+ the application profile.
164
+
165
+ A membership, profile revision, resume identity or hash, answer snapshot,
166
+ certification wording, or affected inspection epoch change invalidates the
167
+ affected approval or attestation. An ordinary member-version checkpoint does
168
+ not invalidate an unchanged resume-content approval. Recompute and ask again
169
+ only for the invalidated scope.
170
+
171
+ ## Finish
172
+
173
+ Bring every accessible member to `review_ready`, preserving each browser tab
174
+ for manual submission. Certify, record, and hand off the exact current
175
+ `review_ready` subset without waiting for unrelated members. Members with
176
+ actions remain frozen and resumable, and require a fresh certification after
177
+ they later become ready. Report one compact table mapping job ID, run ID,
178
+ browser surface/tab label, ATS, state, actions, and evidence status. Raw tab
179
+ identifiers stay local.
180
+
181
+ Never submit a member. After manual submission, mark Applied only from an
182
+ observable success page or explicit user confirmation. Record the submit
183
+ request, success page or `user_confirmation`, and any provider receipt with
184
+ `trackly_record_apply_submission_evidence`; store only the redacted fingerprint
185
+ and typed source. Then reconcile tab closure separately using the
186
+ browser-lifecycle gate. Submission, receipt, and closure never substitute for
187
+ one another.