@mrciphersmith/keryx 0.2.128 → 0.2.131

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 (3) hide show
  1. package/dist/cli.js +333 -89
  2. package/dist/core.js +48 -4
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -24338,6 +24338,49 @@ var init_trusted_wrap_up = __esm(() => {
24338
24338
  consumed = new WeakSet;
24339
24339
  });
24340
24340
 
24341
+ // src/sac/workspace-reference.ts
24342
+ function requireWorkspaceReference(raw) {
24343
+ const normalized = normalizeWorkspaceReference(raw);
24344
+ if (!normalized.ok)
24345
+ throw new Error(normalized.reason);
24346
+ return normalized.uri;
24347
+ }
24348
+ function normalizeWorkspaceReference(raw) {
24349
+ const trimmed = raw.trim();
24350
+ if (trimmed.length === 0) {
24351
+ return { ok: false, reason: 'a workspace reference must be a non-empty workspace-relative path, e.g. "./src/harness/search"' };
24352
+ }
24353
+ if (UNSAFE_ABSOLUTE.test(trimmed) || UNSAFE_SCHEME.test(trimmed) || trimmed.includes("\\")) {
24354
+ return {
24355
+ ok: false,
24356
+ reason: `"${trimmed}" is not a workspace-relative path \u2014 an absolute path, a URL and a Windows path are all refused. Use a path inside the project, e.g. "./src/harness/search".`
24357
+ };
24358
+ }
24359
+ const withoutDotSlash = trimmed.startsWith("./") ? trimmed.slice(2) : trimmed;
24360
+ const withoutTrailingSlash = withoutDotSlash.replace(/\/+$/, "");
24361
+ if (withoutTrailingSlash.length === 0) {
24362
+ return { ok: false, reason: `"${trimmed}" names the project root rather than a path inside it \u2014 name a directory or file, e.g. "./src/harness/search".` };
24363
+ }
24364
+ const segments = withoutTrailingSlash.split("/");
24365
+ if (segments.includes("..") || segments.includes(".")) {
24366
+ return { ok: false, reason: `"${trimmed}" contains a "." or ".." path segment \u2014 a workspace reference must stay inside the project.` };
24367
+ }
24368
+ const uri = `./${withoutTrailingSlash}`;
24369
+ if (!WORKSPACE_REFERENCE_PATTERN.test(uri)) {
24370
+ return {
24371
+ ok: false,
24372
+ reason: `"${trimmed}" is not a usable workspace reference. The stored form is "${uri}" if the characters are the problem: ` + 'each path segment may contain only letters, digits, ".", "_" and "-", and no ".." segment is allowed.'
24373
+ };
24374
+ }
24375
+ return { ok: true, uri };
24376
+ }
24377
+ var WORKSPACE_REFERENCE_PATTERN, UNSAFE_ABSOLUTE, UNSAFE_SCHEME;
24378
+ var init_workspace_reference = __esm(() => {
24379
+ WORKSPACE_REFERENCE_PATTERN = /^\.\/(?!(?:.*\/)?\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
24380
+ UNSAFE_ABSOLUTE = /^[/\\]/;
24381
+ UNSAFE_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
24382
+ });
24383
+
24341
24384
  // src/sac/index.ts
24342
24385
  import { access as access2, readFile as readFile50, realpath as realpath6 } from "fs/promises";
24343
24386
  import { createHash as createHash18 } from "crypto";
@@ -25085,11 +25128,12 @@ async function evaluateStrictSacGuard(input) {
25085
25128
  }
25086
25129
  var idPattern, subjectPattern, revisionPattern, correlationPattern, workspacePathPattern, receiptHashPattern, utcPattern, forbiddenPayloadKeys, normativeSchemaFiles, normativeSchemas, NORMATIVE_SCHEMA_DIR, SacReferenceError, trustedActors, roleRank;
25087
25130
  var init_sac = __esm(() => {
25131
+ init_workspace_reference();
25088
25132
  idPattern = /^[a-z][a-z0-9-]{2,63}$/;
25089
25133
  subjectPattern = /^(?:user|team|service|agent):[a-z0-9][a-z0-9._-]{0,127}$/;
25090
25134
  revisionPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
25091
25135
  correlationPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{15,255}$/;
25092
- workspacePathPattern = /^\.\/(?!.*(?:^|\/)\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
25136
+ workspacePathPattern = WORKSPACE_REFERENCE_PATTERN;
25093
25137
  receiptHashPattern = /^[a-f0-9]{64}$/;
25094
25138
  utcPattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?Z$/;
25095
25139
  forbiddenPayloadKeys = new Set(["prompt", "transcript", "hiddenReasoning", "secret", "secrets", "rawContent"]);
@@ -51085,7 +51129,7 @@ import path47 from "path";
51085
51129
  // package.json
51086
51130
  var package_default = {
51087
51131
  name: "@mrciphersmith/keryx",
51088
- version: "0.2.128",
51132
+ version: "0.2.131",
51089
51133
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
51090
51134
  private: false,
51091
51135
  publishConfig: {
@@ -78293,6 +78337,7 @@ function toMcpTools(ops = METAPROJECT_OPERATIONS, adapterFor = (cwd) => createMe
78293
78337
  }
78294
78338
 
78295
78339
  // src/sac/fwk-service.ts
78340
+ init_workspace_reference();
78296
78341
  init_assembly();
78297
78342
  init_sac();
78298
78343
  init_workspace_service();
@@ -78303,6 +78348,7 @@ import { appendFile as appendFile7, mkdir as mkdir59, open as open2, readFile as
78303
78348
  import path174 from "path";
78304
78349
 
78305
78350
  // src/sac/policy-experiment.ts
78351
+ init_workspace_reference();
78306
78352
  import { createHash as createHash34, createHmac as createHmac2 } from "crypto";
78307
78353
 
78308
78354
  // src/sac/receipt-integrity.ts
@@ -78366,7 +78412,7 @@ function stableValue(value) {
78366
78412
  }
78367
78413
  var stableJson2 = (value) => JSON.stringify(stableValue(value));
78368
78414
  var sha2564 = (value) => createHash34("sha256").update(stableJson2(value), "utf8").digest("hex");
78369
- var workspacePathPattern2 = /^\.\/(?!.*(?:^|\/)\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
78415
+ var workspacePathPattern2 = WORKSPACE_REFERENCE_PATTERN;
78370
78416
  var trustedOutcomes = new WeakMap;
78371
78417
  var trustedSandboxEvidence = new WeakMap;
78372
78418
  var trustedSandboxExecutions = new WeakMap;
@@ -78458,7 +78504,7 @@ var metadataOnly = (value) => {
78458
78504
  var nowIso2 = (now) => now().toISOString();
78459
78505
  var reportHashPattern = /^[a-f0-9]{64}$/;
78460
78506
  var immutableVersionPattern = /(?:^|[-_.:])(?:latest|next|head|main|develop)(?:$|[-_.:])/i;
78461
- var workspacePathPattern3 = /^\.\/(?!.*(?:^|\/)\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
78507
+ var workspacePathPattern3 = WORKSPACE_REFERENCE_PATTERN;
78462
78508
  var policyExperimentConfigPath = [".metaproject", "context-operations", "policy-experiment", "config.json"];
78463
78509
  var stableValue2 = (value) => {
78464
78510
  if (Array.isArray(value))
@@ -79333,6 +79379,7 @@ async function reclaimStaleExternalSlates(cwd, now = () => new Date) {
79333
79379
  init_slate();
79334
79380
  init_workspace_resolve();
79335
79381
  init_redact();
79382
+ init_workspace_reference();
79336
79383
 
79337
79384
  // src/mcp/tools.ts
79338
79385
  import { randomUUID as randomUUID19 } from "crypto";
@@ -79545,7 +79592,8 @@ function buildToolRegistry() {
79545
79592
  const title = stringParam2(params, "title")?.trim() ?? "";
79546
79593
  if (title.length === 0)
79547
79594
  throw new Error("sac.workspaceCreate requires a non-empty 'title'");
79548
- const component = stringParam2(params, "component");
79595
+ const rawComponent = stringParam2(params, "component");
79596
+ const component = rawComponent === undefined ? undefined : requireWorkspaceReference(rawComponent);
79549
79597
  const workspace = await sacWorkspaceService(cwd).create({ request: undefined, requestCorrelationId: randomUUID19(), id: newWorkspaceId(), title, ...component ? { component: { kind: "component", uri: component } } : {} });
79550
79598
  return workspace;
79551
79599
  }
@@ -85129,6 +85177,8 @@ function busInboxFullNotice(droppedTotal) {
85129
85177
  return `bus: inbox full \u2014 ${droppedTotal} older message(s) dropped
85130
85178
  `;
85131
85179
  }
85180
+ var BUS_WAKE_CAPPED_NOTICE = `\u25C7 a peer message arrived; automatic wakes are capped, so it will be delivered with your next message.
85181
+ `;
85132
85182
  function createLeaseHoldController(opts) {
85133
85183
  let wasHeld = false;
85134
85184
  return {
@@ -86977,7 +87027,8 @@ class SandboxedWebTransport {
86977
87027
  ...request.body !== undefined ? { body: request.body } : {},
86978
87028
  ...request.credential !== undefined ? { credential: request.credential } : {},
86979
87029
  ...request.signal !== undefined ? { signal: request.signal } : {},
86980
- localOnly: request.capability === "local-search"
87030
+ localOnly: request.capability === "local-search",
87031
+ browserHeaders: request.capability === "public-search"
86981
87032
  });
86982
87033
  if (!raw.ok) {
86983
87034
  return {
@@ -87024,7 +87075,8 @@ class SandboxedWebTransport {
87024
87075
  address: target.value.address,
87025
87076
  method: request.method,
87026
87077
  ...request.body !== undefined ? { body: request.body } : {},
87027
- ...request.credential !== undefined ? { credential: request.credential } : {}
87078
+ ...request.credential !== undefined ? { credential: request.credential } : {},
87079
+ ...request.browserHeaders === true ? { browserHeaders: true } : {}
87028
87080
  }, request.signal);
87029
87081
  if (!worker.ok)
87030
87082
  return worker;
@@ -87062,6 +87114,54 @@ import { existsSync as existsSync29 } from "fs";
87062
87114
  import { homedir as homedir7 } from "os";
87063
87115
  var WORKER_TIMEOUT_MS = 1e4;
87064
87116
  var MAX_WORKER_OUTPUT_BYTES = 192000;
87117
+ var BROWSER_USER_AGENTS = [
87118
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
87119
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
87120
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
87121
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0",
87122
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0",
87123
+ "Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0",
87124
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15",
87125
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
87126
+ ];
87127
+ var BROWSER_ACCEPT_LANGUAGES = [
87128
+ "en-US,en;q=0.9",
87129
+ "en-US,en;q=0.9,ru;q=0.8",
87130
+ "en-GB,en;q=0.9,en-US;q=0.8",
87131
+ "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
87132
+ "en-CA,en;q=0.9,en-US;q=0.8"
87133
+ ];
87134
+ var BROWSER_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
87135
+ var BROWSER_STATIC_HEADERS = {
87136
+ "accept-encoding": "identity",
87137
+ connection: "keep-alive",
87138
+ "upgrade-insecure-requests": "1",
87139
+ "sec-fetch-dest": "document",
87140
+ "sec-fetch-mode": "navigate",
87141
+ "sec-fetch-site": "none",
87142
+ "sec-fetch-user": "?1",
87143
+ "cache-control": "max-age=0"
87144
+ };
87145
+ var BROWSER_HEADERS_SOURCE = [
87146
+ "(function (random) {",
87147
+ " var userAgents = " + JSON.stringify(BROWSER_USER_AGENTS) + ";",
87148
+ " var acceptLanguages = " + JSON.stringify(BROWSER_ACCEPT_LANGUAGES) + ";",
87149
+ " var staticHeaders = " + JSON.stringify(BROWSER_STATIC_HEADERS) + ";",
87150
+ " var pick = function (list) {",
87151
+ " var value = list[Math.floor(random() * list.length)];",
87152
+ " return value === undefined ? list[0] : value;",
87153
+ " };",
87154
+ " var headers = {",
87155
+ " accept: " + JSON.stringify(BROWSER_ACCEPT) + ",",
87156
+ ' "accept-language": pick(acceptLanguages),',
87157
+ ' "user-agent": pick(userAgents),',
87158
+ " };",
87159
+ " for (var name in staticHeaders) { headers[name] = staticHeaders[name]; }",
87160
+ ' if (random() < 0.5) headers.dnt = "1";',
87161
+ " return headers;",
87162
+ "})"
87163
+ ].join(`
87164
+ `);
87065
87165
  var WORKER_SOURCE = String.raw`
87066
87166
  const { request: httpsRequest } = await import("node:https");
87067
87167
  const { request: httpRequest } = await import("node:http");
@@ -87070,7 +87170,10 @@ const input = JSON.parse(await Bun.stdin.text());
87070
87170
  const fail = () => process.stdout.write(JSON.stringify({ ok: false, reason: "request failed or timed out" }));
87071
87171
  try {
87072
87172
  const timer = setTimeout(() => req.destroy(new Error("timeout")), input.timeoutMs);
87073
- const headers = { accept: "text/html, text/plain, application/json, application/xml, application/xhtml+xml" };
87173
+ const browserHeaders = ${BROWSER_HEADERS_SOURCE};
87174
+ const headers = input.browserHeaders
87175
+ ? browserHeaders(Math.random)
87176
+ : { accept: "text/html, text/plain, application/json, application/xml, application/xhtml+xml" };
87074
87177
  const payload = input.body && typeof input.body === "object" ? { ...input.body } : undefined;
87075
87178
  if (input.credential && input.credential.injection === "header") headers[input.credential.name] = input.credential.value;
87076
87179
  if (input.credential && input.credential.injection === "json-body") {
@@ -87294,7 +87397,7 @@ function webSearchTool(service) {
87294
87397
  const response = await service.search(input.query.trim());
87295
87398
  if (!response.ok) {
87296
87399
  return {
87297
- output: response.reason === "no-active-provider" ? "web_search: no active connected provider. Use /search-provider to configure one, test it, then use /search-connect to select it." : response.reason === "search-failed" ? "web_search: search failed. Retry later; do not switch providers yourself." : "web_search: active provider is unavailable; reconnect it with /search-provider before retrying.",
87400
+ output: response.reason === "no-active-provider" ? "web_search: no active connected provider. Use /search-provider to configure one, test it, then use /search-connect to select it." : response.reason === "search-failed" ? `web_search: search failed${response.detail !== undefined && response.detail.length > 0 ? ` (${response.detail})` : ""}. Wait a few minutes before trying again, and do not switch providers yourself.` : "web_search: active provider is unavailable; reconnect it with /search-provider before retrying.",
87298
87401
  isError: true
87299
87402
  };
87300
87403
  }
@@ -87412,7 +87515,12 @@ function workspaceCreateTool(cwd, getSessionDir) {
87412
87515
  const title = typeof input.title === "string" ? input.title.trim() : "";
87413
87516
  if (title.length === 0)
87414
87517
  return { output: "workspace_create requires a non-empty 'title'", isError: true };
87415
- const component = typeof input.component === "string" && input.component.length > 0 ? input.component : undefined;
87518
+ const rawComponent = typeof input.component === "string" && input.component.length > 0 ? input.component : undefined;
87519
+ const normalizedComponent = rawComponent === undefined ? undefined : normalizeWorkspaceReference(rawComponent);
87520
+ if (normalizedComponent !== undefined && !normalizedComponent.ok) {
87521
+ return { output: "workspace_create: " + normalizedComponent.reason, isError: true };
87522
+ }
87523
+ const component = normalizedComponent?.ok === true ? normalizedComponent.uri : undefined;
87416
87524
  try {
87417
87525
  const workspace = await service5(cwd).create({
87418
87526
  request: undefined,
@@ -88096,7 +88204,7 @@ function rememberExactShellGrant(command, sessionAllow, options) {
88096
88204
  if (!offerExact) {
88097
88205
  return "";
88098
88206
  }
88099
- const stored = allowShellPattern(exact);
88207
+ const stored = allowShellPattern(exact, options?.dir);
88100
88208
  if (stored.length > 0) {
88101
88209
  sessionAllow.add(stored);
88102
88210
  }
@@ -88250,8 +88358,14 @@ var ANOMALY_MARKERS = [
88250
88358
  "/anomaly.js",
88251
88359
  "Unfortunately, bots use DuckDuckGo too"
88252
88360
  ];
88253
- var MIN_GAP_MS = 500;
88254
- var MAX_GAP_MS = 2000;
88361
+ var MIN_GAP_MS = 4000;
88362
+ var MAX_GAP_MS = 8000;
88363
+ var DUCKDUCKGO_ANOMALY_BACKOFF_MS = [5000, 20000];
88364
+ var timing = {
88365
+ minGapMs: MIN_GAP_MS,
88366
+ maxGapMs: MAX_GAP_MS,
88367
+ backoffMs: DUCKDUCKGO_ANOMALY_BACKOFF_MS
88368
+ };
88255
88369
  var lastSearchAt = 0;
88256
88370
  function duckduckgoLiteUrl(query) {
88257
88371
  return `${DUCKDUCKGO_LITE_ORIGIN}?q=${encodeURIComponent(query)}`;
@@ -88267,7 +88381,8 @@ async function maybeDelayDuckDuckGoSearch(signal) {
88267
88381
  lastSearchAt = now;
88268
88382
  return;
88269
88383
  }
88270
- const gap = MIN_GAP_MS + Math.floor(Math.random() * (MAX_GAP_MS - MIN_GAP_MS + 1));
88384
+ const { minGapMs, maxGapMs } = timing;
88385
+ const gap = minGapMs + Math.floor(Math.random() * Math.max(0, maxGapMs - minGapMs + 1));
88271
88386
  const wait = gap - (now - lastSearchAt);
88272
88387
  if (wait > 0)
88273
88388
  await sleep2(wait, signal);
@@ -88387,6 +88502,30 @@ function sleep2(ms, signal) {
88387
88502
  signal?.addEventListener("abort", onAbort, { once: true });
88388
88503
  });
88389
88504
  }
88505
+ async function waitDuckDuckGoBackoff(attempt, signal) {
88506
+ const wait = timing.backoffMs[attempt];
88507
+ if (wait === undefined)
88508
+ return false;
88509
+ if (wait > 0)
88510
+ await sleep2(wait, signal);
88511
+ return true;
88512
+ }
88513
+
88514
+ // src/harness/search/connection-message.ts
88515
+ function describeConnectionFailure(reason) {
88516
+ switch (reason) {
88517
+ case "missing-credential":
88518
+ return "missing credential";
88519
+ case "rate-limited":
88520
+ return "rate limited \u2014 the provider served its bot page to this machine; retrying will not help";
88521
+ case "incompatible-response":
88522
+ return "the endpoint answered, but not with search results";
88523
+ case "transport-failed":
88524
+ default:
88525
+ return "connection validation failed";
88526
+ }
88527
+ }
88528
+ var RATE_LIMITED_SEARCH_ERROR = "rate limited: DuckDuckGo served its bot-check page instead of results, even after retrying. Do not retry or rephrase; wait a few minutes or fetch a known URL directly";
88390
88529
 
88391
88530
  // src/harness/search/registry.ts
88392
88531
  var SEARXNG_DOCS_URL = "https://docs.searxng.org/admin/installation.html";
@@ -88497,29 +88636,31 @@ function createSearchProviderRegistry(transport, resolveCredential = () => {
88497
88636
  query: DUCKDUCKGO_HEALTHCHECK_QUERY
88498
88637
  });
88499
88638
  if (isDuckDuckGoAnomaly(response.status, response.text))
88500
- return { ok: false, reason: "transport-failed" };
88639
+ return { ok: false, reason: "rate-limited" };
88501
88640
  if (!response.ok || response.status < 200 || response.status >= 300)
88502
88641
  return requestFailure(response);
88503
88642
  const parsed = duckduckgoSearchResponse(DUCKDUCKGO_HEALTHCHECK_QUERY, response.text);
88504
- return parsed.ok ? { ok: true } : { ok: false, reason: "transport-failed" };
88643
+ return parsed.ok ? { ok: true } : { ok: false, reason: "rate-limited" };
88505
88644
  },
88506
88645
  async search(_fields, query, signal) {
88507
- await maybeDelayDuckDuckGoSearch(signal);
88508
- const response = await transport.request({
88509
- providerId: "duckduckgo",
88510
- capability: "public-search",
88511
- url: duckduckgoLiteUrl(query),
88512
- method: "GET",
88513
- query,
88514
- ...signal ? { signal } : {}
88515
- });
88516
- if (isDuckDuckGoAnomaly(response.status, response.text)) {
88517
- throw new Error("DuckDuckGo rate-limited this machine");
88646
+ for (let attempt = 0;; attempt += 1) {
88647
+ await maybeDelayDuckDuckGoSearch(signal);
88648
+ const response = await transport.request({
88649
+ providerId: "duckduckgo",
88650
+ capability: "public-search",
88651
+ url: duckduckgoLiteUrl(query),
88652
+ method: "GET",
88653
+ query,
88654
+ ...signal ? { signal } : {}
88655
+ });
88656
+ const parsed = duckduckgoSearchResponse(query, response.text);
88657
+ if (!isDuckDuckGoAnomaly(response.status, response.text) && parsed.ok) {
88658
+ return parsed.value;
88659
+ }
88660
+ if (!await waitDuckDuckGoBackoff(attempt, signal)) {
88661
+ throw new Error(RATE_LIMITED_SEARCH_ERROR);
88662
+ }
88518
88663
  }
88519
- const parsed = duckduckgoSearchResponse(query, response.text);
88520
- if (!parsed.ok)
88521
- throw new Error("DuckDuckGo rate-limited this machine");
88522
- return parsed.value;
88523
88664
  }
88524
88665
  };
88525
88666
  const searxng = {
@@ -88751,8 +88892,12 @@ class SearchProviderController {
88751
88892
  return { ok: false, reason: "no-active-provider" };
88752
88893
  try {
88753
88894
  return { ok: true, value: await descriptor.search(stored?.fields ?? {}, query, signal) };
88754
- } catch {
88755
- return { ok: false, reason: "search-failed" };
88895
+ } catch (error) {
88896
+ return {
88897
+ ok: false,
88898
+ reason: "search-failed",
88899
+ ...error instanceof Error && error.message.length > 0 ? { detail: error.message } : {}
88900
+ };
88756
88901
  }
88757
88902
  }
88758
88903
  config() {
@@ -93181,14 +93326,25 @@ async function classifySession(session) {
93181
93326
  sessionId: session.id,
93182
93327
  ...workspaceId !== undefined ? { workspaceId } : {},
93183
93328
  lastSeenAt: session.updatedAt,
93329
+ reason: "wrap-up-failed",
93184
93330
  wrapUpOutcome: { trigger: wrapUpOutcome.trigger, generatedAt: wrapUpOutcome.generatedAt, groups: wrapUpOutcome.groups }
93185
93331
  }
93186
93332
  };
93187
93333
  }
93188
93334
  if (!await isSlateEngaged(dir))
93189
93335
  return;
93190
- const workspaceId = (await safeReadSlate(dir))?.workspaceId;
93191
- return { kind: "unknown", item: { type: "unknown", sessionId: session.id, ...workspaceId !== undefined ? { workspaceId } : {}, lastSeenAt: session.updatedAt } };
93336
+ const slateRead = await readSlateState(dir);
93337
+ const workspaceId = slateRead.state === "ok" ? slateRead.slate.workspaceId : undefined;
93338
+ return {
93339
+ kind: "unknown",
93340
+ item: {
93341
+ type: "unknown",
93342
+ sessionId: session.id,
93343
+ ...workspaceId !== undefined ? { workspaceId } : {},
93344
+ lastSeenAt: session.updatedAt,
93345
+ reason: slateRead.state === "unreadable" ? "slate-unreadable" : "no-resolution-recorded"
93346
+ }
93347
+ };
93192
93348
  }
93193
93349
  async function readExternalUnboundCandidates(cwd) {
93194
93350
  const extDir = externalSlatesDir(cwd);
@@ -93441,6 +93597,17 @@ async function isSlateEngaged(dir) {
93441
93597
  return false;
93442
93598
  }
93443
93599
  }
93600
+ async function readSlateState(dir) {
93601
+ try {
93602
+ const slate = await readSlate(dir);
93603
+ return slate === undefined ? { state: "absent" } : { state: "ok", slate };
93604
+ } catch (error) {
93605
+ return isNotFound2(error) ? { state: "absent" } : { state: "unreadable" };
93606
+ }
93607
+ }
93608
+ function isNotFound2(error) {
93609
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
93610
+ }
93444
93611
  async function safeReadSlate(dir) {
93445
93612
  try {
93446
93613
  return await readSlate(dir);
@@ -94114,6 +94281,15 @@ function mountEmptyTranscriptSplash(otui, r, transcript) {
94114
94281
  transcript.remove(box);
94115
94282
  };
94116
94283
  }
94284
+ function createSplashLifecycle(opts) {
94285
+ let remove = opts.initialHistoryLength === 0 ? opts.mount() : undefined;
94286
+ return {
94287
+ removeIfShown() {
94288
+ remove?.();
94289
+ remove = undefined;
94290
+ }
94291
+ };
94292
+ }
94117
94293
 
94118
94294
  // src/tui/workspace-inspector.ts
94119
94295
  var WORKSPACE_COMMAND = "/workspace";
@@ -94397,7 +94573,7 @@ function summarizeReviewItem(item) {
94397
94573
  case "unbound-candidate":
94398
94574
  return `${item.sessionId} \u2014 ${item.summary}`;
94399
94575
  case "unknown":
94400
- return `${item.sessionId} \u2014 last seen ${item.lastSeenAt}`;
94576
+ return `${item.sessionId} \u2014 ${unknownReasonLabel(item.reason)} (last seen ${item.lastSeenAt})`;
94401
94577
  }
94402
94578
  }
94403
94579
  function formatReviewListLines(items, selected) {
@@ -94409,6 +94585,26 @@ function formatReviewListLines(items, selected) {
94409
94585
  return `${mark} ${TYPE_LABEL[item.type].padEnd(8)} ${summarizeReviewItem(item)}`;
94410
94586
  });
94411
94587
  }
94588
+ function unknownReasonLabel(reason) {
94589
+ switch (reason) {
94590
+ case "wrap-up-failed":
94591
+ return "wrap-up failed";
94592
+ case "slate-unreadable":
94593
+ return "slate.json unreadable";
94594
+ case "no-resolution-recorded":
94595
+ return "no resolution recorded";
94596
+ }
94597
+ }
94598
+ function unknownReasonDetail(reason) {
94599
+ switch (reason) {
94600
+ case "wrap-up-failed":
94601
+ return "a wrap-up dispatch ran and every group failed - the per-kind reasons are above where recorded; re-run wrap-up after fixing them.";
94602
+ case "slate-unreadable":
94603
+ return "this session's slate.json exists but could not be read. Its record is DAMAGED, not absent: inspect the file before assuming nothing happened.";
94604
+ case "no-resolution-recorded":
94605
+ return "Slate engagement with no proposal, terminal state, unbound-candidate or wrap-up-outcome artifact recorded.";
94606
+ }
94607
+ }
94412
94608
  function describeReviewItem(item) {
94413
94609
  switch (item.type) {
94414
94610
  case "proposal":
@@ -94450,7 +94646,7 @@ function describeReviewItem(item) {
94450
94646
  ] : [
94451
94647
  `Session ${item.sessionId}${item.workspaceId !== undefined ? ` (workspace ${item.workspaceId})` : ""}`,
94452
94648
  `Last seen ${item.lastSeenAt}`,
94453
- "No proposal, terminal state, or unbound-candidate artifact recorded.",
94649
+ "Why unknown: " + unknownReasonDetail(item.reason),
94454
94650
  "",
94455
94651
  `Investigate: keryx sessions list / keryx shell -r ${item.sessionId}`
94456
94652
  ];
@@ -101903,6 +102099,44 @@ function parseBusCommand(line) {
101903
102099
  return { kind: "error", reason: `unknown /bus subcommand "${first}"` };
101904
102100
  }
101905
102101
 
102102
+ // src/tui/bus-join.ts
102103
+ function decideJoinAdoption(input) {
102104
+ if (input.disabled)
102105
+ return "off";
102106
+ if (input.destroyed)
102107
+ return "leave";
102108
+ return "adopt";
102109
+ }
102110
+ function buildBusJoinCallbacks(deps) {
102111
+ return {
102112
+ onEvent: (event) => {
102113
+ if (deps.isDestroyed())
102114
+ return;
102115
+ deps.onSystemLine(`${formatBusEventLine(event)}
102116
+ `);
102117
+ deps.pushToInbox({ ...event, body: event.body ?? "" });
102118
+ deps.markDelivered();
102119
+ },
102120
+ onPeers: (peers) => {
102121
+ if (deps.isDestroyed())
102122
+ return;
102123
+ deps.setFleetPeers(peers.map((peer) => ({
102124
+ name: peer.record.name,
102125
+ state: peer.state,
102126
+ status: peer.record.status,
102127
+ activity: peer.record.activity
102128
+ })));
102129
+ deps.paintFleet();
102130
+ deps.onInboxSizeObserved();
102131
+ deps.paintHoldBanner();
102132
+ deps.onLeaseHoldPoll();
102133
+ const delivered = deps.getDelivered();
102134
+ deps.onBusWakePoll(delivered);
102135
+ deps.resetDelivered();
102136
+ }
102137
+ };
102138
+ }
102139
+
101906
102140
  // src/tui/shell-exit.ts
101907
102141
  function leaveBusThenRelease(holder) {
101908
102142
  holder.leaveBus();
@@ -102090,6 +102324,9 @@ var SIDE_WORKER_DENIED_TOOL_NAMES = new Set([
102090
102324
  "shell_job_output",
102091
102325
  "bus_send"
102092
102326
  ]);
102327
+ function isToolAvailableToSideWorker(tool) {
102328
+ return tool.definition.risk === "read" && !SIDE_WORKER_DENIED_TOOL_NAMES.has(tool.definition.name);
102329
+ }
102093
102330
  function parseGitHubRemote(remote) {
102094
102331
  const trimmed = remote.trim();
102095
102332
  if (trimmed.length === 0) {
@@ -103057,7 +103294,7 @@ function runSearchProviderTestStep(otui, target, controller, provider, fields, c
103057
103294
  }
103058
103295
  if (!tested.ok) {
103059
103296
  settled = "failure";
103060
- const reason = tested.reason === "missing-credential" ? "missing credential" : "connection validation failed";
103297
+ const reason = describeConnectionFailure(tested.reason);
103061
103298
  status.content = otui.t`${otui.red("\u2717")} ${otui.bold(`'${provider.id}' test failed: ${reason}`)} ${otui.dim("(Esc to go back and retry)")}`;
103062
103299
  return;
103063
103300
  }
@@ -104521,11 +104758,10 @@ async function launchTuiAgentShell(opts) {
104521
104758
  };
104522
104759
  deps = { ...deps, onContextCompaction };
104523
104760
  liveDeps = deps;
104524
- let removeSplash;
104761
+ let splash = undefined;
104525
104762
  const applyOpened = (opened, previewHistory) => {
104526
104763
  if (opened.history.length > 0) {
104527
- removeSplash?.();
104528
- removeSplash = undefined;
104764
+ splash?.removeIfShown();
104529
104765
  }
104530
104766
  liveSession = opened.handle;
104531
104767
  history = previewHistory === true ? opened.history.slice(-SESSION_PREVIEW_MESSAGE_COUNT) : opened.history;
@@ -104683,17 +104919,37 @@ async function launchTuiAgentShell(opts) {
104683
104919
  return true;
104684
104920
  }
104685
104921
  bindSlateToLiveSession();
104686
- if (history.length === 0) {
104687
- removeSplash = mountEmptyTranscriptSplash(otui, r, transcript);
104688
- }
104922
+ splash = createSplashLifecycle({
104923
+ mount: () => mountEmptyTranscriptSplash(otui, r, transcript),
104924
+ initialHistoryLength: history.length
104925
+ });
104689
104926
  if (viewedReadOnly) {
104690
- removeSplash?.();
104691
- removeSplash = undefined;
104927
+ splash.removeIfShown();
104692
104928
  }
104693
104929
  refreshWorkspaceSidebar();
104694
104930
  refreshReviewSidebar();
104695
104931
  (async () => {
104696
104932
  try {
104933
+ const { onEvent, onPeers } = buildBusJoinCallbacks({
104934
+ isDestroyed: () => destroyed,
104935
+ onSystemLine: (line) => io.onSystem?.(line),
104936
+ pushToInbox: (event) => busInbox.push(event),
104937
+ markDelivered: () => {
104938
+ busPollDeliveredEvent = true;
104939
+ },
104940
+ setFleetPeers: (peers) => {
104941
+ busFleetPeers = peers;
104942
+ },
104943
+ paintFleet,
104944
+ onInboxSizeObserved: () => busDropNotifier.onInboxSizeObserved(busInbox.size),
104945
+ paintHoldBanner,
104946
+ onLeaseHoldPoll: () => leaseHoldController?.onPoll(),
104947
+ onBusWakePoll: (delivered) => busWakeController?.onPoll(delivered),
104948
+ getDelivered: () => busPollDeliveredEvent,
104949
+ resetDelivered: () => {
104950
+ busPollDeliveredEvent = false;
104951
+ }
104952
+ });
104697
104953
  const joined = await joinBus({
104698
104954
  cwd: sessionCwd,
104699
104955
  sessionId: liveSession.summary.id,
@@ -104708,37 +104964,15 @@ async function launchTuiAgentShell(opts) {
104708
104964
  }),
104709
104965
  onError: makeBusErrorReporter((line) => io.onSystem?.(`${line}
104710
104966
  `)),
104711
- onEvent: (event) => {
104712
- if (destroyed)
104713
- return;
104714
- io.onSystem?.(`${formatBusEventLine(event)}
104715
- `);
104716
- busInbox.push({ ...event, body: event.body ?? "" });
104717
- busPollDeliveredEvent = true;
104718
- },
104719
- onPeers: (peers) => {
104720
- if (destroyed)
104721
- return;
104722
- busFleetPeers = peers.map((peer) => ({
104723
- name: peer.record.name,
104724
- state: peer.state,
104725
- status: peer.record.status,
104726
- activity: peer.record.activity
104727
- }));
104728
- paintFleet();
104729
- busDropNotifier.onInboxSizeObserved(busInbox.size);
104730
- paintHoldBanner();
104731
- leaseHoldController?.onPoll();
104732
- busWakeController?.onPoll(busPollDeliveredEvent);
104733
- busPollDeliveredEvent = false;
104734
- }
104967
+ onEvent,
104968
+ onPeers
104735
104969
  });
104736
104970
  if ("disabled" in joined) {
104737
104971
  io.onSystem?.(`bus: off (${joined.disabled})
104738
104972
  `);
104739
104973
  return;
104740
104974
  }
104741
- if (destroyed) {
104975
+ if (decideJoinAdoption({ destroyed, disabled: false }) === "leave") {
104742
104976
  joined.leave();
104743
104977
  return;
104744
104978
  }
@@ -105560,7 +105794,7 @@ Staying in the current session.
105560
105794
  let answer = "";
105561
105795
  try {
105562
105796
  const base = await opts.makeAgentDeps(currentSel, liveSlateSession, undefined);
105563
- const tools = base.tools.filter((t) => t.definition.risk === "read" && !SIDE_WORKER_DENIED_TOOL_NAMES.has(t.definition.name));
105797
+ const tools = base.tools.filter(isToolAvailableToSideWorker);
105564
105798
  const sideDeps = {
105565
105799
  ...base,
105566
105800
  tools,
@@ -105636,8 +105870,7 @@ Staying in the current session.
105636
105870
  suggestionGate.cancel("new turn started");
105637
105871
  if (origin === "operator") {
105638
105872
  consecutiveAutoWakes = 0;
105639
- removeSplash?.();
105640
- removeSplash = undefined;
105873
+ splash?.removeIfShown();
105641
105874
  }
105642
105875
  const displayLine = summarizeSubmittedLine(line);
105643
105876
  if (chrome.isBusy()) {
@@ -105988,7 +106221,7 @@ Staying in the current session.
105988
106221
  searchProviderController.configure(providerId, { ...descriptor.defaults, ...args.fields }, args.credential);
105989
106222
  const tested = await searchProviderController.test(providerId);
105990
106223
  if (!tested.ok) {
105991
- const reason = tested.reason === "missing-credential" ? "missing credential" : "connection validation failed";
106224
+ const reason = describeConnectionFailure(tested.reason);
105992
106225
  io.onSystem?.(`Configured '${providerId}' but it is not connected yet: ${reason}. Run /search-provider ${providerId} key=<value> to re-test.
105993
106226
  `);
105994
106227
  return;
@@ -106491,8 +106724,7 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
106491
106724
  cap: () => resolveMaxAutoWake(),
106492
106725
  runWake: () => runLine("", "bus-message"),
106493
106726
  printCapped: () => {
106494
- io.onSystem?.(`\u25C7 a peer message arrived; automatic wakes are capped, so it will be delivered with your next message.
106495
- `);
106727
+ io.onSystem?.(BUS_WAKE_CAPPED_NOTICE);
106496
106728
  },
106497
106729
  hasBusDeps: () => deps.busInbox !== undefined
106498
106730
  });
@@ -107987,8 +108219,8 @@ async function runAgentRepl(lines, rich, deps, metaprojectPort, sessionOpts, ini
107987
108219
  }
107988
108220
  };
107989
108221
  const searchProviderController = createDefaultSearchProviderController();
107990
- const sessionShellAllow = new Set(loadShellPermissions().allow);
107991
- let fingerprintAtStart = shellPermissionsFingerprint();
108222
+ const sessionShellAllow = new Set(loadShellPermissions(configDir).allow);
108223
+ let fingerprintAtStart = shellPermissionsFingerprint(configDir);
107992
108224
  let permissionMigrationShown = false;
107993
108225
  let permissionTamperShown = false;
107994
108226
  const iterator = lines[Symbol.asyncIterator]();
@@ -108225,10 +108457,11 @@ ${GUTTER}${style.yellow(`Run: ${evaled.command}`)} ${style.dim(prompt)}`);
108225
108457
  const approved = always || /^y(es)?$/i.test(answer);
108226
108458
  if (always && approved) {
108227
108459
  const stored = rememberExactShellGrant(evaled.command, sessionShellAllow, {
108228
- publishLease: evaled.publishLease
108460
+ publishLease: evaled.publishLease,
108461
+ ...configDir !== undefined ? { dir: configDir } : {}
108229
108462
  });
108230
108463
  if (stored.length > 0) {
108231
- fingerprintAtStart = shellPermissionsFingerprint();
108464
+ fingerprintAtStart = shellPermissionsFingerprint(configDir);
108232
108465
  }
108233
108466
  out(stored.length > 0 ? style.green(`approved \xB7 remembered \u201C${stored}\u201D
108234
108467
  `) : style.yellow(`approved once \xB7 pattern cannot be remembered
@@ -108825,7 +109058,7 @@ ${GUTTER}${turnSeparator()}
108825
109058
  searchProviderController.configure(providerId, { ...descriptor.defaults, ...args.fields }, args.credential);
108826
109059
  const tested = await searchProviderController.test(providerId);
108827
109060
  if (!tested.ok) {
108828
- const reason = tested.reason === "missing-credential" ? "missing credential" : "connection validation failed";
109061
+ const reason = describeConnectionFailure(tested.reason);
108829
109062
  agentIo.onSystem?.(`Configured '${providerId}' but it is not connected yet: ${reason}. Run /search-provider ${providerId} key=<value> to re-test.
108830
109063
  `);
108831
109064
  continue;
@@ -111528,8 +111761,8 @@ async function runServe(args) {
111528
111761
  resolve();
111529
111762
  });
111530
111763
  };
111531
- process.once("SIGINT", finish);
111532
- process.once("SIGTERM", finish);
111764
+ process.on("SIGINT", finish);
111765
+ process.on("SIGTERM", finish);
111533
111766
  });
111534
111767
  }
111535
111768
  function overlay(stored, parsed, port, runtimeAck) {
@@ -115790,7 +116023,8 @@ async function workspaceCommand(args) {
115790
116023
  if (subcommand === "create") {
115791
116024
  rejectUnknownOptions(args.slice(1), new Set(["--title", "--component"]));
115792
116025
  const title = optionValue(args, "--title");
115793
- const component = optionValue(args, "--component");
116026
+ const rawComponent = optionValue(args, "--component");
116027
+ const component = rawComponent === undefined ? undefined : requireWorkspaceReference(rawComponent);
115794
116028
  if (!title)
115795
116029
  throw new Error("Usage: keryx workspace create --title <title> [--component <workspace-relative-ref>]");
115796
116030
  const workspace = await service6().create({ request: undefined, requestCorrelationId: randomUUID40(), id: newWorkspaceId(), title, ...component ? { component: { kind: "component", uri: component } } : {} });
@@ -115819,11 +116053,11 @@ async function workspaceCommand(args) {
115819
116053
  rejectUnknownOptions(args.slice(2), new Set(["--kind", "--uri", "--revision"]));
115820
116054
  const workspaceId = args[1];
115821
116055
  const kind = optionValue(args, "--kind");
115822
- const uri = optionValue(args, "--uri");
116056
+ const rawUri = optionValue(args, "--uri");
115823
116057
  const revision = optionValue(args, "--revision");
115824
- if (!workspaceId || !kind || !uri)
116058
+ if (!workspaceId || !kind || !rawUri)
115825
116059
  throw new Error("Usage: keryx workspace add-resource <workspace-id> --kind <kind> --uri <workspace-relative-ref> [--revision <revision>]");
115826
- console.log(JSON.stringify(await service6().addResource({ request: undefined, requestCorrelationId: randomUUID40(), workspaceId, resource: { kind, uri, ...revision ? { revision } : {} } }), null, 2));
116060
+ console.log(JSON.stringify(await service6().addResource({ request: undefined, requestCorrelationId: randomUUID40(), workspaceId, resource: { kind, uri: requireWorkspaceReference(rawUri), ...revision ? { revision } : {} } }), null, 2));
115827
116061
  return;
115828
116062
  }
115829
116063
  if (subcommand === "archive") {
@@ -116097,7 +116331,7 @@ function renderCatchUp(report, includeLifecycleFlags = true) {
116097
116331
  sections.push(renderSection("Pending proposals", report.proposals, (item) => `- Accept, reject, or dismiss ${item.kind} proposal ${item.proposalId} in workspace ${item.workspaceId}` + `${item.note !== undefined ? `: "${item.note}"` : ""}? ` + `Recommendation: ${item.fresh ? "evidence is fresh \u2014 review now (`keryx workspace review " + item.workspaceId + " " + item.proposalId + " --decision <accepted|rejected|dismissed>`)" : "evidence has drifted since this proposal was created \u2014 treat as stale, re-run wrap-up before deciding"}.`));
116098
116332
  sections.push(renderSection("Blocked sessions (stopped unattended)", report.blocked, (item) => `- Session ${item.sessionId} stopped unattended (${item.terminalState.reason}) at ${item.terminalState.occurredAt}. Resume it, or archive and move on? ` + `Recommendation: \`keryx shell -r ${item.sessionId}\` to resume and unblock it.`));
116099
116333
  sections.push(renderSection("Unbound candidates (wrap-up ran, no workspace bound)", report.unboundCandidates, (item) => `- Session ${item.sessionId} produced untriaged seeds with no workspace bound (${item.summary}). Bind to a workspace and propose, or discard? ` + `Recommendation: pick a workspace, then \`keryx workspace propose <workspace-id> --kind <kind> --session ${item.sessionId}\` (evidence: ${item.evidencePath}).`));
116100
- sections.push(renderSection("Unknown (no resolution recorded)", report.unknown, (item) => `- Session ${item.sessionId} was last seen ${item.lastSeenAt} with no proposal, terminal state, or unbound-candidate artifact recorded. Investigate, or ignore? ` + `Recommendation: \`keryx sessions list\` / \`keryx shell -r ${item.sessionId}\` to see what happened.`));
116334
+ sections.push(renderSection("Unknown (no resolution recorded)", report.unknown, (item) => `- Session ${item.sessionId} was last seen ${item.lastSeenAt}: ${describeUnknownReason(item)}. Investigate, or ignore? ` + `Recommendation: \`keryx sessions list\` / \`keryx shell -r ${item.sessionId}\` to see what happened.`));
116101
116335
  sections.push(renderSection("Unreviewed SAC-owned changes (no proposal on record)", report.unreviewedPaths, (item) => `- Session ${item.sessionId} changed ${item.owner} path \`${item.path}\`${item.status !== undefined ? ` (Status: ${item.status})` : ""} at ${item.changedAt} with NO SAC proposal/receipt behind it \u2014 this looks like it bypassed review. Was this reviewed some other way, or should it be? ` + `Recommendation: \`keryx shell -r ${item.sessionId}\` to see what happened; if the content is good, route it through a real proposal (\`keryx workspace propose ...\`) before trusting it as durable knowledge.`));
116102
116336
  if (includeLifecycleFlags) {
116103
116337
  sections.push(renderSection("Lifecycle flags (component no longer in the graph)", report.lifecycleFlags, (item) => `- ${item.kind} \`${item.ref}\` scopes to \`${item.missingComponent}\`, which is no longer in the code graph (flagged ${item.flaggedAt}). Still relevant, or safe to clean up? ` + `Recommendation: this is report-only \u2014 nothing was archived/edited/removed automatically; ${item.kind === "workspace" ? "`keryx workspace archive " + item.ref + "`" : item.kind === "memory-entry" ? "`keryx memory supersede` or edit the entry directly" : "edit or remove the wiki page directly"} if you decide it's actually stale.`));
@@ -116106,6 +116340,16 @@ function renderCatchUp(report, includeLifecycleFlags = true) {
116106
116340
 
116107
116341
  `);
116108
116342
  }
116343
+ function describeUnknownReason(item) {
116344
+ switch (item.reason) {
116345
+ case "wrap-up-failed":
116346
+ return "a wrap-up dispatch ran and every group failed (re-run it after fixing credentials/conflicts)";
116347
+ case "slate-unreadable":
116348
+ return "its slate.json is present but could not be read - the session's own record is damaged, not merely unrecorded";
116349
+ case "no-resolution-recorded":
116350
+ return "it has Slate engagement but no proposal, terminal state, unbound-candidate or wrap-up-outcome artifact";
116351
+ }
116352
+ }
116109
116353
  function renderSection(title, items, describe) {
116110
116354
  const lines = [`== ${title} ==`];
116111
116355
  if (items.length === 0)
package/dist/core.js CHANGED
@@ -20387,10 +20387,12 @@ __export(exports_service9, {
20387
20387
  normalizeCollaborationResult: () => normalizeCollaborationResult,
20388
20388
  normalizeFwkResult: () => normalizeFwkResult,
20389
20389
  normalizeProposalLifecycleResult: () => normalizeProposalLifecycleResult,
20390
+ normalizeWorkspaceReference: () => normalizeWorkspaceReference,
20390
20391
  proposalNotePath: () => proposalNotePath,
20391
20392
  readExternalSlate: () => readExternalSlate,
20392
20393
  reclaimStaleExternalSlates: () => reclaimStaleExternalSlates,
20393
20394
  redactSensitiveText: () => redactSensitiveText,
20395
+ requireWorkspaceReference: () => requireWorkspaceReference,
20394
20396
  resolveOrCreateWorkspace: () => resolveOrCreateWorkspace,
20395
20397
  resolvePolicyExperiment: () => resolvePolicyExperiment,
20396
20398
  rollbackPolicyExperiment: () => rollbackPolicyExperiment,
@@ -20401,8 +20403,50 @@ __export(exports_service9, {
20401
20403
  });
20402
20404
 
20403
20405
  // src/sac/fwk-service.ts
20404
- init_assembly();
20405
20406
  import { createHash as createHash15, randomUUID as randomUUID4 } from "crypto";
20407
+
20408
+ // src/sac/workspace-reference.ts
20409
+ var WORKSPACE_REFERENCE_PATTERN = /^\.\/(?!(?:.*\/)?\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
20410
+ var UNSAFE_ABSOLUTE = /^[/\\]/;
20411
+ var UNSAFE_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
20412
+ function requireWorkspaceReference(raw) {
20413
+ const normalized = normalizeWorkspaceReference(raw);
20414
+ if (!normalized.ok)
20415
+ throw new Error(normalized.reason);
20416
+ return normalized.uri;
20417
+ }
20418
+ function normalizeWorkspaceReference(raw) {
20419
+ const trimmed = raw.trim();
20420
+ if (trimmed.length === 0) {
20421
+ return { ok: false, reason: 'a workspace reference must be a non-empty workspace-relative path, e.g. "./src/harness/search"' };
20422
+ }
20423
+ if (UNSAFE_ABSOLUTE.test(trimmed) || UNSAFE_SCHEME.test(trimmed) || trimmed.includes("\\")) {
20424
+ return {
20425
+ ok: false,
20426
+ reason: `"${trimmed}" is not a workspace-relative path \u2014 an absolute path, a URL and a Windows path are all refused. Use a path inside the project, e.g. "./src/harness/search".`
20427
+ };
20428
+ }
20429
+ const withoutDotSlash = trimmed.startsWith("./") ? trimmed.slice(2) : trimmed;
20430
+ const withoutTrailingSlash = withoutDotSlash.replace(/\/+$/, "");
20431
+ if (withoutTrailingSlash.length === 0) {
20432
+ return { ok: false, reason: `"${trimmed}" names the project root rather than a path inside it \u2014 name a directory or file, e.g. "./src/harness/search".` };
20433
+ }
20434
+ const segments = withoutTrailingSlash.split("/");
20435
+ if (segments.includes("..") || segments.includes(".")) {
20436
+ return { ok: false, reason: `"${trimmed}" contains a "." or ".." path segment \u2014 a workspace reference must stay inside the project.` };
20437
+ }
20438
+ const uri = `./${withoutTrailingSlash}`;
20439
+ if (!WORKSPACE_REFERENCE_PATTERN.test(uri)) {
20440
+ return {
20441
+ ok: false,
20442
+ reason: `"${trimmed}" is not a usable workspace reference. The stored form is "${uri}" if the characters are the problem: ` + 'each path segment may contain only letters, digits, ".", "_" and "-", and no ".." segment is allowed.'
20443
+ };
20444
+ }
20445
+ return { ok: true, uri };
20446
+ }
20447
+
20448
+ // src/sac/fwk-service.ts
20449
+ init_assembly();
20406
20450
  import { appendFile as appendFile5, mkdir as mkdir28, open as open2, readFile as readFile40, rename as rename6, rm as rm5, stat as stat9, writeFile as writeFile21 } from "fs/promises";
20407
20451
  import path78 from "path";
20408
20452
 
@@ -20415,7 +20459,7 @@ var idPattern = /^[a-z][a-z0-9-]{2,63}$/;
20415
20459
  var subjectPattern = /^(?:user|team|service|agent):[a-z0-9][a-z0-9._-]{0,127}$/;
20416
20460
  var revisionPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
20417
20461
  var correlationPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{15,255}$/;
20418
- var workspacePathPattern = /^\.\/(?!.*(?:^|\/)\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
20462
+ var workspacePathPattern = WORKSPACE_REFERENCE_PATTERN;
20419
20463
  var receiptHashPattern = /^[a-f0-9]{64}$/;
20420
20464
  var utcPattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?Z$/;
20421
20465
  var forbiddenPayloadKeys = new Set(["prompt", "transcript", "hiddenReasoning", "secret", "secrets", "rawContent"]);
@@ -21661,7 +21705,7 @@ var stableJson2 = (value) => JSON.stringify(stableValue(value));
21661
21705
  var sha2562 = (value) => createHash14("sha256").update(stableJson2(value), "utf8").digest("hex");
21662
21706
  var sha256Content = (value) => createHash14("sha256").update(value).digest("hex");
21663
21707
  var pseudonym = (key, value) => createHmac2("sha256", key).update(value, "utf8").digest("hex");
21664
- var workspacePathPattern2 = /^\.\/(?!.*(?:^|\/)\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
21708
+ var workspacePathPattern2 = WORKSPACE_REFERENCE_PATTERN;
21665
21709
  var trustedOutcomes = new WeakMap;
21666
21710
  var trustedSandboxEvidence = new WeakMap;
21667
21711
  var trustedSandboxExecutions = new WeakMap;
@@ -22138,7 +22182,7 @@ var metadataOnly = (value) => {
22138
22182
  var nowIso = (now) => now().toISOString();
22139
22183
  var reportHashPattern = /^[a-f0-9]{64}$/;
22140
22184
  var immutableVersionPattern = /(?:^|[-_.:])(?:latest|next|head|main|develop)(?:$|[-_.:])/i;
22141
- var workspacePathPattern3 = /^\.\/(?!.*(?:^|\/)\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
22185
+ var workspacePathPattern3 = WORKSPACE_REFERENCE_PATTERN;
22142
22186
  var policyExperimentConfigPath = [".metaproject", "context-operations", "policy-experiment", "config.json"];
22143
22187
  var stableValue2 = (value) => {
22144
22188
  if (Array.isArray(value))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.128",
3
+ "version": "0.2.131",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {