@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/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
  }
@@ -6486,13 +6486,22 @@ Usage:
6486
6486
  odla-ai app owners add <email> [--email <odla-account>] [--json]
6487
6487
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
6488
6488
  odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
6489
- odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--body <text>|--proof <text>]
6489
+ odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
6490
6490
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
6491
- odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>]
6492
- odla-ai pm <goal|task|decision|bug> done <id>
6491
+ odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
6492
+ odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
6493
6493
  odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
6494
6494
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
6495
6495
  odla-ai pm <goal|task|decision|bug> rm <id>
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]
6496
6505
  odla-ai whoami [--json]
6497
6506
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6498
6507
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -6597,6 +6606,13 @@ Commands:
6597
6606
  "app". Entities: goal (alias conformance), task (alias kanban),
6598
6607
  decision, bug. Status changes and comments post to each item's
6599
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.
6600
6616
  provision Register services, compose integrations, persist credentials, optionally push secrets.
6601
6617
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6602
6618
  skill Same installer; --agent accepts all, claude, codex, cursor,
@@ -6784,15 +6800,15 @@ async function connectGitHubSecuritySource(options) {
6784
6800
  const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
6785
6801
  const wait2 = options.wait ?? waitForHostedPoll;
6786
6802
  while (true) {
6787
- const state = await requestHostedSecurityJson(
6803
+ const state2 = await requestHostedSecurityJson(
6788
6804
  options,
6789
6805
  `/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
6790
6806
  {},
6791
6807
  "check GitHub connection"
6792
6808
  );
6793
- if (state.status !== "pending") {
6794
- if (state.status === "connected") return state;
6795
- 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}` : ""}`);
6796
6812
  }
6797
6813
  if (now() >= deadline) throw new Error("GitHub connection approval timed out");
6798
6814
  await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
@@ -7243,6 +7259,289 @@ async function codeCommand(parsed, dependencies) {
7243
7259
  });
7244
7260
  }
7245
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
+
7246
7545
  // src/pm-actions.ts
7247
7546
  var FIELD_MAP = {
7248
7547
  title: { key: "title" },
@@ -7251,6 +7550,7 @@ var FIELD_MAP = {
7251
7550
  column: { key: "column" },
7252
7551
  rank: { key: "rank", num: true },
7253
7552
  goal: { key: "goalId" },
7553
+ decision: { key: "decisionId" },
7254
7554
  assignee: { key: "assigneeId" },
7255
7555
  due: { key: "dueAt", num: true },
7256
7556
  proof: { key: "proof" },
@@ -7289,6 +7589,14 @@ function collectFields(parsed, allowClear) {
7289
7589
  }
7290
7590
  return out;
7291
7591
  }
7592
+ function collectEntityFields(entity, parsed, allowClear) {
7593
+ const fields = collectFields(parsed, allowClear);
7594
+ if ((entity === "task" || entity === "bug") && fields.body !== void 0) {
7595
+ if (fields.description === void 0) fields.description = fields.body;
7596
+ delete fields.body;
7597
+ }
7598
+ return fields;
7599
+ }
7292
7600
  function statusCol(entity, r) {
7293
7601
  if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
7294
7602
  if (entity === "task") return String(r.column ?? "");
@@ -7297,13 +7605,20 @@ function statusCol(entity, r) {
7297
7605
  function printRecord(ctx, entity, r) {
7298
7606
  ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
7299
7607
  }
7300
- function emit(ctx, value, human) {
7608
+ function emit2(ctx, value, human) {
7301
7609
  if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
7302
7610
  else human();
7303
7611
  }
7304
7612
  async function pmList(ctx, entity, parsed) {
7305
7613
  const q = new URLSearchParams();
7306
- const filters = { status: "status", severity: "severity", column: "column", goal: "goalId", assignee: "assigneeId" };
7614
+ const filters = {
7615
+ status: "status",
7616
+ severity: "severity",
7617
+ column: "column",
7618
+ goal: "goalId",
7619
+ assignee: "assigneeId",
7620
+ decision: "decisionId"
7621
+ };
7307
7622
  const app = stringOpt(parsed.options.app);
7308
7623
  if (app) q.set("app", app);
7309
7624
  for (const [flag, param] of Object.entries(filters)) {
@@ -7316,7 +7631,7 @@ async function pmList(ctx, entity, parsed) {
7316
7631
  }
7317
7632
  const qs = q.toString();
7318
7633
  const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
7319
- emit(ctx, page, () => {
7634
+ emit2(ctx, page, () => {
7320
7635
  ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
7321
7636
  ctx.out.log("id state app title");
7322
7637
  for (const r of page.records) printRecord(ctx, entity, r);
@@ -7325,25 +7640,82 @@ async function pmList(ctx, entity, parsed) {
7325
7640
  async function pmAdd(ctx, entity, parsed) {
7326
7641
  const appId = stringOpt(parsed.options.app);
7327
7642
  if (!appId) throw new Error("pm add needs --app <appId>");
7328
- const input = collectFields(parsed, false);
7643
+ const input = collectEntityFields(entity, parsed, false);
7329
7644
  if (!input.title) throw new Error("pm add needs --title <title>");
7645
+ if (entity === "bug" && !input.description)
7646
+ throw new Error("pm bug add needs --description <context> (or --body <context>)");
7330
7647
  const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
7331
- emit(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7648
+ emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7332
7649
  }
7333
7650
  async function pmGet(ctx, entity, id) {
7334
7651
  const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
7335
- emit(ctx, record5, () => printRecord(ctx, entity, record5));
7652
+ emit2(ctx, record5, () => printRecord(ctx, entity, record5));
7336
7653
  }
7337
7654
  async function pmSet(ctx, entity, id, parsed) {
7338
- const patch2 = collectFields(parsed, true);
7655
+ const patch2 = collectEntityFields(entity, parsed, true);
7339
7656
  if (Object.keys(patch2).length === 0)
7340
7657
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
7341
7658
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7342
- 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}`));
7343
7660
  }
