@odla-ai/cli 0.23.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
  }
@@ -6426,13 +6426,22 @@ Usage:
6426
6426
  odla-ai app owners add <email> [--email <odla-account>] [--json]
6427
6427
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
6428
6428
  odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
6429
- odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--body <text>|--proof <text>]
6429
+ odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
6430
6430
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
6431
- odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>]
6432
- odla-ai pm <goal|task|decision|bug> done <id>
6431
+ odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
6432
+ odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
6433
6433
  odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
6434
6434
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
6435
6435
  odla-ai pm <goal|task|decision|bug> rm <id>
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]
6436
6445
  odla-ai whoami [--json]
6437
6446
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6438
6447
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -6537,6 +6546,13 @@ Commands:
6537
6546
  "app". Entities: goal (alias conformance), task (alias kanban),
6538
6547
  decision, bug. Status changes and comments post to each item's
6539
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.
6540
6556
  provision Register services, compose integrations, persist credentials, optionally push secrets.
6541
6557
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6542
6558
  skill Same installer; --agent accepts all, claude, codex, cursor,
@@ -6724,15 +6740,15 @@ async function connectGitHubSecuritySource(options) {
6724
6740
  const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
6725
6741
  const wait2 = options.wait ?? waitForHostedPoll;
6726
6742
  while (true) {
6727
- const state = await requestHostedSecurityJson(
6743
+ const state2 = await requestHostedSecurityJson(
6728
6744
  options,
6729
6745
  `/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
6730
6746
  {},
6731
6747
  "check GitHub connection"
6732
6748
  );
6733
- if (state.status !== "pending") {
6734
- if (state.status === "connected") return state;
6735
- 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}` : ""}`);
6736
6752
  }
6737
6753
  if (now() >= deadline) throw new Error("GitHub connection approval timed out");
6738
6754
  await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
@@ -7183,6 +7199,289 @@ async function codeCommand(parsed, dependencies) {
7183
7199
  });
7184
7200
  }
7185
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
+
7186
7485
  // src/pm-actions.ts
7187
7486
  var FIELD_MAP = {
7188
7487
  title: { key: "title" },
@@ -7191,6 +7490,7 @@ var FIELD_MAP = {
7191
7490
  column: { key: "column" },
7192
7491
  rank: { key: "rank", num: true },
7193
7492
  goal: { key: "goalId" },
7493
+ decision: { key: "decisionId" },
7194
7494
  assignee: { key: "assigneeId" },
7195
7495
  due: { key: "dueAt", num: true },
7196
7496
  proof: { key: "proof" },
@@ -7229,6 +7529,14 @@ function collectFields(parsed, allowClear) {
7229
7529
  }
7230
7530
  return out;
7231
7531
  }
7532
+ function collectEntityFields(entity, parsed, allowClear) {
7533
+ const fields = collectFields(parsed, allowClear);
7534
+ if ((entity === "task" || entity === "bug") && fields.body !== void 0) {
7535
+ if (fields.description === void 0) fields.description = fields.body;
7536
+ delete fields.body;
7537
+ }
7538
+ return fields;
7539
+ }
7232
7540
  function statusCol(entity, r) {
7233
7541
  if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
7234
7542
  if (entity === "task") return String(r.column ?? "");
@@ -7237,13 +7545,20 @@ function statusCol(entity, r) {
7237
7545
  function printRecord(ctx, entity, r) {
7238
7546
  ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
7239
7547
  }
7240
- function emit(ctx, value, human) {
7548
+ function emit2(ctx, value, human) {
7241
7549
  if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
7242
7550
  else human();
7243
7551
  }
7244
7552
  async function pmList(ctx, entity, parsed) {
7245
7553
  const q = new URLSearchParams();
7246
- const filters = { status: "status", severity: "severity", column: "column", goal: "goalId", assignee: "assigneeId" };
7554
+ const filters = {
7555
+ status: "status",
7556
+ severity: "severity",
7557
+ column: "column",
7558
+ goal: "goalId",
7559
+ assignee: "assigneeId",
7560
+ decision: "decisionId"
7561
+ };
7247
7562
  const app = stringOpt(parsed.options.app);
7248
7563
  if (app) q.set("app", app);
7249
7564
  for (const [flag, param] of Object.entries(filters)) {
@@ -7256,7 +7571,7 @@ async function pmList(ctx, entity, parsed) {
7256
7571
  }
7257
7572
  const qs = q.toString();
7258
7573
  const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
7259
- emit(ctx, page, () => {
7574
+ emit2(ctx, page, () => {
7260
7575
  ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
7261
7576
  ctx.out.log("id state app title");
7262
7577
  for (const r of page.records) printRecord(ctx, entity, r);
@@ -7265,25 +7580,82 @@ async function pmList(ctx, entity, parsed) {
7265
7580
  async function pmAdd(ctx, entity, parsed) {
7266
7581
  const appId = stringOpt(parsed.options.app);
7267
7582
  if (!appId) throw new Error("pm add needs --app <appId>");
7268
- const input = collectFields(parsed, false);
7583
+ const input = collectEntityFields(entity, parsed, false);
7269
7584
  if (!input.title) throw new Error("pm add needs --title <title>");
7585
+ if (entity === "bug" && !input.description)
7586
+ throw new Error("pm bug add needs --description <context> (or --body <context>)");
7270
7587
  const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
7271
- emit(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7588
+ emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7272
7589
  }
7273
7590
  async function pmGet(ctx, entity, id) {
7274
7591
  const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
7275
- emit(ctx, record5, () => printRecord(ctx, entity, record5));
7592
+ emit2(ctx, record5, () => printRecord(ctx, entity, record5));
7276
7593
  }
7277
7594
  async function pmSet(ctx, entity, id, parsed) {
7278
- const patch2 = collectFields(parsed, true);
7595
+ const patch2 = collectEntityFields(entity, parsed, true);
7279
7596
  if (Object.keys(patch2).length === 0)
7280
7597
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
7281
7598
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7282
- 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}`));
7283
7600
  }
