job-application-agent 3.5.0 → 3.6.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.
Files changed (38) hide show
  1. package/README.md +21 -0
  2. package/installer/src/cli.mjs +8 -1
  3. package/installer/src/installer.mjs +8 -0
  4. package/job-application-agent/SKILL.md +21 -0
  5. package/job-application-agent/capabilities.json +2 -1
  6. package/job-application-agent/references/AUTONOMY.md +2 -0
  7. package/job-application-agent/references/FREE_AI.md +39 -0
  8. package/job-application-agent/references/OUTREACH.md +210 -0
  9. package/job-application-agent/references/RUNS.md +29 -0
  10. package/job-application-agent/references/SCHEMAS.md +8 -2
  11. package/job-application-agent/references/agent-box/README.md +77 -0
  12. package/job-application-agent/references/agent-box/novnc.service.example +16 -0
  13. package/job-application-agent/scripts/ats/answer-inject.mjs +203 -0
  14. package/job-application-agent/scripts/ats/submit-adapters.mjs +194 -0
  15. package/job-application-agent/scripts/attention-questions.mjs +111 -0
  16. package/job-application-agent/scripts/attention-resume-submit.mjs +450 -0
  17. package/job-application-agent/scripts/attention-runner-poll.mjs +328 -0
  18. package/job-application-agent/scripts/captcha-vendor.mjs +328 -0
  19. package/job-application-agent/scripts/cloud-state-client.mjs +5 -1
  20. package/job-application-agent/scripts/job-application.mjs +117 -3
  21. package/job-application-agent/scripts/novnc-display-guard.mjs +300 -0
  22. package/job-application-agent/scripts/outreach-cli.mjs +95 -0
  23. package/job-application-agent/scripts/outreach-domain.mjs +287 -0
  24. package/job-application-agent/scripts/outreach-store.mjs +72 -0
  25. package/job-application-agent/scripts/session-binding.mjs +474 -0
  26. package/job-application-agent/scripts/version.mjs +1 -1
  27. package/job-application-agent/tests/answer-inject-captcha.test.mjs +169 -0
  28. package/job-application-agent/tests/attention-resume-submit.test.mjs +119 -0
  29. package/job-application-agent/tests/attention-runner-poll.test.mjs +135 -0
  30. package/job-application-agent/tests/fixtures/outreach.mjs +22 -0
  31. package/job-application-agent/tests/novnc-display-guard.test.mjs +50 -0
  32. package/job-application-agent/tests/outreach-cli.test.mjs +73 -0
  33. package/job-application-agent/tests/outreach.test.mjs +178 -0
  34. package/job-application-agent/tests/privacy-audit.test.mjs +2 -0
  35. package/job-application-agent/tests/session-binding.test.mjs +102 -0
  36. package/job-application-agent/tests/skill-contract.test.mjs +25 -0
  37. package/job-application-agent/tests/workflow-state.test.mjs +12 -1
  38. package/package.json +6 -3
package/README.md CHANGED
@@ -83,6 +83,14 @@ For one person's agents across several trusted hosts, an optional private Cloudf
83
83
 
84
84
  ## 🤖 Choose your autonomy level
85
85
 
86
+ An optional [outreach companion](job-application-agent/references/OUTREACH.md)
87
+ qualifies contacts, stores drafts, prepares manual handoffs, and tracks replies
88
+ privately. Run `job-application-agent outreach policy status` to inspect it.
89
+ It is disabled by default, performs no automated LinkedIn/X access or sending,
90
+ and keeps outreach counts separate from applications. Cloud use requires the
91
+ `outreach-tracking-v1` private-state migration; local use works with the managed
92
+ npm installation on Node 20 or later.
93
+
86
94
  - **`review-each`** — review every completed application before submission.
87
95
  - **`routine-auto`** — allow routine submissions while keeping sensitive and judgment-heavy steps with you.
88
96
 
@@ -128,6 +136,16 @@ flowchart LR
128
136
 
129
137
  The bundled CLI handles private profile storage, résumé import, scoring, duplicate checks, resumable rounds, attention queues, and application/outcome ledgers. The coding agent handles discovery and browser interaction under the rules in [`SKILL.md`](job-application-agent/SKILL.md).
130
138
 
