@odla-ai/cli 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -553,23 +553,23 @@ function adminAiAuditQuery(filters) {
553
553
  }
554
554
  return `?limit=${filters.limit}`;
555
555
  }
556
- async function readAdminAiAudit(request) {
557
- const response2 = await request.fetch(`${request.platform}/registry/platform/ai-audit${request.query}`, {
558
- headers: request.headers
556
+ async function readAdminAiAudit(request2) {
557
+ const response2 = await request2.fetch(`${request2.platform}/registry/platform/ai-audit${request2.query}`, {
558
+ headers: request2.headers
559
559
  });
560
560
  const body = await responseBody(response2);
561
561
  if (!response2.ok) throw new Error(apiError(response2.status, body));
562
- if (request.json) {
563
- request.stdout.log(JSON.stringify(body, null, 2));
562
+ if (request2.json) {
563
+ request2.stdout.log(JSON.stringify(body, null, 2));
564
564
  return;
565
565
  }
566
566
  const events = isRecord(body) && Array.isArray(body.events) ? body.events.filter(isRecord) : [];
567
- request.stdout.log("when change target before -> after actor");
567
+ request2.stdout.log("when change target before -> after actor");
568
568
  for (const event of events) {
569
569
  const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
570
570
  const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
571
571
  const route2 = before && after ? `${String(before.provider)}/${String(before.model)}@v${String(before.version)} -> ${String(after.provider)}/${String(after.model)}@v${String(after.version)}` : "value not retained";
572
- request.stdout.log([
572
+ request2.stdout.log([
573
573
  timestamp(event.createdAt),
574
574
  String(event.changeKind ?? ""),
575
575
  String(event.purpose ?? event.provider ?? ""),
@@ -625,14 +625,14 @@ function adminAiUsageQuery(filters) {
625
625
  const query = params.toString();
626
626
  return query ? `?${query}` : "";
627
627
  }
628
- async function readAdminAiUsage(request) {
629
- const res = await request.fetch(`${request.platform}/registry/platform/ai-usage${request.query}`, {
630
- headers: request.headers
628
+ async function readAdminAiUsage(request2) {
629
+ const res = await request2.fetch(`${request2.platform}/registry/platform/ai-usage${request2.query}`, {
630
+ headers: request2.headers
631
631
  });
632
632
  const body = await responseBody2(res);
633
633
  if (!res.ok) throw new Error(apiError2("read platform AI usage", res.status, body));
634
- if (request.json) request.stdout.log(JSON.stringify(body, null, 2));
635
- else printUsage(body, request.stdout);
634
+ if (request2.json) request2.stdout.log(JSON.stringify(body, null, 2));
635
+ else printUsage(body, request2.stdout);
636
636
  }
637
637
  function usageLimit(value) {
638
638
  if (!Number.isSafeInteger(value) || value < 1 || value > 500) {
@@ -3094,10 +3094,10 @@ function installSkill(options = {}) {
3094
3094
  current.add(target);
3095
3095
  targets.set(harness, current);
3096
3096
  };
3097
- const plan = (target, content, managedMerge = false, boundary = root) => {
3097
+ const plan = (target, content2, managedMerge = false, boundary = root) => {
3098
3098
  const existing = plans.get(target);
3099
- if (existing && existing.content !== content) throw new Error(`internal agent setup conflict for ${target}`);
3100
- plans.set(target, { target, content, boundary, managedMerge });
3099
+ if (existing && existing.content !== content2) throw new Error(`internal agent setup conflict for ${target}`);
3100
+ plans.set(target, { target, content: content2, boundary, managedMerge });
3101
3101
  };
3102
3102
  const planSkillTree = (targetDir2, boundary = root) => {
3103
3103
  for (const rel of files) plan((0, import_node_path8.join)(targetDir2, rel), (0, import_node_fs10.readFileSync)((0, import_node_path8.join)(sourceDir, rel), "utf8"), false, boundary);
@@ -3883,11 +3883,11 @@ async function gitBlobs(cwd, entries, maxBytes) {
3883
3883
  if (!Number.isSafeInteger(length) || length < 0 || newline + 1 + length >= output.length) {
3884
3884
  throw new Error("git object output has an invalid length");
3885
3885
  }
3886
- const content = output.subarray(newline + 1, newline + 1 + length);
3886
+ const content2 = output.subarray(newline + 1, newline + 1 + length);
3887
3887
  if (output[newline + 1 + length] !== 10) throw new Error("git object output is malformed");
3888
- total += content.byteLength;
3888
+ total += content2.byteLength;
3889
3889
  if (total > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
3890
- blobs.push(content);
3890
+ blobs.push(content2);
3891
3891
  offset = newline + 1 + length + 1;
3892
3892
  }
3893
3893
  return blobs;
@@ -3910,13 +3910,13 @@ async function materializeGitTree(source, commitSha, options = {}) {
3910
3910
  try {
3911
3911
  const blobs = await gitBlobs(sourceDir, entries, maxBytes);
3912
3912
  for (const [index, entry] of entries.entries()) {
3913
- const content = blobs[index];
3914
- byteCount += content.byteLength;
3913
+ const content2 = blobs[index];
3914
+ byteCount += content2.byteLength;
3915
3915
  if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
3916
3916
  const target = (0, import_path2.resolve)(targetRoot, entry.path);
3917
3917
  if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
3918
3918
  await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
3919
- await (0, import_promises3.writeFile)(target, content, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
3919
+ await (0, import_promises3.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
3920
3920
  }
3921
3921
  return {
3922
3922
  root,
@@ -4294,11 +4294,11 @@ var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
4294
4294
  var MAX_PATCH_BYTES = 256 * 1024;
4295
4295
  var MAX_STATE_BYTES = 64 * 1024;
4296
4296
  async function createCodePortableCheckpoint(input) {
4297
- const state = normalizeState(input.state);
4297
+ const state2 = normalizeState(input.state);
4298
4298
  validateBaseAndPatch(input.baseCommitSha, input.patch);
4299
4299
  const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
4300
- const stateDigest = `sha256:${await sha256Hex(canonicalJson(state))}`;
4301
- const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state, stateDigest };
4300
+ const stateDigest = `sha256:${await sha256Hex(canonicalJson(state2))}`;
4301
+ const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
4302
4302
  const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
4303
4303
  return freeze({ ...fields, patch: input.patch, checkpointDigest });
4304
4304
  }
@@ -4319,8 +4319,8 @@ async function verifyCodePortableCheckpoint(value) {
4319
4319
  return created;
4320
4320
  }
4321
4321
  function normalizeState(value) {
4322
- const state = object(value, "state");
4323
- exact2(state, [
4322
+ const state2 = object(value, "state");
4323
+ exact2(state2, [
4324
4324
  "planCursor",
4325
4325
  "conversationRefs",
4326
4326
  "planningInputDigest",
@@ -4332,13 +4332,13 @@ function normalizeState(value) {
4332
4332
  "unresolvedApprovals",
4333
4333
  "trustStatus"
4334
4334
  ]);
4335
- const planCursor = state.planCursor;
4336
- const conversations = strings(state.conversationRefs, "conversationRefs", ID2, 256, false);
4337
- const approvals = strings(state.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
4338
- if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof state.planningInputDigest !== "string" || !DIGEST2.test(state.planningInputDigest) || typeof state.buildPolicyDigest !== "string" || !DIGEST2.test(state.buildPolicyDigest) || state.dependencyLayerDigest !== null && (typeof state.dependencyLayerDigest !== "string" || !DIGEST2.test(state.dependencyLayerDigest)) || state.verificationDigest !== null && (typeof state.verificationDigest !== "string" || !DIGEST2.test(state.verificationDigest)) || state.reviewDigest !== null && (typeof state.reviewDigest !== "string" || !DIGEST2.test(state.reviewDigest)) || !["candidate_untrusted", "verified", "reviewed"].includes(String(state.trustStatus)) || !Array.isArray(state.completedEffects) || state.completedEffects.length > 256) {
4335
+ const planCursor = state2.planCursor;
4336
+ const conversations = strings(state2.conversationRefs, "conversationRefs", ID2, 256, false);
4337
+ const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
4338
+ if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof state2.planningInputDigest !== "string" || !DIGEST2.test(state2.planningInputDigest) || typeof state2.buildPolicyDigest !== "string" || !DIGEST2.test(state2.buildPolicyDigest) || state2.dependencyLayerDigest !== null && (typeof state2.dependencyLayerDigest !== "string" || !DIGEST2.test(state2.dependencyLayerDigest)) || state2.verificationDigest !== null && (typeof state2.verificationDigest !== "string" || !DIGEST2.test(state2.verificationDigest)) || state2.reviewDigest !== null && (typeof state2.reviewDigest !== "string" || !DIGEST2.test(state2.reviewDigest)) || !["candidate_untrusted", "verified", "reviewed"].includes(String(state2.trustStatus)) || !Array.isArray(state2.completedEffects) || state2.completedEffects.length > 256) {
4339
4339
  throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
4340
4340
  }
4341
- const effects = state.completedEffects.map((item) => {
4341
+ const effects = state2.completedEffects.map((item) => {
4342
4342
  const effect = object(item, "completed effect");
4343
4343
  exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
4344
4344
  if (typeof effect.effectId !== "string" || !ID2.test(effect.effectId) || typeof effect.actionDigest !== "string" || !DIGEST2.test(effect.actionDigest) || typeof effect.receiptDigest !== "string" || !DIGEST2.test(effect.receiptDigest)) {
@@ -4353,18 +4353,18 @@ function normalizeState(value) {
4353
4353
  if (new Set(effects.map((item) => item.effectId)).size !== effects.length) {
4354
4354
  throw invalid2("Portable checkpoint repeats a completed effect.");
4355
4355
  }
4356
- const trustStatus = state.trustStatus;
4357
- if (trustStatus === "candidate_untrusted" && (state.verificationDigest !== null || state.reviewDigest !== null) || trustStatus === "verified" && (state.verificationDigest === null || state.reviewDigest !== null) || trustStatus === "reviewed" && (state.verificationDigest === null || state.reviewDigest === null)) {
4356
+ const trustStatus = state2.trustStatus;
4357
+ if (trustStatus === "candidate_untrusted" && (state2.verificationDigest !== null || state2.reviewDigest !== null) || trustStatus === "verified" && (state2.verificationDigest === null || state2.reviewDigest !== null) || trustStatus === "reviewed" && (state2.verificationDigest === null || state2.reviewDigest === null)) {
4358
4358
  throw invalid2("Portable checkpoint trust status does not match its evidence digests.");
4359
4359
  }
4360
4360
  const normalized = {
4361
4361
  planCursor,
4362
4362
  conversationRefs: conversations,
4363
- planningInputDigest: state.planningInputDigest,
4364
- buildPolicyDigest: state.buildPolicyDigest,
4365
- dependencyLayerDigest: state.dependencyLayerDigest,
4366
- verificationDigest: state.verificationDigest,
4367
- reviewDigest: state.reviewDigest,
4363
+ planningInputDigest: state2.planningInputDigest,
4364
+ buildPolicyDigest: state2.buildPolicyDigest,
4365
+ dependencyLayerDigest: state2.dependencyLayerDigest,
4366
+ verificationDigest: state2.verificationDigest,
4367
+ reviewDigest: state2.reviewDigest,
4368
4368
  completedEffects: effects,
4369
4369
  unresolvedApprovals: approvals,
4370
4370
  trustStatus
@@ -4481,60 +4481,60 @@ async function createConversionRegistry(config) {
4481
4481
  policies.set(policy.conversionId, Object.freeze(policy));
4482
4482
  }
4483
4483
  const outputCounts = /* @__PURE__ */ new Map();
4484
- const get = (id, kind) => {
4484
+ const get2 = (id, kind) => {
4485
4485
  const policy = policies.get(id);
4486
4486
  if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
4487
4487
  return policy;
4488
4488
  };
4489
4489
  const checked = (source, id, kind) => {
4490
- const policy = get(id, kind);
4490
+ const policy = get2(id, kind);
4491
4491
  if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
4492
4492
  return policy;
4493
4493
  };
4494
- const emit2 = (source, policy, value) => convert(source, policy, value, outputCounts);
4494
+ const emit3 = (source, policy, value) => convert(source, policy, value, outputCounts);
4495
4495
  const operations = Object.freeze({
4496
4496
  boolean: async (value, id) => {
4497
4497
  const policy = checked(value, id, "boolean");
4498
- return emit2(value, policy, requireBoolean(value.value));
4498
+ return emit3(value, policy, requireBoolean(value.value));
4499
4499
  },
4500
4500
  integer: async (value, id) => {
4501
4501
  const policy = checked(value, id, "integer");
4502
- return emit2(value, policy, boundedInteger(value.value, policy.output));
4502
+ return emit3(value, policy, boundedInteger(value.value, policy.output));
4503
4503
  },
4504
4504
  finiteNumber: async (value, id) => {
4505
4505
  const policy = checked(value, id, "finite_number");
4506
- return emit2(value, policy, boundedNumber(value.value, policy.output));
4506
+ return emit3(value, policy, boundedNumber(value.value, policy.output));
4507
4507
  },
4508
4508
  enum: async (value, id) => {
4509
4509
  const policy = checked(value, id, "enum");
4510
- return emit2(value, policy, enumMember(value.value, policy.output));
4510
+ return emit3(value, policy, enumMember(value.value, policy.output));
4511
4511
  },
4512
4512
  date: async (value, id) => {
4513
4513
  const policy = checked(value, id, "date");
4514
- return emit2(value, policy, canonicalDate(value.value, policy.output));
4514
+ return emit3(value, policy, canonicalDate(value.value, policy.output));
4515
4515
  },
4516
4516
  registeredId: async (value, id) => {
4517
4517
  const policy = checked(value, id, "registered_id");
4518
4518
  const registry = config.registeredIds?.[policy.output.registryId];
4519
4519
  const output = typeof value.value === "string" ? registry?.values[value.value] : void 0;
4520
4520
  if (!output) throw new CamelError("conversion_rejected", "Registered-ID conversion rejected the candidate.");
4521
- return emit2(value, policy, output);
4521
+ return emit3(value, policy, output);
4522
4522
  },
4523
4523
  digest: async (value, id) => {
4524
4524
  const policy = checked(value, id, "digest");
4525
4525
  if (!(value.value instanceof Uint8Array)) throw new CamelError("conversion_rejected", "Digest conversion requires bytes.");
4526
- return emit2(value, policy, await sha256Hex(value.value));
4526
+ return emit3(value, policy, await sha256Hex(value.value));
4527
4527
  },
4528
4528
  measure: async (value, metric, id) => {
4529
4529
  const policy = checked(value, id, "integer");
4530
4530
  const measured = measure(value.value, metric);
4531
- return emit2(value, policy, boundedInteger(measured, policy.output));
4531
+ return emit3(value, policy, boundedInteger(measured, policy.output));
4532
4532
  },
4533
4533
  test: async (value, predicateId, id) => {
4534
4534
  const policy = checked(value, id, "boolean");
4535
4535
  const predicate = config.predicates?.[predicateId];
4536
4536
  if (!predicate) throw new CamelError("conversion_rejected", "Predicate is not registered.");
4537
- return emit2(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
4537
+ return emit3(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
4538
4538
  }
4539
4539
  });
4540
4540
  return Object.freeze({ operations, policy: (id) => policies.get(id) ?? missingPolicy() });
@@ -4753,11 +4753,11 @@ async function digestStagedWorkspace(root, limits) {
4753
4753
  const hash = (0, import_crypto.createHash)("sha256");
4754
4754
  let bytes = 0;
4755
4755
  for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
4756
- const content = await (0, import_promises5.readFile)(file.target);
4757
- bytes += Buffer.byteLength(file.path) + content.byteLength;
4756
+ const content2 = await (0, import_promises5.readFile)(file.target);
4757
+ bytes += Buffer.byteLength(file.path) + content2.byteLength;
4758
4758
  if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
4759
- hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content.byteLength}:`);
4760
- hash.update(content);
4759
+ hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
4760
+ hash.update(content2);
4761
4761
  }
4762
4762
  return `sha256:${hash.digest("hex")}`;
4763
4763
  }
@@ -4852,13 +4852,13 @@ function createCodeRuntimeControlClient(options) {
4852
4852
  if (!Number.isSafeInteger(modelRequestTimeoutMs) || modelRequestTimeoutMs < 3e4 || modelRequestTimeoutMs > 30 * 6e4) {
4853
4853
  throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
4854
4854
  }
4855
- const request = options.fetch ?? fetch;
4855
+ const request2 = options.fetch ?? fetch;
4856
4856
  const call2 = async (path, body, timeoutMs = requestTimeoutMs) => {
4857
4857
  const timeout = AbortSignal.timeout(timeoutMs);
4858
4858
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
4859
4859
  let response2;
4860
4860
  try {
4861
- response2 = await request(`${endpoint}${path}`, {
4861
+ response2 = await request2(`${endpoint}${path}`, {
4862
4862
  method: "POST",
4863
4863
  headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
4864
4864
  body: JSON.stringify(body),
@@ -5765,34 +5765,34 @@ function createCodeToolBroker(options) {
5765
5765
  const policy = createCodePolicyGate(options);
5766
5766
  let tail = Promise.resolve();
5767
5767
  return {
5768
- execute(context, request) {
5769
- const result = tail.then(() => route(context, request, options, recipes, policy));
5768
+ execute(context, request2) {
5769
+ const result = tail.then(() => route(context, request2, options, recipes, policy));
5770
5770
  tail = result.then(() => void 0, () => void 0);
5771
5771
  return result;
5772
5772
  }
5773
5773
  };
5774
5774
  }
5775
- async function route(context, request, options, recipes, policy) {
5775
+ async function route(context, request2, options, recipes, policy) {
5776
5776
  try {
5777
5777
  if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
5778
- if (request.tool === "sandbox.read") return await read(context, request, options, policy);
5779
- if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
5780
- return await recipe(context, request, options, recipes, policy);
5778
+ if (request2.tool === "sandbox.read") return await read(context, request2, options, policy);
5779
+ if (request2.tool === "sandbox.apply_patch") return await patch(context, request2, options, policy);
5780
+ return await recipe(context, request2, options, recipes, policy);
5781
5781
  } catch (reason) {
5782
- return response(request, false, reason instanceof TypeError ? reason.message : "tool failed closed");
5782
+ return response(request2, false, reason instanceof TypeError ? reason.message : "tool failed closed");
5783
5783
  }
5784
5784
  }
5785
- async function read(context, request, options, policy) {
5786
- exactKeys(request.input, ["path", "startLine", "endLine"]);
5787
- const path = stringField(request.input, "path");
5788
- const startLine = optionalInteger(request.input.startLine) ?? 1;
5789
- const endLine = optionalInteger(request.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
5785
+ async function read(context, request2, options, policy) {
5786
+ exactKeys(request2.input, ["path", "startLine", "endLine"]);
5787
+ const path = stringField(request2.input, "path");
5788
+ const startLine = optionalInteger(request2.input.startLine) ?? 1;
5789
+ const endLine = optionalInteger(request2.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
5790
5790
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
5791
5791
  throw new TypeError("requested line range exceeds its bound");
5792
5792
  }
5793
5793
  const paths = await registeredFiles(context.workspaceDir, 2e4);
5794
- const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
5795
- if (!allowed) return response(request, false, "tool denied by CaMeL policy");
5794
+ const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
5795
+ if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
5796
5796
  const target = resolveCodePath(context.workspaceDir, path);
5797
5797
  const info = await (0, import_promises9.stat)(target);
5798
5798
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
@@ -5801,40 +5801,40 @@ async function read(context, request, options, policy) {
5801
5801
  const source = await (0, import_promises9.readFile)(target);
5802
5802
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
5803
5803
  const lines = source.toString("utf8").split("\n");
5804
- const content = lines.slice(startLine - 1, endLine).join("\n");
5805
- if (Buffer.byteLength(content) > (options.maxReadBytes ?? 128 * 1024)) {
5804
+ const content2 = lines.slice(startLine - 1, endLine).join("\n");
5805
+ if (Buffer.byteLength(content2) > (options.maxReadBytes ?? 128 * 1024)) {
5806
5806
  throw new TypeError("read result exceeds its byte bound");
5807
5807
  }
5808
- return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
5808
+ return response(request2, true, content2, { path, startLine, endLine: Math.min(endLine, lines.length) });
5809
5809
  }
5810
- async function patch(context, request, options, policy) {
5811
- exactKeys(request.input, ["patch"]);
5812
- const value = stringField(request.input, "patch");
5810
+ async function patch(context, request2, options, policy) {
5811
+ exactKeys(request2.input, ["patch"]);
5812
+ const value = stringField(request2.input, "patch");
5813
5813
  const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
5814
5814
  if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
5815
5815
  throw new TypeError("patch targets a read-only reference source");
5816
5816
  }
5817
- const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
5818
- if (!allowed) return response(request, false, "tool denied by CaMeL policy");
5817
+ const allowed = await policy.patch(policyContext(context, request2, options, { patch: value }));
5818
+ if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
5819
5819
  await applyCodePatch(context.workspaceDir, value, paths);
5820
- return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
5820
+ return response(request2, true, `Applied patch to ${paths.length} file(s).`, { paths });
5821
5821
  }
5822
- async function recipe(context, request, options, recipes, policy) {
5823
- exactKeys(request.input, ["recipeId"]);
5824
- const recipeId = stringField(request.input, "recipeId");
5822
+ async function recipe(context, request2, options, recipes, policy) {
5823
+ exactKeys(request2.input, ["recipeId"]);
5824
+ const recipeId = stringField(request2.input, "recipeId");
5825
5825
  const digestLimits = {
5826
5826
  maxFiles: options.maxRecipeWorkspaceFiles ?? 2e4,
5827
5827
  maxBytes: options.maxRecipeWorkspaceBytes ?? 512 * 1024 * 1024
5828
5828
  };
5829
5829
  const sourceDigest = await digestStagedWorkspace(context.workspaceDir, digestLimits);
5830
- const allowed = await policy.recipe(policyContext(context, request, options, {
5830
+ const allowed = await policy.recipe(policyContext(context, request2, options, {
5831
5831
  recipeIds: [...recipes.keys()].sort(),
5832
5832
  recipeId,
5833
5833
  sourceDigest
5834
5834
  }));
5835
- if (!allowed) return response(request, false, "tool denied by CaMeL policy");
5835
+ if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
5836
5836
  const selected = recipes.get(recipeId);
5837
- if (!selected) return response(request, false, "build recipe is not registered");
5837
+ if (!selected) return response(request2, false, "build recipe is not registered");
5838
5838
  const staged = await stageWorkspace(context.workspaceDir, {
5839
5839
  maxFiles: digestLimits.maxFiles,
5840
5840
  maxBytes: digestLimits.maxBytes
@@ -5851,7 +5851,7 @@ async function recipe(context, request, options, recipes, policy) {
5851
5851
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
5852
5852
  const ok = result.exitCode === 0 && !result.outputLimitExceeded && !result.timedOut;
5853
5853
  const status = result.timedOut ? "timed out" : result.outputLimitExceeded ? "exceeded output limit" : ok ? "passed" : `failed with exit ${result.exitCode}`;
5854
- return response(request, ok, `Recipe ${recipeId} ${status}.${output ? `
5854
+ return response(request2, ok, `Recipe ${recipeId} ${status}.${output ? `
5855
5855
  ${output}` : ""}`, {
5856
5856
  recipeId,
5857
5857
  exitCode: result.exitCode,
@@ -5863,10 +5863,10 @@ ${output}` : ""}`, {
5863
5863
  await staged.cleanup();
5864
5864
  }
5865
5865
  }
5866
- function policyContext(context, request, options, extra) {
5866
+ function policyContext(context, request2, options, extra) {
5867
5867
  return {
5868
5868
  lease: context.lease,
5869
- request,
5869
+ request: request2,
5870
5870
  workspaceId: `workspace:${context.lease.task.attemptId}`,
5871
5871
  readers: { kind: "principals", principalIds: [options.readerId] },
5872
5872
  ...extra
@@ -5916,8 +5916,8 @@ function optionalInteger(value) {
5916
5916
  if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
5917
5917
  return value;
5918
5918
  }
5919
- function response(request, ok, content, details) {
5920
- return { requestId: request.requestId, ok, content, ...details ? { details } : {} };
5919
+ function response(request2, ok, content2, details) {
5920
+ return { requestId: request2.requestId, ok, content: content2, ...details ? { details } : {} };
5921
5921
  }
5922
5922
  function codeCommandMetadata(payload, resume) {
5923
5923
  const trusted = record22(payload.trustedBase);
@@ -6017,13 +6017,13 @@ function createCodeRuntimeToolBroker(input, lease, role) {
6017
6017
  readerId: `code-session:${lease.task.taskId}`,
6018
6018
  readOnlyPrefixes: [".odla-references"]
6019
6019
  });
6020
- return role === "coding" ? broker : { execute: (context, request) => request.tool === "sandbox.read" ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
6020
+ return role === "coding" ? broker : { execute: (context, request2) => request2.tool === "sandbox.read" ? broker.execute(context, request2) : Promise.resolve({ requestId: request2.requestId, ok: false, content: "review sessions are read-only" }) };
6021
6021
  }
6022
6022
  async function handleCodeRuntimeInference(input) {
6023
- const { command, metadata: metadata2, request, state } = input;
6024
- if (state.tokens >= metadata2.maxTokensPerInteraction) {
6025
- if (!state.noticeEmitted) {
6026
- state.noticeEmitted = true;
6023
+ const { command, metadata: metadata2, request: request2, state: state2 } = input;
6024
+ if (state2.tokens >= metadata2.maxTokensPerInteraction) {
6025
+ if (!state2.noticeEmitted) {
6026
+ state2.noticeEmitted = true;
6027
6027
  await input.event({
6028
6028
  type: "message",
6029
6029
  actor: "system",
@@ -6033,7 +6033,7 @@ async function handleCodeRuntimeInference(input) {
6033
6033
  return {
6034
6034
  protocolVersion: HARNESS_PROTOCOL_VERSION,
6035
6035
  type: "inference.response",
6036
- requestId: request.requestId,
6036
+ requestId: request2.requestId,
6037
6037
  response: {
6038
6038
  id: `budget:${command.commandId}`,
6039
6039
  provider: "openai",
@@ -6047,11 +6047,11 @@ async function handleCodeRuntimeInference(input) {
6047
6047
  }
6048
6048
  const startedAt = Date.now();
6049
6049
  const response2 = await input.control.infer(command.sessionId, {
6050
- requestId: request.requestId,
6050
+ requestId: request2.requestId,
6051
6051
  interactionId: command.commandId,
6052
- call: request.call
6052
+ call: request2.call
6053
6053
  });
6054
- state.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
6054
+ state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
6055
6055
  await input.event({
6056
6056
  type: "usage",
6057
6057
  provider: response2.receipt.provider,
@@ -6060,13 +6060,13 @@ async function handleCodeRuntimeInference(input) {
6060
6060
  outputTokens: response2.receipt.outputTokens,
6061
6061
  durationMs: Date.now() - startedAt,
6062
6062
  interactionId: command.commandId,
6063
- interactionTokens: state.tokens,
6063
+ interactionTokens: state2.tokens,
6064
6064
  interactionMaxTokens: metadata2.maxTokensPerInteraction
6065
6065
  }).catch(() => void 0);
6066
6066
  return {
6067
6067
  protocolVersion: HARNESS_PROTOCOL_VERSION,
6068
6068
  type: "inference.response",
6069
- requestId: request.requestId,
6069
+ requestId: request2.requestId,
6070
6070
  response: response2.response
6071
6071
  };
6072
6072
  }
@@ -6434,6 +6434,14 @@ Usage:
6434
6434
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
6435
6435
  odla-ai pm <goal|task|decision|bug> rm <id>
6436
6436
  odla-ai pm handoff --app <id> [--json]
6437
+ odla-ai discuss groups [--json]
6438
+ odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
6439
+ odla-ai discuss read <topic> [--json]
6440
+ odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"]
6441
+ odla-ai discuss reply <topic> --body "..." [--markup "..."]
6442
+ odla-ai discuss resolve <topic> [--reopen]
6443
+ odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
6444
+ odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
6437
6445
  odla-ai whoami [--json]
6438
6446
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6439
6447
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -6538,6 +6546,13 @@ Commands:
6538
6546
  "app". Entities: goal (alias conformance), task (alias kanban),
6539
6547
  decision, bug. Status changes and comments post to each item's
6540
6548
  @odla-ai/chat discussion thread.
6549
+ discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
6550
+ one group per project, topics with replies, @-mentions of people,
6551
+ agents, PM items, and projects. Built for unattended use \u2014 post a
6552
+ question, then "watch" it until someone answers. "watch" exits 75
6553
+ (not 1) when it times out with nothing new, so a script can tell
6554
+ "no answer yet" from a failure and just rerun. Use "who" to look
6555
+ up the exact @[Label](kind/id) markup for a mention.
6541
6556
  provision Register services, compose integrations, persist credentials, optionally push secrets.
6542
6557
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6543
6558
  skill Same installer; --agent accepts all, claude, codex, cursor,
@@ -6725,15 +6740,15 @@ async function connectGitHubSecuritySource(options) {
6725
6740
  const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
6726
6741
  const wait2 = options.wait ?? waitForHostedPoll;
6727
6742
  while (true) {
6728
- const state = await requestHostedSecurityJson(
6743
+ const state2 = await requestHostedSecurityJson(
6729
6744
  options,
6730
6745
  `/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
6731
6746
  {},
6732
6747
  "check GitHub connection"
6733
6748
  );
6734
- if (state.status !== "pending") {
6735
- if (state.status === "connected") return state;
6736
- throw new Error(`GitHub connection ${state.status}${state.failureCode ? `: ${state.failureCode}` : ""}`);
6749
+ if (state2.status !== "pending") {
6750
+ if (state2.status === "connected") return state2;
6751
+ throw new Error(`GitHub connection ${state2.status}${state2.failureCode ? `: ${state2.failureCode}` : ""}`);
6737
6752
  }
6738
6753
  if (now() >= deadline) throw new Error("GitHub connection approval timed out");
6739
6754
  await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
@@ -7184,6 +7199,289 @@ async function codeCommand(parsed, dependencies) {
7184
7199
  });
7185
7200
  }
7186
7201
 
7202
+ // src/discuss-actions.ts
7203
+ async function request(ctx, method, path, body) {
7204
+ const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7205
+ method,
7206
+ headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
7207
+ body: body === void 0 ? void 0 : JSON.stringify(body)
7208
+ });
7209
+ const data = await res.json().catch(() => ({}));
7210
+ if (!res.ok)
7211
+ throw new Error(`discuss ${method} ${path} failed: ${data.error ?? `registry returned ${res.status}`}`);
7212
+ return data;
7213
+ }
7214
+ function emit(ctx, value, human) {
7215
+ if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
7216
+ else human();
7217
+ }
7218
+ var state = (topic) => topic.resolvedAt ? "resolved" : "open";
7219
+ function bodyWithRefs(post) {
7220
+ if (!post.refs || post.refs.length === 0) return post.body;
7221
+ let out = "";
7222
+ let cursor = 0;
7223
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
7224
+ if (ref.start < cursor || ref.end > post.body.length) continue;
7225
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
7226
+ cursor = ref.end;
7227
+ }
7228
+ return out + post.body.slice(cursor);
7229
+ }
7230
+ function content(parsed) {
7231
+ const markup = stringOpt(parsed.options.markup);
7232
+ if (markup) return { markup };
7233
+ const body = stringOpt(parsed.options.body);
7234
+ if (!body) throw new Error('needs --body "\u2026" (or --markup with @[Label](kind/id) mentions)');
7235
+ return { body };
7236
+ }
7237
+ async function discussGroups(ctx) {
7238
+ const result = await request(
7239
+ ctx,
7240
+ "GET",
7241
+ "/groups"
7242
+ );
7243
+ emit(ctx, result, () => {
7244
+ ctx.out.log("app name started");
7245
+ for (const group of result.groups) {
7246
+ ctx.out.log(`${group.appId} ${group.name} ${group.channelId ? "yes" : "no"}`);
7247
+ }
7248
+ });
7249
+ }
7250
+ async function discussList(ctx, parsed) {
7251
+ const query = new URLSearchParams();
7252
+ for (const [flag, param] of [
7253
+ ["app", "app"],
7254
+ ["q", "q"],
7255
+ ["state", "state"],
7256
+ ["limit", "limit"],
7257
+ ["offset", "offset"]
7258
+ ]) {
7259
+ const value = stringOpt(parsed.options[flag]);
7260
+ if (value) query.set(param, value);
7261
+ }
7262
+ const qs = query.toString();
7263
+ const page = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
7264
+ emit(ctx, page, () => {
7265
+ ctx.out.log(`topics \u2014 ${page.topics.length} of ${page.total}`);
7266
+ ctx.out.log("id state app replies subject");
7267
+ for (const topic of page.topics) {
7268
+ ctx.out.log(
7269
+ `${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
7270
+ );
7271
+ }
7272
+ });
7273
+ }
7274
+ async function discussRead(ctx, id) {
7275
+ const result = await request(
7276
+ ctx,
7277
+ "GET",
7278
+ `/topics/${encodeURIComponent(id)}`
7279
+ );
7280
+ emit(ctx, result, () => {
7281
+ ctx.out.log(`${result.topic.subject} [${state(result.topic)}] ${result.topic.appId ?? ""}`);
7282
+ for (const post of result.posts) {
7283
+ const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7284
+ ctx.out.log(`
7285
+ \u2014 ${who}`);
7286
+ ctx.out.log(bodyWithRefs(post));
7287
+ for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
7288
+ }
7289
+ });
7290
+ }
7291
+ async function discussPost(ctx, parsed) {
7292
+ const appId = stringOpt(parsed.options.app);
7293
+ if (!appId) throw new Error("discuss post needs --app <appId>");
7294
+ const subject = stringOpt(parsed.options.subject);
7295
+ if (!subject) throw new Error('discuss post needs --subject "\u2026"');
7296
+ const created = await request(ctx, "POST", "/topics", {
7297
+ appId,
7298
+ subject,
7299
+ ...content(parsed)
7300
+ });
7301
+ emit(ctx, created, () => ctx.out.log(`opened topic ${created.id}`));
7302
+ }
7303
+ async function discussReply(ctx, id, parsed) {
7304
+ const created = await request(
7305
+ ctx,
7306
+ "POST",
7307
+ `/topics/${encodeURIComponent(id)}/replies`,
7308
+ content(parsed)
7309
+ );
7310
+ emit(ctx, created, () => ctx.out.log(`replied ${created.id}`));
7311
+ }
7312
+ async function discussResolve(ctx, id, resolved) {
7313
+ const result = await request(
7314
+ ctx,
7315
+ "PATCH",
7316
+ `/topics/${encodeURIComponent(id)}`,
7317
+ { resolved }
7318
+ );
7319
+ emit(ctx, result, () => ctx.out.log(`${resolved ? "resolved" : "reopened"} ${result.id}`));
7320
+ }
7321
+ async function discussWho(ctx, parsed) {
7322
+ const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
7323
+ const app = stringOpt(parsed.options.app);
7324
+ if (app) query.set("app", app);
7325
+ const kinds = stringOpt(parsed.options.kinds);
7326
+ if (kinds) query.set("kinds", kinds);
7327
+ const found = await request(ctx, "GET", `/mentionables?${query.toString()}`);
7328
+ emit(ctx, found, () => {
7329
+ ctx.out.log("mention kind hint");
7330
+ for (const item of found.items) {
7331
+ ctx.out.log(`@[${item.label}](${item.kind}/${item.id}) ${item.kind} ${item.hint ?? ""}`);
7332
+ }
7333
+ if (found.failed.length > 0) ctx.out.error(`(no results from: ${found.failed.join(", ")})`);
7334
+ });
7335
+ }
7336
+
7337
+ // src/discuss-watch.ts
7338
+ var DEFAULT_INTERVAL_MS = 15e3;
7339
+ var DEFAULT_TIMEOUT_MS = 15 * 6e4;
7340
+ function numberOpt2(parsed, flag, fallback) {
7341
+ const raw = stringOpt(parsed.options[flag]);
7342
+ const value = raw == null ? NaN : Number(raw);
7343
+ return Number.isFinite(value) && value > 0 ? value : fallback;
7344
+ }
7345
+ async function get(ctx, path) {
7346
+ const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7347
+ headers: { authorization: `Bearer ${ctx.token}` }
7348
+ });
7349
+ const data = await res.json().catch(() => ({}));
7350
+ if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
7351
+ return data;
7352
+ }
7353
+ async function discussWatch(ctx, topicId, parsed) {
7354
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
7355
+ const now = ctx.now ?? Date.now;
7356
+ const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
7357
+ const timeoutMs = numberOpt2(parsed, "timeout", DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
7358
+ const by = stringOpt(parsed.options.by);
7359
+ const self = stringOpt(parsed.options.self);
7360
+ const deadline = now() + timeoutMs;
7361
+ const fresh = (post) => (!by || post.authorId === by) && (!self || post.authorId !== self);
7362
+ let seen = /* @__PURE__ */ new Set();
7363
+ let since = now();
7364
+ if (topicId) {
7365
+ const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
7366
+ seen = new Set(first.posts.map((post) => post.id));
7367
+ }
7368
+ for (; ; ) {
7369
+ if (topicId) {
7370
+ const { posts } = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
7371
+ const added = posts.filter((post) => !seen.has(post.id) && fresh(post));
7372
+ if (added.length > 0) return report2(ctx, { found: true, posts: added });
7373
+ for (const post of posts) seen.add(post.id);
7374
+ } else {
7375
+ const app = stringOpt(parsed.options.app);
7376
+ const { topics } = await get(
7377
+ ctx,
7378
+ `/topics?state=all${app ? `&app=${encodeURIComponent(app)}` : ""}`
7379
+ );
7380
+ const active = topics.filter((topic) => topic.lastActivityAt > since);
7381
+ if (active.length > 0) return report2(ctx, { found: true, topics: active });
7382
+ since = now();
7383
+ }
7384
+ if (now() >= deadline) return report2(ctx, { found: false });
7385
+ await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
7386
+ }
7387
+ }
7388
+ function report2(ctx, result) {
7389
+ if (ctx.json) {
7390
+ ctx.out.log(JSON.stringify(result, null, 2));
7391
+ } else if (result.found) {
7392
+ for (const post of result.posts ?? []) {
7393
+ const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7394
+ ctx.out.log(`\u2014 ${who}
7395
+ ${post.body}`);
7396
+ }
7397
+ for (const topic of result.topics ?? []) {
7398
+ ctx.out.log(`${topic.id} ${topic.appId ?? ""} ${topic.subject}`);
7399
+ }
7400
+ }
7401
+ return result;
7402
+ }
7403
+ var WatchTimeoutError = class extends Error {
7404
+ code = "watch_timeout";
7405
+ constructor() {
7406
+ super("nothing new before the timeout");
7407
+ this.name = "WatchTimeoutError";
7408
+ }
7409
+ };
7410
+
7411
+ // src/discuss-command.ts
7412
+ var ALLOWED = [
7413
+ "config",
7414
+ "token",
7415
+ "email",
7416
+ "json",
7417
+ "app",
7418
+ "subject",
7419
+ "body",
7420
+ "markup",
7421
+ "state",
7422
+ "q",
7423
+ "kinds",
7424
+ "limit",
7425
+ "offset",
7426
+ "reopen",
7427
+ "by",
7428
+ "self",
7429
+ "interval",
7430
+ "timeout"
7431
+ ];
7432
+ function requireId(id, action) {
7433
+ if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
7434
+ return id;
7435
+ }
7436
+ async function buildContext(parsed, deps) {
7437
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
7438
+ const doFetch = deps.fetch ?? fetch;
7439
+ const out = deps.stdout ?? console;
7440
+ const token = await getDeveloperToken(
7441
+ cfg,
7442
+ {
7443
+ configPath: cfg.configPath,
7444
+ token: stringOpt(parsed.options.token),
7445
+ email: stringOpt(parsed.options.email),
7446
+ open: false
7447
+ },
7448
+ doFetch,
7449
+ out
7450
+ );
7451
+ return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
7452
+ }
7453
+ async function discussCommand(parsed, deps = {}) {
7454
+ assertArgs(parsed, ALLOWED, 3);
7455
+ const action = parsed.positionals[1];
7456
+ const id = parsed.positionals[2];
7457
+ if (!action) throw new Error('"discuss" needs an action. Run "odla-ai help".');
7458
+ const ctx = await buildContext(parsed, deps);
7459
+ switch (action) {
7460
+ case "groups":
7461
+ return discussGroups(ctx);
7462
+ case "list":
7463
+ case "topics":
7464
+ return discussList(ctx, parsed);
7465
+ case "read":
7466
+ return discussRead(ctx, requireId(id, "read"));
7467
+ case "post":
7468
+ return discussPost(ctx, parsed);
7469
+ case "reply":
7470
+ return discussReply(ctx, requireId(id, "reply"), parsed);
7471
+ case "resolve":
7472
+ return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true);
7473
+ case "who":
7474
+ return discussWho(ctx, parsed);
7475
+ case "watch": {
7476
+ const result = await discussWatch(ctx, id, parsed);
7477
+ if (!result.found) throw new WatchTimeoutError();
7478
+ return;
7479
+ }
7480
+ default:
7481
+ throw new Error(`unknown discuss action "${action}". Run "odla-ai help".`);
7482
+ }
7483
+ }
7484
+
7187
7485
  // src/pm-actions.ts
7188
7486
  var FIELD_MAP = {
7189
7487
  title: { key: "title" },
@@ -7247,7 +7545,7 @@ function statusCol(entity, r) {
7247
7545
  function printRecord(ctx, entity, r) {
7248
7546
  ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
7249
7547
  }
7250
- function emit(ctx, value, human) {
7548
+ function emit2(ctx, value, human) {
7251
7549
  if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
7252
7550
  else human();
7253
7551
  }
@@ -7273,7 +7571,7 @@ async function pmList(ctx, entity, parsed) {
7273
7571
  }
7274
7572
  const qs = q.toString();
7275
7573
  const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
7276
- emit(ctx, page, () => {
7574
+ emit2(ctx, page, () => {
7277
7575
  ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
7278
7576
  ctx.out.log("id state app title");
7279
7577
  for (const r of page.records) printRecord(ctx, entity, r);
@@ -7287,25 +7585,25 @@ async function pmAdd(ctx, entity, parsed) {
7287
7585
  if (entity === "bug" && !input.description)
7288
7586
  throw new Error("pm bug add needs --description <context> (or --body <context>)");
7289
7587
  const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
7290
- emit(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7588
+ emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7291
7589
  }
7292
7590
  async function pmGet(ctx, entity, id) {
7293
7591
  const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
7294
- emit(ctx, record5, () => printRecord(ctx, entity, record5));
7592
+ emit2(ctx, record5, () => printRecord(ctx, entity, record5));
7295
7593
  }
7296
7594
  async function pmSet(ctx, entity, id, parsed) {
7297
7595
  const patch2 = collectEntityFields(entity, parsed, true);
7298
7596
  if (Object.keys(patch2).length === 0)
7299
7597
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
7300
7598
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7301
- emit(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
7599
+ emit2(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
7302
7600
  }
7303
7601
  async function pmDone(ctx, entity, id, parsed) {
7304
7602
  const decisionId = stringOpt(parsed.options.decision);
7305
7603
  if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
7306
7604
  const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
7307
7605
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7308
- emit(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7606
+ emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7309
7607
  }
7310
7608
  async function allRecords(ctx, entity, appId) {
7311
7609
  const records = [];
@@ -7338,7 +7636,7 @@ async function pmHandoff(ctx, parsed) {
7338
7636
  ...handoff,
7339
7637
  clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
7340
7638
  };
7341
- emit(ctx, result, () => {
7639
+ emit2(ctx, result, () => {
7342
7640
  if (result.clean) {
7343
7641
  ctx.out.log(`${appId}: no unresolved PM work`);
7344
7642
  return;
@@ -7371,7 +7669,7 @@ async function pmComments(ctx, entity, id) {
7371
7669
  "GET",
7372
7670
  `/${entity}/${encodeURIComponent(id)}/comments`
7373
7671
  );
7374
- emit(ctx, messages, () => {
7672
+ emit2(ctx, messages, () => {
7375
7673
  if (messages.length === 0) ctx.out.log("(no comments)");
7376
7674
  else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
7377
7675
  });
@@ -7390,7 +7688,7 @@ var ALIASES = {
7390
7688
  decision: "decision",
7391
7689
  bug: "bug"
7392
7690
  };
7393
- var ALLOWED = [
7691
+ var ALLOWED2 = [
7394
7692
  "config",
7395
7693
  "token",
7396
7694
  "email",
@@ -7414,11 +7712,11 @@ var ALLOWED = [
7414
7712
  "offset",
7415
7713
  "q"
7416
7714
  ];
7417
- function requireId(id, action) {
7715
+ function requireId2(id, action) {
7418
7716
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
7419
7717
  return id;
7420
7718
  }
7421
- async function buildContext(parsed, deps) {
7719
+ async function buildContext2(parsed, deps) {
7422
7720
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
7423
7721
  const doFetch = deps.fetch ?? fetch;
7424
7722
  const out = deps.stdout ?? console;
@@ -7433,14 +7731,14 @@ async function buildContext(parsed, deps) {
7433
7731
  async function pmCommand(parsed, deps = {}) {
7434
7732
  const word = parsed.positionals[1] ?? "";
7435
7733
  if (word === "handoff") {
7436
- assertArgs(parsed, ALLOWED, 2);
7437
- return pmHandoff(await buildContext(parsed, deps), parsed);
7734
+ assertArgs(parsed, ALLOWED2, 2);
7735
+ return pmHandoff(await buildContext2(parsed, deps), parsed);
7438
7736
  }
7439
7737
  const entity = ALIASES[word];
7440
7738
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
7441
7739
  const action = parsed.positionals[2] ?? "list";
7442
- assertArgs(parsed, ALLOWED, 4);
7443
- const ctx = await buildContext(parsed, deps);
7740
+ assertArgs(parsed, ALLOWED2, 4);
7741
+ const ctx = await buildContext2(parsed, deps);
7444
7742
  const id = parsed.positionals[3];
7445
7743
  switch (action) {
7446
7744
  case "list":
@@ -7449,21 +7747,21 @@ async function pmCommand(parsed, deps = {}) {
7449
7747
  case "create":
7450
7748
  return pmAdd(ctx, entity, parsed);
7451
7749
  case "get":
7452
- return pmGet(ctx, entity, requireId(id, action));
7750
+ return pmGet(ctx, entity, requireId2(id, action));
7453
7751
  case "set":
7454
7752
  case "update":
7455
7753
  case "status":
7456
7754
  case "move":
7457
- return pmSet(ctx, entity, requireId(id, action), parsed);
7755
+ return pmSet(ctx, entity, requireId2(id, action), parsed);
7458
7756
  case "done":
7459
- return pmDone(ctx, entity, requireId(id, action), parsed);
7757
+ return pmDone(ctx, entity, requireId2(id, action), parsed);
7460
7758
  case "comment":
7461
- return pmComment(ctx, entity, requireId(id, action), parsed);
7759
+ return pmComment(ctx, entity, requireId2(id, action), parsed);
7462
7760
  case "comments":
7463
- return pmComments(ctx, entity, requireId(id, action));
7761
+ return pmComments(ctx, entity, requireId2(id, action));
7464
7762
  case "rm":
7465
7763
  case "delete":
7466
- return pmRemove(ctx, entity, requireId(id, action));
7764
+ return pmRemove(ctx, entity, requireId2(id, action));
7467
7765
  default:
7468
7766
  throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
7469
7767
  }
@@ -7929,6 +8227,18 @@ var COMMAND_SURFACE = {
7929
8227
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
7930
8228
  capabilities: {},
7931
8229
  code: { connect: {} },
8230
+ // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
8231
+ discuss: {
8232
+ groups: {},
8233
+ list: {},
8234
+ topics: {},
8235
+ read: {},
8236
+ post: {},
8237
+ reply: {},
8238
+ resolve: {},
8239
+ who: {},
8240
+ watch: {}
8241
+ },
7932
8242
  doctor: {},
7933
8243
  help: {},
7934
8244
  init: {},
@@ -8516,7 +8826,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
8516
8826
  return out;
8517
8827
  }
8518
8828
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
8519
- function report2(ctx, impacts) {
8829
+ function report3(ctx, impacts) {
8520
8830
  const covered = impacts.filter((i) => i.runbooks.length);
8521
8831
  ctx.out.log(
8522
8832
  `${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
@@ -8554,7 +8864,7 @@ async function runbookImpact(ctx, options, deps = {}) {
8554
8864
  }
8555
8865
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
8556
8866
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
8557
- report2(ctx, impacts);
8867
+ report3(ctx, impacts);
8558
8868
  }
8559
8869
 
8560
8870
  // src/runbook-lint.ts
@@ -8787,7 +9097,7 @@ async function whoamiCommand(parsed, deps = {}) {
8787
9097
  }
8788
9098
 
8789
9099
  // src/runbook-command.ts
8790
- var ALLOWED2 = [
9100
+ var ALLOWED3 = [
8791
9101
  "config",
8792
9102
  "token",
8793
9103
  "email",
@@ -8827,7 +9137,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
8827
9137
  }
8828
9138
  return loadProjectConfig(configPath);
8829
9139
  }
8830
- async function buildContext2(parsed, deps, action) {
9140
+ async function buildContext3(parsed, deps, action) {
8831
9141
  const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
8832
9142
  const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
8833
9143
  const doFetch = deps.fetch ?? fetch;
@@ -8883,8 +9193,8 @@ async function buildContext2(parsed, deps, action) {
8883
9193
  }
8884
9194
  async function runbookCommand(parsed, deps = {}) {
8885
9195
  const action = parsed.positionals[1] ?? "list";
8886
- assertArgs(parsed, ALLOWED2, action === "ask" || action === "search" ? 64 : 4);
8887
- const ctx = await buildContext2(parsed, deps, action);
9196
+ assertArgs(parsed, ALLOWED3, action === "ask" || action === "search" ? 64 : 4);
9197
+ const ctx = await buildContext3(parsed, deps, action);
8888
9198
  const slug = parsed.positionals[2];
8889
9199
  switch (action) {
8890
9200
  case "list":
@@ -9075,31 +9385,31 @@ function printHostedJob(out, job, platform, appId) {
9075
9385
  url.searchParams.set("job", job.jobId);
9076
9386
  out.log(` Studio: ${url.toString()}`);
9077
9387
  }
9078
- function printHostedReport(out, report3) {
9079
- out.log(`security report ${report3.jobId}: ${report3.repository}@${report3.revision}`);
9080
- out.log(` coverage: ${report3.coverageStatus} cells=${report3.metrics.coverageCells} shallow=${report3.metrics.shallowCells} blocked=${report3.metrics.blockedCells} unscheduled=${report3.metrics.unscheduledCells} budget_exhausted=${report3.metrics.budgetExhaustedCells}`);
9081
- out.log(` findings: confirmed=${report3.metrics.confirmed} needs_reproduction=${report3.metrics.needsReproduction} candidates=${report3.metrics.candidates} rejected=${report3.metrics.rejected}`);
9082
- out.log(` discovery: ${report3.provenance.discovery?.provider ?? "unknown"}/${report3.provenance.discovery?.model ?? "unknown"}`);
9083
- out.log(` validation: ${report3.provenance.validation?.provider ?? "unknown"}/${report3.provenance.validation?.model ?? "unknown"} independent=${String(report3.provenance.independentValidation)}`);
9084
- for (const finding of report3.findings) {
9388
+ function printHostedReport(out, report4) {
9389
+ out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
9390
+ out.log(` coverage: ${report4.coverageStatus} cells=${report4.metrics.coverageCells} shallow=${report4.metrics.shallowCells} blocked=${report4.metrics.blockedCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
9391
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
9392
+ out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
9393
+ out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
9394
+ for (const finding of report4.findings) {
9085
9395
  const location = finding.locations[0];
9086
9396
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
9087
9397
  }
9088
- for (const limitation of report3.limitations) out.log(` limitation: ${limitation}`);
9398
+ for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
9089
9399
  }
9090
- function enforceHostedReportGate(report3, parsed, out, emitSuccess) {
9400
+ function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
9091
9401
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9092
9402
  const candidateValue = parsed.options["fail-on-candidates"];
9093
9403
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9094
9404
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
9095
- const confirmed = report3.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
9096
- const leads = failOnCandidates ? report3.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
9097
- const incomplete = report3.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
9405
+ const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
9406
+ const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
9407
+ const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
9098
9408
  if (confirmed.length || leads.length || incomplete) {
9099
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report3.coverageStatus}` : ""}`);
9409
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
9100
9410
  }
9101
9411
  if (emitSuccess) {
9102
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report3.coverageStatus}. This is not proof that the application is secure.`);
9412
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
9103
9413
  }
9104
9414
  }
9105
9415
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -9182,17 +9492,17 @@ async function runHostedSecurity(options) {
9182
9492
  allowNetwork: false
9183
9493
  }
9184
9494
  });
9185
- const report3 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
9186
- await (0, import_node3.writeSecurityArtifacts)(output, report3);
9187
- const reportDigest = await (0, import_security.securityFingerprint)(report3);
9495
+ const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
9496
+ await (0, import_node3.writeSecurityArtifacts)(output, report4);
9497
+ const reportDigest = await (0, import_security.securityFingerprint)(report4);
9188
9498
  await hosted.complete({
9189
9499
  reportDigest,
9190
- coverageStatus: report3.coverageStatus,
9191
- confirmed: report3.metrics.confirmed,
9192
- candidates: report3.metrics.candidates
9500
+ coverageStatus: report4.coverageStatus,
9501
+ confirmed: report4.metrics.confirmed,
9502
+ candidates: report4.metrics.candidates
9193
9503
  }, { signal: options.signal });
9194
- printSummary(options.stdout ?? console, appId, env, hosted.run, report3, output);
9195
- return Object.freeze({ report: report3, run: hosted.run, output });
9504
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
9505
+ return Object.freeze({ report: report4, run: hosted.run, output });
9196
9506
  }
9197
9507
  function selectEnv(requested, declared, configPath, rootDir) {
9198
9508
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -9202,11 +9512,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
9202
9512
  }
9203
9513
  return env;
9204
9514
  }
9205
- async function injectedToken(options, request) {
9206
- const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
9515
+ async function injectedToken(options, request2) {
9516
+ const value = options.token ?? await options.getToken?.(Object.freeze({ ...request2 }));
9207
9517
  if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
9208
9518
  throw new Error(
9209
- request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
9519
+ request2.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
9210
9520
  );
9211
9521
  }
9212
9522
  return value;
@@ -9218,14 +9528,14 @@ function profileFor(name, maxHuntTasks) {
9218
9528
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
9219
9529
  return { ...profile, maxHuntTasks };
9220
9530
  }
9221
- function printSummary(out, appId, env, run, report3, output) {
9222
- const complete = report3.coverage.filter((cell) => cell.state === "complete").length;
9531
+ function printSummary(out, appId, env, run, report4, output) {
9532
+ const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
9223
9533
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
9224
9534
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
9225
9535
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
9226
- out.log(` coverage: ${report3.coverageStatus} ${complete}/${report3.coverage.length} blocked=${report3.metrics.blockedCells} shallow=${report3.metrics.shallowCells} unscheduled=${report3.metrics.unscheduledCells} budget_exhausted=${report3.metrics.budgetExhaustedCells}`);
9227
- if (report3.callBudget) out.log(` calls: discovery=${formatBudget(report3.callBudget.discovery)} validation=${formatBudget(report3.callBudget.validation)}`);
9228
- out.log(` findings: confirmed=${report3.metrics.confirmed} needs_reproduction=${report3.metrics.needsReproduction} candidates=${report3.metrics.candidates}`);
9536
+ out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
9537
+ if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
9538
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
9229
9539
  out.log(` report: ${(0, import_node_path15.resolve)(output, "REPORT.md")}`);
9230
9540
  }
9231
9541
  function formatBudget(usage) {
@@ -9461,13 +9771,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
9461
9771
  }
9462
9772
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
9463
9773
  }
9464
- const report3 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9774
+ const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9465
9775
  if (parsed.options.json === true) {
9466
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report3 }, null, 2));
9776
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
9467
9777
  } else {
9468
- printHostedReport(context.stdout, report3);
9778
+ printHostedReport(context.stdout, report4);
9469
9779
  }
9470
- enforceHostedReportGate(report3, parsed, context.stdout, parsed.options.json !== true);
9780
+ enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
9471
9781
  }
9472
9782
  async function runLocalSecurityCommand(parsed, dependencies) {
9473
9783
  if (parsed.options.source === true) {
@@ -9508,11 +9818,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9508
9818
  sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
9509
9819
  fetch: doFetch,
9510
9820
  stdout: out,
9511
- getToken: async (request) => {
9512
- if (request.scope === "platform:security:self") {
9821
+ getToken: async (request2) => {
9822
+ if (request2.scope === "platform:security:self") {
9513
9823
  return getScopedPlatformToken({
9514
- platform: request.platform,
9515
- scope: request.scope,
9824
+ platform: request2.platform,
9825
+ scope: request2.scope,
9516
9826
  email: stringOpt(parsed.options.email),
9517
9827
  open,
9518
9828
  fetch: doFetch,
@@ -9521,7 +9831,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9521
9831
  });
9522
9832
  }
9523
9833
  const cfg = await loadProjectConfig(configPath);
9524
- if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
9834
+ if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
9525
9835
  throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
9526
9836
  }
9527
9837
  return getDeveloperToken(
@@ -9534,13 +9844,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9534
9844
  });
9535
9845
  enforceLocalGate(result.report, parsed);
9536
9846
  }
9537
- function enforceLocalGate(report3, parsed) {
9847
+ function enforceLocalGate(report4, parsed) {
9538
9848
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9539
9849
  const candidateValue = parsed.options["fail-on-candidates"];
9540
9850
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9541
- const confirmed = (0, import_security2.findingsAtOrAbove)(report3, failOn);
9542
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report3, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
9543
- const incomplete = report3.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
9851
+ const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
9852
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
9853
+ const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
9544
9854
  if (confirmed.length || leads.length || incomplete) {
9545
9855
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
9546
9856
  }
@@ -9579,9 +9889,9 @@ async function securityCommand(parsed, dependencies) {
9579
9889
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
9580
9890
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
9581
9891
  const context = await hostedSecurityContext(parsed, dependencies);
9582
- const report3 = await getHostedSecurityReport({ ...context, jobId });
9583
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report3, null, 2));
9584
- else printHostedReport(context.stdout, report3);
9892
+ const report4 = await getHostedSecurityReport({ ...context, jobId });
9893
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
9894
+ else printHostedReport(context.stdout, report4);
9585
9895
  return;
9586
9896
  }
9587
9897
  if (sub !== "run") {
@@ -9665,7 +9975,8 @@ async function securityStatus(parsed, dependencies) {
9665
9975
 
9666
9976
  // src/cli.ts
9667
9977
  function exitCodeFor(err) {
9668
- return err?.code === "handshake_pending" ? 75 : 1;
9978
+ const code = err?.code;
9979
+ return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
9669
9980
  }
9670
9981
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9671
9982
  const parsed = parseArgv(argv);
@@ -9713,6 +10024,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9713
10024
  await pmCommand(parsed, dependencies);
9714
10025
  return;
9715
10026
  }
10027
+ if (command === "discuss") {
10028
+ await discussCommand(parsed, dependencies);
10029
+ return;
10030
+ }
9716
10031
  if (command === "provision") {
9717
10032
  await provisionCommand(parsed, dependencies);
9718
10033
  return;