7344
- async function pmDone(ctx, entity, id) {
7345
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: DONE[entity] });
7346
- emit(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7661
+ async function pmDone(ctx, entity, id, parsed) {
7662
+ const decisionId = stringOpt(parsed.options.decision);
7663
+ if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
7664
+ const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
7665
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7666
+ emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7667
+ }
7668
+ async function allRecords(ctx, entity, appId) {
7669
+ const records = [];
7670
+ for (; ; ) {
7671
+ const q = new URLSearchParams({
7672
+ app: appId,
7673
+ limit: "100",
7674
+ offset: String(records.length)
7675
+ });
7676
+ const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
7677
+ records.push(...page.records);
7678
+ if (records.length >= page.total || page.records.length === 0) return records;
7679
+ }
7680
+ }
7681
+ async function pmHandoff(ctx, parsed) {
7682
+ const appId = stringOpt(parsed.options.app);
7683
+ if (!appId) throw new Error("pm handoff needs --app <appId>");
7684
+ const [goals, tasks, bugs] = await Promise.all([
7685
+ allRecords(ctx, "goal", appId),
7686
+ allRecords(ctx, "task", appId),
7687
+ allRecords(ctx, "bug", appId)
7688
+ ]);
7689
+ const handoff = {
7690
+ appId,
7691
+ unmetGoals: goals.filter((record5) => record5.status !== "met"),
7692
+ activeTasks: tasks.filter((record5) => record5.column !== "done"),
7693
+ openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
7694
+ };
7695
+ const result = {
7696
+ ...handoff,
7697
+ clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
7698
+ };
7699
+ emit2(ctx, result, () => {
7700
+ if (result.clean) {
7701
+ ctx.out.log(`${appId}: no unresolved PM work`);
7702
+ return;
7703
+ }
7704
+ ctx.out.log(`${appId}: authoritative PM handoff`);
7705
+ for (const [label, records] of [
7706
+ ["unmet goals", result.unmetGoals],
7707
+ ["active tasks", result.activeTasks],
7708
+ ["open bugs", result.openBugs]
7709
+ ]) {
7710
+ ctx.out.log(`${label}:`);
7711
+ if (!records.length) ctx.out.log("- (none)");
7712
+ else for (const record5 of records) printRecord(
7713
+ ctx,
7714
+ label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
7715
+ record5
7716
+ );
7717
+ }
7718
+ });
7347
7719
  }
7348
7720
  async function pmComment(ctx, entity, id, parsed) {
7349
7721
  const body = stringOpt(parsed.options.body);
@@ -7357,7 +7729,7 @@ async function pmComments(ctx, entity, id) {
7357
7729
  "GET",
7358
7730
  `/${entity}/${encodeURIComponent(id)}/comments`
7359
7731
  );
7360
- emit(ctx, messages, () => {
7732
+ emit2(ctx, messages, () => {
7361
7733
  if (messages.length === 0) ctx.out.log("(no comments)");
7362
7734
  else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
7363
7735
  });
@@ -7376,7 +7748,7 @@ var ALIASES = {
7376
7748
  decision: "decision",
7377
7749
  bug: "bug"
7378
7750
  };
7379
- var ALLOWED = [
7751
+ var ALLOWED2 = [
7380
7752
  "config",
7381
7753
  "token",
7382
7754
  "email",
@@ -7395,15 +7767,16 @@ var ALLOWED = [
7395
7767
  "target",
7396
7768
  "description",
7397
7769
  "desc",
7770
+ "decision",
7398
7771
  "limit",
7399
7772
  "offset",
7400
7773
  "q"
7401
7774
  ];
7402
- function requireId(id, action) {
7775
+ function requireId2(id, action) {
7403
7776
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
7404
7777
  return id;
7405
7778
  }
7406
- async function buildContext(parsed, deps) {
7779
+ async function buildContext2(parsed, deps) {
7407
7780
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
7408
7781
  const doFetch = deps.fetch ?? fetch;
7409
7782
  const out = deps.stdout ?? console;
@@ -7417,11 +7790,15 @@ async function buildContext(parsed, deps) {
7417
7790
  }
7418
7791
  async function pmCommand(parsed, deps = {}) {
7419
7792
  const word = parsed.positionals[1] ?? "";
7793
+ if (word === "handoff") {
7794
+ assertArgs(parsed, ALLOWED2, 2);
7795
+ return pmHandoff(await buildContext2(parsed, deps), parsed);
7796
+ }
7420
7797
  const entity = ALIASES[word];
7421
7798
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
7422
7799
  const action = parsed.positionals[2] ?? "list";
7423
- assertArgs(parsed, ALLOWED, 4);
7424
- const ctx = await buildContext(parsed, deps);
7800
+ assertArgs(parsed, ALLOWED2, 4);
7801
+ const ctx = await buildContext2(parsed, deps);
7425
7802
  const id = parsed.positionals[3];
7426
7803
  switch (action) {
7427
7804
  case "list":
@@ -7430,21 +7807,21 @@ async function pmCommand(parsed, deps = {}) {
7430
7807
  case "create":
7431
7808
  return pmAdd(ctx, entity, parsed);
7432
7809
  case "get":
7433
- return pmGet(ctx, entity, requireId(id, action));
7810
+ return pmGet(ctx, entity, requireId2(id, action));
7434
7811
  case "set":
7435
7812
  case "update":
7436
7813
  case "status":
7437
7814
  case "move":
7438
- return pmSet(ctx, entity, requireId(id, action), parsed);
7815
+ return pmSet(ctx, entity, requireId2(id, action), parsed);
7439
7816
  case "done":
7440
- return pmDone(ctx, entity, requireId(id, action));
7817
+ return pmDone(ctx, entity, requireId2(id, action), parsed);
7441
7818
  case "comment":
7442
- return pmComment(ctx, entity, requireId(id, action), parsed);
7819
+ return pmComment(ctx, entity, requireId2(id, action), parsed);
7443
7820
  case "comments":
7444
- return pmComments(ctx, entity, requireId(id, action));
7821
+ return pmComments(ctx, entity, requireId2(id, action));
7445
7822
  case "rm":
7446
7823
  case "delete":
7447
- return pmRemove(ctx, entity, requireId(id, action));
7824
+ return pmRemove(ctx, entity, requireId2(id, action));
7448
7825
  default:
7449
7826
  throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
7450
7827
  }
@@ -7910,10 +8287,25 @@ var COMMAND_SURFACE = {
7910
8287
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
7911
8288
  capabilities: {},
7912
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
+ },
7913
8302
  doctor: {},
7914
8303
  help: {},
7915
8304
  init: {},
7916
- pm: PM_ENTITIES,
8305
+ pm: {
8306
+ ...PM_ENTITIES,
8307
+ handoff: {}
8308
+ },
7917
8309
  provision: {},
7918
8310
  runbook: {
7919
8311
  ask: {},
@@ -8503,7 +8895,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
8503
8895
  return out;
8504
8896
  }
8505
8897
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
8506
- function report2(ctx, impacts) {
8898
+ function report3(ctx, impacts) {
8507
8899
  const covered = impacts.filter((i) => i.runbooks.length);
8508
8900
  ctx.out.log(
8509
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.`
@@ -8541,7 +8933,7 @@ async function runbookImpact(ctx, options, deps = {}) {
8541
8933
  }
8542
8934
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
8543
8935
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
8544
- report2(ctx, impacts);
8936
+ report3(ctx, impacts);
8545
8937
  }
8546
8938
 
8547
8939
  // src/runbook-lint.ts
@@ -8774,7 +9166,7 @@ async function whoamiCommand(parsed, deps = {}) {
8774
9166
  }
8775
9167
 
8776
9168
  // src/runbook-command.ts
8777
- var ALLOWED2 = [
9169
+ var ALLOWED3 = [
8778
9170
  "config",
8779
9171
  "token",
8780
9172
  "email",
@@ -8814,7 +9206,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
8814
9206
  }
8815
9207
  return loadProjectConfig(configPath);
8816
9208
  }
8817
- async function buildContext2(parsed, deps, action) {
9209
+ async function buildContext3(parsed, deps, action) {
8818
9210
  const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
8819
9211
  const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
8820
9212
  const doFetch = deps.fetch ?? fetch;
@@ -8870,8 +9262,8 @@ async function buildContext2(parsed, deps, action) {
8870
9262
  }
8871
9263
  async function runbookCommand(parsed, deps = {}) {
8872
9264
  const action = parsed.positionals[1] ?? "list";
8873
- assertArgs(parsed, ALLOWED2, action === "ask" || action === "search" ? 64 : 4);
8874
- 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);
8875
9267
  const slug = parsed.positionals[2];
8876
9268
  switch (action) {
8877
9269
  case "list":
@@ -9062,31 +9454,31 @@ function printHostedJob(out, job, platform, appId) {
9062
9454
  url.searchParams.set("job", job.jobId);
9063
9455
  out.log(` Studio: ${url.toString()}`);
9064
9456
  }
9065
- function printHostedReport(out, report3) {
9066
- out.log(`security report ${report3.jobId}: ${report3.repository}@${report3.revision}`);
9067
- 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}`);
9068
- out.log(` findings: confirmed=${report3.metrics.confirmed} needs_reproduction=${report3.metrics.needsReproduction} candidates=${report3.metrics.candidates} rejected=${report3.metrics.rejected}`);
9069
- out.log(` discovery: ${report3.provenance.discovery?.provider ?? "unknown"}/${report3.provenance.discovery?.model ?? "unknown"}`);
9070
- out.log(` validation: ${report3.provenance.validation?.provider ?? "unknown"}/${report3.provenance.validation?.model ?? "unknown"} independent=${String(report3.provenance.independentValidation)}`);
9071
- 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) {
9072
9464
  const location = finding.locations[0];
9073
9465
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
9074
9466
  }
9075
- for (const limitation of report3.limitations) out.log(` limitation: ${limitation}`);
9467
+ for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
9076
9468
  }
9077
- function enforceHostedReportGate(report3, parsed, out, emitSuccess) {
9469
+ function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
9078
9470
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9079
9471
  const candidateValue = parsed.options["fail-on-candidates"];
9080
9472
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9081
9473
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
9082
- const confirmed = report3.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
9083
- const leads = failOnCandidates ? report3.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
9084
- 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;
9085
9477
  if (confirmed.length || leads.length || incomplete) {
9086
- 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}` : ""}`);
9087
9479
  }
9088
9480
  if (emitSuccess) {
9089
- 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.`);
9090
9482
  }
9091
9483
  }
9092
9484
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -9169,17 +9561,17 @@ async function runHostedSecurity(options) {
9169
9561
  allowNetwork: false
9170
9562
  }
9171
9563
  });
9172
- const report3 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
9173
- await (0, import_node3.writeSecurityArtifacts)(output, report3);
9174
- 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);
9175
9567
  await hosted.complete({
9176
9568
  reportDigest,
9177
- coverageStatus: report3.coverageStatus,
9178
- confirmed: report3.metrics.confirmed,
9179
- candidates: report3.metrics.candidates
9569
+ coverageStatus: report4.coverageStatus,
9570
+ confirmed: report4.metrics.confirmed,
9571
+ candidates: report4.metrics.candidates
9180
9572
  }, { signal: options.signal });
9181
- printSummary(options.stdout ?? console, appId, env, hosted.run, report3, output);
9182
- 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 });
9183
9575
  }
9184
9576
  function selectEnv(requested, declared, configPath, rootDir) {
9185
9577
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -9189,11 +9581,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
9189
9581
  }
9190
9582
  return env;
9191
9583
  }
9192
- async function injectedToken(options, request) {
9193
- 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 }));
9194
9586
  if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
9195
9587
  throw new Error(
9196
- 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"
9197
9589
  );
9198
9590
  }
9199
9591
  return value;
@@ -9205,14 +9597,14 @@ function profileFor(name, maxHuntTasks) {
9205
9597
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
9206
9598
  return { ...profile, maxHuntTasks };
9207
9599
  }
9208
- function printSummary(out, appId, env, run, report3, output) {
9209
- 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;
9210
9602
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
9211
9603
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
9212
9604
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
9213
- 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}`);
9214
- if (report3.callBudget) out.log(` calls: discovery=${formatBudget(report3.callBudget.discovery)} validation=${formatBudget(report3.callBudget.validation)}`);
9215
- 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}`);
9216
9608
  out.log(` report: ${(0, import_node_path15.resolve)(output, "REPORT.md")}`);
9217
9609
  }
9218
9610
  function formatBudget(usage) {
@@ -9459,13 +9851,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
9459
9851
  }
9460
9852
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
9461
9853
  }
9462
- const report3 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9854
+ const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9463
9855
  if (parsed.options.json === true) {
9464
- 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));
9465
9857
  } else {
9466
- printHostedReport(context.stdout, report3);
9858
+ printHostedReport(context.stdout, report4);
9467
9859
  }
9468
- enforceHostedReportGate(report3, parsed, context.stdout, parsed.options.json !== true);
9860
+ enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
9469
9861
  }
9470
9862
  async function runLocalSecurityCommand(parsed, dependencies) {
9471
9863
  if (parsed.options.source === true) {
@@ -9506,11 +9898,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9506
9898
  sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
9507
9899
  fetch: doFetch,
9508
9900
  stdout: out,
9509
- getToken: async (request) => {
9510
- if (request.scope === "platform:security:self") {
9901
+ getToken: async (request2) => {
9902
+ if (request2.scope === "platform:security:self") {
9511
9903
  return getScopedPlatformToken({
9512
- platform: request.platform,
9513
- scope: request.scope,
9904
+ platform: request2.platform,
9905
+ scope: request2.scope,
9514
9906
  email: stringOpt(parsed.options.email),
9515
9907
  open,
9516
9908
  fetch: doFetch,
@@ -9519,7 +9911,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9519
9911
  });
9520
9912
  }
9521
9913
  const cfg = await loadProjectConfig(configPath);
9522
- if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
9914
+ if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
9523
9915
  throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
9524
9916
  }
9525
9917
  return getDeveloperToken(
@@ -9532,13 +9924,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9532
9924
  });
9533
9925
  enforceLocalGate(result.report, parsed);
9534
9926
  }
9535
- function enforceLocalGate(report3, parsed) {
9927
+ function enforceLocalGate(report4, parsed) {
9536
9928
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9537
9929
  const candidateValue = parsed.options["fail-on-candidates"];
9538
9930
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9539
- const confirmed = (0, import_security2.findingsAtOrAbove)(report3, failOn);
9540
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report3, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
9541
- 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;
9542
9934
  if (confirmed.length || leads.length || incomplete) {
9543
9935
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
9544
9936
  }
@@ -9577,9 +9969,9 @@ async function securityCommand(parsed, dependencies) {
9577
9969
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
9578
9970
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
9579
9971
  const context = await hostedSecurityContext(parsed, dependencies);
9580
- const report3 = await getHostedSecurityReport({ ...context, jobId });
9581
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report3, null, 2));
9582
- 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);
9583
9975
  return;
9584
9976
  }
9585
9977
  if (sub !== "run") {
@@ -9663,7 +10055,8 @@ async function securityStatus(parsed, dependencies) {
9663
10055
 
9664
10056
  // src/cli.ts
9665
10057
  function exitCodeFor(err) {
9666
- return err?.code === "handshake_pending" ? 75 : 1;
10058
+ const code = err?.code;
10059
+ return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
9667
10060
  }
9668
10061
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9669
10062
  const parsed = parseArgv(argv);
@@ -9711,6 +10104,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9711
10104
  await pmCommand(parsed, dependencies);
9712
10105
  return;
9713
10106
  }
10107
+ if (command === "discuss") {
10108
+ await discussCommand(parsed, dependencies);
10109
+ return;
10110
+ }
9714
10111
  if (command === "provision") {
9715
10112
  await provisionCommand(parsed, dependencies);
9716
10113
  return;