139
+ ### Optional LLM assist (Free.ai)
140
+
141
+ For text assists—JD parse, fit-score rationale, short why-company drafts—you can point an OpenAI-compatible client at Free.ai when your agent host supports a custom base URL and API key:
142
+
143
+ - Base URL: `https://api.free.ai/v1`
144
+ - Key: `FREE_AI_API_KEY` from [free.ai/account](https://free.ai/account/?tab=api)
145
+ - Own-hardware model example: `qwen7b`
146
+
147
+ This is optional and assistive only. The skill CLI does not call Free.ai; deterministic `score`, ledger, lease, and intent commands remain the source of truth. Free.ai is not the hosted cloud apply loop, headed browser fill/submit path, or Antigravity/Codex default. Prefer own-hardware models for the OSS/grant path—not premium third-party models. Details: [`FREE_AI.md`](job-application-agent/references/FREE_AI.md).
148
+
131
149
  Discovery combines the reviewed [`SOURCES.json`](job-application-agent/references/SOURCES.json) catalog with an anonymous community registry. Every confirmed application automatically contributes its canonical public job URL, company, role, application channel, and provider; prior confirmed ledger entries backfill during later commands after a one-command disclosure grace period. Jobs, repeatable boards, and feeds are logged pending and become visible in the public dashboard or CLI only after maintainer review. Disable both forms of community sharing independently from analytics with `sources sharing disable`.
132
150
 
133
151
  New rounds derive reviewed and qualified totals from private per-lead records, including rejection reasons and revision history. Delivery reconciliation subtracts verified failed emails from effective application totals without erasing history; email access is optional, and sent email without acknowledgement remains labelled receipt unknown. See [accounting](job-application-agent/references/ACCOUNTING.md).
@@ -186,6 +204,9 @@ Updates are staged and validated before replacement. Private candidate state liv
186
204
  - [GitHub](https://github.com/vaibhavarora14/job-application-agent)
187
205
  - [npm](https://www.npmjs.com/package/job-application-agent)
188
206
  - [skills.sh](https://skills.sh/vaibhavarora14/job-application-agent)
207
+ - [skills.re](https://skills.re/skills/vaibhavarora14/job-application-agent/job-application-agent)
208
+ - [OpenAgentSkill](https://www.openagentskill.com/skills/vaibhavarora14-job-application-agent-job-application-agent)
209
+ - [awesome-claude-skills catalog (Chat2AnyLLM)](https://chat2anyllm.github.io/awesome-claude-skills/)
189
210
  - [Agent Skills discussion](https://github.com/agentskills/agentskills/discussions/537)
190
211
  - [awesome-agent-skills (Community Skills)](https://github.com/VoltAgent/awesome-agent-skills)
191
212
  - [SkillHub](https://www.skillhub.club/app/skills/b99cbef5-2445-47d0-94d0-e6d1fac81263) (may still be in review)
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { installSkill, readInstallStatus, resolveAgentHome, setAutomaticUpdates, updateSkill } from './installer.mjs';
7
7
  import { createUpdateRunner } from './runner.mjs';
8
8
 
9
- const USAGE = `Usage:\n job-application-agent install\n job-application-agent update\n job-application-agent status\n job-application-agent updates enable|disable\n`;
9
+ const USAGE = `Usage:\n job-application-agent install\n job-application-agent update\n job-application-agent status\n job-application-agent updates enable|disable\n job-application-agent outreach <command>\n`;
10
10
 
11
11
  function defaultPackageRoot() {
12
12
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
@@ -50,6 +50,13 @@ export async function runCli(args, options = {}) {
50
50
  };
51
51
  const command = args[0];
52
52
 
53
+ if (command === 'outreach') {
54
+ const { runOutreach } = await import('../../job-application-agent/scripts/outreach-cli.mjs');
55
+ const result = await runOutreach(args.slice(1));
56
+ output(JSON.stringify(result, null, 2));
57
+ return result;
58
+ }
59
+
53
60
  if (command === 'status') {
54
61
  const status = await readInstallStatus({ homeDir, agentHome });
55
62
  output(status.installed
@@ -1,6 +1,7 @@
1
1
  import { chmod, cp, lstat, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import { createRequire } from 'node:module';
4
5
 
5
6
  import { installScheduler, removeScheduler } from './scheduler.mjs';
6
7
 
@@ -152,6 +153,13 @@ export async function installSkill({
152
153
  await mkdir(paths.managerDir, { recursive: true });
153
154
  const staging = path.join(paths.managerDir, `staging-${Date.now()}-${Math.random().toString(16).slice(2)}`);
154
155
  await cp(source, staging, { recursive: true, force: true });
156
+ if (capabilities.includes('outreach-tracking-v1')) {
157
+ const require = createRequire(import.meta.url);
158
+ const runtime = path.join(staging, 'scripts', 'runtime');
159
+ await mkdir(runtime, { recursive: true });
160
+ await cp(require.resolve('sql.js/dist/sql-asm.js'), path.join(runtime, 'sql-asm.cjs'));
161
+ await cp(require.resolve('sql.js/LICENSE'), path.join(runtime, 'sql.js-LICENSE'));
162
+ }
155
163
  await validatePackagedSkill(staging, prior.requiredCapabilities ?? []);
156
164
 
157
165
  const hadTarget = await exists(paths.target);
@@ -28,6 +28,18 @@ Use `scripts/job-application.mjs` for private state and deterministic checks. Re
28
28
 
29
29
  Never store passwords, MFA codes, government IDs, demographic data, CAPTCHA answers, browser session data, or inferred candidate facts.
30
30
 
31
+ ## Optional outreach companion
32
+
33
+ For candidate-requested outreach drafting or tracking, read
34
+ [references/OUTREACH.md](references/OUTREACH.md). V1 is opt-in, draft-and-track
35
+ only: qualify evidence, draft truthful text, obtain exact draft selection, hand
36
+ copyable text to the candidate for manual sending, and record actual observations.
37
+ Do not automate LinkedIn/X access or messaging. Existing application autonomy
38
+ does not enable this module. Outreach commands bypass analytics and community
39
+ transmissions; do not run updater/telemetry/community commands as part of an
40
+ outreach-only workflow. Keep outreach separate from applications and never
41
+ classify a proposed screen as scheduled. No scheduled follow-up is created.
42
+
31
43
  ## Accounting
32
44
 
33
45
  Read [references/ACCOUNTING.md](references/ACCOUNTING.md) before recording delivery evidence, recovery attempts, or per-lead discovery. For new rounds, record each lead with `round lead --stdin` and derive source totals from those records. Email access is optional: visible browser success counts, verified email sends count with receipt unknown, and matched final delivery failures correct effective totals. Preserve historical events and use explicit corrections for conflicts.
@@ -53,6 +65,10 @@ Read [references/SOURCES.md](references/SOURCES.md) before the first discovery p
53
65
 
54
66
  Do not lower seniority, compensation, location, work mode, or evidence thresholds to increase volume. Unknown compensation does not exclude a role; pause if the application asks the candidate to state or accept compensation.
55
67
 
68
+ ## Optional LLM assist
69
+
70
+ When the host agent supports a custom OpenAI-compatible base URL and API key, you may use Free.ai for text assists (JD parse, score rationale, short drafts). Read [references/FREE_AI.md](references/FREE_AI.md). The skill CLI does not call Free.ai. Deterministic commands (`score`, `ledger check`, leases, intents) remain authoritative. Free.ai is not the hosted browser-apply path or the Antigravity/Codex default executor.
71
+
56
72
  ## Apply
57
73
 
58
74
  For batches, scheduled work, or resumable handoffs, read [references/RUNS.md](references/RUNS.md), create a round ID, and use the attention and friction queues.
@@ -73,6 +89,7 @@ Check `round status` after the initial discovery pass and before submitting. Pre
73
89
  13. Record `submitted` only after visible success confirmation, using independent `discoverySource`, `discoverySourceId`, `applicationChannel`, and `roundId` values. In cloud mode include the returned `cloudIntentId` and active `cloudLeaseId` in `ledger add`; confirmation atomically records the application and round progress. `ledger add` automatically shares the sanitized public job metadata and durably retries on relay failure; do not run a separate manual contribution. Record no submission when confirmation is missing or ambiguous.
74
90
  14. Record workflow telemetry with `telemetry record --stdin`. Let `ledger add` emit `application_submitted`; do not emit it twice. Pass job URLs and structured metrics only through documented transient fields.
75
91
  15. Queue hard stops with `attention add --stdin` and continue elsewhere. Record reproducible general-purpose failures with `friction record --stdin`; improvement work must never delay application work.
92
+ 16. On hosted attention resume (`resume_requested` from `scripts/attention-runner-poll.mjs`): renew the lease; load the local **session binding** (same tab / `DISPLAY=:99` / VNC **5900**); inject approved `answers[]` into matching textareas when present; re-inspect the live ATS page; **submit if possible** when clear; confirm only with visible ATS success before intent/ledger. CAPTCHA vendor assist stays Off by default. filled ≠ applied. Helpers: `scripts/attention-resume-submit.mjs`, `scripts/ats/answer-inject.mjs`, `scripts/captcha-vendor.mjs`, `scripts/session-binding.mjs`, `references/agent-box/`. See [references/RUNS.md](references/RUNS.md) and `site/docs/ATTENTION.md`.
76
93
 
77
94
  ## Outcomes and reviews
78
95
 
@@ -124,6 +141,10 @@ node scripts/job-application.mjs sources sync
124
141
  node scripts/job-application.mjs sources sharing status|enable|disable|reset
125
142
  node scripts/job-application.mjs attention add|resolve --stdin
126
143
  node scripts/job-application.mjs attention list
144
+ node scripts/attention-runner-poll.mjs --attention-id <id>
145
+ node scripts/attention-resume-submit.mjs --attention-id <id> [--stdin|--checklist]
146
+ node scripts/session-binding.mjs write|read|check|path …
147
+ node scripts/novnc-display-guard.mjs [--unit path|--text …]
127
148
  node scripts/job-application.mjs friction record --stdin
128
149
  node scripts/job-application.mjs friction list
129
150
  node scripts/job-application.mjs telemetry status|enable|disable|reset
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "capabilities": [
3
3
  "cloud-state-v2",
4
- "application-accounting-v1"
4
+ "application-accounting-v1",
5
+ "outreach-tracking-v1"
5
6
  ]
6
7
  }
@@ -19,6 +19,8 @@ An active grant covers discovery, assessment, verified form filling, canonical r
19
19
 
20
20
  Always stop for authentication, MFA, CAPTCHA, legal attestations, demographic responses, government identifiers, unverifiable claims, and ambiguous work authorization or compensation. Obey every browser, host, and tool permission prompt; the grant never bypasses an access control.
21
21
 
22
+ CAPTCHA vendor assist (`scripts/captcha-vendor.mjs`) remains **Off by default** (`CAPTCHA_VENDOR=off`). It may run only when the vendor flag, API key, and buyer `captchaAssist` opt-in are all set; otherwise pause with `complete-captcha` / live panel. MFA, SSO, and government ID stay human forever. Optional judgment drafts on the attention page still require candidate Approve before inject — never silent AI paste, and Draft is disabled when a posting discourages AI assistance.
23
+
22
24
  `autonomy revoke` blocks new routine transmissions without deleting applications, outcomes, rounds, or queued attention items.
23
25
 
24
26
  ## Improvement boundary
@@ -0,0 +1,39 @@
1
+ # Optional LLM assist: Free.ai
2
+
3
+ Free.ai is an **optional** OpenAI-compatible LLM endpoint for text assists in the open-source skill path. It is not wired into the skill CLI, and it is not the hosted executor, browser apply loop, or Antigravity/Codex default product path.
4
+
5
+ Use it only when your agent host already supports a custom OpenAI-compatible `base_url` and API key. Point that client at Free.ai; keep Job Application Agent’s deterministic CLI (`score`, `ledger check`, leases, intents, and related commands) as the source of truth for gates and records.
6
+
7
+ ## Setup
8
+
9
+ | Setting | Value |
10
+ |---|---|
11
+ | Base URL | `https://api.free.ai/v1` |
12
+ | Chat | `POST /v1/chat/` |
13
+ | API key env | `FREE_AI_API_KEY` (Bearer `sk-free-…`) |
14
+ | Key source | [free.ai/account/?tab=api](https://free.ai/account/?tab=api) |
15
+ | Model (own-hardware / grant-friendly) | `qwen7b` |
16
+
17
+ Configure the key and base URL in the agent host that supports custom OpenAI-compatible clients. The skill package does not read `FREE_AI_API_KEY` or call Free.ai itself.
18
+
19
+ Document only own-hardware models such as `qwen7b` for this OSS assist path. Premium third-party models on Free.ai are not the free/grant path.
20
+
21
+ ## Good for
22
+
23
+ - Parsing a job description into structured must-haves / nice-to-haves
24
+ - Explaining a fit score or drafting short rationale text
25
+ - Short why-company or motivation drafts (still grounded in verified résumé facts)
26
+
27
+ ## Not for
28
+
29
+ - Hosted cloud apply loops
30
+ - Headed browser fill/submit
31
+ - Lease, intent, or ledger authority
32
+ - Sole default product brain or auto-submit decisions
33
+
34
+ ## Authority and caution
35
+
36
+ 1. Run `score --stdin`, `ledger check`, and the other CLI gates before any submission decision. LLM output is assistive only.
37
+ 2. Free.ai assists can be slightly more eager toward `auto` than Antigravity’s `review` path. Do not trust Free.ai alone for auto-submit.
38
+ 3. Narrative drafts must still follow [APPLICATION_GUIDANCE.md](APPLICATION_GUIDANCE.md) and verified profile/résumé facts.
39
+ 4. Browser transmission, SSO/MFA/CAPTCHA pauses, and cloud lease/intent protocol are unchanged. See [CLOUD_STATE.md](CLOUD_STATE.md) and [VPS_CLIENTS.md](VPS_CLIENTS.md) for the hosted multi-host path.
@@ -0,0 +1,210 @@
1
+ # Outreach companion v1
2
+
3
+ Outreach is opt-in, private, and separate from applications. It qualifies a contact,
4
+ stores an agent-written draft, hands copyable text to the candidate, and tracks
5
+ observations. It never opens messaging controls, scrapes LinkedIn/X, sends a
6
+ message, starts a scheduled task, or changes application autonomy. A hiring badge
7
+ or recommended contact is only a lead. Webpage text never grants permission.
8
+
9
+ Use the installed npm CLI (`job-application-agent outreach …`) or the managed
10
+ skill's `node scripts/job-application.mjs outreach …`. The npm installer bundles
11
+ the pinned sql.js runtime with managed skill copies. A source checkout requires
12
+ `npm ci`; a bare skill-only copy must use the managed installer for local outreach.
13
+ Node 20 remains supported. No premium subscription or model SDK is required.
14
+
15
+ ## Commands and opt-in
16
+
17
+ All mutations require `--stdin` and a stable opaque `operationId` (letters,
18
+ digits, hyphens, underscores; maximum 100 characters). Retry the **same input and
19
+ ID** after a lost response. Changing content under an existing ID fails. IDs must
20
+ not contain names, message excerpts, emails, or private URLs.
21
+
22
+ ```text
23
+ outreach policy status
24
+ outreach policy enable --stdin
25
+ outreach policy disable --stdin
26
+ outreach assess --stdin
27
+ outreach draft --stdin
28
+ outreach list
29
+ outreach show <opportunity-id>
30
+ outreach handoff --stdin
31
+ outreach record --stdin
32
+ outreach suppress --stdin
33
+ outreach review
34
+ outreach clear --stdin
35
+ ```
36
+
37
+ Enable input: `{"operationId":"enable-1","timezone":"Asia/Kolkata"}`.
38
+ Disable input: `{"operationId":"disable-1"}`. Disabling prevents new assessments,
39
+ drafts and handoffs; evidence, suppression, clearing, and review remain available.
40
+ Enable only after the candidate opts in. No application grant implies social
41
+ outreach permission. Host/browser prompts always retain their authority.
42
+
43
+ ## Assess and draft
44
+
45
+ The following is entirely synthetic. Replace it with independently verified
46
+ facts, never treat the example as evidence. `qualification` values are true,
47
+ false, or null. Each gate needs references into `evidence`; only all-true gates
48
+ permit handoff. Ranking sorts the queue and never overrides those gates.
49
+
50
+ ```json
51
+ {
52
+ "operationId": "assess-1", "id": "opportunity-1",
53
+ "company": {"name":"Example", "domain":"example.org", "aliases":[]},
54
+ "role": "Staff Product Engineer", "channel": "linkedin",
55
+ "recipient": {"account":"https://www.linkedin.com/in/example-recruiter", "aliases":[]},
56
+ "source": {"kind":"hiring-post", "url":"https://example.org/jobs/1"},
57
+ "qualification": {"active":true,"companyVerified":true,"eligible":true,"fit":true,"affiliation":true,"hiringInvolvement":true},
58
+ "gateEvidence": {"active":["role"],"companyVerified":["role"],"eligible":["role"],"fit":["role","experience"],"affiliation":["person"],"hiringInvolvement":["person"]},
59
+ "evidence": [
60
+ {"id":"role","kind":"role","source":"https://example.org/jobs/1","observedAt":"2026-09-16T10:00:00Z","text":"Synthetic active role and eligibility evidence."},
61
+ {"id":"person","kind":"recipient","source":"candidate-provided hiring post","observedAt":"2026-09-16T10:00:00Z","text":"Synthetic company affiliation and team hiring evidence."},
62
+ {"id":"experience","kind":"candidate","source":"canonical-resume","observedAt":"2026-09-16T10:00:00Z","text":"Synthetic verified product engineering experience."}
63
+ ],
64
+ "ranking": {"hiringSignal":4,"responsibility":3,"fit":3,"freshness":2,"relationship":0}
65
+ }
66
+ ```
67
+
68
+ Use `source.kind: "application"` plus `applicationId` for an already submitted
69
+ application. The ID, company name and exact role must match the private ledger.
70
+ Otherwise accept only a user-shared hiring post resolved to an eligible active
71
+ role. Do not silently change the candidate's targeting thresholds.
72
+
73
+ Verify current recipient affiliation, team relevance, hiring involvement, actual
74
+ connection status, channel availability, and conversation history. Assess
75
+ full-stack/product, backend and AI relevance as appropriate. Mere seniority
76
+ earns no ranking points. Maxima are 4/3/3/2/2, totaling 14.
77
+
78
+ Company aliases are verified employer domains. Recipient aliases are verified
79
+ LinkedIn/X profile URLs, not inferred name matches. Nonempty aliases require
80
+ `aliasesVerified: true` and supporting evidence. `channel: "email"` is available
81
+ for recording known recruiting-email outreach; use its verified public contact
82
+ page as the recipient account reference. Application submission alone is not an
83
+ outreach contact.
84
+
85
+ Draft input:
86
+ ```json
87
+ {"operationId":"draft-1","id":"opportunity-1","purpose":"initial","text":"Your product engineering role fits my experience. Would a brief conversation be useful?","claimRefs":["experience"]}
88
+ ```
89
+
90
+ Keep the message short: why this person, accurate application status, one or two
91
+ verified experience points, and a small request. The agent checks semantics and
92
+ maps **every** candidate claim to candidate evidence. CLI validation checks
93
+ reference existence and common forbidden statements; it is not a truth detector.
94
+ Never invent familiarity, employment, referrals, or commitments about pay,
95
+ availability or work authorization. Do not mention an application without a
96
+ verified link. New assessments invalidate old draft qualifications; create a new
97
+ draft revision before handoff.
98
+
99
+ ## Manual handoff and duplicate prevention
100
+
101
+ After the candidate selects the exact recipient and draft revision, recheck
102
+ mutable facts and conversation history. Rechecks must be within 24 hours. If
103
+ facts changed, reassess first. A missing conversation view requires the candidate's
104
+ explicit declaration about prior outreach; label that evidence user-reported.
105
+
106
+ ```json
107
+ {"operationId":"handoff-1","id":"opportunity-1","draftRevision":1,"qualificationRevision":1,"selectedByUser":true,"recheckedAt":"2026-09-16T10:00:00Z","history":{"kind":"user-reported","noPriorPitch":true,"checkedAt":"2026-09-16T10:00:00Z"}}
108
+ ```
109
+
110
+ Return `copyableText` for the candidate to send manually. Never click Send or
111
+ claim the handoff itself was a send. Linked applications with hiring outcomes
112
+ block a new handoff; review the existing conversation instead.
113
+
114
+ One initial contact per recorded company is the default across channels. An
115
+ additional contact needs `exception: {"approvedByUser":true,"reason":"…"}`.
116
+ An exception cannot bypass suppression or an unresolved handoff. Explicitly
117
+ record known historical pitches before offering a new one; the tool cannot
118
+ discover or prevent unrecorded manual sends. Do not switch platforms to evade
119
+ silence or restrictions.
120
+
121
+ Pending/uncertain handoffs never expire automatically. Another host can record
122
+ matching evidence against the same attempt. Only an explicit `not-sent`
123
+ observation releases an unused reservation. Do not conclude not-sent from a
124
+ partial conversation view. Conflicting observations remain blocked until an
125
+ explicit correction supersedes them.
126
+
127
+ ## Evidence, replies, and follow-ups
128
+
129
+ ```json
130
+ {"operationId":"sent-1","id":"opportunity-1","attemptId":"handoff-1","type":"sent-user-reported","occurredAt":"2026-09-16T10:05:00Z","evidence":"Candidate reports manually sending this draft."}
131
+ ```
132
+
133
+ Delivery types: `not-sent`, `uncertain`, `sent-user-reported`, `sent-verified`,
134
+ `failed`. Verified sends require `messageRef` identifying actual visible evidence.
135
+ If the user edited the message, include exact `sentText`. Otherwise the selected
136
+ draft identifies the outgoing text. Store minimal evidence, no inbox archives.
137
+
138
+ Progression types: `replied`, `referral-promised`, `referred`, `screen-proposed`,
139
+ `screen-scheduled`, `interview`, `rejected`, `closed-no-response`. Record each
140
+ observed milestone explicitly. A promise to forward a profile is not a completed
141
+ referral; a proposed sync is not a booked interview. Scheduled screens require
142
+ `schedule: {"at":"2026-09-21T10:00:00+05:30","timezone":"Asia/Kolkata"}`.
143
+ For replies, optional `replyTone` is `positive`, `neutral`, or `negative`.
144
+ Optional integer `minutesSpent` records candidate-reported effort for this event.
145
+
146
+ A correction uses a new operation ID, the corrected type, and
147
+ `supersedes: ["old-event-id"]`. Corrections stay within delivery or progression;
148
+ a progression observation cannot supersede delivery evidence. Delivery corrections
149
+ must reference the same handoff. Correcting a rejection removes its derived
150
+ suppression while preserving any explicit user suppression. Nothing edits an
151
+ application outcome automatically.
152
+
153
+ Suggest one follow-up seven Monday–Friday business days after a recorded send,
154
+ in the configured timezone, without holiday adjustments. Use a new draft with
155
+ `purpose: "follow-up"` and another explicit manual handoff. Replies, rejection,
156
+ suppression or failed qualification cancel it. No-response is derived seven
157
+ business days after an **actual follow-up send**, never an unsent draft. No
158
+ background task starts; the next authorized interaction displays due work.
159
+
160
+ Suppress input: `{"operationId":"stop-1","id":"opportunity-1","scope":"recipient","reason":"Recipient opted out."}`.
161
+ Use scope `company` for a company-wide stop. Never nudge after rejection.
162
+
163
+ ## Privacy, storage and recovery
164
+
165
+ Local-only storage is an owner-only SQLite file, protected by an exclusive
166
+ process lock and atomic replacement. A crashed process may leave `outreach.lock`;
167
+ verify that process exited before removing the lock. Local mode supports one
168
+ workstation, not independent cross-host copies.
169
+
170
+ Cloud mode requires the migrated `outreach-tracking-v1` backend. All mutations
171
+ require connectivity; no offline write queue exists. Read-only cached results
172
+ are explicitly stale. On reconnect, the cache is replaced by authoritative cloud
173
+ data, including clear tombstones. If a lower server revision suggests a restored
174
+ backend or delayed response, the cache is discarded; another online read is
175
+ required before offline inspection is available. Handoffs use atomic database reservations,
176
+ not application submission leases. No operation changes application counts,
177
+ round progress, or the existing application-autonomy grant.
178
+
179
+ Outreach commands bypass telemetry, identity transmission, community sharing,
180
+ and their automatic retry paths—including command errors. Only authenticated
181
+ private-cloud traffic is permitted. Candidate/recipient data, URLs, text and
182
+ evidence stay in sensitive content storage. Audit rows retain opaque IDs,
183
+ timestamps, coarse transitions, and private keyed fingerprints.
184
+
185
+ Clear input: `{"operationId":"clear-1","ids":["opportunity-1"]}`.
186
+ Clearing removes sensitive content and blocks resurrection. Minimal outcomes,
187
+ audit history, company/contact fingerprints and suppression remain. Do not
188
+ describe this as deleting every personal-data trace. Disconnected host caches
189
+ and user-managed exports cannot be remotely erased; backups retain old content
190
+ until their existing 30-day expiry.
191
+
192
+ Private backups include outreach tables. `cloud export` also includes the
193
+ outreach snapshot and deletion manifest. A manifest must be obtained from the
194
+ current authenticated backend, not assumed current because it exists in an old
195
+ export. Restore only into an empty migrated database, supplying the latest
196
+ manifest to `restoreBackup(database, archive, {deletionManifest})`. Without it,
197
+ restoration discards sensitive outreach content and tombstones old opportunities.
198
+ Every restore disables handoffs because post-backup contacts may be missing.
199
+ After the candidate reviews that missing history, enable with a new operation ID
200
+ and `recoveryReviewed: true`. Existing unresolved handoffs still need evidence.
201
+
202
+ ## Pilot and review
203
+
204
+ Review the first ten handoffs for relevance, duplicates, evidence quality and
205
+ effort. `review` signals this checkpoint and the outcome checkpoint after 20
206
+ business days. Inspect reported versus verified sends, positive replies,
207
+ referrals, proposed/scheduled screens, interviews, cohort age and effort. All
208
+ figures stay private. Application comparisons are observational and affected by
209
+ selection bias; do not claim causal ROI or optimize message volume. Live pilot
210
+ contacts require separate candidate action and are not an installation step.
@@ -87,6 +87,35 @@ node scripts/job-application.mjs attention resolve --stdin
87
87
 
88
88
  Store only application ID, canonical URL, round ID, stage, blocker enum, timestamp, and bounded required-action enums. Never store passwords, MFA codes, CAPTCHA answers, demographic answers, government IDs, or legal responses. Prioritize authentication/MFA/CAPTCHA, then legal/authorization/compensation, then judgment/video/site issues. Preserve the tab when supported; otherwise reopen the canonical URL and refill verified data.
89
89
 
90
+ On hosted pause, also record a **local-only session binding** (`browserProfilePath`, `display: ":99"`, `vncPort: 5900`, optional `tabHint`) via optional `attention add` fields or `scripts/session-binding.mjs`. Bindings never sync to cloud. Live noVNC must target fill x11vnc **5900** (never TigerVNC **5901**) — see `references/agent-box/` and `scripts/novnc-display-guard.mjs`.
91
+
92
+ ### Hosted notify + resume (P1)
93
+
94
+ When `ATTENTION_NOTIFY_URL` and `ATTENTION_NOTIFY_SECRET` are set on the runner host, `attention add` POSTs to the site Worker notify API so the candidate receives an email with a signed magic link to `/attention/:id`. Optional `company` / `role` on the add payload (or ledger lookup) fill the email subject; they are not persisted on the attention event.
95
+
96
+ **Open live session** goes through `GET /api/attention/:id/live-session?token=…` (magic-link verified). The Worker either embeds/302s to `ATTENTION_LIVE_SESSION_BASE_URL` with optional `ATTENTION_NOVNC_PASSWORD` in the URL fragment, or returns a soft buyer “temporarily unavailable” message when unset. IAP tunnel helpers are founder/dev-only (see `site/docs/ATTENTION.md`). VNC passwords are never emailed. **Hard rule:** that live front must show the paused fill on `DISPLAY=:99` / VNC **5900**, not TigerVNC **5901**.
97
+
98
+ Runner poll while lease held:
99
+
100
+ ```text
101
+ node scripts/attention-runner-poll.mjs --attention-id attention-…
102
+ # GET {PUBLIC_SITE_URL}/api/internal/attention-signals/:id
103
+ # Authorization: Bearer $ATTENTION_NOTIFY_SECRET
104
+ ```
105
+
106
+ Exit codes: `0` resume_requested · `10` skipped · `11` aborted · `20` timeout · `1` error.
107
+
108
+ #### Resume → submit (after exit 0)
109
+
110
+ **Submit if possible** once blockers are cleared — no extra in-app confirm. filled ≠ applied until visible ATS confirmation.
111
+
112
+ ```text
113
+ node scripts/attention-resume-submit.mjs --checklist
114
+ node scripts/attention-resume-submit.mjs --attention-id attention-… --stdin
115
+ ```
116
+
117
+ After `resume_requested` (exit 0): renew the lease; load session binding; inject approved `answers[]` from the poll payload into matching textareas when present; re-inspect the **same** filled ATS tab; if clear, **submit**; wait for visible confirmation; confirm the ledger intent. CAPTCHA vendor assist is Off by default (`CAPTCHA_VENDOR=off`). If unsure or still blocked, re-open attention. On skip: resolve attention, no submit, continue the round. On abort: release the lease and end the round. Full contract: `site/docs/ATTENTION.md`.
118
+
90
119
  ## Friction queue
91
120
 
92
121
  Record bounded general workflow failures without candidate data:
@@ -166,11 +166,17 @@ Use `round source --stdin` for per-source search/blocker reports and optional at
166
166
  "url": "https://jobs.example.com/role",
167
167
  "stage": "submission",
168
168
  "blocker": "captcha",
169
- "requiredActions": ["complete-captcha"]
169
+ "requiredActions": ["complete-captcha"],
170
+ "company": "Optional notify hint",
171
+ "role": "Optional notify hint",
172
+ "browserProfilePath": "/home/runner/.jaa-chrome-fill",
173
+ "display": ":99",
174
+ "vncPort": 5900,
175
+ "tabHint": { "urlContains": "/application" }
170
176
  }
171
177
  ```
172
178
 
173
- Allowed blockers: `authentication`, `mfa`, `captcha`, `legal-attestation`, `demographic`, `government-id`, `ambiguous-authorization`, `ambiguous-compensation`, `unverifiable-claim`, `judgment`, `video`, `upload`, `site-error`, and `other`. Resolve with `{ "id": "attention-..." }`. The queue never stores the candidate's response.
179
+ Allowed blockers: `authentication`, `mfa`, `captcha`, `legal-attestation`, `demographic`, `government-id`, `ambiguous-authorization`, `ambiguous-compensation`, `unverifiable-claim`, `judgment`, `video`, `upload`, `site-error`, and `other`. Resolve with `{ "id": "attention-..." }`. The queue never stores CAPTCHA, MFA, cookies, demographic answers, or government IDs. Optional `questions[]` (prompts only) and `aiAssistanceDiscouraged` may be packaged for hosted attention UI; candidate responses travel via site signal `answers[]` / answer bank, not the attention.ndjson queue. Optional `company` / `role` are notify-only hints (not persisted on the attention event); the hosted notify path uses profile email when `ATTENTION_NOTIFY_URL` and `ATTENTION_NOTIFY_SECRET` are set. Optional `browserProfilePath` / `display` / `vncPort` / `tabHint` write a **local-only** session binding under `session-bindings/` (never cloud; `display` must be `:99`, `vncPort` must be `5900`). See `site/docs/ATTENTION.md` and `references/agent-box/`.
174
180
 
175
181
  ## Friction input
176
182
 
@@ -0,0 +1,77 @@
1
+ # agent-box — fill display + live noVNC binding
2
+
3
+ Hosted fill and the buyer live panel **must share one session**.
4
+
5
+ | Piece | Value | Notes |
6
+ |-------|-------|--------|
7
+ | Fill display | `DISPLAY=:99` | Xvfb headed Chrome |
8
+ | Fill VNC | x11vnc → **`localhost:5900`** | Mirrors `:99` |
9
+ | Live HTTP front | noVNC / websockify historically **`:6080`** | Buyer iframe / magic-link target |
10
+ | Forbidden | TigerVNC **`:1` / `5901`** | Cold XFCE / wrong desktop = product failure |
11
+
12
+ ## Hard rule
13
+
14
+ **Live noVNC = fill display 5900.**
15
+ `websockify` must proxy `localhost:5900`. Never point buyer live at `5901`.
16
+
17
+ Verify on the box:
18
+
19
+ ```bash
20
+ node job-application-agent/scripts/novnc-display-guard.mjs --unit /etc/systemd/system/novnc.service
21
+ # or:
22
+ node job-application-agent/scripts/novnc-display-guard.mjs --text "$(systemctl cat novnc.service)"
23
+ ```
24
+
25
+ Example unit: [`novnc.service.example`](./novnc.service.example).
26
+
27
+ ## Session binding (pause)
28
+
29
+ When attention opens on a filled form, record a **local-only** binding:
30
+
31
+ ```bash
32
+ # Prefer optional fields on attention add (not synced to cloud):
33
+ node scripts/job-application.mjs attention add --stdin <<'JSON'
34
+ {
35
+ "roundId": "round-…",
36
+ "applicationId": "app-…",
37
+ "url": "https://jobs.ashbyhq.com/…/application",
38
+ "stage": "submission",
39
+ "blocker": "captcha",
40
+ "requiredActions": ["complete-captcha"],
41
+ "browserProfilePath": "/home/runner/.jaa-chrome-fill",
42
+ "display": ":99",
43
+ "vncPort": 5900,
44
+ "tabHint": { "urlContains": "/application" }
45
+ }
46
+ JSON
47
+
48
+ # Or write explicitly:
49
+ node scripts/session-binding.mjs write --stdin <<'JSON'
50
+ {
51
+ "attentionId": "attention-…",
52
+ "jobUrl": "https://jobs.ashbyhq.com/…/application",
53
+ "browserProfilePath": "/home/runner/.jaa-chrome-fill",
54
+ "display": ":99",
55
+ "vncPort": 5900
56
+ }
57
+ JSON
58
+ ```
59
+
60
+ Keep Chrome on that **exact filled tab** (same user-data-dir / CDP). Do not wipe the profile mid-pause. Do not navigate to a jobs listing.
61
+
62
+ ## Resume → submit
63
+
64
+ After `attention-runner-poll.mjs` exit `0`:
65
+
66
+ ```bash
67
+ node scripts/attention-resume-submit.mjs --checklist
68
+ node scripts/attention-resume-submit.mjs --attention-id attention-… --stdin <<'JSON'
69
+ {
70
+ "pageUrl": "https://jobs.ashbyhq.com/…/application",
71
+ "submitEnabled": true,
72
+ "leaseHeld": true
73
+ }
74
+ JSON
75
+ ```
76
+
77
+ Submit if the DOM is clear; ledger only after a **visible** ATS confirmation.
@@ -0,0 +1,16 @@
1
+ [Unit]
2
+ Description=noVNC for JobAppAgent fill display (Xvfb :99 / x11vnc 5900)
3
+ After=network.target x11vnc-fill.service
4
+ Requires=x11vnc-fill.service
5
+
6
+ [Service]
7
+ Type=simple
8
+ # HARD RULE: proxy fill x11vnc on localhost:5900 — NEVER TigerVNC 5901.
9
+ # Buyer live panel (ATTENTION_LIVE_SESSION_BASE_URL → :6080) must show the
10
+ # paused filled ATS form on DISPLAY=:99, not a cold desktop.
11
+ ExecStart=/usr/bin/websockify --web=/usr/share/novnc 6080 localhost:5900
12
+ Restart=on-failure
13
+ RestartSec=3
14
+
15
+ [Install]
16
+ WantedBy=multi-user.target