pi-career 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/NOTICE +31 -0
- package/README.md +159 -0
- package/SECURITY.md +45 -0
- package/dist/index.js +2345 -0
- package/docs/design.md +78 -0
- package/docs/licenses/MIT-aho-corasick-memchr.txt +21 -0
- package/docs/licenses/MIT-anstyle-clap-family.txt +19 -0
- package/docs/licenses/MIT-dtolnay-family.txt +23 -0
- package/docs/licenses/MIT-heck.txt +25 -0
- package/docs/licenses/MIT-regex-family.txt +25 -0
- package/docs/licenses/MIT-strsim.txt +23 -0
- package/docs/licenses/MIT-utf8parse.txt +25 -0
- package/docs/licenses/Rust-1.97.1-COPYRIGHT.txt +34 -0
- package/docs/licenses/Rust-1.97.1-LICENSE-MIT.txt +25 -0
- package/docs/licenses/Rust-1.97.1-STDLIB-NOTICE.md +16 -0
- package/docs/licenses/Unicode-3.0.txt +39 -0
- package/docs/native-dependency-inventory.md +116 -0
- package/docs/product-flow.md +47 -0
- package/docs/releasing.md +122 -0
- package/package.json +91 -0
- package/runtime/LICENSE-APACHE +201 -0
- package/runtime/LICENSE-MIT +21 -0
- package/runtime/THIRD_PARTY_NOTICES.md +22 -0
- package/runtime/darwin-arm64/career +0 -0
- package/runtime/darwin-arm64/provenance.json +101 -0
- package/runtime/linux-x64-gnu/career +0 -0
- package/runtime/linux-x64-gnu/provenance.json +101 -0
- package/runtime/manifest.json +109 -0
- package/skills/career-core/SKILL.md +134 -0
- package/skills/career-core/references/cli-contract.md +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2345 @@
|
|
|
1
|
+
// Generated from src/ by npm run build; do not edit.
|
|
2
|
+
// SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
|
|
8
|
+
// src/process.ts
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { isAbsolute } from "node:path";
|
|
11
|
+
import { TextDecoder } from "node:util";
|
|
12
|
+
|
|
13
|
+
// src/errors.ts
|
|
14
|
+
var ERROR_MESSAGES = {
|
|
15
|
+
invalid_request: "The Career Core tool request is invalid.",
|
|
16
|
+
invalid_executable_override: "CAREER_CLI_PATH must be a bounded absolute executable path.",
|
|
17
|
+
unsupported_platform: "No bundled Career Core runtime is available for this platform.",
|
|
18
|
+
bundled_runtime_invalid: "The bundled Career Core runtime failed local integrity verification.",
|
|
19
|
+
missing_executable: "The selected Career Core runtime was not found.",
|
|
20
|
+
executable_unavailable: "The selected Career Core runtime is not available for execution.",
|
|
21
|
+
cancelled: "The Career Core operation was cancelled.",
|
|
22
|
+
timeout: "The Career Core operation exceeded its time limit.",
|
|
23
|
+
stdout_overflow: "The Career Core runtime exceeded the stdout capture limit.",
|
|
24
|
+
stderr_overflow: "The Career Core runtime exceeded the stderr capture limit.",
|
|
25
|
+
result_too_large: "The complete Career Core result exceeds the Pi tool context byte limit; partial output is unavailable and full-result export is not yet supported.",
|
|
26
|
+
result_too_many_lines: "The complete Career Core result exceeds the Pi tool context line limit; partial output is unavailable and full-result export is not yet supported.",
|
|
27
|
+
malformed_result: "The Career Core runtime did not return exactly one valid JSON object.",
|
|
28
|
+
unexpected_stderr: "The Career Core runtime wrote unexpected diagnostics on success.",
|
|
29
|
+
process_signalled: "The Career Core runtime ended because of a signal.",
|
|
30
|
+
career_cli_error: "The Career Core runtime rejected the request with a validated error.",
|
|
31
|
+
cli_failure: "The Career Core runtime failed without a validated error envelope.",
|
|
32
|
+
process_io_failure: "The Career Core runtime stream failed.",
|
|
33
|
+
process_failure: "The Career Core runtime failed unexpectedly.",
|
|
34
|
+
internal_error: "The Career Core Pi adapter failed unexpectedly."
|
|
35
|
+
};
|
|
36
|
+
var CareerInvocationError = class extends Error {
|
|
37
|
+
payload;
|
|
38
|
+
constructor(payload) {
|
|
39
|
+
super(JSON.stringify(payload));
|
|
40
|
+
this.name = "CareerInvocationError";
|
|
41
|
+
this.payload = payload;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function adapterError(code, careerError) {
|
|
45
|
+
return new CareerInvocationError({
|
|
46
|
+
schema_version: "career.pi_error.v1",
|
|
47
|
+
code,
|
|
48
|
+
message: ERROR_MESSAGES[code],
|
|
49
|
+
...careerError === void 0 ? {} : { career_error: careerError }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function publicAdapterError(error) {
|
|
53
|
+
return error instanceof CareerInvocationError ? error : adapterError("internal_error");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/runtime.ts
|
|
57
|
+
import { createHash } from "node:crypto";
|
|
58
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
59
|
+
import path from "node:path";
|
|
60
|
+
import { fileURLToPath } from "node:url";
|
|
61
|
+
var MANIFEST_MAX_BYTES = 65536;
|
|
62
|
+
var RUNTIME_MAX_BYTES = 16777216;
|
|
63
|
+
var SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
64
|
+
var TARGET_LAYOUT = {
|
|
65
|
+
"darwin-arm64": {
|
|
66
|
+
platform: "darwin",
|
|
67
|
+
arch: "arm64",
|
|
68
|
+
libc: null,
|
|
69
|
+
targetTriple: "aarch64-apple-darwin",
|
|
70
|
+
relativePath: "runtime/darwin-arm64/career",
|
|
71
|
+
provenancePath: "runtime/darwin-arm64/provenance.json"
|
|
72
|
+
},
|
|
73
|
+
"linux-x64-gnu": {
|
|
74
|
+
platform: "linux",
|
|
75
|
+
arch: "x64",
|
|
76
|
+
libc: "gnu",
|
|
77
|
+
targetTriple: "x86_64-unknown-linux-gnu",
|
|
78
|
+
relativePath: "runtime/linux-x64-gnu/career",
|
|
79
|
+
provenancePath: "runtime/linux-x64-gnu/provenance.json"
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var verifiedRuntimeKeys = /* @__PURE__ */ new Set();
|
|
83
|
+
var pendingRuntimeVerifications = /* @__PURE__ */ new Map();
|
|
84
|
+
function detectedGlibcVersion() {
|
|
85
|
+
if (process.platform !== "linux") return void 0;
|
|
86
|
+
try {
|
|
87
|
+
const report = process.report?.getReport();
|
|
88
|
+
const header = report?.header;
|
|
89
|
+
const version = header?.glibcVersionRuntime;
|
|
90
|
+
return typeof version === "string" && version.length > 0 ? version : void 0;
|
|
91
|
+
} catch {
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function currentPlatformInfo() {
|
|
96
|
+
const glibcVersionRuntime = detectedGlibcVersion();
|
|
97
|
+
return {
|
|
98
|
+
platform: process.platform,
|
|
99
|
+
arch: process.arch,
|
|
100
|
+
...glibcVersionRuntime === void 0 ? {} : { glibcVersionRuntime }
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function selectRuntimeTarget(info) {
|
|
104
|
+
if (info.platform === "darwin" && info.arch === "arm64") return "darwin-arm64";
|
|
105
|
+
if (info.platform === "linux" && info.arch === "x64" && typeof info.glibcVersionRuntime === "string" && info.glibcVersionRuntime.length > 0) {
|
|
106
|
+
return "linux-x64-gnu";
|
|
107
|
+
}
|
|
108
|
+
throw adapterError("unsupported_platform");
|
|
109
|
+
}
|
|
110
|
+
function isRecord(value) {
|
|
111
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
112
|
+
}
|
|
113
|
+
function isSha256(value) {
|
|
114
|
+
return typeof value === "string" && SHA256_PATTERN.test(value);
|
|
115
|
+
}
|
|
116
|
+
function isBoundedPositiveInteger(value, maximum) {
|
|
117
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= maximum;
|
|
118
|
+
}
|
|
119
|
+
function isBoundedText(value, maximum) {
|
|
120
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximum;
|
|
121
|
+
}
|
|
122
|
+
async function readRuntimeManifest(manifestPath) {
|
|
123
|
+
try {
|
|
124
|
+
const metadata = await lstat(manifestPath);
|
|
125
|
+
if (!metadata.isFile() || metadata.size <= 0 || metadata.size > MANIFEST_MAX_BYTES) {
|
|
126
|
+
throw adapterError("bundled_runtime_invalid");
|
|
127
|
+
}
|
|
128
|
+
const bytes = await readFile(manifestPath);
|
|
129
|
+
if (bytes.length !== metadata.size) throw adapterError("bundled_runtime_invalid");
|
|
130
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
131
|
+
if (!isRecord(parsed)) throw adapterError("bundled_runtime_invalid");
|
|
132
|
+
return parsed;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof CareerInvocationError) throw error;
|
|
135
|
+
throw adapterError("bundled_runtime_invalid");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function manifestHeaderIsValid(manifest) {
|
|
139
|
+
return manifest.schema_version === "pi.career.runtime_manifest.v1" && typeof manifest.career_core?.commit === "string" && /^[a-f0-9]{40}$/.test(manifest.career_core.commit);
|
|
140
|
+
}
|
|
141
|
+
function targetMatchesLayout(target, layout) {
|
|
142
|
+
if (!isRecord(target)) return false;
|
|
143
|
+
return [
|
|
144
|
+
target.platform === layout.platform,
|
|
145
|
+
target.arch === layout.arch,
|
|
146
|
+
target.libc === layout.libc,
|
|
147
|
+
target.target_triple === layout.targetTriple,
|
|
148
|
+
target.path === layout.relativePath,
|
|
149
|
+
target.provenance_path === layout.provenancePath,
|
|
150
|
+
isSha256(target.sha256),
|
|
151
|
+
isBoundedPositiveInteger(target.size_bytes, RUNTIME_MAX_BYTES),
|
|
152
|
+
target.mode === "0755",
|
|
153
|
+
isBoundedText(target.version_output, 100)
|
|
154
|
+
].every(Boolean);
|
|
155
|
+
}
|
|
156
|
+
function validatedTarget(manifest, targetKey) {
|
|
157
|
+
const target = manifest.targets?.[targetKey];
|
|
158
|
+
if (!manifestHeaderIsValid(manifest) || !targetMatchesLayout(target, TARGET_LAYOUT[targetKey])) {
|
|
159
|
+
throw adapterError("bundled_runtime_invalid");
|
|
160
|
+
}
|
|
161
|
+
return target;
|
|
162
|
+
}
|
|
163
|
+
async function verifyRuntimeBinary(executablePath, expected) {
|
|
164
|
+
try {
|
|
165
|
+
if (!path.isAbsolute(executablePath)) throw adapterError("bundled_runtime_invalid");
|
|
166
|
+
const metadata = await lstat(executablePath);
|
|
167
|
+
if (!metadata.isFile() || metadata.size !== expected.size_bytes || (metadata.mode & 511) !== 493 || expected.mode !== "0755") {
|
|
168
|
+
throw adapterError("bundled_runtime_invalid");
|
|
169
|
+
}
|
|
170
|
+
const bytes = await readFile(executablePath);
|
|
171
|
+
if (bytes.length !== expected.size_bytes) throw adapterError("bundled_runtime_invalid");
|
|
172
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
173
|
+
if (digest !== expected.sha256) throw adapterError("bundled_runtime_invalid");
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error instanceof CareerInvocationError) throw error;
|
|
176
|
+
throw adapterError("bundled_runtime_invalid");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function defaultManifestPath() {
|
|
180
|
+
return fileURLToPath(new URL("../runtime/manifest.json", import.meta.url));
|
|
181
|
+
}
|
|
182
|
+
function defaultRuntimeRoot() {
|
|
183
|
+
return fileURLToPath(new URL("../runtime", import.meta.url));
|
|
184
|
+
}
|
|
185
|
+
async function resolveBundledRuntime(options = {}) {
|
|
186
|
+
const targetKey = selectRuntimeTarget(options.platformInfo ?? currentPlatformInfo());
|
|
187
|
+
const manifestPath = options.manifestPath ?? defaultManifestPath();
|
|
188
|
+
const runtimeRoot = options.runtimeRoot ?? defaultRuntimeRoot();
|
|
189
|
+
if (!path.isAbsolute(manifestPath) || !path.isAbsolute(runtimeRoot)) {
|
|
190
|
+
throw adapterError("bundled_runtime_invalid");
|
|
191
|
+
}
|
|
192
|
+
const manifest = await readRuntimeManifest(manifestPath);
|
|
193
|
+
const target = validatedTarget(manifest, targetKey);
|
|
194
|
+
const executablePath = path.join(runtimeRoot, targetKey, "career");
|
|
195
|
+
const verificationKey = `${executablePath}\0${target.size_bytes}\0${target.sha256}`;
|
|
196
|
+
if (verifiedRuntimeKeys.has(verificationKey)) return executablePath;
|
|
197
|
+
let pending = pendingRuntimeVerifications.get(verificationKey);
|
|
198
|
+
if (pending === void 0) {
|
|
199
|
+
pending = verifyRuntimeBinary(executablePath, target);
|
|
200
|
+
pendingRuntimeVerifications.set(verificationKey, pending);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
await pending;
|
|
204
|
+
verifiedRuntimeKeys.add(verificationKey);
|
|
205
|
+
return executablePath;
|
|
206
|
+
} finally {
|
|
207
|
+
pendingRuntimeVerifications.delete(verificationKey);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/process.ts
|
|
212
|
+
var SINGLE_INPUT_MAX_BYTES = 262144;
|
|
213
|
+
var COMPOSITE_INPUT_MAX_BYTES = 1048576;
|
|
214
|
+
var TOOL_RESULT_MAX_BYTES = 5e4;
|
|
215
|
+
var TOOL_RESULT_MAX_LINES = 2e3;
|
|
216
|
+
var STDOUT_CAPTURE_MAX_BYTES = 1048576;
|
|
217
|
+
var STDERR_CAPTURE_MAX_BYTES = 16384;
|
|
218
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
219
|
+
var TERMINATION_GRACE_MS = 250;
|
|
220
|
+
var EXECUTABLE_MAX_BYTES = 4096;
|
|
221
|
+
var KNOWN_CLI_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
222
|
+
"invalid_arguments",
|
|
223
|
+
"input_read_failed",
|
|
224
|
+
"cli_input_too_large",
|
|
225
|
+
"invalid_json",
|
|
226
|
+
"output_write_failed",
|
|
227
|
+
"unsupported_schema_version",
|
|
228
|
+
"source_text_empty",
|
|
229
|
+
"source_text_too_large",
|
|
230
|
+
"source_line_count_exceeded",
|
|
231
|
+
"source_line_too_long",
|
|
232
|
+
"document_id_empty",
|
|
233
|
+
"document_id_too_long",
|
|
234
|
+
"unsupported_enrichment_input_schema_version",
|
|
235
|
+
"unsupported_enrichment_proposal_schema_version",
|
|
236
|
+
"enrichment_not_eligible",
|
|
237
|
+
"enrichment_proposal_too_large",
|
|
238
|
+
"enrichment_non_target_populated",
|
|
239
|
+
"enrichment_field_empty",
|
|
240
|
+
"enrichment_field_too_long",
|
|
241
|
+
"enrichment_list_too_long",
|
|
242
|
+
"enrichment_value_not_grounded",
|
|
243
|
+
"enrichment_entry_invalid",
|
|
244
|
+
"unsupported_analysis_suggestion_review_input_schema_version",
|
|
245
|
+
"unsupported_analysis_suggestion_proposal_schema_version",
|
|
246
|
+
"unsupported_analysis_suggestion_analysis_policy_version",
|
|
247
|
+
"analysis_suggestion_proposal_too_large",
|
|
248
|
+
"analysis_suggestion_list_too_long",
|
|
249
|
+
"unsupported_analysis_replacement_review_input_schema_version",
|
|
250
|
+
"unsupported_analysis_replacement_proposal_schema_version",
|
|
251
|
+
"unsupported_analysis_replacement_analysis_policy_version",
|
|
252
|
+
"analysis_replacement_proposal_too_large",
|
|
253
|
+
"analysis_replacement_list_too_long",
|
|
254
|
+
"unsupported_variant_review_input_schema_version",
|
|
255
|
+
"unsupported_variant_proposal_schema_version",
|
|
256
|
+
"unsupported_variant_materialization_input_schema_version",
|
|
257
|
+
"unsupported_variant_policy_version",
|
|
258
|
+
"variant_proposal_too_large",
|
|
259
|
+
"variant_change_list_too_long",
|
|
260
|
+
"variant_preview_invalid",
|
|
261
|
+
"variant_selection_empty",
|
|
262
|
+
"variant_selection_too_long",
|
|
263
|
+
"variant_selection_invalid"
|
|
264
|
+
]);
|
|
265
|
+
function resolveCareerExecutable(environment = process.env) {
|
|
266
|
+
const override = environment.CAREER_CLI_PATH;
|
|
267
|
+
if (override === void 0) return void 0;
|
|
268
|
+
if (override.length === 0 || override.includes("\0") || Buffer.byteLength(override, "utf8") > EXECUTABLE_MAX_BYTES || !isAbsolute(override)) {
|
|
269
|
+
throw adapterError("invalid_executable_override");
|
|
270
|
+
}
|
|
271
|
+
return override;
|
|
272
|
+
}
|
|
273
|
+
var DOCUMENT_INPUT_LIMITS = {
|
|
274
|
+
"resume.evaluate": SINGLE_INPUT_MAX_BYTES,
|
|
275
|
+
"resume.analyze": SINGLE_INPUT_MAX_BYTES,
|
|
276
|
+
"resume.analysis-suggestions-review": SINGLE_INPUT_MAX_BYTES,
|
|
277
|
+
"resume.analysis-replacements-review": SINGLE_INPUT_MAX_BYTES,
|
|
278
|
+
"resume.normalize": SINGLE_INPUT_MAX_BYTES,
|
|
279
|
+
"resume.enrich": SINGLE_INPUT_MAX_BYTES,
|
|
280
|
+
"resume.variant-review": COMPOSITE_INPUT_MAX_BYTES,
|
|
281
|
+
"resume.variant-materialize": COMPOSITE_INPUT_MAX_BYTES,
|
|
282
|
+
"job.normalize": SINGLE_INPUT_MAX_BYTES,
|
|
283
|
+
"job.match": COMPOSITE_INPUT_MAX_BYTES
|
|
284
|
+
};
|
|
285
|
+
function prepareDiscoveryInvocation(invocation) {
|
|
286
|
+
switch (invocation.operation) {
|
|
287
|
+
case "capabilities":
|
|
288
|
+
if (invocation.schemaId !== void 0) throw adapterError("invalid_request");
|
|
289
|
+
return { args: ["capabilities", "--format", "json-compact"], operation: "capabilities" };
|
|
290
|
+
case "schema-list":
|
|
291
|
+
if (invocation.schemaId !== void 0) throw adapterError("invalid_request");
|
|
292
|
+
return { args: ["schema", "list", "--format", "json-compact"], operation: "schema.list" };
|
|
293
|
+
case "schema-export":
|
|
294
|
+
if (typeof invocation.schemaId !== "string" || invocation.schemaId.length > 100 || !/^career\.[a-z0-9_.-]+\.v[0-9]+$/.test(invocation.schemaId)) {
|
|
295
|
+
throw adapterError("invalid_request");
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
args: ["schema", "export", "--id", invocation.schemaId, "--format", "json-compact"],
|
|
299
|
+
operation: "schema.export"
|
|
300
|
+
};
|
|
301
|
+
default:
|
|
302
|
+
throw adapterError("invalid_request");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function isJsonObject(inputJson) {
|
|
306
|
+
try {
|
|
307
|
+
const parsed = JSON.parse(inputJson);
|
|
308
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function prepareDocumentInvocation(invocation) {
|
|
314
|
+
const operation = `${invocation.kind}.${invocation.operation}`;
|
|
315
|
+
const inputMaxBytes = DOCUMENT_INPUT_LIMITS[operation];
|
|
316
|
+
if (typeof invocation.inputJson !== "string") throw adapterError("invalid_request");
|
|
317
|
+
const inputBytes = Buffer.byteLength(invocation.inputJson, "utf8");
|
|
318
|
+
if (inputMaxBytes === void 0 || inputBytes === 0 || inputBytes > inputMaxBytes || !isJsonObject(invocation.inputJson)) {
|
|
319
|
+
throw adapterError("invalid_request");
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
args: [invocation.kind, invocation.operation, "--input", "-", "--format", "json-compact"],
|
|
323
|
+
input: invocation.inputJson,
|
|
324
|
+
operation
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function prepareInvocation(invocation) {
|
|
328
|
+
return invocation.kind === "discovery" ? prepareDiscoveryInvocation(invocation) : prepareDocumentInvocation(invocation);
|
|
329
|
+
}
|
|
330
|
+
function decodeUtf8(chunks) {
|
|
331
|
+
try {
|
|
332
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
|
|
333
|
+
} catch {
|
|
334
|
+
return void 0;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function parseJsonObject(text) {
|
|
338
|
+
try {
|
|
339
|
+
const parsed = JSON.parse(text);
|
|
340
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
341
|
+
return parsed;
|
|
342
|
+
} catch {
|
|
343
|
+
return void 0;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function parseKnownCliError(stderr) {
|
|
347
|
+
const value = parseJsonObject(stderr);
|
|
348
|
+
if (value === void 0) return void 0;
|
|
349
|
+
if (Object.keys(value).sort().join(",") !== "code,field_path,message,schema_version") return void 0;
|
|
350
|
+
if (value.schema_version !== "career.error.v1") return void 0;
|
|
351
|
+
if (typeof value.code !== "string" || !KNOWN_CLI_ERROR_CODES.has(value.code)) return void 0;
|
|
352
|
+
if (typeof value.message !== "string" || value.message.length === 0 || value.message.length > 500 || !/^[\x20-\x7e]+$/.test(value.message)) {
|
|
353
|
+
return void 0;
|
|
354
|
+
}
|
|
355
|
+
if (value.field_path !== null && (typeof value.field_path !== "string" || value.field_path.length > 100 || !/^[A-Za-z0-9_.\[\]-]*$/.test(value.field_path))) {
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
schema_version: "career.error.v1",
|
|
360
|
+
code: value.code,
|
|
361
|
+
message: value.message,
|
|
362
|
+
field_path: value.field_path
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function outputLineCount(text) {
|
|
366
|
+
if (text.length === 0) return 0;
|
|
367
|
+
const newlineCount = (text.match(/\n/g) ?? []).length;
|
|
368
|
+
return text.endsWith("\n") ? newlineCount : newlineCount + 1;
|
|
369
|
+
}
|
|
370
|
+
function withoutOneTrailingNewline(text) {
|
|
371
|
+
return text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
372
|
+
}
|
|
373
|
+
function executionLimits(options) {
|
|
374
|
+
const limits = {
|
|
375
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
376
|
+
stdoutMax: options.stdoutCaptureMaxBytes ?? STDOUT_CAPTURE_MAX_BYTES,
|
|
377
|
+
stderrMax: options.stderrCaptureMaxBytes ?? STDERR_CAPTURE_MAX_BYTES,
|
|
378
|
+
resultMax: options.toolResultMaxBytes ?? TOOL_RESULT_MAX_BYTES,
|
|
379
|
+
resultLineMax: options.toolResultMaxLines ?? TOOL_RESULT_MAX_LINES
|
|
380
|
+
};
|
|
381
|
+
const positiveIntegers = [
|
|
382
|
+
limits.timeoutMs,
|
|
383
|
+
limits.stdoutMax,
|
|
384
|
+
limits.stderrMax,
|
|
385
|
+
limits.resultMax,
|
|
386
|
+
limits.resultLineMax
|
|
387
|
+
];
|
|
388
|
+
if (positiveIntegers.some((value) => !Number.isSafeInteger(value) || value <= 0)) {
|
|
389
|
+
throw adapterError("invalid_request");
|
|
390
|
+
}
|
|
391
|
+
if (limits.stdoutMax <= limits.resultMax) throw adapterError("invalid_request");
|
|
392
|
+
return limits;
|
|
393
|
+
}
|
|
394
|
+
function throwCliFailure(code, stdout, stderr) {
|
|
395
|
+
if (stdout.trim().length === 0 && code !== null && code >= 2 && code <= 6) {
|
|
396
|
+
const cliError = parseKnownCliError(stderr);
|
|
397
|
+
if (cliError !== void 0) throw adapterError("career_cli_error", cliError);
|
|
398
|
+
}
|
|
399
|
+
throw adapterError("cli_failure");
|
|
400
|
+
}
|
|
401
|
+
function successfulResult(prepared, stdout, stderr, limits) {
|
|
402
|
+
if (stderr.trim().length !== 0) throw adapterError("unexpected_stderr");
|
|
403
|
+
if (Buffer.byteLength(stdout, "utf8") > limits.resultMax) throw adapterError("result_too_large");
|
|
404
|
+
if (outputLineCount(stdout) > limits.resultLineMax) throw adapterError("result_too_many_lines");
|
|
405
|
+
if (parseJsonObject(stdout) === void 0) throw adapterError("malformed_result");
|
|
406
|
+
return { json: withoutOneTrailingNewline(stdout), operation: prepared.operation };
|
|
407
|
+
}
|
|
408
|
+
function completedResult(prepared, completed, limits) {
|
|
409
|
+
if (completed.terminationError !== void 0) throw completed.terminationError;
|
|
410
|
+
if (completed.spawnErrorCode === "ENOENT") throw adapterError("missing_executable");
|
|
411
|
+
if (completed.spawnErrorCode === "EACCES" || completed.spawnErrorCode === "EPERM") {
|
|
412
|
+
throw adapterError("executable_unavailable");
|
|
413
|
+
}
|
|
414
|
+
if (completed.spawnErrorCode !== void 0) throw adapterError("process_failure");
|
|
415
|
+
if (completed.exitSignal !== null) throw adapterError("process_signalled");
|
|
416
|
+
const stdout = decodeUtf8(completed.stdoutChunks);
|
|
417
|
+
const stderr = decodeUtf8(completed.stderrChunks);
|
|
418
|
+
if (stdout === void 0 || stderr === void 0) throw adapterError("malformed_result");
|
|
419
|
+
if (completed.code !== 0) throwCliFailure(completed.code, stdout, stderr);
|
|
420
|
+
return successfulResult(prepared, stdout, stderr, limits);
|
|
421
|
+
}
|
|
422
|
+
async function invokeCareerCli(invocation, signal, options = {}) {
|
|
423
|
+
const prepared = prepareInvocation(invocation);
|
|
424
|
+
if (signal?.aborted) throw adapterError("cancelled");
|
|
425
|
+
const explicitExecutable = options.executable ?? resolveCareerExecutable();
|
|
426
|
+
if (explicitExecutable !== void 0 && (explicitExecutable.length === 0 || explicitExecutable.includes("\0") || Buffer.byteLength(explicitExecutable, "utf8") > EXECUTABLE_MAX_BYTES || !isAbsolute(explicitExecutable))) {
|
|
427
|
+
throw adapterError("invalid_executable_override");
|
|
428
|
+
}
|
|
429
|
+
const executable = explicitExecutable ?? await resolveBundledRuntime();
|
|
430
|
+
if (signal?.aborted) throw adapterError("cancelled");
|
|
431
|
+
const limits = executionLimits(options);
|
|
432
|
+
return new Promise((resolve, reject) => {
|
|
433
|
+
let child;
|
|
434
|
+
try {
|
|
435
|
+
child = spawn(executable, prepared.args, {
|
|
436
|
+
shell: false,
|
|
437
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
438
|
+
windowsHide: true
|
|
439
|
+
});
|
|
440
|
+
} catch {
|
|
441
|
+
reject(adapterError("executable_unavailable"));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const stdoutChunks = [];
|
|
445
|
+
const stderrChunks = [];
|
|
446
|
+
let stdoutBytes = 0;
|
|
447
|
+
let stderrBytes = 0;
|
|
448
|
+
let closed = false;
|
|
449
|
+
let spawnErrorCode;
|
|
450
|
+
let terminationError;
|
|
451
|
+
let killTimer;
|
|
452
|
+
const terminate = (error) => {
|
|
453
|
+
if (terminationError !== void 0 || closed) return;
|
|
454
|
+
terminationError = error;
|
|
455
|
+
try {
|
|
456
|
+
child.kill("SIGTERM");
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
killTimer = setTimeout(() => {
|
|
460
|
+
if (closed) return;
|
|
461
|
+
try {
|
|
462
|
+
child.kill("SIGKILL");
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
465
|
+
}, TERMINATION_GRACE_MS);
|
|
466
|
+
killTimer.unref();
|
|
467
|
+
};
|
|
468
|
+
const timeout = setTimeout(() => terminate(adapterError("timeout")), limits.timeoutMs);
|
|
469
|
+
timeout.unref();
|
|
470
|
+
const onAbort = () => terminate(adapterError("cancelled"));
|
|
471
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
472
|
+
child.once("error", (error) => {
|
|
473
|
+
spawnErrorCode = error.code;
|
|
474
|
+
});
|
|
475
|
+
child.stdin.on("error", (error) => {
|
|
476
|
+
if (error.code !== "EPIPE" && error.code !== "ERR_STREAM_DESTROYED") {
|
|
477
|
+
terminate(adapterError("process_io_failure"));
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
child.stdout.on("data", (chunk) => {
|
|
481
|
+
stdoutBytes += chunk.length;
|
|
482
|
+
if (stdoutBytes > limits.stdoutMax) {
|
|
483
|
+
terminate(adapterError("stdout_overflow"));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
stdoutChunks.push(Buffer.from(chunk));
|
|
487
|
+
});
|
|
488
|
+
child.stderr.on("data", (chunk) => {
|
|
489
|
+
stderrBytes += chunk.length;
|
|
490
|
+
if (stderrBytes > limits.stderrMax) {
|
|
491
|
+
terminate(adapterError("stderr_overflow"));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
stderrChunks.push(Buffer.from(chunk));
|
|
495
|
+
});
|
|
496
|
+
child.once("close", (code, exitSignal) => {
|
|
497
|
+
closed = true;
|
|
498
|
+
clearTimeout(timeout);
|
|
499
|
+
if (killTimer !== void 0) clearTimeout(killTimer);
|
|
500
|
+
signal?.removeEventListener("abort", onAbort);
|
|
501
|
+
try {
|
|
502
|
+
resolve(
|
|
503
|
+
completedResult(
|
|
504
|
+
prepared,
|
|
505
|
+
{
|
|
506
|
+
code,
|
|
507
|
+
exitSignal,
|
|
508
|
+
...spawnErrorCode === void 0 ? {} : { spawnErrorCode },
|
|
509
|
+
...terminationError === void 0 ? {} : { terminationError },
|
|
510
|
+
stdoutChunks,
|
|
511
|
+
stderrChunks
|
|
512
|
+
},
|
|
513
|
+
limits
|
|
514
|
+
)
|
|
515
|
+
);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
reject(publicAdapterError(error));
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
child.stdin.end(prepared.input);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/workflow/commands.ts
|
|
525
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
526
|
+
import {
|
|
527
|
+
BorderedLoader,
|
|
528
|
+
getAgentDir
|
|
529
|
+
} from "@earendil-works/pi-coding-agent";
|
|
530
|
+
|
|
531
|
+
// src/workflow/config.ts
|
|
532
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
533
|
+
import {
|
|
534
|
+
access,
|
|
535
|
+
chmod,
|
|
536
|
+
lstat as lstat2,
|
|
537
|
+
mkdir,
|
|
538
|
+
open,
|
|
539
|
+
readFile as readFile2,
|
|
540
|
+
realpath,
|
|
541
|
+
rename,
|
|
542
|
+
rm
|
|
543
|
+
} from "node:fs/promises";
|
|
544
|
+
import { constants } from "node:fs";
|
|
545
|
+
import path2 from "node:path";
|
|
546
|
+
import { TextDecoder as TextDecoder2 } from "node:util";
|
|
547
|
+
|
|
548
|
+
// src/workflow/types.ts
|
|
549
|
+
var WORKFLOW_CUSTOM_TYPE = "career.workflow";
|
|
550
|
+
var WORKFLOW_STATE_SCHEMA = "pi.career.workflow_state.v1";
|
|
551
|
+
var RESULT_PROJECTION_SCHEMA = "pi.career.result_projection.v1";
|
|
552
|
+
var WORKFLOW_ERROR_MESSAGES = {
|
|
553
|
+
interactive_mode_required: "This career command requires TUI or RPC mode.",
|
|
554
|
+
invalid_command_arguments: "The career command arguments are invalid.",
|
|
555
|
+
config_invalid: "The pi-career configuration is invalid.",
|
|
556
|
+
root_invalid: "The selected resume root is unavailable or invalid.",
|
|
557
|
+
library_empty: "No eligible original resumes are available.",
|
|
558
|
+
vacancy_required: "Set a vacancy with /career-vacancy first.",
|
|
559
|
+
consent_required: "Session-persistence consent was not granted.",
|
|
560
|
+
workflow_cancelled: "The career workflow was cancelled.",
|
|
561
|
+
workflow_stale: "A newer career workflow owns this session.",
|
|
562
|
+
core_result_invalid: "Career Core returned an unexpected result shape.",
|
|
563
|
+
workflow_failed: "The career workflow failed."
|
|
564
|
+
};
|
|
565
|
+
var CareerWorkflowError = class extends Error {
|
|
566
|
+
code;
|
|
567
|
+
constructor(code) {
|
|
568
|
+
super(
|
|
569
|
+
JSON.stringify({
|
|
570
|
+
schema_version: "pi.career.workflow_error.v1",
|
|
571
|
+
code,
|
|
572
|
+
message: WORKFLOW_ERROR_MESSAGES[code]
|
|
573
|
+
})
|
|
574
|
+
);
|
|
575
|
+
this.name = "CareerWorkflowError";
|
|
576
|
+
this.code = code;
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
function workflowError(code) {
|
|
580
|
+
return new CareerWorkflowError(code);
|
|
581
|
+
}
|
|
582
|
+
function workflowErrorMessage(code) {
|
|
583
|
+
return WORKFLOW_ERROR_MESSAGES[code];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/workflow/config.ts
|
|
587
|
+
var CONFIG_SCHEMA = "pi.career.config.v1";
|
|
588
|
+
var CONFIG_MAX_BYTES = 65536;
|
|
589
|
+
var LABEL_MAX_CHARACTERS = 80;
|
|
590
|
+
var SHA256 = /^[a-f0-9]{64}$/;
|
|
591
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
592
|
+
function emptyConfig() {
|
|
593
|
+
return { schema_version: CONFIG_SCHEMA, library_roots: [] };
|
|
594
|
+
}
|
|
595
|
+
function configPath(agentDir) {
|
|
596
|
+
if (!path2.isAbsolute(agentDir)) throw workflowError("config_invalid");
|
|
597
|
+
return path2.join(agentDir, "career", "config.v1.json");
|
|
598
|
+
}
|
|
599
|
+
function rootId(canonicalPath) {
|
|
600
|
+
return createHash2("sha256").update(canonicalPath).digest("hex");
|
|
601
|
+
}
|
|
602
|
+
function isPlainRecord(value) {
|
|
603
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
604
|
+
}
|
|
605
|
+
function exactKeys(value, required, optional = []) {
|
|
606
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
607
|
+
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => allowed.has(key));
|
|
608
|
+
}
|
|
609
|
+
function validLabel(value) {
|
|
610
|
+
return typeof value === "string" && value.length > 0 && value.length <= LABEL_MAX_CHARACTERS && !/[\u0000-\u001f\u007f]/.test(value);
|
|
611
|
+
}
|
|
612
|
+
function parseRoot(value) {
|
|
613
|
+
if (!isPlainRecord(value) || !exactKeys(value, ["id", "path", "label"])) return void 0;
|
|
614
|
+
if (typeof value.id !== "string" || !SHA256.test(value.id) || typeof value.path !== "string" || !path2.isAbsolute(value.path) || value.id !== rootId(value.path) || !validLabel(value.label)) return void 0;
|
|
615
|
+
return { id: value.id, path: value.path, label: value.label };
|
|
616
|
+
}
|
|
617
|
+
function parseConfig(value) {
|
|
618
|
+
if (!isPlainRecord(value) || !exactKeys(value, ["schema_version", "library_roots"], ["generated_variants_root"]) || value.schema_version !== CONFIG_SCHEMA || !Array.isArray(value.library_roots)) throw workflowError("config_invalid");
|
|
619
|
+
const roots = [];
|
|
620
|
+
const ids = /* @__PURE__ */ new Set();
|
|
621
|
+
const paths = /* @__PURE__ */ new Set();
|
|
622
|
+
for (const candidate of value.library_roots) {
|
|
623
|
+
const root = parseRoot(candidate);
|
|
624
|
+
if (root === void 0 || ids.has(root.id) || paths.has(root.path)) {
|
|
625
|
+
throw workflowError("config_invalid");
|
|
626
|
+
}
|
|
627
|
+
ids.add(root.id);
|
|
628
|
+
paths.add(root.path);
|
|
629
|
+
roots.push(root);
|
|
630
|
+
}
|
|
631
|
+
if (value.generated_variants_root !== void 0 && (typeof value.generated_variants_root !== "string" || !path2.isAbsolute(value.generated_variants_root) || value.generated_variants_root.length === 0)) throw workflowError("config_invalid");
|
|
632
|
+
return {
|
|
633
|
+
schema_version: CONFIG_SCHEMA,
|
|
634
|
+
library_roots: roots,
|
|
635
|
+
...typeof value.generated_variants_root === "string" ? { generated_variants_root: value.generated_variants_root } : {}
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
async function loadConfig(agentDir) {
|
|
639
|
+
const file = configPath(agentDir);
|
|
640
|
+
try {
|
|
641
|
+
const metadata = await lstat2(file);
|
|
642
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > CONFIG_MAX_BYTES) {
|
|
643
|
+
throw workflowError("config_invalid");
|
|
644
|
+
}
|
|
645
|
+
if ((metadata.mode & 511) !== 384) throw workflowError("config_invalid");
|
|
646
|
+
const bytes = await readFile2(file);
|
|
647
|
+
if (bytes.length !== metadata.size) throw workflowError("config_invalid");
|
|
648
|
+
const text = new TextDecoder2("utf-8", { fatal: true }).decode(bytes);
|
|
649
|
+
return parseConfig(JSON.parse(text));
|
|
650
|
+
} catch (error) {
|
|
651
|
+
if (error?.code === "ENOENT") return emptyConfig();
|
|
652
|
+
if (error instanceof Error && error.name === "CareerWorkflowError") throw error;
|
|
653
|
+
throw workflowError("config_invalid");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function canonicalizeRoot(inputPath) {
|
|
657
|
+
if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0") || Buffer.byteLength(inputPath, "utf8") > 4096) throw workflowError("root_invalid");
|
|
658
|
+
try {
|
|
659
|
+
const absolute = path2.resolve(inputPath);
|
|
660
|
+
const suppliedMetadata = await lstat2(absolute);
|
|
661
|
+
if (!suppliedMetadata.isDirectory() || suppliedMetadata.isSymbolicLink()) {
|
|
662
|
+
throw workflowError("root_invalid");
|
|
663
|
+
}
|
|
664
|
+
const canonical = await realpath(absolute);
|
|
665
|
+
const metadata = await lstat2(canonical);
|
|
666
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw workflowError("root_invalid");
|
|
667
|
+
await access(canonical, constants.R_OK);
|
|
668
|
+
return canonical;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (error instanceof Error && error.name === "CareerWorkflowError") throw error;
|
|
671
|
+
throw workflowError("root_invalid");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function defaultRootLabel(canonicalPath) {
|
|
675
|
+
const label = path2.basename(canonicalPath).trim();
|
|
676
|
+
return (label || "Resume library").slice(0, LABEL_MAX_CHARACTERS);
|
|
677
|
+
}
|
|
678
|
+
async function addLibraryRoot(config, inputPath, label) {
|
|
679
|
+
const canonical = await canonicalizeRoot(inputPath);
|
|
680
|
+
const chosenLabel = label?.trim() || defaultRootLabel(canonical);
|
|
681
|
+
if (!validLabel(chosenLabel)) throw workflowError("root_invalid");
|
|
682
|
+
const id = rootId(canonical);
|
|
683
|
+
const withoutExisting = config.library_roots.filter((root) => root.id !== id);
|
|
684
|
+
return {
|
|
685
|
+
...config,
|
|
686
|
+
library_roots: [...withoutExisting, { id, path: canonical, label: chosenLabel }]
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function removeLibraryRoot(config, id) {
|
|
690
|
+
return { ...config, library_roots: config.library_roots.filter((root) => root.id !== id) };
|
|
691
|
+
}
|
|
692
|
+
async function writeConfig(agentDir, config, uuid = randomUUID) {
|
|
693
|
+
const validated = parseConfig(config);
|
|
694
|
+
const file = configPath(agentDir);
|
|
695
|
+
const directory = path2.dirname(file);
|
|
696
|
+
const temporaryId = uuid();
|
|
697
|
+
if (!UUID.test(temporaryId)) throw workflowError("config_invalid");
|
|
698
|
+
const temporary = path2.join(directory, `.config.v1.${temporaryId}.tmp`);
|
|
699
|
+
const encoded = Buffer.from(`${JSON.stringify(validated, null, 2)}
|
|
700
|
+
`, "utf8");
|
|
701
|
+
if (encoded.length > CONFIG_MAX_BYTES) throw workflowError("config_invalid");
|
|
702
|
+
let handle;
|
|
703
|
+
try {
|
|
704
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
705
|
+
const directoryMetadata = await lstat2(directory);
|
|
706
|
+
if (!directoryMetadata.isDirectory() || directoryMetadata.isSymbolicLink()) {
|
|
707
|
+
throw workflowError("config_invalid");
|
|
708
|
+
}
|
|
709
|
+
await chmod(directory, 448);
|
|
710
|
+
handle = await open(temporary, "wx", 384);
|
|
711
|
+
await handle.writeFile(encoded);
|
|
712
|
+
await handle.sync();
|
|
713
|
+
await handle.close();
|
|
714
|
+
handle = void 0;
|
|
715
|
+
await chmod(temporary, 384);
|
|
716
|
+
await rename(temporary, file);
|
|
717
|
+
const directoryHandle = await open(directory, "r");
|
|
718
|
+
try {
|
|
719
|
+
await directoryHandle.sync();
|
|
720
|
+
} finally {
|
|
721
|
+
await directoryHandle.close();
|
|
722
|
+
}
|
|
723
|
+
} catch {
|
|
724
|
+
if (handle !== void 0) await handle.close().catch(() => void 0);
|
|
725
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
726
|
+
throw workflowError("config_invalid");
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/workflow/core-input.ts
|
|
731
|
+
function buildResumeInput(resume) {
|
|
732
|
+
return {
|
|
733
|
+
schema_version: "career.resume_input.v1",
|
|
734
|
+
text: resume.text,
|
|
735
|
+
metadata: { document_id: resume.id }
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function buildJobInput(vacancy) {
|
|
739
|
+
return {
|
|
740
|
+
schema_version: "career.job_input.v1",
|
|
741
|
+
text: vacancy.vacancy_text,
|
|
742
|
+
metadata: { document_id: vacancy.state_id }
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function buildJobMatchInput(resume, vacancy) {
|
|
746
|
+
return {
|
|
747
|
+
schema_version: "career.job_match_input.v1",
|
|
748
|
+
resume: buildResumeInput(resume),
|
|
749
|
+
job: buildJobInput(vacancy)
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function serializeCoreInput(value) {
|
|
753
|
+
return JSON.stringify(value);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/workflow/renderers.ts
|
|
757
|
+
import os from "node:os";
|
|
758
|
+
import path4 from "node:path";
|
|
759
|
+
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
760
|
+
import { Container, truncateToWidth } from "@earendil-works/pi-tui";
|
|
761
|
+
|
|
762
|
+
// src/workflow/scan.ts
|
|
763
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
764
|
+
import { lstat as lstat3, opendir, readFile as readFile3, realpath as realpath2 } from "node:fs/promises";
|
|
765
|
+
import path3 from "node:path";
|
|
766
|
+
import { TextDecoder as TextDecoder3 } from "node:util";
|
|
767
|
+
|
|
768
|
+
// src/workflow/text-limit.ts
|
|
769
|
+
var CORE_MAX_CHARACTERS = 5e4;
|
|
770
|
+
function isWithinCoreCharacterLimit(value) {
|
|
771
|
+
let codePoints = 0;
|
|
772
|
+
for (const _codePoint of value) {
|
|
773
|
+
codePoints += 1;
|
|
774
|
+
if (codePoints > CORE_MAX_CHARACTERS) return false;
|
|
775
|
+
}
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/workflow/scan.ts
|
|
780
|
+
var SCAN_MAX_DEPTH = 8;
|
|
781
|
+
var SCAN_MAX_FILES_PER_ROOT = 500;
|
|
782
|
+
var SCAN_MAX_FILES_TOTAL = 2e3;
|
|
783
|
+
var SCAN_MAX_RAW_BYTES = 256 * 1024;
|
|
784
|
+
var SIDECAR_MAX_BYTES = 16384;
|
|
785
|
+
var MAX_DIRECTORY_ENTRIES_PER_ROOT = 1e4;
|
|
786
|
+
var SHA2562 = /^[a-f0-9]{64}$/;
|
|
787
|
+
var ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
788
|
+
function compareText(left, right) {
|
|
789
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
790
|
+
}
|
|
791
|
+
function sha256(value) {
|
|
792
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
793
|
+
}
|
|
794
|
+
function normalizeDocumentText(value) {
|
|
795
|
+
return value.replace(/\r\n?/g, "\n");
|
|
796
|
+
}
|
|
797
|
+
function supportedFormat(file) {
|
|
798
|
+
const extension = path3.extname(file).toLowerCase();
|
|
799
|
+
if (extension === ".md") return "markdown";
|
|
800
|
+
if (extension === ".txt") return "text";
|
|
801
|
+
return void 0;
|
|
802
|
+
}
|
|
803
|
+
function safeLabel(value, fallback) {
|
|
804
|
+
const cleaned = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
805
|
+
return (cleaned || fallback).slice(0, 120);
|
|
806
|
+
}
|
|
807
|
+
function resumeLabel(file, format, text) {
|
|
808
|
+
const fallback = path3.basename(file, path3.extname(file));
|
|
809
|
+
if (format === "markdown") {
|
|
810
|
+
let nonEmpty = 0;
|
|
811
|
+
for (const line of text.split("\n")) {
|
|
812
|
+
if (line.trim().length === 0) continue;
|
|
813
|
+
nonEmpty += 1;
|
|
814
|
+
const match = line.match(/^#\s+(.+?)\s*#*\s*$/);
|
|
815
|
+
if (match?.[1]) return safeLabel(match[1], fallback);
|
|
816
|
+
if (nonEmpty >= 32) break;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return safeLabel(fallback, "Resume");
|
|
820
|
+
}
|
|
821
|
+
function sidecarPath(file) {
|
|
822
|
+
return path3.join(path3.dirname(file), `${path3.basename(file, path3.extname(file))}.pi-career.json`);
|
|
823
|
+
}
|
|
824
|
+
function validSidecar(value) {
|
|
825
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
826
|
+
const record = value;
|
|
827
|
+
const keys = "base_document_id,created_at,kind,schema_version";
|
|
828
|
+
if (Object.keys(record).sort().join(",") !== keys) return false;
|
|
829
|
+
if (record.schema_version !== "pi.career.assisted_variant_meta.v1" || record.kind !== "assisted_variant") {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
if (typeof record.base_document_id !== "string" || !SHA2562.test(record.base_document_id)) return false;
|
|
833
|
+
return typeof record.created_at === "string" && ISO_UTC.test(record.created_at) && Number.isFinite(Date.parse(record.created_at));
|
|
834
|
+
}
|
|
835
|
+
async function readSidecar(file) {
|
|
836
|
+
const sidecar = sidecarPath(file);
|
|
837
|
+
try {
|
|
838
|
+
const metadata = await lstat3(sidecar);
|
|
839
|
+
const invalidMetadata = !metadata.isFile() || metadata.isSymbolicLink() || metadata.size <= 0 || metadata.size > SIDECAR_MAX_BYTES;
|
|
840
|
+
if (invalidMetadata) return { kind: "original", invalid: true };
|
|
841
|
+
const bytes = await readFile3(sidecar);
|
|
842
|
+
if (bytes.length !== metadata.size) return { kind: "original", invalid: true };
|
|
843
|
+
const text = new TextDecoder3("utf-8", { fatal: true }).decode(bytes);
|
|
844
|
+
const parsed = JSON.parse(text);
|
|
845
|
+
if (!validSidecar(parsed)) return { kind: "original", invalid: true };
|
|
846
|
+
return { kind: "assisted_variant", variantGroupId: parsed.base_document_id, invalid: false };
|
|
847
|
+
} catch (error) {
|
|
848
|
+
if (error?.code === "ENOENT") return { kind: "original", invalid: false };
|
|
849
|
+
return { kind: "original", invalid: true };
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function scanRootIsCurrent(root) {
|
|
853
|
+
try {
|
|
854
|
+
const metadata = await lstat3(root.path);
|
|
855
|
+
const canonical = await realpath2(root.path);
|
|
856
|
+
return metadata.isDirectory() && !metadata.isSymbolicLink() && canonical === root.path;
|
|
857
|
+
} catch {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function directoryChildren(current, rootId2, warnings, maximumEntries) {
|
|
862
|
+
const entries = [];
|
|
863
|
+
try {
|
|
864
|
+
const directory = await opendir(current.absolute);
|
|
865
|
+
for await (const entry of directory) {
|
|
866
|
+
entries.push(entry);
|
|
867
|
+
if (entries.length > maximumEntries) {
|
|
868
|
+
return { children: [], entryCount: entries.length, overflow: true };
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
} catch {
|
|
872
|
+
warnings.push({ code: "scan_entry_unavailable", root_id: rootId2 });
|
|
873
|
+
return { children: [], entryCount: 0, overflow: false };
|
|
874
|
+
}
|
|
875
|
+
entries.sort((left, right) => compareText(left.name, right.name));
|
|
876
|
+
const children = [];
|
|
877
|
+
for (const entry of entries) {
|
|
878
|
+
if (entry.isSymbolicLink()) continue;
|
|
879
|
+
const relative = current.relative ? path3.posix.join(current.relative, entry.name) : entry.name;
|
|
880
|
+
const absolute = path3.join(current.absolute, entry.name);
|
|
881
|
+
const depth = current.depth + 1;
|
|
882
|
+
if (entry.isDirectory() && depth <= SCAN_MAX_DEPTH) {
|
|
883
|
+
children.push({ absolute, relative, depth, kind: "directory" });
|
|
884
|
+
} else if (entry.isFile() && supportedFormat(entry.name) !== void 0) {
|
|
885
|
+
children.push({ absolute, relative, depth: current.depth, kind: "file" });
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return { children, entryCount: entries.length, overflow: false };
|
|
889
|
+
}
|
|
890
|
+
function candidateFromPending(current) {
|
|
891
|
+
const format = supportedFormat(current.absolute);
|
|
892
|
+
return format === void 0 ? void 0 : { absolute: current.absolute, relative: current.relative, format };
|
|
893
|
+
}
|
|
894
|
+
async function collectCandidates(root, maximum, warnings) {
|
|
895
|
+
if (!await scanRootIsCurrent(root)) {
|
|
896
|
+
warnings.push({ code: "root_stale", root_id: root.id });
|
|
897
|
+
return { candidates: [], capped: false, stale: true };
|
|
898
|
+
}
|
|
899
|
+
const pending = [{ absolute: root.path, relative: "", depth: 0, kind: "directory" }];
|
|
900
|
+
const candidates = [];
|
|
901
|
+
let visitedEntries = 0;
|
|
902
|
+
let capped = false;
|
|
903
|
+
while (pending.length > 0 && candidates.length < maximum) {
|
|
904
|
+
pending.sort((left, right) => compareText(left.relative, right.relative));
|
|
905
|
+
const current = pending.shift();
|
|
906
|
+
if (current === void 0) break;
|
|
907
|
+
if (current.kind === "file") {
|
|
908
|
+
const candidate = candidateFromPending(current);
|
|
909
|
+
if (candidate !== void 0) candidates.push(candidate);
|
|
910
|
+
capped = candidates.length >= maximum;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const remainingEntryBudget = MAX_DIRECTORY_ENTRIES_PER_ROOT - visitedEntries;
|
|
914
|
+
const { children, entryCount, overflow } = await directoryChildren(
|
|
915
|
+
current,
|
|
916
|
+
root.id,
|
|
917
|
+
warnings,
|
|
918
|
+
remainingEntryBudget
|
|
919
|
+
);
|
|
920
|
+
if (overflow) {
|
|
921
|
+
capped = true;
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
visitedEntries += entryCount;
|
|
925
|
+
pending.push(...children);
|
|
926
|
+
}
|
|
927
|
+
candidates.sort((left, right) => {
|
|
928
|
+
const relative = compareText(left.relative, right.relative);
|
|
929
|
+
if (relative !== 0) return relative;
|
|
930
|
+
return compareText(path3.basename(left.relative), path3.basename(right.relative));
|
|
931
|
+
});
|
|
932
|
+
return { candidates, capped, stale: false };
|
|
933
|
+
}
|
|
934
|
+
async function scanCandidate(root, candidate, warnings) {
|
|
935
|
+
let metadata;
|
|
936
|
+
let canonical;
|
|
937
|
+
try {
|
|
938
|
+
metadata = await lstat3(candidate.absolute);
|
|
939
|
+
canonical = await realpath2(candidate.absolute);
|
|
940
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || canonical !== candidate.absolute || !canonical.startsWith(`${root.path}${path3.sep}`)) return void 0;
|
|
941
|
+
} catch {
|
|
942
|
+
warnings.push({ code: "scan_entry_unavailable", root_id: root.id, relative_path: candidate.relative });
|
|
943
|
+
return void 0;
|
|
944
|
+
}
|
|
945
|
+
if (metadata.size > SCAN_MAX_RAW_BYTES) {
|
|
946
|
+
warnings.push({ code: "raw_file_too_large", root_id: root.id, relative_path: candidate.relative });
|
|
947
|
+
return void 0;
|
|
948
|
+
}
|
|
949
|
+
let decoded;
|
|
950
|
+
try {
|
|
951
|
+
const bytes = await readFile3(canonical);
|
|
952
|
+
if (bytes.length > SCAN_MAX_RAW_BYTES || bytes.length !== metadata.size) {
|
|
953
|
+
warnings.push({ code: "raw_file_too_large", root_id: root.id, relative_path: candidate.relative });
|
|
954
|
+
return void 0;
|
|
955
|
+
}
|
|
956
|
+
decoded = new TextDecoder3("utf-8", { fatal: true }).decode(bytes);
|
|
957
|
+
} catch {
|
|
958
|
+
warnings.push({ code: "invalid_utf8", root_id: root.id, relative_path: candidate.relative });
|
|
959
|
+
return void 0;
|
|
960
|
+
}
|
|
961
|
+
const text = normalizeDocumentText(decoded);
|
|
962
|
+
const id = sha256(canonical);
|
|
963
|
+
const sidecar = await readSidecar(canonical);
|
|
964
|
+
if (sidecar.invalid) {
|
|
965
|
+
warnings.push({ code: "invalid_assisted_sidecar", root_id: root.id, relative_path: candidate.relative });
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
id,
|
|
969
|
+
root_id: root.id,
|
|
970
|
+
path: canonical,
|
|
971
|
+
relative_path: candidate.relative,
|
|
972
|
+
label: resumeLabel(canonical, candidate.format, text),
|
|
973
|
+
kind: sidecar.kind,
|
|
974
|
+
format: candidate.format,
|
|
975
|
+
modified_at: metadata.mtime.toISOString(),
|
|
976
|
+
size_bytes: metadata.size,
|
|
977
|
+
...sidecar.variantGroupId === void 0 ? {} : { variant_group_id: sidecar.variantGroupId },
|
|
978
|
+
...!isWithinCoreCharacterLimit(text) ? { too_large_for_core_input: true } : {},
|
|
979
|
+
text,
|
|
980
|
+
text_sha256: sha256(text)
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
async function scanLibrary(config) {
|
|
984
|
+
const warnings = [];
|
|
985
|
+
const records = [];
|
|
986
|
+
const roots = [];
|
|
987
|
+
let totalCapped = false;
|
|
988
|
+
let scannedCandidateCount = 0;
|
|
989
|
+
for (const root of config.library_roots) {
|
|
990
|
+
const remaining = SCAN_MAX_FILES_TOTAL - scannedCandidateCount;
|
|
991
|
+
if (remaining <= 0) {
|
|
992
|
+
warnings.push({ code: "total_file_cap_reached", root_id: root.id });
|
|
993
|
+
totalCapped = true;
|
|
994
|
+
roots.push({
|
|
995
|
+
root_id: root.id,
|
|
996
|
+
original_count: 0,
|
|
997
|
+
assisted_variant_count: 0,
|
|
998
|
+
too_large_count: 0,
|
|
999
|
+
stale: false,
|
|
1000
|
+
capped: true
|
|
1001
|
+
});
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
const maximum = Math.min(SCAN_MAX_FILES_PER_ROOT, remaining);
|
|
1005
|
+
const collected = await collectCandidates(root, maximum, warnings);
|
|
1006
|
+
scannedCandidateCount += collected.candidates.length;
|
|
1007
|
+
const rootRecords = [];
|
|
1008
|
+
for (const candidate of collected.candidates) {
|
|
1009
|
+
const record = await scanCandidate(root, candidate, warnings);
|
|
1010
|
+
if (record !== void 0) rootRecords.push(record);
|
|
1011
|
+
}
|
|
1012
|
+
records.push(...rootRecords);
|
|
1013
|
+
if (collected.capped) {
|
|
1014
|
+
warnings.push({ code: "root_file_cap_reached", root_id: root.id });
|
|
1015
|
+
if (maximum < SCAN_MAX_FILES_PER_ROOT) totalCapped = true;
|
|
1016
|
+
}
|
|
1017
|
+
roots.push({
|
|
1018
|
+
root_id: root.id,
|
|
1019
|
+
original_count: rootRecords.filter((record) => record.kind === "original").length,
|
|
1020
|
+
assisted_variant_count: rootRecords.filter((record) => record.kind === "assisted_variant").length,
|
|
1021
|
+
too_large_count: rootRecords.filter((record) => record.too_large_for_core_input === true).length,
|
|
1022
|
+
stale: collected.stale,
|
|
1023
|
+
capped: collected.capped
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
return { records, warnings, roots, total_capped: totalCapped };
|
|
1027
|
+
}
|
|
1028
|
+
function eligibleOriginals(scan) {
|
|
1029
|
+
return scan.records.filter(
|
|
1030
|
+
(record) => record.kind === "original" && record.too_large_for_core_input !== true
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/workflow/session-state.ts
|
|
1035
|
+
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1036
|
+
var SHA2563 = /^[a-f0-9]{64}$/;
|
|
1037
|
+
var ISO_UTC2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
1038
|
+
var CARD_MAX_BYTES = 16384;
|
|
1039
|
+
function isRecord2(value) {
|
|
1040
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1041
|
+
}
|
|
1042
|
+
function exactKeys2(value, required) {
|
|
1043
|
+
return Object.keys(value).sort().join(",") === [...required].sort().join(",");
|
|
1044
|
+
}
|
|
1045
|
+
function boundedText(value, maximum) {
|
|
1046
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximum;
|
|
1047
|
+
}
|
|
1048
|
+
function validBase(value) {
|
|
1049
|
+
return value.schema_version === WORKFLOW_STATE_SCHEMA && typeof value.state_id === "string" && UUID2.test(value.state_id) && typeof value.created_at === "string" && ISO_UTC2.test(value.created_at) && Number.isFinite(Date.parse(value.created_at));
|
|
1050
|
+
}
|
|
1051
|
+
function validFlags(value) {
|
|
1052
|
+
if (!isRecord2(value) || !exactKeys2(value, ["adjusted", "provisional", "close_cluster", "stale"])) {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
return [value.adjusted, value.provisional, value.close_cluster, value.stale].every(
|
|
1056
|
+
(flag) => typeof flag === "boolean"
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
function validProjection(value) {
|
|
1060
|
+
if (!isRecord2(value) || !exactKeys2(value, ["schema_version", "core_schema_version", "summary", "ui_flags"])) {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
if (value.schema_version !== RESULT_PROJECTION_SCHEMA || value.core_schema_version !== "career.resume_analysis.v1" && value.core_schema_version !== "career.job_match.v1" || !isRecord2(value.summary) || !validFlags(value.ui_flags)) return false;
|
|
1064
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8") <= CARD_MAX_BYTES;
|
|
1065
|
+
}
|
|
1066
|
+
function isUuid(value) {
|
|
1067
|
+
return typeof value === "string" && UUID2.test(value);
|
|
1068
|
+
}
|
|
1069
|
+
function isSha2562(value) {
|
|
1070
|
+
return typeof value === "string" && SHA2563.test(value);
|
|
1071
|
+
}
|
|
1072
|
+
function parseVacancy(value) {
|
|
1073
|
+
if (!exactKeys2(value, [
|
|
1074
|
+
"schema_version",
|
|
1075
|
+
"kind",
|
|
1076
|
+
"state_id",
|
|
1077
|
+
"created_at",
|
|
1078
|
+
"vacancy_label",
|
|
1079
|
+
"vacancy_text",
|
|
1080
|
+
"vacancy_text_sha256",
|
|
1081
|
+
"source"
|
|
1082
|
+
])) return void 0;
|
|
1083
|
+
if (!boundedText(value.vacancy_label, 120) || typeof value.vacancy_text !== "string" || value.vacancy_text.trim().length === 0 || !isWithinCoreCharacterLimit(value.vacancy_text)) return void 0;
|
|
1084
|
+
if (!isSha2562(value.vacancy_text_sha256) || value.vacancy_text_sha256 !== sha256(value.vacancy_text)) {
|
|
1085
|
+
return void 0;
|
|
1086
|
+
}
|
|
1087
|
+
if (value.source !== "paste" && value.source !== "replace") return void 0;
|
|
1088
|
+
return value;
|
|
1089
|
+
}
|
|
1090
|
+
function parseVacancyClear(value) {
|
|
1091
|
+
const keys = ["schema_version", "kind", "state_id", "created_at", "clears_state_id"];
|
|
1092
|
+
return exactKeys2(value, keys) && isUuid(value.clears_state_id) ? value : void 0;
|
|
1093
|
+
}
|
|
1094
|
+
function parseConsent(value) {
|
|
1095
|
+
const keys = ["schema_version", "kind", "state_id", "created_at", "scope", "granted"];
|
|
1096
|
+
return exactKeys2(value, keys) && value.scope === "session_persistence" && typeof value.granted === "boolean" ? value : void 0;
|
|
1097
|
+
}
|
|
1098
|
+
function parseConsentClear(value) {
|
|
1099
|
+
const keys = ["schema_version", "kind", "state_id", "created_at", "scope", "clears_state_id"];
|
|
1100
|
+
return exactKeys2(value, keys) && value.scope === "session_persistence" && isUuid(value.clears_state_id) ? value : void 0;
|
|
1101
|
+
}
|
|
1102
|
+
function validInputDigests(value) {
|
|
1103
|
+
return isRecord2(value) && exactKeys2(value, ["resume_text_sha256", "vacancy_text_sha256"]) && isSha2562(value.resume_text_sha256) && isSha2562(value.vacancy_text_sha256);
|
|
1104
|
+
}
|
|
1105
|
+
function parseResultCard(value) {
|
|
1106
|
+
if (!exactKeys2(value, [
|
|
1107
|
+
"schema_version",
|
|
1108
|
+
"kind",
|
|
1109
|
+
"state_id",
|
|
1110
|
+
"created_at",
|
|
1111
|
+
"workflow",
|
|
1112
|
+
"run_id",
|
|
1113
|
+
"resume_id",
|
|
1114
|
+
"resume_label",
|
|
1115
|
+
"resume_path_fingerprint",
|
|
1116
|
+
"input_digests",
|
|
1117
|
+
"projection"
|
|
1118
|
+
])) return void 0;
|
|
1119
|
+
if (value.workflow !== "analyze" && value.workflow !== "match") return void 0;
|
|
1120
|
+
if (!isUuid(value.run_id) || !isSha2562(value.resume_id) || !boundedText(value.resume_label, 120)) {
|
|
1121
|
+
return void 0;
|
|
1122
|
+
}
|
|
1123
|
+
if (!isSha2562(value.resume_path_fingerprint) || !validInputDigests(value.input_digests)) {
|
|
1124
|
+
return void 0;
|
|
1125
|
+
}
|
|
1126
|
+
return validProjection(value.projection) ? value : void 0;
|
|
1127
|
+
}
|
|
1128
|
+
function parseWorkflowEntryData(value) {
|
|
1129
|
+
if (!isRecord2(value) || !validBase(value)) return void 0;
|
|
1130
|
+
switch (value.kind) {
|
|
1131
|
+
case "vacancy":
|
|
1132
|
+
return parseVacancy(value);
|
|
1133
|
+
case "vacancy_clear":
|
|
1134
|
+
return parseVacancyClear(value);
|
|
1135
|
+
case "consent":
|
|
1136
|
+
return parseConsent(value);
|
|
1137
|
+
case "consent_clear":
|
|
1138
|
+
return parseConsentClear(value);
|
|
1139
|
+
case "result_card":
|
|
1140
|
+
return parseResultCard(value);
|
|
1141
|
+
default:
|
|
1142
|
+
return void 0;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function workflowDataFromEntries(entries) {
|
|
1146
|
+
const data = [];
|
|
1147
|
+
for (const entry of entries) {
|
|
1148
|
+
if (entry.type !== "custom" || entry.customType !== WORKFLOW_CUSTOM_TYPE) continue;
|
|
1149
|
+
const parsed = parseWorkflowEntryData(entry.data);
|
|
1150
|
+
if (parsed !== void 0) data.push(parsed);
|
|
1151
|
+
}
|
|
1152
|
+
return data;
|
|
1153
|
+
}
|
|
1154
|
+
function workflowResultCards(entries) {
|
|
1155
|
+
return workflowDataFromEntries(entries).filter(
|
|
1156
|
+
(data) => data.kind === "result_card"
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
function reconstructWorkflowState(entries) {
|
|
1160
|
+
let vacancy;
|
|
1161
|
+
let consent;
|
|
1162
|
+
const cards = /* @__PURE__ */ new Map();
|
|
1163
|
+
for (const data of workflowDataFromEntries(entries)) {
|
|
1164
|
+
switch (data.kind) {
|
|
1165
|
+
case "vacancy":
|
|
1166
|
+
vacancy = data;
|
|
1167
|
+
break;
|
|
1168
|
+
case "vacancy_clear":
|
|
1169
|
+
if (vacancy?.state_id === data.clears_state_id) vacancy = void 0;
|
|
1170
|
+
break;
|
|
1171
|
+
case "consent":
|
|
1172
|
+
consent = data;
|
|
1173
|
+
break;
|
|
1174
|
+
case "consent_clear":
|
|
1175
|
+
if (consent?.state_id === data.clears_state_id) consent = void 0;
|
|
1176
|
+
break;
|
|
1177
|
+
case "result_card":
|
|
1178
|
+
cards.set(`${data.workflow}:${data.resume_id}`, data);
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
...vacancy === void 0 ? {} : { vacancy },
|
|
1184
|
+
...consent === void 0 ? {} : { consent },
|
|
1185
|
+
result_cards: [...cards.values()]
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function base(options) {
|
|
1189
|
+
return {
|
|
1190
|
+
schema_version: WORKFLOW_STATE_SCHEMA,
|
|
1191
|
+
state_id: options.uuid(),
|
|
1192
|
+
created_at: options.now().toISOString()
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function createVacancyEntry(text, source, options) {
|
|
1196
|
+
const label = text.split("\n").find((line) => line.trim().length > 0)?.trim() || "Current vacancy";
|
|
1197
|
+
return {
|
|
1198
|
+
...base(options),
|
|
1199
|
+
kind: "vacancy",
|
|
1200
|
+
vacancy_label: label.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 120),
|
|
1201
|
+
vacancy_text: text,
|
|
1202
|
+
vacancy_text_sha256: sha256(text),
|
|
1203
|
+
source
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
function createVacancyClearEntry(vacancy, options) {
|
|
1207
|
+
return { ...base(options), kind: "vacancy_clear", clears_state_id: vacancy.state_id };
|
|
1208
|
+
}
|
|
1209
|
+
function createConsentEntry(granted, options) {
|
|
1210
|
+
return {
|
|
1211
|
+
...base(options),
|
|
1212
|
+
kind: "consent",
|
|
1213
|
+
scope: "session_persistence",
|
|
1214
|
+
granted
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
function withCurrentStaleness(state, scan) {
|
|
1218
|
+
const records = new Map(scan.records.map((record) => [record.id, record]));
|
|
1219
|
+
const cards = state.result_cards.map((card) => {
|
|
1220
|
+
const current = records.get(card.resume_id);
|
|
1221
|
+
const resumeStale = current === void 0 || current.text_sha256 !== card.input_digests.resume_text_sha256;
|
|
1222
|
+
const vacancyStale = card.workflow === "match" && state.vacancy?.vacancy_text_sha256 !== card.input_digests.vacancy_text_sha256;
|
|
1223
|
+
const stale = resumeStale || vacancyStale;
|
|
1224
|
+
return {
|
|
1225
|
+
...card,
|
|
1226
|
+
projection: {
|
|
1227
|
+
...card.projection,
|
|
1228
|
+
ui_flags: { ...card.projection.ui_flags, stale }
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
});
|
|
1232
|
+
return { ...state, result_cards: cards };
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/workflow/renderers.ts
|
|
1236
|
+
var UI_TEXT_MAX = 8e3;
|
|
1237
|
+
function privacyDisplayPath(absolutePath) {
|
|
1238
|
+
const home = os.homedir();
|
|
1239
|
+
const relative = path4.relative(home, absolutePath);
|
|
1240
|
+
if (relative && !relative.startsWith("..") && !path4.isAbsolute(relative)) {
|
|
1241
|
+
return `~${path4.sep}${relative}`;
|
|
1242
|
+
}
|
|
1243
|
+
return path4.basename(absolutePath) || "resume root";
|
|
1244
|
+
}
|
|
1245
|
+
function setupSummary(config, scan, persisted2) {
|
|
1246
|
+
const resumes = scan.records.length;
|
|
1247
|
+
const roots = config.library_roots.length;
|
|
1248
|
+
return `pi-career • ${roots} root${roots === 1 ? "" : "s"} • ${resumes} resume${resumes === 1 ? "" : "s"} • session ${persisted2 ? "persisted" : "transient"}`;
|
|
1249
|
+
}
|
|
1250
|
+
function libraryIndexPreview(config, scan, maximum = 50) {
|
|
1251
|
+
const lines = [];
|
|
1252
|
+
let remaining = maximum;
|
|
1253
|
+
for (const root of config.library_roots) {
|
|
1254
|
+
lines.push(`[${root.label}]`);
|
|
1255
|
+
const records = scan.records.filter((record) => record.root_id === root.id);
|
|
1256
|
+
for (const record of records.slice(0, remaining)) {
|
|
1257
|
+
const labels = [
|
|
1258
|
+
...record.kind === "assisted_variant" ? ["assisted variant"] : [],
|
|
1259
|
+
...record.too_large_for_core_input === true ? ["too large"] : []
|
|
1260
|
+
];
|
|
1261
|
+
lines.push(`- ${record.label}${labels.length > 0 ? ` — ${labels.join(", ")}` : ""}`);
|
|
1262
|
+
}
|
|
1263
|
+
remaining -= Math.min(records.length, remaining);
|
|
1264
|
+
if (remaining === 0) break;
|
|
1265
|
+
}
|
|
1266
|
+
if (scan.records.length > maximum) lines.push(`Showing ${maximum} of ${scan.records.length} indexed resumes.`);
|
|
1267
|
+
return lines.join("\n") || "No indexed resumes";
|
|
1268
|
+
}
|
|
1269
|
+
function librarySummary(config, scan, persisted2) {
|
|
1270
|
+
const originals = scan.records.filter((record) => record.kind === "original").length;
|
|
1271
|
+
const assisted = scan.records.filter((record) => record.kind === "assisted_variant").length;
|
|
1272
|
+
const tooLarge = scan.records.filter((record) => record.too_large_for_core_input === true).length;
|
|
1273
|
+
const staleRoots = scan.roots.filter((root) => root.stale).length;
|
|
1274
|
+
return [
|
|
1275
|
+
`${config.library_roots.length} roots`,
|
|
1276
|
+
`${originals} original resumes`,
|
|
1277
|
+
`${assisted} assisted variants`,
|
|
1278
|
+
`${tooLarge} too large`,
|
|
1279
|
+
`${staleRoots} stale roots`,
|
|
1280
|
+
`${persisted2 ? "persisted" : "transient"} session`
|
|
1281
|
+
].join(" • ");
|
|
1282
|
+
}
|
|
1283
|
+
function summaryRecord(card) {
|
|
1284
|
+
return card.projection.summary;
|
|
1285
|
+
}
|
|
1286
|
+
function recommendationLabel(card) {
|
|
1287
|
+
const recommendation = summaryRecord(card).recommendation;
|
|
1288
|
+
if (recommendation !== null && typeof recommendation === "object" && !Array.isArray(recommendation)) {
|
|
1289
|
+
const label = recommendation.label;
|
|
1290
|
+
return typeof label === "string" ? label : void 0;
|
|
1291
|
+
}
|
|
1292
|
+
return void 0;
|
|
1293
|
+
}
|
|
1294
|
+
function scoreValue(card) {
|
|
1295
|
+
const value = summaryRecord(card).overall_score;
|
|
1296
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1297
|
+
}
|
|
1298
|
+
function score(card) {
|
|
1299
|
+
return String(scoreValue(card) ?? "unavailable");
|
|
1300
|
+
}
|
|
1301
|
+
function badges(card, tie = false) {
|
|
1302
|
+
const flags = card.projection.ui_flags;
|
|
1303
|
+
return [
|
|
1304
|
+
...tie ? ["tie"] : [],
|
|
1305
|
+
...flags.adjusted ? ["adjusted"] : [],
|
|
1306
|
+
...flags.provisional ? ["provisional"] : [],
|
|
1307
|
+
...flags.close_cluster ? ["close cluster"] : [],
|
|
1308
|
+
...flags.stale ? ["stale"] : []
|
|
1309
|
+
];
|
|
1310
|
+
}
|
|
1311
|
+
function objectField(value, field) {
|
|
1312
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value[field] : void 0;
|
|
1313
|
+
}
|
|
1314
|
+
function previewItems(summary, field) {
|
|
1315
|
+
const values = summary[field];
|
|
1316
|
+
if (!Array.isArray(values)) return "none";
|
|
1317
|
+
const items = values.slice(0, 2).flatMap((value) => {
|
|
1318
|
+
const item = objectField(value, "item") ?? objectField(value, "title");
|
|
1319
|
+
return typeof item === "string" ? [item.slice(0, 80)] : [];
|
|
1320
|
+
});
|
|
1321
|
+
return items.length > 0 ? items.join(", ") : "none";
|
|
1322
|
+
}
|
|
1323
|
+
function matchProjectionDetails(projection) {
|
|
1324
|
+
const summary = projection.summary;
|
|
1325
|
+
const confidence = summary.confidence_context;
|
|
1326
|
+
const resumeConfidence = objectField(confidence, "resume_parse_confidence");
|
|
1327
|
+
const jobConfidence = objectField(confidence, "job_parse_confidence");
|
|
1328
|
+
const resumeLabel2 = objectField(resumeConfidence, "label");
|
|
1329
|
+
const resumeScore = objectField(resumeConfidence, "score");
|
|
1330
|
+
const jobLabel = objectField(jobConfidence, "label");
|
|
1331
|
+
const jobScore = objectField(jobConfidence, "score");
|
|
1332
|
+
const warnings = summary.warnings;
|
|
1333
|
+
const warningCount = Array.isArray(warnings) ? warnings.length : 0;
|
|
1334
|
+
return [
|
|
1335
|
+
`confidence resume ${String(resumeLabel2 ?? "unavailable")} ${String(resumeScore ?? "-")} • job ${String(jobLabel ?? "unavailable")} ${String(jobScore ?? "-")}`,
|
|
1336
|
+
`strengths ${previewItems(summary, "top_strengths")}`,
|
|
1337
|
+
`gaps ${previewItems(summary, "top_gaps")} • warnings ${warningCount}`
|
|
1338
|
+
];
|
|
1339
|
+
}
|
|
1340
|
+
function plainResultCard(card, tie = false) {
|
|
1341
|
+
const labels = badges(card, tie);
|
|
1342
|
+
const recommendation = recommendationLabel(card);
|
|
1343
|
+
return [
|
|
1344
|
+
`${card.workflow === "match" ? "Career match" : "Career analyze"}: ${card.resume_label}`,
|
|
1345
|
+
`score ${score(card)}${recommendation ? ` • ${recommendation}` : ""}`,
|
|
1346
|
+
...labels.length > 0 ? [labels.join(" • ")] : [],
|
|
1347
|
+
...card.workflow === "match" ? matchProjectionDetails(card.projection) : []
|
|
1348
|
+
].join("\n");
|
|
1349
|
+
}
|
|
1350
|
+
function stateEntryText(data) {
|
|
1351
|
+
switch (data.kind) {
|
|
1352
|
+
case "vacancy":
|
|
1353
|
+
return `Career vacancy: ${data.vacancy_label}`;
|
|
1354
|
+
case "vacancy_clear":
|
|
1355
|
+
return "Career vacancy cleared";
|
|
1356
|
+
case "consent":
|
|
1357
|
+
return `Career session-persistence consent: ${data.granted ? "granted" : "declined"}`;
|
|
1358
|
+
case "consent_clear":
|
|
1359
|
+
return "Career session-persistence consent cleared";
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function resultCardLines(card, theme, width, tie) {
|
|
1363
|
+
const label = theme.fg("accent", theme.bold(card.resume_label));
|
|
1364
|
+
const flags = badges(card, tie);
|
|
1365
|
+
const recommendation = recommendationLabel(card);
|
|
1366
|
+
const recommendationText = recommendation ? ` • ${recommendation}` : "";
|
|
1367
|
+
const flagText = flags.length > 0 ? flags.join(" • ") : void 0;
|
|
1368
|
+
const matchDetails = card.workflow === "match" ? matchProjectionDetails(card.projection) : [];
|
|
1369
|
+
if (width >= 100) {
|
|
1370
|
+
return [
|
|
1371
|
+
`${label} • score ${score(card)}${recommendationText}${flagText ? ` • ${flagText}` : ""}`,
|
|
1372
|
+
...matchDetails
|
|
1373
|
+
];
|
|
1374
|
+
}
|
|
1375
|
+
if (width >= 80) {
|
|
1376
|
+
return [label, `score ${score(card)}${recommendationText}`, ...flagText ? [flagText] : [], ...matchDetails];
|
|
1377
|
+
}
|
|
1378
|
+
return [label, `score ${score(card)}`, ...flagText ? [flagText] : [], ...matchDetails];
|
|
1379
|
+
}
|
|
1380
|
+
var ResponsiveCard = class {
|
|
1381
|
+
constructor(data, theme, tie) {
|
|
1382
|
+
this.data = data;
|
|
1383
|
+
this.theme = theme;
|
|
1384
|
+
this.tie = tie;
|
|
1385
|
+
}
|
|
1386
|
+
data;
|
|
1387
|
+
theme;
|
|
1388
|
+
tie;
|
|
1389
|
+
render(width) {
|
|
1390
|
+
if (this.data.kind !== "result_card") {
|
|
1391
|
+
return [truncateToWidth(this.theme.fg("muted", stateEntryText(this.data)), width)];
|
|
1392
|
+
}
|
|
1393
|
+
return resultCardLines(this.data, this.theme, width, this.tie).map((line) => truncateToWidth(line, width));
|
|
1394
|
+
}
|
|
1395
|
+
invalidate() {
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
function registerWorkflowEntryRenderer(pi, currentData, currentTie) {
|
|
1399
|
+
pi.registerEntryRenderer(WORKFLOW_RENDERER_TYPE, (entry, _options, theme) => {
|
|
1400
|
+
const parsed = parseWorkflowEntryData(entry.data);
|
|
1401
|
+
if (parsed === void 0) return void 0;
|
|
1402
|
+
const data = currentData?.(parsed.state_id) ?? parsed;
|
|
1403
|
+
const tie = data.kind === "result_card" && currentTie?.(data.state_id) === true;
|
|
1404
|
+
const container = new Container();
|
|
1405
|
+
container.addChild(new DynamicBorder((text) => theme.fg("borderMuted", text)));
|
|
1406
|
+
container.addChild(new ResponsiveCard(data, theme, tie));
|
|
1407
|
+
container.addChild(new DynamicBorder((text) => theme.fg("borderMuted", text)));
|
|
1408
|
+
return container;
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
var WORKFLOW_RENDERER_TYPE = "career.workflow";
|
|
1412
|
+
function oversizeResultMessage(command, runId, code) {
|
|
1413
|
+
return `${command} • run ${runId} • ${code}
|
|
1414
|
+
No partial output exists and the result was not stored.`;
|
|
1415
|
+
}
|
|
1416
|
+
function unavailableMatchResultMessage(runId, resumeLabel2, code) {
|
|
1417
|
+
return `career-match • run ${runId} • ${code}
|
|
1418
|
+
${resumeLabel2} • result unavailable
|
|
1419
|
+
No partial output exists and the result was not stored.`;
|
|
1420
|
+
}
|
|
1421
|
+
function deriveMatchTieStateIds(cards) {
|
|
1422
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
1423
|
+
for (const card of cards) {
|
|
1424
|
+
if (card.workflow !== "match" || scoreValue(card) === void 0) continue;
|
|
1425
|
+
const runCards = byRun.get(card.run_id) ?? [];
|
|
1426
|
+
runCards.push(card);
|
|
1427
|
+
byRun.set(card.run_id, runCards);
|
|
1428
|
+
}
|
|
1429
|
+
const tied = /* @__PURE__ */ new Set();
|
|
1430
|
+
for (const runCards of byRun.values()) {
|
|
1431
|
+
const topScore = Math.max(...runCards.map((card) => scoreValue(card)));
|
|
1432
|
+
const topCards = runCards.filter((card) => scoreValue(card) === topScore);
|
|
1433
|
+
if (topCards.length < 2) continue;
|
|
1434
|
+
for (const card of topCards) tied.add(card.state_id);
|
|
1435
|
+
}
|
|
1436
|
+
return tied;
|
|
1437
|
+
}
|
|
1438
|
+
function rankedRows(ranked) {
|
|
1439
|
+
return ranked.map((item, index) => {
|
|
1440
|
+
const labels = [
|
|
1441
|
+
...item.tie ? ["tie"] : [],
|
|
1442
|
+
...item.closeCluster ? ["close cluster"] : [],
|
|
1443
|
+
...item.projection.ui_flags.provisional ? ["provisional"] : []
|
|
1444
|
+
];
|
|
1445
|
+
return `${index + 1}. ${item.resume.label} — ${item.overallScore} — ${item.recommendation}${labels.length ? ` — ${labels.join(", ")}` : ""}`;
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
function analyzeDetailSections(result) {
|
|
1449
|
+
return [
|
|
1450
|
+
{ label: "checks", value: result.checks },
|
|
1451
|
+
{ label: "confidence_context", value: result.confidence_context },
|
|
1452
|
+
{ label: "top_strengths", value: result.top_strengths },
|
|
1453
|
+
{ label: "top_weaknesses", value: result.top_weaknesses },
|
|
1454
|
+
{ label: "improvement_actions", value: result.improvement_actions },
|
|
1455
|
+
{ label: "warnings", value: result.warnings }
|
|
1456
|
+
];
|
|
1457
|
+
}
|
|
1458
|
+
function matchDetailSections(result, vacancyText) {
|
|
1459
|
+
return [
|
|
1460
|
+
{ label: "category_results", value: result.category_results },
|
|
1461
|
+
{ label: "confidence_context", value: result.confidence_context },
|
|
1462
|
+
{ label: "top_strengths", value: result.top_strengths },
|
|
1463
|
+
{ label: "top_gaps", value: result.top_gaps },
|
|
1464
|
+
{ label: "recommendation", value: result.recommendation },
|
|
1465
|
+
{ label: "warnings", value: result.warnings },
|
|
1466
|
+
{ label: "current vacancy preview", value: vacancyText.slice(0, 1e3) }
|
|
1467
|
+
];
|
|
1468
|
+
}
|
|
1469
|
+
function detailText(section) {
|
|
1470
|
+
const text = JSON.stringify({ [section.label]: section.value }, null, 2);
|
|
1471
|
+
if (Buffer.byteLength(text, "utf8") > UI_TEXT_MAX) {
|
|
1472
|
+
return `${section.label}: detail is too large for the bounded pane; no partial detail is shown.`;
|
|
1473
|
+
}
|
|
1474
|
+
return text;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/workflow/result-projection.ts
|
|
1478
|
+
function isRecord3(value) {
|
|
1479
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1480
|
+
}
|
|
1481
|
+
function numberField(value, field) {
|
|
1482
|
+
const found = value[field];
|
|
1483
|
+
if (typeof found !== "number" || !Number.isFinite(found)) throw workflowError("core_result_invalid");
|
|
1484
|
+
return found;
|
|
1485
|
+
}
|
|
1486
|
+
function recordField(value, field) {
|
|
1487
|
+
const found = value[field];
|
|
1488
|
+
if (!isRecord3(found)) throw workflowError("core_result_invalid");
|
|
1489
|
+
return found;
|
|
1490
|
+
}
|
|
1491
|
+
function arrayField(value, field) {
|
|
1492
|
+
const found = value[field];
|
|
1493
|
+
if (!Array.isArray(found)) throw workflowError("core_result_invalid");
|
|
1494
|
+
return found;
|
|
1495
|
+
}
|
|
1496
|
+
function compactObjects(value, fields, maximum) {
|
|
1497
|
+
return value.slice(0, maximum).flatMap((candidate) => {
|
|
1498
|
+
if (!isRecord3(candidate)) return [];
|
|
1499
|
+
const selected = {};
|
|
1500
|
+
for (const field of fields) {
|
|
1501
|
+
if (candidate[field] !== void 0) selected[field] = candidate[field];
|
|
1502
|
+
}
|
|
1503
|
+
return [selected];
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
function compactWarnings(value) {
|
|
1507
|
+
return compactObjects(value, ["code", "message", "related_fields", "related_categories"], 3);
|
|
1508
|
+
}
|
|
1509
|
+
function parseConfidencePreview(value) {
|
|
1510
|
+
if (!isRecord3(value) || typeof value.label !== "string" || typeof value.score !== "number") {
|
|
1511
|
+
throw workflowError("core_result_invalid");
|
|
1512
|
+
}
|
|
1513
|
+
return { label: value.label, score: value.score };
|
|
1514
|
+
}
|
|
1515
|
+
function parseCoreJson(json) {
|
|
1516
|
+
try {
|
|
1517
|
+
const value = JSON.parse(json);
|
|
1518
|
+
if (!isRecord3(value)) throw workflowError("core_result_invalid");
|
|
1519
|
+
return value;
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
if (error instanceof Error && error.name === "CareerWorkflowError") throw error;
|
|
1522
|
+
throw workflowError("core_result_invalid");
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
function projectResumeAnalysis(result) {
|
|
1526
|
+
if (result.schema_version !== "career.resume_analysis.v1") throw workflowError("core_result_invalid");
|
|
1527
|
+
const checks = arrayField(result, "checks");
|
|
1528
|
+
const confidence = recordField(result, "confidence_context");
|
|
1529
|
+
const parseConfidence = parseConfidencePreview(confidence.parse_confidence);
|
|
1530
|
+
const adjusted = checks.some((check) => isRecord3(check) && check.score_adjusted === true);
|
|
1531
|
+
return {
|
|
1532
|
+
schema_version: RESULT_PROJECTION_SCHEMA,
|
|
1533
|
+
core_schema_version: "career.resume_analysis.v1",
|
|
1534
|
+
summary: {
|
|
1535
|
+
overall_score: numberField(result, "overall_score"),
|
|
1536
|
+
category_scores: recordField(result, "category_scores"),
|
|
1537
|
+
confidence_context: { parse_confidence: parseConfidence },
|
|
1538
|
+
top_strengths: compactObjects(arrayField(result, "top_strengths"), ["area", "title", "status"], 2),
|
|
1539
|
+
top_weaknesses: compactObjects(arrayField(result, "top_weaknesses"), ["area", "title", "status"], 2),
|
|
1540
|
+
improvement_actions: compactObjects(
|
|
1541
|
+
arrayField(result, "improvement_actions"),
|
|
1542
|
+
["priority", "area", "action", "basis_check_id", "status"],
|
|
1543
|
+
2
|
|
1544
|
+
),
|
|
1545
|
+
warnings: compactWarnings(arrayField(result, "warnings"))
|
|
1546
|
+
},
|
|
1547
|
+
ui_flags: { adjusted, provisional: false, close_cluster: false, stale: false }
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function projectJobMatch(result) {
|
|
1551
|
+
if (result.schema_version !== "career.job_match.v1") throw workflowError("core_result_invalid");
|
|
1552
|
+
const categories = arrayField(result, "category_results");
|
|
1553
|
+
const confidence = recordField(result, "confidence_context");
|
|
1554
|
+
const recommendation = recordField(result, "recommendation");
|
|
1555
|
+
if (typeof recommendation.label !== "string") throw workflowError("core_result_invalid");
|
|
1556
|
+
const provisional = confidence.is_uncertain === true;
|
|
1557
|
+
const adjusted = categories.some((category) => isRecord3(category) && category.score_adjusted === true);
|
|
1558
|
+
return {
|
|
1559
|
+
schema_version: RESULT_PROJECTION_SCHEMA,
|
|
1560
|
+
core_schema_version: "career.job_match.v1",
|
|
1561
|
+
summary: {
|
|
1562
|
+
overall_score: numberField(result, "overall_score"),
|
|
1563
|
+
category_scores: recordField(result, "category_scores"),
|
|
1564
|
+
confidence_context: {
|
|
1565
|
+
resume_parse_confidence: parseConfidencePreview(confidence.resume_parse_confidence),
|
|
1566
|
+
job_parse_confidence: parseConfidencePreview(confidence.job_parse_confidence),
|
|
1567
|
+
is_uncertain: provisional
|
|
1568
|
+
},
|
|
1569
|
+
top_strengths: compactObjects(
|
|
1570
|
+
arrayField(result, "top_strengths"),
|
|
1571
|
+
["category", "item", "status", "match_type"],
|
|
1572
|
+
2
|
|
1573
|
+
),
|
|
1574
|
+
top_gaps: compactObjects(arrayField(result, "top_gaps"), ["category", "item", "status"], 2),
|
|
1575
|
+
recommendation: compactObjects([recommendation], ["label", "status"], 1)[0] ?? {},
|
|
1576
|
+
warnings: compactWarnings(arrayField(result, "warnings"))
|
|
1577
|
+
},
|
|
1578
|
+
ui_flags: { adjusted, provisional, close_cluster: false, stale: false }
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
function createResultCard(options) {
|
|
1582
|
+
return {
|
|
1583
|
+
schema_version: WORKFLOW_STATE_SCHEMA,
|
|
1584
|
+
kind: "result_card",
|
|
1585
|
+
state_id: options.uuid(),
|
|
1586
|
+
created_at: options.now().toISOString(),
|
|
1587
|
+
workflow: options.workflow,
|
|
1588
|
+
run_id: options.runId,
|
|
1589
|
+
resume_id: options.resume.id,
|
|
1590
|
+
resume_label: options.resume.label,
|
|
1591
|
+
resume_path_fingerprint: sha256(options.resume.path),
|
|
1592
|
+
input_digests: {
|
|
1593
|
+
resume_text_sha256: options.resume.text_sha256,
|
|
1594
|
+
vacancy_text_sha256: options.vacancy?.vacancy_text_sha256 ?? sha256("")
|
|
1595
|
+
},
|
|
1596
|
+
projection: options.projection
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
var RECOMMENDATION_BUCKET = {
|
|
1600
|
+
apply_now: 3,
|
|
1601
|
+
apply_after_small_edits: 2,
|
|
1602
|
+
improve_first: 1
|
|
1603
|
+
};
|
|
1604
|
+
function recommendationLabel2(result) {
|
|
1605
|
+
const recommendation = recordField(result, "recommendation").label;
|
|
1606
|
+
if (recommendation !== "apply_now" && recommendation !== "apply_after_small_edits" && recommendation !== "improve_first") throw workflowError("core_result_invalid");
|
|
1607
|
+
return recommendation;
|
|
1608
|
+
}
|
|
1609
|
+
function compareText2(left, right) {
|
|
1610
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1611
|
+
}
|
|
1612
|
+
function rankMatches(values) {
|
|
1613
|
+
const ranked = values.map(({ resume, result, projection }) => ({
|
|
1614
|
+
resume,
|
|
1615
|
+
result,
|
|
1616
|
+
projection: projection ?? projectJobMatch(result),
|
|
1617
|
+
overallScore: numberField(result, "overall_score"),
|
|
1618
|
+
recommendation: recommendationLabel2(result),
|
|
1619
|
+
tie: false,
|
|
1620
|
+
closeCluster: false
|
|
1621
|
+
}));
|
|
1622
|
+
ranked.sort((left, right) => {
|
|
1623
|
+
if (left.overallScore !== right.overallScore) return right.overallScore - left.overallScore;
|
|
1624
|
+
const bucket = RECOMMENDATION_BUCKET[right.recommendation] - RECOMMENDATION_BUCKET[left.recommendation];
|
|
1625
|
+
if (bucket !== 0) return bucket;
|
|
1626
|
+
const pathOrder = compareText2(left.resume.path, right.resume.path);
|
|
1627
|
+
return pathOrder !== 0 ? pathOrder : compareText2(left.resume.id, right.resume.id);
|
|
1628
|
+
});
|
|
1629
|
+
const topScore = ranked[0]?.overallScore;
|
|
1630
|
+
const secondScore = ranked[1]?.overallScore;
|
|
1631
|
+
const tie = topScore !== void 0 && secondScore === topScore;
|
|
1632
|
+
const close = topScore !== void 0 && secondScore !== void 0 && topScore - secondScore <= 3;
|
|
1633
|
+
for (const [index, item] of ranked.entries()) {
|
|
1634
|
+
item.tie = tie && item.overallScore === topScore;
|
|
1635
|
+
item.closeCluster = close && index < 2;
|
|
1636
|
+
item.projection = {
|
|
1637
|
+
...item.projection,
|
|
1638
|
+
ui_flags: { ...item.projection.ui_flags, close_cluster: item.closeCluster }
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
return ranked;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// src/workflow/commands.ts
|
|
1645
|
+
var CONSENT_COPY = "Pi may save private vacancy/resume text and result cards in the current session JSONL. `pi-career` does not write documents outside the files you chose. Use `pi --no-session` for an ephemeral run. This is not secure erasure.";
|
|
1646
|
+
var TRANSIENT_NOTICE = "Transient session: pi-career workflow entries are not written to a session JSONL.";
|
|
1647
|
+
var BANNER = "pi-career not configured — run /career-setup";
|
|
1648
|
+
var MAX_FILTER_CHARACTERS = 200;
|
|
1649
|
+
var RunOwner = class {
|
|
1650
|
+
constructor(uuid) {
|
|
1651
|
+
this.uuid = uuid;
|
|
1652
|
+
}
|
|
1653
|
+
uuid;
|
|
1654
|
+
sequence = 0;
|
|
1655
|
+
current;
|
|
1656
|
+
start(ctx) {
|
|
1657
|
+
this.current?.controller.abort();
|
|
1658
|
+
const run = {
|
|
1659
|
+
sequence: ++this.sequence,
|
|
1660
|
+
runId: this.uuid(),
|
|
1661
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
1662
|
+
controller: new AbortController()
|
|
1663
|
+
};
|
|
1664
|
+
this.current = run;
|
|
1665
|
+
return run;
|
|
1666
|
+
}
|
|
1667
|
+
assert(run, ctx) {
|
|
1668
|
+
if (this.current !== run || run.controller.signal.aborted || ctx.sessionManager.getSessionId() !== run.sessionId) throw workflowError("workflow_stale");
|
|
1669
|
+
}
|
|
1670
|
+
invalidate() {
|
|
1671
|
+
this.sequence += 1;
|
|
1672
|
+
this.current?.controller.abort();
|
|
1673
|
+
this.current = void 0;
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
function persisted(ctx) {
|
|
1677
|
+
return ctx.sessionManager.getSessionFile() !== void 0;
|
|
1678
|
+
}
|
|
1679
|
+
function safeAdapterCode(error) {
|
|
1680
|
+
return error instanceof CareerInvocationError ? error.payload.code : void 0;
|
|
1681
|
+
}
|
|
1682
|
+
function isOversizeCode(code) {
|
|
1683
|
+
return code === "result_too_large" || code === "result_too_many_lines";
|
|
1684
|
+
}
|
|
1685
|
+
function requireInteractive(ctx) {
|
|
1686
|
+
if (!ctx.hasUI) throw workflowError("interactive_mode_required");
|
|
1687
|
+
}
|
|
1688
|
+
function parseStatusArgument(args) {
|
|
1689
|
+
const value = args.trim();
|
|
1690
|
+
if (value === "") return "default";
|
|
1691
|
+
if (value === "status") return "status";
|
|
1692
|
+
throw workflowError("invalid_command_arguments");
|
|
1693
|
+
}
|
|
1694
|
+
function parseFilter(args) {
|
|
1695
|
+
const value = args.trim();
|
|
1696
|
+
if (value.length > MAX_FILTER_CHARACTERS || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
1697
|
+
throw workflowError("invalid_command_arguments");
|
|
1698
|
+
}
|
|
1699
|
+
return value.toLowerCase();
|
|
1700
|
+
}
|
|
1701
|
+
function filteredResumes(records, filter) {
|
|
1702
|
+
if (!filter) return records;
|
|
1703
|
+
return records.filter(
|
|
1704
|
+
(record) => `${record.label}
|
|
1705
|
+
${record.relative_path}
|
|
1706
|
+
${record.id}`.toLowerCase().includes(filter)
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
function recordBadges(record) {
|
|
1710
|
+
return [
|
|
1711
|
+
...record.kind === "assisted_variant" ? ["assisted variant"] : [],
|
|
1712
|
+
...record.too_large_for_core_input === true ? ["too large"] : []
|
|
1713
|
+
].join(", ");
|
|
1714
|
+
}
|
|
1715
|
+
function recordOption(record) {
|
|
1716
|
+
const badges2 = recordBadges(record);
|
|
1717
|
+
return `${record.label}${badges2 ? ` — ${badges2}` : ""} — ${record.id.slice(0, 12)}`;
|
|
1718
|
+
}
|
|
1719
|
+
function rootOption(root) {
|
|
1720
|
+
return `${root.label} — ${privacyDisplayPath(root.path)} — ${root.id.slice(0, 12)}`;
|
|
1721
|
+
}
|
|
1722
|
+
async function loadLibrary(dependencies) {
|
|
1723
|
+
const config = await loadConfig(dependencies.agentDir);
|
|
1724
|
+
return { config, scan: await scanLibrary(config) };
|
|
1725
|
+
}
|
|
1726
|
+
async function runOperation(ctx, owner, run, label, operation) {
|
|
1727
|
+
owner.assert(run, ctx);
|
|
1728
|
+
if (ctx.mode !== "tui") {
|
|
1729
|
+
ctx.ui.notify(label, "info");
|
|
1730
|
+
const value = await operation(run.controller.signal);
|
|
1731
|
+
owner.assert(run, ctx);
|
|
1732
|
+
return value;
|
|
1733
|
+
}
|
|
1734
|
+
const result = await ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
1735
|
+
const loader = new BorderedLoader(tui, theme, label);
|
|
1736
|
+
let settled = false;
|
|
1737
|
+
const finish = (value) => {
|
|
1738
|
+
if (settled) return;
|
|
1739
|
+
settled = true;
|
|
1740
|
+
done(value);
|
|
1741
|
+
};
|
|
1742
|
+
loader.onAbort = () => {
|
|
1743
|
+
run.controller.abort();
|
|
1744
|
+
finish(null);
|
|
1745
|
+
};
|
|
1746
|
+
operation(run.controller.signal).then((value) => finish({ ok: true, value })).catch((error) => finish({ ok: false, error }));
|
|
1747
|
+
return loader;
|
|
1748
|
+
});
|
|
1749
|
+
if (result === null) throw workflowError("workflow_cancelled");
|
|
1750
|
+
if (!result.ok) throw result.error;
|
|
1751
|
+
owner.assert(run, ctx);
|
|
1752
|
+
return result.value;
|
|
1753
|
+
}
|
|
1754
|
+
function appendData(pi, owner, run, ctx, data) {
|
|
1755
|
+
owner.assert(run, ctx);
|
|
1756
|
+
pi.appendEntry(WORKFLOW_CUSTOM_TYPE, data);
|
|
1757
|
+
}
|
|
1758
|
+
function retainOversizeFailure(error, resume, unavailable) {
|
|
1759
|
+
const code = safeAdapterCode(error);
|
|
1760
|
+
if (!isOversizeCode(code)) return false;
|
|
1761
|
+
unavailable.set(resume.id, { resume, code });
|
|
1762
|
+
return true;
|
|
1763
|
+
}
|
|
1764
|
+
async function normalizeVacancy(dependencies, vacancy, signal) {
|
|
1765
|
+
const normalized = await dependencies.invoke(
|
|
1766
|
+
{ kind: "job", operation: "normalize", inputJson: serializeCoreInput(buildJobInput(vacancy)) },
|
|
1767
|
+
signal
|
|
1768
|
+
);
|
|
1769
|
+
if (parseCoreJson(normalized.json).schema_version !== "career.job_normalization.v1") {
|
|
1770
|
+
throw workflowError("core_result_invalid");
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
async function analyzeMatchResumes(dependencies, resumes, signal, unavailable) {
|
|
1774
|
+
for (const resume of resumes) {
|
|
1775
|
+
if (signal.aborted) throw workflowError("workflow_cancelled");
|
|
1776
|
+
try {
|
|
1777
|
+
const invocation = await dependencies.invoke(
|
|
1778
|
+
{ kind: "resume", operation: "analyze", inputJson: serializeCoreInput(buildResumeInput(resume)) },
|
|
1779
|
+
signal
|
|
1780
|
+
);
|
|
1781
|
+
projectResumeAnalysis(parseCoreJson(invocation.json));
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
if (!retainOversizeFailure(error, resume, unavailable)) throw error;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
async function matchResumes(dependencies, resumes, vacancy, signal, unavailable) {
|
|
1788
|
+
const matches = [];
|
|
1789
|
+
for (const resume of resumes) {
|
|
1790
|
+
if (signal.aborted) throw workflowError("workflow_cancelled");
|
|
1791
|
+
try {
|
|
1792
|
+
const invocation = await dependencies.invoke(
|
|
1793
|
+
{ kind: "job", operation: "match", inputJson: serializeCoreInput(buildJobMatchInput(resume, vacancy)) },
|
|
1794
|
+
signal
|
|
1795
|
+
);
|
|
1796
|
+
const result = parseCoreJson(invocation.json);
|
|
1797
|
+
if (!unavailable.has(resume.id)) matches.push({ resume, result });
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
if (!retainOversizeFailure(error, resume, unavailable)) throw error;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return matches;
|
|
1803
|
+
}
|
|
1804
|
+
async function executeMatchQueue(dependencies, resumes, vacancy, signal) {
|
|
1805
|
+
const unavailable = /* @__PURE__ */ new Map();
|
|
1806
|
+
await normalizeVacancy(dependencies, vacancy, signal);
|
|
1807
|
+
await analyzeMatchResumes(dependencies, resumes, signal, unavailable);
|
|
1808
|
+
const matches = await matchResumes(dependencies, resumes, vacancy, signal, unavailable);
|
|
1809
|
+
return { matches, unavailable };
|
|
1810
|
+
}
|
|
1811
|
+
function matchBatchSummary(cards, ranked, unavailable, runId) {
|
|
1812
|
+
const visibleCards = cards.slice(0, 20).map(
|
|
1813
|
+
(card, index) => `${index + 1}. ${plainResultCard(card, ranked[index]?.tie === true)}`
|
|
1814
|
+
);
|
|
1815
|
+
const unavailableRows = [...unavailable.values()].map(
|
|
1816
|
+
(item) => unavailableMatchResultMessage(runId, item.resume.label, item.code)
|
|
1817
|
+
);
|
|
1818
|
+
const sections = [
|
|
1819
|
+
...visibleCards.length === 0 ? [] : [visibleCards.join("\n\n")],
|
|
1820
|
+
...ranked.length > visibleCards.length ? [`Showing ${visibleCards.length} of ${ranked.length} ranked rows.`] : [],
|
|
1821
|
+
...unavailableRows.length === 0 ? [] : ["Unranked result-unavailable rows:", ...unavailableRows]
|
|
1822
|
+
];
|
|
1823
|
+
return sections.join("\n\n");
|
|
1824
|
+
}
|
|
1825
|
+
async function showDetail(ctx, sections) {
|
|
1826
|
+
const choice = await ctx.ui.select("Career detail", [...sections.map((section2) => section2.label), "Close"]);
|
|
1827
|
+
const section = sections.find((candidate) => candidate.label === choice);
|
|
1828
|
+
if (section !== void 0) ctx.ui.notify(detailText(section), "info");
|
|
1829
|
+
}
|
|
1830
|
+
function registerCareerCommands(pi, options = {}) {
|
|
1831
|
+
const dependencies = {
|
|
1832
|
+
agentDir: options.agentDir ?? getAgentDir(),
|
|
1833
|
+
invoke: options.invoke ?? invokeCareerCli,
|
|
1834
|
+
now: options.now ?? (() => /* @__PURE__ */ new Date()),
|
|
1835
|
+
uuid: options.uuid ?? randomUUID2
|
|
1836
|
+
};
|
|
1837
|
+
const owner = new RunOwner(dependencies.uuid);
|
|
1838
|
+
let transientNoticeSession;
|
|
1839
|
+
const renderedData = /* @__PURE__ */ new Map();
|
|
1840
|
+
const renderedTieStateIds = /* @__PURE__ */ new Set();
|
|
1841
|
+
const refreshState = async (ctx) => {
|
|
1842
|
+
const library = await loadLibrary(dependencies);
|
|
1843
|
+
const branch = ctx.sessionManager.getBranch();
|
|
1844
|
+
const state = withCurrentStaleness(reconstructWorkflowState(branch), library.scan);
|
|
1845
|
+
renderedData.clear();
|
|
1846
|
+
for (const entry of [
|
|
1847
|
+
...state.vacancy === void 0 ? [] : [state.vacancy],
|
|
1848
|
+
...state.consent === void 0 ? [] : [state.consent],
|
|
1849
|
+
...state.result_cards
|
|
1850
|
+
]) renderedData.set(entry.state_id, entry);
|
|
1851
|
+
renderedTieStateIds.clear();
|
|
1852
|
+
for (const stateId of deriveMatchTieStateIds(workflowResultCards(branch))) {
|
|
1853
|
+
renderedTieStateIds.add(stateId);
|
|
1854
|
+
}
|
|
1855
|
+
return library;
|
|
1856
|
+
};
|
|
1857
|
+
registerWorkflowEntryRenderer(
|
|
1858
|
+
pi,
|
|
1859
|
+
(stateId) => renderedData.get(stateId),
|
|
1860
|
+
(stateId) => renderedTieStateIds.has(stateId)
|
|
1861
|
+
);
|
|
1862
|
+
const ensureConsent = async (ctx, run) => {
|
|
1863
|
+
if (!persisted(ctx)) {
|
|
1864
|
+
if (transientNoticeSession !== run.sessionId) {
|
|
1865
|
+
ctx.ui.notify(TRANSIENT_NOTICE, "info");
|
|
1866
|
+
transientNoticeSession = run.sessionId;
|
|
1867
|
+
}
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const state = reconstructWorkflowState(ctx.sessionManager.getBranch());
|
|
1871
|
+
if (state.consent?.granted === true) return;
|
|
1872
|
+
const choice = await ctx.ui.select(CONSENT_COPY, [
|
|
1873
|
+
"Continue in this session",
|
|
1874
|
+
"Cancel and restart with --no-session"
|
|
1875
|
+
]);
|
|
1876
|
+
owner.assert(run, ctx);
|
|
1877
|
+
const granted = choice === "Continue in this session";
|
|
1878
|
+
appendData(pi, owner, run, ctx, createConsentEntry(granted, dependencies));
|
|
1879
|
+
if (!granted) throw workflowError("consent_required");
|
|
1880
|
+
};
|
|
1881
|
+
const handle = async (ctx, action) => {
|
|
1882
|
+
try {
|
|
1883
|
+
await action();
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
if (!ctx.hasUI) throw error;
|
|
1886
|
+
if (error instanceof CareerWorkflowError) {
|
|
1887
|
+
const type = error.code === "workflow_cancelled" || error.code === "workflow_stale" ? "info" : "error";
|
|
1888
|
+
ctx.ui.notify(workflowErrorMessage(error.code), type);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (error instanceof CareerInvocationError) {
|
|
1892
|
+
ctx.ui.notify(`${error.payload.code}: ${error.payload.message}`, "error");
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
ctx.ui.notify(workflowErrorMessage("workflow_failed"), "error");
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
pi.registerCommand("career-setup", {
|
|
1899
|
+
description: "Configure deterministic resume-library roots",
|
|
1900
|
+
getArgumentCompletions: (prefix) => "status".startsWith(prefix) ? [{ value: "status", label: "status" }] : null,
|
|
1901
|
+
handler: async (args, ctx) => handle(ctx, async () => {
|
|
1902
|
+
requireInteractive(ctx);
|
|
1903
|
+
const mode = parseStatusArgument(args);
|
|
1904
|
+
const run = owner.start(ctx);
|
|
1905
|
+
const { config, scan } = await refreshState(ctx);
|
|
1906
|
+
owner.assert(run, ctx);
|
|
1907
|
+
const summary = setupSummary(config, scan, persisted(ctx));
|
|
1908
|
+
if (mode === "status") {
|
|
1909
|
+
ctx.ui.notify(summary, "info");
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
ctx.ui.notify(summary, "info");
|
|
1913
|
+
if (config.library_roots.length === 0) ctx.ui.notify(BANNER, "warning");
|
|
1914
|
+
const action = await ctx.ui.select("Career setup", ["Add root", "Rescan", "Status", "Close"]);
|
|
1915
|
+
owner.assert(run, ctx);
|
|
1916
|
+
if (action === "Add root") {
|
|
1917
|
+
const rootPath = await ctx.ui.input("Resume root", "Absolute path");
|
|
1918
|
+
if (rootPath === void 0) return;
|
|
1919
|
+
const updated = await addLibraryRoot(config, rootPath);
|
|
1920
|
+
owner.assert(run, ctx);
|
|
1921
|
+
await writeConfig(dependencies.agentDir, updated, dependencies.uuid);
|
|
1922
|
+
owner.assert(run, ctx);
|
|
1923
|
+
const rescanned = await scanLibrary(updated);
|
|
1924
|
+
ctx.ui.notify(setupSummary(updated, rescanned, persisted(ctx)), "info");
|
|
1925
|
+
} else if (action === "Rescan" || action === "Status") {
|
|
1926
|
+
ctx.ui.notify(summary, "info");
|
|
1927
|
+
}
|
|
1928
|
+
})
|
|
1929
|
+
});
|
|
1930
|
+
pi.registerCommand("career-library", {
|
|
1931
|
+
description: "Browse deterministic resume-library entries",
|
|
1932
|
+
getArgumentCompletions: (prefix) => "status".startsWith(prefix) ? [{ value: "status", label: "status" }] : null,
|
|
1933
|
+
handler: async (args, ctx) => handle(ctx, async () => {
|
|
1934
|
+
requireInteractive(ctx);
|
|
1935
|
+
const mode = parseStatusArgument(args);
|
|
1936
|
+
const run = owner.start(ctx);
|
|
1937
|
+
const { config, scan } = await refreshState(ctx);
|
|
1938
|
+
owner.assert(run, ctx);
|
|
1939
|
+
const summary = librarySummary(config, scan, persisted(ctx));
|
|
1940
|
+
if (mode === "status") {
|
|
1941
|
+
ctx.ui.notify(summary, "info");
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const roots = config.library_roots.map(rootOption).join("\n") || "No configured roots";
|
|
1945
|
+
ctx.ui.notify(`${roots}
|
|
1946
|
+
${libraryIndexPreview(config, scan)}
|
|
1947
|
+
${summary}`, "info");
|
|
1948
|
+
const action = await ctx.ui.select("Career library", [
|
|
1949
|
+
"Browse",
|
|
1950
|
+
"Add root",
|
|
1951
|
+
"Remove root",
|
|
1952
|
+
"Rescan",
|
|
1953
|
+
"Status",
|
|
1954
|
+
"Close"
|
|
1955
|
+
]);
|
|
1956
|
+
owner.assert(run, ctx);
|
|
1957
|
+
if (action === "Browse") {
|
|
1958
|
+
if (scan.records.length === 0) {
|
|
1959
|
+
ctx.ui.notify(BANNER, "warning");
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
const optionsByLabel = new Map(scan.records.map((record2) => [recordOption(record2), record2]));
|
|
1963
|
+
const selected = await ctx.ui.select("Indexed resumes", [...optionsByLabel.keys()]);
|
|
1964
|
+
const record = selected === void 0 ? void 0 : optionsByLabel.get(selected);
|
|
1965
|
+
if (record !== void 0) ctx.ui.notify(recordOption(record), "info");
|
|
1966
|
+
} else if (action === "Add root") {
|
|
1967
|
+
const rootPath = await ctx.ui.input("Resume root", "Absolute path");
|
|
1968
|
+
if (rootPath === void 0) return;
|
|
1969
|
+
const updated = await addLibraryRoot(config, rootPath);
|
|
1970
|
+
owner.assert(run, ctx);
|
|
1971
|
+
await writeConfig(dependencies.agentDir, updated, dependencies.uuid);
|
|
1972
|
+
ctx.ui.notify("Resume root added.", "info");
|
|
1973
|
+
} else if (action === "Remove root") {
|
|
1974
|
+
const byOption = new Map(config.library_roots.map((root2) => [rootOption(root2), root2]));
|
|
1975
|
+
const selected = await ctx.ui.select("Remove root from config", [...byOption.keys()]);
|
|
1976
|
+
const root = selected === void 0 ? void 0 : byOption.get(selected);
|
|
1977
|
+
if (root === void 0) return;
|
|
1978
|
+
const updated = removeLibraryRoot(config, root.id);
|
|
1979
|
+
owner.assert(run, ctx);
|
|
1980
|
+
await writeConfig(dependencies.agentDir, updated, dependencies.uuid);
|
|
1981
|
+
ctx.ui.notify("Resume root removed from config; no files were changed.", "info");
|
|
1982
|
+
} else if (action === "Rescan" || action === "Status") {
|
|
1983
|
+
ctx.ui.notify(summary, "info");
|
|
1984
|
+
}
|
|
1985
|
+
})
|
|
1986
|
+
});
|
|
1987
|
+
pi.registerCommand("career-vacancy", {
|
|
1988
|
+
description: "Set, view, replace, or clear the current vacancy",
|
|
1989
|
+
getArgumentCompletions: (prefix) => "clear".startsWith(prefix) ? [{ value: "clear", label: "clear" }] : null,
|
|
1990
|
+
handler: async (args, ctx) => handle(ctx, async () => {
|
|
1991
|
+
const argument = args.trim();
|
|
1992
|
+
if (argument !== "" && argument !== "clear") throw workflowError("invalid_command_arguments");
|
|
1993
|
+
const run = owner.start(ctx);
|
|
1994
|
+
const state = reconstructWorkflowState(ctx.sessionManager.getBranch());
|
|
1995
|
+
if (argument === "clear") {
|
|
1996
|
+
if (state.vacancy !== void 0) {
|
|
1997
|
+
appendData(pi, owner, run, ctx, createVacancyClearEntry(state.vacancy, dependencies));
|
|
1998
|
+
if (ctx.hasUI) ctx.ui.notify("Current career vacancy cleared.", "info");
|
|
1999
|
+
}
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
requireInteractive(ctx);
|
|
2003
|
+
let source = "paste";
|
|
2004
|
+
let prefill = "";
|
|
2005
|
+
if (state.vacancy !== void 0) {
|
|
2006
|
+
const action = await ctx.ui.select("Current career vacancy", ["Replace", "View", "Clear", "Cancel"]);
|
|
2007
|
+
owner.assert(run, ctx);
|
|
2008
|
+
if (action === "View") {
|
|
2009
|
+
ctx.ui.notify(`Current vacancy: ${state.vacancy.vacancy_label}`, "info");
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
if (action === "Clear") {
|
|
2013
|
+
appendData(pi, owner, run, ctx, createVacancyClearEntry(state.vacancy, dependencies));
|
|
2014
|
+
ctx.ui.notify("Current career vacancy cleared.", "info");
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
if (action !== "Replace") return;
|
|
2018
|
+
source = "replace";
|
|
2019
|
+
prefill = state.vacancy.vacancy_text;
|
|
2020
|
+
}
|
|
2021
|
+
const edited = await ctx.ui.editor(source === "replace" ? "Replace career vacancy" : "Paste career vacancy", prefill);
|
|
2022
|
+
if (edited === void 0) return;
|
|
2023
|
+
const text = edited.replace(/\r\n?/g, "\n");
|
|
2024
|
+
if (text.trim().length === 0 || !isWithinCoreCharacterLimit(text)) {
|
|
2025
|
+
throw workflowError("invalid_command_arguments");
|
|
2026
|
+
}
|
|
2027
|
+
await ensureConsent(ctx, run);
|
|
2028
|
+
const vacancy = createVacancyEntry(text, source, dependencies);
|
|
2029
|
+
await runOperation(ctx, owner, run, "Validating vacancy with Career Core…", async (signal) => {
|
|
2030
|
+
const result = await dependencies.invoke(
|
|
2031
|
+
{ kind: "job", operation: "normalize", inputJson: serializeCoreInput(buildJobInput(vacancy)) },
|
|
2032
|
+
signal
|
|
2033
|
+
);
|
|
2034
|
+
const parsed = parseCoreJson(result.json);
|
|
2035
|
+
if (parsed.schema_version !== "career.job_normalization.v1") throw workflowError("core_result_invalid");
|
|
2036
|
+
});
|
|
2037
|
+
appendData(pi, owner, run, ctx, vacancy);
|
|
2038
|
+
ctx.ui.notify(`Current vacancy: ${vacancy.vacancy_label}`, "info");
|
|
2039
|
+
})
|
|
2040
|
+
});
|
|
2041
|
+
pi.registerCommand("career-analyze", {
|
|
2042
|
+
description: "Run deterministic readiness analysis for one original resume",
|
|
2043
|
+
handler: async (args, ctx) => handle(ctx, async () => {
|
|
2044
|
+
requireInteractive(ctx);
|
|
2045
|
+
const filter = parseFilter(args);
|
|
2046
|
+
const run = owner.start(ctx);
|
|
2047
|
+
const { scan } = await refreshState(ctx);
|
|
2048
|
+
const candidates = filteredResumes(eligibleOriginals(scan), filter);
|
|
2049
|
+
if (candidates.length === 0) throw workflowError("library_empty");
|
|
2050
|
+
const byOption = new Map(candidates.map((record) => [recordOption(record), record]));
|
|
2051
|
+
const selected = await ctx.ui.select("Choose an original resume", [...byOption.keys()]);
|
|
2052
|
+
const resume = selected === void 0 ? void 0 : byOption.get(selected);
|
|
2053
|
+
if (resume === void 0) return;
|
|
2054
|
+
owner.assert(run, ctx);
|
|
2055
|
+
await ensureConsent(ctx, run);
|
|
2056
|
+
let result;
|
|
2057
|
+
try {
|
|
2058
|
+
result = await runOperation(ctx, owner, run, "Running deterministic resume analysis…", async (signal) => {
|
|
2059
|
+
const invocation = await dependencies.invoke(
|
|
2060
|
+
{ kind: "resume", operation: "analyze", inputJson: serializeCoreInput(buildResumeInput(resume)) },
|
|
2061
|
+
signal
|
|
2062
|
+
);
|
|
2063
|
+
return parseCoreJson(invocation.json);
|
|
2064
|
+
});
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
const code = safeAdapterCode(error);
|
|
2067
|
+
if (isOversizeCode(code)) {
|
|
2068
|
+
ctx.ui.notify(oversizeResultMessage("career-analyze", run.runId, code), "error");
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
throw error;
|
|
2072
|
+
}
|
|
2073
|
+
const projection = projectResumeAnalysis(result);
|
|
2074
|
+
const card = createResultCard({
|
|
2075
|
+
workflow: "analyze",
|
|
2076
|
+
runId: run.runId,
|
|
2077
|
+
resume,
|
|
2078
|
+
projection,
|
|
2079
|
+
uuid: dependencies.uuid,
|
|
2080
|
+
now: dependencies.now
|
|
2081
|
+
});
|
|
2082
|
+
appendData(pi, owner, run, ctx, card);
|
|
2083
|
+
renderedData.set(card.state_id, card);
|
|
2084
|
+
ctx.ui.notify(plainResultCard(card), "info");
|
|
2085
|
+
const currentState = reconstructWorkflowState(ctx.sessionManager.getBranch());
|
|
2086
|
+
const actions = [
|
|
2087
|
+
"View detail",
|
|
2088
|
+
...currentState.vacancy === void 0 ? [] : ["Career match this resume"],
|
|
2089
|
+
"Close"
|
|
2090
|
+
];
|
|
2091
|
+
const action = await ctx.ui.select("Career analyze result", actions);
|
|
2092
|
+
if (action === "View detail") {
|
|
2093
|
+
await showDetail(ctx, analyzeDetailSections(result));
|
|
2094
|
+
} else if (action === "Career match this resume") {
|
|
2095
|
+
ctx.ui.setEditorText(`/career-match ${resume.id}`);
|
|
2096
|
+
ctx.ui.notify("Prepared a deterministic single-resume career match command.", "info");
|
|
2097
|
+
}
|
|
2098
|
+
})
|
|
2099
|
+
});
|
|
2100
|
+
pi.registerCommand("career-match", {
|
|
2101
|
+
description: "Deterministically rank original resumes against the current vacancy",
|
|
2102
|
+
handler: async (args, ctx) => handle(ctx, async () => {
|
|
2103
|
+
requireInteractive(ctx);
|
|
2104
|
+
const filter = parseFilter(args);
|
|
2105
|
+
const run = owner.start(ctx);
|
|
2106
|
+
const { scan } = await refreshState(ctx);
|
|
2107
|
+
const state = reconstructWorkflowState(ctx.sessionManager.getBranch());
|
|
2108
|
+
const vacancy = state.vacancy;
|
|
2109
|
+
if (vacancy === void 0) throw workflowError("vacancy_required");
|
|
2110
|
+
const candidates = filteredResumes(eligibleOriginals(scan), filter);
|
|
2111
|
+
if (candidates.length === 0) throw workflowError("library_empty");
|
|
2112
|
+
const scope = await ctx.ui.select("Career match", ["All original resumes", "Select subset", "Cancel"]);
|
|
2113
|
+
if (scope === void 0 || scope === "Cancel") return;
|
|
2114
|
+
let selected = candidates;
|
|
2115
|
+
if (scope === "Select subset") {
|
|
2116
|
+
const remaining = new Map(candidates.map((record) => [recordOption(record), record]));
|
|
2117
|
+
selected = [];
|
|
2118
|
+
while (remaining.size > 0) {
|
|
2119
|
+
const choice = await ctx.ui.select("Select resumes", ["Done", ...remaining.keys()]);
|
|
2120
|
+
if (choice === void 0 || choice === "Done") break;
|
|
2121
|
+
const record = remaining.get(choice);
|
|
2122
|
+
if (record !== void 0) {
|
|
2123
|
+
selected.push(record);
|
|
2124
|
+
remaining.delete(choice);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (selected.length === 0) return;
|
|
2128
|
+
}
|
|
2129
|
+
owner.assert(run, ctx);
|
|
2130
|
+
await ensureConsent(ctx, run);
|
|
2131
|
+
const queue = await runOperation(
|
|
2132
|
+
ctx,
|
|
2133
|
+
owner,
|
|
2134
|
+
run,
|
|
2135
|
+
"Running deterministic career match queue…",
|
|
2136
|
+
(signal) => executeMatchQueue(dependencies, selected, vacancy, signal)
|
|
2137
|
+
);
|
|
2138
|
+
owner.assert(run, ctx);
|
|
2139
|
+
const ranked = rankMatches(queue.matches);
|
|
2140
|
+
const cards = ranked.map((item2) => createResultCard({
|
|
2141
|
+
workflow: "match",
|
|
2142
|
+
runId: run.runId,
|
|
2143
|
+
resume: item2.resume,
|
|
2144
|
+
vacancy,
|
|
2145
|
+
projection: item2.projection,
|
|
2146
|
+
uuid: dependencies.uuid,
|
|
2147
|
+
now: dependencies.now
|
|
2148
|
+
}));
|
|
2149
|
+
for (const card of cards) appendData(pi, owner, run, ctx, card);
|
|
2150
|
+
for (const card of cards) renderedData.set(card.state_id, card);
|
|
2151
|
+
for (const stateId of deriveMatchTieStateIds(cards)) renderedTieStateIds.add(stateId);
|
|
2152
|
+
ctx.ui.notify(
|
|
2153
|
+
matchBatchSummary(cards, ranked, queue.unavailable, run.runId),
|
|
2154
|
+
ranked.length === 0 ? "error" : "info"
|
|
2155
|
+
);
|
|
2156
|
+
if (ranked.length === 0) return;
|
|
2157
|
+
const rows = rankedRows(ranked);
|
|
2158
|
+
const byRow = new Map(ranked.map((item2, index) => [rows[index], item2]));
|
|
2159
|
+
const chosen = await ctx.ui.select("Career match detail", [...byRow.keys(), "Close"]);
|
|
2160
|
+
const item = chosen === void 0 ? void 0 : byRow.get(chosen);
|
|
2161
|
+
if (item !== void 0) await showDetail(ctx, matchDetailSections(item.result, vacancy.vacancy_text));
|
|
2162
|
+
})
|
|
2163
|
+
});
|
|
2164
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
2165
|
+
owner.invalidate();
|
|
2166
|
+
transientNoticeSession = void 0;
|
|
2167
|
+
try {
|
|
2168
|
+
const { config, scan } = await refreshState(ctx);
|
|
2169
|
+
if (ctx.hasUI && (config.library_roots.length === 0 || scan.records.length === 0)) {
|
|
2170
|
+
ctx.ui.setWidget("pi-career-setup", [BANNER]);
|
|
2171
|
+
} else if (ctx.hasUI) {
|
|
2172
|
+
ctx.ui.setWidget("pi-career-setup", void 0);
|
|
2173
|
+
}
|
|
2174
|
+
} catch {
|
|
2175
|
+
if (ctx.hasUI) ctx.ui.setWidget("pi-career-setup", [BANNER]);
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
2179
|
+
owner.invalidate();
|
|
2180
|
+
try {
|
|
2181
|
+
await refreshState(ctx);
|
|
2182
|
+
} catch {
|
|
2183
|
+
renderedData.clear();
|
|
2184
|
+
renderedTieStateIds.clear();
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2187
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
2188
|
+
owner.invalidate();
|
|
2189
|
+
renderedData.clear();
|
|
2190
|
+
renderedTieStateIds.clear();
|
|
2191
|
+
transientNoticeSession = void 0;
|
|
2192
|
+
if (ctx.hasUI) ctx.ui.setWidget("pi-career-setup", void 0);
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// src/index.ts
|
|
2197
|
+
var DISCOVERY_OPERATIONS = ["capabilities", "schema-list", "schema-export"];
|
|
2198
|
+
var RESUME_OPERATIONS = [
|
|
2199
|
+
"evaluate",
|
|
2200
|
+
"analyze",
|
|
2201
|
+
"analysis-suggestions-review",
|
|
2202
|
+
"analysis-replacements-review",
|
|
2203
|
+
"normalize",
|
|
2204
|
+
"enrich",
|
|
2205
|
+
"variant-review",
|
|
2206
|
+
"variant-materialize"
|
|
2207
|
+
];
|
|
2208
|
+
var JOB_OPERATIONS = ["normalize", "match"];
|
|
2209
|
+
var discoveryParameters = Type.Object(
|
|
2210
|
+
{
|
|
2211
|
+
operation: StringEnum(DISCOVERY_OPERATIONS, {
|
|
2212
|
+
description: "Discover capabilities, list embedded schemas, or export one embedded schema."
|
|
2213
|
+
}),
|
|
2214
|
+
schema_id: Type.Optional(
|
|
2215
|
+
Type.String({
|
|
2216
|
+
description: "Required only for schema-export; use an exact ID returned by schema-list.",
|
|
2217
|
+
minLength: 1,
|
|
2218
|
+
maxLength: 100,
|
|
2219
|
+
pattern: "^career\\.[a-z0-9_.-]+\\.v[0-9]+$"
|
|
2220
|
+
})
|
|
2221
|
+
)
|
|
2222
|
+
},
|
|
2223
|
+
{ additionalProperties: false }
|
|
2224
|
+
);
|
|
2225
|
+
var resumeParameters = Type.Object(
|
|
2226
|
+
{
|
|
2227
|
+
operation: StringEnum(RESUME_OPERATIONS, {
|
|
2228
|
+
description: "One available deterministic resume CLI operation."
|
|
2229
|
+
}),
|
|
2230
|
+
input_json: Type.String({
|
|
2231
|
+
description: "Exactly one versioned JSON input object as a string. Discover the operation schema first. Private arguments may be stored by Pi unless the session is transient.",
|
|
2232
|
+
minLength: 2,
|
|
2233
|
+
maxLength: COMPOSITE_INPUT_MAX_BYTES
|
|
2234
|
+
})
|
|
2235
|
+
},
|
|
2236
|
+
{ additionalProperties: false }
|
|
2237
|
+
);
|
|
2238
|
+
var jobParameters = Type.Object(
|
|
2239
|
+
{
|
|
2240
|
+
operation: StringEnum(JOB_OPERATIONS, {
|
|
2241
|
+
description: "Normalize a caller-supplied job description or match original resume/job inputs."
|
|
2242
|
+
}),
|
|
2243
|
+
input_json: Type.String({
|
|
2244
|
+
description: "Exactly one versioned JSON input object as a string. Discover the operation schema first. Private arguments may be stored by Pi unless the session is transient.",
|
|
2245
|
+
minLength: 2,
|
|
2246
|
+
maxLength: COMPOSITE_INPUT_MAX_BYTES
|
|
2247
|
+
})
|
|
2248
|
+
},
|
|
2249
|
+
{ additionalProperties: false }
|
|
2250
|
+
);
|
|
2251
|
+
var privacyGuideline = "Before using career_core_resume or career_core_job with private career content, require an explicit user decision about Pi session persistence and recommend starting a new `pi --no-session` transient run; do not claim secure erasure.";
|
|
2252
|
+
function resultContent(json, operation) {
|
|
2253
|
+
return {
|
|
2254
|
+
content: [{ type: "text", text: json }],
|
|
2255
|
+
details: {
|
|
2256
|
+
schema_version: "career.pi_tool_details.v1",
|
|
2257
|
+
operation
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
function careerCoreExtension(pi) {
|
|
2262
|
+
registerCareerCommands(pi);
|
|
2263
|
+
pi.registerTool({
|
|
2264
|
+
name: "career_core_discover",
|
|
2265
|
+
label: "Career Core Discovery",
|
|
2266
|
+
description: "Discover the package-owned deterministic Career Core runtime capabilities and embedded JSON schemas. Makes no network or model request.",
|
|
2267
|
+
promptSnippet: "Discover bundled Career Core capabilities and exact embedded schemas before document operations",
|
|
2268
|
+
promptGuidelines: [
|
|
2269
|
+
"Use career_core_discover before career_core_resume or career_core_job; invoke only capabilities reported as available and do not infer contract shapes."
|
|
2270
|
+
],
|
|
2271
|
+
parameters: discoveryParameters,
|
|
2272
|
+
async execute(_toolCallId, params, signal) {
|
|
2273
|
+
try {
|
|
2274
|
+
const result = await invokeCareerCli(
|
|
2275
|
+
{
|
|
2276
|
+
kind: "discovery",
|
|
2277
|
+
operation: params.operation,
|
|
2278
|
+
...params.schema_id === void 0 ? {} : { schemaId: params.schema_id }
|
|
2279
|
+
},
|
|
2280
|
+
signal
|
|
2281
|
+
);
|
|
2282
|
+
return resultContent(result.json, result.operation);
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
throw publicAdapterError(error);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
2288
|
+
pi.registerTool({
|
|
2289
|
+
name: "career_core_resume",
|
|
2290
|
+
label: "Career Core Resume",
|
|
2291
|
+
description: "Run one bounded package-owned Career Core resume operation with JSON over stdin. Returns the complete authoritative JSON or fails without truncation. Never invokes Cargo, a provider, or the network.",
|
|
2292
|
+
promptSnippet: "Evaluate, analyze, normalize, or review bounded resume inputs through the bundled deterministic runtime",
|
|
2293
|
+
promptGuidelines: [
|
|
2294
|
+
privacyGuideline,
|
|
2295
|
+
"Use career_core_resume only with an exact schema discovered through career_core_discover; preserve every warning, evidence item, uncertainty status, baseline boundary, and assisted/non-authoritative label returned by the tool.",
|
|
2296
|
+
"If career_core_resume reports result_too_large or result_too_many_lines, do not request or use partial output; full-result export is not yet available through pi-career."
|
|
2297
|
+
],
|
|
2298
|
+
parameters: resumeParameters,
|
|
2299
|
+
async execute(_toolCallId, params, signal) {
|
|
2300
|
+
try {
|
|
2301
|
+
const result = await invokeCareerCli(
|
|
2302
|
+
{
|
|
2303
|
+
kind: "resume",
|
|
2304
|
+
operation: params.operation,
|
|
2305
|
+
inputJson: params.input_json
|
|
2306
|
+
},
|
|
2307
|
+
signal
|
|
2308
|
+
);
|
|
2309
|
+
return resultContent(result.json, result.operation);
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
throw publicAdapterError(error);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
pi.registerTool({
|
|
2316
|
+
name: "career_core_job",
|
|
2317
|
+
label: "Career Core Job",
|
|
2318
|
+
description: "Run package-owned Career Core job normalization or conservative matching with JSON over stdin. Returns complete authoritative JSON or fails without truncation. Never fetches URLs, invokes Cargo, a provider, or the network.",
|
|
2319
|
+
promptSnippet: "Normalize job text or conservatively match original resume and job inputs through the bundled deterministic runtime",
|
|
2320
|
+
promptGuidelines: [
|
|
2321
|
+
privacyGuideline,
|
|
2322
|
+
"Use career_core_job only with an exact schema discovered through career_core_discover; preserve source spans, confidence, warnings, uncertainty, conservative equivalence, and recommendation limitations exactly.",
|
|
2323
|
+
"If career_core_job reports result_too_large or result_too_many_lines, do not request or use partial output; full-result export is not yet available through pi-career."
|
|
2324
|
+
],
|
|
2325
|
+
parameters: jobParameters,
|
|
2326
|
+
async execute(_toolCallId, params, signal) {
|
|
2327
|
+
try {
|
|
2328
|
+
const result = await invokeCareerCli(
|
|
2329
|
+
{
|
|
2330
|
+
kind: "job",
|
|
2331
|
+
operation: params.operation,
|
|
2332
|
+
inputJson: params.input_json
|
|
2333
|
+
},
|
|
2334
|
+
signal
|
|
2335
|
+
);
|
|
2336
|
+
return resultContent(result.json, result.operation);
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
throw publicAdapterError(error);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
export {
|
|
2344
|
+
careerCoreExtension as default
|
|
2345
|
+
};
|