job-application-agent 3.3.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,6 +49,8 @@ On Linux, profile storage uses the Secret Service via the `secret-tool` CLI. Ins
49
49
 
50
50
  Unlike macOS Keychain or Windows Credential Manager, the Linux Secret Service has no always-running system daemon: a keyring daemon (GNOME Keyring, KWallet, or similar) must be running in the user session for `secret-tool` to store or read the profile. On a desktop login this is normally already the case; on headless servers, containers, or SSH-only sessions, start one explicitly (e.g. `gnome-keyring-daemon --unlock --components=secrets`) before first use.
51
51
 
52
+ For one person's agents across several trusted hosts, an optional private Cloudflare D1 backend with R2 (or a private Workers KV blob fallback) shares profile, résumé, application, outcome, round, answer, and attention state. Each host receives a separate revocable credential, and one renewable lease ensures only one host submits applications at a time. See [`CLOUD_STATE.md`](job-application-agent/references/CLOUD_STATE.md).
53
+
52
54
  ## ✨ What it does
53
55
 
54
56
  | Stage | Behavior |
@@ -108,24 +110,30 @@ The bundled CLI handles private profile storage, résumé import, scoring, dupli
108
110
 
109
111
  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`.
110
112
 
113
+ Rounds require recorded attempts across at least three distinct discovery sources, including a successful search, and source attribution for confirmed submissions. If more than 60% of submissions come from one source, the agent must explain the concentration. Blockers and empty results are reported; fit requirements never change to meet a source quota. See [round coverage](job-application-agent/references/RUNS.md#discovery-coverage).
114
+
111
115
  ## 🔐 Privacy
112
116
 
113
117
  | Data | Where it stays |
114
118
  |---|---|
115
- | Profile | macOS Keychain, Windows Credential Manager, or Linux Secret Service (libsecret) |
116
- | Résumé and ledgers | Owner-only local state directory |
119
+ | Profile | OS credential store, or private D1 with an owner-only local cache |
120
+ | Analytics name and email (unless opted out) | Private PostHog events after disclosure; local sharing preference in owner-only state |
121
+ | Résumé and ledgers | Owner-only local state, or private D1 plus private blob storage when configured |
117
122
  | Browser login | Existing browser session |
118
123
  | Community-sharing preference and delivery receipts | Owner-only local state directory |
119
124
  | Skill code | Version-controlled installation directory |
120
125
 
121
126
  Candidate data, résumés, application history, credentials, and browser sessions are never committed to this repository.
122
127
 
123
- Anonymous structured analytics are enabled by default to improve the agent. They may include job and workflow categories, but never candidate identity, résumé content, prompts, answers, browser data, IP addresses, or raw errors.
128
+ Private cloud state is opt-in and isolated from public analytics/community services. Client tokens remain in owner-only host configuration, only token hashes are stored server-side, and browser/Gmail credentials never enter the backend.
129
+
130
+ Structured usage analytics and name/email sharing are enabled by default for support and product improvement. After a disclosure command with no identity transmission, subsequent commands include the name and email explicitly saved in the candidate profile in the maintainer's private PostHog analytics. No résumé content, other profile fields, prompts, answers, browser data, IP addresses, or raw errors are sent. `telemetry identity disable` stops identity sharing and rotates the analytics UUID so future usage is anonymous; `telemetry disable` stops all analytics. Previously collected events remain subject to the retention policy.
124
131
 
125
132
  Anonymous community sharing is also enabled by default, separately from analytics. Confirmed applications share only a canonical public job URL, company, role, application channel, optional coarse discovery source, and derived provider URL—never candidate identity, answers, résumé, score, referral parameters, or submission timestamp. Repeatable discovery surfaces share their bounded catalog metadata through a maintainer-review queue. The registry stores no raw installation IDs; record-scoped contributor hashes are used only for deduplication and counts, never as identity or publication authority.
126
133
 
127
134
  ```bash
128
135
  node ~/.agents/skills/job-application-agent/scripts/job-application.mjs telemetry status
136
+ node ~/.agents/skills/job-application-agent/scripts/job-application.mjs telemetry identity disable
129
137
  node ~/.agents/skills/job-application-agent/scripts/job-application.mjs telemetry disable
130
138
  node ~/.agents/skills/job-application-agent/scripts/job-application.mjs sources sharing status
131
139
  node ~/.agents/skills/job-application-agent/scripts/job-application.mjs sources sharing disable
@@ -35,11 +35,26 @@ async function isDirectory(filePath) {
35
35
  try { return (await lstat(filePath)).isDirectory(); } catch (error) { if (error.code === 'ENOENT') return false; throw error; }
36
36
  }
37
37
 
