@mrciphersmith/keryx 0.2.130 → 0.2.132

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.130",
51132
+ version: "0.2.132",
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: {
@@ -59054,7 +59098,7 @@ init_provenance();
59054
59098
 
59055
59099
  // src/sync/diff.ts
59056
59100
  init_provenance();
59057
- var CODE_EXT2 = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|php|c|h|cc|cpp|hpp|cs|swift|kt|scala|sh|vue|svelte)$/;
59101
+ var CODE_EXT2 = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rs|java|rb|php|c|h|cc|cpp|hpp|cs|swift|kt|scala|sh|vue|svelte)$/;
59058
59102
  function isCodeFile(file) {
59059
59103
  return CODE_EXT2.test(file);
59060
59104
  }
@@ -59176,7 +59220,22 @@ On git pull / branch switch, they run 'keryx sync' (advisory report). Run 'keryx
59176
59220
  }
59177
59221
  const code = codeOnly(diff);
59178
59222
  if (totalChanges(code) === 0) {
59179
- console.log(` up to date (built at ${provenance.commit.slice(0, 8)})`);
59223
+ if (apply && provenance.commit !== head.commit) {
59224
+ if (module === "gdwiki") {
59225
+ const gate = await resolveWikiSourceGate(cwd, HEAD_NOT_REQUESTED);
59226
+ if (gate.status !== "fresh") {
59227
+ printApplyOutcome({ recorded: false, gate }, "", ` up to date (built at ${provenance.commit.slice(0, 8)}); HEAD moved to ${head.commit.slice(0, 8)} with no code changes, but provenance NOT advanced`);
59228
+ console.log("");
59229
+ continue;
59230
+ }
59231
+ }
59232
+ await recordProvenance(cwd, module, at);
59233
+ console.log(` up to date (built at ${provenance.commit.slice(0, 8)}); no code changed \u2014 provenance advanced to ${head.commit.slice(0, 8)}`);
59234
+ } else if (provenance.commit !== head.commit) {
59235
+ console.log(` up to date (built at ${provenance.commit.slice(0, 8)}); HEAD moved to ${head.commit.slice(0, 8)} with no code changes \u2014 run \`keryx sync --apply\` to advance provenance`);
59236
+ } else {
59237
+ console.log(` up to date (built at ${provenance.commit.slice(0, 8)})`);
59238
+ }
59180
59239
  console.log("");
59181
59240
  continue;
59182
59241
  }
@@ -78293,6 +78352,7 @@ function toMcpTools(ops = METAPROJECT_OPERATIONS, adapterFor = (cwd) => createMe
78293
78352
  }
78294
78353
 
78295
78354
  // src/sac/fwk-service.ts
78355
+ init_workspace_reference();
78296
78356
  init_assembly();
78297
78357
  init_sac();
78298
78358
  init_workspace_service();
