agentlas 0.7.0 → 0.9.2

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +161 -18
  3. package/bin/agentlas.cjs +8 -8
  4. package/engine/agentlas-core-harness.cjs +212 -0
  5. package/engine/agentlas-desktop-loadout.cjs +527 -0
  6. package/engine/agentlas-doctor.cjs +1 -1
  7. package/engine/agentlas-experience-exchange.cjs +835 -85
  8. package/engine/agentlas-experience-intake.cjs +444 -0
  9. package/engine/agentlas-experience-mcp.cjs +580 -18
  10. package/engine/agentlas-i18n.cjs +10 -10
  11. package/engine/agentlas-input.cjs +5 -4
  12. package/engine/agentlas-mcp-env.cjs +219 -0
  13. package/engine/agentlas-mcp-wrapper.cjs +51 -0
  14. package/engine/agentlas-memory-governance.cjs +1029 -0
  15. package/engine/agentlas-native-host.cjs +129 -39
  16. package/engine/agentlas-parity.cjs +339 -154
  17. package/engine/agentlas-repl.cjs +306 -31
  18. package/engine/agentlas-workforce.cjs +2991 -0
  19. package/engine/agentlas-workload-routing.cjs +523 -0
  20. package/engine/agentlas.cjs +1619 -234
  21. package/engine/bootstrap-schema.sql +1 -1
  22. package/engine/experience-taxonomy-v1.json +49 -0
  23. package/package.json +8 -4
  24. package/scripts/gen-bootstrap-schema.sh +0 -23
  25. package/test/bootstrap-race.cjs +0 -47
  26. package/test/capture-runtime-guard.cjs +0 -122
  27. package/test/cloud-asset-restore.cjs +0 -423
  28. package/test/cloud-cas-client.cjs +0 -333
  29. package/test/cloud-owner-restore.cjs +0 -183
  30. package/test/cloud-runtime-paths.cjs +0 -40
  31. package/test/cloud-save-publish.cjs +0 -487
  32. package/test/credential-env-regression.cjs +0 -52
  33. package/test/engine-hardening-regression.cjs +0 -74
  34. package/test/experience-exchange-contract.cjs +0 -569
  35. package/test/experience-mcp-contract.cjs +0 -391
  36. package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
  37. package/test/login-loopback-security.cjs +0 -115
  38. package/test/mcp-config-isolation.cjs +0 -36
  39. package/test/permission-mapping.cjs +0 -180
  40. package/test/route-regression.cjs +0 -357
  41. package/test/run-api-regression.cjs +0 -322
  42. package/test/runtime-env-protection.cjs +0 -89
  43. package/test/semver-precedence.cjs +0 -39
  44. package/test/smoke.sh +0 -93
  45. package/test/sqlite-driver-probe.cjs +0 -22
  46. package/test/terminal-ui-regression.cjs +0 -477
  47. package/test/timeout-regression.cjs +0 -218
  48. package/test/tool-workspace-boundary.cjs +0 -165
  49. package/test/update-safety.cjs +0 -376
@@ -30,6 +30,64 @@ const EXPERIENCE_RETRIEVAL_MAX_ITEMS = 8;
30
30
  const EXPERIENCE_RETRIEVAL_MAX_TOKENS = 800;
31
31
  const EXCHANGE_LOCK_STALE_MS = 30_000;
32
32
  const EXCHANGE_LOCK_WAIT_MS = 2_000;
33
+ const EXPERIENCE_TAXONOMY_PATH = path.join(__dirname, "experience-taxonomy-v1.json");
34
+ const EXPERIENCE_TAXONOMY_CHECKSUM = "sha256:413833472e423352518f9591cd0e051c5bc0a7971e53ab3dc7b5aaf7d50c37ab";
35
+
36
+ function deepFreeze(value) {
37
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
38
+ Object.freeze(value);
39
+ for (const child of Object.values(value)) deepFreeze(child);
40
+ return value;
41
+ }
42
+
43
+ function validateExperienceTaxonomyContract(value) {
44
+ const issues = [];
45
+ if (!value || typeof value !== "object" || Array.isArray(value)) issues.push("taxonomy must be an object");
46
+ else {
47
+ if (value.schema !== "agentlas.experience-taxonomy.v1") issues.push("taxonomy schema drifted");
48
+ if (value.kind !== "agentlas-experience-taxonomy") issues.push("taxonomy kind drifted");
49
+ if (value.taskSignaturePrefix !== "agentlas.task.v1/") issues.push("taxonomy task prefix drifted");
50
+ if (!Array.isArray(value.taskSlugs) || value.taskSlugs.length !== 23 || value.taskSlugs.includes("general")) issues.push("taxonomy task catalog drifted");
51
+ const environment = value.environment;
52
+ if (
53
+ !environment || environment.osPrefix !== "agentlas.env.v1/os/" ||
54
+ environment.archPrefix !== "agentlas.env.v1/arch/" || environment.runtimePrefix !== "agentlas.env.v1/runtime/" ||
55
+ JSON.stringify(environment.osValues) !== JSON.stringify(["macos", "windows", "linux", "ios", "android", "unknown"]) ||
56
+ JSON.stringify(environment.archValues) !== JSON.stringify(["arm64", "x64", "unknown"]) ||
57
+ environment.runtimePattern !== "^[a-z0-9][a-z0-9._-]{1,63}$" ||
58
+ environment.matching !== "all-canonical-constraints-must-match" ||
59
+ environment.unknownConstraint !== "item-ineligible-base-unaffected"
60
+ ) issues.push("taxonomy environment contract drifted");
61
+ const normalization = value.normalization;
62
+ if (
63
+ !normalization || normalization.unicode !== "NFKC" || normalization.trim !== true || normalization.case !== "lower" ||
64
+ normalization.portableSource !== "canonical-id-only" || normalization.runtimeProfile !== "canonical-id-or-exact-bare-slug" ||
65
+ normalization.fuzzySimilarity !== false || normalization.generalAutoMatch !== false
66
+ ) issues.push("taxonomy normalization contract drifted");
67
+ const checksum = `sha256:${crypto.createHash("sha256").update(canonicalJson(value), "utf8").digest("hex")}`;
68
+ if (checksum !== EXPERIENCE_TAXONOMY_CHECKSUM) issues.push("taxonomy checksum drifted");
69
+ }
70
+ if (issues.length) {
71
+ const error = new Error(issues.join("; "));
72
+ error.code = "experience_taxonomy_drift";
73
+ error.issues = issues;
74
+ throw error;
75
+ }
76
+ return deepFreeze(JSON.parse(canonicalJson(value)));
77
+ }
78
+
79
+ function loadExperienceTaxonomyContract() {
80
+ const stat = fs.lstatSync(EXPERIENCE_TAXONOMY_PATH);
81
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024) throw new Error("Experience taxonomy artifact is unsafe");
82
+ return validateExperienceTaxonomyContract(JSON.parse(fs.readFileSync(EXPERIENCE_TAXONOMY_PATH, "utf8")));
83
+ }
84
+
85
+ const OFFICIAL_EXPERIENCE_CLOUD_HOSTS = new Set([
86
+ "agentlas.cloud",
87
+ "www.agentlas.cloud",
88
+ "api.agentlas.cloud",
89
+ "staging.agentlas.cloud",
90
+ ]);
33
91
 
34
92
  const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/;
35
93
  const HASH_RE = /^sha256:[0-9a-f]{64}$/;
@@ -54,9 +112,7 @@ const PII_PATTERNS = [
54
112
  /\b(?:account|customer|client|tenant|workspace|user)[ _-]?(?:id|key|number|no)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}\b|(?:계정|고객|사용자)[ _-]?(?:id|아이디|번호)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}/i,
55
113
  ];
56
114
  const LOCAL_PATH_PATTERNS = [
57
- /(?:file:\/\/|\/(?:Users|home|private|Volumes|var\/folders)\/)/i,
58
- /\b[A-Za-z]:\\(?:Users|Documents|Desktop|Downloads)\\/i,
59
- /(?:^|[\s'"`])~\/(?:Desktop|Documents|Downloads|Library|\.ssh|\.config)\//i,
115
+ /(?:file:\/\/|(?:^|[\s"'`()\[\]{}=:,;])(?:\.\.[/\\]|~[/\\]|\/(?!\/|\s)(?:[^/\s"'`<>]+\/)*[^/\s"'`<>]+|[A-Za-z]:[/\\]\S+|\\\\[^\\/\s]+[\\/][^\\/\s]+))/i,
60
116
  ];
61
117
  const RAW_INTERACTION_PATTERNS = [
62
118
  /(?:^|\n)\s*(?:system|assistant|user|tool|customer|agent)\s*:\s+/i,
@@ -77,6 +133,8 @@ const BASE_PACKAGE_PATTERNS = [
77
133
  /\bBEGIN AGENTLAS (?:AGENT|PACKAGE)\b/i,
78
134
  ];
79
135
  const OPAQUE_BLOB_RE = /(?:[A-Fa-f0-9]{128,}|[A-Za-z0-9+/]{124,}={0,2})/;
136
+ const PUBLIC_URL_RE = /\bhttps?:\/\/[^\s<>"']+/i;
137
+ const CUSTOMER_DATA_RE = /\b(?:customer|client|tenant|account|workspace|order|invoice)[ _-]?(?:name|email|address|phone|id|number|ref(?:erence)?)\s*[:=#]\s*\S+|(?:고객|클라이언트|계정|주문|송장)[ _-]?(?:이름|이메일|주소|전화|아이디|번호|참조)\s*[:=#]\s*\S+/i;
80
138
  const FORBIDDEN_KEYS = new Set([
81
139
  "basepackage", "basepackagefiles", "baseprompt", "cloudpackage", "contentbase64",
82
140
  "files", "fulltranscript", "rawsource", "systemprompt", "transcript", "messages",
@@ -118,7 +176,8 @@ function normalizeJson(value, seen = new Set()) {
118
176
  seen.delete(value);
119
177
  return result;
120
178
  }
121
- if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) {
179
+ const prototype = value && typeof value === "object" ? Object.getPrototypeOf(value) : undefined;
180
+ if (!value || typeof value !== "object" || (prototype !== Object.prototype && prototype !== null)) {
122
181
  throw new ExperienceBundleValidationError([`canonical JSON forbids ${typeof value}`]);
123
182
  }
124
183
  if (seen.has(value)) throw new ExperienceBundleValidationError(["canonical JSON forbids cyclic values"]);
@@ -153,6 +212,18 @@ function canonicalHash(value) {
153
212
  return `sha256:${crypto.createHash("sha256").update(canonicalJson(value), "utf8").digest("hex")}`;
154
213
  }
155
214
 
215
+ // Load the frozen activation taxonomy only after the canonical JSON machinery
216
+ // and its error type have initialized. Any artifact drift stops startup.
217
+ const EXPERIENCE_TAXONOMY_V1 = loadExperienceTaxonomyContract();
218
+ const CANONICAL_TASK_PREFIX = EXPERIENCE_TAXONOMY_V1.taskSignaturePrefix;
219
+ const CANONICAL_ENV_PREFIX = "agentlas.env.v1/";
220
+ const CANONICAL_TASK_SLUGS = Object.freeze([...EXPERIENCE_TAXONOMY_V1.taskSlugs]);
221
+ const CANONICAL_TASK_IDS = Object.freeze(CANONICAL_TASK_SLUGS.map((slug) => `${CANONICAL_TASK_PREFIX}${slug}`));
222
+ const CANONICAL_TASK_ID_SET = new Set(CANONICAL_TASK_IDS);
223
+ const CANONICAL_OS_VALUES = new Set(EXPERIENCE_TAXONOMY_V1.environment.osValues);
224
+ const CANONICAL_ARCH_VALUES = new Set(EXPERIENCE_TAXONOMY_V1.environment.archValues);
225
+ const CANONICAL_RUNTIME_RE = new RegExp(EXPERIENCE_TAXONOMY_V1.environment.runtimePattern);
226
+
156
227
  function sortedUnique(values) {
157
228
  const byCanonical = new Map();
158
229
  for (const value of values || []) byCanonical.set(canonicalJson(value), value);
@@ -321,8 +392,12 @@ function validateCredentialMetadata(value, label, issues) {
321
392
  const scopes = checkStringList(data.scopes, `${label}.scopes`, 1, 64, issues);
322
393
  scopes.forEach((scope, index) => { if (!/^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/.test(scope)) issues.push(`${label}.scopes[${index}] is invalid`); });
323
394
  }
324
- if (data.setupUrl != null && !/^https:\/\/[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?:\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*)?$/.test(data.setupUrl)) {
325
- issues.push(`${label}.setupUrl must be a value-free HTTPS provider page`);
395
+ if (data.setupUrl != null && (
396
+ typeof data.setupUrl !== "string" ||
397
+ data.setupUrl.length > 2048 ||
398
+ !/^https:\/\/[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?:\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*)?$/.test(data.setupUrl)
399
+ )) {
400
+ issues.push(`${label}.setupUrl must be a value-free HTTPS provider page of at most 2048 characters`);
326
401
  }
327
402
  if (data.brokerMode != null && !["host-bound-broker", "runtime-env-injection", "provider-managed-oauth", "manual-provider-page"].includes(data.brokerMode)) {
328
403
  issues.push(`${label}.brokerMode is invalid`);
@@ -337,9 +412,9 @@ function validateMcpRequirement(value, label, issues) {
337
412
  checkId(data.requirementId, `${label}.requirementId`, issues);
338
413
  checkId(data.catalogId, `${label}.catalogId`, issues);
339
414
  checkString(data.reason, `${label}.reason`, 1, 300, issues);
340
- checkStringList(data.capabilities, `${label}.capabilities`, 1, null, issues, { ids: true });
341
- checkStringList(data.permissions, `${label}.permissions`, 0, null, issues, { ids: true });
342
- const alternatives = checkStringList(data.alternatives, `${label}.alternatives`, 0, null, issues, { ids: true });
415
+ checkStringList(data.capabilities, `${label}.capabilities`, 1, 32, issues, { ids: true });
416
+ checkStringList(data.permissions, `${label}.permissions`, 0, 64, issues, { ids: true });
417
+ const alternatives = checkStringList(data.alternatives, `${label}.alternatives`, 0, 32, issues, { ids: true });
343
418
  if (alternatives.includes(data.catalogId)) issues.push(`${label}.alternatives must not contain catalogId`);
344
419
  if (typeof data.required !== "boolean" || typeof data.requiresKey !== "boolean") issues.push(`${label}.required/requiresKey must be boolean`);
345
420
  if (!Number.isInteger(data.priority) || data.priority < 1 || data.priority > 1000) issues.push(`${label}.priority must be 1..1000`);
@@ -386,8 +461,11 @@ function validateItem(item, index, issues) {
386
461
  checkString(data.summary, `${label}.summary`, 1, 320, issues);
387
462
  if (!Array.isArray(data.instructions) || data.instructions.length < 1 || data.instructions.length > MAX_INSTRUCTIONS_PER_ITEM) issues.push(`${label}.instructions must contain 1..${MAX_INSTRUCTIONS_PER_ITEM} values`);
388
463
  else data.instructions.forEach((step, stepIndex) => checkString(step, `${label}.instructions[${stepIndex}]`, 1, 600, issues));
389
- checkStringList(data.taskSignatures, `${label}.taskSignatures`, 1, MAX_TASK_SIGNATURES_PER_ITEM, issues);
390
- checkStringList(data.environmentConstraints, `${label}.environmentConstraints`, 0, 32, issues);
464
+ checkStringList(data.taskSignatures, `${label}.taskSignatures`, 1, MAX_TASK_SIGNATURES_PER_ITEM, issues, { ids: true });
465
+ const environmentConstraints = checkStringList(data.environmentConstraints, `${label}.environmentConstraints`, 0, 32, issues);
466
+ environmentConstraints.forEach((constraint, constraintIndex) => {
467
+ if (constraint.length > 240) issues.push(`${label}.environmentConstraints[${constraintIndex}] must be at most 240 characters`);
468
+ });
391
469
  checkStringList(data.evidenceReceiptIds, `${label}.evidenceReceiptIds`, 1, MAX_EVIDENCE_REFS_PER_ITEM, issues, { ids: true });
392
470
  checkStringList(data.supersedesItemIds, `${label}.supersedesItemIds`, 0, MAX_STORED_ITEMS, issues, { ids: true });
393
471
  if (typeof data.confidence !== "number" || !Number.isFinite(data.confidence) || data.confidence < 0 || data.confidence > 1) issues.push(`${label}.confidence must be 0..1`);
@@ -431,6 +509,38 @@ function validateBundleSecurity(value, issues) {
431
509
  if (nonMetadata.some((text) => OPAQUE_BLOB_RE.test(text))) issues.push("ExperienceBundle contains a long opaque encoded blob");
432
510
  }
433
511
 
512
+ /**
513
+ * Value-free privacy classification used before a successful run can become a
514
+ * local Operational Experience candidate. It deliberately returns only codes;
515
+ * unsafe source text is never copied into an intake receipt or bundle.
516
+ */
517
+ function portableExperienceSafetyIssues(text) {
518
+ const variants = [String(text || "")];
519
+ let current = variants[0];
520
+ for (let index = 0; index < 3; index += 1) {
521
+ try {
522
+ const decoded = decodeURIComponent(current);
523
+ if (decoded === current) break;
524
+ variants.push(decoded);
525
+ current = decoded;
526
+ } catch { break; }
527
+ }
528
+ const codes = [];
529
+ const hit = (patterns, code) => {
530
+ if (patterns.some((pattern) => variants.some((value) => pattern.test(value)))) codes.push(code);
531
+ };
532
+ hit(SECRET_PATTERNS, "secret-or-credential");
533
+ hit(PII_PATTERNS, "personal-or-customer-identifier");
534
+ hit(LOCAL_PATH_PATTERNS, "local-path");
535
+ hit(RAW_INTERACTION_PATTERNS, "raw-prompt-or-transcript");
536
+ hit(PROMPT_INJECTION_PATTERNS, "prompt-injection-material");
537
+ hit(BASE_PACKAGE_PATTERNS, "base-package-material");
538
+ if (variants.some((value) => PUBLIC_URL_RE.test(value))) codes.push("url");
539
+ if (variants.some((value) => CUSTOMER_DATA_RE.test(value))) codes.push("customer-data");
540
+ if (variants.some((value) => OPAQUE_BLOB_RE.test(value))) codes.push("opaque-blob");
541
+ return [...new Set(codes)].sort(compareCodePoints);
542
+ }
543
+
434
544
  function validateExperienceBundle(payload) {
435
545
  const value = normalizeExperienceBundle(payload);
436
546
  const issues = [];
@@ -574,6 +684,42 @@ function writePrivateTextAtomic(filePath, text) {
574
684
  }
575
685
  }
576
686
 
687
+ function readPrivateFileSnapshot(filePath) {
688
+ recoverPrivateAtomicTarget(filePath);
689
+ if (!fs.existsSync(filePath)) return { exists: false, bytes: null };
690
+ const stat = fs.lstatSync(filePath);
691
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("private transaction target is unsafe");
692
+ return { exists: true, bytes: fs.readFileSync(filePath) };
693
+ }
694
+
695
+ function writePrivateBufferAtomic(filePath, bytes) {
696
+ const dir = path.dirname(filePath);
697
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
698
+ try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
699
+ const temp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.rollback.tmp`);
700
+ try {
701
+ fs.writeFileSync(temp, bytes, { mode: 0o600, flag: "wx" });
702
+ replacePrivateFileAtomic(temp, filePath);
703
+ try { fs.chmodSync(filePath, 0o600); } catch { /* best effort */ }
704
+ } finally {
705
+ try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
706
+ }
707
+ }
708
+
709
+ function restorePrivateFileSnapshot(filePath, snapshot) {
710
+ recoverPrivateAtomicTarget(filePath);
711
+ if (snapshot.exists) {
712
+ writePrivateBufferAtomic(filePath, snapshot.bytes);
713
+ return;
714
+ }
715
+ if (fs.existsSync(filePath)) {
716
+ const stat = fs.lstatSync(filePath);
717
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("private rollback target is unsafe");
718
+ fs.rmSync(filePath, { force: true });
719
+ }
720
+ try { fs.rmSync(`${filePath}.previous`, { force: true }); } catch { /* noop */ }
721
+ }
722
+
577
723
  function exchangeStatePath(userDataDir) {
578
724
  return path.join(userDataDir, "terminal", "experience-exchange-v1.json");
579
725
  }
@@ -696,11 +842,13 @@ function projectScopeHash(cwd) {
696
842
  return canonicalHash({ kind: "terminal-project-scope", path: resolved.normalize("NFC") });
697
843
  }
698
844
 
699
- function saveLocalBundle(userDataDir, validation, options = {}) {
845
+ function commitLocalBundleRecord(userDataDir, validation, options = {}) {
700
846
  const bundle = validation.bundle;
701
847
  return withExchangeStateLock(userDataDir, () => {
702
848
  const storedPath = bundleStorePath(userDataDir, bundle.bundleId);
703
- writePrivateTextAtomic(storedPath, validation.canonicalJson);
849
+ const statePath = exchangeStatePath(userDataDir);
850
+ const storedSnapshot = readPrivateFileSnapshot(storedPath);
851
+ const stateSnapshot = readPrivateFileSnapshot(statePath);
704
852
  const state = loadExchangeState(userDataDir);
705
853
  const now = new Date().toISOString();
706
854
  const previous = state.bundles.find((row) => row.bundleId === bundle.bundleId);
@@ -713,16 +861,31 @@ function saveLocalBundle(userDataDir, validation, options = {}) {
713
861
  compatibleBaseReleaseIds: [...bundle.pack.baseCompatibility.compatibleBaseReleaseIds],
714
862
  projectScopeHash: projectScopeHash(options.cwd),
715
863
  storedAt: now,
716
- remote: previous?.remote || null,
864
+ remote: Object.prototype.hasOwnProperty.call(options, "remote") ? options.remote : previous?.remote || null,
717
865
  };
718
866
  const index = state.bundles.findIndex((item) => item.bundleId === row.bundleId);
719
867
  if (index >= 0) state.bundles[index] = row;
720
868
  else state.bundles.push(row);
721
- saveExchangeState(userDataDir, state);
722
- return row;
869
+ try {
870
+ writePrivateTextAtomic(storedPath, validation.canonicalJson);
871
+ saveExchangeState(userDataDir, state);
872
+ return row;
873
+ } catch (error) {
874
+ const rollbackErrors = [];
875
+ for (const [filePath, snapshot] of [[storedPath, storedSnapshot], [statePath, stateSnapshot]]) {
876
+ try { restorePrivateFileSnapshot(filePath, snapshot); }
877
+ catch (rollbackError) { rollbackErrors.push(rollbackError); }
878
+ }
879
+ if (rollbackErrors.length) error.rollbackErrors = rollbackErrors;
880
+ throw error;
881
+ }
723
882
  });
724
883
  }
725
884
 
885
+ function saveLocalBundle(userDataDir, validation, options = {}) {
886
+ return commitLocalBundleRecord(userDataDir, validation, options);
887
+ }
888
+
726
889
  function readStoredBundle(userDataDir, bundleId) {
727
890
  const file = bundleStorePath(userDataDir, bundleId);
728
891
  recoverPrivateAtomicTarget(file);
@@ -777,9 +940,8 @@ function trustedExperienceOrigin(rawValue, options = {}) {
777
940
  throw new Error("Loopback Experience Web origin requires explicit AGENTLAS_EXPERIENCE_ALLOW_LOOPBACK=1 opt-in");
778
941
  }
779
942
  } else {
780
- const agentlasHost = hostname === "agentlas.cloud" || hostname.endsWith(".agentlas.cloud");
781
- if (parsed.protocol !== "https:" || !agentlasHost || (parsed.port && parsed.port !== "443")) {
782
- throw new Error("Authenticated Experience exchange is restricted to an HTTPS Agentlas origin");
943
+ if (parsed.protocol !== "https:" || !OFFICIAL_EXPERIENCE_CLOUD_HOSTS.has(hostname) || (parsed.port && parsed.port !== "443")) {
944
+ throw new Error("Authenticated Experience exchange is restricted to an explicitly approved HTTPS Agentlas origin");
783
945
  }
784
946
  }
785
947
  return parsed.origin;
@@ -792,9 +954,16 @@ function parseResponseJson(response, label) {
792
954
  function responseError(response, label) {
793
955
  let data = null;
794
956
  try { data = JSON.parse(response.text || "null"); } catch { /* generic below */ }
795
- const error = new Error(`${label} failed (${response.status})${data?.error ? `: ${String(data.error).slice(0, 300)}` : ""}`);
957
+ const serverCode = [data?.errorCode, data?.code, data?.error]
958
+ .find((value) => typeof value === "string" && /^[a-z0-9][a-z0-9._-]{0,95}$/.test(value));
959
+ const detail = typeof data?.message === "string"
960
+ ? data.message
961
+ : typeof data?.error === "string"
962
+ ? data.error
963
+ : "";
964
+ const error = new Error(`${label} failed (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
796
965
  error.status = response.status;
797
- error.code = data?.code || (response.status === 401 || response.status === 403 ? "authentication_refused" : "experience_exchange_failed");
966
+ error.code = serverCode || (response.status === 401 || response.status === 403 ? "authentication_refused" : "experience_exchange_failed");
798
967
  error.details = data;
799
968
  return error;
800
969
  }
@@ -916,21 +1085,47 @@ function validateUploadReceipt(receipt, bundle) {
916
1085
  };
917
1086
  }
918
1087
 
1088
+ function remoteProjection(receipt, baseResolution = null, previousRemote = null) {
1089
+ return {
1090
+ uploadId: receipt.uploadId,
1091
+ status: receipt.status,
1092
+ requestedVisibility: receipt.requestedVisibility,
1093
+ revision: receipt.revision,
1094
+ serverCheckedAt: new Date().toISOString(),
1095
+ receipt,
1096
+ ...(baseResolution ? { baseResolution } : previousRemote?.baseResolution ? { baseResolution: previousRemote.baseResolution } : {}),
1097
+ };
1098
+ }
1099
+
1100
+ function commitServerAcceptedBundle(userDataDir, validation, receipt, baseResolution, options = {}) {
1101
+ const bundle = validation.bundle;
1102
+ try {
1103
+ const state = loadExchangeState(userDataDir);
1104
+ const previous = state.bundles.find((row) => row.bundleId === bundle.bundleId);
1105
+ return commitLocalBundleRecord(userDataDir, validation, {
1106
+ cwd: options.cwd,
1107
+ remote: remoteProjection(receipt, baseResolution, previous?.remote || null),
1108
+ });
1109
+ } catch (error) {
1110
+ const stateError = new Error(
1111
+ `Experience was accepted by the server as ${receipt.uploadId}, but Terminal could not atomically commit the canonical bundle and authoritative receipt. ` +
1112
+ "The prior local bundle/state were restored; rerun the same command and Idempotency-Key to reconcile the same receipt.",
1113
+ );
1114
+ stateError.code = "AGENTLAS_EXPERIENCE_LOCAL_STATE_COMMIT_FAILED";
1115
+ stateError.receipt = receipt;
1116
+ stateError.bundleId = bundle.bundleId;
1117
+ stateError.cause = error;
1118
+ throw stateError;
1119
+ }
1120
+ }
1121
+
919
1122
  function persistRemoteReceipt(userDataDir, bundle, receipt, baseResolution = null) {
920
1123
  try {
921
1124
  return withExchangeStateLock(userDataDir, () => {
922
1125
  const state = loadExchangeState(userDataDir);
923
1126
  const row = state.bundles.find((item) => item.bundleId === bundle.bundleId);
924
1127
  if (!row) throw new Error("local bundle record disappeared before receipt persistence");
925
- row.remote = {
926
- uploadId: receipt.uploadId,
927
- status: receipt.status,
928
- requestedVisibility: receipt.requestedVisibility,
929
- revision: receipt.revision,
930
- serverCheckedAt: new Date().toISOString(),
931
- receipt,
932
- ...(baseResolution ? { baseResolution } : row.remote?.baseResolution ? { baseResolution: row.remote.baseResolution } : {}),
933
- };
1128
+ row.remote = remoteProjection(receipt, baseResolution, row.remote);
934
1129
  saveExchangeState(userDataDir, state);
935
1130
  return row;
936
1131
  });
@@ -990,8 +1185,10 @@ async function publishBundle(validation, options = {}) {
990
1185
  const existingState = loadExchangeState(options.userDataDir);
991
1186
  const existingRow = findStateRecord(existingState, bundle.bundleId);
992
1187
  const exactBaseDescriptor = normalizeBaseDescriptor(options, existingRow?.remote?.baseResolution);
993
- const savedRow = saveLocalBundle(options.userDataDir, normalizedValidation, { cwd: options.cwd });
994
- const baseRelease = await resolveBaseRelease(bundle, { ...options, baseDescriptor: exactBaseDescriptor }, auth, savedRow.remote?.baseResolution);
1188
+ // Preflight and server acceptance happen before the canonical local envelope
1189
+ // is changed. A failed promotion must leave the prior private file/state
1190
+ // byte-identical instead of pairing a public envelope with an old receipt.
1191
+ const baseRelease = await resolveBaseRelease(bundle, { ...options, baseDescriptor: exactBaseDescriptor }, auth, existingRow?.remote?.baseResolution);
995
1192
  let response;
996
1193
  try {
997
1194
  response = await options.fetchHub(`${auth.base}/uploads`, {
@@ -1001,7 +1198,7 @@ async function publishBundle(validation, options = {}) {
1001
1198
  });
1002
1199
  } catch (error) {
1003
1200
  const recovered = await recoverLostUpload(bundle, key, options, auth, error);
1004
- persistRemoteReceipt(options.userDataDir, bundle, recovered.receipt, baseRelease);
1201
+ commitServerAcceptedBundle(options.userDataDir, normalizedValidation, recovered.receipt, baseRelease, { cwd: options.cwd });
1005
1202
  return { ...recovered, dryRun: false, networkUsed: true, baseRelease, publicActivation: false, evaluatorAuthority: false };
1006
1203
  }
1007
1204
  if (!response.ok) throw responseError(response, "Experience draft upload");
@@ -1014,7 +1211,7 @@ async function publishBundle(validation, options = {}) {
1014
1211
  if (receipt.status !== expectedStatus) throw new Error(`Experience ${operation} receipt must be ${expectedStatus}, never ${receipt.status}`);
1015
1212
  const etag = response.headers && typeof response.headers.get === "function" ? response.headers.get("etag") : null;
1016
1213
  if (etag !== `"${receipt.revision}"`) throw new Error("Experience upload ETag does not match the exact receipt revision");
1017
- persistRemoteReceipt(options.userDataDir, bundle, receipt, baseRelease);
1214
+ commitServerAcceptedBundle(options.userDataDir, normalizedValidation, receipt, baseRelease, { cwd: options.cwd });
1018
1215
  return { receipt, replayed: body.replayed, recovered: false, dryRun: false, networkUsed: true, baseRelease, publicActivation: false, evaluatorAuthority: false };
1019
1216
  }
1020
1217
 
@@ -1023,24 +1220,142 @@ function findStateRecord(state, ref) {
1023
1220
  return matches.sort((a, b) => String(b.storedAt).localeCompare(String(a.storedAt)))[0] || null;
1024
1221
  }
1025
1222
 
1223
+ function verifyStoredBundleRow(userDataDir, row) {
1224
+ const validation = readStoredBundle(userDataDir, row.bundleId);
1225
+ const bundle = validation.bundle;
1226
+ const sameIdentity =
1227
+ bundle.bundleId === row.bundleId &&
1228
+ bundle.bundleHash === row.bundleHash &&
1229
+ bundle.pack.experiencePackId === row.experiencePackId &&
1230
+ bundle.pack.releaseId === row.experiencePackReleaseId &&
1231
+ bundle.pack.baseCompatibility.agentDefinitionId === row.agentDefinitionId &&
1232
+ canonicalJson(bundle.pack.baseCompatibility.compatibleBaseReleaseIds) === canonicalJson(row.compatibleBaseReleaseIds);
1233
+ if (!sameIdentity) throw new Error("stored Experience bundle identity does not match its private index");
1234
+ if (row.remote) {
1235
+ const receipt = validateUploadReceipt(row.remote.receipt, bundle);
1236
+ if (
1237
+ receipt.uploadId !== row.remote.uploadId ||
1238
+ receipt.status !== row.remote.status ||
1239
+ receipt.requestedVisibility !== row.remote.requestedVisibility ||
1240
+ receipt.revision !== row.remote.revision
1241
+ ) throw new Error("stored Experience server receipt projection drifted from its private index");
1242
+ if (row.remote.baseResolution) {
1243
+ const base = row.remote.baseResolution;
1244
+ if (
1245
+ base.agentDefinitionId !== row.agentDefinitionId ||
1246
+ !row.compatibleBaseReleaseIds.includes(base.agentReleaseId)
1247
+ ) throw new Error("stored Experience exact base resolution drifted from its private index");
1248
+ }
1249
+ }
1250
+ return validation;
1251
+ }
1252
+
1253
+ function scopedStateRows(userDataDir, cwd) {
1254
+ const scope = projectScopeHash(cwd);
1255
+ return loadExchangeState(userDataDir).bundles
1256
+ .filter((row) => row.projectScopeHash === scope)
1257
+ .sort((a, b) => compareCodePoints(a.experiencePackReleaseId, b.experiencePackReleaseId) || compareCodePoints(a.bundleId, b.bundleId));
1258
+ }
1259
+
1260
+ function resolveScopedStoredRecord(userDataDir, ref, cwd) {
1261
+ if (!ref) throw new Error("an exact Experience bundle, pack release, or upload reference is required");
1262
+ const matches = scopedStateRows(userDataDir, cwd)
1263
+ .filter((row) => [row.bundleId, row.bundleHash, row.experiencePackId, row.experiencePackReleaseId, row.remote?.uploadId].includes(ref));
1264
+ if (!matches.length) throw new Error(`no exact local Experience record exists for this project: ${ref}`);
1265
+ if (matches.length > 1) throw new Error(`Experience reference is ambiguous; use an exact release, bundle, or upload id: ${ref}`);
1266
+ return { row: matches[0], validation: verifyStoredBundleRow(userDataDir, matches[0]) };
1267
+ }
1268
+
1269
+ function publicStoredBundleView(row, validation) {
1270
+ const bundle = validation.bundle;
1271
+ const itemStatusCounts = bundle.items.reduce((counts, item) => {
1272
+ counts[item.status] = (counts[item.status] || 0) + 1;
1273
+ return counts;
1274
+ }, { candidate: 0, promoted: 0, deprecated: 0, rejected: 0 });
1275
+ const remote = row.remote
1276
+ ? {
1277
+ uploadId: row.remote.uploadId,
1278
+ status: row.remote.status,
1279
+ requestedVisibility: row.remote.requestedVisibility,
1280
+ revision: row.remote.revision,
1281
+ serverCheckedAt: row.remote.serverCheckedAt,
1282
+ receiptPresent: true,
1283
+ receiptVerified: true,
1284
+ ...(row.remote.baseResolution ? { exactBaseAgentReleaseId: row.remote.baseResolution.agentReleaseId } : {}),
1285
+ }
1286
+ : null;
1287
+ return {
1288
+ schemaVersion: "agentlas.terminal-experience-local-view.v1",
1289
+ bundleId: row.bundleId,
1290
+ bundleHash: row.bundleHash,
1291
+ experiencePackId: row.experiencePackId,
1292
+ experiencePackReleaseId: row.experiencePackReleaseId,
1293
+ agentDefinitionId: row.agentDefinitionId,
1294
+ compatibleBaseReleaseIds: [...row.compatibleBaseReleaseIds],
1295
+ requestedVisibility: bundle.requestedVisibility,
1296
+ itemCount: bundle.items.length,
1297
+ itemStatusCounts,
1298
+ reviewState: itemStatusCounts.candidate > 0 && itemStatusCounts.promoted === 0 ? "candidate-review" : "curated",
1299
+ storedAt: row.storedAt,
1300
+ currentProjectOnly: true,
1301
+ localBundleVerified: true,
1302
+ remote,
1303
+ publicActivationClaimed: false,
1304
+ evaluatorAuthority: false,
1305
+ };
1306
+ }
1307
+
1308
+ function listStoredExperienceBundles(userDataDir, cwd) {
1309
+ return scopedStateRows(userDataDir, cwd)
1310
+ .map((row) => publicStoredBundleView(row, verifyStoredBundleRow(userDataDir, row)));
1311
+ }
1312
+
1313
+ function inspectStoredExperienceBundle(userDataDir, ref, cwd) {
1314
+ const { row, validation } = resolveScopedStoredRecord(userDataDir, ref, cwd);
1315
+ return publicStoredBundleView(row, validation);
1316
+ }
1317
+
1318
+ function previewWithdrawUpload(ref, options = {}) {
1319
+ const { row, validation } = resolveScopedStoredRecord(options.userDataDir, ref, options.cwd);
1320
+ if (!row.remote?.uploadId || !row.remote?.revision || !row.remote?.receipt) {
1321
+ throw new Error("unpublish requires an exact locally observed server receipt; publish or run experience status first");
1322
+ }
1323
+ const receipt = validateUploadReceipt(row.remote.receipt, validation.bundle);
1324
+ if (receipt.status === "withdrawn") throw new Error("Experience upload is already withdrawn");
1325
+ return {
1326
+ schemaVersion: "agentlas.terminal-experience-unpublish-preview.v1",
1327
+ dryRun: true,
1328
+ action: "unpublish",
1329
+ bundleId: row.bundleId,
1330
+ experiencePackReleaseId: row.experiencePackReleaseId,
1331
+ uploadId: receipt.uploadId,
1332
+ currentStatus: receipt.status,
1333
+ ifMatchRevision: receipt.revision,
1334
+ networkUsed: false,
1335
+ localWriteUsed: false,
1336
+ serverReceiptPresent: true,
1337
+ authority: "local-observed-server-receipt",
1338
+ publicActivationClaimed: false,
1339
+ };
1340
+ }
1341
+
1026
1342
  async function fetchUploadStatus(ref, options = {}) {
1343
+ const { row, validation } = resolveScopedStoredRecord(options.userDataDir, ref, options.cwd);
1344
+ const uploadId = row.remote?.uploadId;
1345
+ if (!uploadId) throw new Error("no exact server upload receipt is known for this local project bundle");
1027
1346
  const auth = await authenticatedContext(options, false);
1028
- const state = loadExchangeState(options.userDataDir);
1029
- const row = findStateRecord(state, ref);
1030
- const uploadId = UPLOAD_ID_RE.test(String(ref || "")) ? ref : row?.remote?.uploadId;
1031
- if (!uploadId) throw new Error("no server upload id is known for this local bundle");
1032
1347
  const response = await options.fetchHub(`${auth.base}/uploads/${encodeURIComponent(uploadId)}`, {
1033
1348
  method: "GET",
1034
1349
  headers: { accept: "application/json", cookie: auth.cookie, origin: auth.origin },
1035
1350
  });
1036
1351
  if (!response.ok) throw responseError(response, "Experience upload status");
1037
1352
  const body = parseResponseJson(response, "Experience upload status");
1038
- const bundle = row ? readStoredBundle(options.userDataDir, row.bundleId).bundle : null;
1353
+ const bundle = validation.bundle;
1039
1354
  const receipt = validateUploadReceipt(body.receipt, bundle);
1040
1355
  if (receipt.uploadId !== uploadId) throw new Error("status receipt id mismatch");
1041
1356
  const etag = response.headers && typeof response.headers.get === "function" ? response.headers.get("etag") : null;
1042
1357
  if (etag !== `"${receipt.revision}"`) throw new Error("status ETag does not match the exact receipt revision");
1043
- if (row) persistRemoteReceipt(options.userDataDir, bundle, receipt);
1358
+ persistRemoteReceipt(options.userDataDir, bundle, receipt);
1044
1359
  return { receipt, authoritative: "server", publicActivation: false, evaluatorAuthority: false };
1045
1360
  }
1046
1361
 
@@ -1075,10 +1390,9 @@ function writePrivateExportAtomic(filePath, text, overwrite) {
1075
1390
  }
1076
1391
 
1077
1392
  async function fetchUploadExport(ref, options = {}) {
1078
- const state = loadExchangeState(options.userDataDir);
1079
- const row = findStateRecord(state, ref);
1080
- const uploadId = UPLOAD_ID_RE.test(String(ref || "")) ? ref : row?.remote?.uploadId;
1081
- if (!uploadId) throw new Error("export requires a server upload id");
1393
+ const { row } = resolveScopedStoredRecord(options.userDataDir, ref, options.cwd);
1394
+ const uploadId = row.remote?.uploadId;
1395
+ if (!uploadId) throw new Error("export requires an exact locally observed server upload receipt");
1082
1396
  const requestedOutput = options.outputPath
1083
1397
  ? path.resolve(options.cwd || process.cwd(), options.outputPath)
1084
1398
  : row
@@ -1118,12 +1432,12 @@ async function fetchUploadExport(ref, options = {}) {
1118
1432
  }
1119
1433
 
1120
1434
  async function withdrawUpload(ref, options = {}) {
1121
- const auth = await authenticatedContext(options, false);
1122
- const state = loadExchangeState(options.userDataDir);
1123
- const row = findStateRecord(state, ref);
1124
- const uploadId = UPLOAD_ID_RE.test(String(ref || "")) ? ref : row?.remote?.uploadId;
1125
- if (!uploadId) throw new Error("withdraw requires a server upload id");
1435
+ const { row, validation } = resolveScopedStoredRecord(options.userDataDir, ref, options.cwd);
1436
+ const uploadId = row.remote?.uploadId;
1437
+ if (!uploadId) throw new Error("unpublish requires an exact locally observed server upload receipt");
1126
1438
  if (!row?.remote?.revision) throw new Error("withdraw requires the exact locally observed server revision; run experience status first");
1439
+ if (row.remote.status === "withdrawn") throw new Error("Experience upload is already withdrawn");
1440
+ const auth = await authenticatedContext(options, false);
1127
1441
  const response = await options.fetchHub(`${auth.base}/uploads/${encodeURIComponent(uploadId)}`, {
1128
1442
  method: "DELETE",
1129
1443
  headers: { accept: "application/json", cookie: auth.cookie, origin: auth.origin, "If-Match": `"${row.remote.revision}"` },
@@ -1133,7 +1447,7 @@ async function withdrawUpload(ref, options = {}) {
1133
1447
  const body = parseResponseJson(response, "Experience withdrawal conflict");
1134
1448
  const current = body.current?.receipt || body.current || body.receipt;
1135
1449
  if (current) {
1136
- const bundle = readStoredBundle(options.userDataDir, row.bundleId).bundle;
1450
+ const bundle = validation.bundle;
1137
1451
  const receipt = validateUploadReceipt(current, bundle);
1138
1452
  persistRemoteReceipt(options.userDataDir, bundle, receipt);
1139
1453
  const error = new Error("Experience withdrawal revision is stale; current server receipt was reconciled locally. Review status and retry.");
@@ -1146,7 +1460,7 @@ async function withdrawUpload(ref, options = {}) {
1146
1460
  throw responseError(response, "Experience withdrawal (server support may be unavailable)");
1147
1461
  }
1148
1462
  const body = parseResponseJson(response, "Experience withdrawal");
1149
- const bundle = readStoredBundle(options.userDataDir, row.bundleId).bundle;
1463
+ const bundle = validation.bundle;
1150
1464
  const receipt = validateUploadReceipt(body.receipt || body, bundle);
1151
1465
  if (receipt.uploadId !== uploadId || receipt.status !== "withdrawn") throw new Error("withdrawal did not return the exact withdrawn server receipt");
1152
1466
  const etag = response.headers && typeof response.headers.get === "function" ? response.headers.get("etag") : null;
@@ -1157,15 +1471,348 @@ async function withdrawUpload(ref, options = {}) {
1157
1471
  return { receipt, authoritative: "server", publicActivation: false };
1158
1472
  }
1159
1473
 
1474
+ const TASK_CLASS_KEYWORDS = Object.freeze({
1475
+ research: ["research", "investigate", "literature review", "market research", "리서치", "연구", "자료 조사"],
1476
+ writing: ["writing", "write article", "write copy", "copywriting", "blog post", "essay", "글쓰기", "글 작성", "카피 작성", "원고 작성"],
1477
+ coding: ["coding", "code implementation", "implement code", "write code", "source code", "코딩", "코드 구현", "코드 작성", "프로그래밍"],
1478
+ debugging: ["debug", "debugging", "bug fix", "fix bug", "troubleshoot", "error", "exception", "failure", "failed", "디버깅", "버그 수정", "오류", "오류 수정", "실패"],
1479
+ design: ["design", "ui design", "ux design", "wireframe", "디자인", "와이어프레임", "화면 설계"],
1480
+ "image-generation": ["image generation", "generate image", "create image", "text to image", "이미지 생성", "그림 생성"],
1481
+ "video-production": ["video production", "create video", "video editing", "영상 제작", "비디오 제작", "영상 편집"],
1482
+ presentation: ["presentation", "slide deck", "powerpoint", "ppt", "프레젠테이션", "발표 자료", "슬라이드", "피피티"],
1483
+ document: ["document", "docx", "pdf document", "document editing", "문서", "문서 작성", "문서 편집"],
1484
+ "data-analysis": ["data analysis", "analyze data", "analytics", "데이터 분석", "통계 분석"],
1485
+ "browser-automation": ["browser automation", "automate browser", "playwright", "브라우저 자동화", "웹 자동화"],
1486
+ "social-publishing": ["social publishing", "publish social", "post to instagram", "post to tiktok", "sns 게시", "소셜 게시", "인스타 업로드", "틱톡 업로드"],
1487
+ marketing: ["marketing", "campaign", "seo", "마케팅", "캠페인"],
1488
+ sales: ["sales", "lead generation", "sales outreach", "영업", "리드 발굴"],
1489
+ "customer-support": ["customer support", "customer service", "support ticket", "고객 지원", "고객 문의", "cs 응대"],
1490
+ ecommerce: ["ecommerce", "e commerce", "online store", "shopify", "이커머스", "온라인 쇼핑몰", "스마트스토어"],
1491
+ "legal-review": ["legal review", "contract review", "legal analysis", "법률 검토", "계약 검토", "법무 검토"],
1492
+ finance: ["finance", "financial analysis", "investment analysis", "재무", "금융", "투자 분석"],
1493
+ "project-planning": ["project planning", "project plan", "roadmap", "프로젝트 계획", "로드맵", "일정 계획"],
1494
+ "agent-building": ["agent building", "build agent", "create agent", "에이전트 빌드", "에이전트 만들", "에이전트 생성"],
1495
+ "workflow-automation": ["workflow automation", "automate workflow", "automation workflow", "워크플로 자동화", "업무 자동화"],
1496
+ "file-operations": ["file operations", "move files", "rename files", "organize files", "파일 작업", "파일 이동", "파일 이름 변경", "파일 정리"],
1497
+ translation: ["translation", "translate", "localization", "번역", "현지화"],
1498
+ });
1499
+
1500
+ function normalizeClassificationText(value) {
1501
+ return String(value || "")
1502
+ .normalize("NFKC")
1503
+ .toLowerCase()
1504
+ .replace(/[^a-z0-9가-힣]+/gi, " ")
1505
+ .replace(/\s+/g, " ")
1506
+ .trim();
1507
+ }
1508
+
1509
+ function normalizedTaxonomyAtom(value) {
1510
+ return typeof value === "string" ? value.normalize("NFKC").trim().toLowerCase() : "";
1511
+ }
1512
+
1513
+ function canonicalSourceTaskId(value) {
1514
+ const normalized = normalizedTaxonomyAtom(value);
1515
+ return normalized.startsWith(CANONICAL_TASK_PREFIX) && CANONICAL_TASK_ID_SET.has(normalized) ? normalized : null;
1516
+ }
1517
+
1518
+ function canonicalTaskId(value) {
1519
+ const normalized = normalizedTaxonomyAtom(value);
1520
+ const source = canonicalSourceTaskId(normalized);
1521
+ if (source) return source;
1522
+ const id = `${CANONICAL_TASK_PREFIX}${normalized}`;
1523
+ return CANONICAL_TASK_ID_SET.has(id) ? id : null;
1524
+ }
1525
+
1526
+ function isCanonicalTaskId(value) {
1527
+ return typeof value === "string" && CANONICAL_TASK_ID_SET.has(value);
1528
+ }
1529
+
1530
+ function keywordOccurs(normalizedPrompt, rawKeyword) {
1531
+ const keyword = normalizeClassificationText(rawKeyword);
1532
+ if (!keyword) return false;
1533
+ if (/[가-힣]/.test(keyword)) return normalizedPrompt.includes(keyword);
1534
+ return ` ${normalizedPrompt} `.includes(` ${keyword} `);
1535
+ }
1536
+
1537
+ function deriveCanonicalTaskClasses(prompt, options = {}) {
1538
+ const declaredRaw = options.declaredTaskClasses ?? options.declaredTaskClass;
1539
+ if (declaredRaw != null && (Array.isArray(declaredRaw) ? declaredRaw.length : String(declaredRaw).trim())) {
1540
+ const declared = (Array.isArray(declaredRaw) ? declaredRaw : [declaredRaw]).map(String);
1541
+ const taskIds = [...new Set(declared.map(canonicalTaskId).filter(Boolean))];
1542
+ const invalidDeclared = declared.filter((value) => !canonicalTaskId(value));
1543
+ return {
1544
+ taskIds: CANONICAL_TASK_IDS.filter((id) => taskIds.includes(id)),
1545
+ source: "declared-task-class",
1546
+ matchedTaskClasses: CANONICAL_TASK_IDS.filter((id) => taskIds.includes(id)),
1547
+ invalidDeclaredCount: invalidDeclared.length,
1548
+ };
1549
+ }
1550
+ const normalizedPrompt = normalizeClassificationText(prompt);
1551
+ const matches = [];
1552
+ for (const slug of CANONICAL_TASK_SLUGS) {
1553
+ if ((TASK_CLASS_KEYWORDS[slug] || []).some((keyword) => keywordOccurs(normalizedPrompt, keyword))) {
1554
+ matches.push(`${CANONICAL_TASK_PREFIX}${slug}`);
1555
+ }
1556
+ }
1557
+ return { taskIds: matches, source: "deterministic-keyword-map", matchedTaskClasses: matches, invalidDeclaredCount: 0 };
1558
+ }
1559
+
1560
+ function parseEnvironmentConstraint(value) {
1561
+ const normalized = normalizedTaxonomyAtom(value);
1562
+ const contract = EXPERIENCE_TAXONOMY_V1.environment;
1563
+ if (normalized.startsWith(contract.osPrefix)) {
1564
+ const selected = normalized.slice(contract.osPrefix.length);
1565
+ return CANONICAL_OS_VALUES.has(selected) ? { dimension: "os", value: selected } : null;
1566
+ }
1567
+ if (normalized.startsWith(contract.archPrefix)) {
1568
+ const selected = normalized.slice(contract.archPrefix.length);
1569
+ return CANONICAL_ARCH_VALUES.has(selected) ? { dimension: "arch", value: selected } : null;
1570
+ }
1571
+ if (normalized.startsWith(contract.runtimePrefix)) {
1572
+ const selected = normalized.slice(contract.runtimePrefix.length);
1573
+ return CANONICAL_RUNTIME_RE.test(selected) ? { dimension: "runtime", value: selected } : null;
1574
+ }
1575
+ return null;
1576
+ }
1577
+
1578
+ function isCanonicalEnvironmentTag(value) {
1579
+ return Boolean(parseEnvironmentConstraint(value));
1580
+ }
1581
+
1160
1582
  function defaultEnvironmentTags(options = {}) {
1161
1583
  const platform = options.platform || process.platform;
1162
1584
  const arch = options.arch || process.arch;
1163
- const platformName = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : platform;
1164
- const tags = new Set([`${platformName}-${arch}`]);
1165
- const locale = options.locale || (() => { try { return Intl.DateTimeFormat().resolvedOptions().locale; } catch { return null; } })();
1166
- if (locale) tags.add(String(locale));
1167
- for (const tag of options.extra || []) if (tag) tags.add(String(tag));
1168
- return [...tags];
1585
+ const platformCandidate = normalizedTaxonomyAtom(platform === "darwin" ? "macos" : platform === "win32" ? "windows" : platform);
1586
+ const archCandidate = normalizedTaxonomyAtom(arch === "x86_64" ? "x64" : arch === "aarch64" ? "arm64" : arch);
1587
+ const runtimeCandidate = normalizedTaxonomyAtom(typeof options.runtime === "string" ? options.runtime : typeof options.runtimeTag === "string" ? options.runtimeTag : "terminal");
1588
+ const platformName = CANONICAL_OS_VALUES.has(platformCandidate) ? platformCandidate : "unknown";
1589
+ const archName = CANONICAL_ARCH_VALUES.has(archCandidate) ? archCandidate : "unknown";
1590
+ const runtimeName = CANONICAL_RUNTIME_RE.test(runtimeCandidate) ? runtimeCandidate : "unknown";
1591
+ return [
1592
+ `${EXPERIENCE_TAXONOMY_V1.environment.osPrefix}${platformName}`,
1593
+ `${EXPERIENCE_TAXONOMY_V1.environment.archPrefix}${archName}`,
1594
+ `${EXPERIENCE_TAXONOMY_V1.environment.runtimePrefix}${runtimeName}`,
1595
+ ];
1596
+ }
1597
+
1598
+ function environmentConstraintsMatch(constraints, environment) {
1599
+ const actual = {
1600
+ os: normalizedTaxonomyAtom(environment?.os),
1601
+ arch: normalizedTaxonomyAtom(environment?.arch),
1602
+ runtime: normalizedTaxonomyAtom(environment?.runtime),
1603
+ };
1604
+ if (!CANONICAL_OS_VALUES.has(actual.os) || !CANONICAL_ARCH_VALUES.has(actual.arch) || !CANONICAL_RUNTIME_RE.test(actual.runtime)) return false;
1605
+ if (actual.os === "unknown" || actual.arch === "unknown" || actual.runtime === "unknown") return false;
1606
+ return (constraints || []).every((raw) => {
1607
+ const parsed = parseEnvironmentConstraint(raw);
1608
+ return Boolean(parsed && actual[parsed.dimension] === parsed.value);
1609
+ });
1610
+ }
1611
+
1612
+ function selectApplicablePortableItems(input = {}) {
1613
+ const profile = new Set([input.taskClass, ...(input.capabilityTags || [])].map(canonicalTaskId).filter(Boolean));
1614
+ if (!profile.size) return [];
1615
+ const eligible = (input.items || []).filter((item) => {
1616
+ if (!item || ["deprecated", "rejected"].includes(item.status)) return false;
1617
+ if (!(item.taskSignatures || []).map(canonicalSourceTaskId).filter(Boolean).some((task) => profile.has(task))) return false;
1618
+ return environmentConstraintsMatch(item.environmentConstraints || [], input.environment || {});
1619
+ });
1620
+ const superseded = new Set(eligible.flatMap((item) => item.supersedesItemIds || []));
1621
+ return eligible
1622
+ .filter((item) => typeof item.experienceItemId === "string" && !superseded.has(item.experienceItemId))
1623
+ .map((item) => item.experienceItemId);
1624
+ }
1625
+
1626
+ function readExactLocalBaseMarker(agentRoot, expectedSlug = null) {
1627
+ if (!agentRoot) return { marker: null, reason: "exact-local-base-marker-unavailable" };
1628
+ const file = path.join(path.resolve(agentRoot), ".agentlas-cloud-package.json");
1629
+ try {
1630
+ const stat = fs.lstatSync(file);
1631
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024) {
1632
+ return { marker: null, reason: "exact-local-base-marker-unsafe" };
1633
+ }
1634
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
1635
+ const slug = String(raw.slug || expectedSlug || "").trim();
1636
+ const packageHashRaw = String(raw.packageHash || "").replace(/^sha256:/i, "").toLowerCase();
1637
+ const packageHashVersion = String(raw.packageHashVersion || "");
1638
+ const cloudId = raw.cloudId == null ? null : String(raw.cloudId);
1639
+ if (
1640
+ !/^[a-z0-9][a-z0-9._-]{0,95}$/.test(slug) ||
1641
+ (expectedSlug && slug !== expectedSlug) ||
1642
+ !/^[a-f0-9]{64}$/.test(packageHashRaw) ||
1643
+ !["path-sha256-v1", "path-sha256-executable-v2"].includes(packageHashVersion) ||
1644
+ (cloudId && !ID_RE.test(cloudId))
1645
+ ) return { marker: null, reason: "exact-local-base-marker-invalid" };
1646
+ return {
1647
+ marker: {
1648
+ slug,
1649
+ cloudId,
1650
+ packageHash: `sha256:${packageHashRaw}`,
1651
+ packageHashVersion,
1652
+ },
1653
+ reason: null,
1654
+ };
1655
+ } catch (error) {
1656
+ return {
1657
+ marker: null,
1658
+ reason: error?.code === "ENOENT" ? "exact-local-base-marker-unavailable" : "exact-local-base-marker-invalid",
1659
+ };
1660
+ }
1661
+ }
1662
+
1663
+ function exactTaskSignatureInPrompt(signature, prompt, options = {}) {
1664
+ if (!isCanonicalTaskId(signature)) return false;
1665
+ return deriveCanonicalTaskClasses(prompt, options).taskIds.includes(signature);
1666
+ }
1667
+
1668
+ /**
1669
+ * Resolve normal Terminal runs without fuzzy identity or semantic guessing.
1670
+ * Automatic retrieval additionally needs one exact Experience release selected
1671
+ * by an authoritative loadout. Merely saving/uploading a compatible bundle is
1672
+ * never attachment consent.
1673
+ */
1674
+ function resolveRuntimeExperienceForAgent(options = {}) {
1675
+ const requested = options.requested || {};
1676
+ if (requested.disabled === true) {
1677
+ return { disabled: true, observableReason: "disabled-by-user", resolution: "skipped" };
1678
+ }
1679
+ const environmentTags = defaultEnvironmentTags(options);
1680
+ if (environmentTags.some((tag) => tag.endsWith("/unknown"))) {
1681
+ return { disabled: true, observableReason: "runtime-environment-unknown", resolution: "skipped" };
1682
+ }
1683
+ if (Array.isArray(requested.environmentTags) && requested.environmentTags.length) {
1684
+ const declaredEnvironment = [...new Set(requested.environmentTags.map(String).filter(Boolean))];
1685
+ const exactDefault = declaredEnvironment.length === environmentTags.length && declaredEnvironment.every((tag) => environmentTags.includes(tag));
1686
+ if (!declaredEnvironment.every(isCanonicalEnvironmentTag)) {
1687
+ return { disabled: true, observableReason: "legacy-environment-constraint-not-runtime-activatable", resolution: "skipped" };
1688
+ }
1689
+ if (!exactDefault) {
1690
+ return { disabled: true, observableReason: "declared-environment-does-not-match-runtime", resolution: "skipped" };
1691
+ }
1692
+ }
1693
+ const explicitBase = String(requested.baseAgentReleaseId || "");
1694
+ const explicitSignatures = [...new Set((requested.taskSignatures || []).map(String).filter(Boolean))];
1695
+ const explicitPackReleases = [...new Set((requested.experiencePackReleaseIds || []).map(String).filter(Boolean))];
1696
+ if (explicitBase || explicitSignatures.length || requested.agentDefinitionId || explicitPackReleases.length) {
1697
+ if (!ID_RE.test(explicitBase) || !explicitSignatures.length || explicitPackReleases.length !== 1 || !ID_RE.test(explicitPackReleases[0])) {
1698
+ return { disabled: true, observableReason: "incomplete-explicit-experience-binding", resolution: "skipped" };
1699
+ }
1700
+ if (explicitSignatures.some((item) => !isCanonicalTaskId(item))) {
1701
+ return { disabled: true, observableReason: "legacy-task-signature-not-runtime-activatable", resolution: "skipped" };
1702
+ }
1703
+ return {
1704
+ disabled: false,
1705
+ baseAgentReleaseId: explicitBase,
1706
+ ...(requested.agentDefinitionId && ID_RE.test(String(requested.agentDefinitionId)) ? { agentDefinitionId: String(requested.agentDefinitionId) } : {}),
1707
+ experiencePackReleaseIds: explicitPackReleases,
1708
+ taskSignatures: explicitSignatures,
1709
+ environmentTags,
1710
+ resolution: "explicit-exact",
1711
+ };
1712
+ }
1713
+ const agent = options.agent;
1714
+ if (!agent || agent.builtin || !agent.slug) {
1715
+ return { disabled: true, observableReason: agent?.builtin ? "builtin-agent-has-no-owned-experience-base" : "no-exact-agent-base", resolution: "skipped" };
1716
+ }
1717
+ const local = readExactLocalBaseMarker(options.agentRoot, agent.slug);
1718
+ if (!local.marker) return { disabled: true, observableReason: local.reason, resolution: "skipped" };
1719
+ const attachedPackReleases = [...new Set(
1720
+ (requested.attachedExperiencePackReleaseIds || []).map(String).filter(Boolean),
1721
+ )];
1722
+ if (attachedPackReleases.length !== 1 || !ID_RE.test(attachedPackReleases[0])) {
1723
+ return { disabled: true, observableReason: "explicit-experience-attachment-required", resolution: "skipped" };
1724
+ }
1725
+ let state;
1726
+ try { state = loadExchangeState(options.userDataDir); }
1727
+ catch { return { disabled: true, observableReason: "local-experience-state-invalid", resolution: "skipped" }; }
1728
+ const scopeHash = projectScopeHash(options.cwd);
1729
+ const matchingRows = state.bundles.filter((row) => {
1730
+ const base = row.remote?.baseResolution;
1731
+ return attachedPackReleases.includes(row.experiencePackReleaseId) &&
1732
+ row.projectScopeHash === scopeHash && base &&
1733
+ base.slug === local.marker.slug &&
1734
+ (!local.marker.cloudId || base.cloudId === local.marker.cloudId) &&
1735
+ base.packageHash === local.marker.packageHash &&
1736
+ base.packageHashVersion === local.marker.packageHashVersion &&
1737
+ base.agentDefinitionId === row.agentDefinitionId &&
1738
+ row.compatibleBaseReleaseIds.includes(base.agentReleaseId);
1739
+ });
1740
+ if (!matchingRows.length) {
1741
+ return { disabled: true, observableReason: "exact-local-base-release-unavailable", resolution: "skipped" };
1742
+ }
1743
+ const baseKeys = new Set(matchingRows.map((row) => {
1744
+ const base = row.remote.baseResolution;
1745
+ return `${base.agentDefinitionId}\0${base.agentReleaseId}\0${base.packageHash}`;
1746
+ }));
1747
+ if (baseKeys.size !== 1) {
1748
+ return { disabled: true, observableReason: "ambiguous-exact-base-release", resolution: "skipped" };
1749
+ }
1750
+ const base = matchingRows[0].remote.baseResolution;
1751
+ const taskClassResolution = deriveCanonicalTaskClasses(options.prompt, {
1752
+ declaredTaskClasses: requested.declaredTaskClasses ?? options.declaredTaskClasses ?? options.declaredTaskClass,
1753
+ });
1754
+ if (taskClassResolution.invalidDeclaredCount) {
1755
+ return { disabled: true, observableReason: "invalid-declared-task-class", resolution: "skipped" };
1756
+ }
1757
+ if (!taskClassResolution.taskIds.length) {
1758
+ return { disabled: true, observableReason: "canonical-task-class-unresolved", resolution: "skipped" };
1759
+ }
1760
+ const environment = new Set(environmentTags);
1761
+ const classifiedTasks = new Set(taskClassResolution.taskIds);
1762
+ const taskSignatures = new Set();
1763
+ let sawPromotedItem = false;
1764
+ let sawCanonicalSignature = false;
1765
+ let sawMatchingCanonicalTask = false;
1766
+ let sawLegacyEnvironmentForMatch = false;
1767
+ let sawCanonicalEnvironmentMismatch = false;
1768
+ for (const row of matchingRows) {
1769
+ let bundle;
1770
+ try { bundle = readStoredBundle(options.userDataDir, row.bundleId).bundle; }
1771
+ catch { continue; }
1772
+ for (const item of bundle.items) {
1773
+ if (item.status !== "promoted") continue;
1774
+ sawPromotedItem = true;
1775
+ const canonicalSignatures = item.taskSignatures.filter(isCanonicalTaskId);
1776
+ if (canonicalSignatures.length) sawCanonicalSignature = true;
1777
+ const matchedSignatures = canonicalSignatures.filter((signature) => classifiedTasks.has(signature));
1778
+ if (!matchedSignatures.length) continue;
1779
+ sawMatchingCanonicalTask = true;
1780
+ if (!item.environmentConstraints.every(isCanonicalEnvironmentTag)) {
1781
+ sawLegacyEnvironmentForMatch = true;
1782
+ continue;
1783
+ }
1784
+ if (!item.environmentConstraints.every((constraint) => environment.has(constraint))) {
1785
+ sawCanonicalEnvironmentMismatch = true;
1786
+ continue;
1787
+ }
1788
+ for (const signature of matchedSignatures) taskSignatures.add(signature);
1789
+ }
1790
+ }
1791
+ if (!taskSignatures.size) {
1792
+ const observableReason = sawPromotedItem && !sawCanonicalSignature
1793
+ ? "legacy-task-signature-not-auto-activatable"
1794
+ : sawMatchingCanonicalTask && sawLegacyEnvironmentForMatch
1795
+ ? "legacy-environment-constraint-not-auto-activatable"
1796
+ : sawMatchingCanonicalTask && sawCanonicalEnvironmentMismatch
1797
+ ? "canonical-environment-constraint-mismatch"
1798
+ : "canonical-task-signature-unavailable";
1799
+ return {
1800
+ disabled: true,
1801
+ observableReason,
1802
+ resolution: "skipped",
1803
+ taskClassResolution,
1804
+ };
1805
+ }
1806
+ return {
1807
+ disabled: false,
1808
+ baseAgentReleaseId: base.agentReleaseId,
1809
+ agentDefinitionId: base.agentDefinitionId,
1810
+ experiencePackReleaseIds: attachedPackReleases,
1811
+ taskSignatures: [...taskSignatures].sort(compareCodePoints),
1812
+ environmentTags,
1813
+ resolution: "automatic-exact",
1814
+ taskClassResolution,
1815
+ };
1169
1816
  }
1170
1817
 
1171
1818
  function estimateTokens(text) {
@@ -1175,33 +1822,49 @@ function estimateTokens(text) {
1175
1822
  function buildLocalExperienceAdvisory(options = {}) {
1176
1823
  const empty = { text: "", itemIds: [], estimatedTokens: 0, authority: "local-advisory", serverRentalResolutionReceiptPresent: false };
1177
1824
  if (!options.userDataDir || !options.cwd || !ID_RE.test(String(options.baseAgentReleaseId || ""))) return empty;
1178
- const taskSignatures = new Set((options.taskSignatures || []).map(String).filter(Boolean));
1825
+ const experiencePackReleaseIds = new Set(
1826
+ (options.experiencePackReleaseIds || []).map(String).filter((value) => ID_RE.test(value)),
1827
+ );
1828
+ if (experiencePackReleaseIds.size !== 1) return empty;
1829
+ const taskSignatures = new Set((options.taskSignatures || []).map(String).filter(isCanonicalTaskId));
1179
1830
  if (!taskSignatures.size) return empty;
1180
- const environmentTags = new Set((options.environmentTags || defaultEnvironmentTags(options)).map(String));
1831
+ const resolvedEnvironmentTags = (options.environmentTags || defaultEnvironmentTags(options)).map(String).filter(isCanonicalEnvironmentTag);
1832
+ if (resolvedEnvironmentTags.some((tag) => tag.endsWith("/unknown"))) return empty;
1833
+ const environmentTags = new Set(resolvedEnvironmentTags);
1181
1834
  const state = loadExchangeState(options.userDataDir);
1182
1835
  const projectHash = projectScopeHash(options.cwd);
1183
1836
  const candidates = [];
1184
1837
  for (const row of state.bundles) {
1185
- if (row.projectScopeHash !== projectHash || !row.compatibleBaseReleaseIds.includes(options.baseAgentReleaseId)) continue;
1838
+ if (
1839
+ !experiencePackReleaseIds.has(row.experiencePackReleaseId) ||
1840
+ row.projectScopeHash !== projectHash ||
1841
+ !row.compatibleBaseReleaseIds.includes(options.baseAgentReleaseId)
1842
+ ) continue;
1186
1843
  if (options.agentDefinitionId && row.agentDefinitionId !== options.agentDefinitionId) continue;
1187
1844
  let validation;
1188
1845
  try { validation = readStoredBundle(options.userDataDir, row.bundleId); } catch { continue; }
1189
1846
  for (const item of validation.bundle.items) {
1190
1847
  if (item.status !== "promoted") continue;
1191
1848
  if (!item.taskSignatures.some((signature) => taskSignatures.has(signature))) continue;
1849
+ if (!item.environmentConstraints.every(isCanonicalEnvironmentTag)) continue;
1192
1850
  if (!item.environmentConstraints.every((constraint) => environmentTags.has(constraint))) continue;
1193
1851
  candidates.push(item);
1194
1852
  }
1195
1853
  }
1196
1854
  candidates.sort((a, b) => Number(b.confidence) - Number(a.confidence) || compareCodePoints(a.experienceItemId, b.experienceItemId));
1197
1855
  const header = "[AGENTLAS_LOCAL_EXPERIENCE_ADVISORY v1] NO SERVER RENTAL-RESOLUTION RECEIPT. Local user-attested procedures only; not evaluator-verified and not reputation evidence.";
1856
+ const reservedTokens = Number.isInteger(options.reservedTokens)
1857
+ ? Math.max(0, Math.min(EXPERIENCE_RETRIEVAL_MAX_TOKENS, options.reservedTokens))
1858
+ : 0;
1859
+ const dynamicTokenBudget = Math.max(0, EXPERIENCE_RETRIEVAL_MAX_TOKENS - reservedTokens);
1860
+ if (estimateTokens(header) > dynamicTokenBudget) return empty;
1198
1861
  let text = header;
1199
1862
  const itemIds = [];
1200
1863
  for (const item of candidates) {
1201
1864
  if (itemIds.length >= EXPERIENCE_RETRIEVAL_MAX_ITEMS || itemIds.includes(item.experienceItemId)) continue;
1202
1865
  const line = `\n- [${item.experienceItemId}] ${item.summary}\n Steps: ${item.instructions.join(" | ")}`;
1203
1866
  const next = `${text}${line}`;
1204
- if (estimateTokens(next) > EXPERIENCE_RETRIEVAL_MAX_TOKENS) continue;
1867
+ if (estimateTokens(next) > dynamicTokenBudget) continue;
1205
1868
  text = next;
1206
1869
  itemIds.push(item.experienceItemId);
1207
1870
  }
@@ -1237,6 +1900,39 @@ function renderPublish(result) {
1237
1900
  ].join("\n");
1238
1901
  }
1239
1902
 
1903
+ function publicUploadReceipt(receipt) {
1904
+ return {
1905
+ schema: receipt.schema,
1906
+ uploadId: receipt.uploadId,
1907
+ bundleId: receipt.bundleId,
1908
+ bundleHash: receipt.bundleHash,
1909
+ experiencePackId: receipt.experiencePackId,
1910
+ experienceReleaseId: receipt.experienceReleaseId,
1911
+ status: receipt.status,
1912
+ requestedVisibility: receipt.requestedVisibility,
1913
+ revision: receipt.revision,
1914
+ createdAt: receipt.createdAt,
1915
+ updatedAt: receipt.updatedAt,
1916
+ ...(receipt.errorCode ? { errorCode: receipt.errorCode } : {}),
1917
+ };
1918
+ }
1919
+
1920
+ function publicCommandExchangeResult(result) {
1921
+ return {
1922
+ ...(result.receipt ? { receipt: publicUploadReceipt(result.receipt) } : {}),
1923
+ ...(BUNDLE_ID_RE.test(String(result.bundleId || "")) ? { bundleId: result.bundleId } : {}),
1924
+ ...(HASH_RE.test(String(result.bundleHash || "")) ? { bundleHash: result.bundleHash } : {}),
1925
+ ...(["private", "unlisted", "public"].includes(result.requestedVisibility) ? { requestedVisibility: result.requestedVisibility } : {}),
1926
+ ...(typeof result.replayed === "boolean" ? { replayed: result.replayed } : {}),
1927
+ ...(typeof result.recovered === "boolean" ? { recovered: result.recovered } : {}),
1928
+ ...(typeof result.dryRun === "boolean" ? { dryRun: result.dryRun } : {}),
1929
+ ...(typeof result.networkUsed === "boolean" ? { networkUsed: result.networkUsed } : {}),
1930
+ ...(typeof result.authoritative === "string" ? { authoritative: result.authoritative } : {}),
1931
+ publicActivation: false,
1932
+ evaluatorAuthority: false,
1933
+ };
1934
+ }
1935
+
1240
1936
  function baseDescriptorFromFlags(flags) {
1241
1937
  return {
1242
1938
  ...(flags["base-slug"] ? { slug: flags["base-slug"] } : {}),
@@ -1255,25 +1951,62 @@ async function cmdExperienceExchange(options = {}) {
1255
1951
 
1256
1952
  if (sub === "help" || sub === "--help" || sub === "-h") {
1257
1953
  const help = [
1954
+ "agentlas experience list",
1955
+ "agentlas experience inspect <exact-release-id|bundle-id|upload-id>",
1258
1956
  "agentlas experience validate <bundle.agentlas-experience.json>",
1259
1957
  "agentlas experience save <bundle> --base-cloud-id <id>|--base-slug <slug> --base-package-hash sha256:<hash>",
1260
1958
  "agentlas experience publish <bundle> --visibility unlisted|public --base-cloud-id <id>|--base-slug <slug> --base-package-hash sha256:<hash>",
1261
1959
  "agentlas experience status <bundle-id|upload-id>",
1960
+ "agentlas experience unpublish <exact-release-id|bundle-id|upload-id> [--dry-run]",
1262
1961
  "agentlas experience withdraw <bundle-id|upload-id>",
1263
1962
  "agentlas experience export <bundle-id|upload-id> [--out file] [--overwrite]",
1264
1963
  "Options: --dry-run (zero network/write), --idempotency-key <safe-key>, save --local-only",
1265
- "Legacy local intents: list|inspect|unpublish|legacy-publish",
1964
+ "Legacy pack-only local intents: legacy-list|legacy-inspect|legacy-publish|legacy-unpublish",
1266
1965
  "publish requests verification only; Terminal never claims evaluator verification or public activation.",
1267
1966
  ].join("\n");
1268
1967
  emit(help);
1269
1968
  return { help: true };
1270
1969
  }
1271
1970
 
1272
- if (["list", "ls", "inspect", "show", "unpublish", "legacy-publish", "legacy-unpublish"].includes(sub)) {
1971
+ if (["legacy-list", "legacy-inspect", "legacy-publish", "legacy-unpublish"].includes(sub)) {
1273
1972
  if (typeof options.legacyCommand !== "function") throw new Error("legacy local-intent Experience handler is unavailable");
1274
- const mapped = sub === "legacy-publish" ? "publish" : sub === "legacy-unpublish" ? "unpublish" : sub;
1973
+ const mapped = sub.slice("legacy-".length);
1275
1974
  return options.legacyCommand({ ...options, args: [mapped, ...args.slice(1)] });
1276
1975
  }
1976
+ if (sub === "list" || sub === "ls") {
1977
+ if (flags._.length) throw new Error("usage: agentlas experience list [--json]");
1978
+ const bundles = listStoredExperienceBundles(options.userDataDir, options.cwd);
1979
+ const result = {
1980
+ schemaVersion: "agentlas.terminal-experience-local-list.v1",
1981
+ currentProjectOnly: true,
1982
+ networkUsed: false,
1983
+ bundles,
1984
+ };
1985
+ const lines = bundles.length
1986
+ ? ["LOCAL PORTABLE EXPERIENCE BUNDLES · current project only · no network", ...bundles.map((bundle) =>
1987
+ `- ${bundle.experiencePackId}@${bundle.experiencePackReleaseId} · ${bundle.itemCount} item(s) · ${bundle.reviewState} · Hub: ${bundle.remote ? `${bundle.remote.status} (${bundle.remote.uploadId})` : "not submitted"}`)]
1988
+ : ["No Portable Experience Bundles are stored for this project.", "Hub was not contacted."];
1989
+ emit(flags.json ? JSON.stringify(result, null, 2) : lines.join("\n"));
1990
+ return result;
1991
+ }
1992
+ if (sub === "inspect" || sub === "show") {
1993
+ const ref = flags._[0];
1994
+ if (!ref || flags._.length !== 1) throw new Error("usage: agentlas experience inspect <exact-release-id|bundle-id|upload-id>");
1995
+ const bundle = inspectStoredExperienceBundle(options.userDataDir, ref, options.cwd);
1996
+ const result = { ...bundle, networkUsed: false };
1997
+ emit(flags.json ? JSON.stringify(result, null, 2) : [
1998
+ `${bundle.experiencePackId}@${bundle.experiencePackReleaseId}`,
1999
+ `bundle: ${bundle.bundleId} · ${bundle.itemCount} item(s) · local integrity: verified`,
2000
+ `review state: ${bundle.reviewState} · candidates ${bundle.itemStatusCounts.candidate} · promoted ${bundle.itemStatusCounts.promoted}`,
2001
+ `compatible base releases: ${bundle.compatibleBaseReleaseIds.join(", ")}`,
2002
+ bundle.remote
2003
+ ? `Hub receipt: ${bundle.remote.status} · ${bundle.remote.uploadId} · exact revision ${bundle.remote.revision}`
2004
+ : "Hub receipt: none · not submitted",
2005
+ "Owner/account, local path, raw content, prompt, transcript, and credentials are intentionally omitted.",
2006
+ "Public activation/evaluator authority: not claimed.",
2007
+ ].join("\n"));
2008
+ return result;
2009
+ }
1277
2010
  if (sub === "validate") {
1278
2011
  const validation = readBundleFile(flags._[0], options.cwd);
1279
2012
  const result = { valid: true, bundleId: validation.bundle.bundleId, bundleHash: validation.bundle.bundleHash, packContentHash: validation.bundle.pack.contentHash, items: validation.bundle.items.length, canonicalBytes: validation.canonicalBytes, networkUsed: false, authority: "local-validation" };
@@ -1300,24 +2033,11 @@ async function cmdExperienceExchange(options = {}) {
1300
2033
  idempotencyKey: flags["idempotency-key"] || null,
1301
2034
  baseDescriptor: baseDescriptorFromFlags(flags),
1302
2035
  });
1303
- emit(flags.json ? JSON.stringify(result, null, 2) : renderPublish(result));
2036
+ emit(flags.json ? JSON.stringify(publicCommandExchangeResult(result), null, 2) : renderPublish(result));
1304
2037
  return result;
1305
2038
  }
1306
2039
  if (sub === "publish") {
1307
- // Compatibility: the old pack-only command remains a local-intent alias.
1308
2040
  const source = flags._[0];
1309
- if (source && !BUNDLE_ID_RE.test(source)) {
1310
- const absolute = path.resolve(options.cwd || process.cwd(), source);
1311
- try {
1312
- const parsed = JSON.parse(fs.readFileSync(absolute, "utf8"));
1313
- if (parsed?.schemaVersion === "agentlas.experience-pack.v1") {
1314
- if (typeof options.legacyCommand !== "function") throw new Error("legacy handler unavailable");
1315
- return options.legacyCommand({ ...options, args: ["publish", ...args.slice(1)] });
1316
- }
1317
- } catch (error) {
1318
- if (error?.code !== "ENOENT" && !String(error?.message || "").includes("JSON")) throw error;
1319
- }
1320
- }
1321
2041
  const validation = resolveBundleInput(options.userDataDir, source, options.cwd);
1322
2042
  const result = await publishBundle(validation, {
1323
2043
  ...options,
@@ -1327,12 +2047,12 @@ async function cmdExperienceExchange(options = {}) {
1327
2047
  idempotencyKey: flags["idempotency-key"] || null,
1328
2048
  baseDescriptor: baseDescriptorFromFlags(flags),
1329
2049
  });
1330
- emit(flags.json ? JSON.stringify(result, null, 2) : renderPublish(result));
2050
+ emit(flags.json ? JSON.stringify(publicCommandExchangeResult(result), null, 2) : renderPublish(result));
1331
2051
  return result;
1332
2052
  }
1333
2053
  if (sub === "status") {
1334
2054
  const result = await fetchUploadStatus(flags._[0], options);
1335
- emit(flags.json ? JSON.stringify(result, null, 2) : `Server-authoritative status: ${result.receipt.status} · ${result.receipt.uploadId}\nrequested visibility: ${result.receipt.requestedVisibility} · Terminal did not assert public activation/evaluator reputation`);
2055
+ emit(flags.json ? JSON.stringify(publicCommandExchangeResult(result), null, 2) : `Server-authoritative status: ${result.receipt.status} · ${result.receipt.uploadId}\nrequested visibility: ${result.receipt.requestedVisibility} · Terminal did not assert public activation/evaluator reputation`);
1336
2056
  return result;
1337
2057
  }
1338
2058
  if (sub === "export") {
@@ -1345,17 +2065,18 @@ async function cmdExperienceExchange(options = {}) {
1345
2065
  emit(flags.json ? JSON.stringify(result, null, 2) : `Experience exported: ${result.outputPath}\nbundle hash: ${result.bundleHash}`);
1346
2066
  return result;
1347
2067
  }
1348
- if (sub === "withdraw") {
2068
+ if (sub === "withdraw" || sub === "unpublish") {
2069
+ if (!flags._[0] || flags._.length !== 1) throw new Error("usage: agentlas experience unpublish <exact-release-id|bundle-id|upload-id> [--dry-run]");
1349
2070
  if (flags["dry-run"] === true) {
1350
- const result = { dryRun: true, withdrawn: false, networkUsed: false, ref: flags._[0] || null };
1351
- emit(flags.json ? JSON.stringify(result, null, 2) : "DRY RUN · no withdrawal request sent · server state unchanged");
2071
+ const result = previewWithdrawUpload(flags._[0], options);
2072
+ emit(flags.json ? JSON.stringify(result, null, 2) : `DRY RUN · exact upload ${result.uploadId} at ${result.ifMatchRevision}\nnetwork/write used: no · server state unchanged · no new receipt`);
1352
2073
  return result;
1353
2074
  }
1354
2075
  const result = await withdrawUpload(flags._[0], options);
1355
- emit(flags.json ? JSON.stringify(result, null, 2) : `Server-authoritative withdrawal: ${result.receipt.uploadId} · withdrawn\nExisting receipts/history remain; no public activation claim.`);
2076
+ emit(flags.json ? JSON.stringify(publicCommandExchangeResult(result), null, 2) : `Server-authoritative unpublication: ${result.receipt.uploadId} · withdrawn\nExisting receipts/history remain; no public activation claim.`);
1356
2077
  return result;
1357
2078
  }
1358
- throw new Error("unknown experience subcommand (validate|save|publish|status|export|withdraw; legacy: list|inspect|unpublish|legacy-publish)");
2079
+ throw new Error("unknown experience subcommand (list|inspect|validate|save|publish|status|export|unpublish|withdraw; legacy: legacy-list|legacy-inspect|legacy-publish|legacy-unpublish)");
1359
2080
  }
1360
2081
 
1361
2082
  module.exports = {
@@ -1365,6 +2086,12 @@ module.exports = {
1365
2086
  MAX_STORED_ITEMS,
1366
2087
  EXPERIENCE_RETRIEVAL_MAX_ITEMS,
1367
2088
  EXPERIENCE_RETRIEVAL_MAX_TOKENS,
2089
+ EXPERIENCE_TAXONOMY_V1,
2090
+ EXPERIENCE_TAXONOMY_CHECKSUM,
2091
+ CANONICAL_TASK_PREFIX,
2092
+ CANONICAL_ENV_PREFIX,
2093
+ CANONICAL_TASK_SLUGS,
2094
+ CANONICAL_TASK_IDS,
1368
2095
  ExperienceBundleValidationError,
1369
2096
  canonicalJson,
1370
2097
  canonicalHash,
@@ -1384,7 +2111,16 @@ module.exports = {
1384
2111
  loadExchangeState,
1385
2112
  withExchangeStateLock,
1386
2113
  saveLocalBundle,
2114
+ commitServerAcceptedBundle,
1387
2115
  readStoredBundle,
2116
+ verifyStoredBundleRow,
2117
+ scopedStateRows,
2118
+ resolveScopedStoredRecord,
2119
+ listStoredExperienceBundles,
2120
+ inspectStoredExperienceBundle,
2121
+ previewWithdrawUpload,
2122
+ publicUploadReceipt,
2123
+ publicCommandExchangeResult,
1388
2124
  projectScopeHash,
1389
2125
  idempotencyKeyForBundle,
1390
2126
  idempotencyKeyHash,
@@ -1394,8 +2130,22 @@ module.exports = {
1394
2130
  fetchUploadExport,
1395
2131
  withdrawUpload,
1396
2132
  defaultEnvironmentTags,
2133
+ loadExperienceTaxonomyContract,
2134
+ validateExperienceTaxonomyContract,
2135
+ canonicalSourceTaskId,
2136
+ canonicalTaskId,
2137
+ isCanonicalTaskId,
2138
+ parseEnvironmentConstraint,
2139
+ isCanonicalEnvironmentTag,
2140
+ environmentConstraintsMatch,
2141
+ selectApplicablePortableItems,
2142
+ deriveCanonicalTaskClasses,
2143
+ readExactLocalBaseMarker,
2144
+ exactTaskSignatureInPrompt,
2145
+ resolveRuntimeExperienceForAgent,
1397
2146
  estimateTokens,
1398
2147
  buildLocalExperienceAdvisory,
1399
2148
  augmentRuntimeSystemWithLocalExperience,
2149
+ portableExperienceSafetyIssues,
1400
2150
  cmdExperienceExchange,
1401
2151
  };