@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/index.cjs CHANGED
@@ -613,23 +613,23 @@ function adminAiAuditQuery(filters) {
613
613
  }
614
614
  return `?limit=${filters.limit}`;
615
615
  }
616
- async function readAdminAiAudit(request) {
617
- const response2 = await request.fetch(`${request.platform}/registry/platform/ai-audit${request.query}`, {
618
- headers: request.headers
616
+ async function readAdminAiAudit(request2) {
617
+ const response2 = await request2.fetch(`${request2.platform}/registry/platform/ai-audit${request2.query}`, {
618
+ headers: request2.headers
619
619
  });
620
620
  const body = await responseBody(response2);
621
621
  if (!response2.ok) throw new Error(apiError(response2.status, body));
622
- if (request.json) {
623
- request.stdout.log(JSON.stringify(body, null, 2));
622
+ if (request2.json) {
623
+ request2.stdout.log(JSON.stringify(body, null, 2));
624
624
  return;
625
625
  }
626
626
  const events = isRecord(body) && Array.isArray(body.events) ? body.events.filter(isRecord) : [];
627
- request.stdout.log("when change target before -> after actor");
627
+ request2.stdout.log("when change target before -> after actor");
628
628
  for (const event of events) {
629
629
  const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
630
630
  const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
631
631
  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";
632
- request.stdout.log([
632
+ request2.stdout.log([
633
633
  timestamp(event.createdAt),
634
634
  String(event.changeKind ?? ""),
635
635
  String(event.purpose ?? event.provider ?? ""),
@@ -685,14 +685,14 @@ function adminAiUsageQuery(filters) {
685
685
  const query = params.toString();
686
686
  return query ? `?${query}` : "";
687
687
  }
688
- async function readAdminAiUsage(request) {
689
- const res = await request.fetch(`${request.platform}/registry/platform/ai-usage${request.query}`, {
690
- headers: request.headers
688
+ async function readAdminAiUsage(request2) {
689
+ const res = await request2.fetch(`${request2.platform}/registry/platform/ai-usage${request2.query}`, {
690
+ headers: request2.headers
691
691
  });
692
692
  const body = await responseBody2(res);
693
693
  if (!res.ok) throw new Error(apiError2("read platform AI usage", res.status, body));
694
- if (request.json) request.stdout.log(JSON.stringify(body, null, 2));
695
- else printUsage(body, request.stdout);
694
+ if (request2.json) request2.stdout.log(JSON.stringify(body, null, 2));
695
+ else printUsage(body, request2.stdout);
696
696
  }
697
697
  function usageLimit(value) {
698
698
  if (!Number.isSafeInteger(value) || value < 1 || value > 500) {
@@ -3154,10 +3154,10 @@ function installSkill(options = {}) {
3154
3154
  current.add(target);
3155
3155
  targets.set(harness, current);
3156
3156
  };
3157
- const plan = (target, content, managedMerge = false, boundary = root) => {
3157
+ const plan = (target, content2, managedMerge = false, boundary = root) => {
3158
3158
  const existing = plans.get(target);
3159
- if (existing && existing.content !== content) throw new Error(`internal agent setup conflict for ${target}`);
3160
- plans.set(target, { target, content, boundary, managedMerge });
3159
+ if (existing && existing.content !== content2) throw new Error(`internal agent setup conflict for ${target}`);
3160
+ plans.set(target, { target, content: content2, boundary, managedMerge });
3161
3161
  };
3162
3162
  const planSkillTree = (targetDir2, boundary = root) => {
3163
3163
  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);
@@ -3943,11 +3943,11 @@ async function gitBlobs(cwd, entries, maxBytes) {
3943
3943
  if (!Number.isSafeInteger(length) || length < 0 || newline + 1 + length >= output.length) {
3944
3944
  throw new Error("git object output has an invalid length");
3945
3945
  }
3946
- const content = output.subarray(newline + 1, newline + 1 + length);
3946
+ const content2 = output.subarray(newline + 1, newline + 1 + length);
3947
3947
  if (output[newline + 1 + length] !== 10) throw new Error("git object output is malformed");
3948
- total += content.byteLength;
3948
+ total += content2.byteLength;
3949
3949
  if (total > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
3950
- blobs.push(content);
3950
+ blobs.push(content2);
3951
3951
  offset = newline + 1 + length + 1;
3952
3952
  }
3953
3953
  return blobs;
@@ -3970,13 +3970,13 @@ async function materializeGitTree(source, commitSha, options = {}) {
3970
3970
  try {
3971
3971
  const blobs = await gitBlobs(sourceDir, entries, maxBytes);
3972
3972
  for (const [index, entry] of entries.entries()) {
3973
- const content = blobs[index];
3974
- byteCount += content.byteLength;
3973
+ const content2 = blobs[index];
3974
+ byteCount += content2.byteLength;
3975
3975
  if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
3976
3976
  const target = (0, import_path2.resolve)(targetRoot, entry.path);
3977
3977
  if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
3978
3978
  await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
3979
- await (0, import_promises3.writeFile)(target, content, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
3979
+ await (0, import_promises3.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
3980
3980
  }
3981
3981
  return {
3982
3982
  root,
@@ -4354,11 +4354,11 @@ var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
4354
4354
  var MAX_PATCH_BYTES = 256 * 1024;
4355
4355
  var MAX_STATE_BYTES = 64 * 1024;
4356
4356
  async function createCodePortableCheckpoint(input) {
4357
- const state = normalizeState(input.state);
4357
+ const state2 = normalizeState(input.state);
4358
4358
  validateBaseAndPatch(input.baseCommitSha, input.patch);
4359
4359
  const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
4360
- const stateDigest = `sha256:${await sha256Hex(canonicalJson(state))}`;
4361
- const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state, stateDigest };
4360
+ const stateDigest = `sha256:${await sha256Hex(canonicalJson(state2))}`;
4361
+ const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
4362
4362
  const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
4363
4363
  return freeze({ ...fields, patch: input.patch, checkpointDigest });
4364
4364
  }
@@ -4379,8 +4379,8 @@ async function verifyCodePortableCheckpoint(value) {
4379
4379
  return created;
4380
4380
  }
4381
4381
  function normalizeState(value) {
4382
- const state = object(value, "state");
4383
- exact2(state, [
4382
+ const state2 = object(value, "state");
4383
+ exact2(state2, [
4384
4384
  "planCursor",
4385
4385
  "conversationRefs",
4386
4386
  "planningInputDigest",
@@ -4392,13 +4392,13 @@ function normalizeState(value) {
4392
4392
  "unresolvedApprovals",
4393
4393
  "trustStatus"
4394
4394
  ]);
4395
- const planCursor = state.planCursor;
4396
- const conversations = strings(state.conversationRefs, "conversationRefs", ID2, 256, false);
4397
- const approvals = strings(state.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
4398
- 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) {
4395
+ const planCursor = state2.planCursor;
4396
+ const conversations = strings(state2.conversationRefs, "conversationRefs", ID2, 256, false);
4397
+ const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
4398
+ 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) {
4399
4399
  throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
4400
4400
  }
4401
- const effects = state.completedEffects.map((item) => {
4401
+ const effects = state2.completedEffects.map((item) => {
4402
4402
  const effect = object(item, "completed effect");
4403
4403
  exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
4404
4404
  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)) {
@@ -4413,18 +4413,18 @@ function normalizeState(value) {
4413
4413
  if (new Set(effects.map((item) => item.effectId)).size !== effects.length) {
4414
4414
  throw invalid2("Portable checkpoint repeats a completed effect.");
4415
4415
  }
4416
- const trustStatus = state.trustStatus;
4417
- 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)) {
4416
+ const trustStatus = state2.trustStatus;
4417
+ 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)) {
4418
4418
  throw invalid2("Portable checkpoint trust status does not match its evidence digests.");
4419
4419
  }
4420
4420
  const normalized = {
4421
4421
  planCursor,
4422
4422
  conversationRefs: conversations,
4423
- planningInputDigest: state.planningInputDigest,
4424
- buildPolicyDigest: state.buildPolicyDigest,
4425
- dependencyLayerDigest: state.dependencyLayerDigest,
4426
- verificationDigest: state.verificationDigest,
4427
- reviewDigest: state.reviewDigest,
4423
+ planningInputDigest: state2.planningInputDigest,
4424
+ buildPolicyDigest: state2.buildPolicyDigest,
4425
+ dependencyLayerDigest: state2.dependencyLayerDigest,
4426
+ verificationDigest: state2.verificationDigest,
4427
+ reviewDigest: state2.reviewDigest,
4428
4428
  completedEffects: effects,
4429
4429
  unresolvedApprovals: approvals,
4430
4430
  trustStatus
@@ -4541,60 +4541,60 @@ async function createConversionRegistry(config) {
4541
4541
  policies.set(policy.conversionId, Object.freeze(policy));
4542
4542
  }
4543
4543
  const outputCounts = /* @__PURE__ */ new Map();
4544
- const get = (id, kind) => {
4544
+ const get2 = (id, kind) => {
4545
4545
  const policy = policies.get(id);
4546
4546
  if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
4547
4547
  return policy;
4548
4548
  };
4549
4549
  const checked = (source, id, kind) => {
4550
- const policy = get(id, kind);
4550
+ const policy = get2(id, kind);
4551
4551
  if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
4552
4552
  return policy;
4553
4553
  };
4554
- const emit2 = (source, policy, value) => convert(source, policy, value, outputCounts);
4554
+ const emit3 = (source, policy, value) => convert(source, policy, value, outputCounts);
4555
4555
  const operations = Object.freeze({
4556
4556
  boolean: async (value, id) => {
4557
4557
  const policy = checked(value, id, "boolean");
4558
- return emit2(value, policy, requireBoolean(value.value));
4558
+ return emit3(value, policy, requireBoolean(value.value));
4559
4559
  },
4560
4560
  integer: async (value, id) => {
4561
4561
  const policy = checked(value, id, "integer");
4562
- return emit2(value, policy, boundedInteger(value.value, policy.output));
4562
+ return emit3(value, policy, boundedInteger(value.value, policy.output));
4563
4563
  },
4564
4564
  finiteNumber: async (value, id) => {
4565
4565
  const policy = checked(value, id, "finite_number");
4566
- return emit2(value, policy, boundedNumber(value.value, policy.output));
4566
+ return emit3(value, policy, boundedNumber(value.value, policy.output));
4567
4567
  },
4568
4568
  enum: async (value, id) => {
4569
4569
  const policy = checked(value, id, "enum");
4570
- return emit2(value, policy, enumMember(value.value, policy.output));
4570
+ return emit3(value, policy, enumMember(value.value, policy.output));
4571
4571
  },
4572
4572
  date: async (value, id) => {
4573
4573
  const policy = checked(value, id, "date");
4574
- return emit2(value, policy, canonicalDate(value.value, policy.output));
4574
+ return emit3(value, policy, canonicalDate(value.value, policy.output));
4575
4575
  },
4576
4576
  registeredId: async (value, id) => {
4577
4577
  const policy = checked(value, id, "registered_id");
4578
4578
  const registry = config.registeredIds?.[policy.output.registryId];
4579
4579
  const output = typeof value.value === "string" ? registry?.values[value.value] : void 0;
4580
4580
  if (!output) throw new CamelError("conversion_rejected", "Registered-ID conversion rejected the candidate.");
4581
- return emit2(value, policy, output);
4581
+ return emit3(value, policy, output);
4582
4582
  },
4583
4583
  digest: async (value, id) => {
4584
4584
  const policy = checked(value, id, "digest");
4585
4585
  if (!(value.value instanceof Uint8Array)) throw new CamelError("conversion_rejected", "Digest conversion requires bytes.");
4586
- return emit2(value, policy, await sha256Hex(value.value));
4586
+ return emit3(value, policy, await sha256Hex(value.value));
4587
4587
  },
4588
4588
  measure: async (value, metric, id) => {
4589
4589
  const policy = checked(value, id, "integer");
4590
4590
  const measured = measure(value.value, metric);
4591
- return emit2(value, policy, boundedInteger(measured, policy.output));
4591
+ return emit3(value, policy, boundedInteger(measured, policy.output));
4592
4592
  },
4593
4593
  test: async (value, predicateId, id) => {
4594
4594
  const policy = checked(value, id, "boolean");
4595
4595
  const predicate = config.predicates?.[predicateId];
4596
4596
  if (!predicate) throw new CamelError("conversion_rejected", "Predicate is not registered.");
4597
- return emit2(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
4597
+ return emit3(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
4598
4598
  }
4599
4599
  });
4600
4600
  return Object.freeze({ operations, policy: (id) => policies.get(id) ?? missingPolicy() });
@@ -4813,11 +4813,11 @@ async function digestStagedWorkspace(root, limits) {
4813
4813
  const hash = (0, import_crypto.createHash)("sha256");
4814
4814
  let bytes = 0;
4815
4815
  for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
4816
- const content = await (0, import_promises5.readFile)(file.target);
4817
- bytes += Buffer.byteLength(file.path) + content.byteLength;
4816
+ const content2 = await (0, import_promises5.readFile)(file.target);
4817
+ bytes += Buffer.byteLength(file.path) + content2.byteLength;
4818
4818
  if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
4819
- hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content.byteLength}:`);
4820
- hash.update(content);
4819
+ hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
4820
+ hash.update(content2);
4821
4821
  }
4822
4822
  return `sha256:${hash.digest("hex")}`;
4823
4823
  }
@@ -4912,13 +4912,13 @@ function createCodeRuntimeControlClient(options) {
4912
4912
  if (!Number.isSafeInteger(modelRequestTimeoutMs) || modelRequestTimeoutMs < 3e4 || modelRequestTimeoutMs > 30 * 6e4) {
4913
4913
  throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
4914
4914
  }
4915
- const request = options.fetch ?? fetch;
4915
+ const request2 = options.fetch ?? fetch;
4916
4916
  const call2 = async (path, body, timeoutMs = requestTimeoutMs) => {
4917
4917
  const timeout = AbortSignal.timeout(timeoutMs);
4918
4918
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
4919
4919
  let response2;
4920
4920
  try {
4921
- response2 = await request(`${endpoint}${path}`, {
4921
+ response2 = await request2(`${endpoint}${path}`, {
4922
4922
  method: "POST",
4923
4923
  headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
4924
4924
  body: JSON.stringify(body),
@@ -5825,34 +5825,34 @@ function createCodeToolBroker(options) {
5825
5825
  const policy = createCodePolicyGate(options);
5826
5826
  let tail = Promise.resolve();
5827
5827
  return {
5828
- execute(context, request) {
5829
- const result = tail.then(() => route(context, request, options, recipes, policy));
5828
+ execute(context, request2) {
5829
+ const result = tail.then(() => route(context, request2, options, recipes, policy));
5830
5830
  tail = result.then(() => void 0, () => void 0);
5831
5831
  return result;
5832
5832
  }
5833
5833
  };
5834
5834
  }
5835
- async function route(context, request, options, recipes, policy) {
5835
+ async function route(context, request2, options, recipes, policy) {
5836
5836
  try {
5837
5837
  if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
5838
- if (request.tool === "sandbox.read") return await read(context, request, options, policy);
5839
- if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
5840
- return await recipe(context, request, options, recipes, policy);
5838
+ if (request2.tool === "sandbox.read") return await read(context, request2, options, policy);
5839
+ if (request2.tool === "sandbox.apply_patch") return await patch(context, request2, options, policy);
5840
+ return await recipe(context, request2, options, recipes, policy);
5841
5841
  } catch (reason) {
5842
- return response(request, false, reason instanceof TypeError ? reason.message : "tool failed closed");
5842
+ return response(request2, false, reason instanceof TypeError ? reason.message : "tool failed closed");
5843
5843
  }
5844
5844
  }
5845
- async function read(context, request, options, policy) {
5846
- exactKeys(request.input, ["path", "startLine", "endLine"]);
5847
- const path = stringField(request.input, "path");
5848
- const startLine = optionalInteger(request.input.startLine) ?? 1;
5849
- const endLine = optionalInteger(request.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
5845
+ async function read(context, request2, options, policy) {
5846
+ exactKeys(request2.input, ["path", "startLine", "endLine"]);
5847
+ const path = stringField(request2.input, "path");
5848
+ const startLine = optionalInteger(request2.input.startLine) ?? 1;
5849
+ const endLine = optionalInteger(request2.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
5850
5850
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
5851
5851
  throw new TypeError("requested line range exceeds its bound");
5852
5852
  }
5853
5853
  const paths = await registeredFiles(context.workspaceDir, 2e4);
5854
- const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
5855
- if (!allowed) return response(request, false, "tool denied by CaMeL policy");
5854
+ const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
5855
+ if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
5856
5856
  const target = resolveCodePath(context.workspaceDir, path);
5857
5857
  const info = await (0, import_promises9.stat)(target);
5858
5858
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
@@ -5861,40 +5861,40 @@ async function read(context, request, options, policy) {
5861
5861
  const source = await (0, import_promises9.readFile)(target);
5862
5862
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
5863
5863
  const lines = source.toString("utf8").split("\n");
5864
- const content = lines.slice(startLine - 1, endLine).join("\n");
5865
- if (Buffer.byteLength(content) > (options.maxReadBytes ?? 128 * 1024)) {
5864
+ const content2 = lines.slice(startLine - 1, endLine).join("\n");
5865
+ if (Buffer.byteLength(content2) > (options.maxReadBytes ?? 128 * 1024)) {
5866
5866
  throw new TypeError("read result exceeds its byte bound");
5867
5867
  }
5868
- return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
5868
+ return response(request2, true, content2, { path, startLine, endLine: Math.min(endLine, lines.length) });
5869
5869
  }
5870
- async function patch(context, request, options, policy) {
5871
- exactKeys(request.input, ["patch"]);
5872
- const value = stringField(request.input, "patch");
5870
+ async function patch(context, request2, options, policy) {
5871
+ exactKeys(request2.input, ["patch"]);
5872
+ const value = stringField(request2.input, "patch");
5873
5873
  const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
5874
5874
  if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
5875
5875
  throw new TypeError("patch targets a read-only reference source");
5876
5876
  }
5877
- const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
5878
- if (!allowed) return response(request, false, "tool denied by CaMeL policy");
5877
+ const allowed = await policy.patch(policyContext(context, request2, options, { patch: value }));
5878
+ if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
5879
5879
  await applyCodePatch(context.workspaceDir, value, paths);
5880
- return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
5880
+ return response(request2, true, `Applied patch to ${paths.length} file(s).`, { paths });
5881
5881
  }
5882
- async function recipe(context, request, options, recipes, policy) {
5883
- exactKeys(request.input, ["recipeId"]);
5884
- const recipeId = stringField(request.input, "recipeId");
5882
+ async function recipe(context, request2, options, recipes, policy) {
5883
+ exactKeys(request2.input, ["recipeId"]);
5884
+ const recipeId = stringField(request2.input, "recipeId");
5885
5885
  const digestLimits = {
5886
5886
  maxFiles: options.maxRecipeWorkspaceFiles ?? 2e4,
5887
5887
  maxBytes: options.maxRecipeWorkspaceBytes ?? 512 * 1024 * 1024
5888
5888
  };
5889
5889
  const sourceDigest = await digestStagedWorkspace(context.workspaceDir, digestLimits);
5890
- const allowed = await policy.recipe(policyContext(context, request, options, {
5890
+ const allowed = await policy.recipe(policyContext(context, request2, options, {
5891
5891
  recipeIds: [...recipes.keys()].sort(),
5892
5892
  recipeId,
5893
5893
  sourceDigest
5894
5894
  }));
5895
- if (!allowed) return response(request, false, "tool denied by CaMeL policy");
5895
+ if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
5896
5896
  const selected = recipes.get(recipeId);
5897
- if (!selected) return response(request, false, "build recipe is not registered");
5897
+ if (!selected) return response(request2, false, "build recipe is not registered");
5898
5898
  const staged = await stageWorkspace(context.workspaceDir, {
5899
5899
  maxFiles: digestLimits.maxFiles,
5900
5900
  maxBytes: digestLimits.maxBytes
@@ -5911,7 +5911,7 @@ async function recipe(context, request, options, recipes, policy) {
5911
5911
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
5912
5912
  const ok = result.exitCode === 0 && !result.outputLimitExceeded && !result.timedOut;
5913
5913
  const status = result.timedOut ? "timed out" : result.outputLimitExceeded ? "exceeded output limit" : ok ? "passed" : `failed with exit ${result.exitCode}`;
5914
- return response(request, ok, `Recipe ${recipeId} ${status}.${output ? `
5914
+ return response(request2, ok, `Recipe ${recipeId} ${status}.${output ? `
5915
5915
  ${output}` : ""}`, {
5916
5916
  recipeId,
5917
5917
  exitCode: result.exitCode,
@@ -5923,10 +5923,10 @@ ${output}` : ""}`, {
5923
5923
  await staged.cleanup();
5924
5924
  }
5925
5925
  }
5926
- function policyContext(context, request, options, extra) {
5926
+ function policyContext(context, request2, options, extra) {
5927
5927
  return {
5928
5928
  lease: context.lease,
5929
- request,
5929
+ request: request2,
5930
5930
  workspaceId: `workspace:${context.lease.task.attemptId}`,
5931
5931
  readers: { kind: "principals", principalIds: [options.readerId] },
5932
5932
  ...extra
@@ -5976,8 +5976,8 @@ function optionalInteger(value) {
5976
5976
  if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
5977
5977
  return value;
5978
5978
  }
5979
- function response(request, ok, content, details) {
5980
- return { requestId: request.requestId, ok, content, ...details ? { details } : {} };
5979
+ function response(request2, ok, content2, details) {
5980
+ return { requestId: request2.requestId, ok, content: content2, ...details ? { details } : {} };
5981
5981
  }
5982
5982
  function codeCommandMetadata(payload, resume) {
5983
5983
  const trusted = record22(payload.trustedBase);
@@ -6077,13 +6077,13 @@ function createCodeRuntimeToolBroker(input, lease, role) {
6077
6077
  readerId: `code-session:${lease.task.taskId}`,
6078
6078
  readOnlyPrefixes: [".odla-references"]
6079
6079
  });
6080
- 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" }) };
6080
+ 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" }) };
6081
6081
  }
6082
6082
  async function handleCodeRuntimeInference(input) {
6083
- const { command, metadata: metadata2, request, state } = input;
6084
- if (state.tokens >= metadata2.maxTokensPerInteraction) {
6085
- if (!state.noticeEmitted) {
6086
- state.noticeEmitted = true;
6083
+ const { command, metadata: metadata2, request: request2, state: state2 } = input;
6084
+ if (state2.tokens >= metadata2.maxTokensPerInteraction) {
6085
+ if (!state2.noticeEmitted) {
6086
+ state2.noticeEmitted = true;
6087
6087
  await input.event({
6088
6088
  type: "message",
6089
6089
  actor: "system",
@@ -6093,7 +6093,7 @@ async function handleCodeRuntimeInference(input) {
6093
6093
  return {
6094
6094
  protocolVersion: HARNESS_PROTOCOL_VERSION,
6095
6095
  type: "inference.response",
6096
- requestId: request.requestId,
6096
+ requestId: request2.requestId,
6097
6097
  response: {
6098
6098
  id: `budget:${command.commandId}`,
6099
6099
  provider: "openai",
@@ -6107,11 +6107,11 @@ async function handleCodeRuntimeInference(input) {
6107
6107
  }
6108
6108
  const startedAt = Date.now();
6109
6109
  const response2 = await input.control.infer(command.sessionId, {
6110
- requestId: request.requestId,
6110
+ requestId: request2.requestId,
6111
6111
  interactionId: command.commandId,
6112
- call: request.call
6112
+ call: request2.call
6113
6113
  });
6114
- state.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
6114
+ state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
6115
6115
  await input.event({
6116
6116
  type: "usage",
6117
6117
  provider: response2.receipt.provider,
@@ -6120,13 +6120,13 @@ async function handleCodeRuntimeInference(input) {
6120
6120
  outputTokens: response2.receipt.outputTokens,
6121
6121
  durationMs: Date.now() - startedAt,
6122
6122
  interactionId: command.commandId,
6123
- interactionTokens: state.tokens,
6123
+ interactionTokens: state2.tokens,
6124
6124
  interactionMaxTokens: metadata2.maxTokensPerInteraction
6125
6125
  }).catch(() => void 0);
6126
6126
  return {
6127
6127
  protocolVersion: HARNESS_PROTOCOL_VERSION,
6128
6128
  type: "inference.response",
6129
- requestId: request.requestId,
6129
+ requestId: request2.requestId,
6130
6130
  response: response2.response
6131
6131
  };
6132
6132
  }
@@ -6494,6 +6494,14 @@ Usage:
6494
6494
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
6495
6495
  odla-ai pm <goal|task|decision|bug> rm <id>
6496
6496
  odla-ai pm handoff --app <id> [--json]
6497
+ odla-ai discuss groups [--json]
6498
+ odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
6499
+ odla-ai discuss read <topic> [--json]
6500
+ odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"]
6501
+ odla-ai discuss reply <topic> --body "..." [--markup "..."]
6502
+ odla-ai discuss resolve <topic> [--reopen]
6503
+ odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
6504
+ odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
6497
6505
  odla-ai whoami [--json]
6498
6506
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6499
6507
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -6598,6 +6606,13 @@ Commands:
6598
6606
  "app". Entities: goal (alias conformance), task (alias kanban),
6599
6607
  decision, bug. Status changes and comments post to each item's
6600
6608
  @odla-ai/chat discussion thread.
6609
+ discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
6610
+ one group per project, topics with replies, @-mentions of people,
6611
+ agents, PM items, and projects. Built for unattended use \u2014 post a
6612
+ question, then "watch" it until someone answers. "watch" exits 75
6613
+ (not 1) when it times out with nothing new, so a script can tell
6614
+ "no answer yet" from a failure and just rerun. Use "who" to look
6615
+ up the exact @[Label](kind/id) markup for a mention.
6601
6616
  provision Register services, compose integrations, persist credentials, optionally push secrets.
6602
6617
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6603
6618
  skill Same installer; --agent accepts all, claude, codex, cursor,
@@ -6785,15 +6800,15 @@ async function connectGitHubSecuritySource(options) {
6785
6800
  const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
6786
6801
  const wait2 = options.wait ?? waitForHostedPoll;
6787
6802
  while (true) {
6788
- const state = await requestHostedSecurityJson(
6803
+ const state2 = await requestHostedSecurityJson(
6789
6804
  options,
6790
6805
  `/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
6791
6806
  {},
6792
6807
  "check GitHub connection"
6793
6808
  );
6794
- if (state.status !== "pending") {
6795
- if (state.status === "connected") return state;
6796
- throw new Error(`GitHub connection ${state.status}${state.failureCode ? `: ${state.failureCode}` : ""}`);
6809
+ if (state2.status !== "pending") {
6810
+ if (state2.status === "connected") return state2;
6811
+ throw new Error(`GitHub connection ${state2.status}${state2.failureCode ? `: ${state2.failureCode}` : ""}`);
6797
6812
  }
6798
6813
  if (now() >= deadline) throw new Error("GitHub connection approval timed out");
6799
6814
  await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
@@ -7244,6 +7259,289 @@ async function codeCommand(parsed, dependencies) {
7244
7259
  });
7245
7260
  }
7246
7261
 
7262
+ // src/discuss-actions.ts
7263
+ async function request(ctx, method, path, body) {
7264
+ const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7265
+ method,
7266
+ headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
7267
+ body: body === void 0 ? void 0 : JSON.stringify(body)
7268
+ });
7269
+ const data = await res.json().catch(() => ({}));
7270
+ if (!res.ok)
7271
+ throw new Error(`discuss ${method} ${path} failed: ${data.error ?? `registry returned ${res.status}`}`);
7272
+ return data;
7273
+ }
7274
+ function emit(ctx, value, human) {
7275
+ if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
7276
+ else human();
7277
+ }
7278
+ var state = (topic) => topic.resolvedAt ? "resolved" : "open";
7279
+ function bodyWithRefs(post) {
7280
+ if (!post.refs || post.refs.length === 0) return post.body;
7281
+ let out = "";
7282
+ let cursor = 0;
7283
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
7284
+ if (ref.start < cursor || ref.end > post.body.length) continue;
7285
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
7286
+ cursor = ref.end;
7287
+ }
7288
+ return out + post.body.slice(cursor);
7289
+ }
7290
+ function content(parsed) {
7291
+ const markup = stringOpt(parsed.options.markup);
7292
+ if (markup) return { markup };
7293
+ const body = stringOpt(parsed.options.body);
7294
+ if (!body) throw new Error('needs --body "\u2026" (or --markup with @[Label](kind/id) mentions)');
7295
+ return { body };
7296
+ }
7297
+ async function discussGroups(ctx) {
7298
+ const result = await request(
7299
+ ctx,
7300
+ "GET",
7301
+ "/groups"
7302
+ );
7303
+ emit(ctx, result, () => {
7304
+ ctx.out.log("app name started");
7305
+ for (const group of result.groups) {
7306
+ ctx.out.log(`${group.appId} ${group.name} ${group.channelId ? "yes" : "no"}`);
7307
+ }
7308
+ });
7309
+ }
7310
+ async function discussList(ctx, parsed) {
7311
+ const query = new URLSearchParams();
7312
+ for (const [flag, param] of [
7313
+ ["app", "app"],
7314
+ ["q", "q"],
7315
+ ["state", "state"],
7316
+ ["limit", "limit"],
7317
+ ["offset", "offset"]
7318
+ ]) {
7319
+ const value = stringOpt(parsed.options[flag]);
7320
+ if (value) query.set(param, value);
7321
+ }
7322
+ const qs = query.toString();
7323
+ const page = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
7324
+ emit(ctx, page, () => {
7325
+ ctx.out.log(`topics \u2014 ${page.topics.length} of ${page.total}`);
7326
+ ctx.out.log("id state app replies subject");
7327
+ for (const topic of page.topics) {
7328
+ ctx.out.log(
7329
+ `${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
7330
+ );
7331
+ }
7332
+ });
7333
+ }
7334
+ async function discussRead(ctx, id) {
7335
+ const result = await request(
7336
+ ctx,
7337
+ "GET",
7338
+ `/topics/${encodeURIComponent(id)}`
7339
+ );
7340
+ emit(ctx, result, () => {
7341
+ ctx.out.log(`${result.topic.subject} [${state(result.topic)}] ${result.topic.appId ?? ""}`);
7342
+ for (const post of result.posts) {
7343
+ const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7344
+ ctx.out.log(`
7345
+ \u2014 ${who}`);
7346
+ ctx.out.log(bodyWithRefs(post));
7347
+ for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
7348
+ }
7349
+ });
7350
+ }
7351
+ async function discussPost(ctx, parsed) {
7352
+ const appId = stringOpt(parsed.options.app);
7353
+ if (!appId) throw new Error("discuss post needs --app <appId>");
7354
+ const subject = stringOpt(parsed.options.subject);
7355
+ if (!subject) throw new Error('discuss post needs --subject "\u2026"');
7356
+ const created = await request(ctx, "POST", "/topics", {
7357
+ appId,
7358
+ subject,
7359
+ ...content(parsed)
7360
+ });
7361
+ emit(ctx, created, () => ctx.out.log(`opened topic ${created.id}`));
7362
+ }
7363
+ async function discussReply(ctx, id, parsed) {
7364
+ const created = await request(
7365
+ ctx,
7366
+ "POST",
7367
+ `/topics/${encodeURIComponent(id)}/replies`,
7368
+ content(parsed)
7369
+ );
7370
+ emit(ctx, created, () => ctx.out.log(`replied ${created.id}`));
7371
+ }
7372
+ async function discussResolve(ctx, id, resolved) {
7373
+ const result = await request(
7374
+ ctx,
7375
+ "PATCH",
7376
+ `/topics/${encodeURIComponent(id)}`,
7377
+ { resolved }
7378
+ );
7379
+ emit(ctx, result, () => ctx.out.log(`${resolved ? "resolved" : "reopened"} ${result.id}`));
7380
+ }
7381
+ async function discussWho(ctx, parsed) {
7382
+ const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
7383
+ const app = stringOpt(parsed.options.app);
7384
+ if (app) query.set("app", app);
7385
+ const kinds = stringOpt(parsed.options.kinds);
7386
+ if (kinds) query.set("kinds", kinds);
7387
+ const found = await request(ctx, "GET", `/mentionables?${query.toString()}`);
7388
+ emit(ctx, found, () => {
7389
+ ctx.out.log("mention kind hint");
7390
+ for (const item of found.items) {
7391
+ ctx.out.log(`@[${item.label}](${item.kind}/${item.id}) ${item.kind} ${item.hint ?? ""}`);
7392
+ }
7393
+ if (found.failed.length > 0) ctx.out.error(`(no results from: ${found.failed.join(", ")})`);
7394
+ });
7395
+ }
7396
+
7397
+ // src/discuss-watch.ts
7398
+ var DEFAULT_INTERVAL_MS = 15e3;
7399
+ var DEFAULT_TIMEOUT_MS = 15 * 6e4;
7400
+ function numberOpt2(parsed, flag, fallback) {
7401
+ const raw = stringOpt(parsed.options[flag]);
7402
+ const value = raw == null ? NaN : Number(raw);
7403
+ return Number.isFinite(value) && value > 0 ? value : fallback;
7404
+ }
7405
+ async function get(ctx, path) {
7406
+ const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7407
+ headers: { authorization: `Bearer ${ctx.token}` }
7408
+ });
7409
+ const data = await res.json().catch(() => ({}));
7410
+ if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
7411
+ return data;
7412
+ }
7413
+ async function discussWatch(ctx, topicId, parsed) {
7414
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
7415
+ const now = ctx.now ?? Date.now;
7416
+ const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
7417
+ const timeoutMs = numberOpt2(parsed, "timeout", DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
7418
+ const by = stringOpt(parsed.options.by);
7419
+ const self = stringOpt(parsed.options.self);
7420
+ const deadline = now() + timeoutMs;
7421
+ const fresh = (post) => (!by || post.authorId === by) && (!self || post.authorId !== self);
7422
+ let seen = /* @__PURE__ */ new Set();
7423
+ let since = now();
7424
+ if (topicId) {
7425
+ const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
7426
+ seen = new Set(first.posts.map((post) => post.id));
7427
+ }
7428
+ for (; ; ) {
7429
+ if (topicId) {
7430
+ const { posts } = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
7431
+ const added = posts.filter((post) => !seen.has(post.id) && fresh(post));
7432
+ if (added.length > 0) return report2(ctx, { found: true, posts: added });
7433
+ for (const post of posts) seen.add(post.id);
7434
+ } else {
7435
+ const app = stringOpt(parsed.options.app);
7436
+ const { topics } = await get(
7437
+ ctx,
7438
+ `/topics?state=all${app ? `&app=${encodeURIComponent(app)}` : ""}`
7439
+ );
7440
+ const active = topics.filter((topic) => topic.lastActivityAt > since);
7441
+ if (active.length > 0) return report2(ctx, { found: true, topics: active });
7442
+ since = now();
7443
+ }
7444
+ if (now() >= deadline) return report2(ctx, { found: false });
7445
+ await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
7446
+ }
7447
+ }
7448
+ function report2(ctx, result) {
7449
+ if (ctx.json) {
7450
+ ctx.out.log(JSON.stringify(result, null, 2));
7451
+ } else if (result.found) {
7452
+ for (const post of result.posts ?? []) {
7453
+ const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7454
+ ctx.out.log(`\u2014 ${who}
7455
+ ${post.body}`);
7456
+ }
7457
+ for (const topic of result.topics ?? []) {
7458
+ ctx.out.log(`${topic.id} ${topic.appId ?? ""} ${topic.subject}`);
7459
+ }
7460
+ }
7461
+ return result;
7462
+ }
7463
+ var WatchTimeoutError = class extends Error {
7464
+ code = "watch_timeout";
7465
+ constructor() {
7466
+ super("nothing new before the timeout");
7467
+ this.name = "WatchTimeoutError";
7468
+ }
7469
+ };
7470
+
7471
+ // src/discuss-command.ts
7472
+ var ALLOWED = [
7473
+ "config",
7474
+ "token",
7475
+ "email",
7476
+ "json",
7477
+ "app",
7478
+ "subject",
7479
+ "body",
7480
+ "markup",
7481
+ "state",
7482
+ "q",
7483
+ "kinds",
7484
+ "limit",
7485
+ "offset",
7486
+ "reopen",
7487
+ "by",
7488
+ "self",
7489
+ "interval",
7490
+ "timeout"
7491
+ ];
7492
+ function requireId(id, action) {
7493
+ if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
7494
+ return id;
7495
+ }
7496
+ async function buildContext(parsed, deps) {
7497
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
7498
+ const doFetch = deps.fetch ?? fetch;
7499
+ const out = deps.stdout ?? console;
7500
+ const token = await getDeveloperToken(
7501
+ cfg,
7502
+ {
7503
+ configPath: cfg.configPath,
7504
+ token: stringOpt(parsed.options.token),
7505
+ email: stringOpt(parsed.options.email),
7506
+ open: false
7507
+ },
7508
+ doFetch,
7509
+ out
7510
+ );
7511
+ return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
7512
+ }
7513
+ async function discussCommand(parsed, deps = {}) {
7514
+ assertArgs(parsed, ALLOWED, 3);
7515
+ const action = parsed.positionals[1];
7516
+ const id = parsed.positionals[2];
7517
+ if (!action) throw new Error('"discuss" needs an action. Run "odla-ai help".');
7518
+ const ctx = await buildContext(parsed, deps);
7519
+ switch (action) {
7520
+ case "groups":
7521
+ return discussGroups(ctx);
7522
+ case "list":
7523
+ case "topics":
7524
+ return discussList(ctx, parsed);
7525
+ case "read":
7526
+ return discussRead(ctx, requireId(id, "read"));
7527
+ case "post":
7528
+ return discussPost(ctx, parsed);
7529
+ case "reply":
7530
+ return discussReply(ctx, requireId(id, "reply"), parsed);
7531
+ case "resolve":
7532
+ return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true);
7533
+ case "who":
7534
+ return discussWho(ctx, parsed);
7535
+ case "watch": {
7536
+ const result = await discussWatch(ctx, id, parsed);
7537
+ if (!result.found) throw new WatchTimeoutError();
7538
+ return;
7539
+ }
7540
+ default:
7541
+ throw new Error(`unknown discuss action "${action}". Run "odla-ai help".`);
7542
+ }
7543
+ }
7544
+
7247
7545
  // src/pm-actions.ts
7248
7546
  var FIELD_MAP = {
7249
7547
  title: { key: "title" },
@@ -7307,7 +7605,7 @@ function statusCol(entity, r) {
7307
7605
  function printRecord(ctx, entity, r) {
7308
7606
  ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
7309
7607
  }
7310
- function emit(ctx, value, human) {
7608
+ function emit2(ctx, value, human) {
7311
7609
  if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
7312
7610
  else human();
7313
7611
  }
@@ -7333,7 +7631,7 @@ async function pmList(ctx, entity, parsed) {
7333
7631
  }
7334
7632
  const qs = q.toString();
7335
7633
  const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
7336
- emit(ctx, page, () => {
7634
+ emit2(ctx, page, () => {
7337
7635
  ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
7338
7636
  ctx.out.log("id state app title");
7339
7637
  for (const r of page.records) printRecord(ctx, entity, r);
@@ -7347,25 +7645,25 @@ async function pmAdd(ctx, entity, parsed) {
7347
7645
  if (entity === "bug" && !input.description)
7348
7646
  throw new Error("pm bug add needs --description <context> (or --body <context>)");
7349
7647
  const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
7350
- emit(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7648
+ emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7351
7649
  }
7352
7650
  async function pmGet(ctx, entity, id) {
7353
7651
  const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
7354
- emit(ctx, record5, () => printRecord(ctx, entity, record5));
7652
+ emit2(ctx, record5, () => printRecord(ctx, entity, record5));
7355
7653
  }
7356
7654
  async function pmSet(ctx, entity, id, parsed) {
7357
7655
  const patch2 = collectEntityFields(entity, parsed, true);
7358
7656
  if (Object.keys(patch2).length === 0)
7359
7657
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
7360
7658
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7361
- emit(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
7659
+ emit2(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
7362
7660
  }
7363
7661
  async function pmDone(ctx, entity, id, parsed) {
7364
7662
  const decisionId = stringOpt(parsed.options.decision);
7365
7663
  if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
7366
7664
  const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
7367
7665
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7368
- emit(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7666
+ emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7369
7667
  }
7370
7668
  async function allRecords(ctx, entity, appId) {
7371
7669
  const records = [];
@@ -7398,7 +7696,7 @@ async function pmHandoff(ctx, parsed) {
7398
7696
  ...handoff,
7399
7697
  clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
7400
7698
  };
7401
- emit(ctx, result, () => {
7699
+ emit2(ctx, result, () => {
7402
7700
  if (result.clean) {
7403
7701
  ctx.out.log(`${appId}: no unresolved PM work`);
7404
7702
  return;
@@ -7431,7 +7729,7 @@ async function pmComments(ctx, entity, id) {
7431
7729
  "GET",
7432
7730
  `/${entity}/${encodeURIComponent(id)}/comments`
7433
7731
  );
7434
- emit(ctx, messages, () => {
7732
+ emit2(ctx, messages, () => {
7435
7733
  if (messages.length === 0) ctx.out.log("(no comments)");
7436
7734
  else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
7437
7735
  });
@@ -7450,7 +7748,7 @@ var ALIASES = {
7450
7748
  decision: "decision",
7451
7749
  bug: "bug"
7452
7750
  };
7453
- var ALLOWED = [
7751
+ var ALLOWED2 = [
7454
7752
  "config",
7455
7753
  "token",
7456
7754
  "email",
@@ -7474,11 +7772,11 @@ var ALLOWED = [
7474
7772
  "offset",
7475
7773
  "q"
7476
7774
  ];
7477
- function requireId(id, action) {
7775
+ function requireId2(id, action) {
7478
7776
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
7479
7777
  return id;
7480
7778
  }
7481
- async function buildContext(parsed, deps) {
7779
+ async function buildContext2(parsed, deps) {
7482
7780
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
7483
7781
  const doFetch = deps.fetch ?? fetch;
7484
7782
  const out = deps.stdout ?? console;
@@ -7493,14 +7791,14 @@ async function buildContext(parsed, deps) {
7493
7791
  async function pmCommand(parsed, deps = {}) {
7494
7792
  const word = parsed.positionals[1] ?? "";
7495
7793
  if (word === "handoff") {
7496
- assertArgs(parsed, ALLOWED, 2);
7497
- return pmHandoff(await buildContext(parsed, deps), parsed);
7794
+ assertArgs(parsed, ALLOWED2, 2);
7795
+ return pmHandoff(await buildContext2(parsed, deps), parsed);
7498
7796
  }
7499
7797
  const entity = ALIASES[word];
7500
7798
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
7501
7799
  const action = parsed.positionals[2] ?? "list";
7502
- assertArgs(parsed, ALLOWED, 4);
7503
- const ctx = await buildContext(parsed, deps);
7800
+ assertArgs(parsed, ALLOWED2, 4);
7801
+ const ctx = await buildContext2(parsed, deps);
7504
7802
  const id = parsed.positionals[3];
7505
7803
  switch (action) {
7506
7804
  case "list":
@@ -7509,21 +7807,21 @@ async function pmCommand(parsed, deps = {}) {
7509
7807
  case "create":
7510
7808
  return pmAdd(ctx, entity, parsed);
7511
7809
  case "get":
7512
- return pmGet(ctx, entity, requireId(id, action));
7810
+ return pmGet(ctx, entity, requireId2(id, action));
7513
7811
  case "set":
7514
7812
  case "update":
7515
7813
  case "status":
7516
7814
  case "move":
7517
- return pmSet(ctx, entity, requireId(id, action), parsed);
7815
+ return pmSet(ctx, entity, requireId2(id, action), parsed);
7518
7816
  case "done":
7519
- return pmDone(ctx, entity, requireId(id, action), parsed);
7817
+ return pmDone(ctx, entity, requireId2(id, action), parsed);
7520
7818
  case "comment":
7521
- return pmComment(ctx, entity, requireId(id, action), parsed);
7819
+ return pmComment(ctx, entity, requireId2(id, action), parsed);
7522
7820
  case "comments":
7523
- return pmComments(ctx, entity, requireId(id, action));
7821
+ return pmComments(ctx, entity, requireId2(id, action));
7524
7822
  case "rm":
7525
7823
  case "delete":
7526
- return pmRemove(ctx, entity, requireId(id, action));
7824
+ return pmRemove(ctx, entity, requireId2(id, action));
7527
7825
  default:
7528
7826
  throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
7529
7827
  }
@@ -7989,6 +8287,18 @@ var COMMAND_SURFACE = {
7989
8287
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
7990
8288
  capabilities: {},
7991
8289
  code: { connect: {} },
8290
+ // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
8291
+ discuss: {
8292
+ groups: {},
8293
+ list: {},
8294
+ topics: {},
8295
+ read: {},
8296
+ post: {},
8297
+ reply: {},
8298
+ resolve: {},
8299
+ who: {},
8300
+ watch: {}
8301
+ },
7992
8302
  doctor: {},
7993
8303
  help: {},
7994
8304
  init: {},
@@ -8585,7 +8895,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
8585
8895
  return out;
8586
8896
  }
8587
8897
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
8588
- function report2(ctx, impacts) {
8898
+ function report3(ctx, impacts) {
8589
8899
  const covered = impacts.filter((i) => i.runbooks.length);
8590
8900
  ctx.out.log(
8591
8901
  `${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.`
@@ -8623,7 +8933,7 @@ async function runbookImpact(ctx, options, deps = {}) {
8623
8933
  }
8624
8934
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
8625
8935
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
8626
- report2(ctx, impacts);
8936
+ report3(ctx, impacts);
8627
8937
  }
8628
8938
 
8629
8939
  // src/runbook-lint.ts
@@ -8856,7 +9166,7 @@ async function whoamiCommand(parsed, deps = {}) {
8856
9166
  }
8857
9167
 
8858
9168
  // src/runbook-command.ts
8859
- var ALLOWED2 = [
9169
+ var ALLOWED3 = [
8860
9170
  "config",
8861
9171
  "token",
8862
9172
  "email",
@@ -8896,7 +9206,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
8896
9206
  }
8897
9207
  return loadProjectConfig(configPath);
8898
9208
  }
8899
- async function buildContext2(parsed, deps, action) {
9209
+ async function buildContext3(parsed, deps, action) {
8900
9210
  const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
8901
9211
  const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
8902
9212
  const doFetch = deps.fetch ?? fetch;
@@ -8952,8 +9262,8 @@ async function buildContext2(parsed, deps, action) {
8952
9262
  }
8953
9263
  async function runbookCommand(parsed, deps = {}) {
8954
9264
  const action = parsed.positionals[1] ?? "list";
8955
- assertArgs(parsed, ALLOWED2, action === "ask" || action === "search" ? 64 : 4);
8956
- const ctx = await buildContext2(parsed, deps, action);
9265
+ assertArgs(parsed, ALLOWED3, action === "ask" || action === "search" ? 64 : 4);
9266
+ const ctx = await buildContext3(parsed, deps, action);
8957
9267
  const slug = parsed.positionals[2];
8958
9268
  switch (action) {
8959
9269
  case "list":
@@ -9144,31 +9454,31 @@ function printHostedJob(out, job, platform, appId) {
9144
9454
  url.searchParams.set("job", job.jobId);
9145
9455
  out.log(` Studio: ${url.toString()}`);
9146
9456
  }
9147
- function printHostedReport(out, report3) {
9148
- out.log(`security report ${report3.jobId}: ${report3.repository}@${report3.revision}`);
9149
- 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}`);
9150
- out.log(` findings: confirmed=${report3.metrics.confirmed} needs_reproduction=${report3.metrics.needsReproduction} candidates=${report3.metrics.candidates} rejected=${report3.metrics.rejected}`);
9151
- out.log(` discovery: ${report3.provenance.discovery?.provider ?? "unknown"}/${report3.provenance.discovery?.model ?? "unknown"}`);
9152
- out.log(` validation: ${report3.provenance.validation?.provider ?? "unknown"}/${report3.provenance.validation?.model ?? "unknown"} independent=${String(report3.provenance.independentValidation)}`);
9153
- for (const finding of report3.findings) {
9457
+ function printHostedReport(out, report4) {
9458
+ out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
9459
+ 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}`);
9460
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
9461
+ out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
9462
+ out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
9463
+ for (const finding of report4.findings) {
9154
9464
  const location = finding.locations[0];
9155
9465
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
9156
9466
  }
9157
- for (const limitation of report3.limitations) out.log(` limitation: ${limitation}`);
9467
+ for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
9158
9468
  }
9159
- function enforceHostedReportGate(report3, parsed, out, emitSuccess) {
9469
+ function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
9160
9470
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9161
9471
  const candidateValue = parsed.options["fail-on-candidates"];
9162
9472
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9163
9473
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
9164
- const confirmed = report3.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
9165
- const leads = failOnCandidates ? report3.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
9166
- const incomplete = report3.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
9474
+ const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
9475
+ const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
9476
+ const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
9167
9477
  if (confirmed.length || leads.length || incomplete) {
9168
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report3.coverageStatus}` : ""}`);
9478
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
9169
9479
  }
9170
9480
  if (emitSuccess) {
9171
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report3.coverageStatus}. This is not proof that the application is secure.`);
9481
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
9172
9482
  }
9173
9483
  }
9174
9484
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -9251,17 +9561,17 @@ async function runHostedSecurity(options) {
9251
9561
  allowNetwork: false
9252
9562
  }
9253
9563
  });
9254
- const report3 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
9255
- await (0, import_node3.writeSecurityArtifacts)(output, report3);
9256
- const reportDigest = await (0, import_security.securityFingerprint)(report3);
9564
+ const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
9565
+ await (0, import_node3.writeSecurityArtifacts)(output, report4);
9566
+ const reportDigest = await (0, import_security.securityFingerprint)(report4);
9257
9567
  await hosted.complete({
9258
9568
  reportDigest,
9259
- coverageStatus: report3.coverageStatus,
9260
- confirmed: report3.metrics.confirmed,
9261
- candidates: report3.metrics.candidates
9569
+ coverageStatus: report4.coverageStatus,
9570
+ confirmed: report4.metrics.confirmed,
9571
+ candidates: report4.metrics.candidates
9262
9572
  }, { signal: options.signal });
9263
- printSummary(options.stdout ?? console, appId, env, hosted.run, report3, output);
9264
- return Object.freeze({ report: report3, run: hosted.run, output });
9573
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
9574
+ return Object.freeze({ report: report4, run: hosted.run, output });
9265
9575
  }
9266
9576
  function selectEnv(requested, declared, configPath, rootDir) {
9267
9577
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -9271,11 +9581,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
9271
9581
  }
9272
9582
  return env;
9273
9583
  }
9274
- async function injectedToken(options, request) {
9275
- const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
9584
+ async function injectedToken(options, request2) {
9585
+ const value = options.token ?? await options.getToken?.(Object.freeze({ ...request2 }));
9276
9586
  if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
9277
9587
  throw new Error(
9278
- request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
9588
+ request2.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
9279
9589
  );
9280
9590
  }
9281
9591
  return value;
@@ -9287,14 +9597,14 @@ function profileFor(name, maxHuntTasks) {
9287
9597
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
9288
9598
  return { ...profile, maxHuntTasks };
9289
9599
  }
9290
- function printSummary(out, appId, env, run, report3, output) {
9291
- const complete = report3.coverage.filter((cell) => cell.state === "complete").length;
9600
+ function printSummary(out, appId, env, run, report4, output) {
9601
+ const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
9292
9602
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
9293
9603
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
9294
9604
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
9295
- 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}`);
9296
- if (report3.callBudget) out.log(` calls: discovery=${formatBudget(report3.callBudget.discovery)} validation=${formatBudget(report3.callBudget.validation)}`);
9297
- out.log(` findings: confirmed=${report3.metrics.confirmed} needs_reproduction=${report3.metrics.needsReproduction} candidates=${report3.metrics.candidates}`);
9605
+ 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}`);
9606
+ if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
9607
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
9298
9608
  out.log(` report: ${(0, import_node_path15.resolve)(output, "REPORT.md")}`);
9299
9609
  }
9300
9610
  function formatBudget(usage) {
@@ -9541,13 +9851,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
9541
9851
  }
9542
9852
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
9543
9853
  }
9544
- const report3 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9854
+ const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9545
9855
  if (parsed.options.json === true) {
9546
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report3 }, null, 2));
9856
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
9547
9857
  } else {
9548
- printHostedReport(context.stdout, report3);
9858
+ printHostedReport(context.stdout, report4);
9549
9859
  }
9550
- enforceHostedReportGate(report3, parsed, context.stdout, parsed.options.json !== true);
9860
+ enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
9551
9861
  }
9552
9862
  async function runLocalSecurityCommand(parsed, dependencies) {
9553
9863
  if (parsed.options.source === true) {
@@ -9588,11 +9898,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9588
9898
  sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
9589
9899
  fetch: doFetch,
9590
9900
  stdout: out,
9591
- getToken: async (request) => {
9592
- if (request.scope === "platform:security:self") {
9901
+ getToken: async (request2) => {
9902
+ if (request2.scope === "platform:security:self") {
9593
9903
  return getScopedPlatformToken({
9594
- platform: request.platform,
9595
- scope: request.scope,
9904
+ platform: request2.platform,
9905
+ scope: request2.scope,
9596
9906
  email: stringOpt(parsed.options.email),
9597
9907
  open,
9598
9908
  fetch: doFetch,
@@ -9601,7 +9911,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9601
9911
  });
9602
9912
  }
9603
9913
  const cfg = await loadProjectConfig(configPath);
9604
- if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
9914
+ if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
9605
9915
  throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
9606
9916
  }
9607
9917
  return getDeveloperToken(
@@ -9614,13 +9924,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9614
9924
  });
9615
9925
  enforceLocalGate(result.report, parsed);
9616
9926
  }
9617
- function enforceLocalGate(report3, parsed) {
9927
+ function enforceLocalGate(report4, parsed) {
9618
9928
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9619
9929
  const candidateValue = parsed.options["fail-on-candidates"];
9620
9930
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9621
- const confirmed = (0, import_security2.findingsAtOrAbove)(report3, failOn);
9622
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report3, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
9623
- const incomplete = report3.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
9931
+ const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
9932
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
9933
+ const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
9624
9934
  if (confirmed.length || leads.length || incomplete) {
9625
9935
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
9626
9936
  }
@@ -9659,9 +9969,9 @@ async function securityCommand(parsed, dependencies) {
9659
9969
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
9660
9970
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
9661
9971
  const context = await hostedSecurityContext(parsed, dependencies);
9662
- const report3 = await getHostedSecurityReport({ ...context, jobId });
9663
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report3, null, 2));
9664
- else printHostedReport(context.stdout, report3);
9972
+ const report4 = await getHostedSecurityReport({ ...context, jobId });
9973
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
9974
+ else printHostedReport(context.stdout, report4);
9665
9975
  return;
9666
9976
  }
9667
9977
  if (sub !== "run") {
@@ -9745,7 +10055,8 @@ async function securityStatus(parsed, dependencies) {
9745
10055
 
9746
10056
  // src/cli.ts
9747
10057
  function exitCodeFor(err) {
9748
- return err?.code === "handshake_pending" ? 75 : 1;
10058
+ const code = err?.code;
10059
+ return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
9749
10060
  }
9750
10061
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9751
10062
  const parsed = parseArgv(argv);
@@ -9793,6 +10104,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9793
10104
  await pmCommand(parsed, dependencies);
9794
10105
  return;
9795
10106
  }
10107
+ if (command === "discuss") {
10108
+ await discussCommand(parsed, dependencies);
10109
+ return;
10110
+ }
9796
10111
  if (command === "provision") {
9797
10112
  await provisionCommand(parsed, dependencies);
9798
10113
  return;