@mrciphersmith/keryx 0.2.69 → 0.2.70

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
@@ -5648,6 +5648,9 @@ var init_command = __esm(() => {
5648
5648
  init_pull();
5649
5649
  });
5650
5650
 
5651
+ // src/gdgraph/types.ts
5652
+ var UNKNOWN_IMPORT_KIND = "unknown-static";
5653
+
5651
5654
  // src/gdgraph/config.ts
5652
5655
  import path47 from "path";
5653
5656
  function gdgraphConfigPath(cwd) {
@@ -6287,8 +6290,8 @@ async function buildGraph(projectRoot) {
6287
6290
  const language = getLanguage(file);
6288
6291
  const resolver = pickResolver(resolvers, language);
6289
6292
  const isLanguageAware = language === "java" || language === "python";
6290
- const specifiers = extractImportSpecifiers(content, language);
6291
- for (const specifier of specifiers) {
6293
+ const records = extractImportRecords(content, language);
6294
+ for (const { specifier, kind: importKind } of records) {
6292
6295
  const resolved = resolveImport(projectRoot, file, specifier, fileSet, resolver);
6293
6296
  const asset = resolved ? null : resolveAssetImport(projectRoot, file, specifier, resolver);
6294
6297
  const shouldTrackUnresolved = specifier.startsWith(".") || resolver.matchesAlias(specifier) || isLanguageAware;
@@ -6308,7 +6311,8 @@ async function buildGraph(projectRoot) {
6308
6311
  from: file,
6309
6312
  to: resolved ?? asset ?? specifier,
6310
6313
  kind: resolved ? "imports" : asset ? "asset" : "unresolved",
6311
- specifier
6314
+ specifier,
6315
+ importKind
6312
6316
  });
6313
6317
  }
6314
6318
  }
@@ -6352,18 +6356,33 @@ async function collectSourceFiles(projectRoot) {
6352
6356
  await walk3(".");
6353
6357
  return { files: result.sort(), skippedDirectories: skippedDirectories.sort() };
6354
6358
  }
6355
- function extractImportSpecifiers(content, language) {
6359
+ function extractImportRecords(content, language) {
6356
6360
  if (language === "java" || language === "python") {
6357
- return extractImportSpecifiersFallback(content);
6361
+ return extractImportSpecifiersFallback(content).map((specifier) => ({
6362
+ specifier,
6363
+ kind: UNKNOWN_IMPORT_KIND
6364
+ }));
6358
6365
  }
6359
6366
  const scanned = scanImportsOrEmpty(content);
6360
6367
  const fallback = extractImportSpecifiersFallback(content);
6361
- return [...new Set([...scanned, ...fallback])].sort();
6368
+ const kindBySpecifier = new Map;
6369
+ for (const { specifier, kind } of scanned) {
6370
+ const existing = kindBySpecifier.get(specifier);
6371
+ if (!existing || existing === "dynamic-import" && kind !== "dynamic-import") {
6372
+ kindBySpecifier.set(specifier, kind);
6373
+ }
6374
+ }
6375
+ for (const specifier of fallback) {
6376
+ if (!kindBySpecifier.has(specifier)) {
6377
+ kindBySpecifier.set(specifier, UNKNOWN_IMPORT_KIND);
6378
+ }
6379
+ }
6380
+ return [...kindBySpecifier.entries()].map(([specifier, kind]) => ({ specifier, kind })).sort((a, b) => a.specifier.localeCompare(b.specifier));
6362
6381
  }
6363
6382
  function scanImportsOrEmpty(content) {
6364
6383
  try {
6365
6384
  const scanner = new Bun.Transpiler({ loader: "tsx" });
6366
- return scanner.scanImports(content).map((entry) => entry.path).filter((specifier) => typeof specifier === "string" && specifier.length > 0);
6385
+ return scanner.scanImports(content).filter((entry) => typeof entry.path === "string" && entry.path.length > 0).map((entry) => ({ specifier: entry.path, kind: entry.kind }));
6367
6386
  } catch {
6368
6387
  return [];
6369
6388
  }
@@ -6906,7 +6925,7 @@ function getCycles(graph) {
6906
6925
  adjacency.set(node.path, []);
6907
6926
  }
6908
6927
  for (const edge of graph.edges) {
6909
- if (edge.kind !== "imports") {
6928
+ if (edge.kind !== "imports" || edge.importKind === "dynamic-import") {
6910
6929
  continue;
6911
6930
  }
6912
6931
  adjacency.get(edge.from)?.push(edge.to);
@@ -19200,8 +19219,8 @@ function buildAgentSystemInstruction(orient, ctx = {}) {
19200
19219
  ` + ` Do NOT thrash search_code/read_wiki instead of wiki enrich.
19201
19220
  ` + "- Optional prep: `keryx wiki collect` then enrich.\n" + "- Other keryx work (graph, health, memory, flow, testing): use the matching read tool above " + "(graph_affected/graph_query/graph_path/graph_symbol, health_status, memory_search, flow_status, test_related) " + "FIRST \u2014 they return the same data as the equivalent `keryx \u2026` CLI command without spending shell_exec's " + "scarce budget slot. Reach for `shell_exec` with the CLI only when the user wants to actually RUN a workflow " + `(mutate state, kick off a job) or needs an option no read tool covers.
19202
19221
 
19203
- ` + "ALWAYS use a tool to obtain facts instead of guessing; never fabricate paths, file " + "contents, or results. Be economical with output tokens: lead with the conclusion, " + "if the user asks you to run, inspect, or execute anything, call the relevant tool before " + `sending explanatory text.
19204
- ` + "give the shortest correct answer, prefer bullet points over prose, and omit preamble. " + "Do NOT paste large tool/command output back into your reply \u2014 the compact tool result " + "is already in context; reference it instead of repeating it.";
19222
+ ` + "ALWAYS use a tool to obtain facts instead of guessing; never fabricate paths, file " + "contents, or results. " + "If the user asks you to run, inspect, or execute anything, call the relevant tool before " + `sending explanatory text.
19223
+ ` + "Be economical with output LENGTH: lead with the conclusion, " + "give the shortest correct answer, prefer bullet points over prose, and omit preamble. " + "That economy governs prose only \u2014 never how many tools you call. " + "Do NOT paste large tool/command output back into your reply \u2014 the compact tool result " + "is already in context; reference it instead of repeating it. When a tool's result is " + "itself the deliverable you are about to report (e.g. a list of cycles, orphans, or " + "dependents) \u2014 not merely an input you go on to reason over \u2014 check it against source " + "before presenting it as fact; do not add this check to every call, only where the " + "result is the answer.";
19205
19224
  const trimmed = orient?.trim() ?? "";
19206
19225
  if (trimmed.length === 0) {
19207
19226
  return base;
@@ -23554,7 +23573,66 @@ function assertTransition(from, to) {
23554
23573
  ].join(", ") || "(none)"}`);
23555
23574
  }
23556
23575
  }
23557
- var TRANSITIONS;
23576
+ function taskGateStatus(task) {
23577
+ if (task.status !== "done") {
23578
+ return "not-terminal";
23579
+ }
23580
+ return task.disposition === "failed" ? "terminal-fail" : "terminal-pass";
23581
+ }
23582
+ function isUnreasonedSkip(task) {
23583
+ return task.disposition === "skipped" && !task.dispositionReason?.trim();
23584
+ }
23585
+ function isBlockedTask(task) {
23586
+ return task.disposition === "blocked";
23587
+ }
23588
+ function isUnknownDisposition(task) {
23589
+ if (task.disposition === undefined || task.disposition === null) {
23590
+ return false;
23591
+ }
23592
+ if (task.disposition === "failed" || task.disposition === "blocked") {
23593
+ return false;
23594
+ }
23595
+ return !GATE_PASSING_DISPOSITIONS.has(task.disposition);
23596
+ }
23597
+ function evaluateTaskGate(tasks) {
23598
+ const open2 = [];
23599
+ const failed = [];
23600
+ const unreasonedSkips = [];
23601
+ const blocked = [];
23602
+ const unknownDisposition = [];
23603
+ for (const task of tasks) {
23604
+ const status = taskGateStatus(task);
23605
+ if (status === "not-terminal") {
23606
+ open2.push(task.id);
23607
+ continue;
23608
+ }
23609
+ if (status === "terminal-fail") {
23610
+ failed.push(task.id);
23611
+ continue;
23612
+ }
23613
+ if (isUnknownDisposition(task)) {
23614
+ unknownDisposition.push(task.id);
23615
+ continue;
23616
+ }
23617
+ if (isBlockedTask(task)) {
23618
+ blocked.push(task.id);
23619
+ continue;
23620
+ }
23621
+ if (isUnreasonedSkip(task)) {
23622
+ unreasonedSkips.push(task.id);
23623
+ }
23624
+ }
23625
+ return {
23626
+ passed: open2.length === 0 && failed.length === 0 && unreasonedSkips.length === 0 && blocked.length === 0 && unknownDisposition.length === 0,
23627
+ open: open2,
23628
+ failed,
23629
+ unreasonedSkips,
23630
+ blocked,
23631
+ unknownDisposition,
23632
+ total: tasks.length
23633
+ };
23634
+ }
23635
+ var TRANSITIONS, GATE_PASSING_DISPOSITIONS;
23558
23636
  var init_machine = __esm(() => {
23559
23637
  TRANSITIONS = {
23560
23638
  initializing: ["ready", "blocked"],
@@ -23565,6 +23643,7 @@ var init_machine = __esm(() => {
23565
23643
  blocked: [],
23566
23644
  done: []
23567
23645
  };
23646
+ GATE_PASSING_DISPOSITIONS = new Set(["completed", "skipped"]);
23568
23647
  });
23569
23648
 
23570
23649
  // src/flow/schema.ts
@@ -23603,6 +23682,17 @@ function flowStateSchema() {
23603
23682
  }
23604
23683
  },
23605
23684
  acChecksum: { type: ["string", "null"], description: "SHA-256 of the frozen acceptance criteria." },
23685
+ gates: {
23686
+ type: "object",
23687
+ additionalProperties: true,
23688
+ description: "Which completion gates this package opted into at creation. Absent on packages created before a gate existed, which is how a gate is added without retroactively invalidating history: an absent flag reports the gate as skipped rather than passed.",
23689
+ properties: {
23690
+ tasks: {
23691
+ type: "boolean",
23692
+ description: "Written by `flow init`. When true, `flow complete` fails while any task is non-terminal."
23693
+ }
23694
+ }
23695
+ },
23606
23696
  acConfirmed: {
23607
23697
  type: "object",
23608
23698
  description: "Per-AC confirmation records keyed by AC id (e.g. AC1).",
@@ -23692,6 +23782,10 @@ function flowStateSchema() {
23692
23782
  enum: ["completed", "blocked", "failed", "skipped"],
23693
23783
  description: "v2: terminal outcome when status is done (distinct from status)."
23694
23784
  },
23785
+ dispositionReason: {
23786
+ type: "string",
23787
+ description: "Why the task ended this way. Required in practice for `skipped`: the task gate treats an unreasoned skip as non-terminal, because a skip nobody has to justify is a one-flag bypass of the gate."
23788
+ },
23695
23789
  acRefs: { type: "array", items: { type: "string" }, description: "v2: AC ids this task addresses." },
23696
23790
  evidenceRefs: { type: "array", items: { type: "string" }, description: "v2: artifact paths." },
23697
23791
  budget: {
@@ -24047,6 +24141,7 @@ function createFlowService(deps) {
24047
24141
  const createdAt = now();
24048
24142
  const flow = {
24049
24143
  schemaVersion: 1,
24144
+ gates: { tasks: true },
24050
24145
  id,
24051
24146
  slug: slug2,
24052
24147
  title,
@@ -24132,7 +24227,7 @@ function createFlowService(deps) {
24132
24227
  return save(input2.cwd, dir, flow, "task-added", `${task.id}: ${task.title}`);
24133
24228
  });
24134
24229
  },
24135
- async taskDone({ cwd, id, taskId, disposition, evidenceRefs, runLink }) {
24230
+ async taskDone({ cwd, id, taskId, disposition, reason, evidenceRefs, runLink }) {
24136
24231
  return mutate(cwd, id, async ({ dir, flow }) => {
24137
24232
  await assertAcIntact(cwd, dir, flow);
24138
24233
  const task = flow.tasks.find((item) => item.id.toUpperCase() === taskId.toUpperCase());
@@ -24140,7 +24235,14 @@ function createFlowService(deps) {
24140
24235
  throw new Error(`Task not found: ${taskId}. Known: ${flow.tasks.map((t) => t.id).join(", ")}`);
24141
24236
  }
24142
24237
  task.status = "done";
24238
+ const previousDisposition = task.disposition;
24143
24239
  task.disposition = disposition ?? task.disposition ?? "completed";
24240
+ if (disposition !== undefined && disposition !== previousDisposition) {
24241
+ delete task.dispositionReason;
24242
+ }
24243
+ if (reason?.trim()) {
24244
+ task.dispositionReason = reason.trim();
24245
+ }
24144
24246
  if (evidenceRefs !== undefined) {
24145
24247
  task.evidenceRefs = evidenceRefs;
24146
24248
  }
@@ -24150,6 +24252,20 @@ function createFlowService(deps) {
24150
24252
  return save(cwd, dir, flow, "task-done", `${task.id}: ${task.title}`);
24151
24253
  });
24152
24254
  },
24255
+ async taskAttempt({ cwd, id, taskId, outcome, detail }) {
24256
+ return mutate(cwd, id, async ({ dir, flow }) => {
24257
+ await assertAcIntact(cwd, dir, flow);
24258
+ const task = flow.tasks.find((item) => item.id.toUpperCase() === taskId.toUpperCase());
24259
+ if (!task) {
24260
+ throw new Error(`Task not found: ${taskId}. Known: ${flow.tasks.map((t) => t.id).join(", ")}`);
24261
+ }
24262
+ const attempts = task.attempts ?? { count: 0, log: [] };
24263
+ attempts.count += 1;
24264
+ attempts.log.push({ at: now(), outcome, ...detail?.trim() ? { detail: detail.trim() } : {} });
24265
+ task.attempts = attempts;
24266
+ return save(cwd, dir, flow, "task-attempt", `${task.id}: ${outcome} (attempt ${attempts.count})${detail?.trim() ? ` \u2014 ${detail.trim()}` : ""}`);
24267
+ });
24268
+ },
24153
24269
  async acConfirm({ cwd, id, criterion, note: note2 }) {
24154
24270
  return mutate(cwd, id, async ({ dir, flow }) => {
24155
24271
  await assertAcIntact(cwd, dir, flow);
@@ -24243,6 +24359,7 @@ function createFlowService(deps) {
24243
24359
  detail: "tracker unavailable; verify PR checks manually"
24244
24360
  });
24245
24361
  }
24362
+ gates.push(taskGate(flow));
24246
24363
  try {
24247
24364
  const health = await deps.healthGate(cwd);
24248
24365
  gates.push(health.status === "fail" ? { name: "health", status: "fail", detail: health.reasons.join("; ") || "health gate failed" } : { name: "health", status: "pass", detail: `health gate: ${health.status}` });
@@ -24436,6 +24553,40 @@ async function appendIdMap(cwd, entry) {
24436
24553
  await writeFileAtomic(file, `${JSON.stringify(entries, null, 2)}
24437
24554
  `);
24438
24555
  }
24556
+ function taskGate(flow) {
24557
+ if (!flow.gates?.tasks) {
24558
+ return {
24559
+ name: "tasks",
24560
+ status: "skipped",
24561
+ detail: "task gate not enabled for this package (created before the gate); " + "flows created by this keryx version opt in automatically"
24562
+ };
24563
+ }
24564
+ const verdict = evaluateTaskGate(flow.tasks);
24565
+ if (verdict.passed) {
24566
+ return {
24567
+ name: "tasks",
24568
+ status: "pass",
24569
+ detail: `${verdict.total} task(s) terminal`
24570
+ };
24571
+ }
24572
+ const reasons = [];
24573
+ if (verdict.open.length > 0) {
24574
+ reasons.push(`not done: ${verdict.open.join(", ")}`);
24575
+ }
24576
+ if (verdict.failed.length > 0) {
24577
+ reasons.push(`failed: ${verdict.failed.join(", ")}`);
24578
+ }
24579
+ if (verdict.unreasonedSkips.length > 0) {
24580
+ reasons.push(`skipped without a recorded reason: ${verdict.unreasonedSkips.join(", ")} ` + '(use: keryx flow task done <id> <Tn> --disposition skipped --reason "<why>")');
24581
+ }
24582
+ if (verdict.blocked.length > 0) {
24583
+ reasons.push(`blocked: ${verdict.blocked.join(", ")} ` + "(a blocked task did not get done; resolve it, or close it as skipped with a reason)");
24584
+ }
24585
+ if (verdict.unknownDisposition.length > 0) {
24586
+ reasons.push(`unrecognised disposition: ${verdict.unknownDisposition.join(", ")} ` + "(expected completed | blocked | failed | skipped)");
24587
+ }
24588
+ return { name: "tasks", status: "fail", detail: reasons.join("; ") };
24589
+ }
24439
24590
  async function isPlaceholderAc(cwd, dir) {
24440
24591
  const content = await Bun.file(acPath(cwd, dir)).text();
24441
24592
  return content.includes("<replace with a hard, verifiable criterion");
@@ -40756,6 +40907,11 @@ init_github();
40756
40907
  init_service6();
40757
40908
  init_guard();
40758
40909
  import path124 from "path";
40910
+
40911
+ // src/flow/types.ts
40912
+ var ATTEMPT_CLI_OUTCOMES = ["started", "failed", "blocked"];
40913
+
40914
+ // src/commands/flow.ts
40759
40915
  var VALID_TASK_KINDS = ["context", "implement", "test", "verify", "review", "docs"];
40760
40916
  function parseTaskKind(raw) {
40761
40917
  if (raw === undefined) {
@@ -40766,6 +40922,26 @@ function parseTaskKind(raw) {
40766
40922
  }
40767
40923
  return raw;
40768
40924
  }
40925
+ function positional(args2, index) {
40926
+ const value = args2[index];
40927
+ return value === undefined || value.startsWith("--") ? undefined : value;
40928
+ }
40929
+ var VALID_DISPOSITIONS = ["completed", "blocked", "failed", "skipped"];
40930
+ function parseDisposition(raw) {
40931
+ if (raw === undefined) {
40932
+ return;
40933
+ }
40934
+ if (!VALID_DISPOSITIONS.includes(raw)) {
40935
+ throw new Error(`Invalid --disposition "${raw}". Expected one of: ${VALID_DISPOSITIONS.join(", ")}`);
40936
+ }
40937
+ return raw;
40938
+ }
40939
+ function parseAttemptOutcome(raw) {
40940
+ if (raw === undefined || !ATTEMPT_CLI_OUTCOMES.includes(raw)) {
40941
+ throw new Error(`Invalid --outcome "${raw ?? ""}". Expected one of: ${ATTEMPT_CLI_OUTCOMES.join(", ")}`);
40942
+ }
40943
+ return raw;
40944
+ }
40769
40945
  function flowStatusLabel(status) {
40770
40946
  if (status === "done") {
40771
40947
  return style.green(status);
@@ -40972,22 +41148,48 @@ async function runTask(args2) {
40972
41148
  return;
40973
41149
  }
40974
41150
  if (sub === "done") {
40975
- const id = args2[1];
40976
- const taskId = args2[2];
41151
+ const id = positional(args2, 1);
41152
+ const taskId = positional(args2, 2);
40977
41153
  if (!id || !taskId) {
40978
- throw new Error("Usage: keryx flow task done <id> <taskId> [--disposition completed|blocked|failed|skipped]");
41154
+ throw new Error('Usage: keryx flow task done <id> <taskId> [--disposition completed|blocked|failed|skipped] [--reason "<why>"]');
40979
41155
  }
41156
+ const disposition = parseDisposition(optionValue(args2, "--disposition"));
41157
+ const reason = optionValue(args2, "--reason");
40980
41158
  const flow = await getService3().taskDone({
40981
41159
  cwd: process.cwd(),
40982
41160
  id,
40983
41161
  taskId,
40984
- disposition: optionValue(args2, "--disposition")
41162
+ disposition,
41163
+ reason
40985
41164
  });
40986
41165
  const done = flow.tasks.filter((task) => task.status === "done").length;
40987
41166
  console.log(` ${style.green(symbols.ok)} Task ${style.bold(taskId.toUpperCase())} done ${style.dim(`(${done}/${flow.tasks.length})`)}`);
41167
+ if (disposition === "skipped" && !reason?.trim()) {
41168
+ note('A skipped task without --reason "<why>" fails the task gate at `keryx flow complete`. Re-run with a reason to record why the work was not needed.');
41169
+ }
41170
+ if (disposition === "blocked") {
41171
+ note("A blocked task fails the task gate at `keryx flow complete` \u2014 it is recorded as terminal, " + "but the work did not happen. Resolve it, or close it as skipped with a reason.");
41172
+ }
40988
41173
  return;
40989
41174
  }
40990
- throw new Error("Usage: keryx flow task <add|done> ...");
41175
+ if (sub === "attempt") {
41176
+ const id = positional(args2, 1);
41177
+ const taskId = positional(args2, 2);
41178
+ if (!id || !taskId) {
41179
+ throw new Error(`Usage: keryx flow task attempt <id> <taskId> --outcome ${ATTEMPT_CLI_OUTCOMES.join("|")} [--detail "<what happened>"]`);
41180
+ }
41181
+ const flow = await getService3().taskAttempt({
41182
+ cwd: process.cwd(),
41183
+ id,
41184
+ taskId,
41185
+ outcome: parseAttemptOutcome(optionValue(args2, "--outcome")),
41186
+ detail: optionValue(args2, "--detail")
41187
+ });
41188
+ const task = flow.tasks.find((item) => item.id.toUpperCase() === taskId.toUpperCase());
41189
+ console.log(` ${style.green(symbols.ok)} Attempt recorded on ${style.bold(taskId.toUpperCase())} ${style.dim(`(count ${task?.attempts?.count ?? 0})`)}`);
41190
+ return;
41191
+ }
41192
+ throw new Error("Usage: keryx flow task <add|done|attempt> ...");
40991
41193
  }
40992
41194
  async function runAc(args2) {
40993
41195
  const sub = args2[0];
@@ -41118,7 +41320,8 @@ function printHelp10() {
41118
41320
  "keryx flow freeze <id>",
41119
41321
  "keryx flow start <id>",
41120
41322
  'keryx flow task add <id> --title "<t>" [--kind context|implement|test|verify|review|docs] [--depends T1,T2]',
41121
- "keryx flow task done <id> <taskId> [--disposition completed|blocked|failed|skipped]",
41323
+ 'keryx flow task done <id> <taskId> [--disposition completed|blocked|failed|skipped] [--reason "<why>"]',
41324
+ 'keryx flow task attempt <id> <taskId> --outcome started|failed|blocked [--detail "<what happened>"]',
41122
41325
  'keryx flow ac confirm <id> <ACn> [--note "<evidence>"]',
41123
41326
  'keryx flow ac update <id> --reason "<why>"',
41124
41327
  "keryx flow implemented <id> --pr <url>",
@@ -41136,16 +41339,17 @@ init_args();
41136
41339
 
41137
41340
  // src/review/managed.ts
41138
41341
  init_validator();
41139
- init_fs();
41140
- init_store2();
41141
41342
  import { mkdir as mkdir48, readFile as readFile66, readdir as readdir20 } from "fs/promises";
41142
41343
  import path125 from "path";
41344
+ init_fs();
41345
+ init_store2();
41143
41346
 
41144
41347
  // src/review/types.ts
41145
41348
  var MANAGED_REVIEW_MODES = ["attach-review", "review-flow", "ingest"];
41146
41349
  var REVIEW_TARGET_KINDS = ["pr", "issue", "branch", "path", "report"];
41147
41350
  var REVIEW_PACKAGE_STATUSES = ["draft", "reviewed", "decided", "learned", "closed"];
41148
41351
  var REVIEW_COVERAGE_STATUSES = ["run", "skipped", "failed", "needs_context"];
41352
+ var REVIEW_FINDING_CONFIDENCES = ["high", "medium", "low"];
41149
41353
 
41150
41354
  // src/review/managed.ts
41151
41355
  var REQUIRED_ARTIFACTS = ["scope", "coverage", "report", "findings", "learning", "decisions"];
@@ -41174,7 +41378,14 @@ async function createManagedReviewPackage(input2) {
41174
41378
  const packageDir = packagePath(input2.cwd, input2.mode, reviewId, flowMatch);
41175
41379
  const coverage = normalizeCoverage(input2.coverage, input2.reviewers);
41176
41380
  const report = await readReport(input2);
41177
- const findings = normalizeFindings(report, input2.mode, flowMatch !== null);
41381
+ const findings = await normalizeFindings({
41382
+ report,
41383
+ reportLabel: reportLabel(input2),
41384
+ mode: input2.mode,
41385
+ attachedToFlow: flowMatch !== null,
41386
+ source: input2.findings,
41387
+ reviewers: coverage.filter((entry) => entry.status === "run").map((entry) => entry.reviewer)
41388
+ });
41178
41389
  const manifest = buildManifest2({
41179
41390
  input: input2,
41180
41391
  reviewId,
@@ -41191,13 +41402,17 @@ async function createManagedReviewPackage(input2) {
41191
41402
  if (violations.length > 0) {
41192
41403
  throw new Error(`Refusing to record findings that do not enumerate their class: ${violations.map((finding) => `${finding.id} (${finding.severity})`).join(", ")}. A blocker or major must carry class_scope with sites and enumeration_method \u2014 every site holding the shape, and how the set was derived.`);
41193
41404
  }
41405
+ const contractErrors = await schemaErrors(findings);
41406
+ if (contractErrors.length > 0) {
41407
+ throw new Error(`Refusing to record findings that do not satisfy review-finding.schema.json: ${contractErrors.map((error2) => `${error2.path} ${error2.message}`).join("; ")}`);
41408
+ }
41194
41409
  await mkdir48(packageDir, { recursive: true });
41195
41410
  await writeFileAtomic(path125.join(packageDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
41196
41411
  `);
41197
41412
  await writeFileAtomic(path125.join(packageDir, "scope.md"), renderScope(input2, flowMatch, at));
41198
41413
  await writeFileAtomic(path125.join(packageDir, "coverage.md"), renderCoverage(coverage));
41199
41414
  await writeFileAtomic(path125.join(packageDir, "report.md"), renderReport(report, input2.mode));
41200
- await writeFileAtomic(path125.join(packageDir, "findings.json"), `${JSON.stringify(findings, null, 2)}
41415
+ await writeFileAtomic(path125.join(packageDir, "findings.json"), `${JSON.stringify(findings.map(toContractFinding), null, 2)}
41201
41416
  `);
41202
41417
  await writeFileAtomic(path125.join(packageDir, "learning.md"), renderLearning(findings));
41203
41418
  await writeFileAtomic(path125.join(packageDir, "decisions.md"), renderDecisions(findings));
@@ -41354,7 +41569,147 @@ async function readReport(input2) {
41354
41569
  No reviewer findings recorded yet.
41355
41570
  `;
41356
41571
  }
41357
- function normalizeFindings(report, mode, attachedToFlow) {
41572
+ function reportLabel(input2) {
41573
+ return input2.reportPath ? path125.resolve(input2.cwd, input2.reportPath) : "the report text passed to keryx review";
41574
+ }
41575
+ async function normalizeFindings(args2) {
41576
+ if (args2.source !== undefined) {
41577
+ return triage(fromStructuredSource(args2.source, args2.reviewers), args2);
41578
+ }
41579
+ const structured = parseEmbeddedFindings(args2.report, args2.reportLabel);
41580
+ if (structured !== null) {
41581
+ return triage(fromStructuredSource(structured, args2.reviewers), args2);
41582
+ }
41583
+ return triage(parseLegacyReport(args2.report, args2.reviewers), args2);
41584
+ }
41585
+ function triage(findings, args2) {
41586
+ const classification = args2.mode === "ingest" ? "valid_followup" : "skill_learning_candidate";
41587
+ return findings.map((finding) => {
41588
+ const triaged = {
41589
+ ...finding,
41590
+ classification,
41591
+ flow_relevance: args2.attachedToFlow ? "post_flow_feedback" : "standalone_review"
41592
+ };
41593
+ if (finding.learning_candidate === undefined && classification === "skill_learning_candidate") {
41594
+ triaged.learning_candidate = true;
41595
+ }
41596
+ return triaged;
41597
+ });
41598
+ }
41599
+ function toContractFinding(finding) {
41600
+ const record = {
41601
+ id: finding.id,
41602
+ reviewer: finding.reviewer,
41603
+ severity: finding.severity,
41604
+ problem: finding.problem,
41605
+ impact: finding.impact,
41606
+ suggested_fix: finding.suggested_fix,
41607
+ evidence: finding.evidence,
41608
+ confidence: finding.confidence
41609
+ };
41610
+ if (finding.file !== undefined) {
41611
+ record.file = finding.file;
41612
+ }
41613
+ if (finding.line !== undefined) {
41614
+ record.line = finding.line;
41615
+ }
41616
+ if (finding.symbol !== undefined) {
41617
+ record.symbol = finding.symbol;
41618
+ }
41619
+ if (finding.dedupe_key !== undefined) {
41620
+ record.dedupe_key = finding.dedupe_key;
41621
+ }
41622
+ if (finding.blocking_merge !== undefined) {
41623
+ record.blocking_merge = finding.blocking_merge;
41624
+ }
41625
+ if (finding.related_skill !== undefined) {
41626
+ record.related_skill = finding.related_skill;
41627
+ }
41628
+ if (finding.learning_candidate !== undefined) {
41629
+ record.learning_candidate = finding.learning_candidate;
41630
+ }
41631
+ if (finding.class_scope !== undefined) {
41632
+ record.class_scope = finding.class_scope;
41633
+ }
41634
+ return record;
41635
+ }
41636
+ var EMBEDDED_FINDINGS_FENCE = /^ {0,3}(`{3,}|~{3,})[^\n]*\bkeryx:findings\b[^\n]*$/gm;
41637
+ function parseEmbeddedFindings(report, reportLabel2) {
41638
+ const fences = [...report.matchAll(EMBEDDED_FINDINGS_FENCE)];
41639
+ if (fences.length === 0) {
41640
+ return null;
41641
+ }
41642
+ if (fences.length > 1) {
41643
+ throw new Error(`${reportLabel2} carries ${fences.length} keryx:findings blocks (at character ${fences.map((fence2) => String(fence2.index ?? 0)).join(" and ")}); exactly one is allowed. Concatenating one block per reviewer drops every block after the first \u2014 merge them into a single array.`);
41644
+ }
41645
+ const fence = fences[0];
41646
+ let parsed;
41647
+ try {
41648
+ parsed = JSON.parse(embeddedBlockBody(report, fence));
41649
+ } catch (error2) {
41650
+ throw new Error(`${reportLabel2} carries a keryx:findings block that is not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`);
41651
+ }
41652
+ if (!Array.isArray(parsed) && !isReviewerResult(parsed)) {
41653
+ throw new Error(`${reportLabel2} carries a keryx:findings block that is ${describeJson(parsed)}, not an array of findings or a { reviewer, findings } result.`);
41654
+ }
41655
+ return parsed;
41656
+ }
41657
+ function embeddedBlockBody(report, fence) {
41658
+ const marker2 = fence[1] ?? "```";
41659
+ const char = marker2.startsWith("~") ? "~" : "`";
41660
+ const afterOpen = report.slice((fence.index ?? 0) + fence[0].length).replace(/^\r?\n/, "");
41661
+ const closing = afterOpen.match(new RegExp(`^ {0,3}\\${char}{${marker2.length},}\\s*$`, "m"));
41662
+ return closing?.index === undefined ? afterOpen : afterOpen.slice(0, closing.index);
41663
+ }
41664
+ function describeJson(value) {
41665
+ if (value === null) {
41666
+ return "JSON null";
41667
+ }
41668
+ return `a JSON ${typeof value}`;
41669
+ }
41670
+ function fromStructuredSource(source, reviewers) {
41671
+ const flattened = [];
41672
+ for (const entry of Array.isArray(source) ? source : [source]) {
41673
+ if (isReviewerResult(entry)) {
41674
+ for (const finding of entry.findings) {
41675
+ if (finding.reviewer || entry.reviewer === undefined) {
41676
+ flattened.push(finding);
41677
+ continue;
41678
+ }
41679
+ flattened.push({ ...finding, reviewer: entry.reviewer });
41680
+ }
41681
+ continue;
41682
+ }
41683
+ flattened.push(entry);
41684
+ }
41685
+ return flattened.map((finding) => coerceStructured(finding, reviewers));
41686
+ }
41687
+ function isReviewerResult(value) {
41688
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Array.isArray(value.findings);
41689
+ }
41690
+ async function schemaErrors(findings) {
41691
+ const schema = await loadSchema("review-finding");
41692
+ const errors = [];
41693
+ for (const [index, finding] of findings.entries()) {
41694
+ for (const error2 of await validateJson(toContractFinding(finding), schema)) {
41695
+ errors.push({ path: error2.path.replace(/^\$/, `$[${index}]`), message: error2.message });
41696
+ }
41697
+ }
41698
+ return errors;
41699
+ }
41700
+ function coerceStructured(finding, reviewers) {
41701
+ const record = { ...finding };
41702
+ if (typeof record.reviewer !== "string" || record.reviewer === "") {
41703
+ record.reviewer = defaultReviewer(reviewers);
41704
+ }
41705
+ record.summary = typeof finding.problem === "string" ? finding.problem : "";
41706
+ record.class_scope_present = finding.class_scope !== undefined;
41707
+ return record;
41708
+ }
41709
+ function defaultReviewer(reviewers) {
41710
+ return reviewers.length === 1 && reviewers[0] ? reviewers[0] : "review-orchestrator";
41711
+ }
41712
+ function parseLegacyReport(report, reviewers) {
41358
41713
  const findings = [];
41359
41714
  const lines = report.split(`
41360
41715
  `);
@@ -41366,18 +41721,128 @@ function normalizeFindings(report, mode, attachedToFlow) {
41366
41721
  const id = heading2.id;
41367
41722
  const summary = heading2.summary || "Review finding";
41368
41723
  const block = findingBlock(lines, index);
41369
- findings.push({
41724
+ const location = parseLocation(labelledField(block, ["file", "location"]));
41725
+ const classScope = parseClassScope(block);
41726
+ const finding = {
41370
41727
  id,
41371
41728
  severity: severityFor(line, block),
41372
- reviewer: "review-orchestrator",
41729
+ reviewer: legacyReviewer(block, reviewers),
41730
+ problem: labelledField(block, ["problem", "issue"]) ?? summary.replace(SEVERITY_PREFIX, ""),
41731
+ impact: labelledField(block, ["impact", "why it matters"]) ?? NOT_RECORDED("impact"),
41732
+ suggested_fix: labelledField(block, ["suggested fix", "suggested_fix", "fix", "recommendation"]) ?? NOT_RECORDED("suggested_fix"),
41733
+ evidence: labelledField(block, ["evidence", "proof"]) ?? NOT_RECORDED("evidence"),
41734
+ confidence: legacyConfidence(block),
41373
41735
  summary,
41374
- classification: mode === "ingest" ? "valid_followup" : "skill_learning_candidate",
41375
- flow_relevance: attachedToFlow ? "post_flow_feedback" : "standalone_review",
41376
- class_scope_present: hasClassScope(block)
41377
- });
41736
+ class_scope_present: classScope !== null
41737
+ };
41738
+ if (classScope !== null) {
41739
+ finding.class_scope = classScope;
41740
+ }
41741
+ if (location.file !== undefined) {
41742
+ finding.file = location.file;
41743
+ }
41744
+ if (location.line !== undefined) {
41745
+ finding.line = location.line;
41746
+ }
41747
+ findings.push(finding);
41378
41748
  }
41379
41749
  return findings;
41380
41750
  }
41751
+ var SEVERITY_PREFIX = /^\s*(?:\*\*|__)?(?:blocker|major|minor|info)(?:\*\*|__)?\s*[:\u2014\u2013-]\s*/i;
41752
+ var NOT_RECORDED = (field3) => `not recorded: derived from a markdown review report, which carried no ${field3} field`;
41753
+ function labelledField(block, labels) {
41754
+ const lines = block.split(`
41755
+ `);
41756
+ for (const [index, line] of lines.entries()) {
41757
+ const match = line.match(LABEL_LINE);
41758
+ const label = match?.[1]?.trim().toLowerCase().replace(/\*\*|__|`/g, "");
41759
+ if (label === undefined || !labels.includes(label)) {
41760
+ continue;
41761
+ }
41762
+ const parts = [(match?.[2] ?? "").trim()];
41763
+ for (let i = index + 1;i < lines.length; i += 1) {
41764
+ const next = lines[i] ?? "";
41765
+ if (next.trim() === "" || /^\s*[-*+]\s/.test(next) || /^\s*(?:\*\*|__)/.test(next)) {
41766
+ break;
41767
+ }
41768
+ parts.push(next.trim());
41769
+ }
41770
+ const value = parts.join(" ").trim().replace(/^[`*_]+|[`*_]+$/g, "").trim();
41771
+ if (value !== "") {
41772
+ return value;
41773
+ }
41774
+ }
41775
+ return;
41776
+ }
41777
+ var LABEL_LINE = /^[\s>]*(?:[-*+]\s+)?((?:\*\*|__)?[A-Za-z][A-Za-z _-]*(?:\*\*|__)?)\s*[:=]\s*(.*)$/;
41778
+ function parseLocation(value) {
41779
+ if (value === undefined) {
41780
+ return {};
41781
+ }
41782
+ const match = value.replace(/`/g, "").trim().match(/^([^\s:]+?\.[A-Za-z0-9]+)(?::(\d+))?\b/);
41783
+ if (!match?.[1]) {
41784
+ return {};
41785
+ }
41786
+ const line = match[2] === undefined ? undefined : Number(match[2]);
41787
+ return line === undefined || Number.isNaN(line) ? { file: match[1] } : { file: match[1], line };
41788
+ }
41789
+ function legacyReviewer(block, reviewers) {
41790
+ const attribution = labelledField(block, ["reviewer", "found by", "found independently by", "reviewers"]);
41791
+ const named = attribution?.match(/\breview-[a-z0-9-]+\b/i);
41792
+ if (named?.[0]) {
41793
+ return named[0].toLowerCase();
41794
+ }
41795
+ return defaultReviewer(reviewers);
41796
+ }
41797
+ function legacyConfidence(block) {
41798
+ const declared = labelledField(block, ["confidence"])?.toLowerCase();
41799
+ if (declared !== undefined) {
41800
+ for (const value of REVIEW_FINDING_CONFIDENCES) {
41801
+ if (declared.startsWith(value)) {
41802
+ return value;
41803
+ }
41804
+ }
41805
+ }
41806
+ return "low";
41807
+ }
41808
+ function parseClassScope(block) {
41809
+ const lines = block.split(`
41810
+ `);
41811
+ const start = lines.findIndex((line) => /class[_ ]scope/i.test(line));
41812
+ if (start === -1) {
41813
+ return null;
41814
+ }
41815
+ const rest = lines.slice(start).join(`
41816
+ `);
41817
+ const sitesMatch = rest.match(/\bsites\b\s*[:=]\s*([\s\S]*?)(?=\benumeration_method\b|$)/i);
41818
+ const methodMatch = rest.match(/\benumeration_method\b\s*[:=]\s*([\s\S]*?)(?=\n\s*\n|$)/i);
41819
+ const sites = sitesMatch?.[1] === undefined ? [] : parseSites(sitesMatch[1]);
41820
+ const method = unquote(methodMatch?.[1]?.replace(/\s+/g, " ").trim() ?? "");
41821
+ if (sites.length === 0 || method === "") {
41822
+ return null;
41823
+ }
41824
+ return { sites, enumeration_method: method };
41825
+ }
41826
+ function unquote(value) {
41827
+ const trimmed = value.trim().replace(/[.;,]$/, "").trim();
41828
+ return /^(["'`])[\s\S]*\1$/.test(trimmed) ? trimmed.slice(1, -1).trim() : trimmed;
41829
+ }
41830
+ function parseSites(raw) {
41831
+ const text = raw.replace(/\s+/g, " ").trim().replace(/[;,]\s*$/, "");
41832
+ if (text === "") {
41833
+ return [];
41834
+ }
41835
+ if (text.startsWith("[")) {
41836
+ try {
41837
+ const parsed = JSON.parse(text.slice(0, text.lastIndexOf("]") + 1));
41838
+ if (Array.isArray(parsed)) {
41839
+ return parsed.map((item) => String(item)).filter((item) => item !== "");
41840
+ }
41841
+ } catch {}
41842
+ }
41843
+ const separator = text.includes(";") ? ";" : ",";
41844
+ return text.split(separator).map((item) => item.replace(/^[-*+\s`"]+|[\s`",]+$/g, "").trim()).filter((item) => item !== "");
41845
+ }
41381
41846
  var HEADING_MARKER = /^(?:#{1,6}|[-*+])\s+/;
41382
41847
  var FINDING_IDENTIFIER = /^(\[)?\s*(F-\d{3,})\b\s*(\])?/i;
41383
41848
  var TITLE_SEPARATOR = /^\s*[:\u2014\u2013-]+\s*/;
@@ -41419,10 +41884,6 @@ function findingBlock(lines, headingIndex) {
41419
41884
  return out.join(`
41420
41885
  `);
41421
41886
  }
41422
- function hasClassScope(block) {
41423
- const lower = block.toLowerCase();
41424
- return (lower.includes("class_scope") || lower.includes("class scope")) && lower.includes("sites") && lower.includes("enumeration_method");
41425
- }
41426
41887
  function classScopeViolations(findings) {
41427
41888
  return findings.filter((finding) => (finding.severity === "blocker" || finding.severity === "major") && finding.class_scope_present !== true);
41428
41889
  }
@@ -41493,7 +41954,7 @@ function renderLearning(findings) {
41493
41954
 
41494
41955
  ## Skill Learning
41495
41956
 
41496
- ${candidates.map((finding) => `- \`review-orchestrator\` <- ${finding.id}: ${finding.summary}`).join(`
41957
+ ${candidates.map((finding) => `- \`${finding.reviewer}\` <- ${finding.id}: ${finding.summary}`).join(`
41497
41958
  `)}
41498
41959
  `;
41499
41960
  }
@@ -41506,7 +41967,7 @@ function renderDecisions(findings) {
41506
41967
  }
41507
41968
  return `# Decisions
41508
41969
 
41509
- ${findings.map((finding) => `- ${finding.id}: create follow-up task or learning proposal (${finding.classification}).`).join(`
41970
+ ${findings.map((finding) => `- ${finding.id}: create follow-up task or learning proposal (${finding.classification}, ${finding.flow_relevance}).`).join(`
41510
41971
  `)}
41511
41972
  `;
41512
41973
  }
@@ -42765,6 +43226,24 @@ var COMMAND_DESCRIPTORS = [
42765
43226
  read: false,
42766
43227
  sideEffects: ["renames .metaproject/flows/**", "writes .metaproject/flows/id-map.json"]
42767
43228
  },
43229
+ {
43230
+ module: "sandbox",
43231
+ command: "sandbox status",
43232
+ summary: "OS sandbox launcher availability and the per-capability containment matrix for this platform. Report only \u2014 never runs a contained command, always exits 0.",
43233
+ intent: [
43234
+ "\u043F\u0440\u043E\u0432\u0435\u0440\u044C sandbox",
43235
+ "\u0441\u0442\u0430\u0442\u0443\u0441 \u043F\u0435\u0441\u043E\u0447\u043D\u0438\u0446\u044B",
43236
+ "sandbox status",
43237
+ "is bubblewrap installed",
43238
+ "\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043B\u0438 bubblewrap",
43239
+ "os containment status",
43240
+ "check os sandbox",
43241
+ "\u043A\u0430\u043A\u0430\u044F \u0438\u0437\u043E\u043B\u044F\u0446\u0438\u044F \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430"
43242
+ ],
43243
+ args: [{ name: "json", type: "bool", required: false, desc: "structured JSON report" }],
43244
+ json: true,
43245
+ read: true
43246
+ },
42768
43247
  {
42769
43248
  module: "security",
42770
43249
  command: "security scan",
@@ -43709,14 +44188,14 @@ async function handlePolicy(cwd, args2) {
43709
44188
  return;
43710
44189
  }
43711
44190
  const config = await loadSecurityConfig(cwd);
43712
- const schemaErrors = validateSecurityConfig(config);
44191
+ const schemaErrors2 = validateSecurityConfig(config);
43713
44192
  const checksum = verifyConfigChecksum(config);
43714
44193
  heading("keryx security policy validate");
43715
44194
  console.log("");
43716
- if (schemaErrors.length === 0) {
44195
+ if (schemaErrors2.length === 0) {
43717
44196
  console.log(` ${style.green(symbols.ok)} config schema: valid`);
43718
44197
  } else {
43719
- for (const error2 of schemaErrors) {
44198
+ for (const error2 of schemaErrors2) {
43720
44199
  console.log(` ${style.red(symbols.cross)} ${error2}`);
43721
44200
  }
43722
44201
  }
@@ -43725,7 +44204,7 @@ async function handlePolicy(cwd, args2) {
43725
44204
  } else {
43726
44205
  console.log(` ${style.red(symbols.cross)} configChecksum: mismatch (expected ${checksum.expected})`);
43727
44206
  }
43728
- const ok = schemaErrors.length === 0 && checksum.match;
44207
+ const ok = schemaErrors2.length === 0 && checksum.match;
43729
44208
  process.exitCode = ok ? 0 : 1;
43730
44209
  }
43731
44210
  async function handleIncidents(cwd, args2) {
@@ -43940,14 +44419,347 @@ function printSecurityHelp() {
43940
44419
  ]);
43941
44420
  }
43942
44421
 
44422
+ // src/harness/process/sandbox/detect.ts
44423
+ import { existsSync as realExistsSync } from "fs";
44424
+ import path133 from "path";
44425
+
44426
+ // src/harness/process/sandbox/seatbelt.ts
44427
+ var SANDBOX_EXEC_PATH = "/usr/bin/sandbox-exec";
44428
+ function sbplString(value) {
44429
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
44430
+ }
44431
+ var DEVICE_WRITE_LITERALS = [
44432
+ "/dev/null",
44433
+ "/dev/zero",
44434
+ "/dev/stdin",
44435
+ "/dev/stdout",
44436
+ "/dev/stderr",
44437
+ "/dev/tty",
44438
+ "/dev/dtracehelper",
44439
+ "/dev/random",
44440
+ "/dev/urandom"
44441
+ ];
44442
+ function buildSeatbeltProfile(profile) {
44443
+ const lines = [
44444
+ "(version 1)",
44445
+ "(allow default)",
44446
+ "",
44447
+ ";; --- filesystem writes: deny everything, then re-allow workspace roots ---",
44448
+ '(deny file-write* (subpath "/"))'
44449
+ ];
44450
+ for (const root of profile.writableRoots) {
44451
+ lines.push(`(allow file-write* (subpath ${sbplString(root)}))`);
44452
+ }
44453
+ lines.push(`(allow file-write-data ${DEVICE_WRITE_LITERALS.map((d) => `(literal ${sbplString(d)})`).join(" ")})`);
44454
+ if (profile.readDenyList.length > 0) {
44455
+ lines.push("", ";; --- secret read-deny ---");
44456
+ for (const secret of profile.readDenyList) {
44457
+ lines.push(`(deny file-read* (subpath ${sbplString(secret)}))`);
44458
+ }
44459
+ }
44460
+ if (profile.network === "off") {
44461
+ lines.push("", ";; --- network off ---", "(deny network*)");
44462
+ } else if (profile.network === "restricted") {
44463
+ lines.push("", ";; --- network restricted to loopback allowlist proxy ---", "(deny network*)");
44464
+ if (profile.proxy) {
44465
+ lines.push(`(allow network-outbound (remote ip ${sbplString(`localhost:${profile.proxy.port}`)}))`);
44466
+ }
44467
+ }
44468
+ return `${lines.join(`
44469
+ `)}
44470
+ `;
44471
+ }
44472
+ function wrapSeatbelt(command, profile) {
44473
+ const profileText = buildSeatbeltProfile(profile);
44474
+ return {
44475
+ path: SANDBOX_EXEC_PATH,
44476
+ argv: ["sandbox-exec", "-p", profileText, command.path, ...command.argv.slice(1)],
44477
+ env: command.env,
44478
+ cwd: command.cwd
44479
+ };
44480
+ }
44481
+
44482
+ // src/harness/process/sandbox/bwrap.ts
44483
+ import { statSync as statSync4 } from "fs";
44484
+ var BWRAP_PROGRAM = "bwrap";
44485
+ function inspectMaskTarget(target) {
44486
+ try {
44487
+ return statSync4(target).isDirectory() ? "dir" : "file";
44488
+ } catch {
44489
+ return "missing";
44490
+ }
44491
+ }
44492
+ function buildBwrapArgs(profile, inspect = inspectMaskTarget) {
44493
+ const args2 = [
44494
+ "--ro-bind",
44495
+ "/",
44496
+ "/",
44497
+ "--dev",
44498
+ "/dev",
44499
+ "--proc",
44500
+ "/proc",
44501
+ "--tmpfs",
44502
+ "/tmp"
44503
+ ];
44504
+ for (const root of profile.writableRoots) {
44505
+ args2.push("--bind", root, root);
44506
+ }
44507
+ for (const secret of profile.readDenyList) {
44508
+ const kind = inspect(secret);
44509
+ if (kind === "dir") {
44510
+ args2.push("--tmpfs", secret);
44511
+ } else if (kind === "file") {
44512
+ args2.push("--ro-bind", "/dev/null", secret);
44513
+ }
44514
+ }
44515
+ if (profile.network === "off") {
44516
+ args2.push("--unshare-net");
44517
+ }
44518
+ args2.push("--unshare-pid", "--unshare-ipc", "--die-with-parent", "--new-session");
44519
+ return args2;
44520
+ }
44521
+ function wrapBwrap(command, profile, launcherPath = BWRAP_PROGRAM) {
44522
+ return {
44523
+ path: launcherPath,
44524
+ argv: [BWRAP_PROGRAM, ...buildBwrapArgs(profile), "--", command.path, ...command.argv.slice(1)],
44525
+ env: command.env,
44526
+ cwd: command.cwd
44527
+ };
44528
+ }
44529
+
44530
+ // src/harness/process/sandbox/adapter.ts
44531
+ import { createHash as createHash21 } from "crypto";
44532
+
44533
+ // src/harness/process/sandbox/wrap.ts
44534
+ function wrapWithSandbox(command, profile, opts) {
44535
+ if (profile.mode === "danger-full-access") {
44536
+ return { ok: true, command, wrapped: false };
44537
+ }
44538
+ if (opts.platform === "darwin") {
44539
+ return { ok: true, command: wrapSeatbelt(command, profile), wrapped: true };
44540
+ }
44541
+ if (opts.platform === "linux") {
44542
+ if (profile.network === "restricted") {
44543
+ return {
44544
+ ok: false,
44545
+ reason: "network=restricted is not yet enforced on Linux (needs a network namespace + proxy relay); use network off/on or run inside a container."
44546
+ };
44547
+ }
44548
+ const wrapped = opts.bwrapPath ? wrapBwrap(command, profile, opts.bwrapPath) : wrapBwrap(command, profile);
44549
+ return { ok: true, command: wrapped, wrapped: true };
44550
+ }
44551
+ return {
44552
+ ok: false,
44553
+ reason: `OS sandbox is unsupported on platform "${opts.platform}"; run inside WSL2 or a container, or use an explicit danger-full-access override.`
44554
+ };
44555
+ }
44556
+
44557
+ // src/harness/process/sandbox/adapter.ts
44558
+ function spawnError(command, message2) {
44559
+ return {
44560
+ kind: "spawn-error",
44561
+ observedHash: createHash21("sha256").update(`${command.path}
44562
+ ${command.argv.join(" ")}`, "utf8").digest("hex"),
44563
+ errorMessage: message2
44564
+ };
44565
+ }
44566
+
44567
+ class SandboxedProcessAdapter {
44568
+ opts;
44569
+ constructor(opts) {
44570
+ this.opts = opts;
44571
+ }
44572
+ spawn(command) {
44573
+ const { profile, inner, platform, launcherAvailable, bwrapPath } = this.opts;
44574
+ const failClosed = profile.required || profile.network === "restricted" || (this.opts.failIfUnavailable ?? true);
44575
+ if (profile.mode === "danger-full-access") {
44576
+ return inner.spawn(command);
44577
+ }
44578
+ if (!launcherAvailable) {
44579
+ if (failClosed) {
44580
+ return spawnError(command, `OS sandbox launcher unavailable on ${platform} for program "${command.path}"; failing closed (install bubblewrap on Linux, or relax failIfUnavailable to run unsandboxed).`);
44581
+ }
44582
+ return inner.spawn(command);
44583
+ }
44584
+ const wrap2 = wrapWithSandbox(command, profile, {
44585
+ platform,
44586
+ ...bwrapPath !== undefined ? { bwrapPath } : {}
44587
+ });
44588
+ if (!wrap2.ok) {
44589
+ if (failClosed) {
44590
+ return spawnError(command, `sandbox wrap refused program "${command.path}" on ${platform}: ${wrap2.reason}`);
44591
+ }
44592
+ return inner.spawn(command);
44593
+ }
44594
+ const observation = inner.spawn(wrap2.command);
44595
+ if (observation.kind === "spawn-error") {
44596
+ const detail = observation.errorMessage ?? "unknown spawn error";
44597
+ return spawnError(command, `sandbox spawn failed for "${command.path}" via ${platform} launcher: ${detail}`);
44598
+ }
44599
+ if (observation.kind === "clean-exit" && observation.exitCode === 71 && wrap2.wrapped) {
44600
+ return spawnError(command, `sandbox launcher returned exit 71 (EX_OSERR) for "${command.path}" on ${platform}; often missing/non-executable helper or path denied inside the sandbox`);
44601
+ }
44602
+ return observation;
44603
+ }
44604
+ }
44605
+
44606
+ // src/harness/process/sandbox/detect.ts
44607
+ function detectSandboxLauncher(opts = {}) {
44608
+ const platform = opts.platform ?? process.platform;
44609
+ const exists2 = opts.existsSync ?? realExistsSync;
44610
+ if (platform === "darwin") {
44611
+ if (exists2(SANDBOX_EXEC_PATH)) {
44612
+ return { available: true, platform, path: SANDBOX_EXEC_PATH };
44613
+ }
44614
+ return { available: false, platform, reason: `${SANDBOX_EXEC_PATH} not found` };
44615
+ }
44616
+ if (platform === "linux") {
44617
+ const env = opts.env ?? process.env;
44618
+ const dirs = (env.PATH ?? "").split(path133.delimiter).filter(Boolean);
44619
+ for (const dir of dirs) {
44620
+ const candidate = path133.join(dir, BWRAP_PROGRAM);
44621
+ if (exists2(candidate)) {
44622
+ return { available: true, platform, path: candidate };
44623
+ }
44624
+ }
44625
+ return {
44626
+ available: false,
44627
+ platform,
44628
+ reason: "bubblewrap (bwrap) not found on PATH; install it (apt install bubblewrap / dnf install bubblewrap)"
44629
+ };
44630
+ }
44631
+ return { available: false, platform, reason: `OS sandbox unsupported on platform "${platform}"` };
44632
+ }
44633
+ function resolveSandboxAdapter(profile, inner, opts = {}) {
44634
+ const info = detectSandboxLauncher(opts);
44635
+ const adapter = new SandboxedProcessAdapter({
44636
+ profile,
44637
+ inner,
44638
+ platform: info.platform,
44639
+ launcherAvailable: info.available,
44640
+ ...info.path !== undefined ? { bwrapPath: info.path } : {},
44641
+ ...opts.failIfUnavailable !== undefined ? { failIfUnavailable: opts.failIfUnavailable } : {}
44642
+ });
44643
+ return { adapter, info };
44644
+ }
44645
+
44646
+ // src/harness/process/sandbox/capability-matrix.ts
44647
+ var SANDBOX_CAPABILITY_MATRIX = [
44648
+ { capability: "Filesystem containment", linux: "supported", darwin: "supported" },
44649
+ { capability: "Network OFF", linux: "supported", darwin: "supported" },
44650
+ { capability: "Domain allowlist", flag: "--allowed-domains", linux: "not-implemented", darwin: "supported" },
44651
+ { capability: "Credential masking", flag: "--mask-env", linux: "not-implemented", darwin: "supported" }
44652
+ ];
44653
+ function capabilityStatusFor(row, platform) {
44654
+ return platform === "linux" ? row.linux : row.darwin;
44655
+ }
44656
+ function isKnownSandboxPlatform(platform) {
44657
+ return platform === "linux" || platform === "darwin";
44658
+ }
44659
+
44660
+ // src/commands/sandbox.ts
44661
+ var LAUNCHER_NAME = {
44662
+ linux: "bubblewrap (bwrap)",
44663
+ darwin: "Seatbelt (sandbox-exec)"
44664
+ };
44665
+ var LAUNCHER_NOT_INSTALLED = "launcher not installed";
44666
+ var NOT_IMPLEMENTED_ON_PLATFORM = "not implemented on this platform";
44667
+ function buildSandboxReport(deps = {}) {
44668
+ const info = detectSandboxLauncher(deps);
44669
+ const platform = info.platform;
44670
+ const known = isKnownSandboxPlatform(platform);
44671
+ const makeRow = (row, kind, status) => ({
44672
+ capability: row.capability,
44673
+ kind,
44674
+ status,
44675
+ ...row.flag !== undefined ? { flag: row.flag } : {}
44676
+ });
44677
+ const capabilities = SANDBOX_CAPABILITY_MATRIX.map((row) => {
44678
+ if (!known) {
44679
+ return makeRow(row, "not-implemented", `${NOT_IMPLEMENTED_ON_PLATFORM} ("${platform}") \u2014 the OS sandbox has no support for this platform at all.`);
44680
+ }
44681
+ const status = capabilityStatusFor(row, platform);
44682
+ if (status === "not-implemented") {
44683
+ return makeRow(row, "not-implemented", `${NOT_IMPLEMENTED_ON_PLATFORM} \u2014 installing the OS sandbox launcher would not change this.`);
44684
+ }
44685
+ if (!info.available) {
44686
+ return makeRow(row, "launcher-missing", `requires ${LAUNCHER_NAME[platform]}; ${LAUNCHER_NOT_INSTALLED}.`);
44687
+ }
44688
+ return makeRow(row, "available", "available.");
44689
+ });
44690
+ return {
44691
+ platform,
44692
+ launcher: info,
44693
+ launcherName: known ? LAUNCHER_NAME[platform] : undefined,
44694
+ capabilities
44695
+ };
44696
+ }
44697
+ function renderSandboxReport(report) {
44698
+ const lines = [];
44699
+ lines.push("keryx sandbox status");
44700
+ lines.push("");
44701
+ lines.push(`Platform: ${report.platform}`);
44702
+ if (report.launcher.available) {
44703
+ lines.push(`Launcher: available (${report.launcherName ?? "unknown"}${report.launcher.path ? ` at ${report.launcher.path}` : ""})`);
44704
+ } else {
44705
+ lines.push(`Launcher: not found${report.launcherName ? ` (${report.launcherName})` : ""} \u2014 ${report.launcher.reason ?? "unavailable"}`);
44706
+ }
44707
+ lines.push("");
44708
+ lines.push("Capability matrix (this platform):");
44709
+ for (const cap of report.capabilities) {
44710
+ const flagNote = cap.flag ? ` (${cap.flag})` : "";
44711
+ lines.push(` - ${cap.capability}${flagNote}: ${cap.status}`);
44712
+ }
44713
+ lines.push("");
44714
+ lines.push("This is a report, not a gate \u2014 it always exits 0. A contained run still fails closed " + "regardless of what this prints; nothing here changes that behaviour.");
44715
+ return lines.join(`
44716
+ `);
44717
+ }
44718
+ function printHelp14() {
44719
+ console.log(`keryx sandbox status [--json] \u2014 OS sandbox launcher availability and the per-capability containment matrix
44720
+
44721
+ Usage:
44722
+ keryx sandbox status [--json]
44723
+
44724
+ Reports, for the current platform, whether the OS sandbox launcher (bubblewrap
44725
+ on Linux, Seatbelt on macOS) is installed, and for each containment capability
44726
+ (filesystem containment, network-off, domain allowlist, credential masking)
44727
+ whether it is available, blocked only on the launcher being missing, or not
44728
+ implemented on this platform at all \u2014 those are different findings and are
44729
+ never worded the same way. This command never runs a contained command and
44730
+ always exits 0: it is a report, not a gate. Run it any time; installation also
44731
+ prints this same information once, up front.
44732
+ `);
44733
+ }
44734
+ async function sandboxCommand(args2 = [], deps = {}) {
44735
+ const subcommand = args2[0];
44736
+ if (subcommand === "--help" || subcommand === "-h") {
44737
+ printHelp14();
44738
+ return;
44739
+ }
44740
+ if (subcommand !== undefined && subcommand !== "status" && subcommand !== "--json") {
44741
+ console.error(`Unknown sandbox command: ${subcommand}`);
44742
+ printHelp14();
44743
+ process.exitCode = 1;
44744
+ return;
44745
+ }
44746
+ const json = args2.includes("--json");
44747
+ const report = buildSandboxReport(deps);
44748
+ if (json) {
44749
+ console.log(JSON.stringify(report, null, 2));
44750
+ } else {
44751
+ console.log(renderSandboxReport(report));
44752
+ }
44753
+ }
44754
+
43943
44755
  // src/commands/mcp.ts
43944
44756
  init_args();
43945
- import path138 from "path";
44757
+ import path139 from "path";
43946
44758
 
43947
44759
  // src/mcp/discovery.ts
43948
44760
  init_fs();
43949
44761
  init_json();
43950
- import path133 from "path";
44762
+ import path134 from "path";
43951
44763
  var MODULE_MANIFEST_KEY = {
43952
44764
  gdgraph: "gdgraph",
43953
44765
  security: "security",
@@ -44009,7 +44821,7 @@ function buildDiscovery(manifest) {
44009
44821
  };
44010
44822
  }
44011
44823
  async function loadDiscovery(cwd) {
44012
- const manifestPath = path133.join(cwd, ".metaproject", "metaproject.json");
44824
+ const manifestPath = path134.join(cwd, ".metaproject", "metaproject.json");
44013
44825
  if (!await pathExists(manifestPath)) {
44014
44826
  return buildDiscovery({});
44015
44827
  }
@@ -44037,8 +44849,8 @@ async function invokeStructured(op, port, params) {
44037
44849
  switch (op.name) {
44038
44850
  case "search_code": {
44039
44851
  const pattern = stringParam(params, "pattern") ?? "";
44040
- const path134 = stringParam(params, "path");
44041
- return port.searchCode({ pattern, ...path134 !== undefined ? { path: path134 } : {} });
44852
+ const path135 = stringParam(params, "path");
44853
+ return port.searchCode({ pattern, ...path135 !== undefined ? { path: path135 } : {} });
44042
44854
  }
44043
44855
  case "graph_affected": {
44044
44856
  const target = stringParam(params, "file") ?? stringParam(params, "target") ?? "";
@@ -44053,8 +44865,8 @@ async function invokeStructured(op, port, params) {
44053
44865
  return port.memorySearch({ query });
44054
44866
  }
44055
44867
  case "read_wiki": {
44056
- const path134 = stringParam(params, "path") ?? "";
44057
- return port.readWiki({ path: path134 });
44868
+ const path135 = stringParam(params, "path") ?? "";
44869
+ return port.readWiki({ path: path135 });
44058
44870
  }
44059
44871
  default:
44060
44872
  return op.invoke(port, params);
@@ -44080,17 +44892,17 @@ init_sac();
44080
44892
  init_workspace_service();
44081
44893
  init_store2();
44082
44894
  init_fs();
44083
- import { createHash as createHash23, randomUUID as randomUUID15 } from "crypto";
44895
+ import { createHash as createHash24, randomUUID as randomUUID15 } from "crypto";
44084
44896
  import { appendFile as appendFile5, mkdir as mkdir52, open as open2, readFile as readFile73, rename as rename6, rm as rm7, stat as stat6, writeFile as writeFile46 } from "fs/promises";
44085
- import path134 from "path";
44897
+ import path135 from "path";
44086
44898
 
44087
44899
  // src/sac/policy-experiment.ts
44088
- import { createHash as createHash22, createHmac as createHmac2 } from "crypto";
44900
+ import { createHash as createHash23, createHmac as createHmac2 } from "crypto";
44089
44901
 
44090
44902
  // src/sac/receipt-integrity.ts
44091
44903
  init_sac();
44092
- import { createHash as createHash21 } from "crypto";
44093
- var sha2563 = (value) => createHash21("sha256").update(value, "utf8").digest("hex");
44904
+ import { createHash as createHash22 } from "crypto";
44905
+ var sha2563 = (value) => createHash22("sha256").update(value, "utf8").digest("hex");
44094
44906
  function bodyOf(receipt) {
44095
44907
  const { integrity: _integrity, ...body } = receipt;
44096
44908
  return body;
@@ -44147,7 +44959,7 @@ function stableValue(value) {
44147
44959
  return value;
44148
44960
  }
44149
44961
  var stableJson2 = (value) => JSON.stringify(stableValue(value));
44150
- var sha2564 = (value) => createHash22("sha256").update(stableJson2(value), "utf8").digest("hex");
44962
+ var sha2564 = (value) => createHash23("sha256").update(stableJson2(value), "utf8").digest("hex");
44151
44963
  var workspacePathPattern2 = /^\.\/(?!.*(?:^|\/)\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
44152
44964
  var trustedOutcomes = new WeakMap;
44153
44965
  var trustedSandboxEvidence = new WeakMap;
@@ -44167,7 +44979,7 @@ function policyCorpusDigest(manifest, rows, quarantine) {
44167
44979
  function splitFor(receiptHash, outcome, split) {
44168
44980
  if (outcome.caseClass === "adversarial")
44169
44981
  return "adversarial";
44170
- const byte = Number.parseInt(createHash22("sha256").update(`${split.seed}\x00${receiptHash}`).digest("hex").slice(0, 2), 16);
44982
+ const byte = Number.parseInt(createHash23("sha256").update(`${split.seed}\x00${receiptHash}`).digest("hex").slice(0, 2), 16);
44171
44983
  return byte / 256 * 100 < split.holdoutPercent ? "holdout" : "train";
44172
44984
  }
44173
44985
  function quarantineCounts(entries) {
@@ -44251,12 +45063,12 @@ var stableValue2 = (value) => {
44251
45063
  return value;
44252
45064
  };
44253
45065
  var stableJson3 = (value) => JSON.stringify(stableValue2(value));
44254
- var sha2565 = (value) => createHash23("sha256").update(stableJson3(value), "utf8").digest("hex");
45066
+ var sha2565 = (value) => createHash24("sha256").update(stableJson3(value), "utf8").digest("hex");
44255
45067
  var isImmutableVersion2 = (value) => typeof value === "string" && value.length >= 3 && value.length <= 256 && /^[A-Za-z0-9][A-Za-z0-9._:+-]*$/.test(value) && /\d/.test(value) && !immutableVersionPattern.test(value);
44256
45068
  var receiptHashPattern2 = /^[a-f0-9]{64}$/;
44257
45069
  var missingIdentity = Object.freeze({ ledgerBytes: 0, device: "0", inode: "0", modifiedNs: "0", changedNs: "0" });
44258
45070
  var isMissing = (error2) => error2 instanceof Error && error2.code === "ENOENT";
44259
- var checkpointHash = (body) => createHash23("sha256").update(JSON.stringify(body), "utf8").digest("hex");
45071
+ var checkpointHash = (body) => createHash24("sha256").update(JSON.stringify(body), "utf8").digest("hex");
44260
45072
  async function ledgerIdentity(ledger) {
44261
45073
  const value = await stat6(ledger, { bigint: true });
44262
45074
  if (value.size > BigInt(Number.MAX_SAFE_INTEGER))
@@ -44401,7 +45213,7 @@ var isConfigRecord = (value) => typeof value.enabled === "boolean" && typeof val
44401
45213
  async function readPinnedJson(input2) {
44402
45214
  const ref = await resolveWorkspaceReference({ workspaceRoot: input2.workspaceRoot, kind: "artifact", uri: input2.uri });
44403
45215
  const raw = await readFile73(ref, "utf8");
44404
- const digest2 = createHash23("sha256").update(raw).digest("hex");
45216
+ const digest2 = createHash24("sha256").update(raw).digest("hex");
44405
45217
  if (digest2 !== input2.digest)
44406
45218
  throw new Error("policy experiment pinned artifact digest mismatch");
44407
45219
  return { artifact: JSON.parse(raw), digest: digest2 };
@@ -44416,7 +45228,7 @@ function hasReportIntegrity(report) {
44416
45228
  return reportHashPattern.test(reportDigest) && sha2565(body) === reportDigest;
44417
45229
  }
44418
45230
  async function resolvePolicySelection(workspaceRoot, canonicalFallback) {
44419
- const configPath3 = path134.join(workspaceRoot, ...policyExperimentConfigPath);
45231
+ const configPath3 = path135.join(workspaceRoot, ...policyExperimentConfigPath);
44420
45232
  let configJson;
44421
45233
  try {
44422
45234
  const raw = await readFile73(configPath3, "utf8").catch((error2) => {
@@ -44518,7 +45330,7 @@ async function diagnosePolicyReadiness(workspaceRoot) {
44518
45330
  const integrityReady = steps.every((entry) => entry.step === "activation-flags" || entry.status === "pass");
44519
45331
  return Object.freeze({ configPresent, enabled: enabled2, killSwitch: killSwitch2, integrityReady, candidateWouldActivate: integrityReady && enabled2 && !killSwitch2, steps: Object.freeze([...steps]) });
44520
45332
  };
44521
- const configPath3 = path134.join(workspaceRoot, ...policyExperimentConfigPath);
45333
+ const configPath3 = path135.join(workspaceRoot, ...policyExperimentConfigPath);
44522
45334
  let configJson;
44523
45335
  try {
44524
45336
  const raw = await readFile73(configPath3, "utf8").catch((error2) => {
@@ -44749,9 +45561,9 @@ class FwkReadService {
44749
45561
  const recordedAt = nowIso2(now);
44750
45562
  const id = `receipt-${randomUUID15().replace(/-/g, "").slice(0, 16)}`;
44751
45563
  const base = { schemaVersion: "1.0", id, workspaceId, actor, action, decision, recordedAt, cost: { tokens: 0, toolCalls: 1, elapsedMs: 0 }, contextAssembly: { traceRef: assembly.traceRef, configurationRevision: assembly.configurationRevision, selected: assembly.selected.map((id2) => `./ids/${id2}`), omittedOptional: assembly.omittedOptional.map((id2) => `./ids/${id2}`) }, policy: { ref: assembly.policyRef, revision: assembly.policyRevision }, ...action === "resource" ? { resourceRef: `./ids/${resourceId ?? "unknown"}` } : {} };
44752
- const ledger = path134.join(this.options.canonical.workspaceRoot, ".metaproject", "context-operations", "access-receipts.jsonl");
44753
- const checkpointPath = path134.join(path134.dirname(ledger), "access-receipts.checkpoint.json");
44754
- await mkdir52(path134.dirname(ledger), { recursive: true, mode: 448 });
45564
+ const ledger = path135.join(this.options.canonical.workspaceRoot, ".metaproject", "context-operations", "access-receipts.jsonl");
45565
+ const checkpointPath = path135.join(path135.dirname(ledger), "access-receipts.checkpoint.json");
45566
+ await mkdir52(path135.dirname(ledger), { recursive: true, mode: 448 });
44755
45567
  return withFileLock2(`${ledger}.lock`, async () => {
44756
45568
  const state = await resolveLedgerState(ledger, checkpointPath, this.options.verifyReceiptLedger ?? verifyAccessReceiptLedger);
44757
45569
  const receipt = sealAccessReceipt(base, state.headHash);
@@ -44797,12 +45609,12 @@ function createLocalFwkReadService(cwd, opts) {
44797
45609
  const flow = manifest.resources.find((resource) => resource.kind === "flow");
44798
45610
  const facts = await Promise.all(manifest.resources.filter((resource) => resource.kind === "evidence").map(async (resource, index) => {
44799
45611
  const raw = await workspaces.readResourceForActor({ actorContext, workspaceId, resource });
44800
- const revision = createHash23("sha256").update(raw).digest("hex");
45612
+ const revision = createHash24("sha256").update(raw).digest("hex");
44801
45613
  return { id: `fact-${index}`, uri: resource.uri, revision: resource.revision ?? revision, observedAt: manifest.updatedAt, expiresAt: "9999-12-31T23:59:59Z", trust: "primary", visible: true, statement: `Evidence reference ${resource.uri}`, status: resource.revision === revision || resource.revision === undefined ? "fresh" : "stale" };
44802
45614
  }));
44803
45615
  const knowHow = await Promise.all(manifest.resources.filter((resource) => resource.kind === "wiki" || resource.kind === "memory" || resource.kind === "skill").map(async (resource, index) => {
44804
45616
  const raw = await workspaces.readResourceForActor({ actorContext, workspaceId, resource, encoding: "utf8" });
44805
- const revision = createHash23("sha256").update(raw).digest("hex");
45617
+ const revision = createHash24("sha256").update(raw).digest("hex");
44806
45618
  const accepted = /^Status:\s*(accepted|reviewed)\s*$/mi.test(raw);
44807
45619
  return { id: `knowhow-${index}`, kind: resource.kind, uri: resource.uri, revision: resource.revision ?? revision, trust: "accepted", status: resource.revision === revision || resource.revision === undefined ? "fresh" : "stale", accepted, visible: true };
44808
45620
  }));
@@ -44844,7 +45656,7 @@ init_workspace_service();
44844
45656
  init_sac();
44845
45657
  import { appendFile as appendFile6, mkdir as mkdir53, readFile as readFile74 } from "fs/promises";
44846
45658
  import { randomUUID as randomUUID16 } from "crypto";
44847
- import path135 from "path";
45659
+ import path136 from "path";
44848
45660
 
44849
45661
  class CollaborationServiceError extends Error {
44850
45662
  code;
@@ -44860,7 +45672,7 @@ class CollaborationService {
44860
45672
  this.input = input2;
44861
45673
  }
44862
45674
  file(id) {
44863
- return path135.join(this.input.workspaceRoot, ".metaproject", "workspaces", id, "activity.jsonl");
45675
+ return path136.join(this.input.workspaceRoot, ".metaproject", "workspaces", id, "activity.jsonl");
44864
45676
  }
44865
45677
  async overview(input2) {
44866
45678
  const manifest = await this.input.workspaces.show(input2);
@@ -44885,7 +45697,7 @@ class CollaborationService {
44885
45697
  if (!isWorkspaceOwner(manifest.members, actor.subject))
44886
45698
  throw new CollaborationServiceError("access_denied", "owner authority is required");
44887
45699
  const activity = this.validate({ schemaVersion: "1.0", id: `activity-${randomUUID16()}`, workspaceId: manifest.id, actorSubject: actor.subject, occurredAt: (this.input.now ?? (() => new Date))().toISOString(), ...input2.activity });
44888
- await mkdir53(path135.dirname(this.file(manifest.id)), { recursive: true, mode: 448 });
45700
+ await mkdir53(path136.dirname(this.file(manifest.id)), { recursive: true, mode: 448 });
44889
45701
  await appendFile6(this.file(manifest.id), `${JSON.stringify(activity)}
44890
45702
  `, { mode: 384 });
44891
45703
  return normalizeCollaborationResult(activity);
@@ -44919,9 +45731,9 @@ init_workspace_service();
44919
45731
  init_fs();
44920
45732
  init_machine_wrap_up();
44921
45733
  import { mkdir as mkdir54, readdir as readdir23, readFile as readFile75 } from "fs/promises";
44922
- import path136 from "path";
45734
+ import path137 from "path";
44923
45735
  function externalSlatesDir(cwd) {
44924
- return path136.join(cwd, ".keryx", "external-slates");
45736
+ return path137.join(cwd, ".keryx", "external-slates");
44925
45737
  }
44926
45738
  var EXTERNAL_SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
44927
45739
  function assertValidExternalSessionId(externalSessionId) {
@@ -44931,14 +45743,14 @@ function assertValidExternalSessionId(externalSessionId) {
44931
45743
  }
44932
45744
  function externalSlatePath(cwd, externalSessionId) {
44933
45745
  assertValidExternalSessionId(externalSessionId);
44934
- return path136.join(externalSlatesDir(cwd), `${externalSessionId}.json`);
45746
+ return path137.join(externalSlatesDir(cwd), `${externalSessionId}.json`);
44935
45747
  }
44936
45748
  function externalSlateLockPath(cwd, externalSessionId) {
44937
45749
  return `${externalSlatePath(cwd, externalSessionId)}.lock`;
44938
45750
  }
44939
45751
  function externalSlateEvidenceDir(cwd, externalSessionId) {
44940
45752
  assertValidExternalSessionId(externalSessionId);
44941
- return path136.join(externalSlatesDir(cwd), externalSessionId);
45753
+ return path137.join(externalSlatesDir(cwd), externalSessionId);
44942
45754
  }
44943
45755
  async function readExternalSlate(cwd, externalSessionId) {
44944
45756
  try {
@@ -45487,7 +46299,7 @@ function buildToolRegistry() {
45487
46299
 
45488
46300
  // src/mcp/resources.ts
45489
46301
  init_fs();
45490
- import path137 from "path";
46302
+ import path138 from "path";
45491
46303
  import { readdir as readdir24, readFile as readFile77, stat as stat7 } from "fs/promises";
45492
46304
  var URI_PREFIX = "metaproject://";
45493
46305
  function mimeForPath(filePath) {
@@ -45500,13 +46312,13 @@ function mimeForPath(filePath) {
45500
46312
  return "text/plain";
45501
46313
  }
45502
46314
  function dataRoot3(cwd) {
45503
- return path137.join(cwd, ".metaproject", "data");
46315
+ return path138.join(cwd, ".metaproject", "data");
45504
46316
  }
45505
46317
  function wikiRoot(cwd) {
45506
- return path137.join(cwd, ".metaproject", "wiki");
46318
+ return path138.join(cwd, ".metaproject", "wiki");
45507
46319
  }
45508
46320
  function memoryRoot2(cwd) {
45509
- return path137.join(cwd, ".metaproject", "memory");
46321
+ return path138.join(cwd, ".metaproject", "memory");
45510
46322
  }
45511
46323
  async function walkFiles(root) {
45512
46324
  if (!await pathExists(root)) {
@@ -45520,7 +46332,7 @@ async function walkFiles(root) {
45520
46332
  return [];
45521
46333
  }
45522
46334
  for (const entry of entries) {
45523
- const full = path137.join(root, entry.name);
46335
+ const full = path138.join(root, entry.name);
45524
46336
  if (entry.isDirectory()) {
45525
46337
  out.push(...await walkFiles(full));
45526
46338
  } else if (entry.isFile()) {
@@ -45545,9 +46357,9 @@ async function listArtifacts(cwd) {
45545
46357
  if (!moduleEntry.isDirectory()) {
45546
46358
  continue;
45547
46359
  }
45548
- const artifactsDir2 = path137.join(base, moduleEntry.name, "artifacts");
46360
+ const artifactsDir2 = path138.join(base, moduleEntry.name, "artifacts");
45549
46361
  for (const file of await walkFiles(artifactsDir2)) {
45550
- const rel = toPosix(path137.relative(artifactsDir2, file));
46362
+ const rel = toPosix(path138.relative(artifactsDir2, file));
45551
46363
  const relPath = `${moduleEntry.name}/${rel}`;
45552
46364
  listings.push({
45553
46365
  uri: `${URI_PREFIX}artifacts/${relPath}`,
@@ -45561,7 +46373,7 @@ async function listArtifacts(cwd) {
45561
46373
  async function listUnderRoot(cwd, cls, root) {
45562
46374
  const listings = [];
45563
46375
  for (const file of await walkFiles(root)) {
45564
- const rel = toPosix(path137.relative(root, file));
46376
+ const rel = toPosix(path138.relative(root, file));
45565
46377
  listings.push({
45566
46378
  uri: `${URI_PREFIX}${cls}/${rel}`,
45567
46379
  name: rel,
@@ -45611,12 +46423,12 @@ function resolveConfined(cwd, cls, relPath) {
45611
46423
  if (moduleName.includes("..") || moduleName.length === 0) {
45612
46424
  return null;
45613
46425
  }
45614
- const root2 = path137.join(dataRoot3(cwd), moduleName, "artifacts");
45615
- const absolute2 = path137.resolve(root2, rest);
46426
+ const root2 = path138.join(dataRoot3(cwd), moduleName, "artifacts");
46427
+ const absolute2 = path138.resolve(root2, rest);
45616
46428
  return isPathInside(root2, absolute2) ? { root: root2, absolute: absolute2 } : null;
45617
46429
  }
45618
46430
  const root = cls === "wiki" ? wikiRoot(cwd) : memoryRoot2(cwd);
45619
- const absolute = path137.resolve(root, relPath);
46431
+ const absolute = path138.resolve(root, relPath);
45620
46432
  return isPathInside(root, absolute) ? { root, absolute } : null;
45621
46433
  }
45622
46434
  async function readResource(cwd, roots, uri) {
@@ -45816,7 +46628,7 @@ async function mcpCommand(args2 = [], cwd = process.cwd()) {
45816
46628
  }
45817
46629
  if (!subcommand || subcommand === "serve") {
45818
46630
  const http = args2.includes("--http");
45819
- const projectRoot = path138.resolve(optionValue(args2, "--cwd") ?? cwd);
46631
+ const projectRoot = path139.resolve(optionValue(args2, "--cwd") ?? cwd);
45820
46632
  try {
45821
46633
  await serveMcp({ cwd: projectRoot, http });
45822
46634
  } catch (error2) {
@@ -45850,7 +46662,7 @@ async function handleInstall2(cwd, args2) {
45850
46662
  console.log(outcome.snippet ?? "");
45851
46663
  continue;
45852
46664
  }
45853
- const rel = path138.relative(cwd, outcome.filePath);
46665
+ const rel = path139.relative(cwd, outcome.filePath);
45854
46666
  if (outcome.errors.length > 0) {
45855
46667
  for (const e of outcome.errors) {
45856
46668
  console.log(` ${style.red(symbols.cross)} ${e}`);
@@ -45890,7 +46702,7 @@ async function handleUninstall2(cwd, args2) {
45890
46702
  console.log(` ${style.gray(symbols.off)} ${outcome.id} ${style.dim("no file to change")}`);
45891
46703
  continue;
45892
46704
  }
45893
- const rel = path138.relative(cwd, outcome.filePath);
46705
+ const rel = path139.relative(cwd, outcome.filePath);
45894
46706
  console.log(` ${outcome.removed ? style.green(symbols.ok) : style.gray(symbols.off)} ${outcome.id} ${style.dim(outcome.removed ? `removed from ${rel}` : "nothing to remove")}`);
45895
46707
  }
45896
46708
  }
@@ -45920,14 +46732,14 @@ function printMcpHelp() {
45920
46732
  // src/commands/status.ts
45921
46733
  init_fs();
45922
46734
  init_json();
45923
- import path139 from "path";
46735
+ import path140 from "path";
45924
46736
  async function statusCommand(args2 = []) {
45925
46737
  if (args2.includes("--help") || args2.includes("-h")) {
45926
- printHelp14();
46738
+ printHelp15();
45927
46739
  return;
45928
46740
  }
45929
- const root = path139.join(process.cwd(), ".metaproject");
45930
- const manifestPath = path139.join(root, "metaproject.json");
46741
+ const root = path140.join(process.cwd(), ".metaproject");
46742
+ const manifestPath = path140.join(root, "metaproject.json");
45931
46743
  if (!await pathExists(root)) {
45932
46744
  console.log("Metaproject: not initialized");
45933
46745
  console.log("Run: keryx init");
@@ -45954,7 +46766,7 @@ async function statusCommand(args2 = []) {
45954
46766
  console.log(` ${name}: ${moduleConfig.enabled ? "enabled" : "disabled"}`);
45955
46767
  }
45956
46768
  }
45957
- function printHelp14() {
46769
+ function printHelp15() {
45958
46770
  console.log(`keryx status \u2014 whether this project has a .metaproject workspace, and which modules are on
45959
46771
 
45960
46772
  Usage:
@@ -46023,7 +46835,7 @@ init_make_provider();
46023
46835
  init_profiles();
46024
46836
 
46025
46837
  // src/harness/run/run.ts
46026
- import { createHash as createHash27 } from "crypto";
46838
+ import { createHash as createHash28 } from "crypto";
46027
46839
 
46028
46840
  // src/harness/completion/gate.ts
46029
46841
  var SCHEMA_VERSION2 = 1;
@@ -46074,10 +46886,10 @@ function evaluateCompletion(input2, deps) {
46074
46886
  }
46075
46887
 
46076
46888
  // src/harness/evidence/redaction.ts
46077
- import { createHash as createHash24 } from "crypto";
46889
+ import { createHash as createHash25 } from "crypto";
46078
46890
  var CLEAN_CATEGORY = "none";
46079
46891
  function hashContent(content) {
46080
- return createHash24("sha256").update(content, "utf8").digest("hex");
46892
+ return createHash25("sha256").update(content, "utf8").digest("hex");
46081
46893
  }
46082
46894
  function redactForPersistence(content, deps) {
46083
46895
  const scan2 = deps.scan(content);
@@ -46128,7 +46940,7 @@ async function metaprojectBlastRadius(port, target) {
46128
46940
  }
46129
46941
 
46130
46942
  // src/harness/session/session.ts
46131
- import { createHash as createHash25 } from "crypto";
46943
+ import { createHash as createHash26 } from "crypto";
46132
46944
  var SCHEMA_VERSION3 = 1;
46133
46945
  function canonicalStringify(value) {
46134
46946
  if (Array.isArray(value)) {
@@ -46142,7 +46954,7 @@ function canonicalStringify(value) {
46142
46954
  return JSON.stringify(value) ?? "null";
46143
46955
  }
46144
46956
  function sha2566(input2) {
46145
- return createHash25("sha256").update(input2, "utf8").digest("hex");
46957
+ return createHash26("sha256").update(input2, "utf8").digest("hex");
46146
46958
  }
46147
46959
  function deepFreeze(value) {
46148
46960
  if (value !== null && typeof value === "object") {
@@ -46234,11 +47046,11 @@ class AppendOnlySession {
46234
47046
  }
46235
47047
 
46236
47048
  // src/harness/context/manifest.ts
46237
- import { createHash as createHash26 } from "crypto";
47049
+ import { createHash as createHash27 } from "crypto";
46238
47050
  var MAX_CONTEXT_BYTES = 2 * 1024 * 1024;
46239
47051
  var MAX_CONTEXT_TOKENS = 200000;
46240
47052
  function sha2567(input2) {
46241
- return createHash26("sha256").update(input2, "utf8").digest("hex");
47053
+ return createHash27("sha256").update(input2, "utf8").digest("hex");
46242
47054
  }
46243
47055
  function normalizeSource(value) {
46244
47056
  const record = typeof value === "object" && value !== null ? value : {};
@@ -46364,7 +47176,7 @@ function canonicalize2(value) {
46364
47176
  return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalize2(record[key])}`).join(",")}}`;
46365
47177
  }
46366
47178
  function sha2568(input2) {
46367
- return createHash27("sha256").update(input2, "utf8").digest("hex");
47179
+ return createHash28("sha256").update(input2, "utf8").digest("hex");
46368
47180
  }
46369
47181
  function uniqueInOrder2(values) {
46370
47182
  const seen = new Set;
@@ -46802,7 +47614,7 @@ function replayOffline(fixture, run, deps) {
46802
47614
  }
46803
47615
 
46804
47616
  // src/harness/tool/registry.ts
46805
- import { createHash as createHash28 } from "crypto";
47617
+ import { createHash as createHash29 } from "crypto";
46806
47618
  function canonicalize3(value) {
46807
47619
  if (value === null || typeof value !== "object") {
46808
47620
  return JSON.stringify(value) ?? "null";
@@ -46816,7 +47628,7 @@ function canonicalize3(value) {
46816
47628
  return `{${entries.join(",")}}`;
46817
47629
  }
46818
47630
  function sha2569(input2) {
46819
- return createHash28("sha256").update(input2, "utf8").digest("hex");
47631
+ return createHash29("sha256").update(input2, "utf8").digest("hex");
46820
47632
  }
46821
47633
  function definitionHash(definition) {
46822
47634
  return sha2569(canonicalize3(definition));
@@ -46967,11 +47779,11 @@ function runContainedProcess(input2, deps) {
46967
47779
 
46968
47780
  // src/harness/process/real-process-adapter.ts
46969
47781
  import { spawnSync } from "child_process";
46970
- import { createHash as createHash29 } from "crypto";
47782
+ import { createHash as createHash30 } from "crypto";
46971
47783
  var DEFAULT_TIMEOUT_MS = 5000;
46972
47784
  var DEFAULT_MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
46973
47785
  function sha256Hex4(input2) {
46974
- return createHash29("sha256").update(input2, "utf8").digest("hex");
47786
+ return createHash30("sha256").update(input2, "utf8").digest("hex");
46975
47787
  }
46976
47788
  function byteLength(chunk) {
46977
47789
  if (chunk === null || chunk === undefined)
@@ -47040,7 +47852,7 @@ ${command.argv.join(" ")}`);
47040
47852
  }
47041
47853
 
47042
47854
  // src/harness/process/sandbox/profile.ts
47043
- import path140 from "path";
47855
+ import path141 from "path";
47044
47856
  var DEFAULT_SECRET_SUBPATHS = [
47045
47857
  ".ssh",
47046
47858
  ".gnupg",
@@ -47065,7 +47877,7 @@ function defaultReadDenyList(home) {
47065
47877
  if (!home) {
47066
47878
  return [];
47067
47879
  }
47068
- return DEFAULT_SECRET_SUBPATHS.map((sub) => path140.join(home, sub));
47880
+ return DEFAULT_SECRET_SUBPATHS.map((sub) => path141.join(home, sub));
47069
47881
  }
47070
47882
  function defaultSandboxProfile(cwd, tmpDir, home) {
47071
47883
  return {
@@ -47081,230 +47893,6 @@ function dedupe2(values) {
47081
47893
  return [...new Set(values.filter((v) => v.length > 0))];
47082
47894
  }
47083
47895
 
47084
- // src/harness/process/sandbox/detect.ts
47085
- import { existsSync as realExistsSync } from "fs";
47086
- import path141 from "path";
47087
-
47088
- // src/harness/process/sandbox/seatbelt.ts
47089
- var SANDBOX_EXEC_PATH = "/usr/bin/sandbox-exec";
47090
- function sbplString(value) {
47091
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
47092
- }
47093
- var DEVICE_WRITE_LITERALS = [
47094
- "/dev/null",
47095
- "/dev/zero",
47096
- "/dev/stdin",
47097
- "/dev/stdout",
47098
- "/dev/stderr",
47099
- "/dev/tty",
47100
- "/dev/dtracehelper",
47101
- "/dev/random",
47102
- "/dev/urandom"
47103
- ];
47104
- function buildSeatbeltProfile(profile) {
47105
- const lines = [
47106
- "(version 1)",
47107
- "(allow default)",
47108
- "",
47109
- ";; --- filesystem writes: deny everything, then re-allow workspace roots ---",
47110
- '(deny file-write* (subpath "/"))'
47111
- ];
47112
- for (const root of profile.writableRoots) {
47113
- lines.push(`(allow file-write* (subpath ${sbplString(root)}))`);
47114
- }
47115
- lines.push(`(allow file-write-data ${DEVICE_WRITE_LITERALS.map((d) => `(literal ${sbplString(d)})`).join(" ")})`);
47116
- if (profile.readDenyList.length > 0) {
47117
- lines.push("", ";; --- secret read-deny ---");
47118
- for (const secret of profile.readDenyList) {
47119
- lines.push(`(deny file-read* (subpath ${sbplString(secret)}))`);
47120
- }
47121
- }
47122
- if (profile.network === "off") {
47123
- lines.push("", ";; --- network off ---", "(deny network*)");
47124
- } else if (profile.network === "restricted") {
47125
- lines.push("", ";; --- network restricted to loopback allowlist proxy ---", "(deny network*)");
47126
- if (profile.proxy) {
47127
- lines.push(`(allow network-outbound (remote ip ${sbplString(`localhost:${profile.proxy.port}`)}))`);
47128
- }
47129
- }
47130
- return `${lines.join(`
47131
- `)}
47132
- `;
47133
- }
47134
- function wrapSeatbelt(command, profile) {
47135
- const profileText = buildSeatbeltProfile(profile);
47136
- return {
47137
- path: SANDBOX_EXEC_PATH,
47138
- argv: ["sandbox-exec", "-p", profileText, command.path, ...command.argv.slice(1)],
47139
- env: command.env,
47140
- cwd: command.cwd
47141
- };
47142
- }
47143
-
47144
- // src/harness/process/sandbox/bwrap.ts
47145
- import { statSync as statSync4 } from "fs";
47146
- var BWRAP_PROGRAM = "bwrap";
47147
- function inspectMaskTarget(target) {
47148
- try {
47149
- return statSync4(target).isDirectory() ? "dir" : "file";
47150
- } catch {
47151
- return "missing";
47152
- }
47153
- }
47154
- function buildBwrapArgs(profile, inspect = inspectMaskTarget) {
47155
- const args2 = [
47156
- "--ro-bind",
47157
- "/",
47158
- "/",
47159
- "--dev",
47160
- "/dev",
47161
- "--proc",
47162
- "/proc",
47163
- "--tmpfs",
47164
- "/tmp"
47165
- ];
47166
- for (const root of profile.writableRoots) {
47167
- args2.push("--bind", root, root);
47168
- }
47169
- for (const secret of profile.readDenyList) {
47170
- const kind = inspect(secret);
47171
- if (kind === "dir") {
47172
- args2.push("--tmpfs", secret);
47173
- } else if (kind === "file") {
47174
- args2.push("--ro-bind", "/dev/null", secret);
47175
- }
47176
- }
47177
- if (profile.network === "off") {
47178
- args2.push("--unshare-net");
47179
- }
47180
- args2.push("--unshare-pid", "--unshare-ipc", "--die-with-parent", "--new-session");
47181
- return args2;
47182
- }
47183
- function wrapBwrap(command, profile, launcherPath = BWRAP_PROGRAM) {
47184
- return {
47185
- path: launcherPath,
47186
- argv: [BWRAP_PROGRAM, ...buildBwrapArgs(profile), "--", command.path, ...command.argv.slice(1)],
47187
- env: command.env,
47188
- cwd: command.cwd
47189
- };
47190
- }
47191
-
47192
- // src/harness/process/sandbox/adapter.ts
47193
- import { createHash as createHash30 } from "crypto";
47194
-
47195
- // src/harness/process/sandbox/wrap.ts
47196
- function wrapWithSandbox(command, profile, opts) {
47197
- if (profile.mode === "danger-full-access") {
47198
- return { ok: true, command, wrapped: false };
47199
- }
47200
- if (opts.platform === "darwin") {
47201
- return { ok: true, command: wrapSeatbelt(command, profile), wrapped: true };
47202
- }
47203
- if (opts.platform === "linux") {
47204
- if (profile.network === "restricted") {
47205
- return {
47206
- ok: false,
47207
- reason: "network=restricted is not yet enforced on Linux (needs a network namespace + proxy relay); use network off/on or run inside a container."
47208
- };
47209
- }
47210
- const wrapped = opts.bwrapPath ? wrapBwrap(command, profile, opts.bwrapPath) : wrapBwrap(command, profile);
47211
- return { ok: true, command: wrapped, wrapped: true };
47212
- }
47213
- return {
47214
- ok: false,
47215
- reason: `OS sandbox is unsupported on platform "${opts.platform}"; run inside WSL2 or a container, or use an explicit danger-full-access override.`
47216
- };
47217
- }
47218
-
47219
- // src/harness/process/sandbox/adapter.ts
47220
- function spawnError(command, message2) {
47221
- return {
47222
- kind: "spawn-error",
47223
- observedHash: createHash30("sha256").update(`${command.path}
47224
- ${command.argv.join(" ")}`, "utf8").digest("hex"),
47225
- errorMessage: message2
47226
- };
47227
- }
47228
-
47229
- class SandboxedProcessAdapter {
47230
- opts;
47231
- constructor(opts) {
47232
- this.opts = opts;
47233
- }
47234
- spawn(command) {
47235
- const { profile, inner, platform, launcherAvailable, bwrapPath } = this.opts;
47236
- const failClosed = profile.required || profile.network === "restricted" || (this.opts.failIfUnavailable ?? true);
47237
- if (profile.mode === "danger-full-access") {
47238
- return inner.spawn(command);
47239
- }
47240
- if (!launcherAvailable) {
47241
- if (failClosed) {
47242
- return spawnError(command, `OS sandbox launcher unavailable on ${platform} for program "${command.path}"; failing closed (install bubblewrap on Linux, or relax failIfUnavailable to run unsandboxed).`);
47243
- }
47244
- return inner.spawn(command);
47245
- }
47246
- const wrap2 = wrapWithSandbox(command, profile, {
47247
- platform,
47248
- ...bwrapPath !== undefined ? { bwrapPath } : {}
47249
- });
47250
- if (!wrap2.ok) {
47251
- if (failClosed) {
47252
- return spawnError(command, `sandbox wrap refused program "${command.path}" on ${platform}: ${wrap2.reason}`);
47253
- }
47254
- return inner.spawn(command);
47255
- }
47256
- const observation = inner.spawn(wrap2.command);
47257
- if (observation.kind === "spawn-error") {
47258
- const detail = observation.errorMessage ?? "unknown spawn error";
47259
- return spawnError(command, `sandbox spawn failed for "${command.path}" via ${platform} launcher: ${detail}`);
47260
- }
47261
- if (observation.kind === "clean-exit" && observation.exitCode === 71 && wrap2.wrapped) {
47262
- return spawnError(command, `sandbox launcher returned exit 71 (EX_OSERR) for "${command.path}" on ${platform}; often missing/non-executable helper or path denied inside the sandbox`);
47263
- }
47264
- return observation;
47265
- }
47266
- }
47267
-
47268
- // src/harness/process/sandbox/detect.ts
47269
- function detectSandboxLauncher(opts = {}) {
47270
- const platform = opts.platform ?? process.platform;
47271
- const exists2 = opts.existsSync ?? realExistsSync;
47272
- if (platform === "darwin") {
47273
- if (exists2(SANDBOX_EXEC_PATH)) {
47274
- return { available: true, platform, path: SANDBOX_EXEC_PATH };
47275
- }
47276
- return { available: false, platform, reason: `${SANDBOX_EXEC_PATH} not found` };
47277
- }
47278
- if (platform === "linux") {
47279
- const env = opts.env ?? process.env;
47280
- const dirs = (env.PATH ?? "").split(path141.delimiter).filter(Boolean);
47281
- for (const dir of dirs) {
47282
- const candidate = path141.join(dir, BWRAP_PROGRAM);
47283
- if (exists2(candidate)) {
47284
- return { available: true, platform, path: candidate };
47285
- }
47286
- }
47287
- return {
47288
- available: false,
47289
- platform,
47290
- reason: "bubblewrap (bwrap) not found on PATH; install it (apt install bubblewrap / dnf install bubblewrap)"
47291
- };
47292
- }
47293
- return { available: false, platform, reason: `OS sandbox unsupported on platform "${platform}"` };
47294
- }
47295
- function resolveSandboxAdapter(profile, inner, opts = {}) {
47296
- const info = detectSandboxLauncher(opts);
47297
- const adapter = new SandboxedProcessAdapter({
47298
- profile,
47299
- inner,
47300
- platform: info.platform,
47301
- launcherAvailable: info.available,
47302
- ...info.path !== undefined ? { bwrapPath: info.path } : {},
47303
- ...opts.failIfUnavailable !== undefined ? { failIfUnavailable: opts.failIfUnavailable } : {}
47304
- });
47305
- return { adapter, info };
47306
- }
47307
-
47308
47896
  // src/lib/sandbox-config.ts
47309
47897
  init_config_dir();
47310
47898
  init_shell_config();
@@ -47952,7 +48540,7 @@ function parseArgs2(args2) {
47952
48540
  let unattended;
47953
48541
  let goal;
47954
48542
  let workspace;
47955
- const positional = [];
48543
+ const positional2 = [];
47956
48544
  for (let i = 1;i < args2.length; i++) {
47957
48545
  const arg = args2[i];
47958
48546
  if (arg === "--provider") {
@@ -47972,10 +48560,10 @@ function parseArgs2(args2) {
47972
48560
  const next = args2[i + 1];
47973
48561
  workspace = next !== undefined && !KNOWN_HARNESS_RUN_FLAGS.has(next) ? args2[++i] : undefined;
47974
48562
  } else if (arg !== undefined) {
47975
- positional.push(arg);
48563
+ positional2.push(arg);
47976
48564
  }
47977
48565
  }
47978
- const parsed = { provider, model, prompt: goal !== undefined && goal.length > 0 ? goal : positional.join(" ") };
48566
+ const parsed = { provider, model, prompt: goal !== undefined && goal.length > 0 ? goal : positional2.join(" ") };
47979
48567
  if (baseUrl !== undefined)
47980
48568
  parsed.baseUrl = baseUrl;
47981
48569
  if (record !== undefined)
@@ -50682,7 +51270,7 @@ function suggestShellPatterns(command) {
50682
51270
  exact,
50683
51271
  prefix,
50684
51272
  offerExact: !neverRemember && validateShellPattern(exact).ok,
50685
- offerPrefix: !neverRemember && validateShellPattern(prefix).ok
51273
+ offerPrefix: !neverRemember && validateShellPattern(prefix).ok && isShellCommandAllowed(trimmed, [prefix])
50686
51274
  };
50687
51275
  }
50688
51276
  function parseShellExecCommand(inputJson) {
@@ -53997,7 +54585,7 @@ import { spawnSync as spawnSync2 } from "child_process";
53997
54585
  // package.json
53998
54586
  var package_default = {
53999
54587
  name: "@mrciphersmith/keryx",
54000
- version: "0.2.69",
54588
+ version: "0.2.70",
54001
54589
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
54002
54590
  private: false,
54003
54591
  publishConfig: {
@@ -67679,7 +68267,7 @@ async function shellCommand(args2, runtime = {}) {
67679
68267
  async function sessionsCommand(args2) {
67680
68268
  const sub = args2[0] ?? "list";
67681
68269
  if (sub === "--help" || sub === "-h" || sub === "help") {
67682
- printHelp15();
68270
+ printHelp16();
67683
68271
  return;
67684
68272
  }
67685
68273
  const cwd = process.cwd();
@@ -67785,7 +68373,7 @@ async function sessionsCommand(args2) {
67785
68373
  return;
67786
68374
  }
67787
68375
  console.error(`Unknown sessions subcommand: ${sub}`);
67788
- printHelp15();
68376
+ printHelp16();
67789
68377
  process.exitCode = 1;
67790
68378
  }
67791
68379
  function pad(s, n) {
@@ -67794,7 +68382,7 @@ function pad(s, n) {
67794
68382
  function clip6(s, n) {
67795
68383
  return s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
67796
68384
  }
67797
- function printHelp15() {
68385
+ function printHelp16() {
67798
68386
  console.log(`keryx sessions
67799
68387
 
67800
68388
  Per-project interactive shell sessions (isolated by git root / cwd).
@@ -67857,7 +68445,7 @@ function buildInitFlags(next, profile) {
67857
68445
  async function modulesCommand(args2 = []) {
67858
68446
  const sub = args2[0];
67859
68447
  if (sub === "--help" || sub === "-h" || sub === "help") {
67860
- printHelp16();
68448
+ printHelp17();
67861
68449
  return;
67862
68450
  }
67863
68451
  const wantsJson = args2.includes("--json") && (sub === undefined || sub === "status" || sub === "list" || sub === "--json");
@@ -67914,7 +68502,7 @@ async function modulesCommand(args2 = []) {
67914
68502
  }
67915
68503
  }
67916
68504
  } else {
67917
- printHelp16();
68505
+ printHelp17();
67918
68506
  process.exitCode = 1;
67919
68507
  return;
67920
68508
  }
@@ -67957,7 +68545,7 @@ function setsEqual(a, b) {
67957
68545
  }
67958
68546
  return true;
67959
68547
  }
67960
- function printHelp16() {
68548
+ function printHelp17() {
67961
68549
  helpTitle("keryx modules", "view and toggle Metaproject modules");
67962
68550
  helpUsage([
67963
68551
  "keryx modules",
@@ -69429,7 +70017,7 @@ var ENABLE_FLAG = "--enable";
69429
70017
  var DISABLE_FLAG = "--disable";
69430
70018
  async function serveCommand(args2 = []) {
69431
70019
  if (args2.includes("--help") || args2.includes("-h") || args2[0] === "help") {
69432
- printHelp17();
70020
+ printHelp18();
69433
70021
  return;
69434
70022
  }
69435
70023
  const sub = args2[0];
@@ -69450,14 +70038,14 @@ async function serveCommand(args2 = []) {
69450
70038
  return;
69451
70039
  }
69452
70040
  console.error(`Unknown serve command: ${sanitizeForDisplay(sub)}`);
69453
- printHelp17();
70041
+ printHelp18();
69454
70042
  process.exitCode = 1;
69455
70043
  }
69456
70044
  async function runServe(args2) {
69457
70045
  const parsed = parseArgs3(args2, BIND_FLAGS, [ACK_FLAG]);
69458
70046
  if (!parsed.ok) {
69459
70047
  console.error(parsed.message);
69460
- printHelp17();
70048
+ printHelp18();
69461
70049
  process.exitCode = 1;
69462
70050
  return;
69463
70051
  }
@@ -69559,7 +70147,7 @@ function runStatus6(args2) {
69559
70147
  const parsed = parseArgs3(args2, [], ["--json"]);
69560
70148
  if (!parsed.ok) {
69561
70149
  console.error(parsed.message);
69562
- printHelp17();
70150
+ printHelp18();
69563
70151
  process.exitCode = 1;
69564
70152
  return;
69565
70153
  }
@@ -69606,7 +70194,7 @@ function runToken(args2) {
69606
70194
  const rest = parseArgs3(args2.slice(1), [], []);
69607
70195
  if (!rest.ok) {
69608
70196
  console.error(rest.message);
69609
- printHelp17();
70197
+ printHelp18();
69610
70198
  process.exitCode = 1;
69611
70199
  return;
69612
70200
  }
@@ -69647,7 +70235,7 @@ function runToken(args2) {
69647
70235
  return;
69648
70236
  }
69649
70237
  console.error(sub === undefined ? "Missing token subcommand" : `Unknown token command: ${sanitizeForDisplay(sub)}`);
69650
- printHelp17();
70238
+ printHelp18();
69651
70239
  process.exitCode = 1;
69652
70240
  }
69653
70241
  function printTokenOnce(token) {
@@ -69672,7 +70260,7 @@ function runConfig(args2) {
69672
70260
  const parsed = parseArgs3(args2.slice(1), BIND_FLAGS, [ACK_FLAG, FORCE_FLAG]);
69673
70261
  if (!parsed.ok) {
69674
70262
  console.error(parsed.message);
69675
- printHelp17();
70263
+ printHelp18();
69676
70264
  process.exitCode = 1;
69677
70265
  return;
69678
70266
  }
@@ -69722,7 +70310,7 @@ function runConfig(args2) {
69722
70310
  const parsed = parseArgs3(args2.slice(1), [], ["--json"]);
69723
70311
  if (!parsed.ok) {
69724
70312
  console.error(parsed.message);
69725
- printHelp17();
70313
+ printHelp18();
69726
70314
  process.exitCode = 1;
69727
70315
  return;
69728
70316
  }
@@ -69739,14 +70327,14 @@ function runConfig(args2) {
69739
70327
  return;
69740
70328
  }
69741
70329
  console.error(sub === undefined ? "Missing config subcommand" : `Unknown config command: ${sanitizeForDisplay(sub)}`);
69742
- printHelp17();
70330
+ printHelp18();
69743
70331
  process.exitCode = 1;
69744
70332
  }
69745
70333
  function runConfigSet(args2) {
69746
70334
  const parsed = parseArgs3(args2, BIND_FLAGS, [ACK_FLAG, NO_ACK_FLAG, ENABLE_FLAG, DISABLE_FLAG]);
69747
70335
  if (!parsed.ok) {
69748
70336
  console.error(parsed.message);
69749
- printHelp17();
70337
+ printHelp18();
69750
70338
  process.exitCode = 1;
69751
70339
  return;
69752
70340
  }
@@ -69817,7 +70405,7 @@ function printConfig(config) {
69817
70405
  console.log(` approvals: expire after ${config.approval.expirySeconds}s, max ${config.approval.maxPendingPerSession} pending per session`);
69818
70406
  console.log(` non-loopback acknowledged: ${config.bind.acknowledgeNonLoopback === true}`);
69819
70407
  }
69820
- function printHelp17() {
70408
+ function printHelp18() {
69821
70409
  helpTitle("keryx serve", "loopback-bound HTTP entry over this install (off by default)");
69822
70410
  helpUsage([
69823
70411
  `keryx serve [--bind <addr>] [--port <n>] [--profile <name>] [${ACK_FLAG}]`,
@@ -69863,7 +70451,7 @@ init_git_hooks();
69863
70451
  async function updateCommand(args2 = []) {
69864
70452
  const options = parseUpdateArgs(args2);
69865
70453
  if (options.help) {
69866
- printHelp18();
70454
+ printHelp19();
69867
70455
  return;
69868
70456
  }
69869
70457
  const projectRoot = process.cwd();
@@ -71012,7 +71600,7 @@ function runtimeSourcePath2(relativePath) {
71012
71600
  function escapeRegExp6(value) {
71013
71601
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
71014
71602
  }
71015
- function printHelp18() {
71603
+ function printHelp19() {
71016
71604
  helpTitle("keryx update", "refresh .metaproject service files (data left untouched)");
71017
71605
  helpUsage(["keryx update [--skip-runtime] [--hooks] [--no-tasks]"]);
71018
71606
  heading("Default behavior");
@@ -71040,7 +71628,7 @@ async function dashboardCommand(args2 = []) {
71040
71628
  const options = parseOptions(args2);
71041
71629
  const subcommand = options.positionals[0];
71042
71630
  if (!subcommand || options.help) {
71043
- printHelp19();
71631
+ printHelp20();
71044
71632
  return;
71045
71633
  }
71046
71634
  if (subcommand === "build") {
@@ -71058,7 +71646,7 @@ async function dashboardCommand(args2 = []) {
71058
71646
  return;
71059
71647
  }
71060
71648
  console.log(` ${style.red(symbols.cross)} Unknown dashboard command: ${subcommand}`);
71061
- printHelp19();
71649
+ printHelp20();
71062
71650
  process.exitCode = 1;
71063
71651
  }
71064
71652
  function parseOptions(args2) {
@@ -71083,7 +71671,7 @@ async function openFile(filePath) {
71083
71671
  });
71084
71672
  });
71085
71673
  }
71086
- function printHelp19() {
71674
+ function printHelp20() {
71087
71675
  helpTitle("keryx dashboard", "build and open the human dashboard");
71088
71676
  helpUsage([
71089
71677
  "keryx dashboard build",
@@ -73656,7 +74244,7 @@ function service5() {
73656
74244
  async function workspaceCommand(args2) {
73657
74245
  const subcommand = args2[0];
73658
74246
  if (!subcommand || subcommand === "help" || args2.includes("--help") || args2.includes("-h"))
73659
- return printHelp20();
74247
+ return printHelp21();
73660
74248
  try {
73661
74249
  if (subcommand === "create") {
73662
74250
  rejectUnknownOptions(args2.slice(1), new Set(["--title", "--component"]));
@@ -73906,7 +74494,7 @@ function booleanFlagDefaultTrue(args2, name) {
73906
74494
  return false;
73907
74495
  throw new Error(`Unknown value for ${name}: "${value}" \u2014 expected true or false`);
73908
74496
  }
73909
- function printHelp20() {
74497
+ function printHelp21() {
73910
74498
  console.log(`keryx workspace create --title <title> [--component <workspace-relative-ref>]
73911
74499
  keryx workspace list [--include-archived]
73912
74500
  keryx workspace show <workspace-id>
@@ -73978,6 +74566,7 @@ var CLI_ROUTES = {
73978
74566
  standard: standardCommand,
73979
74567
  commands: commandsCommand,
73980
74568
  security: securityCommand,
74569
+ sandbox: sandboxCommand,
73981
74570
  mcp: mcpCommand,
73982
74571
  harness: harnessCommand,
73983
74572
  shell: shellCommand,
@@ -73990,7 +74579,7 @@ async function main() {
73990
74579
  const args2 = process.argv.slice(2);
73991
74580
  const command = args2[0];
73992
74581
  if (command === "--help" || command === "-h" || command === "help" || !command) {
73993
- printHelp21();
74582
+ printHelp22();
73994
74583
  return;
73995
74584
  }
73996
74585
  if (command === "--version" || command === "-v") {
@@ -74003,10 +74592,10 @@ async function main() {
74003
74592
  return;
74004
74593
  }
74005
74594
  console.error(`Unknown command: ${command}`);
74006
- printHelp21();
74595
+ printHelp22();
74007
74596
  process.exitCode = 1;
74008
74597
  }
74009
- function printHelp21() {
74598
+ function printHelp22() {
74010
74599
  console.log(`keryx ${VERSION2}
74011
74600
 
74012
74601
  Usage:
@@ -74084,6 +74673,8 @@ Usage:
74084
74673
  keryx security scan <path> [--json]
74085
74674
  keryx security scan-mcp <manifest|dir> [--json]
74086
74675
  keryx security check-input [--source <kind>] [--file <path>]
74676
+ keryx sandbox status [--json]
74677
+ OS sandbox launcher availability + per-capability containment matrix (report, not a gate)
74087
74678
  keryx security check-output [--target <kind>] [--file <path>]
74088
74679
  keryx security redact <path> [--out <path>]
74089
74680
  keryx security report [--since <ref>]
@@ -74125,6 +74716,7 @@ Commands:
74125
74716
  standard Validate the workspace against the Metaproject Standard
74126
74717
  commands Agent-callable command registry (intents, args, output, model usage)
74127
74718
  security Policy-based scanning, redaction, guardrails and audit reports
74719
+ sandbox Report OS sandbox launcher availability and the per-capability containment matrix
74128
74720
  mcp Expose Metaproject services over the Model Context Protocol (opt-in)
74129
74721
  `);
74130
74722
  }