@@ -78303,6 +78363,7 @@ import { appendFile as appendFile7, mkdir as mkdir59, open as open2, readFile as
78303
78363
  import path174 from "path";
78304
78364
 
78305
78365
  // src/sac/policy-experiment.ts
78366
+ init_workspace_reference();
78306
78367
  import { createHash as createHash34, createHmac as createHmac2 } from "crypto";
78307
78368
 
78308
78369
  // src/sac/receipt-integrity.ts
@@ -78366,7 +78427,7 @@ function stableValue(value) {
78366
78427
  }
78367
78428
  var stableJson2 = (value) => JSON.stringify(stableValue(value));
78368
78429
  var sha2564 = (value) => createHash34("sha256").update(stableJson2(value), "utf8").digest("hex");
78369
- var workspacePathPattern2 = /^\.\/(?!.*(?:^|\/)\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
78430
+ var workspacePathPattern2 = WORKSPACE_REFERENCE_PATTERN;
78370
78431
  var trustedOutcomes = new WeakMap;
78371
78432
  var trustedSandboxEvidence = new WeakMap;
78372
78433
  var trustedSandboxExecutions = new WeakMap;
@@ -78458,7 +78519,7 @@ var metadataOnly = (value) => {
78458
78519
  var nowIso2 = (now) => now().toISOString();
78459
78520
  var reportHashPattern = /^[a-f0-9]{64}$/;
78460
78521
  var immutableVersionPattern = /(?:^|[-_.:])(?:latest|next|head|main|develop)(?:$|[-_.:])/i;
78461
- var workspacePathPattern3 = /^\.\/(?!.*(?:^|\/)\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
78522
+ var workspacePathPattern3 = WORKSPACE_REFERENCE_PATTERN;
78462
78523
  var policyExperimentConfigPath = [".metaproject", "context-operations", "policy-experiment", "config.json"];
78463
78524
  var stableValue2 = (value) => {
78464
78525
  if (Array.isArray(value))
@@ -79333,6 +79394,7 @@ async function reclaimStaleExternalSlates(cwd, now = () => new Date) {
79333
79394
  init_slate();
79334
79395
  init_workspace_resolve();
79335
79396
  init_redact();
79397
+ init_workspace_reference();
79336
79398
 
79337
79399
  // src/mcp/tools.ts
79338
79400
  import { randomUUID as randomUUID19 } from "crypto";
@@ -79545,7 +79607,8 @@ function buildToolRegistry() {
79545
79607
  const title = stringParam2(params, "title")?.trim() ?? "";
79546
79608
  if (title.length === 0)
79547
79609
  throw new Error("sac.workspaceCreate requires a non-empty 'title'");
79548
- const component = stringParam2(params, "component");
79610
+ const rawComponent = stringParam2(params, "component");
79611
+ const component = rawComponent === undefined ? undefined : requireWorkspaceReference(rawComponent);
79549
79612
  const workspace = await sacWorkspaceService(cwd).create({ request: undefined, requestCorrelationId: randomUUID19(), id: newWorkspaceId(), title, ...component ? { component: { kind: "component", uri: component } } : {} });
79550
79613
  return workspace;
79551
79614
  }
@@ -87467,7 +87530,12 @@ function workspaceCreateTool(cwd, getSessionDir) {
87467
87530
  const title = typeof input.title === "string" ? input.title.trim() : "";
87468
87531
  if (title.length === 0)
87469
87532
  return { output: "workspace_create requires a non-empty 'title'", isError: true };
87470
- const component = typeof input.component === "string" && input.component.length > 0 ? input.component : undefined;
87533
+ const rawComponent = typeof input.component === "string" && input.component.length > 0 ? input.component : undefined;
87534
+ const normalizedComponent = rawComponent === undefined ? undefined : normalizeWorkspaceReference(rawComponent);
87535
+ if (normalizedComponent !== undefined && !normalizedComponent.ok) {
87536
+ return { output: "workspace_create: " + normalizedComponent.reason, isError: true };
87537
+ }
87538
+ const component = normalizedComponent?.ok === true ? normalizedComponent.uri : undefined;
87471
87539
  try {
87472
87540
  const workspace = await service5(cwd).create({
87473
87541
  request: undefined,
@@ -93273,14 +93341,25 @@ async function classifySession(session) {
93273
93341
  sessionId: session.id,
93274
93342
  ...workspaceId !== undefined ? { workspaceId } : {},
93275
93343
  lastSeenAt: session.updatedAt,
93344
+ reason: "wrap-up-failed",
93276
93345
  wrapUpOutcome: { trigger: wrapUpOutcome.trigger, generatedAt: wrapUpOutcome.generatedAt, groups: wrapUpOutcome.groups }
93277
93346
  }
93278
93347
  };
93279
93348
  }
93280
93349
  if (!await isSlateEngaged(dir))
93281
93350
  return;
93282
- const workspaceId = (await safeReadSlate(dir))?.workspaceId;
93283
- return { kind: "unknown", item: { type: "unknown", sessionId: session.id, ...workspaceId !== undefined ? { workspaceId } : {}, lastSeenAt: session.updatedAt } };
93351
+ const slateRead = await readSlateState(dir);
93352
+ const workspaceId = slateRead.state === "ok" ? slateRead.slate.workspaceId : undefined;
93353
+ return {
93354
+ kind: "unknown",
93355
+ item: {
93356
+ type: "unknown",
93357
+ sessionId: session.id,
93358
+ ...workspaceId !== undefined ? { workspaceId } : {},
93359
+ lastSeenAt: session.updatedAt,
93360
+ reason: slateRead.state === "unreadable" ? "slate-unreadable" : "no-resolution-recorded"
93361
+ }
93362
+ };
93284
93363
  }
93285
93364
  async function readExternalUnboundCandidates(cwd) {
93286
93365
  const extDir = externalSlatesDir(cwd);
@@ -93533,6 +93612,17 @@ async function isSlateEngaged(dir) {
93533
93612
  return false;
93534
93613
  }
93535
93614
  }
93615
+ async function readSlateState(dir) {
93616
+ try {
93617
+ const slate = await readSlate(dir);
93618
+ return slate === undefined ? { state: "absent" } : { state: "ok", slate };
93619
+ } catch (error) {
93620
+ return isNotFound2(error) ? { state: "absent" } : { state: "unreadable" };
93621
+ }
93622
+ }
93623
+ function isNotFound2(error) {
93624
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
93625
+ }
93536
93626
  async function safeReadSlate(dir) {
93537
93627
  try {
93538
93628
  return await readSlate(dir);
@@ -94498,7 +94588,7 @@ function summarizeReviewItem(item) {
94498
94588
  case "unbound-candidate":
94499
94589
  return `${item.sessionId} \u2014 ${item.summary}`;
94500
94590
  case "unknown":
94501
- return `${item.sessionId} \u2014 last seen ${item.lastSeenAt}`;
94591
+ return `${item.sessionId} \u2014 ${unknownReasonLabel(item.reason)} (last seen ${item.lastSeenAt})`;
94502
94592
  }
94503
94593
  }
94504
94594
  function formatReviewListLines(items, selected) {
@@ -94510,6 +94600,26 @@ function formatReviewListLines(items, selected) {
94510
94600
  return `${mark} ${TYPE_LABEL[item.type].padEnd(8)} ${summarizeReviewItem(item)}`;
94511
94601
  });
94512
94602
  }
94603
+ function unknownReasonLabel(reason) {
94604
+ switch (reason) {
94605
+ case "wrap-up-failed":
94606
+ return "wrap-up failed";
94607
+ case "slate-unreadable":
94608
+ return "slate.json unreadable";
94609
+ case "no-resolution-recorded":
94610
+ return "no resolution recorded";
94611
+ }
94612
+ }
94613
+ function unknownReasonDetail(reason) {
94614
+ switch (reason) {
94615
+ case "wrap-up-failed":
94616
+ 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.";
94617
+ case "slate-unreadable":
94618
+ 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.";
94619
+ case "no-resolution-recorded":
94620
+ return "Slate engagement with no proposal, terminal state, unbound-candidate or wrap-up-outcome artifact recorded.";
94621
+ }
94622
+ }
94513
94623
  function describeReviewItem(item) {
94514
94624
  switch (item.type) {
94515
94625
  case "proposal":
@@ -94551,7 +94661,7 @@ function describeReviewItem(item) {
94551
94661
  ] : [
94552
94662
  `Session ${item.sessionId}${item.workspaceId !== undefined ? ` (workspace ${item.workspaceId})` : ""}`,
94553
94663
  `Last seen ${item.lastSeenAt}`,
94554
- "No proposal, terminal state, or unbound-candidate artifact recorded.",
94664
+ "Why unknown: " + unknownReasonDetail(item.reason),
94555
94665
  "",
94556
94666
  `Investigate: keryx sessions list / keryx shell -r ${item.sessionId}`
94557
94667
  ];
@@ -115928,7 +116038,8 @@ async function workspaceCommand(args) {
115928
116038
  if (subcommand === "create") {
115929
116039
  rejectUnknownOptions(args.slice(1), new Set(["--title", "--component"]));
115930
116040
  const title = optionValue(args, "--title");
115931
- const component = optionValue(args, "--component");
116041
+ const rawComponent = optionValue(args, "--component");
116042
+ const component = rawComponent === undefined ? undefined : requireWorkspaceReference(rawComponent);
115932
116043
  if (!title)
115933
116044
  throw new Error("Usage: keryx workspace create --title <title> [--component <workspace-relative-ref>]");
115934
116045
  const workspace = await service6().create({ request: undefined, requestCorrelationId: randomUUID40(), id: newWorkspaceId(), title, ...component ? { component: { kind: "component", uri: component } } : {} });
@@ -115957,11 +116068,11 @@ async function workspaceCommand(args) {
115957
116068
  rejectUnknownOptions(args.slice(2), new Set(["--kind", "--uri", "--revision"]));
115958
116069
  const workspaceId = args[1];
115959
116070
  const kind = optionValue(args, "--kind");
115960
- const uri = optionValue(args, "--uri");
116071
+ const rawUri = optionValue(args, "--uri");
115961
116072
  const revision = optionValue(args, "--revision");
115962
- if (!workspaceId || !kind || !uri)
116073
+ if (!workspaceId || !kind || !rawUri)
115963
116074
  throw new Error("Usage: keryx workspace add-resource <workspace-id> --kind <kind> --uri <workspace-relative-ref> [--revision <revision>]");
115964
- console.log(JSON.stringify(await service6().addResource({ request: undefined, requestCorrelationId: randomUUID40(), workspaceId, resource: { kind, uri, ...revision ? { revision } : {} } }), null, 2));
116075
+ console.log(JSON.stringify(await service6().addResource({ request: undefined, requestCorrelationId: randomUUID40(), workspaceId, resource: { kind, uri: requireWorkspaceReference(rawUri), ...revision ? { revision } : {} } }), null, 2));
115965
116076
  return;
115966
116077
  }
115967
116078
  if (subcommand === "archive") {
@@ -116235,7 +116346,7 @@ function renderCatchUp(report, includeLifecycleFlags = true) {
116235
116346
  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"}.`));
116236
116347
  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.`));
116237
116348
  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}).`));
116238
- 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.`));
116349
+ 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.`));
116239
116350
  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.`));
116240
116351
  if (includeLifecycleFlags) {
116241
116352
  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.`));
@@ -116244,6 +116355,16 @@ function renderCatchUp(report, includeLifecycleFlags = true) {
116244
116355
 
116245
116356
  `);
116246
116357
  }
116358
+ function describeUnknownReason(item) {
116359
+ switch (item.reason) {
116360
+ case "wrap-up-failed":
116361
+ return "a wrap-up dispatch ran and every group failed (re-run it after fixing credentials/conflicts)";
116362
+ case "slate-unreadable":
116363
+ return "its slate.json is present but could not be read - the session's own record is damaged, not merely unrecorded";
116364
+ case "no-resolution-recorded":
116365
+ return "it has Slate engagement but no proposal, terminal state, unbound-candidate or wrap-up-outcome artifact";
116366
+ }
116367
+ }
116247
116368
  function renderSection(title, items, describe) {
116248
116369
  const lines = [`== ${title} ==`];
116249
116370
  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.130",
3
+ "version": "0.2.132",
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": {
@@ -128,6 +128,27 @@ export async function checkGraphStaleness(cwd: string): Promise<StalenessCheck>
128
128
  let gitFailed = false;
129
129
 
130
130
  // --- new commit -----------------------------------------------------------
131
+ //
132
+ // Flow 280 (the graph-provenance stall): this comparison is intentionally a
133
+ // bare commit-hash check, and it stays one. The disagreement that flow
134
+ // fixed — a `.metaproject/`-only commit left `keryx sync` saying "nothing
135
+ // to rebuild" while THIS check said "stale" forever, because nothing ever
136
+ // advanced the provenance commit this reads — was not a defect in this
137
+ // comparison. `checkGraphStaleness` has no notion of "since provenance was
138
+ // last recorded" and no per-commit diff to reason from; it only ever sees
139
+ // the two endpoints (`provenance.commit`, `head.commit`). Rederiving "did
140
+ // any code file change between them" here would mean re-running the same
141
+ // diff `../sync/diff.ts` already computes, in a module whose contract is a
142
+ // cheap, dependency-light freshness probe (staleness.test.ts, AFC-10) —
143
+ // and would leave that diff computed twice, once here and once in sync's
144
+ // own loop, with the two free to drift. The fix instead makes the ONE place
145
+ // that already has the diff (`../commands/sync.ts`'s per-module loop)
146
+ // advance provenance to HEAD when that diff is empty, so this check's
147
+ // simple "did the commit move" question starts answering "no" again — which
148
+ // is also why it is correct for this to stay a hard equality check: once
149
+ // provenance is honestly advanced, an ADVANCING mismatch here really does
150
+ // mean the graph predates HEAD, and this is exactly the check the wiki gate
151
+ // (`../wiki/staleness.ts`) needs to keep trusting for that.
131
152
  const head = await gitHead(cwd);
132
153
  if (head === null) {
133
154
  gitFailed = true;