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/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.1.1 or configure a compatible local career executable.",
19
- runtime_acquisition_failed: "Automatic acquisition of @revazi/career@0.1.1 failed. Install that exact package or configure a compatible local career executable.",
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.1.1";
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.1.1";
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
- "bin/career.js",
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
- for (const name of executableNames()) {
368
- const candidate = path.join(directory, name);
369
- try {
370
- const metadata = await lstat(candidate);
371
- if (!metadata.isFile() && !metadata.isSymbolicLink()) continue;
372
- await access(candidate, fsConstants.X_OK);
373
- return route(candidate, [], "path");
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 || process.platform === "win32" || (metadata.mode & 73) !== 0);
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 === "bin/career.js" && exactStringArray(manifest.files, CAREER_LAUNCHER_FILES) && exactKeys2(optionalDependencies, CAREER_PLATFORM_PACKAGES) && CAREER_PLATFORM_PACKAGES.every(
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() || process.platform !== "win32" && (nodeMetadata.mode & 73) === 0) return void 0;
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), "bin", "career.js");
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() || process.platform !== "win32" && (nodeMetadata.mode & 73) === 0) throw new Error("invalid node executable");
486
- const derivedNpmCliPath = process.platform === "win32" ? path.join(path.dirname(realNodePath), "node_modules", "npm", "bin", "npm-cli.js") : path.join(
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 (!samePath(realNpmCliPath, derivedNpmCliPath)) throw new Error("invalid npm path");
462
+ if (realNpmCliPath !== derivedNpmCliPath) throw new Error("invalid npm path");
496
463
  const npmMetadata = await lstat(realNpmCliPath);
497
- if (!npmMetadata.isFile() || process.platform !== "win32" && (npmMetadata.mode & 73) === 0) throw new Error("invalid npm executable");
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 (!samePath(await realpath(suppliedPath), realNpmCliPath)) {
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" && !(process.platform === "win32" && windowsControlled.has(key.toLowerCase())) && !FORBIDDEN_NPM_ENVIRONMENT.has(key.toLowerCase().replaceAll("-", "_"))
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
- if (process.platform === "win32") {
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
- locatorNode,
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.normalize(value);
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 createHash2 } from "node:crypto";
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 createHash2("sha256").update(value).digest("hex");
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 validSidecar(value) {
1539
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1540
- const record = value;
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 > SIDECAR_MAX_BYTES;
1554
- if (invalidMetadata) return { kind: "original", invalid: true };
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: "original", invalid: true };
1713
+ if (bytes.length !== metadata.size) return { kind: "quarantined" };
1557
1714
  const text = new TextDecoder4("utf-8", { fatal: true }).decode(bytes);
1558
- const parsed = JSON.parse(text);
1559
- if (!validSidecar(parsed)) return { kind: "original", invalid: true };
1560
- return { kind: "assisted_variant", variantGroupId: parsed.base_document_id, invalid: false };
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") return { kind: "original", invalid: false };
1563
- return { kind: "original", invalid: true };
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 sidecar = await readSidecar(canonical);
1694
- if (sidecar.invalid) {
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 isRecord3(value) {
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 (!isRecord3(found)) throw workflowError("core_result_invalid");
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 (!isRecord3(candidate)) return [];
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 (!isRecord3(value) || typeof value.label !== "string" || typeof value.score !== "number") {
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 (!isRecord3(value)) throw workflowError("core_result_invalid");
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) => isRecord3(check) && check.score_adjusted === true);
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) => isRecord3(category) && category.score_adjusted === true);
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 ISO_UTC2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
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 isRecord4(value) {
2134
+ function isRecord5(value) {
1938
2135
  return value !== null && typeof value === "object" && !Array.isArray(value);
1939
2136
  }
1940
- function exactKeys4(value, required, optional = []) {
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" && ISO_UTC2.test(value.created_at) && Number.isFinite(Date.parse(value.created_at));
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 (!isRecord4(value) || !exactKeys4(value, ["adjusted", "provisional", "close_cluster", "stale"])) {
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 (!isRecord4(value) || !exactKeys4(value, ["schema_version", "core_schema_version", "summary", "ui_flags"])) {
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" || !isRecord4(value.summary) || !validFlags(value.ui_flags)) return false;
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 (!exactKeys4(value, [
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 exactKeys4(value, keys) && isUuid(value.clears_state_id) ? value : void 0;
2188
+ return exactKeys5(value, keys) && isUuid(value.clears_state_id) ? value : void 0;
1992
2189
  }
1993
2190
  function parseVacancy(value) {
1994
- if (!exactKeys4(value, [
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 exactKeys4(value, keys) && isUuid(value.clears_state_id) ? value : void 0;
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 exactKeys4(value, keys) && value.scope === "session_persistence" && typeof value.granted === "boolean" ? value : void 0;
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 exactKeys4(value, keys) && value.scope === "session_persistence" && isUuid(value.clears_state_id) ? value : void 0;
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 isRecord4(value) && exactKeys4(value, ["resume_text_sha256", "vacancy_text_sha256"]) && isSha256(value.resume_text_sha256) && isSha256(value.vacancy_text_sha256);
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 (!exactKeys4(value, [
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 (!isRecord4(value) || !validBase(value)) return void 0;
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 isRecord5(value) {
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 (!isRecord5(value) || !hasExactKeys(value, [
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 (!isRecord5(value) || !hasExactKeys(value, [
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 (!isRecord5(value) || !hasExactKeys(value, [
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 (!isRecord5(payload) || !hasExactKeys(payload, ["changes"]) || !Array.isArray(payload.changes) || payload.changes.length > 50) {
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 (!isRecord5(payload) || !hasExactKeys(payload, ["suggestions"]) || !Array.isArray(payload.suggestions) || payload.suggestions.length > 3) {
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 (!isRecord5(payload) || !hasExactKeys(payload, ["replacements"]) || !Array.isArray(payload.replacements) || payload.replacements.length > 3) {
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 isRecord6(value) {
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 (!isRecord6(payload) || Object.keys(payload).join("") !== "selected_change_ids") {
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 (!isRecord6(payload)) throw careerRunError("invalid_request");
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) => isRecord6(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 = isRecord6(value.baseline_analysis) ? value.baseline_analysis : value;
2682
- const checks = isRecord6(analysis) && Array.isArray(analysis.checks) ? analysis.checks : [];
2683
- return checks.flatMap((check) => isRecord6(check) && Array.isArray(check.evidence) ? [{ check_id: check.check_id, evidence: check.evidence }] : []);
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 = isRecord6(value.baseline_analysis) ? value.baseline_analysis : value;
2695
- return isRecord6(analysis) && Array.isArray(analysis.checks) ? analysis.checks : [];
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) => isRecord6(candidate) && (candidate.change_id === item || candidate.suggestion_id === item || candidate.replacement_id === item));
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", { review: entry.handle, ...summary }, {
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
- summary: `${retainedChangeIds.length} retained change(s)`
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) => isRecord6(change) && typeof change.change_id === "string" ? [change.change_id] : []
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", { variant: entry.handle, ...summary }, {
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/workbench.ts
3537
- var WORKBENCH_MAX_SOURCE_CHARACTERS = 8e4;
3538
- var WORKBENCH_MAX_PROMPT_BYTES = 262144;
3539
- var WORKBENCH_MAX_QUESTION_CHARACTERS = 4e3;
3540
- function characterCount(value) {
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
- return "The resume is plain text. Preserve its existing section order, line structure, and all unchanged wording in every proposed edit.";
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; never analyze, match, or save it as original.`;
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
- const text = detailText(section);
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",