pi-career 0.2.0 → 0.3.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 +28 -2
- package/NOTICE +1 -1
- package/README.md +72 -40
- package/SECURITY.md +7 -7
- package/dist/index.js +1406 -296
- package/docs/application-workspaces.md +6 -5
- package/docs/career-run-roadmap.md +37 -27
- package/docs/design.md +24 -19
- package/docs/native-dependency-inventory.md +1 -1
- package/docs/product-flow.md +11 -6
- package/docs/releasing.md +55 -32
- package/docs/variant-save-workflow.md +303 -0
- package/package.json +6 -1
- package/skills/career-core/SKILL.md +52 -133
- package/skills/career-core/references/cli-contract.md +6 -6
package/dist/index.js
CHANGED
|
@@ -12,11 +12,12 @@ import { TextDecoder as TextDecoder2 } from "node:util";
|
|
|
12
12
|
|
|
13
13
|
// src/errors.ts
|
|
14
14
|
var ERROR_MESSAGES = {
|
|
15
|
+
unsupported_platform: "pi-career supports only macOS and Linux.",
|
|
15
16
|
invalid_request: "The Career Core tool request is invalid.",
|
|
16
17
|
invalid_executable_override: "CAREER_CLI_PATH must be a bounded absolute executable path.",
|
|
17
18
|
managed_contract_invalid: "The selected Career Core runtime has incompatible managed contracts.",
|
|
18
|
-
runtime_unavailable: "No compatible Career Core runtime is available. Install @revazi/career@0.
|
|
19
|
-
runtime_acquisition_failed: "Automatic acquisition of @revazi/career@0.
|
|
19
|
+
runtime_unavailable: "No compatible Career Core runtime is available. Install @revazi/career@0.2.0 or configure a compatible local career executable.",
|
|
20
|
+
runtime_acquisition_failed: "Automatic acquisition of @revazi/career@0.2.0 failed. Install that exact package or configure a compatible local career executable.",
|
|
20
21
|
missing_executable: "The selected Career Core runtime was not found.",
|
|
21
22
|
executable_unavailable: "The selected Career Core runtime is not available for execution.",
|
|
22
23
|
cancelled: "The Career Core operation was cancelled.",
|
|
@@ -63,7 +64,7 @@ var MANAGED_INVOKE_OPTIONS = {
|
|
|
63
64
|
toolResultMaxBytes: MANAGED_OUTPUT_MAX_BYTES,
|
|
64
65
|
toolResultMaxLines: MANAGED_RESULT_MAX_LINES
|
|
65
66
|
};
|
|
66
|
-
var EXPECTED_CORE_VERSION = "0.
|
|
67
|
+
var EXPECTED_CORE_VERSION = "0.2.0";
|
|
67
68
|
var EXPECTED_OPERATIONS = {
|
|
68
69
|
"core.capabilities": {
|
|
69
70
|
capability: "core.capabilities",
|
|
@@ -292,21 +293,20 @@ import { createRequire } from "node:module";
|
|
|
292
293
|
import path from "node:path";
|
|
293
294
|
import { TextDecoder } from "node:util";
|
|
294
295
|
var CAREER_PACKAGE_NAME = "@revazi/career";
|
|
295
|
-
var CAREER_PACKAGE_VERSION = "0.
|
|
296
|
+
var CAREER_PACKAGE_VERSION = "0.2.0";
|
|
296
297
|
var CAREER_LAUNCHER_SCHEMA = "career.npm_launcher.v2";
|
|
297
298
|
var CAREER_TARGET_CATALOG = "targets.json";
|
|
299
|
+
var CAREER_LAUNCHER_ENTRY = "bin/career.js";
|
|
298
300
|
var CAREER_PLATFORM_PACKAGES = Object.freeze([
|
|
299
301
|
"@revazi/career-darwin-arm64",
|
|
300
302
|
"@revazi/career-darwin-x64",
|
|
301
303
|
"@revazi/career-linux-x64-gnu",
|
|
302
304
|
"@revazi/career-linux-arm64-gnu",
|
|
303
305
|
"@revazi/career-linux-x64-musl",
|
|
304
|
-
"@revazi/career-linux-arm64-musl"
|
|
305
|
-
"@revazi/career-win32-x64-msvc",
|
|
306
|
-
"@revazi/career-win32-arm64-msvc"
|
|
306
|
+
"@revazi/career-linux-arm64-musl"
|
|
307
307
|
]);
|
|
308
308
|
var CAREER_LAUNCHER_FILES = Object.freeze([
|
|
309
|
-
|
|
309
|
+
CAREER_LAUNCHER_ENTRY,
|
|
310
310
|
CAREER_TARGET_CATALOG,
|
|
311
311
|
"README.md",
|
|
312
312
|
"LICENSE-MIT",
|
|
@@ -324,6 +324,7 @@ var ACQUISITION_TIMEOUT_MS = 12e4;
|
|
|
324
324
|
var TERMINATION_GRACE_MS = 250;
|
|
325
325
|
var CANONICAL_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
326
326
|
var SOURCE_ORDER = ["path", "package-local", "acquired"];
|
|
327
|
+
var SUPPORTED_PLATFORMS = /* @__PURE__ */ new Set(["darwin", "linux"]);
|
|
327
328
|
var requireFromPackage = createRequire(import.meta.url);
|
|
328
329
|
var cachedResolution;
|
|
329
330
|
var pendingResolution;
|
|
@@ -336,7 +337,11 @@ function runtimeEnvironmentKey(environment) {
|
|
|
336
337
|
environmentValue(environment, "PATH") ?? null
|
|
337
338
|
]);
|
|
338
339
|
}
|
|
340
|
+
function assertSupportedPlatform() {
|
|
341
|
+
if (!SUPPORTED_PLATFORMS.has(process.platform)) throw adapterError("unsupported_platform");
|
|
342
|
+
}
|
|
339
343
|
function resolveCareerExecutable(environment = process.env) {
|
|
344
|
+
assertSupportedPlatform();
|
|
340
345
|
const override = environmentValue(environment, "CAREER_CLI_PATH");
|
|
341
346
|
if (override === void 0) return void 0;
|
|
342
347
|
if (override.length === 0 || override.includes("\0") || Buffer.byteLength(override, "utf8") > EXECUTABLE_MAX_BYTES || !path.isAbsolute(override)) {
|
|
@@ -359,20 +364,15 @@ function pathEntries(environmentPath, cwd) {
|
|
|
359
364
|
if (environmentPath === void 0 || environmentPath.includes("\0") || Buffer.byteLength(environmentPath, "utf8") > PATH_MAX_BYTES) return [];
|
|
360
365
|
return environmentPath.split(path.delimiter).map((entry) => path.resolve(entry || cwd));
|
|
361
366
|
}
|
|
362
|
-
function executableNames() {
|
|
363
|
-
return process.platform === "win32" ? ["career.exe", "career.com", "career"] : ["career"];
|
|
364
|
-
}
|
|
365
367
|
async function pathRoute(environment, cwd) {
|
|
366
368
|
for (const directory of pathEntries(environmentValue(environment, "PATH"), cwd)) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
} catch {
|
|
375
|
-
}
|
|
369
|
+
const candidate = path.join(directory, "career");
|
|
370
|
+
try {
|
|
371
|
+
const metadata = await lstat(candidate);
|
|
372
|
+
if (!metadata.isFile() && !metadata.isSymbolicLink()) continue;
|
|
373
|
+
await access(candidate, fsConstants.X_OK);
|
|
374
|
+
return route(candidate, [], "path");
|
|
375
|
+
} catch {
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
378
|
return void 0;
|
|
@@ -381,7 +381,7 @@ function isRecord2(value) {
|
|
|
381
381
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
382
382
|
}
|
|
383
383
|
function boundedRegularFile(metadata, maximumBytes, executable) {
|
|
384
|
-
return !metadata.isSymbolicLink() && metadata.isFile() && metadata.size >= 2 && metadata.size <= maximumBytes && (!executable ||
|
|
384
|
+
return !metadata.isSymbolicLink() && metadata.isFile() && metadata.size >= 2 && metadata.size <= maximumBytes && (!executable || (metadata.mode & 73) !== 0);
|
|
385
385
|
}
|
|
386
386
|
function exactKeys2(value, expected) {
|
|
387
387
|
return Object.keys(value).sort().join("\0") === [...expected].sort().join("\0");
|
|
@@ -392,7 +392,7 @@ function exactStringArray(value, expected) {
|
|
|
392
392
|
function launcherManifestIsValid(manifest) {
|
|
393
393
|
if (!isRecord2(manifest) || !isRecord2(manifest.bin) || !isRecord2(manifest.optionalDependencies) || !isRecord2(manifest.career_launcher)) return false;
|
|
394
394
|
const optionalDependencies = manifest.optionalDependencies;
|
|
395
|
-
return manifest.name === CAREER_PACKAGE_NAME && manifest.version === CAREER_PACKAGE_VERSION && exactKeys2(manifest.bin, ["career"]) && manifest.bin.career ===
|
|
395
|
+
return manifest.name === CAREER_PACKAGE_NAME && manifest.version === CAREER_PACKAGE_VERSION && exactKeys2(manifest.bin, ["career"]) && manifest.bin.career === CAREER_LAUNCHER_ENTRY && exactStringArray(manifest.files, CAREER_LAUNCHER_FILES) && exactKeys2(optionalDependencies, CAREER_PLATFORM_PACKAGES) && CAREER_PLATFORM_PACKAGES.every(
|
|
396
396
|
(packageName) => optionalDependencies[packageName] === CAREER_PACKAGE_VERSION
|
|
397
397
|
) && exactKeys2(manifest.career_launcher, [
|
|
398
398
|
"schema_version",
|
|
@@ -410,12 +410,12 @@ async function readLauncherRoute(manifestPath, source, execPath) {
|
|
|
410
410
|
]);
|
|
411
411
|
if (!boundedRegularFile(manifestMetadata, PACKAGE_MANIFEST_MAX_BYTES, false)) return void 0;
|
|
412
412
|
const nodeMetadata = await lstat(realNodePath);
|
|
413
|
-
if (!nodeMetadata.isFile() ||
|
|
413
|
+
if (!nodeMetadata.isFile() || (nodeMetadata.mode & 73) === 0) return void 0;
|
|
414
414
|
const bytes = await readFile(manifestPath);
|
|
415
415
|
if (bytes.length !== manifestMetadata.size) return void 0;
|
|
416
416
|
const manifest = JSON.parse(bytes.toString("utf8"));
|
|
417
417
|
if (!launcherManifestIsValid(manifest)) return void 0;
|
|
418
|
-
const launcherPath = path.join(path.dirname(manifestPath),
|
|
418
|
+
const launcherPath = path.join(path.dirname(manifestPath), CAREER_LAUNCHER_ENTRY);
|
|
419
419
|
const launcherMetadata = await lstat(launcherPath);
|
|
420
420
|
if (!boundedRegularFile(launcherMetadata, LAUNCHER_MAX_BYTES, true)) return void 0;
|
|
421
421
|
return route(realNodePath, [launcherPath], source);
|
|
@@ -442,48 +442,15 @@ async function packageLocalRoute(dependencies) {
|
|
|
442
442
|
function npmExecPathValues(environment) {
|
|
443
443
|
return Object.entries(environment).filter(([key]) => key.toLowerCase() === "npm_execpath").map(([, value]) => value);
|
|
444
444
|
}
|
|
445
|
-
function samePath(left, right) {
|
|
446
|
-
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
447
|
-
}
|
|
448
|
-
function environmentValues(environment, key) {
|
|
449
|
-
return Object.entries(environment).filter(([name]) => name.toLowerCase() === key.toLowerCase()).map(([, value]) => value);
|
|
450
|
-
}
|
|
451
|
-
async function trustedWindowsShell(environment) {
|
|
452
|
-
const roots = environmentValues(environment, "SystemRoot");
|
|
453
|
-
const suppliedShells = environmentValues(environment, "ComSpec");
|
|
454
|
-
if (roots.length !== 1 || suppliedShells.length > 1) throw new Error("invalid Windows tools");
|
|
455
|
-
const root = roots[0];
|
|
456
|
-
if (typeof root !== "string" || !path.isAbsolute(root) || root.includes("\0")) {
|
|
457
|
-
throw new Error("invalid Windows root");
|
|
458
|
-
}
|
|
459
|
-
const rootMetadata = await lstat(root);
|
|
460
|
-
if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) {
|
|
461
|
-
throw new Error("invalid Windows root");
|
|
462
|
-
}
|
|
463
|
-
const realSystemRoot = await realpath(root);
|
|
464
|
-
const derivedShell = path.join(realSystemRoot, "System32", "cmd.exe");
|
|
465
|
-
const realShell = await realpath(derivedShell);
|
|
466
|
-
if (!samePath(realShell, derivedShell)) throw new Error("invalid Windows shell path");
|
|
467
|
-
const shellMetadata = await lstat(realShell);
|
|
468
|
-
if (shellMetadata.isSymbolicLink() || !shellMetadata.isFile()) {
|
|
469
|
-
throw new Error("invalid Windows shell");
|
|
470
|
-
}
|
|
471
|
-
if (suppliedShells.length === 1) {
|
|
472
|
-
const suppliedShell = suppliedShells[0];
|
|
473
|
-
if (typeof suppliedShell !== "string" || !path.isAbsolute(suppliedShell)) {
|
|
474
|
-
throw new Error("invalid ComSpec");
|
|
475
|
-
}
|
|
476
|
-
if (!samePath(await realpath(suppliedShell), realShell)) throw new Error("conflicting ComSpec");
|
|
477
|
-
}
|
|
478
|
-
return { scriptShell: realShell, systemRoot: realSystemRoot };
|
|
479
|
-
}
|
|
480
445
|
async function trustedNpm(execPath, environment) {
|
|
481
446
|
try {
|
|
482
447
|
if (!path.isAbsolute(execPath)) throw new Error("invalid node path");
|
|
483
448
|
const realNodePath = await realpath(execPath);
|
|
484
449
|
const nodeMetadata = await lstat(realNodePath);
|
|
485
|
-
if (!nodeMetadata.isFile() ||
|
|
486
|
-
|
|
450
|
+
if (!nodeMetadata.isFile() || (nodeMetadata.mode & 73) === 0) {
|
|
451
|
+
throw new Error("invalid node executable");
|
|
452
|
+
}
|
|
453
|
+
const derivedNpmCliPath = path.join(
|
|
487
454
|
path.dirname(path.dirname(realNodePath)),
|
|
488
455
|
"lib",
|
|
489
456
|
"node_modules",
|
|
@@ -492,9 +459,11 @@ async function trustedNpm(execPath, environment) {
|
|
|
492
459
|
"npm-cli.js"
|
|
493
460
|
);
|
|
494
461
|
const realNpmCliPath = await realpath(derivedNpmCliPath);
|
|
495
|
-
if (
|
|
462
|
+
if (realNpmCliPath !== derivedNpmCliPath) throw new Error("invalid npm path");
|
|
496
463
|
const npmMetadata = await lstat(realNpmCliPath);
|
|
497
|
-
if (!npmMetadata.isFile() ||
|
|
464
|
+
if (!npmMetadata.isFile() || (npmMetadata.mode & 73) === 0) {
|
|
465
|
+
throw new Error("invalid npm executable");
|
|
466
|
+
}
|
|
498
467
|
const supplied = npmExecPathValues(environment);
|
|
499
468
|
if (supplied.length > 1) throw new Error("ambiguous npm_execpath");
|
|
500
469
|
if (supplied.length === 1) {
|
|
@@ -502,19 +471,10 @@ async function trustedNpm(execPath, environment) {
|
|
|
502
471
|
if (typeof suppliedPath !== "string" || !path.isAbsolute(suppliedPath)) {
|
|
503
472
|
throw new Error("invalid npm_execpath");
|
|
504
473
|
}
|
|
505
|
-
if (
|
|
474
|
+
if (await realpath(suppliedPath) !== realNpmCliPath) {
|
|
506
475
|
throw new Error("conflicting npm_execpath");
|
|
507
476
|
}
|
|
508
477
|
}
|
|
509
|
-
if (process.platform === "win32") {
|
|
510
|
-
const windows = await trustedWindowsShell(environment);
|
|
511
|
-
return {
|
|
512
|
-
nodePath: realNodePath,
|
|
513
|
-
npmCliPath: realNpmCliPath,
|
|
514
|
-
scriptShell: windows.scriptShell,
|
|
515
|
-
windowsSystemRoot: windows.systemRoot
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
478
|
return { nodePath: realNodePath, npmCliPath: realNpmCliPath, scriptShell: "/bin/sh" };
|
|
519
479
|
} catch {
|
|
520
480
|
throw adapterError("runtime_acquisition_failed");
|
|
@@ -532,24 +492,9 @@ var FORBIDDEN_NPM_ENVIRONMENT = /* @__PURE__ */ new Set([
|
|
|
532
492
|
"npm_config_script_shell"
|
|
533
493
|
]);
|
|
534
494
|
function sanitizedNpmEnvironment(environment, trusted) {
|
|
535
|
-
const windowsControlled = /* @__PURE__ */ new Set(["comspec", "pathext", "systemroot", "windir"]);
|
|
536
495
|
const sanitized = Object.fromEntries(Object.entries(environment).filter(
|
|
537
|
-
([key, value]) => typeof value === "string" && key.toLowerCase() !== "path" && !
|
|
496
|
+
([key, value]) => typeof value === "string" && key.toLowerCase() !== "path" && !FORBIDDEN_NPM_ENVIRONMENT.has(key.toLowerCase().replaceAll("-", "_"))
|
|
538
497
|
));
|
|
539
|
-
if (process.platform === "win32" && trusted.windowsSystemRoot !== void 0) {
|
|
540
|
-
return {
|
|
541
|
-
...sanitized,
|
|
542
|
-
PATH: [
|
|
543
|
-
path.dirname(trusted.nodePath),
|
|
544
|
-
path.dirname(trusted.scriptShell),
|
|
545
|
-
trusted.windowsSystemRoot
|
|
546
|
-
].join(path.delimiter),
|
|
547
|
-
ComSpec: trusted.scriptShell,
|
|
548
|
-
SystemRoot: trusted.windowsSystemRoot,
|
|
549
|
-
WINDIR: trusted.windowsSystemRoot,
|
|
550
|
-
PATHEXT: ".COM;.EXE;.BAT;.CMD"
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
498
|
return {
|
|
554
499
|
...sanitized,
|
|
555
500
|
PATH: [path.dirname(trusted.nodePath), "/usr/bin", "/bin"].join(path.delimiter)
|
|
@@ -576,12 +521,7 @@ function decodeAcquiredLauncherOutput(stdout) {
|
|
|
576
521
|
function terminateAcquisitionProcessTree(child, signal) {
|
|
577
522
|
try {
|
|
578
523
|
if (child.pid === void 0) throw new Error("missing process id");
|
|
579
|
-
|
|
580
|
-
process.kill(child.pid, "SIGBREAK");
|
|
581
|
-
if (signal === "SIGKILL") child.kill("SIGKILL");
|
|
582
|
-
} else {
|
|
583
|
-
process.kill(-child.pid, signal);
|
|
584
|
-
}
|
|
524
|
+
process.kill(-child.pid, signal);
|
|
585
525
|
} catch {
|
|
586
526
|
try {
|
|
587
527
|
child.kill(signal);
|
|
@@ -591,7 +531,6 @@ function terminateAcquisitionProcessTree(child, signal) {
|
|
|
591
531
|
}
|
|
592
532
|
async function runAcquisition(trusted, environment, cwd, signal) {
|
|
593
533
|
const { nodePath, npmCliPath, scriptShell } = trusted;
|
|
594
|
-
const locatorNode = process.platform === "win32" ? path.basename(nodePath) : nodePath;
|
|
595
534
|
if (signal?.aborted) throw adapterError("cancelled");
|
|
596
535
|
const arguments_ = [
|
|
597
536
|
npmCliPath,
|
|
@@ -604,7 +543,7 @@ async function runAcquisition(trusted, environment, cwd, signal) {
|
|
|
604
543
|
`--registry=${CANONICAL_NPM_REGISTRY}`,
|
|
605
544
|
`--package=${CAREER_PACKAGE_SPEC}`,
|
|
606
545
|
"--",
|
|
607
|
-
|
|
546
|
+
nodePath,
|
|
608
547
|
"-p",
|
|
609
548
|
LOCATE_ACQUIRED_PATH_EXPRESSION
|
|
610
549
|
];
|
|
@@ -616,8 +555,7 @@ async function runAcquisition(trusted, environment, cwd, signal) {
|
|
|
616
555
|
detached: true,
|
|
617
556
|
env: sanitizedNpmEnvironment(environment, trusted),
|
|
618
557
|
shell: false,
|
|
619
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
620
|
-
windowsHide: true
|
|
558
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
621
559
|
});
|
|
622
560
|
} catch {
|
|
623
561
|
resolve({ code: null, signal: null, spawnError: true, stdout: [], stderrBytes: 0 });
|
|
@@ -753,6 +691,7 @@ async function resolveUncached(options) {
|
|
|
753
691
|
throw adapterError("runtime_unavailable");
|
|
754
692
|
}
|
|
755
693
|
async function resolveCareerRuntime(options) {
|
|
694
|
+
assertSupportedPlatform();
|
|
756
695
|
const environment = options.environment ?? process.env;
|
|
757
696
|
const environmentKey = runtimeEnvironmentKey(environment);
|
|
758
697
|
const useCache = options.afterSource === void 0 && options.dependencies === void 0;
|
|
@@ -1013,8 +952,7 @@ function executePrepared(prepared, runtime, signal, limits) {
|
|
|
1013
952
|
try {
|
|
1014
953
|
child = spawn2(runtime.command, [...runtime.argumentPrefix, ...prepared.args], {
|
|
1015
954
|
shell: false,
|
|
1016
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1017
|
-
windowsHide: true
|
|
955
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1018
956
|
});
|
|
1019
957
|
} catch {
|
|
1020
958
|
reject(new ProcessAttemptError(adapterError("executable_unavailable"), false));
|
|
@@ -1139,6 +1077,7 @@ async function executeResolved(prepared, runtime, signal, limits) {
|
|
|
1139
1077
|
}
|
|
1140
1078
|
}
|
|
1141
1079
|
async function invokeCareerCli(invocation, signal, options = {}) {
|
|
1080
|
+
assertSupportedPlatform();
|
|
1142
1081
|
const prepared = prepareInvocation(invocation);
|
|
1143
1082
|
if (signal?.aborted) throw adapterError("cancelled");
|
|
1144
1083
|
const limits = executionLimits(options);
|
|
@@ -1163,7 +1102,7 @@ async function invokeCareerCli(invocation, signal, options = {}) {
|
|
|
1163
1102
|
// src/managed/tool.ts
|
|
1164
1103
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1165
1104
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
1166
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
1105
|
+
import { Text as Text2 } from "@earendil-works/pi-tui";
|
|
1167
1106
|
|
|
1168
1107
|
// src/workflow/config.ts
|
|
1169
1108
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -1250,7 +1189,7 @@ function validLabel(value) {
|
|
|
1250
1189
|
}
|
|
1251
1190
|
function normalizeGeneratedVariantsRoot(value) {
|
|
1252
1191
|
if (typeof value !== "string" || value.length === 0 || Buffer.byteLength(value, "utf8") > PATH_MAX_BYTES2 || !path2.isAbsolute(value) || /[\u0000-\u001f\u007f]/.test(value)) return void 0;
|
|
1253
|
-
const normalized = path2.
|
|
1192
|
+
const normalized = path2.resolve(value);
|
|
1254
1193
|
return path2.dirname(normalized) === normalized ? void 0 : normalized;
|
|
1255
1194
|
}
|
|
1256
1195
|
function parseRoot(value) {
|
|
@@ -1415,7 +1354,7 @@ function serializeCoreInput(value) {
|
|
|
1415
1354
|
}
|
|
1416
1355
|
|
|
1417
1356
|
// src/workflow/scan.ts
|
|
1418
|
-
import { createHash as
|
|
1357
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1419
1358
|
import { lstat as lstat3, opendir, readFile as readFile3, realpath as realpath3 } from "node:fs/promises";
|
|
1420
1359
|
import path3 from "node:path";
|
|
1421
1360
|
import { TextDecoder as TextDecoder4 } from "node:util";
|
|
@@ -1489,20 +1428,245 @@ function isWithinCoreCharacterLimit(value) {
|
|
|
1489
1428
|
return true;
|
|
1490
1429
|
}
|
|
1491
1430
|
|
|
1431
|
+
// src/workflow/variant-metadata.ts
|
|
1432
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1433
|
+
|
|
1434
|
+
// src/workflow/strict-json.ts
|
|
1435
|
+
var MAX_DEPTH = 32;
|
|
1436
|
+
var NUMBER_PATTERN = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/;
|
|
1437
|
+
function invalidJson() {
|
|
1438
|
+
throw new SyntaxError("invalid strict JSON");
|
|
1439
|
+
}
|
|
1440
|
+
function parseStrictJson(text) {
|
|
1441
|
+
let offset = 0;
|
|
1442
|
+
function skipWhitespace() {
|
|
1443
|
+
while (offset < text.length && /[\u0009\u000a\u000d\u0020]/.test(text[offset])) offset += 1;
|
|
1444
|
+
}
|
|
1445
|
+
function skipEscape() {
|
|
1446
|
+
offset += 1;
|
|
1447
|
+
const escaped = text[offset];
|
|
1448
|
+
if (escaped === "u") {
|
|
1449
|
+
if (!/^[0-9a-fA-F]{4}$/.test(text.slice(offset + 1, offset + 5))) invalidJson();
|
|
1450
|
+
offset += 5;
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (escaped === void 0 || !['"', "\\", "/", "b", "f", "n", "r", "t"].includes(escaped)) {
|
|
1454
|
+
invalidJson();
|
|
1455
|
+
}
|
|
1456
|
+
offset += 1;
|
|
1457
|
+
}
|
|
1458
|
+
function parseString() {
|
|
1459
|
+
if (text[offset] !== '"') invalidJson();
|
|
1460
|
+
const start = offset;
|
|
1461
|
+
offset += 1;
|
|
1462
|
+
while (offset < text.length) {
|
|
1463
|
+
const character = text[offset];
|
|
1464
|
+
if (character === '"') {
|
|
1465
|
+
offset += 1;
|
|
1466
|
+
try {
|
|
1467
|
+
return JSON.parse(text.slice(start, offset));
|
|
1468
|
+
} catch {
|
|
1469
|
+
invalidJson();
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (text.charCodeAt(offset) <= 31) invalidJson();
|
|
1473
|
+
if (character === "\\") skipEscape();
|
|
1474
|
+
else offset += 1;
|
|
1475
|
+
}
|
|
1476
|
+
invalidJson();
|
|
1477
|
+
}
|
|
1478
|
+
function parseObject2(depth) {
|
|
1479
|
+
offset += 1;
|
|
1480
|
+
skipWhitespace();
|
|
1481
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1482
|
+
if (text[offset] === "}") {
|
|
1483
|
+
offset += 1;
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
while (offset < text.length) {
|
|
1487
|
+
const key = parseString();
|
|
1488
|
+
if (keys.has(key)) invalidJson();
|
|
1489
|
+
keys.add(key);
|
|
1490
|
+
skipWhitespace();
|
|
1491
|
+
if (text[offset] !== ":") invalidJson();
|
|
1492
|
+
offset += 1;
|
|
1493
|
+
parseValue(depth + 1);
|
|
1494
|
+
skipWhitespace();
|
|
1495
|
+
if (text[offset] === "}") {
|
|
1496
|
+
offset += 1;
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (text[offset] !== ",") invalidJson();
|
|
1500
|
+
offset += 1;
|
|
1501
|
+
skipWhitespace();
|
|
1502
|
+
}
|
|
1503
|
+
invalidJson();
|
|
1504
|
+
}
|
|
1505
|
+
function parseArray(depth) {
|
|
1506
|
+
offset += 1;
|
|
1507
|
+
skipWhitespace();
|
|
1508
|
+
if (text[offset] === "]") {
|
|
1509
|
+
offset += 1;
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
while (offset < text.length) {
|
|
1513
|
+
parseValue(depth + 1);
|
|
1514
|
+
skipWhitespace();
|
|
1515
|
+
if (text[offset] === "]") {
|
|
1516
|
+
offset += 1;
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if (text[offset] !== ",") invalidJson();
|
|
1520
|
+
offset += 1;
|
|
1521
|
+
skipWhitespace();
|
|
1522
|
+
}
|
|
1523
|
+
invalidJson();
|
|
1524
|
+
}
|
|
1525
|
+
function parseValue(depth) {
|
|
1526
|
+
if (depth > MAX_DEPTH) invalidJson();
|
|
1527
|
+
skipWhitespace();
|
|
1528
|
+
const character = text[offset];
|
|
1529
|
+
if (character === "{") return parseObject2(depth);
|
|
1530
|
+
if (character === "[") return parseArray(depth);
|
|
1531
|
+
if (character === '"') {
|
|
1532
|
+
parseString();
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
for (const literal of ["true", "false", "null"]) {
|
|
1536
|
+
if (text.startsWith(literal, offset)) {
|
|
1537
|
+
offset += literal.length;
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const number = text.slice(offset).match(NUMBER_PATTERN)?.[0];
|
|
1542
|
+
if (number !== void 0) {
|
|
1543
|
+
offset += number.length;
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
invalidJson();
|
|
1547
|
+
}
|
|
1548
|
+
if (text.length === 0) invalidJson();
|
|
1549
|
+
parseValue(0);
|
|
1550
|
+
skipWhitespace();
|
|
1551
|
+
if (offset !== text.length) invalidJson();
|
|
1552
|
+
try {
|
|
1553
|
+
return JSON.parse(text);
|
|
1554
|
+
} catch {
|
|
1555
|
+
invalidJson();
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// src/workflow/variant-metadata.ts
|
|
1560
|
+
var ASSISTED_SIDECAR_MAX_BYTES = 16384;
|
|
1561
|
+
var MANAGED_VARIANTS_MARKER_NAME = ".pi-career-variants.json";
|
|
1562
|
+
var MANAGED_VARIANTS_MARKER_SCHEMA = "pi.career.variants_directory.v1";
|
|
1563
|
+
var ASSISTED_VARIANT_SCHEMA_V1 = "pi.career.assisted_variant_meta.v1";
|
|
1564
|
+
var ASSISTED_VARIANT_SCHEMA_V2 = "pi.career.assisted_variant_meta.v2";
|
|
1565
|
+
var SHA2562 = /^[a-f0-9]{64}$/;
|
|
1566
|
+
var CANONICAL_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
1567
|
+
var LEGACY_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
1568
|
+
function isRecord3(value) {
|
|
1569
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1570
|
+
}
|
|
1571
|
+
function exactKeys4(value, expected) {
|
|
1572
|
+
return Object.keys(value).sort().join("\0") === [...expected].sort().join("\0");
|
|
1573
|
+
}
|
|
1574
|
+
function validSha256(value) {
|
|
1575
|
+
return typeof value === "string" && SHA2562.test(value);
|
|
1576
|
+
}
|
|
1577
|
+
function validCanonicalTimestamp(value) {
|
|
1578
|
+
return typeof value === "string" && CANONICAL_TIMESTAMP.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
1579
|
+
}
|
|
1580
|
+
function sha256Bytes(value) {
|
|
1581
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
1582
|
+
}
|
|
1583
|
+
function encodeCanonical(value) {
|
|
1584
|
+
return Buffer.from(`${JSON.stringify(value, null, 2)}
|
|
1585
|
+
`, "utf8");
|
|
1586
|
+
}
|
|
1587
|
+
function encodeManagedVariantsMarker(libraryRootId, createdAt) {
|
|
1588
|
+
if (!validSha256(libraryRootId) || !validCanonicalTimestamp(createdAt)) {
|
|
1589
|
+
throw new TypeError("invalid managed variants marker");
|
|
1590
|
+
}
|
|
1591
|
+
return encodeCanonical({
|
|
1592
|
+
schema_version: MANAGED_VARIANTS_MARKER_SCHEMA,
|
|
1593
|
+
kind: "managed_variants_directory",
|
|
1594
|
+
library_root_id: libraryRootId,
|
|
1595
|
+
created_at: createdAt
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
function parseManagedVariantsMarker(text, expectedLibraryRootId) {
|
|
1599
|
+
try {
|
|
1600
|
+
const value = parseStrictJson(text);
|
|
1601
|
+
if (!isRecord3(value) || !exactKeys4(value, [
|
|
1602
|
+
"schema_version",
|
|
1603
|
+
"kind",
|
|
1604
|
+
"library_root_id",
|
|
1605
|
+
"created_at"
|
|
1606
|
+
])) return void 0;
|
|
1607
|
+
if (value.schema_version !== MANAGED_VARIANTS_MARKER_SCHEMA || value.kind !== "managed_variants_directory" || value.library_root_id !== expectedLibraryRootId || !validSha256(value.library_root_id) || !validCanonicalTimestamp(value.created_at)) return void 0;
|
|
1608
|
+
return value;
|
|
1609
|
+
} catch {
|
|
1610
|
+
return void 0;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function encodeAssistedVariantMetadataV2(metadata) {
|
|
1614
|
+
if (!validSha256(metadata.base_document_id) || !validSha256(metadata.base_text_sha256) || !validSha256(metadata.artifact_sha256) || !validCanonicalTimestamp(metadata.created_at)) throw new TypeError("invalid assisted variant metadata");
|
|
1615
|
+
return encodeCanonical({
|
|
1616
|
+
schema_version: ASSISTED_VARIANT_SCHEMA_V2,
|
|
1617
|
+
kind: "assisted_variant",
|
|
1618
|
+
authority: "assisted_non_authoritative",
|
|
1619
|
+
base_document_id: metadata.base_document_id,
|
|
1620
|
+
base_text_sha256: metadata.base_text_sha256,
|
|
1621
|
+
artifact_sha256: metadata.artifact_sha256,
|
|
1622
|
+
created_at: metadata.created_at
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
function parseLegacyMetadata(value) {
|
|
1626
|
+
if (!exactKeys4(value, ["schema_version", "kind", "base_document_id", "created_at"])) {
|
|
1627
|
+
return void 0;
|
|
1628
|
+
}
|
|
1629
|
+
if (value.kind !== "assisted_variant" || !validSha256(value.base_document_id) || typeof value.created_at !== "string" || !LEGACY_TIMESTAMP.test(value.created_at) || !Number.isFinite(Date.parse(value.created_at))) return void 0;
|
|
1630
|
+
return { schemaVersion: ASSISTED_VARIANT_SCHEMA_V1, baseDocumentId: value.base_document_id };
|
|
1631
|
+
}
|
|
1632
|
+
function parseHashBoundMetadata(value, artifactBytes) {
|
|
1633
|
+
if (!exactKeys4(value, [
|
|
1634
|
+
"schema_version",
|
|
1635
|
+
"kind",
|
|
1636
|
+
"authority",
|
|
1637
|
+
"base_document_id",
|
|
1638
|
+
"base_text_sha256",
|
|
1639
|
+
"artifact_sha256",
|
|
1640
|
+
"created_at"
|
|
1641
|
+
])) return void 0;
|
|
1642
|
+
if (value.kind !== "assisted_variant" || value.authority !== "assisted_non_authoritative" || !validSha256(value.base_document_id) || !validSha256(value.base_text_sha256) || !validSha256(value.artifact_sha256) || value.artifact_sha256 !== sha256Bytes(artifactBytes) || !validCanonicalTimestamp(value.created_at)) return void 0;
|
|
1643
|
+
return { schemaVersion: ASSISTED_VARIANT_SCHEMA_V2, baseDocumentId: value.base_document_id };
|
|
1644
|
+
}
|
|
1645
|
+
function parseAssistedVariantMetadata(text, artifactBytes) {
|
|
1646
|
+
try {
|
|
1647
|
+
const value = parseStrictJson(text);
|
|
1648
|
+
if (!isRecord3(value)) return void 0;
|
|
1649
|
+
if (value.schema_version === ASSISTED_VARIANT_SCHEMA_V1) return parseLegacyMetadata(value);
|
|
1650
|
+
if (value.schema_version === ASSISTED_VARIANT_SCHEMA_V2) {
|
|
1651
|
+
return parseHashBoundMetadata(value, artifactBytes);
|
|
1652
|
+
}
|
|
1653
|
+
return void 0;
|
|
1654
|
+
} catch {
|
|
1655
|
+
return void 0;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1492
1659
|
// src/workflow/scan.ts
|
|
1493
1660
|
var SCAN_MAX_DEPTH = 8;
|
|
1494
1661
|
var SCAN_MAX_FILES_PER_ROOT = 500;
|
|
1495
1662
|
var SCAN_MAX_FILES_TOTAL = 2e3;
|
|
1496
1663
|
var SCAN_MAX_RAW_BYTES = 256 * 1024;
|
|
1497
|
-
var SIDECAR_MAX_BYTES = 16384;
|
|
1498
1664
|
var MAX_DIRECTORY_ENTRIES_PER_ROOT = 1e4;
|
|
1499
|
-
var SHA2562 = /^[a-f0-9]{64}$/;
|
|
1500
|
-
var ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
1501
1665
|
function compareText(left, right) {
|
|
1502
1666
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
1503
1667
|
}
|
|
1504
1668
|
function sha256(value) {
|
|
1505
|
-
return
|
|
1669
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
1506
1670
|
}
|
|
1507
1671
|
function normalizeDocumentText(value) {
|
|
1508
1672
|
return value.replace(/\r\n?/g, "\n");
|
|
@@ -1535,34 +1699,58 @@ function resumeLabel(file, format, text) {
|
|
|
1535
1699
|
function sidecarPath(file) {
|
|
1536
1700
|
return path3.join(path3.dirname(file), `${path3.basename(file, path3.extname(file))}.pi-career.json`);
|
|
1537
1701
|
}
|
|
1538
|
-
function
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
const keys = "base_document_id,created_at,kind,schema_version";
|
|
1542
|
-
if (Object.keys(record).sort().join(",") !== keys) return false;
|
|
1543
|
-
if (record.schema_version !== "pi.career.assisted_variant_meta.v1" || record.kind !== "assisted_variant") {
|
|
1544
|
-
return false;
|
|
1545
|
-
}
|
|
1546
|
-
if (typeof record.base_document_id !== "string" || !SHA2562.test(record.base_document_id)) return false;
|
|
1547
|
-
return typeof record.created_at === "string" && ISO_UTC.test(record.created_at) && Number.isFinite(Date.parse(record.created_at));
|
|
1702
|
+
function privateMode(metadata, expected) {
|
|
1703
|
+
const userId = process.geteuid?.() ?? process.getuid?.();
|
|
1704
|
+
return userId !== void 0 && metadata.uid === userId && (metadata.mode & 511) === expected;
|
|
1548
1705
|
}
|
|
1549
|
-
async function readSidecar(file) {
|
|
1706
|
+
async function readSidecar(file, artifactBytes, required) {
|
|
1550
1707
|
const sidecar = sidecarPath(file);
|
|
1551
1708
|
try {
|
|
1552
1709
|
const metadata = await lstat3(sidecar);
|
|
1553
|
-
const invalidMetadata = !metadata.isFile() || metadata.isSymbolicLink() || metadata.size <= 0 || metadata.size >
|
|
1554
|
-
if (invalidMetadata) return { kind: "
|
|
1710
|
+
const invalidMetadata = !metadata.isFile() || metadata.isSymbolicLink() || metadata.size <= 0 || metadata.size > ASSISTED_SIDECAR_MAX_BYTES;
|
|
1711
|
+
if (invalidMetadata) return { kind: "quarantined" };
|
|
1555
1712
|
const bytes = await readFile3(sidecar);
|
|
1556
|
-
if (bytes.length !== metadata.size) return { kind: "
|
|
1713
|
+
if (bytes.length !== metadata.size) return { kind: "quarantined" };
|
|
1557
1714
|
const text = new TextDecoder4("utf-8", { fatal: true }).decode(bytes);
|
|
1558
|
-
const parsed =
|
|
1559
|
-
if (
|
|
1560
|
-
|
|
1715
|
+
const parsed = parseAssistedVariantMetadata(text, artifactBytes);
|
|
1716
|
+
if (parsed === void 0 || parsed.schemaVersion === "pi.career.assisted_variant_meta.v2" && !privateMode(metadata, 384)) {
|
|
1717
|
+
return { kind: "quarantined" };
|
|
1718
|
+
}
|
|
1719
|
+
return { kind: "assisted_variant", variantGroupId: parsed.baseDocumentId };
|
|
1561
1720
|
} catch (error) {
|
|
1562
|
-
if (error?.code === "ENOENT")
|
|
1563
|
-
|
|
1721
|
+
if (error?.code === "ENOENT") {
|
|
1722
|
+
return { kind: required ? "quarantined" : "original" };
|
|
1723
|
+
}
|
|
1724
|
+
return { kind: "quarantined" };
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
function managedVariantsPath(config, root) {
|
|
1728
|
+
const configured = config.generated_variants_root === void 0 ? void 0 : path3.resolve(config.generated_variants_root);
|
|
1729
|
+
return configured !== void 0 && path3.dirname(configured) === root.path ? configured : path3.join(root.path, "variants");
|
|
1730
|
+
}
|
|
1731
|
+
async function managedVariantsDirectory(config, root) {
|
|
1732
|
+
const directoryPath = managedVariantsPath(config, root);
|
|
1733
|
+
try {
|
|
1734
|
+
const directory = await lstat3(directoryPath);
|
|
1735
|
+
const canonical = await realpath3(directoryPath);
|
|
1736
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || canonical !== directoryPath || !privateMode(directory, 448)) return { path: directoryPath, markerValid: false };
|
|
1737
|
+
const markerPath = path3.join(directoryPath, MANAGED_VARIANTS_MARKER_NAME);
|
|
1738
|
+
const marker = await lstat3(markerPath);
|
|
1739
|
+
if (!marker.isFile() || marker.isSymbolicLink() || marker.size <= 0 || marker.size > ASSISTED_SIDECAR_MAX_BYTES || !privateMode(marker, 384)) return { path: directoryPath, markerValid: false };
|
|
1740
|
+
const bytes = await readFile3(markerPath);
|
|
1741
|
+
if (bytes.length !== marker.size) return { path: directoryPath, markerValid: false };
|
|
1742
|
+
const text = new TextDecoder4("utf-8", { fatal: true }).decode(bytes);
|
|
1743
|
+
return {
|
|
1744
|
+
path: directoryPath,
|
|
1745
|
+
markerValid: parseManagedVariantsMarker(text, root.id) !== void 0
|
|
1746
|
+
};
|
|
1747
|
+
} catch {
|
|
1748
|
+
return { path: directoryPath, markerValid: false };
|
|
1564
1749
|
}
|
|
1565
1750
|
}
|
|
1751
|
+
function containingManagedVariants(file, managedDirectories) {
|
|
1752
|
+
return managedDirectories.filter((managed) => file.startsWith(`${managed.path}${path3.sep}`));
|
|
1753
|
+
}
|
|
1566
1754
|
async function scanRootIsCurrent(root) {
|
|
1567
1755
|
try {
|
|
1568
1756
|
const metadata = await lstat3(root.path);
|
|
@@ -1645,7 +1833,7 @@ async function collectCandidates(root, maximum, warnings2) {
|
|
|
1645
1833
|
});
|
|
1646
1834
|
return { candidates, capped, stale: false };
|
|
1647
1835
|
}
|
|
1648
|
-
async function scanCandidate(root, candidate, warnings2) {
|
|
1836
|
+
async function scanCandidate(root, candidate, managedDirectories, warnings2) {
|
|
1649
1837
|
let metadata;
|
|
1650
1838
|
let canonical;
|
|
1651
1839
|
try {
|
|
@@ -1690,9 +1878,15 @@ async function scanCandidate(root, candidate, warnings2) {
|
|
|
1690
1878
|
}
|
|
1691
1879
|
const text = normalizeDocumentText(decoded);
|
|
1692
1880
|
const id = sha256(canonical);
|
|
1693
|
-
const
|
|
1694
|
-
if (
|
|
1881
|
+
const containingManaged = containingManagedVariants(canonical, managedDirectories);
|
|
1882
|
+
if (containingManaged.some((managed) => !managed.markerValid)) {
|
|
1883
|
+
warnings2.push({ code: "invalid_assisted_sidecar", root_id: root.id, relative_path: candidate.relative });
|
|
1884
|
+
return void 0;
|
|
1885
|
+
}
|
|
1886
|
+
const sidecar = await readSidecar(canonical, bytes, containingManaged.length > 0);
|
|
1887
|
+
if (sidecar.kind === "quarantined") {
|
|
1695
1888
|
warnings2.push({ code: "invalid_assisted_sidecar", root_id: root.id, relative_path: candidate.relative });
|
|
1889
|
+
return void 0;
|
|
1696
1890
|
}
|
|
1697
1891
|
return {
|
|
1698
1892
|
id,
|
|
@@ -1714,6 +1908,9 @@ async function scanLibrary(config) {
|
|
|
1714
1908
|
const warnings2 = [];
|
|
1715
1909
|
const records = [];
|
|
1716
1910
|
const roots = [];
|
|
1911
|
+
const managedDirectories = await Promise.all(
|
|
1912
|
+
config.library_roots.map((root) => managedVariantsDirectory(config, root))
|
|
1913
|
+
);
|
|
1717
1914
|
let totalCapped = false;
|
|
1718
1915
|
let scannedCandidateCount = 0;
|
|
1719
1916
|
for (const root of config.library_roots) {
|
|
@@ -1736,7 +1933,7 @@ async function scanLibrary(config) {
|
|
|
1736
1933
|
scannedCandidateCount += collected.candidates.length;
|
|
1737
1934
|
const rootRecords = [];
|
|
1738
1935
|
for (const candidate of collected.candidates) {
|
|
1739
|
-
const record = await scanCandidate(root, candidate, warnings2);
|
|
1936
|
+
const record = await scanCandidate(root, candidate, managedDirectories, warnings2);
|
|
1740
1937
|
if (record !== void 0) rootRecords.push(record);
|
|
1741
1938
|
}
|
|
1742
1939
|
records.push(...rootRecords);
|
|
@@ -1762,7 +1959,7 @@ function eligibleOriginals(scan) {
|
|
|
1762
1959
|
}
|
|
1763
1960
|
|
|
1764
1961
|
// src/workflow/result-projection.ts
|
|
1765
|
-
function
|
|
1962
|
+
function isRecord4(value) {
|
|
1766
1963
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1767
1964
|
}
|
|
1768
1965
|
function numberField(value, field) {
|
|
@@ -1772,7 +1969,7 @@ function numberField(value, field) {
|
|
|
1772
1969
|
}
|
|
1773
1970
|
function recordField(value, field) {
|
|
1774
1971
|
const found = value[field];
|
|
1775
|
-
if (!
|
|
1972
|
+
if (!isRecord4(found)) throw workflowError("core_result_invalid");
|
|
1776
1973
|
return found;
|
|
1777
1974
|
}
|
|
1778
1975
|
function arrayField(value, field) {
|
|
@@ -1782,7 +1979,7 @@ function arrayField(value, field) {
|
|
|
1782
1979
|
}
|
|
1783
1980
|
function compactObjects(value, fields, maximum) {
|
|
1784
1981
|
return value.slice(0, maximum).flatMap((candidate) => {
|
|
1785
|
-
if (!
|
|
1982
|
+
if (!isRecord4(candidate)) return [];
|
|
1786
1983
|
const selected = {};
|
|
1787
1984
|
for (const field of fields) {
|
|
1788
1985
|
if (candidate[field] !== void 0) selected[field] = candidate[field];
|
|
@@ -1794,7 +1991,7 @@ function compactWarnings(value) {
|
|
|
1794
1991
|
return compactObjects(value, ["code", "message", "related_fields", "related_categories"], 3);
|
|
1795
1992
|
}
|
|
1796
1993
|
function parseConfidencePreview(value) {
|
|
1797
|
-
if (!
|
|
1994
|
+
if (!isRecord4(value) || typeof value.label !== "string" || typeof value.score !== "number") {
|
|
1798
1995
|
throw workflowError("core_result_invalid");
|
|
1799
1996
|
}
|
|
1800
1997
|
return { label: value.label, score: value.score };
|
|
@@ -1802,7 +1999,7 @@ function parseConfidencePreview(value) {
|
|
|
1802
1999
|
function parseCoreJson(json) {
|
|
1803
2000
|
try {
|
|
1804
2001
|
const value = JSON.parse(json);
|
|
1805
|
-
if (!
|
|
2002
|
+
if (!isRecord4(value)) throw workflowError("core_result_invalid");
|
|
1806
2003
|
return value;
|
|
1807
2004
|
} catch (error) {
|
|
1808
2005
|
if (error instanceof Error && error.name === "CareerWorkflowError") throw error;
|
|
@@ -1814,7 +2011,7 @@ function projectResumeAnalysis(result) {
|
|
|
1814
2011
|
const checks = arrayField(result, "checks");
|
|
1815
2012
|
const confidence = recordField(result, "confidence_context");
|
|
1816
2013
|
const parseConfidence = parseConfidencePreview(confidence.parse_confidence);
|
|
1817
|
-
const adjusted = checks.some((check) =>
|
|
2014
|
+
const adjusted = checks.some((check) => isRecord4(check) && check.score_adjusted === true);
|
|
1818
2015
|
return {
|
|
1819
2016
|
schema_version: RESULT_PROJECTION_SCHEMA,
|
|
1820
2017
|
core_schema_version: "career.resume_analysis.v1",
|
|
@@ -1841,7 +2038,7 @@ function projectJobMatch(result) {
|
|
|
1841
2038
|
const recommendation = recordField(result, "recommendation");
|
|
1842
2039
|
if (typeof recommendation.label !== "string") throw workflowError("core_result_invalid");
|
|
1843
2040
|
const provisional = confidence.is_uncertain === true;
|
|
1844
|
-
const adjusted = categories.some((category) =>
|
|
2041
|
+
const adjusted = categories.some((category) => isRecord4(category) && category.score_adjusted === true);
|
|
1845
2042
|
return {
|
|
1846
2043
|
schema_version: RESULT_PROJECTION_SCHEMA,
|
|
1847
2044
|
core_schema_version: "career.job_match.v1",
|
|
@@ -1932,12 +2129,12 @@ function rankMatches(values) {
|
|
|
1932
2129
|
// src/workflow/session-state.ts
|
|
1933
2130
|
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;
|
|
1934
2131
|
var SHA2563 = /^[a-f0-9]{64}$/;
|
|
1935
|
-
var
|
|
2132
|
+
var ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
1936
2133
|
var CARD_MAX_BYTES = 16384;
|
|
1937
|
-
function
|
|
2134
|
+
function isRecord5(value) {
|
|
1938
2135
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1939
2136
|
}
|
|
1940
|
-
function
|
|
2137
|
+
function exactKeys5(value, required, optional = []) {
|
|
1941
2138
|
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
1942
2139
|
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => allowed.has(key));
|
|
1943
2140
|
}
|
|
@@ -1948,10 +2145,10 @@ function boundedLabel(value) {
|
|
|
1948
2145
|
return typeof value === "string" && value.length > 0 && [...value].length <= 120 && !/[\u0000-\u001f\u007f]/.test(value);
|
|
1949
2146
|
}
|
|
1950
2147
|
function validBase(value) {
|
|
1951
|
-
return value.schema_version === WORKFLOW_STATE_SCHEMA && typeof value.state_id === "string" && UUID2.test(value.state_id) && typeof value.created_at === "string" &&
|
|
2148
|
+
return value.schema_version === WORKFLOW_STATE_SCHEMA && typeof value.state_id === "string" && UUID2.test(value.state_id) && typeof value.created_at === "string" && ISO_UTC.test(value.created_at) && Number.isFinite(Date.parse(value.created_at));
|
|
1952
2149
|
}
|
|
1953
2150
|
function validFlags(value) {
|
|
1954
|
-
if (!
|
|
2151
|
+
if (!isRecord5(value) || !exactKeys5(value, ["adjusted", "provisional", "close_cluster", "stale"])) {
|
|
1955
2152
|
return false;
|
|
1956
2153
|
}
|
|
1957
2154
|
return [value.adjusted, value.provisional, value.close_cluster, value.stale].every(
|
|
@@ -1959,10 +2156,10 @@ function validFlags(value) {
|
|
|
1959
2156
|
);
|
|
1960
2157
|
}
|
|
1961
2158
|
function validProjection(value) {
|
|
1962
|
-
if (!
|
|
2159
|
+
if (!isRecord5(value) || !exactKeys5(value, ["schema_version", "core_schema_version", "summary", "ui_flags"])) {
|
|
1963
2160
|
return false;
|
|
1964
2161
|
}
|
|
1965
|
-
if (value.schema_version !== RESULT_PROJECTION_SCHEMA || value.core_schema_version !== "career.resume_analysis.v1" && value.core_schema_version !== "career.job_match.v1" || !
|
|
2162
|
+
if (value.schema_version !== RESULT_PROJECTION_SCHEMA || value.core_schema_version !== "career.resume_analysis.v1" && value.core_schema_version !== "career.job_match.v1" || !isRecord5(value.summary) || !validFlags(value.ui_flags)) return false;
|
|
1966
2163
|
return Buffer.byteLength(JSON.stringify(value), "utf8") <= CARD_MAX_BYTES;
|
|
1967
2164
|
}
|
|
1968
2165
|
function isUuid(value) {
|
|
@@ -1973,7 +2170,7 @@ function isSha256(value) {
|
|
|
1973
2170
|
}
|
|
1974
2171
|
var APPLICATION_STATUSES = /* @__PURE__ */ new Set(["preparing", "applied", "interviewing", "closed"]);
|
|
1975
2172
|
function parseApplication(value) {
|
|
1976
|
-
if (!
|
|
2173
|
+
if (!exactKeys5(value, [
|
|
1977
2174
|
"schema_version",
|
|
1978
2175
|
"kind",
|
|
1979
2176
|
"state_id",
|
|
@@ -1988,10 +2185,10 @@ function parseApplication(value) {
|
|
|
1988
2185
|
}
|
|
1989
2186
|
function parseApplicationClear(value) {
|
|
1990
2187
|
const keys = ["schema_version", "kind", "state_id", "created_at", "clears_state_id"];
|
|
1991
|
-
return
|
|
2188
|
+
return exactKeys5(value, keys) && isUuid(value.clears_state_id) ? value : void 0;
|
|
1992
2189
|
}
|
|
1993
2190
|
function parseVacancy(value) {
|
|
1994
|
-
if (!
|
|
2191
|
+
if (!exactKeys5(value, [
|
|
1995
2192
|
"schema_version",
|
|
1996
2193
|
"kind",
|
|
1997
2194
|
"state_id",
|
|
@@ -2011,21 +2208,21 @@ function parseVacancy(value) {
|
|
|
2011
2208
|
}
|
|
2012
2209
|
function parseVacancyClear(value) {
|
|
2013
2210
|
const keys = ["schema_version", "kind", "state_id", "created_at", "clears_state_id"];
|
|
2014
|
-
return
|
|
2211
|
+
return exactKeys5(value, keys) && isUuid(value.clears_state_id) ? value : void 0;
|
|
2015
2212
|
}
|
|
2016
2213
|
function parseConsent(value) {
|
|
2017
2214
|
const keys = ["schema_version", "kind", "state_id", "created_at", "scope", "granted"];
|
|
2018
|
-
return
|
|
2215
|
+
return exactKeys5(value, keys) && value.scope === "session_persistence" && typeof value.granted === "boolean" ? value : void 0;
|
|
2019
2216
|
}
|
|
2020
2217
|
function parseConsentClear(value) {
|
|
2021
2218
|
const keys = ["schema_version", "kind", "state_id", "created_at", "scope", "clears_state_id"];
|
|
2022
|
-
return
|
|
2219
|
+
return exactKeys5(value, keys) && value.scope === "session_persistence" && isUuid(value.clears_state_id) ? value : void 0;
|
|
2023
2220
|
}
|
|
2024
2221
|
function validInputDigests(value) {
|
|
2025
|
-
return
|
|
2222
|
+
return isRecord5(value) && exactKeys5(value, ["resume_text_sha256", "vacancy_text_sha256"]) && isSha256(value.resume_text_sha256) && isSha256(value.vacancy_text_sha256);
|
|
2026
2223
|
}
|
|
2027
2224
|
function parseResultCard(value) {
|
|
2028
|
-
if (!
|
|
2225
|
+
if (!exactKeys5(value, [
|
|
2029
2226
|
"schema_version",
|
|
2030
2227
|
"kind",
|
|
2031
2228
|
"state_id",
|
|
@@ -2049,7 +2246,7 @@ function parseResultCard(value) {
|
|
|
2049
2246
|
return validProjection(value.projection) ? value : void 0;
|
|
2050
2247
|
}
|
|
2051
2248
|
function parseWorkflowEntryData(value) {
|
|
2052
|
-
if (!
|
|
2249
|
+
if (!isRecord5(value) || !validBase(value)) return void 0;
|
|
2053
2250
|
switch (value.kind) {
|
|
2054
2251
|
case "application":
|
|
2055
2252
|
return parseApplication(value);
|
|
@@ -2203,11 +2400,18 @@ var MESSAGES = {
|
|
|
2203
2400
|
result_not_found: "The requested ephemeral result handle is unavailable or expired.",
|
|
2204
2401
|
review_not_found: "The requested ephemeral review handle is unavailable or expired.",
|
|
2205
2402
|
selection_invalid: "Selected change IDs are invalid for this reviewed proposal.",
|
|
2403
|
+
pdf_materialization_unsupported: "PDF review changes are manual-application guidance and cannot be materialized from extracted text.",
|
|
2206
2404
|
managed_contract_invalid: "The selected Career Core managed-adapter contracts are incompatible.",
|
|
2207
2405
|
managed_result_invalid: "Career Core returned an unexpected managed-workflow result.",
|
|
2208
2406
|
managed_result_capacity: "The complete Career Core result exceeds the bounded in-memory managed-result capacity.",
|
|
2209
2407
|
detail_too_large: "The requested model-visible detail is too large; request a narrower section.",
|
|
2210
|
-
session_changed: "The Pi session changed; run career_run context again for fresh ephemeral handles."
|
|
2408
|
+
session_changed: "The Pi session changed; run career_run context again for fresh ephemeral handles.",
|
|
2409
|
+
variant_save_unavailable: "The materialized variant is unavailable, stale, or ineligible for local saving.",
|
|
2410
|
+
variant_save_destination_invalid: "The managed variants destination is unavailable or does not meet the private-directory contract.",
|
|
2411
|
+
variant_save_preview_changed: "The exact save preview changed or was cancelled; no file was written.",
|
|
2412
|
+
variant_save_collision: "A save destination already exists; no existing file was replaced.",
|
|
2413
|
+
variant_save_verification_failed: "The saved variant could not be verified as an excluded assisted artifact.",
|
|
2414
|
+
variant_save_status_unknown: "The save reached an indeterminate local-filesystem state; inspect the approved destination before retrying."
|
|
2211
2415
|
};
|
|
2212
2416
|
var CareerRunError = class extends Error {
|
|
2213
2417
|
code;
|
|
@@ -2221,6 +2425,9 @@ var CareerRunError = class extends Error {
|
|
|
2221
2425
|
this.code = code;
|
|
2222
2426
|
}
|
|
2223
2427
|
};
|
|
2428
|
+
function careerRunErrorMessage(code) {
|
|
2429
|
+
return MESSAGES[code];
|
|
2430
|
+
}
|
|
2224
2431
|
function careerRunError(code) {
|
|
2225
2432
|
return new CareerRunError(code);
|
|
2226
2433
|
}
|
|
@@ -2246,7 +2453,7 @@ var SECTIONS = /* @__PURE__ */ new Set([
|
|
|
2246
2453
|
"certifications",
|
|
2247
2454
|
"other"
|
|
2248
2455
|
]);
|
|
2249
|
-
function
|
|
2456
|
+
function isRecord6(value) {
|
|
2250
2457
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2251
2458
|
}
|
|
2252
2459
|
function hasExactKeys(value, keys) {
|
|
@@ -2267,7 +2474,7 @@ function stringList(value, minimum, maximum, itemMaximum) {
|
|
|
2267
2474
|
return new Set(strings).size === strings.length ? [...strings] : void 0;
|
|
2268
2475
|
}
|
|
2269
2476
|
function parseVariantChange(value) {
|
|
2270
|
-
if (!
|
|
2477
|
+
if (!isRecord6(value) || !hasExactKeys(value, [
|
|
2271
2478
|
"section",
|
|
2272
2479
|
"start_line",
|
|
2273
2480
|
"end_line",
|
|
@@ -2290,7 +2497,7 @@ function parseVariantChange(value) {
|
|
|
2290
2497
|
};
|
|
2291
2498
|
}
|
|
2292
2499
|
function parseAnalysisSuggestion(value) {
|
|
2293
|
-
if (!
|
|
2500
|
+
if (!isRecord6(value) || !hasExactKeys(value, [
|
|
2294
2501
|
"basis_check_id",
|
|
2295
2502
|
"start_line",
|
|
2296
2503
|
"end_line",
|
|
@@ -2310,7 +2517,7 @@ function parseAnalysisSuggestion(value) {
|
|
|
2310
2517
|
};
|
|
2311
2518
|
}
|
|
2312
2519
|
function parseAnalysisReplacement(value) {
|
|
2313
|
-
if (!
|
|
2520
|
+
if (!isRecord6(value) || !hasExactKeys(value, [
|
|
2314
2521
|
"basis_check_id",
|
|
2315
2522
|
"start_line",
|
|
2316
2523
|
"end_line",
|
|
@@ -2330,7 +2537,7 @@ function parseAnalysisReplacement(value) {
|
|
|
2330
2537
|
};
|
|
2331
2538
|
}
|
|
2332
2539
|
function parseVariantChanges(payload) {
|
|
2333
|
-
if (!
|
|
2540
|
+
if (!isRecord6(payload) || !hasExactKeys(payload, ["changes"]) || !Array.isArray(payload.changes) || payload.changes.length > 50) {
|
|
2334
2541
|
throw new Error("managed_payload_invalid");
|
|
2335
2542
|
}
|
|
2336
2543
|
const changes = payload.changes.map(parseVariantChange);
|
|
@@ -2338,7 +2545,7 @@ function parseVariantChanges(payload) {
|
|
|
2338
2545
|
return changes;
|
|
2339
2546
|
}
|
|
2340
2547
|
function parseAnalysisSuggestions(payload) {
|
|
2341
|
-
if (!
|
|
2548
|
+
if (!isRecord6(payload) || !hasExactKeys(payload, ["suggestions"]) || !Array.isArray(payload.suggestions) || payload.suggestions.length > 3) {
|
|
2342
2549
|
throw new Error("managed_payload_invalid");
|
|
2343
2550
|
}
|
|
2344
2551
|
const suggestions = payload.suggestions.map(parseAnalysisSuggestion);
|
|
@@ -2348,7 +2555,7 @@ function parseAnalysisSuggestions(payload) {
|
|
|
2348
2555
|
return suggestions;
|
|
2349
2556
|
}
|
|
2350
2557
|
function parseAnalysisReplacements(payload) {
|
|
2351
|
-
if (!
|
|
2558
|
+
if (!isRecord6(payload) || !hasExactKeys(payload, ["replacements"]) || !Array.isArray(payload.replacements) || payload.replacements.length > 3) {
|
|
2352
2559
|
throw new Error("managed_payload_invalid");
|
|
2353
2560
|
}
|
|
2354
2561
|
const replacements = payload.replacements.map(parseAnalysisReplacement);
|
|
@@ -2370,7 +2577,7 @@ function handlePrefix(kind) {
|
|
|
2370
2577
|
return kind === "review" ? "review" : kind === "variant" ? "variant" : "result";
|
|
2371
2578
|
}
|
|
2372
2579
|
function entryBytes(entry) {
|
|
2373
|
-
return Buffer.byteLength(entry.json, "utf8") + (entry.reviewInput === void 0 ? 0 : Buffer.byteLength(JSON.stringify(entry.reviewInput), "utf8"));
|
|
2580
|
+
return Buffer.byteLength(entry.json, "utf8") + (entry.reviewInput === void 0 ? 0 : Buffer.byteLength(JSON.stringify(entry.reviewInput), "utf8")) + (entry.variantSource === void 0 ? 0 : Buffer.byteLength(JSON.stringify(entry.variantSource), "utf8"));
|
|
2374
2581
|
}
|
|
2375
2582
|
var ManagedRegistry = class {
|
|
2376
2583
|
constructor(uuid, now) {
|
|
@@ -2489,9 +2696,38 @@ var careerRunParameters = Type.Object({
|
|
|
2489
2696
|
|
|
2490
2697
|
// src/managed/engine.ts
|
|
2491
2698
|
var MODEL_DETAIL_MAX_BYTES = 5e4;
|
|
2492
|
-
function
|
|
2699
|
+
function isRecord7(value) {
|
|
2493
2700
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2494
2701
|
}
|
|
2702
|
+
function stringArray(value) {
|
|
2703
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2704
|
+
}
|
|
2705
|
+
function validVariantSelectionChange(value, expectedId) {
|
|
2706
|
+
if (!isRecord7(value)) return false;
|
|
2707
|
+
return [
|
|
2708
|
+
value.change_id === expectedId,
|
|
2709
|
+
typeof value.section === "string",
|
|
2710
|
+
Number.isSafeInteger(value.start_line),
|
|
2711
|
+
Number.isSafeInteger(value.end_line),
|
|
2712
|
+
typeof value.original_text === "string",
|
|
2713
|
+
typeof value.proposed_text === "string",
|
|
2714
|
+
stringArray(value.resume_evidence),
|
|
2715
|
+
stringArray(value.vacancy_evidence)
|
|
2716
|
+
].every(Boolean);
|
|
2717
|
+
}
|
|
2718
|
+
function variantSelectionChange(value, expectedId) {
|
|
2719
|
+
if (!validVariantSelectionChange(value, expectedId)) return void 0;
|
|
2720
|
+
return {
|
|
2721
|
+
change_id: expectedId,
|
|
2722
|
+
section: value.section,
|
|
2723
|
+
start_line: value.start_line,
|
|
2724
|
+
end_line: value.end_line,
|
|
2725
|
+
original_text: value.original_text,
|
|
2726
|
+
proposed_text: value.proposed_text,
|
|
2727
|
+
resume_evidence: [...value.resume_evidence],
|
|
2728
|
+
vacancy_evidence: [...value.vacancy_evidence]
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2495
2731
|
function arrayField2(value, field) {
|
|
2496
2732
|
const found = value[field];
|
|
2497
2733
|
if (!Array.isArray(found)) throw careerRunError("managed_result_invalid");
|
|
@@ -2509,13 +2745,13 @@ function parseConsentDecision(payload) {
|
|
|
2509
2745
|
return payload;
|
|
2510
2746
|
}
|
|
2511
2747
|
function parseMaterializeRequest(payload) {
|
|
2512
|
-
if (!
|
|
2748
|
+
if (!isRecord7(payload) || Object.keys(payload).join("") !== "selected_change_ids") {
|
|
2513
2749
|
throw careerRunError("invalid_request");
|
|
2514
2750
|
}
|
|
2515
2751
|
return parseSelectedChangeIds(payload.selected_change_ids);
|
|
2516
2752
|
}
|
|
2517
2753
|
function parseDetailRequest(payload) {
|
|
2518
|
-
if (!
|
|
2754
|
+
if (!isRecord7(payload)) throw careerRunError("invalid_request");
|
|
2519
2755
|
const keys = Object.keys(payload).sort().join("\0");
|
|
2520
2756
|
if (keys !== "section" && keys !== "item\0section") throw careerRunError("invalid_request");
|
|
2521
2757
|
if (!DETAIL_SECTIONS.includes(payload.section)) {
|
|
@@ -2643,7 +2879,7 @@ function compactReplacementReview(result) {
|
|
|
2643
2879
|
};
|
|
2644
2880
|
}
|
|
2645
2881
|
function compactCanonicalChanges(changes) {
|
|
2646
|
-
return changes.map((change) =>
|
|
2882
|
+
return changes.map((change) => isRecord7(change) ? {
|
|
2647
2883
|
change_id: change.change_id,
|
|
2648
2884
|
section: change.section,
|
|
2649
2885
|
start_line: change.start_line,
|
|
@@ -2678,9 +2914,9 @@ function compactVariant(result) {
|
|
|
2678
2914
|
};
|
|
2679
2915
|
}
|
|
2680
2916
|
function evidenceDetail(value) {
|
|
2681
|
-
const analysis =
|
|
2682
|
-
const checks =
|
|
2683
|
-
return checks.flatMap((check) =>
|
|
2917
|
+
const analysis = isRecord7(value.baseline_analysis) ? value.baseline_analysis : value;
|
|
2918
|
+
const checks = isRecord7(analysis) && Array.isArray(analysis.checks) ? analysis.checks : [];
|
|
2919
|
+
return checks.flatMap((check) => isRecord7(check) && Array.isArray(check.evidence) ? [{ check_id: check.check_id, evidence: check.evidence }] : []);
|
|
2684
2920
|
}
|
|
2685
2921
|
var DETAIL_SUMMARIES = {
|
|
2686
2922
|
"resume.analyze": compactAnalyze,
|
|
@@ -2691,8 +2927,8 @@ var DETAIL_SUMMARIES = {
|
|
|
2691
2927
|
"resume.variant.materialize": compactVariant
|
|
2692
2928
|
};
|
|
2693
2929
|
function analysisChecks(value) {
|
|
2694
|
-
const analysis =
|
|
2695
|
-
return
|
|
2930
|
+
const analysis = isRecord7(value.baseline_analysis) ? value.baseline_analysis : value;
|
|
2931
|
+
return isRecord7(analysis) && Array.isArray(analysis.checks) ? analysis.checks : [];
|
|
2696
2932
|
}
|
|
2697
2933
|
function reviewedItems(value) {
|
|
2698
2934
|
const items = value.changes ?? value.suggestions ?? value.replacements ?? value.selected_changes ?? [];
|
|
@@ -2700,7 +2936,7 @@ function reviewedItems(value) {
|
|
|
2700
2936
|
return items;
|
|
2701
2937
|
}
|
|
2702
2938
|
function exactReviewedItem(items, item) {
|
|
2703
|
-
const found = items.find((candidate) =>
|
|
2939
|
+
const found = items.find((candidate) => isRecord7(candidate) && (candidate.change_id === item || candidate.suggestion_id === item || candidate.replacement_id === item));
|
|
2704
2940
|
if (found === void 0) throw careerRunError("result_not_found");
|
|
2705
2941
|
return found;
|
|
2706
2942
|
}
|
|
@@ -2730,6 +2966,23 @@ function mapInternalError(error) {
|
|
|
2730
2966
|
}
|
|
2731
2967
|
throw managedFailure(error);
|
|
2732
2968
|
}
|
|
2969
|
+
function selectableVariantReview(review) {
|
|
2970
|
+
if (review === void 0) throw careerRunError("review_not_found");
|
|
2971
|
+
if (![review.operation === "resume.variant.review", review.retainedChangeIds !== void 0].every(Boolean)) {
|
|
2972
|
+
throw careerRunError("review_not_found");
|
|
2973
|
+
}
|
|
2974
|
+
if (review.materializationAllowed !== true) throw careerRunError("pdf_materialization_unsupported");
|
|
2975
|
+
ensureSchema(review.value, "career.resume_variant_review.v1");
|
|
2976
|
+
validateAuthority(review.value);
|
|
2977
|
+
return review;
|
|
2978
|
+
}
|
|
2979
|
+
function selectableVariantChanges(review) {
|
|
2980
|
+
const values = arrayField2(review.value, "changes");
|
|
2981
|
+
if (values.length !== review.retainedChangeIds.length) throw careerRunError("managed_result_invalid");
|
|
2982
|
+
const changes = values.map((value, index) => variantSelectionChange(value, review.retainedChangeIds[index]));
|
|
2983
|
+
if (changes.some((change) => change === void 0)) throw careerRunError("managed_result_invalid");
|
|
2984
|
+
return changes;
|
|
2985
|
+
}
|
|
2733
2986
|
var CareerRunEngine = class {
|
|
2734
2987
|
constructor(options) {
|
|
2735
2988
|
this.options = options;
|
|
@@ -2754,6 +3007,53 @@ var CareerRunEngine = class {
|
|
|
2754
3007
|
shutdown() {
|
|
2755
3008
|
this.registry.clear();
|
|
2756
3009
|
}
|
|
3010
|
+
materializedVariantForSave(handle, ctx) {
|
|
3011
|
+
try {
|
|
3012
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
3013
|
+
this.registry.enterSession(sessionId);
|
|
3014
|
+
requireConsent(ctx);
|
|
3015
|
+
if (!this.registry.hasContext(sessionId)) throw careerRunError("context_required");
|
|
3016
|
+
const entry = this.registry.get(handle, "variant");
|
|
3017
|
+
if (entry === void 0 || entry.operation !== "resume.variant.materialize" || entry.variantSource === void 0) throw careerRunError("variant_save_unavailable");
|
|
3018
|
+
ensureSchema(entry.value, "career.resume_variant.v1");
|
|
3019
|
+
validateAuthority(entry.value);
|
|
3020
|
+
const assistedText = entry.value.assisted_resume_text;
|
|
3021
|
+
const selected = arrayField2(entry.value, "selected_changes");
|
|
3022
|
+
if (typeof assistedText !== "string" || selected.length === 0) {
|
|
3023
|
+
throw careerRunError("managed_result_invalid");
|
|
3024
|
+
}
|
|
3025
|
+
const selectedChangeIds = selected.map((change) => isRecord7(change) && typeof change.change_id === "string" ? change.change_id : "");
|
|
3026
|
+
if (selectedChangeIds.some((id) => !/^change-[0-9]{4}$/.test(id))) {
|
|
3027
|
+
throw careerRunError("managed_result_invalid");
|
|
3028
|
+
}
|
|
3029
|
+
return {
|
|
3030
|
+
handle: entry.handle,
|
|
3031
|
+
assistedText,
|
|
3032
|
+
selectedChangeIds,
|
|
3033
|
+
source: { ...entry.variantSource }
|
|
3034
|
+
};
|
|
3035
|
+
} catch (error) {
|
|
3036
|
+
mapInternalError(error);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
variantSelectionReview(handle, ctx) {
|
|
3040
|
+
try {
|
|
3041
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
3042
|
+
this.registry.enterSession(sessionId);
|
|
3043
|
+
requireConsent(ctx);
|
|
3044
|
+
if (!this.registry.hasContext(sessionId)) throw careerRunError("context_required");
|
|
3045
|
+
const review = selectableVariantReview(this.registry.get(handle, "review"));
|
|
3046
|
+
return {
|
|
3047
|
+
handle: review.handle,
|
|
3048
|
+
authority: "assisted_non_authoritative",
|
|
3049
|
+
changes: selectableVariantChanges(review),
|
|
3050
|
+
discarded_changes: arrayField2(review.value, "discarded_changes"),
|
|
3051
|
+
warnings: warnings(review.value)
|
|
3052
|
+
};
|
|
3053
|
+
} catch (error) {
|
|
3054
|
+
mapInternalError(error);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
2757
3057
|
async run(params, signal, ctx) {
|
|
2758
3058
|
try {
|
|
2759
3059
|
this.registry.enterSession(ctx.sessionManager.getSessionId());
|
|
@@ -2994,20 +3294,32 @@ var CareerRunEngine = class {
|
|
|
2994
3294
|
json: invocation.json,
|
|
2995
3295
|
value,
|
|
2996
3296
|
reviewInput: input,
|
|
2997
|
-
retainedChangeIds
|
|
3297
|
+
retainedChangeIds,
|
|
3298
|
+
materializationAllowed: resume.format !== "pdf",
|
|
3299
|
+
variantSource: {
|
|
3300
|
+
resumeId: resume.id,
|
|
3301
|
+
rootId: resume.root_id,
|
|
3302
|
+
format: resume.format,
|
|
3303
|
+
textSha256: resume.text_sha256
|
|
3304
|
+
}
|
|
2998
3305
|
});
|
|
2999
|
-
return resultEnvelope("variant-review", {
|
|
3306
|
+
return resultEnvelope("variant-review", {
|
|
3307
|
+
review: entry.handle,
|
|
3308
|
+
...summary,
|
|
3309
|
+
next_action: resume.format === "pdf" ? "Stop this turn. Present the reviewed changes as manual-application guidance only; extracted PDF text cannot be materialized as a styled resume." : `Stop this turn. In TUI, ask the user to run /career-review ${entry.handle}; otherwise show exact change details and ask for explicit canonical IDs. Only a later user turn may materialize the selected IDs.`
|
|
3310
|
+
}, {
|
|
3000
3311
|
status: "complete",
|
|
3001
3312
|
handle: entry.handle,
|
|
3002
|
-
|
|
3313
|
+
action: resume.format === "pdf" ? "pdf_manual" : "review_select",
|
|
3314
|
+
summary: resume.format === "pdf" ? `${retainedChangeIds.length} retained change(s) • PDF manual application only` : `${retainedChangeIds.length} retained change(s) • explicit selection required`
|
|
3003
3315
|
});
|
|
3004
3316
|
}
|
|
3005
3317
|
retainedChangeIds(value) {
|
|
3006
3318
|
const changes = arrayField2(value, "changes");
|
|
3007
3319
|
const ids = changes.flatMap(
|
|
3008
|
-
(change) =>
|
|
3320
|
+
(change) => isRecord7(change) && typeof change.change_id === "string" ? [change.change_id] : []
|
|
3009
3321
|
);
|
|
3010
|
-
if (ids.length !== changes.length) throw careerRunError("managed_result_invalid");
|
|
3322
|
+
if (ids.length !== changes.length || new Set(ids).size !== ids.length || ids.some((id) => !/^change-[0-9]{4}$/.test(id))) throw careerRunError("managed_result_invalid");
|
|
3011
3323
|
return ids;
|
|
3012
3324
|
}
|
|
3013
3325
|
async materialize(params, signal, ctx) {
|
|
@@ -3018,6 +3330,8 @@ var CareerRunEngine = class {
|
|
|
3018
3330
|
if (review === void 0 || review.operation !== "resume.variant.review" || review.reviewInput === void 0 || review.retainedChangeIds === void 0) {
|
|
3019
3331
|
throw careerRunError("review_not_found");
|
|
3020
3332
|
}
|
|
3333
|
+
if (review.materializationAllowed !== true) throw careerRunError("pdf_materialization_unsupported");
|
|
3334
|
+
if (review.variantSource === void 0) throw careerRunError("managed_result_invalid");
|
|
3021
3335
|
const selected = parseMaterializeRequest(params.payload);
|
|
3022
3336
|
const retained = new Set(review.retainedChangeIds);
|
|
3023
3337
|
if (selected.some((id) => !retained.has(id))) throw careerRunError("selection_invalid");
|
|
@@ -3038,11 +3352,17 @@ var CareerRunEngine = class {
|
|
|
3038
3352
|
kind: "variant",
|
|
3039
3353
|
operation: "resume.variant.materialize",
|
|
3040
3354
|
json: invocation.json,
|
|
3041
|
-
value
|
|
3355
|
+
value,
|
|
3356
|
+
variantSource: review.variantSource
|
|
3042
3357
|
});
|
|
3043
|
-
return resultEnvelope("materialize", {
|
|
3358
|
+
return resultEnvelope("materialize", {
|
|
3359
|
+
variant: entry.handle,
|
|
3360
|
+
...summary,
|
|
3361
|
+
next_action: `This assisted/non-authoritative result remains in memory. To review and save it locally, the user may run /career-save ${entry.handle}; never invoke saving as a model action.`
|
|
3362
|
+
}, {
|
|
3044
3363
|
status: "complete",
|
|
3045
3364
|
handle: entry.handle,
|
|
3365
|
+
action: "save_available",
|
|
3046
3366
|
summary: `${selected.length} selected change(s) materialized`
|
|
3047
3367
|
});
|
|
3048
3368
|
}
|
|
@@ -3081,93 +3401,6 @@ var CareerRunEngine = class {
|
|
|
3081
3401
|
}
|
|
3082
3402
|
};
|
|
3083
3403
|
|
|
3084
|
-
// src/managed/tool.ts
|
|
3085
|
-
function managedToolActive(pi, includeRaw) {
|
|
3086
|
-
const current = pi.getActiveTools();
|
|
3087
|
-
const retained = current.filter((name) => !RAW_TOOL_NAMES.includes(name));
|
|
3088
|
-
const next = includeRaw ? [...retained, ...RAW_TOOL_NAMES] : retained;
|
|
3089
|
-
if (!next.includes(MANAGED_TOOL_NAME)) next.push(MANAGED_TOOL_NAME);
|
|
3090
|
-
pi.setActiveTools([...new Set(next)]);
|
|
3091
|
-
}
|
|
3092
|
-
function registerCareerRun(pi, options = {}) {
|
|
3093
|
-
const engine = new CareerRunEngine({
|
|
3094
|
-
pi,
|
|
3095
|
-
agentDir: options.agentDir ?? getAgentDir(),
|
|
3096
|
-
invoke: options.invoke ?? invokeCareerCli,
|
|
3097
|
-
now: options.now ?? (() => /* @__PURE__ */ new Date()),
|
|
3098
|
-
uuid: options.uuid ?? randomUUID2
|
|
3099
|
-
});
|
|
3100
|
-
pi.registerTool({
|
|
3101
|
-
name: MANAGED_TOOL_NAME,
|
|
3102
|
-
label: "Career",
|
|
3103
|
-
description: "Run managed local Career Core workflows with ephemeral handles and native payload objects instead of nested JSON strings.",
|
|
3104
|
-
promptGuidelines: [
|
|
3105
|
-
"Start with context. If consent is required, ask first; consent payload is `approve` or `decline`. Use returned handles; match/variant-review use the current vacancy implicitly.",
|
|
3106
|
-
"Proposal payloads use Core fields. Materialize payload is {selected_change_ids:[...]}; detail payload is {section,item?}. Preserve warnings/uncertainty/authority, and never select changes automatically."
|
|
3107
|
-
],
|
|
3108
|
-
parameters: careerRunParameters,
|
|
3109
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
3110
|
-
onUpdate?.({
|
|
3111
|
-
content: [{ type: "text", text: `Running career ${params.command}…` }],
|
|
3112
|
-
details: { schema_version: "pi.career.run_details.v1", command: params.command }
|
|
3113
|
-
});
|
|
3114
|
-
return engine.run(params, signal, ctx);
|
|
3115
|
-
},
|
|
3116
|
-
renderCall(args, theme) {
|
|
3117
|
-
return new Text(
|
|
3118
|
-
theme.fg("toolTitle", theme.bold("career ")) + theme.fg("accent", args.command ?? "run"),
|
|
3119
|
-
0,
|
|
3120
|
-
0
|
|
3121
|
-
);
|
|
3122
|
-
},
|
|
3123
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
3124
|
-
if (isPartial) return new Text(theme.fg("warning", "Running Career Core…"), 0, 0);
|
|
3125
|
-
const details = result.details;
|
|
3126
|
-
if (details === void 0) return new Text(theme.fg("dim", "Career result unavailable"), 0, 0);
|
|
3127
|
-
const lines = [
|
|
3128
|
-
theme.fg(details.status === "consent_required" ? "warning" : "success", details.summary),
|
|
3129
|
-
...expanded && details.handle !== void 0 ? [theme.fg("dim", details.handle)] : []
|
|
3130
|
-
];
|
|
3131
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
3132
|
-
}
|
|
3133
|
-
});
|
|
3134
|
-
pi.registerCommand("career-tools", {
|
|
3135
|
-
description: "Choose managed or advanced raw Career Core tools",
|
|
3136
|
-
getArgumentCompletions: (prefix) => ["managed", "raw", "status"].filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value })),
|
|
3137
|
-
handler: async (args, ctx) => {
|
|
3138
|
-
const mode = args.trim();
|
|
3139
|
-
if (mode === "managed") managedToolActive(pi, false);
|
|
3140
|
-
else if (mode === "raw") managedToolActive(pi, true);
|
|
3141
|
-
else if (mode !== "status" && mode !== "") {
|
|
3142
|
-
ctx.ui.notify("Usage: /career-tools managed|raw|status", "warning");
|
|
3143
|
-
return;
|
|
3144
|
-
}
|
|
3145
|
-
const activeRaw = RAW_TOOL_NAMES.filter((name) => pi.getActiveTools().includes(name));
|
|
3146
|
-
ctx.ui.notify(
|
|
3147
|
-
`Career tools: career_run active; raw Career Core tools ${activeRaw.length === 0 ? "inactive" : "active"}.`,
|
|
3148
|
-
"info"
|
|
3149
|
-
);
|
|
3150
|
-
}
|
|
3151
|
-
});
|
|
3152
|
-
pi.on("session_start", (_event, ctx) => {
|
|
3153
|
-
engine.enterSession(ctx.sessionManager.getSessionId());
|
|
3154
|
-
managedToolActive(pi, false);
|
|
3155
|
-
});
|
|
3156
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
3157
|
-
engine.resetSession(ctx.sessionManager.getSessionId());
|
|
3158
|
-
});
|
|
3159
|
-
pi.on("session_shutdown", () => {
|
|
3160
|
-
engine.shutdown();
|
|
3161
|
-
});
|
|
3162
|
-
}
|
|
3163
|
-
|
|
3164
|
-
// src/workflow/commands.ts
|
|
3165
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3166
|
-
import {
|
|
3167
|
-
BorderedLoader,
|
|
3168
|
-
getAgentDir as getAgentDir2
|
|
3169
|
-
} from "@earendil-works/pi-coding-agent";
|
|
3170
|
-
|
|
3171
3404
|
// src/workflow/renderers.ts
|
|
3172
3405
|
import os from "node:os";
|
|
3173
3406
|
import path4 from "node:path";
|
|
@@ -3533,21 +3766,910 @@ var DetailViewer = class {
|
|
|
3533
3766
|
}
|
|
3534
3767
|
};
|
|
3535
3768
|
|
|
3536
|
-
// src/workflow/
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
return [...value].length;
|
|
3542
|
-
}
|
|
3543
|
-
function formatRule(resume) {
|
|
3544
|
-
if (resume.format === "pdf") {
|
|
3545
|
-
return "The resume came from searchable PDF text extraction. You cannot inspect its visual layout, typography, columns, spacing, or graphics. Return targeted section-level suggestions and replacement snippets for the user to apply in the styled source; never claim that PDF styling was preserved or inspected.";
|
|
3546
|
-
}
|
|
3547
|
-
if (resume.format === "markdown") {
|
|
3548
|
-
return "The resume includes Markdown structure. Preserve its existing heading hierarchy, list structure, ordering, and all unchanged wording in every proposed edit.";
|
|
3769
|
+
// src/workflow/detail-viewer.ts
|
|
3770
|
+
async function showDetailText(ctx, label, text) {
|
|
3771
|
+
if (ctx.mode !== "tui") {
|
|
3772
|
+
ctx.ui.notify(text, "info");
|
|
3773
|
+
return;
|
|
3549
3774
|
}
|
|
3550
|
-
|
|
3775
|
+
await ctx.ui.custom((tui, theme, keybindings, done) => new DetailViewer(
|
|
3776
|
+
label,
|
|
3777
|
+
text,
|
|
3778
|
+
theme,
|
|
3779
|
+
keybindings,
|
|
3780
|
+
Math.max(1, Math.min(20, tui.terminal.rows - 6)),
|
|
3781
|
+
() => tui.requestRender(),
|
|
3782
|
+
() => done(void 0)
|
|
3783
|
+
));
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
// src/managed/review-selector.ts
|
|
3787
|
+
var CONTINUE = "Continue with selected changes";
|
|
3788
|
+
var BACK_TO_REVIEW = "Back to reviewed changes";
|
|
3789
|
+
var CANCEL = "Cancel";
|
|
3790
|
+
var REPEAT_SELECTION = { done: false };
|
|
3791
|
+
function lineRange(change) {
|
|
3792
|
+
return change.start_line === change.end_line ? `line ${change.start_line}` : `lines ${change.start_line}-${change.end_line}`;
|
|
3793
|
+
}
|
|
3794
|
+
function exactChangeValue(change) {
|
|
3795
|
+
return {
|
|
3796
|
+
change_id: change.change_id,
|
|
3797
|
+
section: change.section,
|
|
3798
|
+
start_line: change.start_line,
|
|
3799
|
+
end_line: change.end_line,
|
|
3800
|
+
original_text: change.original_text,
|
|
3801
|
+
proposed_text: change.proposed_text,
|
|
3802
|
+
resume_evidence: change.resume_evidence,
|
|
3803
|
+
vacancy_evidence: change.vacancy_evidence
|
|
3804
|
+
};
|
|
3805
|
+
}
|
|
3806
|
+
function exactChangeText(change) {
|
|
3807
|
+
return JSON.stringify(exactChangeValue(change), null, 2);
|
|
3808
|
+
}
|
|
3809
|
+
function selectedChangeText(change, index, total) {
|
|
3810
|
+
return [
|
|
3811
|
+
`Change ${index + 1} of ${total}`,
|
|
3812
|
+
`ID: ${JSON.stringify(change.change_id)}`,
|
|
3813
|
+
`Section: ${JSON.stringify(change.section)}`,
|
|
3814
|
+
`Line bounds: ${change.start_line}-${change.end_line}`,
|
|
3815
|
+
`Before: ${JSON.stringify(change.original_text)}`,
|
|
3816
|
+
`After: ${JSON.stringify(change.proposed_text)}`,
|
|
3817
|
+
`Resume evidence (${change.resume_evidence.length}): ${JSON.stringify(change.resume_evidence)}`,
|
|
3818
|
+
`Vacancy evidence (${change.vacancy_evidence.length}): ${JSON.stringify(change.vacancy_evidence)}`
|
|
3819
|
+
].join("\n");
|
|
3820
|
+
}
|
|
3821
|
+
function selectedChangesText(review, selected) {
|
|
3822
|
+
return [
|
|
3823
|
+
`Authority: ${review.authority}`,
|
|
3824
|
+
`Selected changes: ${selected.length}`,
|
|
3825
|
+
"",
|
|
3826
|
+
selected.map((change, index) => selectedChangeText(change, index, selected.length)).join("\n\n")
|
|
3827
|
+
].join("\n");
|
|
3828
|
+
}
|
|
3829
|
+
function noticeText(review) {
|
|
3830
|
+
return JSON.stringify({
|
|
3831
|
+
authority: review.authority,
|
|
3832
|
+
warnings: review.warnings,
|
|
3833
|
+
discarded_changes: review.discarded_changes
|
|
3834
|
+
}, null, 2);
|
|
3835
|
+
}
|
|
3836
|
+
function changeOption(change, included) {
|
|
3837
|
+
return `${change.change_id} • ${change.section} • ${lineRange(change)} • ${included ? "included" : "excluded"}`;
|
|
3838
|
+
}
|
|
3839
|
+
function noticesOption(review, reviewed) {
|
|
3840
|
+
return `Warnings and discards • ${review.warnings.length} warnings • ${review.discarded_changes.length} discarded • ${reviewed ? "reviewed" : "review required"}`;
|
|
3841
|
+
}
|
|
3842
|
+
function changeOptions(review, included) {
|
|
3843
|
+
return new Map(review.changes.map((change) => [
|
|
3844
|
+
changeOption(change, included.has(change.change_id)),
|
|
3845
|
+
change
|
|
3846
|
+
]));
|
|
3847
|
+
}
|
|
3848
|
+
function selectedAction(selected, notices, byOption) {
|
|
3849
|
+
const fixed = /* @__PURE__ */ new Map([
|
|
3850
|
+
[void 0, { kind: "cancel" }],
|
|
3851
|
+
[CANCEL, { kind: "cancel" }],
|
|
3852
|
+
[notices, { kind: "notices" }],
|
|
3853
|
+
[CONTINUE, { kind: "continue" }]
|
|
3854
|
+
]);
|
|
3855
|
+
const change = byOption.get(selected ?? "");
|
|
3856
|
+
return fixed.get(selected) ?? (change === void 0 ? { kind: "cancel" } : { kind: "change", change });
|
|
3857
|
+
}
|
|
3858
|
+
async function nextReviewAction(ctx, review, state) {
|
|
3859
|
+
const notices = noticesOption(review, state.noticesReviewed);
|
|
3860
|
+
const byOption = changeOptions(review, state.included);
|
|
3861
|
+
const selected = await ctx.ui.select(
|
|
3862
|
+
`Career reviewed changes • assisted/non-authoritative • ${state.included.size} selected`,
|
|
3863
|
+
[notices, ...byOption.keys(), CONTINUE, CANCEL]
|
|
3864
|
+
);
|
|
3865
|
+
return selectedAction(selected, notices, byOption);
|
|
3866
|
+
}
|
|
3867
|
+
function selectionOptions(wasIncluded) {
|
|
3868
|
+
return wasIncluded ? ["Keep included", "Exclude", "Back without changing"] : ["Include", "Keep excluded", "Back without changing"];
|
|
3869
|
+
}
|
|
3870
|
+
async function exactSelection(ctx, change, included) {
|
|
3871
|
+
await showDetailText(
|
|
3872
|
+
ctx,
|
|
3873
|
+
`${change.change_id} • ${change.section} • ${lineRange(change)}`,
|
|
3874
|
+
exactChangeText(change)
|
|
3875
|
+
);
|
|
3876
|
+
const decision = await ctx.ui.select(
|
|
3877
|
+
`Explicit selection • ${change.change_id}`,
|
|
3878
|
+
selectionOptions(included.has(change.change_id))
|
|
3879
|
+
);
|
|
3880
|
+
const decisions = /* @__PURE__ */ new Map([
|
|
3881
|
+
["Include", () => included.add(change.change_id)],
|
|
3882
|
+
["Keep included", () => included.add(change.change_id)],
|
|
3883
|
+
["Exclude", () => included.delete(change.change_id)],
|
|
3884
|
+
["Keep excluded", () => included.delete(change.change_id)]
|
|
3885
|
+
]);
|
|
3886
|
+
decisions.get(decision)?.();
|
|
3887
|
+
}
|
|
3888
|
+
function completeSelection(ctx, review, state) {
|
|
3889
|
+
if (!state.noticesReviewed) {
|
|
3890
|
+
ctx.ui.notify("Review all Career Core warnings and discarded-change reasons before continuing.", "warning");
|
|
3891
|
+
return void 0;
|
|
3892
|
+
}
|
|
3893
|
+
if (state.included.size === 0) {
|
|
3894
|
+
ctx.ui.notify("Include at least one exact canonical change before continuing.", "warning");
|
|
3895
|
+
return void 0;
|
|
3896
|
+
}
|
|
3897
|
+
return review.changes.filter((change) => state.included.has(change.change_id));
|
|
3898
|
+
}
|
|
3899
|
+
function prepareOption(count) {
|
|
3900
|
+
return `Prepare ${count} selected change ID${count === 1 ? "" : "s"}`;
|
|
3901
|
+
}
|
|
3902
|
+
function finalChangeOption(change, index, total) {
|
|
3903
|
+
return `Review selected ${index + 1}/${total} • ${change.change_id} • ${change.section} • ${lineRange(change)}`;
|
|
3904
|
+
}
|
|
3905
|
+
function confirmationOutcome(decision, prepare, selected) {
|
|
3906
|
+
const outcomes = /* @__PURE__ */ new Map([
|
|
3907
|
+
[void 0, { done: true }],
|
|
3908
|
+
[CANCEL, { done: true }],
|
|
3909
|
+
[BACK_TO_REVIEW, REPEAT_SELECTION],
|
|
3910
|
+
[prepare, { done: true, selection: selected.map((change) => change.change_id) }]
|
|
3911
|
+
]);
|
|
3912
|
+
return outcomes.get(decision) ?? { done: true };
|
|
3913
|
+
}
|
|
3914
|
+
async function confirmSelection(ctx, selected) {
|
|
3915
|
+
const prepare = prepareOption(selected.length);
|
|
3916
|
+
const byOption = new Map(selected.map((change, index) => [
|
|
3917
|
+
finalChangeOption(change, index, selected.length),
|
|
3918
|
+
{ change, index }
|
|
3919
|
+
]));
|
|
3920
|
+
while (true) {
|
|
3921
|
+
const decision = await ctx.ui.select(
|
|
3922
|
+
"Final selection • inspect, prepare, or go back • nothing runs automatically",
|
|
3923
|
+
[...byOption.keys(), prepare, BACK_TO_REVIEW, CANCEL]
|
|
3924
|
+
);
|
|
3925
|
+
const target = byOption.get(decision ?? "");
|
|
3926
|
+
if (target === void 0) return confirmationOutcome(decision, prepare, selected);
|
|
3927
|
+
await showDetailText(
|
|
3928
|
+
ctx,
|
|
3929
|
+
`Selected ${target.index + 1}/${selected.length} • ${target.change.change_id}`,
|
|
3930
|
+
selectedChangeText(target.change, target.index, selected.length)
|
|
3931
|
+
);
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
async function continueSelection(ctx, review, state) {
|
|
3935
|
+
const selected = completeSelection(ctx, review, state);
|
|
3936
|
+
if (selected === void 0) return REPEAT_SELECTION;
|
|
3937
|
+
await showDetailText(
|
|
3938
|
+
ctx,
|
|
3939
|
+
`${selected.length} selected change${selected.length === 1 ? "" : "s"} • final review`,
|
|
3940
|
+
selectedChangesText(review, selected)
|
|
3941
|
+
);
|
|
3942
|
+
return await confirmSelection(ctx, selected);
|
|
3943
|
+
}
|
|
3944
|
+
async function reviewNotices(ctx, review, state) {
|
|
3945
|
+
await showDetailText(ctx, "Warnings and discarded changes", noticeText(review));
|
|
3946
|
+
state.noticesReviewed = true;
|
|
3947
|
+
return REPEAT_SELECTION;
|
|
3948
|
+
}
|
|
3949
|
+
async function reviewChange(ctx, state, change) {
|
|
3950
|
+
await exactSelection(ctx, change, state.included);
|
|
3951
|
+
return REPEAT_SELECTION;
|
|
3952
|
+
}
|
|
3953
|
+
async function applyAction(ctx, review, state, action) {
|
|
3954
|
+
const handlers = {
|
|
3955
|
+
cancel: async () => ({ done: true }),
|
|
3956
|
+
notices: async () => await reviewNotices(ctx, review, state),
|
|
3957
|
+
continue: async () => await continueSelection(ctx, review, state),
|
|
3958
|
+
change: async () => await reviewChange(
|
|
3959
|
+
ctx,
|
|
3960
|
+
state,
|
|
3961
|
+
action.change
|
|
3962
|
+
)
|
|
3963
|
+
};
|
|
3964
|
+
return await handlers[action.kind]();
|
|
3965
|
+
}
|
|
3966
|
+
function noticesRequired(review) {
|
|
3967
|
+
return [review.warnings.length > 0, review.discarded_changes.length > 0].some(Boolean);
|
|
3968
|
+
}
|
|
3969
|
+
function selectableReview(ctx, review) {
|
|
3970
|
+
return [ctx.mode === "tui", review.changes.length > 0].every(Boolean);
|
|
3971
|
+
}
|
|
3972
|
+
async function selectVariantChanges(ctx, review) {
|
|
3973
|
+
if (!selectableReview(ctx, review)) return void 0;
|
|
3974
|
+
const state = {
|
|
3975
|
+
included: /* @__PURE__ */ new Set(),
|
|
3976
|
+
noticesReviewed: !noticesRequired(review)
|
|
3977
|
+
};
|
|
3978
|
+
while (true) {
|
|
3979
|
+
const action = await nextReviewAction(ctx, review, state);
|
|
3980
|
+
const outcome = await applyAction(ctx, review, state, action);
|
|
3981
|
+
if (outcome.done) return outcome.selection;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
function materializeEditorText(reviewHandle, selectedChangeIds) {
|
|
3985
|
+
const request = {
|
|
3986
|
+
command: "materialize",
|
|
3987
|
+
handle: reviewHandle,
|
|
3988
|
+
payload: { selected_change_ids: selectedChangeIds }
|
|
3989
|
+
};
|
|
3990
|
+
return [
|
|
3991
|
+
"I explicitly reviewed and selected these canonical Career Core changes in /career-review.",
|
|
3992
|
+
"",
|
|
3993
|
+
`Call career_run with exactly this request: ${JSON.stringify(request)}`,
|
|
3994
|
+
"",
|
|
3995
|
+
"Use exactly these selected IDs and the unchanged review handle. Keep the result assisted/non-authoritative. Do not analyze or match it as an original, and do not save or write any file."
|
|
3996
|
+
].join("\n");
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
// src/workflow/variant-save.ts
|
|
4000
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
4001
|
+
import { constants as constants2 } from "node:fs";
|
|
4002
|
+
import {
|
|
4003
|
+
chmod as chmod2,
|
|
4004
|
+
link,
|
|
4005
|
+
lstat as lstat4,
|
|
4006
|
+
mkdir as mkdir2,
|
|
4007
|
+
open as open2,
|
|
4008
|
+
readFile as readFile4,
|
|
4009
|
+
readdir,
|
|
4010
|
+
realpath as realpath4,
|
|
4011
|
+
unlink
|
|
4012
|
+
} from "node:fs/promises";
|
|
4013
|
+
import path5 from "node:path";
|
|
4014
|
+
import { TextDecoder as TextDecoder5 } from "node:util";
|
|
4015
|
+
import {
|
|
4016
|
+
withFileMutationQueue
|
|
4017
|
+
} from "@earendil-works/pi-coding-agent";
|
|
4018
|
+
var ARTIFACT_MAX_BYTES = 262144;
|
|
4019
|
+
var PREVIEW_MAX_BYTES = 524288;
|
|
4020
|
+
var PATH_MAX_BYTES3 = 4096;
|
|
4021
|
+
var CONFIRM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
4022
|
+
var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
4023
|
+
var VARIANT_HANDLE = /^variant:[a-f0-9-]{8,64}$/;
|
|
4024
|
+
var SHA2564 = /^[a-f0-9]{64}$/;
|
|
4025
|
+
var CHANGE_ID = /^change-[0-9]{4}$/;
|
|
4026
|
+
var DEFAULT_FS = {
|
|
4027
|
+
chmod: chmod2,
|
|
4028
|
+
link,
|
|
4029
|
+
lstat: lstat4,
|
|
4030
|
+
mkdir: mkdir2,
|
|
4031
|
+
open: open2,
|
|
4032
|
+
readFile: readFile4,
|
|
4033
|
+
readdir,
|
|
4034
|
+
realpath: realpath4,
|
|
4035
|
+
unlink
|
|
4036
|
+
};
|
|
4037
|
+
function isNodeError(error, code) {
|
|
4038
|
+
return error !== null && typeof error === "object" && error.code === code;
|
|
4039
|
+
}
|
|
4040
|
+
function hash(value) {
|
|
4041
|
+
return createHash4("sha256").update(value).digest("hex");
|
|
4042
|
+
}
|
|
4043
|
+
function hasUnpairedSurrogate(value) {
|
|
4044
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
4045
|
+
const code = value.charCodeAt(index);
|
|
4046
|
+
if (code >= 55296 && code <= 56319) {
|
|
4047
|
+
const next = value.charCodeAt(index + 1);
|
|
4048
|
+
if (!(next >= 56320 && next <= 57343)) return true;
|
|
4049
|
+
index += 1;
|
|
4050
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
4051
|
+
return true;
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
return false;
|
|
4055
|
+
}
|
|
4056
|
+
function sameCandidate(left, right) {
|
|
4057
|
+
return left.handle === right.handle && left.assistedText === right.assistedText && left.selectedChangeIds.join("\0") === right.selectedChangeIds.join("\0") && left.source.resumeId === right.source.resumeId && left.source.rootId === right.source.rootId && left.source.format === right.source.format && left.source.textSha256 === right.source.textSha256;
|
|
4058
|
+
}
|
|
4059
|
+
function validCandidateIdentity(candidate) {
|
|
4060
|
+
return VARIANT_HANDLE.test(candidate.handle) && SHA2564.test(candidate.source.resumeId) && SHA2564.test(candidate.source.rootId) && SHA2564.test(candidate.source.textSha256) && (candidate.source.format === "markdown" || candidate.source.format === "text");
|
|
4061
|
+
}
|
|
4062
|
+
function validSelectedChanges(selectedChangeIds) {
|
|
4063
|
+
return selectedChangeIds.length > 0 && new Set(selectedChangeIds).size === selectedChangeIds.length && selectedChangeIds.every((id) => CHANGE_ID.test(id));
|
|
4064
|
+
}
|
|
4065
|
+
function validateCandidate(candidate) {
|
|
4066
|
+
if (!validCandidateIdentity(candidate) || !validSelectedChanges(candidate.selectedChangeIds) || candidate.assistedText.length === 0 || candidate.assistedText.includes("\r") || hasUnpairedSurrogate(candidate.assistedText)) {
|
|
4067
|
+
throw careerRunError("variant_save_unavailable");
|
|
4068
|
+
}
|
|
4069
|
+
const bytes = Buffer.from(candidate.assistedText, "utf8");
|
|
4070
|
+
if (bytes.length === 0 || bytes.length > ARTIFACT_MAX_BYTES) {
|
|
4071
|
+
throw careerRunError("variant_save_unavailable");
|
|
4072
|
+
}
|
|
4073
|
+
return bytes;
|
|
4074
|
+
}
|
|
4075
|
+
function effectiveUserId() {
|
|
4076
|
+
return process.geteuid?.() ?? process.getuid?.();
|
|
4077
|
+
}
|
|
4078
|
+
function privateMetadata(metadata, mode) {
|
|
4079
|
+
const userId = effectiveUserId();
|
|
4080
|
+
return userId !== void 0 && metadata.uid === userId && (metadata.mode & 511) === mode;
|
|
4081
|
+
}
|
|
4082
|
+
function directManagedRoot(config, root) {
|
|
4083
|
+
const configured = config.generated_variants_root === void 0 ? void 0 : path5.resolve(config.generated_variants_root);
|
|
4084
|
+
return configured !== void 0 && path5.dirname(configured) === root.path ? configured : path5.join(root.path, "variants");
|
|
4085
|
+
}
|
|
4086
|
+
function validBoundedPath(value) {
|
|
4087
|
+
return path5.isAbsolute(value) && path5.normalize(value) === value && Buffer.byteLength(value, "utf8") <= PATH_MAX_BYTES3 && !/[\u0000-\u001f\u007f]/.test(value);
|
|
4088
|
+
}
|
|
4089
|
+
function validDestination(config, root, directoryPath) {
|
|
4090
|
+
return validBoundedPath(root.path) && validBoundedPath(directoryPath) && path5.dirname(directoryPath) === root.path && directoryPath !== root.path && !config.library_roots.some((configuredRoot) => configuredRoot.path === directoryPath);
|
|
4091
|
+
}
|
|
4092
|
+
function basicTimestamp(createdAt) {
|
|
4093
|
+
return createdAt.replace(/[-:.]/g, "");
|
|
4094
|
+
}
|
|
4095
|
+
function canonicalJson(value) {
|
|
4096
|
+
return `${JSON.stringify(value, null, 2)}
|
|
4097
|
+
`;
|
|
4098
|
+
}
|
|
4099
|
+
function fileNameFor(candidate, createdAt, saveId) {
|
|
4100
|
+
const extension = candidate.source.format === "markdown" ? "md" : "txt";
|
|
4101
|
+
const suffix = saveId.replaceAll("-", "").slice(0, 8);
|
|
4102
|
+
return `resume-assisted-${basicTimestamp(createdAt)}-${suffix}.${extension}`;
|
|
4103
|
+
}
|
|
4104
|
+
var VariantSaveWorkflow = class {
|
|
4105
|
+
constructor(options) {
|
|
4106
|
+
this.options = options;
|
|
4107
|
+
this.fs = options.fs ?? DEFAULT_FS;
|
|
4108
|
+
}
|
|
4109
|
+
options;
|
|
4110
|
+
fs;
|
|
4111
|
+
receipts = /* @__PURE__ */ new Map();
|
|
4112
|
+
rootLocks = /* @__PURE__ */ new Map();
|
|
4113
|
+
clearReceipts() {
|
|
4114
|
+
this.receipts.clear();
|
|
4115
|
+
}
|
|
4116
|
+
async run(handle, ctx, resolveCandidate) {
|
|
4117
|
+
if (!ctx.isIdle()) throw careerRunError("variant_save_unavailable");
|
|
4118
|
+
const candidate = resolveCandidate();
|
|
4119
|
+
const existing = this.receipts.get(handle);
|
|
4120
|
+
if (existing !== void 0) {
|
|
4121
|
+
if (!sameCandidate(existing.plan.candidate, candidate) || existing.plan.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
4122
|
+
throw careerRunError("variant_save_unavailable");
|
|
4123
|
+
}
|
|
4124
|
+
if (!await this.verifyPublishedPair(existing.plan)) {
|
|
4125
|
+
throw careerRunError("variant_save_verification_failed");
|
|
4126
|
+
}
|
|
4127
|
+
await this.verifyRescan(existing.plan);
|
|
4128
|
+
return {
|
|
4129
|
+
status: "existing",
|
|
4130
|
+
artifactPath: existing.plan.artifactPath,
|
|
4131
|
+
sidecarPath: existing.plan.sidecarPath
|
|
4132
|
+
};
|
|
4133
|
+
}
|
|
4134
|
+
const plan = await this.preparePlan(candidate, ctx.sessionManager.getSessionId());
|
|
4135
|
+
const reviewed = await ctx.ui.editor("Review exact assisted-variant save plan", plan.previewText);
|
|
4136
|
+
if (reviewed === void 0) return { status: "cancelled" };
|
|
4137
|
+
if (reviewed !== plan.previewText) throw careerRunError("variant_save_preview_changed");
|
|
4138
|
+
const confirmed = await ctx.ui.confirm(
|
|
4139
|
+
"Save assisted resume variant?",
|
|
4140
|
+
[
|
|
4141
|
+
`Save ID: ${plan.saveId}`,
|
|
4142
|
+
`Directory: ${plan.directoryPath}`,
|
|
4143
|
+
`Artifact: ${path5.basename(plan.artifactPath)} (${plan.artifactBytes.length} bytes, ${hash(plan.artifactBytes)})`,
|
|
4144
|
+
`Sidecar: ${path5.basename(plan.sidecarPath)} (${plan.sidecarBytes.length} bytes, ${hash(plan.sidecarBytes)})`,
|
|
4145
|
+
...plan.markerBytes === void 0 ? [] : [
|
|
4146
|
+
`Create managed marker: ${MANAGED_VARIANTS_MARKER_NAME} (${plan.markerBytes.length} bytes, ${hash(plan.markerBytes)})`
|
|
4147
|
+
],
|
|
4148
|
+
"This is assisted/non-authoritative. Existing files will never be replaced."
|
|
4149
|
+
].join("\n"),
|
|
4150
|
+
{ timeout: CONFIRM_TIMEOUT_MS }
|
|
4151
|
+
);
|
|
4152
|
+
if (!confirmed || ctx.signal?.aborted) return { status: "cancelled" };
|
|
4153
|
+
const current = resolveCandidate();
|
|
4154
|
+
if (!sameCandidate(plan.candidate, current) || plan.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
4155
|
+
throw careerRunError("variant_save_unavailable");
|
|
4156
|
+
}
|
|
4157
|
+
const paths = [plan.artifactPath, plan.sidecarPath, ...plan.markerBytes === void 0 ? [] : [plan.markerPath]].sort();
|
|
4158
|
+
await this.withRootLock(plan.directoryPath, () => this.withMutationQueues(paths, async () => {
|
|
4159
|
+
const lockedCandidate = resolveCandidate();
|
|
4160
|
+
if (!sameCandidate(plan.candidate, lockedCandidate) || plan.sessionId !== ctx.sessionManager.getSessionId() || !ctx.isIdle() || ctx.signal?.aborted) {
|
|
4161
|
+
throw careerRunError("variant_save_unavailable");
|
|
4162
|
+
}
|
|
4163
|
+
await this.revalidatePlan(plan);
|
|
4164
|
+
await this.publishPlan(plan);
|
|
4165
|
+
}));
|
|
4166
|
+
this.receipts.set(handle, { plan });
|
|
4167
|
+
await this.verifyRescan(plan);
|
|
4168
|
+
return { status: "saved", artifactPath: plan.artifactPath, sidecarPath: plan.sidecarPath };
|
|
4169
|
+
}
|
|
4170
|
+
async currentOriginal(candidate) {
|
|
4171
|
+
const config = await loadConfig(this.options.agentDir);
|
|
4172
|
+
const scan = await scanLibrary(config);
|
|
4173
|
+
const root = config.library_roots.find((value) => value.id === candidate.source.rootId);
|
|
4174
|
+
const rootSummary = scan.roots.find((value) => value.root_id === candidate.source.rootId);
|
|
4175
|
+
const matches = eligibleOriginals(scan).filter((record) => record.id === candidate.source.resumeId && record.root_id === candidate.source.rootId && record.format === candidate.source.format && record.text_sha256 === candidate.source.textSha256);
|
|
4176
|
+
if (root === void 0 || rootSummary === void 0 || rootSummary.stale || rootSummary.capped || scan.total_capped || matches.length !== 1) throw careerRunError("variant_save_unavailable");
|
|
4177
|
+
const directoryPath = directManagedRoot(config, root);
|
|
4178
|
+
if (!validDestination(config, root, directoryPath)) {
|
|
4179
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4180
|
+
}
|
|
4181
|
+
const destination = await this.inspectDestination(directoryPath, root);
|
|
4182
|
+
return { config, root, record: matches[0], destination };
|
|
4183
|
+
}
|
|
4184
|
+
async inspectDestination(directoryPath, root) {
|
|
4185
|
+
const markerPath = path5.join(directoryPath, MANAGED_VARIANTS_MARKER_NAME);
|
|
4186
|
+
let metadata;
|
|
4187
|
+
try {
|
|
4188
|
+
metadata = await this.fs.lstat(directoryPath);
|
|
4189
|
+
} catch (error) {
|
|
4190
|
+
if (isNodeError(error, "ENOENT")) return { kind: "absent", directoryPath, markerPath };
|
|
4191
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4192
|
+
}
|
|
4193
|
+
const canonical = await this.fs.realpath(directoryPath).catch(() => void 0);
|
|
4194
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || canonical !== directoryPath || !privateMetadata(metadata, 448)) throw careerRunError("variant_save_destination_invalid");
|
|
4195
|
+
const entries = await this.fs.readdir(directoryPath).catch(() => void 0);
|
|
4196
|
+
if (entries === void 0) throw careerRunError("variant_save_destination_invalid");
|
|
4197
|
+
const markerState = await this.inspectMarker(markerPath, root.id);
|
|
4198
|
+
if (markerState === "valid") return { kind: "managed", directoryPath, markerPath };
|
|
4199
|
+
if (markerState === "invalid" || entries.length !== 0) {
|
|
4200
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4201
|
+
}
|
|
4202
|
+
return { kind: "empty", directoryPath, markerPath };
|
|
4203
|
+
}
|
|
4204
|
+
async inspectMarker(markerPath, expectedRootId) {
|
|
4205
|
+
try {
|
|
4206
|
+
const marker = await this.fs.lstat(markerPath);
|
|
4207
|
+
if (!marker.isFile() || marker.isSymbolicLink() || !privateMetadata(marker, 384) || marker.size <= 0 || marker.size > ASSISTED_SIDECAR_MAX_BYTES) return "invalid";
|
|
4208
|
+
const bytes = await this.fs.readFile(markerPath);
|
|
4209
|
+
if (bytes.length !== marker.size) return "invalid";
|
|
4210
|
+
const text = new TextDecoder5("utf-8", { fatal: true }).decode(bytes);
|
|
4211
|
+
return parseManagedVariantsMarker(text, expectedRootId) === void 0 ? "invalid" : "valid";
|
|
4212
|
+
} catch (error) {
|
|
4213
|
+
return isNodeError(error, "ENOENT") ? "absent" : "invalid";
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
async preparePlan(candidate, sessionId) {
|
|
4217
|
+
const artifactBytes = validateCandidate(candidate);
|
|
4218
|
+
const prepared = await this.currentOriginal(candidate);
|
|
4219
|
+
const saveId = this.options.uuid().toLowerCase();
|
|
4220
|
+
const createdAt = this.options.now().toISOString();
|
|
4221
|
+
if (!UUID3.test(saveId)) throw careerRunError("variant_save_unavailable");
|
|
4222
|
+
const fileName = fileNameFor(candidate, createdAt, saveId);
|
|
4223
|
+
const artifactPath = path5.join(prepared.destination.directoryPath, fileName);
|
|
4224
|
+
const sidecarPath2 = path5.join(
|
|
4225
|
+
prepared.destination.directoryPath,
|
|
4226
|
+
`${fileName.slice(0, -path5.extname(fileName).length)}.pi-career.json`
|
|
4227
|
+
);
|
|
4228
|
+
if (![prepared.destination.markerPath, artifactPath, sidecarPath2].every(validBoundedPath)) {
|
|
4229
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4230
|
+
}
|
|
4231
|
+
await this.requireAbsent(artifactPath);
|
|
4232
|
+
await this.requireAbsent(sidecarPath2);
|
|
4233
|
+
const sidecarBytes = encodeAssistedVariantMetadataV2({
|
|
4234
|
+
base_document_id: candidate.source.resumeId,
|
|
4235
|
+
base_text_sha256: candidate.source.textSha256,
|
|
4236
|
+
artifact_sha256: sha256Bytes(artifactBytes),
|
|
4237
|
+
created_at: createdAt
|
|
4238
|
+
});
|
|
4239
|
+
const markerBytes = prepared.destination.kind === "managed" ? void 0 : encodeManagedVariantsMarker(prepared.root.id, createdAt);
|
|
4240
|
+
if (sidecarBytes.length > ASSISTED_SIDECAR_MAX_BYTES || markerBytes !== void 0 && markerBytes.length > ASSISTED_SIDECAR_MAX_BYTES) {
|
|
4241
|
+
throw careerRunError("variant_save_unavailable");
|
|
4242
|
+
}
|
|
4243
|
+
const preview = {
|
|
4244
|
+
schema_version: "pi.career.variant_save_preview.v1",
|
|
4245
|
+
save_id: saveId,
|
|
4246
|
+
initialize_directory: markerBytes !== void 0,
|
|
4247
|
+
directory_path: prepared.destination.directoryPath,
|
|
4248
|
+
marker: markerBytes === void 0 ? null : {
|
|
4249
|
+
path: prepared.destination.markerPath,
|
|
4250
|
+
utf8_bytes: markerBytes.length,
|
|
4251
|
+
sha256: hash(markerBytes),
|
|
4252
|
+
text: markerBytes.toString("utf8")
|
|
4253
|
+
},
|
|
4254
|
+
artifact: {
|
|
4255
|
+
path: artifactPath,
|
|
4256
|
+
format: candidate.source.format,
|
|
4257
|
+
utf8_bytes: artifactBytes.length,
|
|
4258
|
+
sha256: hash(artifactBytes),
|
|
4259
|
+
text: candidate.assistedText
|
|
4260
|
+
},
|
|
4261
|
+
sidecar: {
|
|
4262
|
+
path: sidecarPath2,
|
|
4263
|
+
utf8_bytes: sidecarBytes.length,
|
|
4264
|
+
sha256: hash(sidecarBytes),
|
|
4265
|
+
text: sidecarBytes.toString("utf8")
|
|
4266
|
+
}
|
|
4267
|
+
};
|
|
4268
|
+
const previewText = canonicalJson(preview);
|
|
4269
|
+
if (Buffer.byteLength(previewText, "utf8") > PREVIEW_MAX_BYTES) {
|
|
4270
|
+
throw careerRunError("variant_save_unavailable");
|
|
4271
|
+
}
|
|
4272
|
+
return {
|
|
4273
|
+
saveId,
|
|
4274
|
+
sessionId,
|
|
4275
|
+
candidate,
|
|
4276
|
+
createdAt,
|
|
4277
|
+
directoryPath: prepared.destination.directoryPath,
|
|
4278
|
+
markerPath: prepared.destination.markerPath,
|
|
4279
|
+
artifactPath,
|
|
4280
|
+
sidecarPath: sidecarPath2,
|
|
4281
|
+
artifactBytes,
|
|
4282
|
+
sidecarBytes,
|
|
4283
|
+
...markerBytes === void 0 ? {} : { markerBytes },
|
|
4284
|
+
initialDestinationKind: prepared.destination.kind,
|
|
4285
|
+
previewText
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4288
|
+
async revalidatePlan(plan) {
|
|
4289
|
+
validateCandidate(plan.candidate);
|
|
4290
|
+
const prepared = await this.currentOriginal(plan.candidate);
|
|
4291
|
+
if (prepared.destination.directoryPath !== plan.directoryPath || prepared.destination.markerPath !== plan.markerPath || prepared.destination.kind !== plan.initialDestinationKind) throw careerRunError("variant_save_destination_invalid");
|
|
4292
|
+
await this.requireAbsent(plan.artifactPath);
|
|
4293
|
+
await this.requireAbsent(plan.sidecarPath);
|
|
4294
|
+
}
|
|
4295
|
+
async requireAbsent(file) {
|
|
4296
|
+
try {
|
|
4297
|
+
await this.fs.lstat(file);
|
|
4298
|
+
throw careerRunError("variant_save_collision");
|
|
4299
|
+
} catch (error) {
|
|
4300
|
+
if (error instanceof CareerRunError) throw error;
|
|
4301
|
+
if (!isNodeError(error, "ENOENT")) throw careerRunError("variant_save_destination_invalid");
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
async publishPlan(plan) {
|
|
4305
|
+
if (plan.markerBytes !== void 0) await this.initializeDirectory(plan);
|
|
4306
|
+
let sidecarTemp;
|
|
4307
|
+
let artifactTemp;
|
|
4308
|
+
let sidecarLinked = false;
|
|
4309
|
+
let artifactLinked = false;
|
|
4310
|
+
try {
|
|
4311
|
+
sidecarTemp = await this.writeTemp(plan, "sidecar", plan.sidecarBytes);
|
|
4312
|
+
artifactTemp = await this.writeTemp(plan, "artifact", plan.artifactBytes);
|
|
4313
|
+
await this.publishTemp(sidecarTemp, plan.sidecarPath);
|
|
4314
|
+
sidecarLinked = true;
|
|
4315
|
+
await this.publishTemp(artifactTemp, plan.artifactPath);
|
|
4316
|
+
artifactLinked = true;
|
|
4317
|
+
await this.safeUnlink(sidecarTemp.path);
|
|
4318
|
+
await this.safeUnlink(artifactTemp.path);
|
|
4319
|
+
await this.syncDirectory(plan.directoryPath);
|
|
4320
|
+
if (!await this.verifyPublishedPair(plan, sidecarTemp.metadata, artifactTemp.metadata)) {
|
|
4321
|
+
throw careerRunError("variant_save_status_unknown");
|
|
4322
|
+
}
|
|
4323
|
+
} catch (error) {
|
|
4324
|
+
if (sidecarTemp !== void 0) await this.safeUnlink(sidecarTemp.path);
|
|
4325
|
+
if (artifactTemp !== void 0) await this.safeUnlink(artifactTemp.path);
|
|
4326
|
+
if (sidecarTemp !== void 0 && artifactTemp !== void 0 && await this.verifyPublishedPair(plan, sidecarTemp.metadata, artifactTemp.metadata)) return;
|
|
4327
|
+
if (!artifactLinked && sidecarTemp !== void 0) {
|
|
4328
|
+
await this.unlinkIfIdentity(plan.sidecarPath, sidecarTemp.metadata);
|
|
4329
|
+
}
|
|
4330
|
+
this.throwPublicationFailure(error, sidecarLinked, artifactLinked);
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
throwPublicationFailure(error, sidecarLinked, artifactLinked) {
|
|
4334
|
+
if (error instanceof CareerRunError) throw error;
|
|
4335
|
+
if (isNodeError(error, "EEXIST")) throw careerRunError("variant_save_collision");
|
|
4336
|
+
throw careerRunError(sidecarLinked || artifactLinked ? "variant_save_status_unknown" : "variant_save_destination_invalid");
|
|
4337
|
+
}
|
|
4338
|
+
async initializeDirectory(plan) {
|
|
4339
|
+
if (plan.markerBytes === void 0) return;
|
|
4340
|
+
if (plan.initialDestinationKind === "absent") {
|
|
4341
|
+
try {
|
|
4342
|
+
await this.fs.mkdir(plan.directoryPath, { mode: 448, recursive: false });
|
|
4343
|
+
} catch (error) {
|
|
4344
|
+
if (isNodeError(error, "EEXIST")) throw careerRunError("variant_save_collision");
|
|
4345
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4346
|
+
}
|
|
4347
|
+
await this.fs.chmod(plan.directoryPath, 448).catch(() => {
|
|
4348
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4351
|
+
const directory = await this.fs.lstat(plan.directoryPath).catch(() => {
|
|
4352
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4353
|
+
});
|
|
4354
|
+
const canonical = await this.fs.realpath(plan.directoryPath).catch(() => {
|
|
4355
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4356
|
+
});
|
|
4357
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || canonical !== plan.directoryPath || !privateMetadata(directory, 448)) throw careerRunError("variant_save_destination_invalid");
|
|
4358
|
+
await this.requireAbsent(plan.markerPath);
|
|
4359
|
+
const markerTemp = await this.writeTemp(plan, "marker", plan.markerBytes);
|
|
4360
|
+
try {
|
|
4361
|
+
await this.publishTemp(markerTemp, plan.markerPath);
|
|
4362
|
+
await this.safeUnlink(markerTemp.path);
|
|
4363
|
+
await this.syncDirectory(plan.directoryPath);
|
|
4364
|
+
if (!await this.verifyExactFile(plan.markerPath, plan.markerBytes, markerTemp.metadata)) {
|
|
4365
|
+
throw careerRunError("variant_save_status_unknown");
|
|
4366
|
+
}
|
|
4367
|
+
} catch (error) {
|
|
4368
|
+
await this.safeUnlink(markerTemp.path);
|
|
4369
|
+
if (error instanceof CareerRunError) throw error;
|
|
4370
|
+
if (isNodeError(error, "EEXIST")) throw careerRunError("variant_save_collision");
|
|
4371
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
async writeTemp(plan, role, bytes) {
|
|
4375
|
+
const temporary = path5.join(plan.directoryPath, `.pi-career-${plan.saveId}-${role}.tmp`);
|
|
4376
|
+
let handle;
|
|
4377
|
+
try {
|
|
4378
|
+
handle = await this.fs.open(
|
|
4379
|
+
temporary,
|
|
4380
|
+
constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY | constants2.O_NOFOLLOW,
|
|
4381
|
+
384
|
|
4382
|
+
);
|
|
4383
|
+
await handle.writeFile(bytes);
|
|
4384
|
+
await handle.sync();
|
|
4385
|
+
await handle.chmod(384);
|
|
4386
|
+
const metadata = await handle.stat();
|
|
4387
|
+
await handle.close();
|
|
4388
|
+
handle = void 0;
|
|
4389
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || !privateMetadata(metadata, 384) || metadata.size !== bytes.length) {
|
|
4390
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4391
|
+
}
|
|
4392
|
+
const checked = await this.fs.readFile(temporary);
|
|
4393
|
+
if (!checked.equals(bytes)) throw careerRunError("variant_save_destination_invalid");
|
|
4394
|
+
return { path: temporary, metadata };
|
|
4395
|
+
} catch (error) {
|
|
4396
|
+
if (handle !== void 0) await handle.close().catch(() => void 0);
|
|
4397
|
+
await this.safeUnlink(temporary);
|
|
4398
|
+
if (error instanceof CareerRunError) throw error;
|
|
4399
|
+
if (isNodeError(error, "EEXIST")) throw careerRunError("variant_save_collision");
|
|
4400
|
+
throw careerRunError("variant_save_destination_invalid");
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
async publishTemp(temporary, finalPath) {
|
|
4404
|
+
await this.fs.link(temporary.path, finalPath);
|
|
4405
|
+
}
|
|
4406
|
+
async verifyExactFile(file, expected, identity) {
|
|
4407
|
+
try {
|
|
4408
|
+
const metadata = await this.fs.lstat(file);
|
|
4409
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || !privateMetadata(metadata, 384) || metadata.size !== expected.length || identity !== void 0 && (metadata.dev !== identity.dev || metadata.ino !== identity.ino)) return false;
|
|
4410
|
+
const bytes = await this.fs.readFile(file);
|
|
4411
|
+
return bytes.equals(expected);
|
|
4412
|
+
} catch {
|
|
4413
|
+
return false;
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
async verifyPublishedPair(plan, sidecarIdentity, artifactIdentity) {
|
|
4417
|
+
if (!await this.verifyExactFile(plan.sidecarPath, plan.sidecarBytes, sidecarIdentity) || !await this.verifyExactFile(plan.artifactPath, plan.artifactBytes, artifactIdentity)) return false;
|
|
4418
|
+
try {
|
|
4419
|
+
const text = new TextDecoder5("utf-8", { fatal: true }).decode(await this.fs.readFile(plan.sidecarPath));
|
|
4420
|
+
const parsed = parseAssistedVariantMetadata(text, plan.artifactBytes);
|
|
4421
|
+
return parsed?.baseDocumentId === plan.candidate.source.resumeId;
|
|
4422
|
+
} catch {
|
|
4423
|
+
return false;
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
async verifyRescan(plan) {
|
|
4427
|
+
const config = await loadConfig(this.options.agentDir);
|
|
4428
|
+
const scan = await scanLibrary(config);
|
|
4429
|
+
const root = scan.roots.find((value) => value.root_id === plan.candidate.source.rootId);
|
|
4430
|
+
const configuredRoot = config.library_roots.find((value) => value.id === plan.candidate.source.rootId);
|
|
4431
|
+
const relativePath = configuredRoot === void 0 ? void 0 : path5.relative(configuredRoot.path, plan.artifactPath).split(path5.sep).join("/");
|
|
4432
|
+
const records = scan.records.filter((record) => record.path === plan.artifactPath);
|
|
4433
|
+
const eligible = eligibleOriginals(scan).some((record) => record.path === plan.artifactPath);
|
|
4434
|
+
if (root === void 0 || configuredRoot === void 0 || relativePath === void 0 || root.stale || root.capped || scan.total_capped || records.length !== 1 || records[0].format !== plan.candidate.source.format || records[0].text !== plan.candidate.assistedText || records[0].kind !== "assisted_variant" || records[0].variant_group_id !== plan.candidate.source.resumeId || eligible || scan.warnings.some((warning) => warning.code === "invalid_assisted_sidecar" && warning.root_id === plan.candidate.source.rootId && warning.relative_path === relativePath)) throw careerRunError("variant_save_verification_failed");
|
|
4435
|
+
}
|
|
4436
|
+
async unlinkIfIdentity(file, identity) {
|
|
4437
|
+
try {
|
|
4438
|
+
const metadata = await this.fs.lstat(file);
|
|
4439
|
+
if (metadata.dev === identity.dev && metadata.ino === identity.ino) await this.fs.unlink(file);
|
|
4440
|
+
} catch {
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
async safeUnlink(file) {
|
|
4444
|
+
await this.fs.unlink(file).catch(() => void 0);
|
|
4445
|
+
}
|
|
4446
|
+
async syncDirectory(directory) {
|
|
4447
|
+
let handle;
|
|
4448
|
+
try {
|
|
4449
|
+
handle = await this.fs.open(directory, constants2.O_RDONLY);
|
|
4450
|
+
await handle.sync();
|
|
4451
|
+
await handle.close();
|
|
4452
|
+
} catch {
|
|
4453
|
+
if (handle !== void 0) await handle.close().catch(() => void 0);
|
|
4454
|
+
throw careerRunError("variant_save_status_unknown");
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
async withMutationQueues(paths, operation) {
|
|
4458
|
+
const run = (index) => index >= paths.length ? operation() : withFileMutationQueue(paths[index], () => run(index + 1));
|
|
4459
|
+
return run(0);
|
|
4460
|
+
}
|
|
4461
|
+
async withRootLock(root, operation) {
|
|
4462
|
+
const previous = this.rootLocks.get(root) ?? Promise.resolve();
|
|
4463
|
+
let release = () => void 0;
|
|
4464
|
+
const gate = new Promise((resolve) => {
|
|
4465
|
+
release = resolve;
|
|
4466
|
+
});
|
|
4467
|
+
const current = previous.then(() => gate);
|
|
4468
|
+
this.rootLocks.set(root, current);
|
|
4469
|
+
await previous;
|
|
4470
|
+
try {
|
|
4471
|
+
return await operation();
|
|
4472
|
+
} finally {
|
|
4473
|
+
release();
|
|
4474
|
+
if (this.rootLocks.get(root) === current) this.rootLocks.delete(root);
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
};
|
|
4478
|
+
|
|
4479
|
+
// src/managed/tool.ts
|
|
4480
|
+
var REVIEW_HANDLE_PATTERN = /^review:[a-f0-9-]{8,64}$/;
|
|
4481
|
+
var VARIANT_HANDLE_PATTERN = /^variant:[a-f0-9-]{8,64}$/;
|
|
4482
|
+
function managedToolActive(pi, includeRaw) {
|
|
4483
|
+
const current = pi.getActiveTools();
|
|
4484
|
+
const retained = current.filter((name) => !RAW_TOOL_NAMES.includes(name));
|
|
4485
|
+
const next = includeRaw ? [...retained, ...RAW_TOOL_NAMES] : retained;
|
|
4486
|
+
if (!next.includes(MANAGED_TOOL_NAME)) next.push(MANAGED_TOOL_NAME);
|
|
4487
|
+
pi.setActiveTools([...new Set(next)]);
|
|
4488
|
+
}
|
|
4489
|
+
function registerCareerRun(pi, options = {}) {
|
|
4490
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
4491
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
4492
|
+
const uuid = options.uuid ?? randomUUID2;
|
|
4493
|
+
const engine = new CareerRunEngine({
|
|
4494
|
+
pi,
|
|
4495
|
+
agentDir,
|
|
4496
|
+
invoke: options.invoke ?? invokeCareerCli,
|
|
4497
|
+
now,
|
|
4498
|
+
uuid
|
|
4499
|
+
});
|
|
4500
|
+
const variantSave = new VariantSaveWorkflow({ agentDir, now, uuid });
|
|
4501
|
+
pi.registerTool({
|
|
4502
|
+
name: MANAGED_TOOL_NAME,
|
|
4503
|
+
label: "Career",
|
|
4504
|
+
description: "Run managed local Career Core workflows with ephemeral handles and native payload objects instead of nested JSON strings.",
|
|
4505
|
+
promptGuidelines: [
|
|
4506
|
+
"Start with context. If consent is required, ask first; consent payload is `approve` or `decline`. Use returned handles; match/variant-review use the current vacancy implicitly.",
|
|
4507
|
+
"Proposal payloads use Core fields. Materialize payload is {selected_change_ids:[...]}; detail payload is {section,item?}. Preserve warnings/uncertainty/authority, and never select changes automatically.",
|
|
4508
|
+
"career_run variant-review must end its turn. For non-PDF originals in TUI, direct the user to /career-review with the returned review handle; only a later user-submitted turn may materialize explicitly selected IDs. PDF changes remain manual guidance only.",
|
|
4509
|
+
"After materialization, never initiate persistence. The user alone may run /career-save with the returned variant handle for exact local preview and confirmation."
|
|
4510
|
+
],
|
|
4511
|
+
parameters: careerRunParameters,
|
|
4512
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
4513
|
+
onUpdate?.({
|
|
4514
|
+
content: [{ type: "text", text: `Running career ${params.command}…` }],
|
|
4515
|
+
details: { schema_version: "pi.career.run_details.v1", command: params.command }
|
|
4516
|
+
});
|
|
4517
|
+
const result = await engine.run(params, signal, ctx);
|
|
4518
|
+
if (params.command === "consent" && params.payload === "decline") {
|
|
4519
|
+
variantSave.clearReceipts();
|
|
4520
|
+
}
|
|
4521
|
+
return params.command === "variant-review" ? { ...result, terminate: true } : result;
|
|
4522
|
+
},
|
|
4523
|
+
renderCall(args, theme) {
|
|
4524
|
+
return new Text2(
|
|
4525
|
+
theme.fg("toolTitle", theme.bold("career ")) + theme.fg("accent", args.command ?? "run"),
|
|
4526
|
+
0,
|
|
4527
|
+
0
|
|
4528
|
+
);
|
|
4529
|
+
},
|
|
4530
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
4531
|
+
if (isPartial) return new Text2(theme.fg("warning", "Running Career Core…"), 0, 0);
|
|
4532
|
+
const details = result.details;
|
|
4533
|
+
if (details === void 0) return new Text2(theme.fg("dim", "Career result unavailable"), 0, 0);
|
|
4534
|
+
const lines = [
|
|
4535
|
+
theme.fg(details.status === "consent_required" ? "warning" : "success", details.summary),
|
|
4536
|
+
...details.action === "review_select" && details.handle !== void 0 ? [theme.fg("accent", `Run /career-review ${details.handle}`)] : [],
|
|
4537
|
+
...details.action === "save_available" && details.handle !== void 0 ? [theme.fg("accent", `User may run /career-save ${details.handle}`)] : [],
|
|
4538
|
+
...expanded && details.handle !== void 0 && details.action !== "review_select" && details.action !== "save_available" ? [theme.fg("dim", details.handle)] : []
|
|
4539
|
+
];
|
|
4540
|
+
return new Text2(lines.join("\n"), 0, 0);
|
|
4541
|
+
}
|
|
4542
|
+
});
|
|
4543
|
+
pi.registerCommand("career-review", {
|
|
4544
|
+
description: "Review and explicitly select retained variant changes in TUI",
|
|
4545
|
+
handler: async (args, ctx) => {
|
|
4546
|
+
if (ctx.mode !== "tui") {
|
|
4547
|
+
ctx.ui.notify("/career-review requires TUI mode.", "error");
|
|
4548
|
+
return;
|
|
4549
|
+
}
|
|
4550
|
+
const handle = args.trim();
|
|
4551
|
+
if (!REVIEW_HANDLE_PATTERN.test(handle)) {
|
|
4552
|
+
ctx.ui.notify("Usage: /career-review review:<ephemeral-handle>", "warning");
|
|
4553
|
+
return;
|
|
4554
|
+
}
|
|
4555
|
+
try {
|
|
4556
|
+
await ctx.waitForIdle();
|
|
4557
|
+
const review = engine.variantSelectionReview(handle, ctx);
|
|
4558
|
+
if (review.changes.length === 0) {
|
|
4559
|
+
ctx.ui.notify("This review has no retained changes to select.", "warning");
|
|
4560
|
+
return;
|
|
4561
|
+
}
|
|
4562
|
+
const selected = await selectVariantChanges(ctx, review);
|
|
4563
|
+
if (selected === void 0) return;
|
|
4564
|
+
ctx.ui.setEditorText(materializeEditorText(review.handle, selected));
|
|
4565
|
+
ctx.ui.notify(
|
|
4566
|
+
`${selected.length} reviewed change ID${selected.length === 1 ? "" : "s"} prepared in the editor. Review and submit manually; nothing was materialized, sent, saved, or written.`,
|
|
4567
|
+
"info"
|
|
4568
|
+
);
|
|
4569
|
+
} catch (error) {
|
|
4570
|
+
if (error instanceof CareerRunError) {
|
|
4571
|
+
ctx.ui.notify(careerRunErrorMessage(error.code), "error");
|
|
4572
|
+
return;
|
|
4573
|
+
}
|
|
4574
|
+
ctx.ui.notify("The reviewed-change selector failed without persisting a selection.", "error");
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
});
|
|
4578
|
+
pi.registerCommand("career-save", {
|
|
4579
|
+
description: "Preview and explicitly save one current assisted Markdown/text materialization",
|
|
4580
|
+
handler: async (args, ctx) => {
|
|
4581
|
+
if (ctx.mode !== "tui" && ctx.mode !== "rpc") {
|
|
4582
|
+
ctx.ui.notify("/career-save requires TUI or RPC mode.", "error");
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
if (!ctx.isIdle()) {
|
|
4586
|
+
ctx.ui.notify("Wait for the current agent run to settle before saving.", "warning");
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
const handle = args.trim();
|
|
4590
|
+
if (!VARIANT_HANDLE_PATTERN.test(handle)) {
|
|
4591
|
+
ctx.ui.notify("Usage: /career-save variant:<ephemeral-handle>", "warning");
|
|
4592
|
+
return;
|
|
4593
|
+
}
|
|
4594
|
+
try {
|
|
4595
|
+
const outcome = await variantSave.run(
|
|
4596
|
+
handle,
|
|
4597
|
+
ctx,
|
|
4598
|
+
() => engine.materializedVariantForSave(handle, ctx)
|
|
4599
|
+
);
|
|
4600
|
+
if (outcome.status === "cancelled") {
|
|
4601
|
+
ctx.ui.notify("Assisted-variant save cancelled; no file was written.", "info");
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
ctx.ui.notify(
|
|
4605
|
+
`${outcome.status === "existing" ? "Verified existing" : "Saved"} assisted variant: ${outcome.artifactPath}
|
|
4606
|
+
Sidecar: ${outcome.sidecarPath}`,
|
|
4607
|
+
"info"
|
|
4608
|
+
);
|
|
4609
|
+
} catch (error) {
|
|
4610
|
+
if (error instanceof CareerRunError) {
|
|
4611
|
+
ctx.ui.notify(careerRunErrorMessage(error.code), "error");
|
|
4612
|
+
return;
|
|
4613
|
+
}
|
|
4614
|
+
ctx.ui.notify("The assisted variant could not be saved or verified.", "error");
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
});
|
|
4618
|
+
pi.registerCommand("career-tools", {
|
|
4619
|
+
description: "Choose managed or advanced raw Career Core tools",
|
|
4620
|
+
getArgumentCompletions: (prefix) => ["managed", "raw", "status"].filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value })),
|
|
4621
|
+
handler: async (args, ctx) => {
|
|
4622
|
+
const mode = args.trim();
|
|
4623
|
+
if (mode === "managed") managedToolActive(pi, false);
|
|
4624
|
+
else if (mode === "raw") managedToolActive(pi, true);
|
|
4625
|
+
else if (mode !== "status" && mode !== "") {
|
|
4626
|
+
ctx.ui.notify("Usage: /career-tools managed|raw|status", "warning");
|
|
4627
|
+
return;
|
|
4628
|
+
}
|
|
4629
|
+
const activeRaw = RAW_TOOL_NAMES.filter((name) => pi.getActiveTools().includes(name));
|
|
4630
|
+
ctx.ui.notify(
|
|
4631
|
+
`Career tools: career_run active; raw Career Core tools ${activeRaw.length === 0 ? "inactive" : "active"}.`,
|
|
4632
|
+
"info"
|
|
4633
|
+
);
|
|
4634
|
+
}
|
|
4635
|
+
});
|
|
4636
|
+
pi.on("session_start", (_event, ctx) => {
|
|
4637
|
+
variantSave.clearReceipts();
|
|
4638
|
+
engine.enterSession(ctx.sessionManager.getSessionId());
|
|
4639
|
+
managedToolActive(pi, false);
|
|
4640
|
+
});
|
|
4641
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
4642
|
+
variantSave.clearReceipts();
|
|
4643
|
+
engine.resetSession(ctx.sessionManager.getSessionId());
|
|
4644
|
+
});
|
|
4645
|
+
pi.on("session_shutdown", () => {
|
|
4646
|
+
variantSave.clearReceipts();
|
|
4647
|
+
engine.shutdown();
|
|
4648
|
+
});
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
// src/workflow/commands.ts
|
|
4652
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
4653
|
+
import {
|
|
4654
|
+
BorderedLoader,
|
|
4655
|
+
getAgentDir as getAgentDir2
|
|
4656
|
+
} from "@earendil-works/pi-coding-agent";
|
|
4657
|
+
|
|
4658
|
+
// src/workflow/workbench.ts
|
|
4659
|
+
var WORKBENCH_MAX_SOURCE_CHARACTERS = 8e4;
|
|
4660
|
+
var WORKBENCH_MAX_PROMPT_BYTES = 262144;
|
|
4661
|
+
var WORKBENCH_MAX_QUESTION_CHARACTERS = 4e3;
|
|
4662
|
+
function characterCount(value) {
|
|
4663
|
+
return [...value].length;
|
|
4664
|
+
}
|
|
4665
|
+
function formatRule(resume) {
|
|
4666
|
+
if (resume.format === "pdf") {
|
|
4667
|
+
return "The resume came from searchable PDF text extraction. You cannot inspect its visual layout, typography, columns, spacing, or graphics. Return targeted section-level suggestions and replacement snippets for the user to apply in the styled source; never claim that PDF styling was preserved or inspected.";
|
|
4668
|
+
}
|
|
4669
|
+
if (resume.format === "markdown") {
|
|
4670
|
+
return "The resume includes Markdown structure. Preserve its existing heading hierarchy, list structure, ordering, and all unchanged wording in every proposed edit.";
|
|
4671
|
+
}
|
|
4672
|
+
return "The resume is plain text. Preserve its existing section order, line structure, and all unchanged wording in every proposed edit.";
|
|
3551
4673
|
}
|
|
3552
4674
|
function workflowProtocol(mode, resume) {
|
|
3553
4675
|
switch (mode) {
|
|
@@ -3578,7 +4700,7 @@ function workflowProtocol(mode, resume) {
|
|
|
3578
4700
|
}
|
|
3579
4701
|
return `${review}
|
|
3580
4702
|
4. Ask the user to select retained IDs, then stop; never select or materialize now.
|
|
3581
|
-
5. Only after later selection, call career_run "materialize" with the returned review handle and selected IDs. Keep it assisted/non-authoritative
|
|
4703
|
+
5. Only after later selection, call career_run "materialize" with the returned review handle and selected IDs. Keep it assisted/non-authoritative and never analyze or match it as original. Do not initiate persistence; only tell the user they may run the returned \`/career-save <variant-handle>\` command for a separate exact local preview and confirmation.`;
|
|
3582
4704
|
}
|
|
3583
4705
|
case "question":
|
|
3584
4706
|
return `1. Answer from the original; use complete analysis or matching when relevant.
|
|
@@ -3866,20 +4988,7 @@ async function showDetail(ctx, sections) {
|
|
|
3866
4988
|
const choice = await ctx.ui.select("Career detail", [...sections.map((section2) => section2.label), "Close"]);
|
|
3867
4989
|
const section = sections.find((candidate) => candidate.label === choice);
|
|
3868
4990
|
if (section === void 0) return;
|
|
3869
|
-
|
|
3870
|
-
if (ctx.mode !== "tui") {
|
|
3871
|
-
ctx.ui.notify(text, "info");
|
|
3872
|
-
return;
|
|
3873
|
-
}
|
|
3874
|
-
await ctx.ui.custom((tui, theme, keybindings, done) => new DetailViewer(
|
|
3875
|
-
section.label,
|
|
3876
|
-
text,
|
|
3877
|
-
theme,
|
|
3878
|
-
keybindings,
|
|
3879
|
-
Math.max(1, Math.min(20, tui.terminal.rows - 6)),
|
|
3880
|
-
() => tui.requestRender(),
|
|
3881
|
-
() => done(void 0)
|
|
3882
|
-
));
|
|
4991
|
+
await showDetailText(ctx, section.label, detailText(section));
|
|
3883
4992
|
}
|
|
3884
4993
|
function registerCareerCommands(pi, options = {}) {
|
|
3885
4994
|
const dependencies = {
|
|
@@ -4552,6 +5661,7 @@ function resultContent(json, operation) {
|
|
|
4552
5661
|
};
|
|
4553
5662
|
}
|
|
4554
5663
|
function careerCoreExtension(pi) {
|
|
5664
|
+
assertSupportedPlatform();
|
|
4555
5665
|
registerCareerCommands(pi);
|
|
4556
5666
|
pi.registerTool({
|
|
4557
5667
|
name: "career_core_discover",
|