7284
- async function pmDone(ctx, entity, id) {
7285
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: DONE[entity] });
7286
- emit(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7601
+ async function pmDone(ctx, entity, id, parsed) {
7602
+ const decisionId = stringOpt(parsed.options.decision);
7603
+ if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
7604
+ const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
7605
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7606
+ emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7607
+ }
7608
+ async function allRecords(ctx, entity, appId) {
7609
+ const records = [];
7610
+ for (; ; ) {
7611
+ const q = new URLSearchParams({
7612
+ app: appId,
7613
+ limit: "100",
7614
+ offset: String(records.length)
7615
+ });
7616
+ const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
7617
+ records.push(...page.records);
7618
+ if (records.length >= page.total || page.records.length === 0) return records;
7619
+ }
7620
+ }
7621
+ async function pmHandoff(ctx, parsed) {
7622
+ const appId = stringOpt(parsed.options.app);
7623
+ if (!appId) throw new Error("pm handoff needs --app <appId>");
7624
+ const [goals, tasks, bugs] = await Promise.all([
7625
+ allRecords(ctx, "goal", appId),
7626
+ allRecords(ctx, "task", appId),
7627
+ allRecords(ctx, "bug", appId)
7628
+ ]);
7629
+ const handoff = {
7630
+ appId,
7631
+ unmetGoals: goals.filter((record5) => record5.status !== "met"),
7632
+ activeTasks: tasks.filter((record5) => record5.column !== "done"),
7633
+ openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
7634
+ };
7635
+ const result = {
7636
+ ...handoff,
7637
+ clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
7638
+ };
7639
+ emit2(ctx, result, () => {
7640
+ if (result.clean) {
7641
+ ctx.out.log(`${appId}: no unresolved PM work`);
7642
+ return;
7643
+ }
7644
+ ctx.out.log(`${appId}: authoritative PM handoff`);
7645
+ for (const [label, records] of [
7646
+ ["unmet goals", result.unmetGoals],
7647
+ ["active tasks", result.activeTasks],
7648
+ ["open bugs", result.openBugs]
7649
+ ]) {
7650
+ ctx.out.log(`${label}:`);
7651
+ if (!records.length) ctx.out.log("- (none)");
7652
+ else for (const record5 of records) printRecord(
7653
+ ctx,
7654
+ label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
7655
+ record5
7656
+ );
7657
+ }
7658
+ });
7287
7659
  }
7288
7660
  async function pmComment(ctx, entity, id, parsed) {
7289
7661
  const body = stringOpt(parsed.options.body);
@@ -7297,7 +7669,7 @@ async function pmComments(ctx, entity, id) {
7297
7669
  "GET",
7298
7670
  `/${entity}/${encodeURIComponent(id)}/comments`
7299
7671
  );
7300
- emit(ctx, messages, () => {
7672
+ emit2(ctx, messages, () => {
7301
7673
  if (messages.length === 0) ctx.out.log("(no comments)");
7302
7674
  else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
7303
7675
  });
@@ -7316,7 +7688,7 @@ var ALIASES = {
7316
7688
  decision: "decision",
7317
7689
  bug: "bug"
7318
7690
  };
7319
- var ALLOWED = [
7691
+ var ALLOWED2 = [
7320
7692
  "config",
7321
7693
  "token",
7322
7694
  "email",
@@ -7335,15 +7707,16 @@ var ALLOWED = [
7335
7707
  "target",
7336
7708
  "description",
7337
7709
  "desc",
7710
+ "decision",
7338
7711
  "limit",
7339
7712
  "offset",
7340
7713
  "q"
7341
7714
  ];
7342
- function requireId(id, action) {
7715
+ function requireId2(id, action) {
7343
7716
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
7344
7717
  return id;
7345
7718
  }
7346
- async function buildContext(parsed, deps) {
7719
+ async function buildContext2(parsed, deps) {
7347
7720
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
7348
7721
  const doFetch = deps.fetch ?? fetch;
7349
7722
  const out = deps.stdout ?? console;
@@ -7357,11 +7730,15 @@ async function buildContext(parsed, deps) {
7357
7730
  }
7358
7731
  async function pmCommand(parsed, deps = {}) {
7359
7732
  const word = parsed.positionals[1] ?? "";
7733
+ if (word === "handoff") {
7734
+ assertArgs(parsed, ALLOWED2, 2);
7735
+ return pmHandoff(await buildContext2(parsed, deps), parsed);
7736
+ }
7360
7737
  const entity = ALIASES[word];
7361
7738
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
7362
7739
  const action = parsed.positionals[2] ?? "list";
7363
- assertArgs(parsed, ALLOWED, 4);
7364
- const ctx = await buildContext(parsed, deps);
7740
+ assertArgs(parsed, ALLOWED2, 4);
7741
+ const ctx = await buildContext2(parsed, deps);
7365
7742
  const id = parsed.positionals[3];
7366
7743
  switch (action) {
7367
7744
  case "list":
@@ -7370,21 +7747,21 @@ async function pmCommand(parsed, deps = {}) {
7370
7747
  case "create":
7371
7748
  return pmAdd(ctx, entity, parsed);
7372
7749
  case "get":
7373
- return pmGet(ctx, entity, requireId(id, action));
7750
+ return pmGet(ctx, entity, requireId2(id, action));
7374
7751
  case "set":
7375
7752
  case "update":
7376
7753
  case "status":
7377
7754
  case "move":
7378
- return pmSet(ctx, entity, requireId(id, action), parsed);
7755
+ return pmSet(ctx, entity, requireId2(id, action), parsed);
7379
7756
  case "done":
7380
- return pmDone(ctx, entity, requireId(id, action));
7757
+ return pmDone(ctx, entity, requireId2(id, action), parsed);
7381
7758
  case "comment":
7382
- return pmComment(ctx, entity, requireId(id, action), parsed);
7759
+ return pmComment(ctx, entity, requireId2(id, action), parsed);
7383
7760
  case "comments":
7384
- return pmComments(ctx, entity, requireId(id, action));
7761
+ return pmComments(ctx, entity, requireId2(id, action));
7385
7762
  case "rm":
7386
7763
  case "delete":
7387
- return pmRemove(ctx, entity, requireId(id, action));
7764
+ return pmRemove(ctx, entity, requireId2(id, action));
7388
7765
  default:
7389
7766
  throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
7390
7767
  }
@@ -7850,10 +8227,25 @@ var COMMAND_SURFACE = {
7850
8227
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
7851
8228
  capabilities: {},
7852
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
+ },
7853
8242
  doctor: {},
7854
8243
  help: {},
7855
8244
  init: {},
7856
- pm: PM_ENTITIES,
8245
+ pm: {
8246
+ ...PM_ENTITIES,
8247
+ handoff: {}
8248
+ },
7857
8249
  provision: {},
7858
8250
  runbook: {
7859
8251
  ask: {},
@@ -8434,7 +8826,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
8434
8826
  return out;
8435
8827
  }
8436
8828
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
8437
- function report2(ctx, impacts) {
8829
+ function report3(ctx, impacts) {
8438
8830
  const covered = impacts.filter((i) => i.runbooks.length);
8439
8831
  ctx.out.log(
8440
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.`
@@ -8472,7 +8864,7 @@ async function runbookImpact(ctx, options, deps = {}) {
8472
8864
  }
8473
8865
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
8474
8866
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
8475
- report2(ctx, impacts);
8867
+ report3(ctx, impacts);
8476
8868
  }
8477
8869
 
8478
8870
  // src/runbook-lint.ts
@@ -8705,7 +9097,7 @@ async function whoamiCommand(parsed, deps = {}) {
8705
9097
  }
8706
9098
 
8707
9099
  // src/runbook-command.ts
8708
- var ALLOWED2 = [
9100
+ var ALLOWED3 = [
8709
9101
  "config",
8710
9102
  "token",
8711
9103
  "email",
@@ -8745,7 +9137,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
8745
9137
  }
8746
9138
  return loadProjectConfig(configPath);
8747
9139
  }
8748
- async function buildContext2(parsed, deps, action) {
9140
+ async function buildContext3(parsed, deps, action) {
8749
9141
  const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
8750
9142
  const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
8751
9143
  const doFetch = deps.fetch ?? fetch;
@@ -8801,8 +9193,8 @@ async function buildContext2(parsed, deps, action) {
8801
9193
  }
8802
9194
  async function runbookCommand(parsed, deps = {}) {
8803
9195
  const action = parsed.positionals[1] ?? "list";
8804
- assertArgs(parsed, ALLOWED2, action === "ask" || action === "search" ? 64 : 4);
8805
- 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);
8806
9198
  const slug = parsed.positionals[2];
8807
9199
  switch (action) {
8808
9200
  case "list":
@@ -8993,31 +9385,31 @@ function printHostedJob(out, job, platform, appId) {
8993
9385
  url.searchParams.set("job", job.jobId);
8994
9386
  out.log(` Studio: ${url.toString()}`);
8995
9387
  }
8996
- function printHostedReport(out, report3) {
8997
- out.log(`security report ${report3.jobId}: ${report3.repository}@${report3.revision}`);
8998
- 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}`);
8999
- out.log(` findings: confirmed=${report3.metrics.confirmed} needs_reproduction=${report3.metrics.needsReproduction} candidates=${report3.metrics.candidates} rejected=${report3.metrics.rejected}`);
9000
- out.log(` discovery: ${report3.provenance.discovery?.provider ?? "unknown"}/${report3.provenance.discovery?.model ?? "unknown"}`);
9001
- out.log(` validation: ${report3.provenance.validation?.provider ?? "unknown"}/${report3.provenance.validation?.model ?? "unknown"} independent=${String(report3.provenance.independentValidation)}`);
9002
- 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) {
9003
9395
  const location = finding.locations[0];
9004
9396
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
9005
9397
  }
9006
- for (const limitation of report3.limitations) out.log(` limitation: ${limitation}`);
9398
+ for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
9007
9399
  }
9008
- function enforceHostedReportGate(report3, parsed, out, emitSuccess) {
9400
+ function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
9009
9401
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9010
9402
  const candidateValue = parsed.options["fail-on-candidates"];
9011
9403
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9012
9404
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
9013
- const confirmed = report3.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
9014
- const leads = failOnCandidates ? report3.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
9015
- 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;
9016
9408
  if (confirmed.length || leads.length || incomplete) {
9017
- 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}` : ""}`);
9018
9410
  }
9019
9411
  if (emitSuccess) {
9020
- 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.`);
9021
9413
  }
9022
9414
  }
9023
9415
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -9100,17 +9492,17 @@ async function runHostedSecurity(options) {
9100
9492
  allowNetwork: false
9101
9493
  }
9102
9494
  });
9103
- const report3 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
9104
- await (0, import_node3.writeSecurityArtifacts)(output, report3);
9105
- 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);
9106
9498
  await hosted.complete({
9107
9499
  reportDigest,
9108
- coverageStatus: report3.coverageStatus,
9109
- confirmed: report3.metrics.confirmed,
9110
- candidates: report3.metrics.candidates
9500
+ coverageStatus: report4.coverageStatus,
9501
+ confirmed: report4.metrics.confirmed,
9502
+ candidates: report4.metrics.candidates
9111
9503
  }, { signal: options.signal });
9112
- printSummary(options.stdout ?? console, appId, env, hosted.run, report3, output);
9113
- 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 });
9114
9506
  }
9115
9507
  function selectEnv(requested, declared, configPath, rootDir) {
9116
9508
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -9120,11 +9512,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
9120
9512
  }
9121
9513
  return env;
9122
9514
  }
9123
- async function injectedToken(options, request) {
9124
- 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 }));
9125
9517
  if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
9126
9518
  throw new Error(
9127
- 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"
9128
9520
  );
9129
9521
  }
9130
9522
  return value;
@@ -9136,14 +9528,14 @@ function profileFor(name, maxHuntTasks) {
9136
9528
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
9137
9529
  return { ...profile, maxHuntTasks };
9138
9530
  }
9139
- function printSummary(out, appId, env, run, report3, output) {
9140
- 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;
9141
9533
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
9142
9534
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
9143
9535
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
9144
- 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}`);
9145
- if (report3.callBudget) out.log(` calls: discovery=${formatBudget(report3.callBudget.discovery)} validation=${formatBudget(report3.callBudget.validation)}`);
9146
- 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}`);
9147
9539
  out.log(` report: ${(0, import_node_path15.resolve)(output, "REPORT.md")}`);
9148
9540
  }
9149
9541
  function formatBudget(usage) {
@@ -9379,13 +9771,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
9379
9771
  }
9380
9772
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
9381
9773
  }
9382
- const report3 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9774
+ const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9383
9775
  if (parsed.options.json === true) {
9384
- 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));
9385
9777
  } else {
9386
- printHostedReport(context.stdout, report3);
9778
+ printHostedReport(context.stdout, report4);
9387
9779
  }
9388
- enforceHostedReportGate(report3, parsed, context.stdout, parsed.options.json !== true);
9780
+ enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
9389
9781
  }
9390
9782
  async function runLocalSecurityCommand(parsed, dependencies) {
9391
9783
  if (parsed.options.source === true) {
@@ -9426,11 +9818,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9426
9818
  sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
9427
9819
  fetch: doFetch,
9428
9820
  stdout: out,
9429
- getToken: async (request) => {
9430
- if (request.scope === "platform:security:self") {
9821
+ getToken: async (request2) => {
9822
+ if (request2.scope === "platform:security:self") {
9431
9823
  return getScopedPlatformToken({
9432
- platform: request.platform,
9433
- scope: request.scope,
9824
+ platform: request2.platform,
9825
+ scope: request2.scope,
9434
9826
  email: stringOpt(parsed.options.email),
9435
9827
  open,
9436
9828
  fetch: doFetch,
@@ -9439,7 +9831,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9439
9831
  });
9440
9832
  }
9441
9833
  const cfg = await loadProjectConfig(configPath);
9442
- if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
9834
+ if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
9443
9835
  throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
9444
9836
  }
9445
9837
  return getDeveloperToken(
@@ -9452,13 +9844,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9452
9844
  });
9453
9845
  enforceLocalGate(result.report, parsed);
9454
9846
  }
9455
- function enforceLocalGate(report3, parsed) {
9847
+ function enforceLocalGate(report4, parsed) {
9456
9848
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9457
9849
  const candidateValue = parsed.options["fail-on-candidates"];
9458
9850
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9459
- const confirmed = (0, import_security2.findingsAtOrAbove)(report3, failOn);
9460
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report3, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
9461
- 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;
9462
9854
  if (confirmed.length || leads.length || incomplete) {
9463
9855
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
9464
9856
  }
@@ -9497,9 +9889,9 @@ async function securityCommand(parsed, dependencies) {
9497
9889
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
9498
9890
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
9499
9891
  const context = await hostedSecurityContext(parsed, dependencies);
9500
- const report3 = await getHostedSecurityReport({ ...context, jobId });
9501
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report3, null, 2));
9502
- 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);
9503
9895
  return;
9504
9896
  }
9505
9897
  if (sub !== "run") {
@@ -9583,7 +9975,8 @@ async function securityStatus(parsed, dependencies) {
9583
9975
 
9584
9976
  // src/cli.ts
9585
9977
  function exitCodeFor(err) {
9586
- return err?.code === "handshake_pending" ? 75 : 1;
9978
+ const code = err?.code;
9979
+ return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
9587
9980
  }
9588
9981
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9589
9982
  const parsed = parseArgv(argv);
@@ -9631,6 +10024,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9631
10024
  await pmCommand(parsed, dependencies);
9632
10025
  return;
9633
10026
  }
10027
+ if (command === "discuss") {
10028
+ await discussCommand(parsed, dependencies);
10029
+ return;
10030
+ }
9634
10031
  if (command === "provision") {
9635
10032
  await provisionCommand(parsed, dependencies);
9636
10033
  return;