38
- async function validatePackagedSkill(source) {
38
+ async function packagedCapabilities(source) {
39
+ try {
40
+ const value = JSON.parse(await readFile(path.join(source, 'capabilities.json'), 'utf8'));
41
+ return Array.isArray(value.capabilities) ? value.capabilities.filter((item) => typeof item === 'string') : [];
42
+ } catch (error) {
43
+ if (error.code === 'ENOENT') return [];
44
+ throw new Error('Invalid packaged skill: capabilities.json is malformed.');
45
+ }
46
+ }
47
+
48
+ async function validatePackagedSkill(source, requiredCapabilities = []) {
39
49
  const skillFile = path.join(source, 'SKILL.md');
40
50
  const content = await readFile(skillFile, 'utf8').catch(() => '');
41
51
  if (!content.trim()) throw new Error('Invalid packaged skill: SKILL.md is missing or empty.');
42
52
  await stat(path.join(source, 'scripts', 'job-application.mjs')).catch(() => { throw new Error('Invalid packaged skill: application CLI is missing.'); });
53
+ const capabilities = await packagedCapabilities(source);
54
+ for (const required of requiredCapabilities) {
55
+ if (!capabilities.includes(required)) throw new Error(`Invalid packaged skill: required capability ${required} is missing.`);
56
+ }
57
+ return capabilities;
43
58
  }
44
59
 
45
60
  async function writeConfig(configPath, config) {
@@ -131,12 +146,13 @@ export async function installSkill({
131
146
  const paths = pathsFor(home);
132
147
  const source = path.join(packageRoot, SKILL_NAME);
133
148
  await migrateLegacyCodexInstall({ homeDir, agentHome: home, legacyHome: legacyHome || resolveLegacyCodexHome(homeDir) });
134
- await validatePackagedSkill(source);
149
+ const prior = await readInstallStatus({ homeDir, agentHome: home });
150
+ const capabilities = await validatePackagedSkill(source, prior.requiredCapabilities ?? []);
135
151
  await mkdir(path.dirname(paths.target), { recursive: true });
136
152
  await mkdir(paths.managerDir, { recursive: true });
137
153
  const staging = path.join(paths.managerDir, `staging-${Date.now()}-${Math.random().toString(16).slice(2)}`);
138
154
  await cp(source, staging, { recursive: true, force: true });
139
- await validatePackagedSkill(staging);
155
+ await validatePackagedSkill(staging, prior.requiredCapabilities ?? []);
140
156
 
141
157
  const hadTarget = await exists(paths.target);
142
158
  if (hadTarget) {
@@ -151,13 +167,14 @@ export async function installSkill({
151
167
  throw error;
152
168
  }
153
169
 
154
- const prior = await readInstallStatus({ homeDir, agentHome: home });
155
170
  const config = {
156
171
  installed: true,
157
172
  installedVersion: packageVersion,
158
173
  automaticUpdates: prior.installed ? prior.automaticUpdates !== false : true,
159
174
  installedAt: prior.installedAt || new Date().toISOString(),
160
175
  updatedAt: new Date().toISOString(),
176
+ capabilities,
177
+ ...(prior.requiredCapabilities ? { requiredCapabilities: prior.requiredCapabilities } : {}),
161
178
  };
162
179
  await writeConfig(paths.configPath, config);
163
180
  await syncVendorSkillCopies({ homeDir, sourceSkillDir: paths.target });
@@ -20,11 +20,11 @@ Use `scripts/job-application.mjs` for private state and deterministic checks. Re
20
20
  1. Ask for a local PDF or read-only Google Docs resume URL. Import it without modifying the source.
21
21
  2. Run `profile check`. If it reports missing or legacy fields, collect only facts that cannot be preserved or defaulted, then run `profile migrate --stdin`. Use `profile set --stdin` for a new profile.
22
22
  3. Preserve identity fields during migration. Map legacy `salaryPreference` to `targetCompensation`. Add `compensationFloor` only when the candidate provides an amount, currency, and annual comparison basis.
23
- 4. Store the profile in OS-backed profile storage (macOS Keychain, Windows Credential Manager with a DPAPI-protected local file, or Linux Secret Service via `secret-tool`). Store the canonical resume and append-only ledgers in the owner-only state directory.
23
+ 4. Store the profile in OS-backed profile storage (macOS Keychain, Windows Credential Manager with a DPAPI-protected local file, or Linux Secret Service via `secret-tool`). Store the canonical resume and append-only ledgers in the owner-only state directory. When private cloud state is configured, read and write the profile, résumé, and structured records through the v2 adapter instead. Owner-only local caches support browser uploads on macOS and Linux without Keychain access.
24
24
  5. Use `review-each` for per-application approval. Use `routine-auto` only when the current request authorizes the destination or batch and every automatic-eligibility condition passes.
25
25
  6. When the candidate explicitly grants continuing autonomy, read [references/AUTONOMY.md](references/AUTONOMY.md) and persist it with `autonomy grant --stdin`. Do not repeat skill-level upload or submission approval prompts while the active grant and profile both use `routine-auto`.
26
26
  7. Obey browser and tool confirmation requirements regardless of the stored mode or autonomy grant.
27
- 8. Disclose default-enabled structured anonymous analytics and the `telemetry disable` control. Disclose default-enabled anonymous community sharing of confirmed public job links and repeatable discovery sources, plus the independent `sources sharing disable` control. The CLI also displays these disclosures before the first eligible transmission.
27
+ 8. Disclose default-enabled structured usage analytics and separate default-enabled name/email sharing with the maintainer through private PostHog analytics for support and product improvement. Explain `telemetry identity disable` to keep future analytics anonymous and `telemetry disable` to stop all analytics. Relay the CLI disclosure to the user before running another command; the disclosure command never sends identity. Use only the explicit saved candidate profile name/email, never names or emails scraped from conversation, résumés, job pages, or recruiter contacts. Honor an opt-out immediately. Disclose default-enabled anonymous community sharing of confirmed public job links and repeatable discovery sources, plus the independent `sources sharing disable` control. The CLI also displays these disclosures before the first eligible transmission.
28
28
 
29
29
  Never store passwords, MFA codes, government IDs, demographic data, CAPTCHA answers, browser session data, or inferred candidate facts.
30
30
 
@@ -33,6 +33,7 @@ Never store passwords, MFA codes, government IDs, demographic data, CAPTCHA answ
33
33
  Read [references/SOURCES.md](references/SOURCES.md) before the first discovery pass in a workflow.
34
34
 
35
35
  1. Run `sources jobs` for recently confirmed direct job links and `sources list` (optionally filtered) for the highest-signal packaged and maintainer-reviewed discovery sources. Resolve every lead to the direct employer or ATS page.
36
+ For each round, select at least three distinct relevant discovery sources before applying. Search across them before working deeply through one feed; include alternatives to the previous round's dominant source. Record each actual search, including zero suitable results, or an observed access blocker with `round source --stdin`. Two YC views count as one network; recruiter inboxes and user-supplied links supplement discovery but do not satisfy the three-source minimum. Do not claim that listing the catalog means a board was searched. Keep a blocked source in the report and continue to accessible alternatives.
36
37
  2. Attribute the lead with coarse `discoverySource`, stable packaged or community `discoverySourceId` when known, and independent `applicationChannel`. Treat a one-off user link as `user-supplied`. Whenever a user or agent discovers a repeatable public board, feed, directory, or careers index that is not already listed, run `sources suggest --stdin`; the CLI contributes its sanitized metadata by default unless community sharing has been disabled.
37
38
  3. Verify the application channel immediately before assessment. Mark it `active`, `closed`, or `unclear`.
38
39
  4. Classify eligibility only after checking residence, location, work authorization, sponsorship, schedule, and employment type.
@@ -51,20 +52,23 @@ Do not lower seniority, compensation, location, work mode, or evidence threshold
51
52
  ## Apply
52
53
 
53
54
  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.
54
-
55
- 1. Recheck employer, title, direct domain, posting status, eligibility, and `autoEligible` immediately before submission.
56
- 2. Run `ledger check --stdin` with the internal ledger ID, canonical URL, employer job ID, company, and role when available. Review both requisition duplicate status and same-company history.
57
- 3. Stop on a hard ledger-ID, canonical-URL, employer-job-ID, or requisition duplicate. Treat a same-company/same-role alias as a possible duplicate. Use `duplicateOverride: "NEW REQUISITION CONFIRMED"` only after verifying it is a distinct requisition.
58
- 4. For a genuinely different role at a previously applied company, follow `companyReapply`: proceed automatically only when it returns `eligible-after-cooldown` (15 full days since the latest company application and no recorded outcome). `cooldown-active` and `follow-up-present` require the candidate's explicit approval and `companyReapplyOverride: "CANDIDATE APPROVED EARLY REAPPLICATION"`.
59
- 5. Keep authentication in the existing browser session. Never inspect cookies, local storage, passwords, or session files.
60
- 6. Fill only explicit profile fields, candidate-provided answers, or facts verified in the canonical resume.
61
- 7. Follow [references/APPLICATION_GUIDANCE.md](references/APPLICATION_GUIDANCE.md) for narrative answers.
62
- 8. Upload only the canonical resume unless the candidate explicitly provides another attachment. Resolve its absolute path with `resume path`, then follow [references/BROWSER_UPLOADS.md](references/BROWSER_UPLOADS.md). Use the browser's privileged path-based upload capability first; treat a visible native file picker as a fallback.
63
- 9. Do not answer demographic questions. Stop for login/SSO/MFA, CAPTCHA, legal attestations, unclear authorization or compensation, sensitive identifiers, and judgment-only questions.
64
- 10. Verify every required field, answer, attachment, and disclosure. Submit when the current request or active autonomy grant authorizes it.
65
- 11. Record `submitted` only after visible success confirmation, using independent `discoverySource`, `discoverySourceId`, `applicationChannel`, and `roundId` values. `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.
66
- 12. 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.
67
- 13. 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.
55
+ Check `round status` after the initial discovery pass and before submitting. Preserve source attribution independently of the ATS. A round cannot complete without recorded coverage and attribution; if one discovery source supplies more than 60% of confirmed submissions, explain why using the reviewed alternatives and their fit or access results. Do not submit weaker matches to balance source percentages. Report searched sources, blockers, source mix, and any concentration explanation when handing off or completing a round.
56
+
57
+ 1. When private cloud state is configured, run `cloud status`, acquire the application-run lease with `cloud lease-acquire`, and renew it at least every five minutes. A client without the live lease may research and draft but must not submit.
58
+ 2. Recheck employer, title, direct domain, posting status, eligibility, and `autoEligible` immediately before submission.
59
+ 3. Run `ledger check --stdin` with the internal ledger ID, canonical URL, employer job ID, company, and role when available. Review both requisition duplicate status and same-company history.
60
+ 4. Stop on a hard ledger-ID, canonical-URL, employer-job-ID, or requisition duplicate. Treat a same-company/same-role alias as a possible duplicate. Use `duplicateOverride: "NEW REQUISITION CONFIRMED"` only after verifying it is a distinct requisition.
61
+ 5. For a genuinely different role at a previously applied company, follow `companyReapply`: proceed automatically only when it returns `eligible-after-cooldown` (15 full days since the latest company application and no recorded outcome). `cooldown-active` and `follow-up-present` require the candidate's explicit approval and `companyReapplyOverride: "CANDIDATE APPROVED EARLY REAPPLICATION"`.
62
+ 6. Keep authentication in the existing browser session. Never inspect cookies, local storage, passwords, or session files.
63
+ 7. Fill only explicit profile fields, candidate-provided answers, or facts verified in the canonical resume.
64
+ 8. Follow [references/APPLICATION_GUIDANCE.md](references/APPLICATION_GUIDANCE.md) for narrative answers.
65
+ 9. Upload only the canonical resume unless the candidate explicitly provides another attachment. Resolve its absolute path with `resume path`, then follow [references/BROWSER_UPLOADS.md](references/BROWSER_UPLOADS.md). Use the browser's privileged path-based upload capability first; treat a visible native file picker as a fallback.
66
+ 10. Do not answer demographic questions. Stop for login/SSO/MFA, CAPTCHA, legal attestations, unclear authorization or compensation, sensitive identifiers, and judgment-only questions.
67
+ 11. In cloud mode, create an application intent with `cloud intent-prepare --stdin` immediately before transmission. It rechecks the active lease and cloud duplicate history. If transmission occurs but confirmation is ambiguous, mark it with `cloud intent-sent --stdin`; never retry that application until the ATS or sent email is verified.
68
+ 12. Verify every required field, answer, attachment, and disclosure. Submit when the current request or active autonomy grant authorizes it.
69
+ 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.
70
+ 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.
71
+ 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.
68
72
 
69
73
  ## Outcomes and reviews
70
74
 
@@ -81,6 +85,12 @@ For batches, scheduled work, or resumable handoffs, read [references/RUNS.md](re
81
85
  ## Commands
82
86
 
83
87
  ```text
88
+ node scripts/job-application.mjs cloud configure --stdin
89
+ node scripts/job-application.mjs cloud status
90
+ node scripts/job-application.mjs cloud reconcile [--dry-run]
91
+ node scripts/job-application.mjs cloud export [owner-only-path]
92
+ node scripts/job-application.mjs cloud lease-acquire|lease-renew|lease-release
93
+ node scripts/job-application.mjs cloud intent-prepare|intent-sent|intent-confirm --stdin
84
94
  node scripts/job-application.mjs profile set --stdin
85
95
  node scripts/job-application.mjs profile migrate --stdin
86
96
  node scripts/job-application.mjs profile check
@@ -95,7 +105,7 @@ node scripts/job-application.mjs ledger review
95
105
  node scripts/job-application.mjs ledger review-ack --stdin
96
106
  node scripts/job-application.mjs autonomy grant --stdin
97
107
  node scripts/job-application.mjs autonomy status|preview|revoke
98
- node scripts/job-application.mjs round start|complete --stdin
108
+ node scripts/job-application.mjs round start|source|complete --stdin
99
109
  node scripts/job-application.mjs round status [round-id]
100
110
  node scripts/job-application.mjs sources list [--stdin]
101
111
  node scripts/job-application.mjs sources jobs [--stdin]
@@ -108,6 +118,7 @@ node scripts/job-application.mjs attention list
108
118
  node scripts/job-application.mjs friction record --stdin
109
119
  node scripts/job-application.mjs friction list
110
120
  node scripts/job-application.mjs telemetry status|enable|disable|reset
121
+ node scripts/job-application.mjs telemetry identity status|enable|disable
111
122
  node scripts/job-application.mjs telemetry preview --stdin
112
123
  node scripts/job-application.mjs telemetry record --stdin
113
124
  ```
@@ -0,0 +1,5 @@
1
+ {
2
+ "capabilities": [
3
+ "cloud-state-v2"
4
+ ]
5
+ }
@@ -4,10 +4,15 @@ Job Application Agent includes default-enabled, opt-out usage analytics. The pur
4
4
 
5
5
  The first eligible command displays a disclosure. New installations may send that command's events after the disclosure. Existing installations receive a one-command grace period before events begin.
6
6
 
7
+ Name and email sharing is also enabled by default, separately from usage analytics. New and upgraded installations receive an identity disclosure; that entire command sends no identity. Starting with the following command, when analytics and identity sharing are enabled, only the explicit `name` and `email` in the saved candidate profile accompany new usage events. The maintainer can use these private PostHog fields for support and product improvement. Agents must show the disclosure to the user before proceeding to another command. Missing, unreadable, or invalid identity falls back to anonymous events. No conversation or résumé extraction is performed by telemetry.
8
+
7
9
  ## Controls
8
10
 
9
11
  ```text
10
12
  node scripts/job-application.mjs telemetry status
13
+ node scripts/job-application.mjs telemetry identity status
14
+ node scripts/job-application.mjs telemetry identity disable
15
+ node scripts/job-application.mjs telemetry identity enable
11
16
  node scripts/job-application.mjs telemetry disable
12
17
  node scripts/job-application.mjs telemetry enable
13
18
  node scripts/job-application.mjs telemetry reset
@@ -15,10 +20,13 @@ node scripts/job-application.mjs telemetry preview --stdin
15
20
  node scripts/job-application.mjs telemetry record --stdin
16
21
  ```
17
22
 
18
- - `disable` stops future collection while preserving the anonymous installation ID.
19
- - `enable` resumes collection with the same anonymous installation ID.
23
+ - `identity disable` stops name/email sharing without disabling usage analytics. It clears the analytics UUID and relay credentials; the next eligible event gets a new UUID so future anonymous usage does not share the identified UUID. The command itself sends nothing.
24
+ - `identity enable` resumes default-on identity sharing after another disclosure grace command when previously opted out. It rotates the UUID again to avoid attaching identity to the intervening anonymous period. It does not re-enable disabled usage analytics.
25
+ - `status` and `identity status` show the UUID and sharing/disclosure flags, never the candidate name/email or relay token. They send nothing and do not acknowledge disclosure.
26
+ - `disable` stops all future analytics collection while preserving the installation ID and identity-sharing preference.
27
+ - `enable` resumes collection with the same installation ID and preserves the identity-sharing preference.
20
28
  - `reset` disables collection and removes the anonymous ID and relay token. Enabling later creates a new identity.
21
- - `preview` validates and shows an event without transmitting it.
29
+ - `preview` validates and shows structured event properties without transmitting them. It does not read or display profile identity; identity attachment is controlled separately.
22
30
  - `record` rejects undocumented events and properties; browser workflows use it for started, step, pause, skip, and round events. Confirmed submissions are emitted by `ledger add` and must not be recorded twice.
23
31
  - `status`, `disable`, `reset`, and `preview` never transmit an event. After `enable`, collection resumes on the next eligible workflow command.
24
32
  - Previously collected events remain until the analytics retention period expires. Disabling or resetting does not issue a historical-deletion request.
@@ -29,11 +37,15 @@ Community sharing is a separate default-enabled feature with independent `source
29
37
 
30
38
  ## Identity boundary
31
39
 
32
- Analytics never includes the candidate's name, email, phone, exact address, profile URLs, candidate location, work authorization, personal compensation or compensation floor, target profile or thresholds, resume or attachments, must-have evidence or coverage details, rejection reasons, prompts, responses, job descriptions, form questions, drafted answers, notes, passwords, MFA, CAPTCHA, legal or demographic answers, browser data, IP address, request headers, user agent, or raw error messages.
40
+ The only candidate identity fields allowed are the explicitly saved name and email, under the separately disclosed opt-out control above. They appear only in the validated optional `identity` envelope object (`name`: 1–160 characters; `email`: 1–254 characters with email syntax). Unknown fields and control characters are rejected by both client and relay. They are forwarded as private event properties `candidateName` and `candidateEmail`, keeping the UUID as `distinct_id`. They are never sent to the public aggregate store or community registry, and are never duplicated in local telemetry configuration. Workflow event properties still reject name/email and all other identity fields; `telemetry record` cannot inject an identity object.
41
+
42
+ Analytics never includes phone, exact address, profile URLs, candidate location, work authorization, personal compensation or compensation floor, target profile or thresholds, resume or attachments, must-have evidence or coverage details, rejection reasons, prompts, responses, job descriptions, form questions, drafted answers, notes, passwords, MFA, CAPTCHA, legal or demographic answers, browser data, IP address, request headers, user agent, or raw error messages.
43
+
44
+ Opt-out prevents future identity collection; it does not delete or anonymize previously collected events. Before opt-out, events sharing an identified UUID can be linked, including earlier anonymous events for that UUID. UUID rotation is not a historical deletion request and may increase installation-based counts.
33
45
 
34
46
  Structured job context may include company, role title, canonical destination domain, a SHA-256 hash of the job URL after removing query parameters and fragments, bounded discovery source, ATS/application channel, job country, work mode, employment type, seniority, role family, published salary band, fit score, match/gap categories, workflow stages, field categories, pause reasons, submission result, outcome, bounded interview quality, and bounded interview failure point.
35
47
 
36
- The more specific local `discoverySourceId` catalog attribution is not transmitted in v1.
48
+ Per-application `discoverySourceId` attribution is not transmitted. Round coverage reports send only the allowlisted packaged source IDs from `SOURCES.json`; custom/community IDs are collapsed to `community`. Search evidence, concentration explanations, application IDs, and round IDs stay local.
37
49
 
38
50
  Local attention details and friction evidence are never transmitted. Analytics may receive only their already-documented bounded stage, ATS, pause reason, result, and aggregate count fields.
39
51
 
@@ -45,6 +57,7 @@ Company and title values are bounded and rejected when they resemble an email, p
45
57
  |---|---|
46
58
  | `installation_started` | OS family, Node major version, submission mode |
47
59
  | `command_completed` | Command category, result, duration bucket |
60
+ | `source_checked` | Allowlisted packaged source ID or `community`, searched/blocked status, reviewed/qualified counts, optional bounded blocker |
48
61
  | `job_discovered` | Company, title, job hash/domain, ATS/source, job country, work mode, seniority, employment type, role family, published salary band |
49
62
  | `job_assessed` | Company, title, job hash/domain, ATS, fit score, eligibility, decision, match/gap tags |
50
63
  | `application_started` | Job hash, ATS, approval mode, required-field count, resume/cover-letter/referral requirements |
@@ -52,7 +65,7 @@ Company and title values are bounded and rejected when they resemble an email, p
52
65
  | `application_paused` | Job hash, ATS, stage, bounded reason |
53
66
  | `application_skipped` | Job hash, bounded reason, fit score, eligibility |
54
67
  | `application_submitted` | Company, title, job hash/domain, ATS, duration, fields filled, short-answer count, resume-upload Boolean, approval mode |
55
- | `round_completed` | Requested/submitted/assessed/skipped/paused/error counts, duration bucket |
68
+ | `round_completed` | Requested/submitted/assessed/skipped/paused/error counts, duration bucket; optional attempted/searched/blocked source counts, maximum source share percentage, bounded concentration reason |
56
69
  | `outcome_recorded` | Company, title, job hash/domain, ATS, outcome, days since submission, optional bounded interview quality/failure point |
57
70
  | `review_generated` | Canonical unique-submission and outcome counts, review-due Boolean |
58
71
  | `skill_error` | Stable error code, workflow stage, ATS/job hash when available, recoverable Boolean |
@@ -65,9 +78,13 @@ Only documented enums, bounded numbers, Booleans, bounded company/title/country
65
78
  - PostHog US Cloud stores personless events with `$process_person_profile: false`.
66
79
  - Every event disables GeoIP enrichment with `$geoip_disable: true`, and the PostHog project discards incoming IP data.
67
80
  - The project does not call PostHog identify, alias, group, person-property, autocapture, or session-replay features.
68
- - Anonymous installation IDs remain stable until reset.
81
+ - Installation IDs remain stable until reset or an identity-sharing boundary change. An identified UUID is pseudonymous, not anonymous.
69
82
  - The product retention policy is 24 months and dashboards are private. Dashboard queries exclude data older than 24 months.
70
83
  - A public usage dashboard exposes only fixed aggregate metrics. It never exposes raw events or anonymous installation IDs, rolls segment counts below three into `other`, and caches results at the edge for 15 minutes.
71
84
  - After PostHog accepts an event, the relay best-effort increments a separate Cloudflare D1 store containing daily counters and HMAC-derived installation hashes. The public endpoint reads only this aggregate store; it has no PostHog read credential.
72
85
  - PostHog US Cloud must be configured with a 24-month raw-event TTL before production telemetry is considered fully retention-compliant. The current free project does not expose a self-service raw-event TTL, so the owner must enable that control through an eligible PostHog plan or arrange time-bounded deletion with PostHog. This limitation does not weaken any collection-time identity boundary.
73
86
  - The Worker does not forward client IPs or request headers, and Worker observability is disabled.
87
+
88
+ ## Release ordering
89
+
90
+ Deploy the relay with optional identity-envelope support before releasing the CLI. Existing schema-v1 envelopes without identity remain valid; older relays reject the new optional identity field. Then release the CLI and updated disclosures together. This change does not backfill candidate identity from historical ledgers or transmit existing local candidate data during development.
@@ -0,0 +1,19 @@
1
+ # Optional private cloud state
2
+
3
+ Private cloud state lets multiple trusted agent hosts use one authoritative candidate dataset. It is separate from anonymous analytics and the public community registry.
4
+
5
+ Configure each host with a different revocable client token:
6
+
7
+ ```text
8
+ echo '{"version":2,"url":"https://private-worker.example","clientId":"mac-codex","clientName":"Mac Codex","token":"..."}' | node scripts/job-application.mjs cloud configure --stdin
9
+ node scripts/job-application.mjs cloud status
10
+ node scripts/job-application.mjs cloud reconcile --dry-run
11
+ ```
12
+
13
+ The owner-only configuration file stores the token with mode `0600`. The server stores only its SHA-256 hash. D1 stores revisioned documents, append-only records, application intents, and the single-writer lease. R2 stores the canonical résumé, migration snapshots, and 30-day backups when enabled; an existing private Workers KV namespace is supported as a compatible blob fallback.
14
+
15
+ Browser sessions, Gmail credentials, passwords, verification codes, CAPTCHA responses, demographic responses, legal answers, and device-specific telemetry credentials never enter private cloud state.
16
+
17
+ Normal profile, résumé, ledger, outcome, round, attention, review, and friction commands automatically reconcile through the configured backend. `cloud export` creates an owner-only JSON archive. During a cloud outage, continue cached research and drafts but do not transmit a new application.
18
+
19
+ For a shared Linux host where Codex and another agent need separate credentials, follow [`VPS_CLIENTS.md`](VPS_CLIENTS.md). Keep a single scheduler and rely on the D1 lease—not local process assumptions—to enforce the one-writer rule.
@@ -1,17 +1,61 @@
1
1
  # Resumable application runs
2
2
 
3
+ ## Shared cloud coordination
4
+
5
+ If `cloud status` reports a Cloudflare D1 backend (`cloudflare-d1-r2` or the private `cloudflare-d1-kv` blob fallback), acquire one 15-minute application-run lease before any transmission and renew it every five minutes. Other clients may still read shared state and record independent outcomes. A cloud outage pauses new submissions, while cached research and drafting may continue.
6
+
7
+ Immediately before sending an application, create an intent containing its application ID, canonical URL, round ID, and active lease ID. After visible confirmation, pass the intent and lease IDs to `ledger add`; the Worker records the submission and round progress together. If a send may have happened but confirmation is unavailable, mark the intent `sent-unverified`. Lease expiry never makes an unverified intent safe to retry.
8
+
3
9
  ## Round lifecycle
4
10
 
5
11
  Start each batch with an explicit ID:
6
12
 
7
13
  ```text
8
14
  node scripts/job-application.mjs round start --stdin
15
+ node scripts/job-application.mjs round source --stdin
9
16
  node scripts/job-application.mjs round status [round-id]
10
17
  node scripts/job-application.mjs round complete --stdin
11
18
  ```
12
19
 
13
20
  Start input: `{ "requestedCount": 30 }`. Complete input: `{ "roundId": "round-..." }`.
14
21
 
22
+ ## Discovery coverage
23
+
24
+ Before submitting, search at least three relevant independent sources from `sources list`, including alternatives to the last round's dominant source. Listing the catalog or browsing multiple jobs on one board is not source coverage. Record a report after each actual search or observed access blocker:
25
+
26
+ ```json
27
+ {
28
+ "roundId": "round-...",
29
+ "sourceId": "linkedin-jobs-feed",
30
+ "status": "searched",
31
+ "reviewedCount": 12,
32
+ "qualifiedCount": 3,
33
+ "evidence": "Reviewed current target-matching listings; three met the unchanged fit criteria."
34
+ }
35
+ ```
36
+
37
+ `status` is `searched` or `blocked`. Empty search results count as a search with zero counts, with evidence explaining the query/filter and absence of suitable postings. `reviewedCount` and `qualifiedCount` are bounded at 10,000; qualified cannot exceed reviewed. A blocked report must have zero counts and a `blocker` of `login`, `mfa`, `captcha`, `site-error`, or `access-unavailable`. Evidence is required, at most 2,000 characters, and stays private. Record concrete observations, never invented evidence or raw page content, credentials, recruiter identities, or answers. Reports describe agent-observed work; the CLI cannot independently verify browser activity.
38
+
39
+ The minimum is three distinct sources attempted and at least one successfully searched. Blockers count toward attempts, not searches; try accessible alternatives whenever available. Repeated checks do not increase diversity. The two YC catalog entries count as one network. Inbound recruiting messages and user-provided links may supply good leads but do not count toward the three discovery sources. `round status` returns source reports, search/blocker counts, submission distribution, and missing attribution. A later blocker does not erase a prior search.
40
+
41
+ Every confirmed submission needs a `discoverySourceId` referring to a searched source. For older ledger entries without it, pass `applicationIds` in the matching `round source` report to append attribution without rewriting the ledger. Attribution must reference confirmed applications in that round and cannot conflict with their saved source. Coverage updates cannot modify a completed round. Always record real confirmed submissions even if discovery coverage is incomplete; the completion gate does not prevent accurate ledger accounting.
42
+
43
+ If any discovery source supplies more than 60% of confirmed submissions, `round complete` requires `concentrationReason` and a private `concentrationEvidence` explanation (at most 2,000 characters). Reasons are `stronger-fit`, `alternatives-exhausted`, `access-blocked`, or `candidate-directed`. For example:
44
+
45
+ ```json
46
+ {
47
+ "roundId": "round-...",
48
+ "concentrationReason": "stronger-fit",
49
+ "concentrationEvidence": "The other searched sources had no eligible Senior/Staff matches; selected LinkedIn leads met all existing requirements."
50
+ }
51
+ ```
52
+
53
+ This is a discovery requirement, not an application quota. Never lower fit, eligibility, compensation, or evidence requirements to diversify submissions. ATS concentration alone does not imply discovery concentration: jobs from several boards may all use Ashby. Report coverage and blockers on unfinished handoffs too; `round complete` also continues to enforce the confirmed-submission target. Previously completed rounds remain historical; open rounds need coverage before completion.
54
+
55
+ Coverage reports emit bounded `source_checked` analytics automatically. Only allowlisted packaged source IDs (community sources become `community`), counts, status, and blocker codes are sent. Notes, application IDs, exact community IDs, and round IDs stay local. Completion emits aggregate coverage counts, maximum source share, and the reason code, never the explanation. Failed best-effort analytics does not erase local reports.
56
+
57
+ ## Submission accounting
58
+
15
59
  Count only unique applications with a visible employer/ATS confirmation or a verified sent recruiting email that were also added to the ledger with the same `roundId`. Filled forms, blockers, drafts, unsent email, and ambiguous confirmations never count. `round complete` rejects an under-target round.
16
60
 
17
61
  Run both company-level and requisition-level duplicate checks before filling and again immediately before transmission. Hard ledger-ID, canonical-URL, employer-job-ID, and requisition duplicates always stop. Same-role aliases require a verified distinct requisition and `NEW REQUISITION CONFIRMED`. A genuinely different role at the same company may proceed automatically only when `companyReapply.decision` is `eligible-after-cooldown`: 15 full days have passed since the latest company application and no outcome has been recorded. `cooldown-active` and `follow-up-present` require explicit candidate approval.
@@ -125,13 +125,17 @@ Add only after visible success confirmation.
125
125
  "fieldsFilled": 14,
126
126
  "shortAnswerCount": 2,
127
127
  "resumeUploaded": true
128
- }
128
+ },
129
+ "cloudIntentId": "intent UUID returned before transmission",
130
+ "cloudLeaseId": "active application-run lease UUID"
129
131
  }
130
132
  ```
131
133
 
132
134
  Use `duplicateOverride` only for a verified distinct requisition after a possible-duplicate warning. Use `companyReapplyOverride` only when the candidate explicitly approves a different-role reapplication during the cooldown or after a recorded outcome. Both override phrases and `telemetry` are transient. An accepted company reapplication override stores only `reapplicationApproval: "candidate-explicit"` in the private ledger. Use approval `APPROVE SUBMIT` for per-application approval or `STANDING AUTHORIZATION` when the current request authorizes routine batch submission.
133
135
 
134
- `discoverySource`, `discoverySourceId`, `applicationChannel`, and `roundId` are optional for backward compatibility and should be supplied for new resumable rounds. `discoverySourceId` accepts stable packaged and `community-…` source IDs; it remains local and is not included in telemetry. After a visibly confirmed submission, `ledger add` automatically contributes only the canonical public URL, company, role, application channel, optional coarse discovery source, and derived provider URL to the pending community job registry. Existing installations receive a one-command disclosure grace period before historical backfill; `sources sharing disable` opts out. The private answers, score, submission time, IDs, and round remain local, and no job appears publicly before maintainer review. `ledger check` returns hard duplicate status, bounded same-company history, and the same `companyReapply` decision enforced by `ledger add` while holding the application lock.
136
+ `cloudIntentId` and `cloudLeaseId` are transient coordination fields. They are required for the strongest atomic cloud path and are never written into the application payload itself.
137
+
138
+ `discoverySource`, `discoverySourceId`, `applicationChannel`, and `roundId` are optional for backward compatibility and should be supplied for new resumable rounds. `discoverySourceId` accepts stable packaged and `community-…` source IDs; per-application attribution stays in private state and is not included in telemetry. Separate source-coverage events may include packaged source IDs, while community IDs are collapsed to `community`. After a visibly confirmed submission, `ledger add` automatically contributes only the canonical public URL, company, role, application channel, optional coarse discovery source, and derived provider URL to the pending community job registry. Existing installations receive a one-command disclosure grace period before historical backfill; `sources sharing disable` opts out. Private answers, score, submission time, IDs, and round remain in the owner-only local or configured cloud state, and no job appears publicly before maintainer review. `ledger check` returns hard duplicate status, bounded same-company history, and the same `companyReapply` decision enforced by `ledger add` while holding the application lock.
135
139
 
136
140
  ## Autonomy grant input
137
141
 
@@ -145,11 +149,13 @@ The owner-only `autonomy.json` stores the fixed routine scopes, grant time, and
145
149
 
146
150
  ## Round input
147
151
 
152
+ Use `round source --stdin` for per-source search/blocker reports and optional attribution of existing confirmed application IDs. The exact coverage and concentration contracts, completion requirements, and examples are in [RUNS.md](RUNS.md#discovery-coverage). Evidence remains local; only allowlisted packaged source IDs and bounded coverage metrics enter analytics.
153
+
148
154
  ```json
149
155
  { "requestedCount": 30 }
150
156
  ```
151
157
 
152
- `round start --stdin` appends a `started` event to owner-only `rounds.ndjson` and returns a generated `roundId`. Add that ID to every confirmed ledger entry. `round complete --stdin` accepts `{ "roundId": "round-..." }` and appends a completion event only after the target count is present in the ledger.
158
+ `round start --stdin` appends a `started` event to owner-only `rounds.ndjson` and returns a generated `roundId`. Add that ID to every confirmed ledger entry. `round complete --stdin` accepts `{ "roundId": "round-..." }`, plus concentration reason and evidence when required, and appends a completion event only after the target count, source coverage, attribution, and concentration requirements are satisfied.
153
159
 
154
160
  ## Attention input
155
161
 
@@ -30,7 +30,7 @@ Store three independent attribution fields when available:
30
30
  - `discoverySourceId`: stable packaged or community ID such as `yc-work-at-a-startup` or `community-abcdef1234567890`.
31
31
  - `applicationChannel`: actual submission channel such as `greenhouse`, `ashby`, `lever`, `company`, or `email`.
32
32
 
33
- `discoverySourceId` remains local in v1 and is not transmitted by telemetry.
33
+ Per-application `discoverySourceId` remains local. Round coverage analytics may send allowlisted packaged source IDs; all community source IDs are collapsed to `community`. See [RUNS.md](RUNS.md) for the required discovery coverage, blocker reports, and concentration explanations.
34
34
 
35
35
  ## Community sharing
36
36
 
@@ -0,0 +1,33 @@
1
+ # VPS client instructions
2
+
3
+ This host is a reader by default. It must acquire the shared Cloudflare application-run lease before transmitting any application.
4
+
5
+ ## VPS Codex
6
+
7
+ Use the installed skill and its default Linux cloud configuration:
8
+
9
+ ```sh
10
+ node "$HOME/.agents/skills/job-application-agent/scripts/job-application.mjs" cloud status
11
+ ```
12
+
13
+ All profile, resume, ledger, outcome, round, attention, and review commands must use that same CLI. Do not create or treat an independent local ledger as authoritative.
14
+
15
+ ## VPS Antigravity
16
+
17
+ Use the dedicated wrapper so Antigravity presents its separately revocable client credential:
18
+
19
+ ```sh
20
+ "$HOME/.local/bin/job-application-agent-antigravity" cloud status
21
+ ```
22
+
23
+ Never copy the Codex credential into Antigravity configuration or print either token. Browser sessions, Gmail credentials, passwords, and verification codes stay on the host and are not uploaded.
24
+
25
+ ## Writer protocol
26
+
27
+ 1. Require a healthy `cloud status` result.
28
+ 2. Acquire the application-run lease before application work and renew it every five minutes.
29
+ 3. Create an application intent before each transmission.
30
+ 4. Confirm the intent only after visible submission evidence; mark an ambiguous send `sent-unverified` and do not retry it.
31
+ 5. Release the lease when the run finishes or stops.
32
+
33
+ If cloud state is unavailable, continue cached research and drafting only. Do not submit.