job-application-agent 3.7.0 → 3.7.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/job-application-agent/SKILL.md +2 -1
- package/job-application-agent/references/SCHEMAS.md +2 -2
- package/job-application-agent/scripts/job-application.mjs +53 -13
- package/job-application-agent/scripts/version.mjs +1 -1
- package/job-application-agent/tests/job-application.test.mjs +159 -1
- package/job-application-agent/tests/skill-contract.test.mjs +19 -0
- package/package.json +1 -1
|
@@ -76,7 +76,7 @@ Check `round status` after the initial discovery pass and before submitting. Pre
|
|
|
76
76
|
|
|
77
77
|
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.
|
|
78
78
|
2. Recheck employer, title, direct domain, posting status, eligibility, and `autoEligible` immediately before submission.
|
|
79
|
-
3. Run `ledger check --stdin` with
|
|
79
|
+
3. Run `ledger check --stdin` with any one identifier set: job URL, internal application id, employer job id plus company, or company+role. Include more identifiers when known. A company+role match is a possible duplicate and returns the stored URL; never treat it as a hard already-applied. Review both requisition duplicate status and same-company history.
|
|
80
80
|
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.
|
|
81
81
|
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"`.
|
|
82
82
|
6. Keep authentication in the existing browser session. Never inspect cookies, local storage, passwords, or session files.
|
|
@@ -95,6 +95,7 @@ Check `round status` after the initial discovery pass and before submitting. Pre
|
|
|
95
95
|
|
|
96
96
|
- Keep `applications.ndjson` and `outcomes.ndjson` append-only. Never delete or rewrite historical rows.
|
|
97
97
|
- Record outcomes with `ledger outcome --stdin`. Use structured rejection reasons and mark each as `explicit` or `inferred`. Do not treat an inference as a candidate fact.
|
|
98
|
+
- Run `ledger check` and `ledger outcome` as two separate CLI processes. Do not combine them in one invocation. When mail has company and role but no URL or id, look up the row with `ledger check` first; that hit is only a possible duplicate and includes the stored URL and id. Then pass the returned `match.id` to `ledger outcome`. If `match.id` is absent, stop and ask; do not guess among fuzzy company+role hits.
|
|
98
99
|
- After an interview, optionally record `interviewQuality` (`promising`, `viable`, `weak`, or `dead`) and a bounded `failurePoint`. Keep free-form interview notes private.
|
|
99
100
|
- Rely on idempotent outcome recording; identical events do not append rows or emit duplicate telemetry.
|
|
100
101
|
- Audit matched delivery failures with authorized email tools when available; otherwise report delivery not audited and continue. Keep delivery failures separate from hiring rejections.
|
|
@@ -81,7 +81,7 @@ Assessment output retains `review`, `ask`, `skip`, and `exclude`, and adds `auto
|
|
|
81
81
|
|
|
82
82
|
## Duplicate check input
|
|
83
83
|
|
|
84
|
-
Include
|
|
84
|
+
`ledger check` accepts any one identifier set: job URL, internal application id, employer job id plus company, or company+role. Include more identifiers when known. Company-only, role-only, and empty objects are rejected.
|
|
85
85
|
|
|
86
86
|
```json
|
|
87
87
|
{
|
|
@@ -93,7 +93,7 @@ Include as many identifiers as are known.
|
|
|
93
93
|
}
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
The check removes fragments and non-job query parameters while retaining recognized job or requisition identifiers. Matching ledger ID, canonical URL, or same-company employer job ID is a hard duplicate. Same company and role without a shared job ID is a possible duplicate.
|
|
96
|
+
The check removes fragments and non-job query parameters while retaining recognized job or requisition identifiers. Matching ledger ID, canonical URL, or same-company employer job ID is a hard duplicate. Same company and role without a shared job ID is a possible duplicate and includes the stored URL. Never promote a company+role match to a hard already-applied. `ledger add` still requires a real job URL.
|
|
97
97
|
|
|
98
98
|
`ledger check` also returns `companyReapply`. A genuinely different role is `eligible-after-cooldown` only when at least 15 full days have passed since the latest application to that company and no outcome has been recorded for that application. Hard duplicates are never eligible. Same-role matches, `cooldown-active`, and `follow-up-present` remain blocked at `ledger add` unless their exact documented override is present.
|
|
99
99
|
|
|
@@ -208,6 +208,11 @@ function string(value, label, max = 5000) {
|
|
|
208
208
|
return value.trim();
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
function optionalPresentString(candidate, key, max) {
|
|
212
|
+
if (!Object.hasOwn(candidate, key)) return '';
|
|
213
|
+
return string(candidate[key], `candidate.${key}`, max);
|
|
214
|
+
}
|
|
215
|
+
|
|
211
216
|
function stringArray(value, label, required = false) {
|
|
212
217
|
if (!Array.isArray(value) || (required && value.length === 0)) throw new Error(`${label} must be ${required ? 'a non-empty' : 'an'} array of strings.`);
|
|
213
218
|
return value.map((item, index) => string(item, `${label}[${index}]`, 300));
|
|
@@ -481,6 +486,26 @@ export function validateLedgerEntry(input) {
|
|
|
481
486
|
return normalized;
|
|
482
487
|
}
|
|
483
488
|
|
|
489
|
+
export function validateLedgerCheckCandidate(input) {
|
|
490
|
+
const candidate = object(input, 'candidate');
|
|
491
|
+
const id = optionalPresentString(candidate, 'id', 180);
|
|
492
|
+
const url = optionalPresentString(candidate, 'url', 2048);
|
|
493
|
+
const employerJobId = optionalPresentString(candidate, 'employerJobId', 300);
|
|
494
|
+
const company = optionalPresentString(candidate, 'company', 300);
|
|
495
|
+
const role = optionalPresentString(candidate, 'role', 300);
|
|
496
|
+
if (!url && !id && !(employerJobId && company) && !(company && role)) {
|
|
497
|
+
throw new Error('ledger check requires at least one identifier set: url, id, employerJobId+company, or company+role.');
|
|
498
|
+
}
|
|
499
|
+
if (url) normalizeUrl(url);
|
|
500
|
+
return {
|
|
501
|
+
...(id ? { id } : {}),
|
|
502
|
+
...(url ? { url } : {}),
|
|
503
|
+
...(employerJobId ? { employerJobId } : {}),
|
|
504
|
+
...(company ? { company } : {}),
|
|
505
|
+
...(role ? { role } : {}),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
484
509
|
export function validateSubmissionTelemetry(input) {
|
|
485
510
|
if (input == null) return {};
|
|
486
511
|
const value = object(input, 'entry.telemetry');
|
|
@@ -519,6 +544,15 @@ function rolesLikelySame(left, right) {
|
|
|
519
544
|
return shared / Math.min(leftTokens.size, rightTokens.size) >= 0.75;
|
|
520
545
|
}
|
|
521
546
|
|
|
547
|
+
function latestMatchingEntry(entries, predicate) {
|
|
548
|
+
const matches = entries.filter(predicate);
|
|
549
|
+
if (matches.length === 0) return null;
|
|
550
|
+
return [...matches]
|
|
551
|
+
.filter((entry) => !Number.isNaN(Date.parse(entry.submittedAt)))
|
|
552
|
+
.sort((left, right) => Date.parse(right.submittedAt) - Date.parse(left.submittedAt))[0]
|
|
553
|
+
?? matches.at(-1);
|
|
554
|
+
}
|
|
555
|
+
|
|
522
556
|
const canonicalApplicationKey = accountingApplicationKey;
|
|
523
557
|
|
|
524
558
|
function businessDaysBetween(startValue, endValue) {
|
|
@@ -995,21 +1029,28 @@ async function canonicalResumePath() {
|
|
|
995
1029
|
function duplicateResult(entries, candidate, outcomes = [], now = new Date()) {
|
|
996
1030
|
const candidateCompany = normalizedText(candidate.company);
|
|
997
1031
|
const candidateRole = normalizedText(candidate.role);
|
|
998
|
-
const candidateUrl = normalizeUrl(candidate.url);
|
|
1032
|
+
const candidateUrl = candidate.url ? normalizeUrl(candidate.url) : null;
|
|
999
1033
|
const sameCompanyRole = (entry) => candidateCompany && candidateRole
|
|
1000
1034
|
&& normalizedText(entry.company) === candidateCompany
|
|
1001
1035
|
&& rolesLikelySame(entry.role, candidate.role);
|
|
1002
|
-
const
|
|
1036
|
+
const exactCompanyRole = (entry) => candidateCompany && candidateRole
|
|
1037
|
+
&& normalizedText(entry.company) === candidateCompany
|
|
1038
|
+
&& normalizedText(entry.role) === candidateRole;
|
|
1039
|
+
const hardId = candidate.id ? entries.find((entry) => entry.id === candidate.id) : null;
|
|
1003
1040
|
const hardEmployerJobId = entries.find((entry) => candidate.employerJobId && entry.employerJobId
|
|
1004
1041
|
&& normalizedText(entry.company) === candidateCompany
|
|
1005
1042
|
&& entry.employerJobId.toLowerCase() === String(candidate.employerJobId).toLowerCase());
|
|
1006
|
-
const hardUrl = entries.find((entry) => normalizeUrl(entry.url) === candidateUrl);
|
|
1043
|
+
const hardUrl = candidateUrl ? entries.find((entry) => normalizeUrl(entry.url) === candidateUrl) : null;
|
|
1007
1044
|
const hard = hardId ?? hardEmployerJobId ?? hardUrl;
|
|
1008
1045
|
const hardReason = hardId ? 'id' : hardEmployerJobId ? 'employer-job-id' : hardUrl ? 'url' : null;
|
|
1009
|
-
const
|
|
1046
|
+
const exact = hard ? null : latestMatchingEntry(entries, exactCompanyRole);
|
|
1047
|
+
const fuzzyMatches = hard || exact ? [] : entries.filter(sameCompanyRole);
|
|
1048
|
+
const ambiguousCompanyRole = fuzzyMatches.length > 1;
|
|
1049
|
+
const possible = hard ? null : exact ?? (fuzzyMatches.length === 1 ? fuzzyMatches[0] : null);
|
|
1010
1050
|
const match = hard ?? possible;
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1051
|
+
const historyCompany = candidateCompany || normalizedText(hard?.company);
|
|
1052
|
+
const sameCompanyEntries = historyCompany
|
|
1053
|
+
? entries.filter((entry) => normalizedText(entry.company) === historyCompany)
|
|
1013
1054
|
: [];
|
|
1014
1055
|
const companyApplications = sameCompanyEntries.slice(-20).map((entry) => ({
|
|
1015
1056
|
id: entry.id,
|
|
@@ -1029,15 +1070,15 @@ function duplicateResult(entries, candidate, outcomes = [], now = new Date()) {
|
|
|
1029
1070
|
: false;
|
|
1030
1071
|
let companyReapplyDecision = 'fresh-company';
|
|
1031
1072
|
if (hard) companyReapplyDecision = 'hard-duplicate';
|
|
1032
|
-
else if (possible) companyReapplyDecision = 'same-role-review';
|
|
1073
|
+
else if (possible || ambiguousCompanyRole) companyReapplyDecision = 'same-role-review';
|
|
1033
1074
|
else if (latestCompanyApplication && hasFollowUp) companyReapplyDecision = 'follow-up-present';
|
|
1034
1075
|
else if (latestCompanyApplication && daysSinceLatest < COMPANY_REAPPLY_COOLDOWN_DAYS) companyReapplyDecision = 'cooldown-active';
|
|
1035
1076
|
else if (latestCompanyApplication) companyReapplyDecision = 'eligible-after-cooldown';
|
|
1036
1077
|
return {
|
|
1037
1078
|
duplicate: Boolean(hard),
|
|
1038
|
-
possibleDuplicate: Boolean(possible),
|
|
1039
|
-
reason: hardReason ?? (possible ? 'company-role' : null),
|
|
1040
|
-
match: match ? { id: match.id, company: match.company, role: match.role, submittedAt: match.submittedAt } : null,
|
|
1079
|
+
possibleDuplicate: Boolean(possible) || ambiguousCompanyRole,
|
|
1080
|
+
reason: hardReason ?? (possible || ambiguousCompanyRole ? 'company-role' : null),
|
|
1081
|
+
match: match ? { id: match.id, company: match.company, role: match.role, submittedAt: match.submittedAt, url: match.url } : null,
|
|
1041
1082
|
sameCompany: companyApplications.length > 0,
|
|
1042
1083
|
companyApplications,
|
|
1043
1084
|
companyReapply: {
|
|
@@ -1052,12 +1093,11 @@ function duplicateResult(entries, candidate, outcomes = [], now = new Date()) {
|
|
|
1052
1093
|
}
|
|
1053
1094
|
|
|
1054
1095
|
async function ledgerCheck(candidate) {
|
|
1055
|
-
|
|
1096
|
+
const normalized = validateLedgerCheckCandidate(candidate);
|
|
1056
1097
|
const dir = await ensureStateDir();
|
|
1057
1098
|
const entries = await jsonLines(join(dir, 'applications.ndjson'));
|
|
1058
1099
|
const outcomes = await jsonLines(join(dir, 'outcomes.ndjson'));
|
|
1059
|
-
|
|
1060
|
-
return duplicateResult(entries, candidate, outcomes);
|
|
1100
|
+
return duplicateResult(entries, normalized, outcomes);
|
|
1061
1101
|
}
|
|
1062
1102
|
|
|
1063
1103
|
async function ledgerAdd(entryInput, duplicateOverride, companyReapplyOverride, cloudIntent = null) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SKILL_VERSION = '3.7.
|
|
1
|
+
export const SKILL_VERSION = '3.7.1';
|
|
@@ -7,7 +7,7 @@ import test from 'node:test';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { createServer } from 'node:http';
|
|
9
9
|
|
|
10
|
-
import { buildReview, commandCategory, durationBucket, migrateProfile, profileStatus, scoreJob, telemetryErrorCode, telemetryJobAssessed, validateLedgerEntry, validateProfile, validateSubmissionTelemetry } from '../scripts/job-application.mjs';
|
|
10
|
+
import { buildReview, commandCategory, durationBucket, migrateProfile, profileStatus, scoreJob, telemetryErrorCode, telemetryJobAssessed, validateLedgerCheckCandidate, validateLedgerEntry, validateProfile, validateSubmissionTelemetry } from '../scripts/job-application.mjs';
|
|
11
11
|
|
|
12
12
|
const target = {
|
|
13
13
|
name: 'Test Candidate',
|
|
@@ -347,6 +347,164 @@ test('warns on same-company role matches and serializes concurrent duplicate sub
|
|
|
347
347
|
assert.equal(possible.possibleDuplicate, true);
|
|
348
348
|
});
|
|
349
349
|
|
|
350
|
+
test('accepts any one ledger check identifier set and never hard-matches company+role', () => {
|
|
351
|
+
assert.deepEqual(validateLedgerCheckCandidate({ url: 'https://jobs.example.com/123' }), { url: 'https://jobs.example.com/123' });
|
|
352
|
+
assert.deepEqual(validateLedgerCheckCandidate({ id: 'example-role-1' }), { id: 'example-role-1' });
|
|
353
|
+
assert.deepEqual(validateLedgerCheckCandidate({ employerJobId: 'example:123', company: 'Example' }), {
|
|
354
|
+
employerJobId: 'example:123', company: 'Example',
|
|
355
|
+
});
|
|
356
|
+
assert.deepEqual(validateLedgerCheckCandidate({ company: 'Example', role: 'Senior Product Engineer' }), {
|
|
357
|
+
company: 'Example', role: 'Senior Product Engineer',
|
|
358
|
+
});
|
|
359
|
+
const identifierError = /ledger check requires at least one identifier set: url, id, employerJobId\+company, or company\+role/;
|
|
360
|
+
assert.throws(() => validateLedgerCheckCandidate({}), identifierError);
|
|
361
|
+
assert.throws(() => validateLedgerCheckCandidate({ company: 'Example' }), identifierError);
|
|
362
|
+
assert.throws(() => validateLedgerCheckCandidate({ role: 'Senior Product Engineer' }), identifierError);
|
|
363
|
+
assert.throws(() => validateLedgerCheckCandidate({ employerJobId: 'example:123' }), identifierError);
|
|
364
|
+
try {
|
|
365
|
+
validateLedgerCheckCandidate({});
|
|
366
|
+
} catch (error) {
|
|
367
|
+
assert.match(error.message, identifierError);
|
|
368
|
+
assert.doesNotMatch(error.message, /candidate\.url must/);
|
|
369
|
+
}
|
|
370
|
+
assert.throws(() => validateLedgerCheckCandidate({ url: 'not-a-url' }), /Invalid URL|invalid/i);
|
|
371
|
+
assert.throws(() => validateLedgerCheckCandidate({ id: 'example-role-1', url: 123 }), /candidate\.url must/);
|
|
372
|
+
assert.throws(() => validateLedgerCheckCandidate({ id: 'example-role-1', url: null }), /candidate\.url must/);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('looks up ledger rows without requiring a URL on check', async (t) => {
|
|
376
|
+
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-check-lookup-'));
|
|
377
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
378
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
379
|
+
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
380
|
+
const storedUrl = 'https://jobs.example.com/123';
|
|
381
|
+
await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
|
|
382
|
+
id: 'example-role-1', company: 'Example', role: 'Senior Product Engineer', url: storedUrl, source: 'company', score: 88,
|
|
383
|
+
status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {}, employerJobId: 'example:123',
|
|
384
|
+
})}\n`);
|
|
385
|
+
const env = isolatedCliEnv(directory);
|
|
386
|
+
|
|
387
|
+
const byUrl = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
|
|
388
|
+
input: JSON.stringify({ url: 'https://jobs.example.com/123?utm_source=board' }), env, encoding: 'utf8',
|
|
389
|
+
}));
|
|
390
|
+
assert.equal(byUrl.duplicate, true);
|
|
391
|
+
assert.equal(byUrl.reason, 'url');
|
|
392
|
+
|
|
393
|
+
const byId = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
|
|
394
|
+
input: JSON.stringify({ id: 'example-role-1' }), env, encoding: 'utf8',
|
|
395
|
+
}));
|
|
396
|
+
assert.equal(byId.duplicate, true);
|
|
397
|
+
assert.equal(byId.possibleDuplicate, false);
|
|
398
|
+
assert.equal(byId.reason, 'id');
|
|
399
|
+
assert.equal(byId.match.url, storedUrl);
|
|
400
|
+
|
|
401
|
+
const byEmployerJob = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
|
|
402
|
+
input: JSON.stringify({ employerJobId: 'example:123', company: 'Example' }), env, encoding: 'utf8',
|
|
403
|
+
}));
|
|
404
|
+
assert.equal(byEmployerJob.duplicate, true);
|
|
405
|
+
assert.equal(byEmployerJob.possibleDuplicate, false);
|
|
406
|
+
assert.equal(byEmployerJob.reason, 'employer-job-id');
|
|
407
|
+
|
|
408
|
+
const newerUrl = 'https://jobs.example.com/123-new';
|
|
409
|
+
await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
|
|
410
|
+
id: 'example-role-1', company: 'Example', role: 'Senior Product Engineer', url: storedUrl, source: 'company', score: 88,
|
|
411
|
+
status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {}, employerJobId: 'example:123',
|
|
412
|
+
})}\n${JSON.stringify({
|
|
413
|
+
id: 'example-role-1b', company: 'Example', role: 'Senior Product Engineer', url: newerUrl, source: 'company', score: 88,
|
|
414
|
+
status: 'submitted', submittedAt: '2026-02-01T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
|
|
415
|
+
})}\n`);
|
|
416
|
+
|
|
417
|
+
const byCompanyRole = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
|
|
418
|
+
input: JSON.stringify({ company: 'Example', role: 'Senior Product Engineer' }), env, encoding: 'utf8',
|
|
419
|
+
}));
|
|
420
|
+
assert.equal(byCompanyRole.duplicate, false);
|
|
421
|
+
assert.equal(byCompanyRole.possibleDuplicate, true);
|
|
422
|
+
assert.equal(byCompanyRole.reason, 'company-role');
|
|
423
|
+
assert.equal(byCompanyRole.match.url, newerUrl);
|
|
424
|
+
assert.equal(byCompanyRole.match.id, 'example-role-1b');
|
|
425
|
+
|
|
426
|
+
const seniorUrl = 'https://jobs.example.com/senior';
|
|
427
|
+
const staffUrl = 'https://jobs.example.com/staff';
|
|
428
|
+
await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
|
|
429
|
+
id: 'example-senior', company: 'Example', role: 'Senior Software Engineer', url: seniorUrl, source: 'company', score: 88,
|
|
430
|
+
status: 'submitted', submittedAt: '2026-01-10T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
|
|
431
|
+
})}\n${JSON.stringify({
|
|
432
|
+
id: 'example-staff', company: 'Example', role: 'Staff Software Engineer', url: staffUrl, source: 'company', score: 90,
|
|
433
|
+
status: 'submitted', submittedAt: '2026-02-10T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
|
|
434
|
+
})}\n`);
|
|
435
|
+
const exactSenior = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
|
|
436
|
+
input: JSON.stringify({ company: 'Example', role: 'Senior Software Engineer' }), env, encoding: 'utf8',
|
|
437
|
+
}));
|
|
438
|
+
assert.equal(exactSenior.possibleDuplicate, true);
|
|
439
|
+
assert.equal(exactSenior.match.id, 'example-senior');
|
|
440
|
+
assert.equal(exactSenior.match.url, seniorUrl);
|
|
441
|
+
|
|
442
|
+
await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
|
|
443
|
+
id: 'example-backend', company: 'Example', role: 'Backend Engineer', url: 'https://jobs.example.com/backend', source: 'company', score: 84,
|
|
444
|
+
status: 'submitted', submittedAt: '2026-01-05T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
|
|
445
|
+
})}\n${JSON.stringify({
|
|
446
|
+
id: 'example-frontend', company: 'Example', role: 'Frontend Engineer', url: 'https://jobs.example.com/frontend', source: 'company', score: 85,
|
|
447
|
+
status: 'submitted', submittedAt: '2026-03-01T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
|
|
448
|
+
})}\n`);
|
|
449
|
+
const ambiguousEngineer = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
|
|
450
|
+
input: JSON.stringify({ company: 'Example', role: 'Engineer' }), env, encoding: 'utf8',
|
|
451
|
+
}));
|
|
452
|
+
assert.equal(ambiguousEngineer.duplicate, false);
|
|
453
|
+
assert.equal(ambiguousEngineer.possibleDuplicate, true);
|
|
454
|
+
assert.equal(ambiguousEngineer.reason, 'company-role');
|
|
455
|
+
assert.equal(ambiguousEngineer.match, null);
|
|
456
|
+
|
|
457
|
+
const identifierError = /ledger check requires at least one identifier set: url, id, employerJobId\+company, or company\+role/;
|
|
458
|
+
for (const input of [{}, { company: 'Example' }, { role: 'Senior Product Engineer' }]) {
|
|
459
|
+
const failed = await runCli(script, ['ledger', 'check', '--stdin'], input, env);
|
|
460
|
+
assert.equal(failed.code, 1);
|
|
461
|
+
assert.match(failed.stderr, identifierError);
|
|
462
|
+
assert.doesNotMatch(failed.stderr, /candidate\.url must/);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const malformedUrl = await runCli(script, ['ledger', 'check', '--stdin'], { id: 'example-role-1', url: 123 }, env);
|
|
466
|
+
assert.equal(malformedUrl.code, 1);
|
|
467
|
+
assert.match(malformedUrl.stderr, /candidate\.url must/);
|
|
468
|
+
|
|
469
|
+
assert.equal(byId.sameCompany, true);
|
|
470
|
+
assert.equal(byId.companyApplications.length, 1);
|
|
471
|
+
assert.equal(byId.companyApplications[0].id, 'example-role-1');
|
|
472
|
+
|
|
473
|
+
const invalidUrl = await runCli(script, ['ledger', 'check', '--stdin'], { url: 'not-a-url' }, env);
|
|
474
|
+
assert.equal(invalidUrl.code, 1);
|
|
475
|
+
assert.match(invalidUrl.stderr, /Invalid URL|invalid/i);
|
|
476
|
+
|
|
477
|
+
const missingAddUrl = await runCli(script, ['ledger', 'add', '--stdin'], {
|
|
478
|
+
id: 'example-role-2', company: 'Example', role: 'Staff Engineer', source: 'company', score: 80,
|
|
479
|
+
status: 'submitted', submittedAt: '2026-01-16T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
|
|
480
|
+
}, env);
|
|
481
|
+
assert.equal(missingAddUrl.code, 1);
|
|
482
|
+
assert.match(missingAddUrl.stderr, /entry\.url must/);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test('records mail and looks up company+role in separate CLI processes', async (t) => {
|
|
486
|
+
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-outcome-then-check-'));
|
|
487
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
488
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
489
|
+
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
490
|
+
const storedUrl = 'https://jobs.example.com/apollo-1';
|
|
491
|
+
await writeFile(join(directory, 'applications.ndjson'), `${JSON.stringify({
|
|
492
|
+
id: 'apollo-role-1', company: 'Apollo', role: 'Staff Engineer', url: storedUrl, source: 'company', score: 90,
|
|
493
|
+
status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
|
|
494
|
+
})}\n`);
|
|
495
|
+
const env = isolatedCliEnv(directory);
|
|
496
|
+
const outcome = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], {
|
|
497
|
+
input: JSON.stringify({ id: 'apollo-role-1', status: 'rejected', occurredAt: '2026-01-20T09:00:00Z' }), env, encoding: 'utf8',
|
|
498
|
+
}));
|
|
499
|
+
assert.equal(outcome.recorded, true);
|
|
500
|
+
const check = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], {
|
|
501
|
+
input: JSON.stringify({ company: 'Apollo', role: 'Staff Engineer' }), env, encoding: 'utf8',
|
|
502
|
+
}));
|
|
503
|
+
assert.equal(check.duplicate, false);
|
|
504
|
+
assert.equal(check.possibleDuplicate, true);
|
|
505
|
+
assert.equal(check.match.url, storedUrl);
|
|
506
|
+
});
|
|
507
|
+
|
|
350
508
|
test('records structured outcomes idempotently without duplicate rows', async (t) => {
|
|
351
509
|
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-outcomes-'));
|
|
352
510
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
@@ -43,6 +43,25 @@ test('documents durable autonomy, resumable rounds, attention and friction contr
|
|
|
43
43
|
assert.match(agentBox, /DISPLAY=:99/);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
test('documents flexible ledger check lookup as a separate process from outcome', async () => {
|
|
47
|
+
const skill = await readFile(new URL('../SKILL.md', import.meta.url), 'utf8');
|
|
48
|
+
const marketplaceSkill = await readFile(new URL('../../skills/job-application-agent/SKILL.md', import.meta.url), 'utf8');
|
|
49
|
+
const schemas = await readFile(new URL('../references/SCHEMAS.md', import.meta.url), 'utf8');
|
|
50
|
+
|
|
51
|
+
for (const copy of [skill, marketplaceSkill]) {
|
|
52
|
+
assert.match(copy, /two separate CLI processes/);
|
|
53
|
+
assert.match(copy, /company\+role/);
|
|
54
|
+
assert.match(copy, /possible duplicate/);
|
|
55
|
+
assert.match(copy, /never treat it as a hard already-applied/);
|
|
56
|
+
assert.match(copy, /look up the row with `ledger check` first/);
|
|
57
|
+
assert.match(copy, /pass the returned `match\.id` to `ledger outcome`/);
|
|
58
|
+
assert.match(copy, /If `match\.id` is absent, stop and ask/);
|
|
59
|
+
}
|
|
60
|
+
assert.match(schemas, /any one identifier set/);
|
|
61
|
+
assert.match(schemas, /Never promote a company\+role match to a hard already-applied/);
|
|
62
|
+
assert.match(schemas, /ledger add` still requires a real job URL/);
|
|
63
|
+
});
|
|
64
|
+
|
|
46
65
|
test('documents Free.ai as optional LLM assist only', async () => {
|
|
47
66
|
const skill = await readFile(new URL('../SKILL.md', import.meta.url), 'utf8');
|
|
48
67
|
const freeAi = await readFile(new URL('../references/FREE_AI.md', import.meta.url), 'utf8');
|
package/package.json
CHANGED