trackly-cli 0.16.0 → 0.17.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/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.6.0';
11
+ const SKILL_VERSION = '4.7.0';
12
12
  const CLI_USER_AGENT = `trackly-cli/${PACKAGE_VERSION}`;
13
13
  const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
14
14
 
@@ -263,7 +263,7 @@ const startApplyRunSchema = z.object({
263
263
  }
264
264
  });
265
265
 
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
+ const APPLY_RELIABILITY_PROMPT = 'Protocol 3.6 / skill 4.7.0 reliability gate: recover active work first. Retain the latest explicit target as hard, prove genuine applicant fields before counting access, obtain approval for the exact accessible jobs before form mutation, fill all deterministic fields before one true-gap question packet, and validate each value-free phase checkpoint. 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.';
267
267
 
268
268
  function registerApplyTools(
269
269
  server,
@@ -1199,7 +1199,7 @@ function registerApplyTools(
1199
1199
  content: { type: 'text', text: APPLY_RELIABILITY_PROMPT },
1200
1200
  }, {
1201
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.' },
1202
+ content: { type: 'text', text: 'Before generating questions or filling controls, run the skill 4.7.0 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.' },
1203
1203
  }, {
1204
1204
  role: 'user',
1205
1205
  content: { type: 'text', text: 'Only active=true identifies resumable execution work. Active=false and preserved=true is terminal read-only reconciliation evidence.' },
@@ -1207,7 +1207,7 @@ function registerApplyTools(
1207
1207
  role: 'user',
1208
1208
  content: {
1209
1209
  type: 'text',
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.',
1210
+ text: 'Protocol 3.6.0 reliability gate for new work: require MCP contract 3.7.3 and skill 4.7.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.',
1211
1211
  },
1212
1212
  }, {
1213
1213
  role: 'user',
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "trackly-cli",
9
- "version": "0.16.0",
9
+ "version": "0.17.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.16.0",
3
+ "version": "0.17.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.16.0",
20
+ "version": "0.17.0",
21
21
  "packages": [
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "trackly-cli",
25
- "version": "0.16.0",
25
+ "version": "0.17.0",
26
26
  "runtimeHint": "npx",
27
27
  "runtimeArguments": [
28
28
  {
@@ -20,15 +20,22 @@ Use Trackly as the source of truth for profile answers, documents, queue decisio
20
20
 
21
21
  ## Start every run
22
22
 
23
- Before selecting or resuming work, read
23
+ Before selecting, probing, or mutating work, read
24
+ [references/operational-checkpoints.md](references/operational-checkpoints.md),
25
+ [references/access-probe.md](references/access-probe.md),
24
26
  [references/batch-orchestration.md](references/batch-orchestration.md) and
25
- [references/browser-lifecycle.md](references/browser-lifecycle.md). Before generating an employer-form question packet, also read and run
27
+ [references/browser-lifecycle.md](references/browser-lifecycle.md). Read
28
+ [references/performance-telemetry.md](references/performance-telemetry.md) before
29
+ reusing a form schema, grouping templates, or emitting Apply telemetry. Before generating an employer-form question packet, also read and run
26
30
  [references/answer-resolution.md](references/answer-resolution.md). These
27
- references define the server-frozen batch, grouped-question, tab-reconciliation,
28
- and missing-surface recovery contract; they are mandatory even for a one-job batch.
31
+ references define the phase checkpoints, access state machine, server-frozen
32
+ batch, grouped-question, tab-reconciliation, and missing-surface recovery
33
+ contract; they are mandatory even for a one-job batch. Validate each local
34
+ value-free phase receipt with `scripts/validate-phase-checkpoint.js` before
35
+ claiming that phase complete.
29
36
  Read [references/legal-clarifications.md](references/legal-clarifications.md) when a form asks for a legal, regulatory, consent, or employer-relationship decision.
30
37
 
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.
38
+ 1. Call `trackly_get_apply_protocol`. Skill 4.7.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.
32
39
  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.
33
40
  3. Save answers with `trackly_update_application_profile`:
34
41
  - Use `answered`, `intentionally_blank`, `declined`, or `unknown` exactly.
@@ -73,7 +80,7 @@ Read [references/legal-clarifications.md](references/legal-clarifications.md) wh
73
80
  - 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.
74
81
  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.
75
82
  - 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.
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.
83
+ - 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.7.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.
77
84
  - 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.
78
85
  7. Pass the browser readiness gate before preparing resume bytes:
79
86
  - Use semantic browser control through Codex in-app browser controls, Chrome MCP/extension browser control, or Claude in Chrome.
@@ -123,7 +130,7 @@ Follow this order:
123
130
  - Use the canonical English school name from Trackly when the ATS offers that exact committed option. If a closed ATS selector does not offer it, select only a verified catalog option for the same institution; never leave free-typed text as though it were committed.
124
131
  - Treat partial dates as unknown at the missing precision. If Trackly has only a year but the ATS requires a month, ask once and sync the complete date before selecting either control. Never accept an ATS-selected current/default month or infer an education month.
125
132
  8. Use the canonical `consent.background_check_if_advanced` field only when the form explicitly asks for consent to a background check if the candidate advances. If it is unknown, ask before selecting it and save the answer at the user's chosen scope. Never infer it from privacy, demographic, recruiting-data, general application, criminal-record, or professional-reference consent. Treat the latter two as separate unknown consent questions unless the current profile schema supplies their own canonical fields.
126
- 9. For a free-text application response, read [references/application-writing.md](references/application-writing.md). Draft from supported profile and role facts, assemble the complete local claim-reference packet, set `claimsComplete: true` only after confirming that the packet covers the whole draft, then call `trackly_lint_application_text`. Do not enter the answer until deterministic lint passes and every claim has a supported evidence reference. Omitting claim metadata is a blocking lint failure, including for drafts the agent believes contain no claims. Do not require a separate writing or humanizer skill.
133
+ 9. For a free-text application response, read [references/application-writing.md](references/application-writing.md). Draft from supported profile and role facts, then run Humanizer automatically when available. Treat its output as a new revision: rebuild the complete local claim-reference packet against that exact final text, set `claimsComplete: true` only after confirming that the packet covers the whole revision, and then call `trackly_lint_application_text`. Apply the self-contained gate whether or not Humanizer is available, and do not block the application merely because Humanizer is unavailable. Do not enter the answer until the exact final revision passes deterministic lint and every claim has a supported evidence reference. Omitting claim metadata is a blocking lint failure, including for drafts the agent believes contain no claims.
127
134
  10. Run the full integrity gate, including the final consent checkbox, every visible error, all steps, and any correction banner.
128
135
 
129
136
  If browser startup or file work reports `ENOSPC`, `EACCES`, quota, or another I/O error, call `trackly_diagnose_local_path` on the exact implicated path before explaining the cause. Never generalize one path failure to the whole disk without matching measured evidence.
@@ -0,0 +1,68 @@
1
+ # Access-probe state machine
2
+
3
+ Use this reference for every candidate whose current access state is not already
4
+ durably terminal. A provider hint is scheduling input, never sufficient proof.
5
+
6
+ ## States
7
+
8
+ - `requisition_loaded`: the exact stored HTTPS requisition loaded and its
9
+ employer, role, requisition, and complete origin policy passed, including a
10
+ tenant only when the policy requires one.
11
+ - `apply_entry_found`: a genuine application-entry control exists.
12
+ - `intermediate_apply_shell`: a modal, chooser, overview, marketing shell,
13
+ guest landing page, resume-choice screen, or routing page. Accessibility is
14
+ still unknown.
15
+ - `applicant_fields_reached`: genuine applicant controls are visible,
16
+ semantically editable, and reachable without credentials.
17
+ - `authentication_required`: an existing-account login blocks applicant fields.
18
+ - `account_creation_required`: a new account blocks applicant fields.
19
+ - `otp_required`: email or SMS verification blocks applicant fields.
20
+ - `captcha_before_form`: human verification blocks applicant fields.
21
+ - `captcha_at_submit`: applicant fields are usable, but the user must solve a
22
+ final challenge at Submit.
23
+ - `inactive`: the exact posting is removed, closed, or no longer accepts
24
+ applications.
25
+ - `manual_only`: provider policy forbids automated form mutation.
26
+ - `unknown_unobservable`: the adapter cannot prove committed semantic controls.
27
+
28
+ ## Transition contract
29
+
30
+ 1. Begin from only the exact backend-stored requisition URL.
31
+ 2. Validate HTTPS, employer, role, requisition, and the complete authoritative
32
+ origin policy. For a vendor-hosted ATS, this includes the supplied tenant
33
+ rule and exact verified tenant. For `trackly_employer_source_exact_origin`,
34
+ validate only the exact listed origin; do not invent or require an ATS tenant.
35
+ 3. Follow ordinary non-sensitive application-entry controls while remaining in
36
+ `intermediate_apply_shell`.
37
+ 4. Stop only at genuine applicant controls or one typed blocker. An Apply button,
38
+ modal, guest shell, overview, or readable job page is not access proof.
39
+ 5. Never enter private data, credentials, create an account, solve OTP or
40
+ CAPTCHA, or change an applicant control during the probe.
41
+ 6. Count a job as accessible only after `applicant_fields_reached` is durably
42
+ proven and the active contract's `accessible` disposition is durably
43
+ recorded. Use `accessible`, not the local state name, in the access checkpoint.
44
+ `captcha_at_submit` may continue through review because the user owns Submit;
45
+ every other access wall is parked without consuming capacity.
46
+ 7. Revalidate origin, any policy-required tenant, and exact requisition after
47
+ every redirect.
48
+ 8. Keep `inactive` as the exact local page observation, but do not claim that
49
+ Trackly stored a disposition its active contract does not expose. Under MCP
50
+ contract 3.7.3, persist and checkpoint the conservative non-counting
51
+ `unknown_unobservable` disposition with `applicantControlsObserved: false`,
52
+ and report the local inactive reason separately without raw page text. Use a
53
+ future exact `inactive` disposition only when the fetched contract exposes
54
+ it. Preserve a typed control-plane conflict rather than inventing a
55
+ replacement run.
56
+
57
+ ## Provider evidence
58
+
59
+ | Provider or pattern | Insufficient | Accessible | Blocker |
60
+ |---|---|---|---|
61
+ | Greenhouse | Job description or Apply button | Editable name/contact or equivalent applicant controls on the exact tenant and requisition | Login or challenge before fields |
62
+ | Ashby | Overview tab | Application tab with editable applicant controls | Account or challenge before controls |
63
+ | Workday | Start Application modal or Apply Manually choice | Actual applicant-data page without a credential wall | Create Account, Sign In, OTP, or pre-form challenge means `account_creation_required`, `authentication_required`, `otp_required`, or `captcha_before_form` |
64
+ | Adobe or Microsoft style | Readable guest landing page or application shell | Editable applicant controls before login | Login before fields means `authentication_required` |
65
+ | Amazon | Job description and Apply button | Editable applicant fields before Amazon Jobs authentication | Amazon sign-in means `authentication_required` |
66
+
67
+ When browser and accessibility surfaces disagree, use
68
+ `unknown_unobservable`. Never upgrade uncertainty to accessibility.
@@ -8,7 +8,18 @@ Use this reference for free-text questions such as "Why this company?", motivati
8
8
  - These fields never block an application run when they are unknown. The user can continue with the plain default style for the current run or use saved style instructions; asking the user to paste a sample is a fallback only when they explicitly want to calibrate before a completed run provides approved text.
9
9
  - Only after durable submitted/applied reconciliation, offer once to save one to three of the user's approved free-text answers as the global `writing.voice_sample`. Show which answers would be included, require an explicit yes, and save nothing on silence or ambiguity. Because the field is sensitive, make the offer only with active sensitive-storage consent; otherwise ask for consent first or skip it. If the user chooses to decline a voice sample, save `writing.voice_sample` with state `declined` at global scope and no answer text so the offer is not repeated. The user may also choose intentionally blank style instructions.
10
10
  - Treat the sample and preferences as private user data. Never copy them into the public skill, logs, observations, or another user's defaults.
11
- - A separate humanizer or writing skill may be used when available, but it is optional. This gate remains authoritative and self-contained.
11
+ - When the Humanizer skill is available, run it automatically for every
12
+ supported employer-specific draft before the anti-slop gate. Humanizer is
13
+ mandatory when available; do not wait for the user to remind you. The
14
+ self-contained anti-slop gate remains the mandatory fallback when Humanizer
15
+ is unavailable and the final authority in every environment. Mark Humanizer
16
+ `available` only when the current session exposes the `humanizer` skill and
17
+ its `SKILL.md` can be read. A matching file elsewhere on disk is not runtime
18
+ availability. If either check fails, record `unavailable` and use the fallback.
19
+ - After Humanizer returns, treat the result as a new draft. Discard any earlier
20
+ claim-reference packet, rebuild it against the exact final revision, and run
21
+ deterministic lint only on that same revision. Never reuse a pre-Humanizer
22
+ lint result or claim proof.
12
23
 
13
24
  ## Draft from evidence
14
25
 
@@ -30,7 +41,14 @@ Before entering the response:
30
41
 
31
42
  1. Remove generic praise, inflated claims, vague transitions, boilerplate conclusions, and chatbot phrases.
32
43
  2. Rewrite `not just X, but Y`, ornamental rule-of-three lists, and dangling `-ing` clauses unless the user's sample clearly uses them naturally.
33
- 3. Resolve `writing.em_dash_policy`; unanswered defaults to `forbid`. Build the complete local claim-reference packet and set `claimsComplete: true` only after checking the whole draft. Call `trackly_lint_application_text` and treat its deterministic lint as a blocking gate. Missing claim metadata is a failure, even for an apparently claim-free draft. Never enter text with a failed lint result. `allow_if_voice_sample` requires explicit saved evidence that the approved sample uses that punctuation.
44
+ 3. Resolve `writing.em_dash_policy`; unanswered defaults to `forbid`, so the
45
+ default output contains no em dash. Build the complete local claim-reference
46
+ packet and set `claimsComplete: true` only after checking the whole draft.
47
+ Call `trackly_lint_application_text` and treat its deterministic lint as a
48
+ blocking gate. Missing claim metadata is a failure, even for an apparently
49
+ claim-free draft. Never enter text with a failed lint result or an unsupported
50
+ claim. `allow_if_voice_sample` requires explicit saved evidence that the
51
+ approved sample uses that punctuation.
34
52
  4. Vary sentence length and structure. Avoid a sequence of equally sized, equally formal sentences.
35
53
  5. Prefer active verbs, concrete nouns, real numbers, and named examples already supported by the profile.
36
54
  6. Read the answer aloud. If it sounds like a press release, generic cover letter, or assistant response, rewrite it.
@@ -0,0 +1,188 @@
1
+ # Operational checkpoints
2
+
3
+ Use this page as the run card. The detailed references define how to perform
4
+ each action; this page defines when a phase is allowed to advance.
5
+
6
+ ## Invariant checklist
7
+
8
+ - Keep the latest explicit target as the hard target. An earlier target never
9
+ returns after the user changes it. A smaller interim wave is not completion;
10
+ continue replacements until the hard target is reached or the server proves
11
+ queue exhaustion.
12
+ - Prove access to genuine applicant fields before counting a job as accessible.
13
+ - Show the exact jobs and require them to be approved before any form mutation.
14
+ Approval to inspect or probe is not approval to fill.
15
+ - After job approval, inventory and fill every known value before producing one
16
+ grouped question packet containing only true gaps.
17
+ - Resolve education, employment, confirmed EEO, authorization, resume, and
18
+ supported writing from the current Trackly profile and evidence. Never infer
19
+ a restricted or jurisdiction-specific fact.
20
+ - Run Humanizer for every supported employer-specific draft when that skill is
21
+ available. Always run the self-contained writing gate as the fallback and
22
+ final authority.
23
+ - Never activate Submit. The user owns the final submission action.
24
+ - Preserve every review-ready tab and unsaved draft across turns, handoffs, and
25
+ restarts.
26
+ - Reconcile positive submission evidence before changing Trackly state. Close a
27
+ completed tab only after durable `submitted` plus `applied_confirmed` proof and
28
+ the saved cleanup policy.
29
+
30
+ ## Execution mode decision
31
+
32
+ | User intent | Mode | Required behavior |
33
+ |---|---|---|
34
+ | Fill or apply to the next N accessible jobs | `complete_next_n_accessible` | Recover the active execution first; retain the latest explicit target; replace typed blockers until target or exhaustion. |
35
+ | Inspect the next N queue records | Fixed inspection | Freeze exactly N records; do not replace or replenish them. |
36
+ | Continue after tab, app, or context loss | Active recovery | Restore exact server lineage, tab state, and mutation authority as three independent receipts. |
37
+ | Reconcile work the user already submitted | Terminal reconciliation | Record current-epoch positive evidence before outcomes; do not reopen or refill forms. |
38
+
39
+ Never use the fixed-batch approval endpoint for an accessible execution. Use the
40
+ execution-scoped resume approval required by the current protocol. Never create
41
+ a replacement run for a member that already has one or for a queued member whose
42
+ run start conflicts; preserve it and report the typed control-plane conflict.
43
+
44
+ ## Two approval gates
45
+
46
+ 1. **Job approval:** after non-mutating access probes prove the exact accessible
47
+ set, show company, role, requisition identity when available, and any shortfall
48
+ from the hard target. No application control may be changed before approval.
49
+ 2. **Answer approval:** after approval, fill all deterministic controls first.
50
+ Ask once for only `missing_fact`, `live_consent`, or user-resolvable
51
+ `forbidden_inference` items that are visible now. A final truthfulness
52
+ certification remains per run.
53
+
54
+ ## Phase receipts
55
+
56
+ Keep these receipts local and value-free. Never include labels, answers, names,
57
+ emails, phone numbers, URLs, resume paths, demographics, or free-text content.
58
+ Before claiming a phase complete, resolve `<skill-dir>` as the directory that
59
+ contains the loaded `SKILL.md`, then run from any working directory:
60
+
61
+ ```text
62
+ node "<skill-dir>/scripts/validate-phase-checkpoint.js" <phase>
63
+ ```
64
+
65
+ Pass `{"receipt": {...}, "expectedContext": {...}}` on standard input. The
66
+ envelope permits only those two fields. `expectedContext` is required for every
67
+ phase and must be built only from the current authoritative work receipt. For
68
+ selection, include `workMode`, the authoritative `latestExplicitTarget`,
69
+ `batchId`, `queueExhausted`, and `selectableJobIds`: the access-proven job IDs
70
+ for an accessible execution or exact frozen member job IDs for a fixed
71
+ inspection. Include `executionId` only for an accessible execution. For access,
72
+ include the
73
+ exact current `waveJobIds`, including typed blockers. For later phases, include
74
+ the exact `approvedJobIds` from the validated selection or authoritative
75
+ recovered approval receipt. A nonzero exit means the
76
+ phase is not complete. The script accepts `selection`, `access`, `fill`,
77
+ `review`, and `reconciliation`.
78
+
79
+ ### Selection
80
+
81
+ Required fields:
82
+
83
+ - `workMode`: `accessible_execution` or `fixed_inspection`;
84
+ - positive safe-integer `batchId`, plus positive safe-integer `executionId` only
85
+ for an accessible execution;
86
+ - `latestExplicitTarget`: integer 1-20 for an accessible execution, or 1-100
87
+ for a fixed inspection, matching the public batch-size contract;
88
+ - `approvedJobIds`: unique positive safe-integer Trackly job IDs for the exact
89
+ approved set. This may be
90
+ empty only when the accessible queue is exhausted;
91
+ - `approvalRecorded: true`;
92
+ - `noFormMutationBeforeApproval: true`; and
93
+ - `queueExhausted`: boolean. A non-empty set smaller than the target is a valid
94
+ interim wave; only an empty set requires this to be true.
95
+
96
+ Every selection field must match `expectedContext`, and every approved job ID
97
+ must belong to `expectedContext.selectableJobIds`. The user's approval may
98
+ authorize a subset, but it can never introduce a job or shrink the hard target.
99
+
100
+ ### Access
101
+
102
+ Required fields:
103
+
104
+ - the same mode-specific current work lineage used by Fill: `workMode`,
105
+ `batchId`, `memberId`, `jobId`, `runId`, and non-negative `inspectionEpoch`,
106
+ plus `executionId` only for `accessible_execution`; every value must match
107
+ the current `expectedContext`, and `jobId` must belong to its exact
108
+ `waveJobIds` set. This set includes terminal blockers so their required probe
109
+ receipts can validate before replacement scheduling;
110
+ - a terminal classification supported by the active durable disposition
111
+ contract. Under contract 3.7.3, locally observed `applicant_fields_reached`
112
+ uses durable `accessible`, while a locally observed `inactive` posting uses
113
+ conservative `unknown_unobservable`; never put either unsupported local state
114
+ name in the receipt or claim Trackly stored it;
115
+ - `exactRequisitionVerified`, `originPolicyVerified`, and
116
+ `nonMutatingProbe` all true;
117
+ - `privateDataEntered: false`; and
118
+ - `applicantControlsObserved: true` only for durable `accessible` and
119
+ `captcha_at_submit`.
120
+
121
+ ### Fill
122
+
123
+ Required fields:
124
+
125
+ - `workMode`: `accessible_execution` or `fixed_inspection`;
126
+ - positive safe-integer `batchId`, `memberId`, `jobId`, and `runId`, plus a
127
+ non-negative safe-integer `inspectionEpoch`; every value must equal the same
128
+ field in `expectedContext`, and `jobId` must belong to
129
+ `expectedContext.approvedJobIds`;
130
+ - positive safe-integer `executionId` for `accessible_execution`, matching
131
+ `expectedContext`; omit it from both objects for `fixed_inspection`;
132
+ - `visibleControlCount`, `committedControlCount`, and `typedExceptionCount` as
133
+ non-negative safe integers, where committed plus exceptions equals visible;
134
+ - `knownOmissionCount: 0`;
135
+ - `knownFieldsFilledBeforeQuestions: true`;
136
+ - `parserSensitiveFieldsRechecked: true`;
137
+ - `educationAndEmploymentVerified: true`;
138
+ - `writingPresent`: boolean;
139
+ - `localWritingGate`: `passed` when writing is present, otherwise
140
+ `not_applicable`;
141
+ - `humanizerAvailability`: `available`, `unavailable`, or `not_applicable`;
142
+ - `humanizerRan: true` when Humanizer is available;
143
+ - `humanizerFallbackUsed: true` only when writing is present and Humanizer is
144
+ unavailable. This names the fallback path; `localWritingGate` is the
145
+ always-run final authority; and
146
+ - `questionPacketTrueGapsOnly: true`.
147
+
148
+ The fill receipt fails when any known visible control lacks either a committed
149
+ receipt or a typed exception.
150
+
151
+ ### Review
152
+
153
+ Required fields:
154
+
155
+ - the same mode-specific lineage as Fill: `workMode`, `batchId`, `memberId`,
156
+ `jobId`, `runId`, and non-negative `inspectionEpoch`, plus `executionId` only
157
+ for `accessible_execution`; every value must match the current
158
+ `expectedContext`, and `jobId` must remain in its approved set;
159
+ - `finalIntegrityPassed`, `truthConfirmationRecorded`,
160
+ `reviewTabPreserved`, and `userVisibleHandoffProven` all true; and
161
+ - `submitActivated: false`.
162
+
163
+ ### Reconciliation
164
+
165
+ Required fields:
166
+
167
+ - the same mode-specific current work lineage and approved-job binding required
168
+ by Review. Reconciliation stays bound to the submitted run even though it is
169
+ a post-submission phase;
170
+ - `positiveSubmissionEvidenceRecorded: true`;
171
+ - `memberLifecycle: submitted`;
172
+ - `tracklyJobStatus: applied_confirmed`;
173
+ - `cleanupPreference`: `never`, `submitted_only`, or
174
+ `submitted_and_probe_blockers`;
175
+ - `browserTabStatus`: `open`, `missing`, `closure_unverified`, or
176
+ `closed_verified`; `missing` records a durable missing-tab observation but is
177
+ not closure proof; and
178
+ - for `closed_verified`, `completeTabInventoryRecorded`,
179
+ `closeReceiptRecorded`, and `postCloseUnionAbsenceProven` true.
180
+
181
+ `cleanupPreference: never` forbids `closed_verified`, even when closure evidence
182
+ exists, because evidence cannot override the user's saved no-close policy.
183
+ For `open`, `missing`, or `closure_unverified`, all three close-proof fields must
184
+ be false or omitted.
185
+
186
+ Every final report must state the application lifecycle, Trackly job status,
187
+ browser tab status, and whether absence is actually `closed_verified`. These
188
+ are separate facts and none implies another.
@@ -0,0 +1,61 @@
1
+ # Performance and value-free telemetry
2
+
3
+ Improve speed only by reusing structure and batching requests. Never weaken
4
+ identity, consent, committed-control, preservation, or Submit-boundary gates.
5
+
6
+ ## Value-free schema cache
7
+
8
+ Cache only normalized control fingerprints, allowlisted opaque semantic-label
9
+ codes, control types, requiredness, conditional dependencies,
10
+ provider/tenant/template identity, interaction strategy, commit-verification
11
+ strategy, parser/rerender risk flags, and cache version/expiry.
12
+
13
+ Never cache raw semantic-label text, raw answers, contact values, demographic
14
+ values, resume text or paths, credentials, URLs, employer-form free text, or
15
+ any label code outside the allowlist.
16
+
17
+ Invalidate on a profile revision, form schema fingerprint, inspection epoch,
18
+ meaningful wording change, provider/tenant mismatch, or cache expiry. A cache hit
19
+ skips structural rediscovery only; reread and verify every committed live value.
20
+
21
+ ## Batch resolver and template grouping
22
+
23
+ - Fetch the current profile once per stable revision and request only keys
24
+ required by visible forms.
25
+ - Resolve a shared question once only when scope, expected type, employer,
26
+ office, jurisdiction, and wording semantics all match.
27
+ - Group same-tenant forms with identical normalized control fingerprints into
28
+ one structural fill plan.
29
+ - Apply that plan independently to every exact run and tab. Keep requisition
30
+ identity, provenance, committed-value verification, and browser bindings
31
+ separate.
32
+ - Deduplicate missing questions, not committed-control receipts.
33
+ - Preserve bounded bulk dispositions, observations, checkpoints, and outcomes.
34
+ After a bulk 5xx, refetch before deciding whether to retry or split.
35
+
36
+ ## Events
37
+
38
+ Emit these only when the current protocol or analytics surface explicitly
39
+ supports them:
40
+
41
+ - `apply_probe_stage_reached`
42
+ - `apply_access_disposition_committed`
43
+ - `apply_member_run_start_conflict`
44
+ - `apply_form_schema_cache_hit`
45
+ - `apply_form_schema_cache_miss`
46
+ - `apply_profile_resolution_completed`
47
+ - `apply_known_field_omitted`
48
+ - `apply_control_commit_corrected`
49
+ - `apply_resume_parser_repair`
50
+ - `apply_question_packet_generated`
51
+ - `apply_free_text_humanized`
52
+ - `apply_handoff_receipt_committed`
53
+ - `apply_submission_evidence_committed`
54
+ - `apply_outcome_reconciled`
55
+ - `apply_tab_closed_verified`
56
+
57
+ Use only provider/support level, scenario code, opaque execution/member/run IDs,
58
+ inspection epoch/version, cache hit or miss, phase elapsed time, typed error code,
59
+ and counts by resolver class. Never include raw labels, answers, URLs, names,
60
+ emails, phone numbers, resume paths, demographics, or free-text content. Never
61
+ invent an event or field that the active protocol does not expose.
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { StringDecoder } = require('node:string_decoder');
5
+
6
+ const ACCESS_STATES = new Set([
7
+ 'accessible',
8
+ 'authentication_required',
9
+ 'account_creation_required',
10
+ 'otp_required',
11
+ 'captcha_before_form',
12
+ 'captcha_at_submit',
13
+ 'manual_only',
14
+ 'unknown_unobservable',
15
+ ]);
16
+
17
+ const CLEANUP_PREFERENCES = new Set([
18
+ 'never',
19
+ 'submitted_only',
20
+ 'submitted_and_probe_blockers',
21
+ ]);
22
+
23
+ const TAB_STATES = new Set(['open', 'missing', 'closure_unverified', 'closed_verified']);
24
+ const MAX_RECEIPT_BYTES = 64 * 1024;
25
+ const WORK_MODES = new Set(['accessible_execution', 'fixed_inspection']);
26
+ const COMMON_LINEAGE_FIELDS = ['workMode', 'batchId', 'memberId', 'jobId', 'runId', 'inspectionEpoch'];
27
+
28
+ const PHASE_FIELDS = {
29
+ selection: new Set([
30
+ 'workMode',
31
+ 'executionId',
32
+ 'batchId',
33
+ 'latestExplicitTarget',
34
+ 'approvedJobIds',
35
+ 'approvalRecorded',
36
+ 'noFormMutationBeforeApproval',
37
+ 'queueExhausted',
38
+ ]),
39
+ access: new Set([
40
+ 'workMode',
41
+ 'executionId',
42
+ 'batchId',
43
+ 'memberId',
44
+ 'jobId',
45
+ 'runId',
46
+ 'inspectionEpoch',
47
+ 'classification',
48
+ 'exactRequisitionVerified',
49
+ 'originPolicyVerified',
50
+ 'nonMutatingProbe',
51
+ 'privateDataEntered',
52
+ 'applicantControlsObserved',
53
+ ]),
54
+ fill: new Set([
55
+ 'workMode',
56
+ 'executionId',
57
+ 'batchId',
58
+ 'memberId',
59
+ 'jobId',
60
+ 'runId',
61
+ 'inspectionEpoch',
62
+ 'visibleControlCount',
63
+ 'committedControlCount',
64
+ 'typedExceptionCount',
65
+ 'knownOmissionCount',
66
+ 'knownFieldsFilledBeforeQuestions',
67
+ 'parserSensitiveFieldsRechecked',
68
+ 'educationAndEmploymentVerified',
69
+ 'writingPresent',
70
+ 'localWritingGate',
71
+ 'humanizerAvailability',
72
+ 'humanizerRan',
73
+ 'humanizerFallbackUsed',
74
+ 'questionPacketTrueGapsOnly',
75
+ ]),
76
+ review: new Set([
77
+ 'workMode',
78
+ 'executionId',
79
+ 'batchId',
80
+ 'memberId',
81
+ 'jobId',
82
+ 'runId',
83
+ 'inspectionEpoch',
84
+ 'finalIntegrityPassed',
85
+ 'truthConfirmationRecorded',
86
+ 'submitActivated',
87
+ 'reviewTabPreserved',
88
+ 'userVisibleHandoffProven',
89
+ ]),
90
+ reconciliation: new Set([
91
+ 'workMode',
92
+ 'executionId',
93
+ 'batchId',
94
+ 'memberId',
95
+ 'jobId',
96
+ 'runId',
97
+ 'inspectionEpoch',
98
+ 'positiveSubmissionEvidenceRecorded',
99
+ 'memberLifecycle',
100
+ 'tracklyJobStatus',
101
+ 'cleanupPreference',
102
+ 'browserTabStatus',
103
+ 'completeTabInventoryRecorded',
104
+ 'closeReceiptRecorded',
105
+ 'postCloseUnionAbsenceProven',
106
+ ]),
107
+ };
108
+
109
+ function requireTrue(errors, receipt, field) {
110
+ if (receipt[field] !== true) errors.push(`${field} must be true`);
111
+ }
112
+
113
+ function requireFalse(errors, receipt, field) {
114
+ if (receipt[field] !== false) errors.push(`${field} must be false`);
115
+ }
116
+
117
+ function requireTracklyId(errors, receipt, field) {
118
+ if (!Number.isSafeInteger(receipt[field]) || receipt[field] < 1) {
119
+ errors.push(`${field} must be a positive safe integer`);
120
+ }
121
+ }
122
+
123
+ function requireCount(errors, receipt, field) {
124
+ if (!Number.isSafeInteger(receipt[field]) || receipt[field] < 0) {
125
+ errors.push(`${field} must be a non-negative safe integer`);
126
+ }
127
+ }
128
+
129
+ function validateCurrentLineage(errors, receipt, expectedContext, jobSetField = 'approvedJobIds') {
130
+ if (!WORK_MODES.has(receipt.workMode)) errors.push('workMode must be accessible_execution or fixed_inspection');
131
+ for (const field of ['batchId', 'memberId', 'jobId', 'runId']) requireTracklyId(errors, receipt, field);
132
+ if (!Number.isSafeInteger(receipt.inspectionEpoch) || receipt.inspectionEpoch < 0) {
133
+ errors.push('inspectionEpoch must be a non-negative safe integer');
134
+ }
135
+ if (receipt.workMode === 'accessible_execution') {
136
+ requireTracklyId(errors, receipt, 'executionId');
137
+ } else if (receipt.workMode === 'fixed_inspection' && Object.hasOwn(receipt, 'executionId')) {
138
+ errors.push('executionId must be omitted for fixed_inspection');
139
+ }
140
+ if (!expectedContext || typeof expectedContext !== 'object' || Array.isArray(expectedContext)) {
141
+ errors.push('expectedContext is required');
142
+ return;
143
+ }
144
+ const expectedFields = new Set([...COMMON_LINEAGE_FIELDS, jobSetField]);
145
+ if (receipt.workMode === 'accessible_execution') expectedFields.add('executionId');
146
+ for (const field of Object.keys(expectedContext)) {
147
+ if (!expectedFields.has(field)) errors.push(`unexpected expectedContext field: ${field}`);
148
+ }
149
+ for (const field of COMMON_LINEAGE_FIELDS) {
150
+ if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
151
+ }
152
+ if (receipt.workMode === 'accessible_execution'
153
+ && receipt.executionId !== expectedContext.executionId) {
154
+ errors.push('executionId must match expectedContext');
155
+ }
156
+ if (receipt.workMode === 'fixed_inspection' && Object.hasOwn(expectedContext, 'executionId')) {
157
+ errors.push('expectedContext.executionId must be omitted for fixed_inspection');
158
+ }
159
+ if (!Array.isArray(expectedContext[jobSetField])
160
+ || expectedContext[jobSetField].some((id) => !Number.isSafeInteger(id) || id < 1)) {
161
+ errors.push(`expectedContext.${jobSetField} must contain positive safe integers`);
162
+ } else if (!expectedContext[jobSetField].includes(receipt.jobId)) {
163
+ errors.push(`jobId must belong to expectedContext.${jobSetField}`);
164
+ }
165
+ }
166
+
167
+ function validateSelection(receipt, expectedContext) {
168
+ const errors = [];
169
+ if (!WORK_MODES.has(receipt.workMode)) {
170
+ errors.push('workMode must be accessible_execution or fixed_inspection');
171
+ }
172
+ requireTracklyId(errors, receipt, 'batchId');
173
+ if (receipt.workMode === 'accessible_execution') {
174
+ requireTracklyId(errors, receipt, 'executionId');
175
+ } else if (receipt.workMode === 'fixed_inspection' && Object.hasOwn(receipt, 'executionId')) {
176
+ errors.push('executionId must be omitted for fixed_inspection');
177
+ }
178
+ const maximumTarget = receipt.workMode === 'fixed_inspection' ? 100 : 20;
179
+ if (!Number.isInteger(receipt.latestExplicitTarget)
180
+ || receipt.latestExplicitTarget < 1
181
+ || receipt.latestExplicitTarget > maximumTarget) {
182
+ errors.push(`latestExplicitTarget must be an integer from 1 to ${maximumTarget} for ${receipt.workMode || 'the selected work mode'}`);
183
+ }
184
+ if (!Array.isArray(receipt.approvedJobIds)) {
185
+ errors.push('approvedJobIds must be an array');
186
+ } else {
187
+ if (Number.isInteger(receipt.latestExplicitTarget)
188
+ && receipt.approvedJobIds.length > receipt.latestExplicitTarget) {
189
+ errors.push('approvedJobIds must not exceed latestExplicitTarget');
190
+ }
191
+ const normalized = receipt.approvedJobIds.filter((id) => Number.isSafeInteger(id) && id > 0);
192
+ if (normalized.length !== receipt.approvedJobIds.length) errors.push('approvedJobIds must contain positive safe integers');
193
+ if (new Set(normalized).size !== normalized.length) errors.push('approvedJobIds must be unique');
194
+ if (receipt.approvedJobIds.length === 0 && receipt.queueExhausted !== true) {
195
+ errors.push('approvedJobIds may be empty only when queueExhausted is true');
196
+ }
197
+ }
198
+ requireTrue(errors, receipt, 'approvalRecorded');
199
+ requireTrue(errors, receipt, 'noFormMutationBeforeApproval');
200
+ if (typeof receipt.queueExhausted !== 'boolean') errors.push('queueExhausted must be a boolean');
201
+ if (!expectedContext || typeof expectedContext !== 'object' || Array.isArray(expectedContext)) {
202
+ errors.push('expectedContext is required');
203
+ return errors;
204
+ }
205
+ const expectedFields = new Set(['workMode', 'batchId', 'latestExplicitTarget', 'selectableJobIds', 'queueExhausted']);
206
+ if (receipt.workMode === 'accessible_execution') expectedFields.add('executionId');
207
+ for (const field of Object.keys(expectedContext)) {
208
+ if (!expectedFields.has(field)) errors.push(`unexpected expectedContext field: ${field}`);
209
+ }
210
+ if (receipt.workMode !== expectedContext.workMode) errors.push('workMode must match expectedContext');
211
+ if (receipt.batchId !== expectedContext.batchId) errors.push('batchId must match expectedContext');
212
+ if (receipt.workMode === 'accessible_execution'
213
+ && receipt.executionId !== expectedContext.executionId) {
214
+ errors.push('executionId must match expectedContext');
215
+ }
216
+ if (receipt.workMode === 'fixed_inspection' && Object.hasOwn(expectedContext, 'executionId')) {
217
+ errors.push('expectedContext.executionId must be omitted for fixed_inspection');
218
+ }
219
+ if (receipt.latestExplicitTarget !== expectedContext.latestExplicitTarget) {
220
+ errors.push('latestExplicitTarget must match expectedContext');
221
+ }
222
+ if (receipt.queueExhausted !== expectedContext.queueExhausted) {
223
+ errors.push('queueExhausted must match expectedContext');
224
+ }
225
+ if (!Array.isArray(expectedContext.selectableJobIds)
226
+ || expectedContext.selectableJobIds.some((id) => !Number.isSafeInteger(id) || id < 1)
227
+ || new Set(expectedContext.selectableJobIds).size !== expectedContext.selectableJobIds.length) {
228
+ errors.push('expectedContext.selectableJobIds must contain unique positive safe integers');
229
+ } else if (Array.isArray(receipt.approvedJobIds)
230
+ && receipt.approvedJobIds.some((id) => !expectedContext.selectableJobIds.includes(id))) {
231
+ errors.push('approvedJobIds must belong to expectedContext.selectableJobIds');
232
+ }
233
+ return errors;
234
+ }
235
+
236
+ function validateAccess(receipt, expectedContext) {
237
+ const errors = [];
238
+ validateCurrentLineage(errors, receipt, expectedContext, 'waveJobIds');
239
+ if (!ACCESS_STATES.has(receipt.classification)) errors.push('classification must be a terminal access state');
240
+ requireTrue(errors, receipt, 'exactRequisitionVerified');
241
+ requireTrue(errors, receipt, 'originPolicyVerified');
242
+ requireTrue(errors, receipt, 'nonMutatingProbe');
243
+ requireFalse(errors, receipt, 'privateDataEntered');
244
+ if (['accessible', 'captcha_at_submit'].includes(receipt.classification)) {
245
+ requireTrue(errors, receipt, 'applicantControlsObserved');
246
+ } else {
247
+ requireFalse(errors, receipt, 'applicantControlsObserved');
248
+ }
249
+ return errors;
250
+ }
251
+
252
+ function validateFill(receipt, expectedContext) {
253
+ const errors = [];
254
+ validateCurrentLineage(errors, receipt, expectedContext);
255
+ for (const field of ['visibleControlCount', 'committedControlCount', 'typedExceptionCount', 'knownOmissionCount']) {
256
+ requireCount(errors, receipt, field);
257
+ }
258
+ if (Number.isSafeInteger(receipt.visibleControlCount)
259
+ && Number.isSafeInteger(receipt.committedControlCount)
260
+ && Number.isSafeInteger(receipt.typedExceptionCount)
261
+ && receipt.committedControlCount + receipt.typedExceptionCount !== receipt.visibleControlCount) {
262
+ errors.push('committedControlCount plus typedExceptionCount must equal visibleControlCount');
263
+ }
264
+ if (receipt.knownOmissionCount !== 0) errors.push('knownOmissionCount must be 0');
265
+ requireTrue(errors, receipt, 'knownFieldsFilledBeforeQuestions');
266
+ requireTrue(errors, receipt, 'parserSensitiveFieldsRechecked');
267
+ requireTrue(errors, receipt, 'educationAndEmploymentVerified');
268
+ if (typeof receipt.writingPresent !== 'boolean') errors.push('writingPresent must be a boolean');
269
+ if (!['passed', 'not_applicable'].includes(receipt.localWritingGate)) {
270
+ errors.push('localWritingGate must be passed or not_applicable');
271
+ }
272
+ if (!['available', 'unavailable', 'not_applicable'].includes(receipt.humanizerAvailability)) {
273
+ errors.push('humanizerAvailability is invalid');
274
+ }
275
+ if (typeof receipt.humanizerRan !== 'boolean') errors.push('humanizerRan must be a boolean');
276
+ if (typeof receipt.humanizerFallbackUsed !== 'boolean') errors.push('humanizerFallbackUsed must be a boolean');
277
+ if (receipt.writingPresent === true) {
278
+ if (receipt.localWritingGate !== 'passed') errors.push('localWritingGate must pass when writing is present');
279
+ if (receipt.humanizerAvailability === 'available') {
280
+ requireTrue(errors, receipt, 'humanizerRan');
281
+ requireFalse(errors, receipt, 'humanizerFallbackUsed');
282
+ } else if (receipt.humanizerAvailability === 'unavailable') {
283
+ requireFalse(errors, receipt, 'humanizerRan');
284
+ requireTrue(errors, receipt, 'humanizerFallbackUsed');
285
+ } else {
286
+ errors.push('humanizerAvailability must be available or unavailable when writing is present');
287
+ }
288
+ } else if (receipt.writingPresent === false) {
289
+ if (receipt.localWritingGate !== 'not_applicable') errors.push('localWritingGate must be not_applicable when writing is absent');
290
+ if (receipt.humanizerAvailability !== 'not_applicable') errors.push('humanizerAvailability must be not_applicable when writing is absent');
291
+ requireFalse(errors, receipt, 'humanizerRan');
292
+ requireFalse(errors, receipt, 'humanizerFallbackUsed');
293
+ }
294
+ requireTrue(errors, receipt, 'questionPacketTrueGapsOnly');
295
+ return errors;
296
+ }
297
+
298
+ function validateReview(receipt, expectedContext) {
299
+ const errors = [];
300
+ validateCurrentLineage(errors, receipt, expectedContext);
301
+ requireTrue(errors, receipt, 'finalIntegrityPassed');
302
+ requireTrue(errors, receipt, 'truthConfirmationRecorded');
303
+ requireFalse(errors, receipt, 'submitActivated');
304
+ requireTrue(errors, receipt, 'reviewTabPreserved');
305
+ requireTrue(errors, receipt, 'userVisibleHandoffProven');
306
+ return errors;
307
+ }
308
+
309
+ function validateReconciliation(receipt, expectedContext) {
310
+ const errors = [];
311
+ validateCurrentLineage(errors, receipt, expectedContext);
312
+ requireTrue(errors, receipt, 'positiveSubmissionEvidenceRecorded');
313
+ if (receipt.memberLifecycle !== 'submitted') errors.push('memberLifecycle must be submitted');
314
+ if (receipt.tracklyJobStatus !== 'applied_confirmed') errors.push('tracklyJobStatus must be applied_confirmed');
315
+ if (!CLEANUP_PREFERENCES.has(receipt.cleanupPreference)) errors.push('cleanupPreference is invalid');
316
+ if (!TAB_STATES.has(receipt.browserTabStatus)) errors.push('browserTabStatus is invalid');
317
+ if (receipt.cleanupPreference === 'never' && receipt.browserTabStatus === 'closed_verified') {
318
+ errors.push('browserTabStatus cannot be closed_verified when cleanupPreference is never');
319
+ }
320
+ if (receipt.browserTabStatus === 'closed_verified') {
321
+ requireTrue(errors, receipt, 'completeTabInventoryRecorded');
322
+ requireTrue(errors, receipt, 'closeReceiptRecorded');
323
+ requireTrue(errors, receipt, 'postCloseUnionAbsenceProven');
324
+ } else {
325
+ for (const field of ['completeTabInventoryRecorded', 'closeReceiptRecorded', 'postCloseUnionAbsenceProven']) {
326
+ if (Object.hasOwn(receipt, field) && receipt[field] !== false) {
327
+ errors.push(`${field} must be false or omitted unless browserTabStatus is closed_verified`);
328
+ }
329
+ }
330
+ }
331
+ return errors;
332
+ }
333
+
334
+ const VALIDATORS = {
335
+ selection: validateSelection,
336
+ access: validateAccess,
337
+ fill: validateFill,
338
+ review: validateReview,
339
+ reconciliation: validateReconciliation,
340
+ };
341
+
342
+ function validateCheckpoint(phase, receipt, expectedContext) {
343
+ if (!Object.hasOwn(VALIDATORS, phase)) return [`unknown phase: ${phase}`];
344
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) return ['receipt must be a JSON object'];
345
+ const unexpectedFields = Object.keys(receipt)
346
+ .filter((field) => !PHASE_FIELDS[phase].has(field))
347
+ .map((field) => `unexpected field: ${field}`);
348
+ return [...unexpectedFields, ...VALIDATORS[phase](receipt, expectedContext)];
349
+ }
350
+
351
+ async function readReceiptInput(stream) {
352
+ const decoder = new StringDecoder('utf8');
353
+ let input = '';
354
+ let inputBytes = 0;
355
+ let oversized = false;
356
+ for await (const chunk of stream) {
357
+ if (oversized) continue;
358
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
359
+ inputBytes += bytes.length;
360
+ if (inputBytes > MAX_RECEIPT_BYTES) {
361
+ oversized = true;
362
+ input = '';
363
+ continue;
364
+ }
365
+ input += decoder.write(bytes);
366
+ }
367
+ if (!oversized) input += decoder.end();
368
+ return { input, oversized };
369
+ }
370
+
371
+ async function main() {
372
+ const phase = process.argv[2];
373
+ if (process.stdin.isTTY) {
374
+ process.stderr.write('receipt envelope must be provided on standard input\n');
375
+ process.exitCode = 1;
376
+ return;
377
+ }
378
+ const { input, oversized } = await readReceiptInput(process.stdin);
379
+ if (oversized) {
380
+ process.stderr.write(`receipt must be at most ${MAX_RECEIPT_BYTES} bytes\n`);
381
+ process.exitCode = 1;
382
+ return;
383
+ }
384
+ let envelope;
385
+ try {
386
+ envelope = JSON.parse(input);
387
+ } catch (_) {
388
+ process.stderr.write('receipt must be valid JSON\n');
389
+ process.exitCode = 1;
390
+ return;
391
+ }
392
+ if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)
393
+ || !Object.hasOwn(envelope, 'receipt')) {
394
+ process.stderr.write('input must be an envelope with receipt and optional expectedContext\n');
395
+ process.exitCode = 1;
396
+ return;
397
+ }
398
+ const unexpectedEnvelopeFields = Object.keys(envelope)
399
+ .filter((field) => !['receipt', 'expectedContext'].includes(field));
400
+ if (unexpectedEnvelopeFields.length > 0) {
401
+ process.stderr.write(`${unexpectedEnvelopeFields.map((field) => `unexpected envelope field: ${field}`).join('\n')}\n`);
402
+ process.exitCode = 1;
403
+ return;
404
+ }
405
+ const errors = validateCheckpoint(phase, envelope.receipt, envelope.expectedContext);
406
+ if (errors.length > 0) {
407
+ process.stderr.write(`${errors.join('\n')}\n`);
408
+ process.exitCode = 1;
409
+ return;
410
+ }
411
+ process.stdout.write(`${phase} checkpoint valid\n`);
412
+ }
413
+
414
+ if (require.main === module) {
415
+ main().catch(() => {
416
+ process.stderr.write('unable to read receipt\n');
417
+ process.exitCode = 1;
418
+ });
419
+ }
420
+
421
+ module.exports = { readReceiptInput, validateCheckpoint };