@odla-ai/cli 0.24.0 → 0.25.1

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.
@@ -540,23 +540,23 @@ function adminAiAuditQuery(filters) {
540
540
  }
541
541
  return `?limit=${filters.limit}`;
542
542
  }
543
- async function readAdminAiAudit(request) {
544
- const response2 = await request.fetch(`${request.platform}/registry/platform/ai-audit${request.query}`, {
545
- headers: request.headers
543
+ async function readAdminAiAudit(request2) {
544
+ const response2 = await request2.fetch(`${request2.platform}/registry/platform/ai-audit${request2.query}`, {
545
+ headers: request2.headers
546
546
  });
547
547
  const body = await responseBody(response2);
548
548
  if (!response2.ok) throw new Error(apiError(response2.status, body));
549
- if (request.json) {
550
- request.stdout.log(JSON.stringify(body, null, 2));
549
+ if (request2.json) {
550
+ request2.stdout.log(JSON.stringify(body, null, 2));
551
551
  return;
552
552
  }
553
553
  const events = isRecord(body) && Array.isArray(body.events) ? body.events.filter(isRecord) : [];
554
- request.stdout.log("when change target before -> after actor");
554
+ request2.stdout.log("when change target before -> after actor");
555
555
  for (const event of events) {
556
556
  const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
557
557
  const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
558
558
  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";
559
- request.stdout.log([
559
+ request2.stdout.log([
560
560
  timestamp(event.createdAt),
561
561
  String(event.changeKind ?? ""),
562
562
  String(event.purpose ?? event.provider ?? ""),
@@ -598,14 +598,14 @@ function adminAiUsageQuery(filters) {
598
598
  const query = params.toString();
599
599
  return query ? `?${query}` : "";
600
600
  }
601
- async function readAdminAiUsage(request) {
602
- const res = await request.fetch(`${request.platform}/registry/platform/ai-usage${request.query}`, {
603
- headers: request.headers
601
+ async function readAdminAiUsage(request2) {
602
+ const res = await request2.fetch(`${request2.platform}/registry/platform/ai-usage${request2.query}`, {
603
+ headers: request2.headers
604
604
  });
605
605
  const body = await responseBody2(res);
606
606
  if (!res.ok) throw new Error(apiError2("read platform AI usage", res.status, body));
607
- if (request.json) request.stdout.log(JSON.stringify(body, null, 2));
608
- else printUsage(body, request.stdout);
607
+ if (request2.json) request2.stdout.log(JSON.stringify(body, null, 2));
608
+ else printUsage(body, request2.stdout);
609
609
  }
610
610
  function usageLimit(value) {
611
611
  if (!Number.isSafeInteger(value) || value < 1 || value > 500) {
@@ -2381,10 +2381,10 @@ function installSkill(options = {}) {
2381
2381
  current.add(target);
2382
2382
  targets.set(harness, current);
2383
2383
  };
2384
- const plan = (target, content, managedMerge = false, boundary = root) => {
2384
+ const plan = (target, content2, managedMerge = false, boundary = root) => {
2385
2385
  const existing = plans.get(target);
2386
- if (existing && existing.content !== content) throw new Error(`internal agent setup conflict for ${target}`);
2387
- plans.set(target, { target, content, boundary, managedMerge });
2386
+ if (existing && existing.content !== content2) throw new Error(`internal agent setup conflict for ${target}`);
2387
+ plans.set(target, { target, content: content2, boundary, managedMerge });
2388
2388
  };
2389
2389
  const planSkillTree = (targetDir2, boundary = root) => {
2390
2390
  for (const rel of files) plan(join5(targetDir2, rel), readFileSync5(join5(sourceDir, rel), "utf8"), false, boundary);
@@ -2825,15 +2825,15 @@ async function connectGitHubSecuritySource(options) {
2825
2825
  const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
2826
2826
  const wait2 = options.wait ?? waitForHostedPoll;
2827
2827
  while (true) {
2828
- const state = await requestHostedSecurityJson(
2828
+ const state2 = await requestHostedSecurityJson(
2829
2829
  options,
2830
2830
  `/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
2831
2831
  {},
2832
2832
  "check GitHub connection"
2833
2833
  );
2834
- if (state.status !== "pending") {
2835
- if (state.status === "connected") return state;
2836
- throw new Error(`GitHub connection ${state.status}${state.failureCode ? `: ${state.failureCode}` : ""}`);
2834
+ if (state2.status !== "pending") {
2835
+ if (state2.status === "connected") return state2;
2836
+ throw new Error(`GitHub connection ${state2.status}${state2.failureCode ? `: ${state2.failureCode}` : ""}`);
2837
2837
  }
2838
2838
  if (now() >= deadline) throw new Error("GitHub connection approval timed out");
2839
2839
  await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
@@ -3388,11 +3388,11 @@ async function gitBlobs(cwd, entries, maxBytes) {
3388
3388
  if (!Number.isSafeInteger(length) || length < 0 || newline + 1 + length >= output.length) {
3389
3389
  throw new Error("git object output has an invalid length");
3390
3390
  }
3391
- const content = output.subarray(newline + 1, newline + 1 + length);
3391
+ const content2 = output.subarray(newline + 1, newline + 1 + length);
3392
3392
  if (output[newline + 1 + length] !== 10) throw new Error("git object output is malformed");
3393
- total += content.byteLength;
3393
+ total += content2.byteLength;
3394
3394
  if (total > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
3395
- blobs.push(content);
3395
+ blobs.push(content2);
3396
3396
  offset = newline + 1 + length + 1;
3397
3397
  }
3398
3398
  return blobs;
@@ -3415,13 +3415,13 @@ async function materializeGitTree(source, commitSha, options = {}) {
3415
3415
  try {
3416
3416
  const blobs = await gitBlobs(sourceDir, entries, maxBytes);
3417
3417
  for (const [index, entry] of entries.entries()) {
3418
- const content = blobs[index];
3419
- byteCount += content.byteLength;
3418
+ const content2 = blobs[index];
3419
+ byteCount += content2.byteLength;
3420
3420
  if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
3421
3421
  const target = resolve6(targetRoot, entry.path);
3422
3422
  if (!target.startsWith(`${resolve6(targetRoot)}${sep2}`)) throw new TypeError("Git tree path escapes workspace");
3423
3423
  await mkdir(resolve6(target, ".."), { recursive: true });
3424
- await writeFile2(target, content, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
3424
+ await writeFile2(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
3425
3425
  }
3426
3426
  return {
3427
3427
  root,
@@ -3799,11 +3799,11 @@ var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
3799
3799
  var MAX_PATCH_BYTES = 256 * 1024;
3800
3800
  var MAX_STATE_BYTES = 64 * 1024;
3801
3801
  async function createCodePortableCheckpoint(input) {
3802
- const state = normalizeState(input.state);
3802
+ const state2 = normalizeState(input.state);
3803
3803
  validateBaseAndPatch(input.baseCommitSha, input.patch);
3804
3804
  const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
3805
- const stateDigest = `sha256:${await sha256Hex(canonicalJson(state))}`;
3806
- const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state, stateDigest };
3805
+ const stateDigest = `sha256:${await sha256Hex(canonicalJson(state2))}`;
3806
+ const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
3807
3807
  const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
3808
3808
  return freeze({ ...fields, patch: input.patch, checkpointDigest });
3809
3809
  }
@@ -3824,8 +3824,8 @@ async function verifyCodePortableCheckpoint(value) {
3824
3824
  return created;
3825
3825
  }
3826
3826
  function normalizeState(value) {
3827
- const state = object(value, "state");
3828
- exact2(state, [
3827
+ const state2 = object(value, "state");
3828
+ exact2(state2, [
3829
3829
  "planCursor",
3830
3830
  "conversationRefs",
3831
3831
  "planningInputDigest",
@@ -3837,13 +3837,13 @@ function normalizeState(value) {
3837
3837
  "unresolvedApprovals",
3838
3838
  "trustStatus"
3839
3839
  ]);
3840
- const planCursor = state.planCursor;
3841
- const conversations = strings(state.conversationRefs, "conversationRefs", ID2, 256, false);
3842
- const approvals = strings(state.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
3843
- 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) {
3840
+ const planCursor = state2.planCursor;
3841
+ const conversations = strings(state2.conversationRefs, "conversationRefs", ID2, 256, false);
3842
+ const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
3843
+ 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) {
3844
3844
  throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
3845
3845
  }
3846
- const effects = state.completedEffects.map((item) => {
3846
+ const effects = state2.completedEffects.map((item) => {
3847
3847
  const effect = object(item, "completed effect");
3848
3848
  exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
3849
3849
  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)) {
@@ -3858,18 +3858,18 @@ function normalizeState(value) {
3858
3858
  if (new Set(effects.map((item) => item.effectId)).size !== effects.length) {
3859
3859
  throw invalid2("Portable checkpoint repeats a completed effect.");
3860
3860
  }
3861
- const trustStatus = state.trustStatus;
3862
- 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)) {
3861
+ const trustStatus = state2.trustStatus;
3862
+ 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)) {
3863
3863
  throw invalid2("Portable checkpoint trust status does not match its evidence digests.");
3864
3864
  }
3865
3865
  const normalized = {
3866
3866
  planCursor,
3867
3867
  conversationRefs: conversations,
3868
- planningInputDigest: state.planningInputDigest,
3869
- buildPolicyDigest: state.buildPolicyDigest,
3870
- dependencyLayerDigest: state.dependencyLayerDigest,
3871
- verificationDigest: state.verificationDigest,
3872
- reviewDigest: state.reviewDigest,
3868
+ planningInputDigest: state2.planningInputDigest,
3869
+ buildPolicyDigest: state2.buildPolicyDigest,
3870
+ dependencyLayerDigest: state2.dependencyLayerDigest,
3871
+ verificationDigest: state2.verificationDigest,
3872
+ reviewDigest: state2.reviewDigest,
3873
3873
  completedEffects: effects,
3874
3874
  unresolvedApprovals: approvals,
3875
3875
  trustStatus
@@ -3986,60 +3986,60 @@ async function createConversionRegistry(config) {
3986
3986
  policies.set(policy.conversionId, Object.freeze(policy));
3987
3987
  }
3988
3988
  const outputCounts = /* @__PURE__ */ new Map();
3989
- const get = (id, kind) => {
3989
+ const get2 = (id, kind) => {
3990
3990
  const policy = policies.get(id);
3991
3991
  if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
3992
3992
  return policy;
3993
3993
  };
3994
3994
  const checked = (source, id, kind) => {
3995
- const policy = get(id, kind);
3995
+ const policy = get2(id, kind);
3996
3996
  if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
3997
3997
  return policy;
3998
3998
  };
3999
- const emit2 = (source, policy, value) => convert(source, policy, value, outputCounts);
3999
+ const emit3 = (source, policy, value) => convert(source, policy, value, outputCounts);
4000
4000
  const operations = Object.freeze({
4001
4001
  boolean: async (value, id) => {
4002
4002
  const policy = checked(value, id, "boolean");
4003
- return emit2(value, policy, requireBoolean(value.value));
4003
+ return emit3(value, policy, requireBoolean(value.value));
4004
4004
  },
4005
4005
  integer: async (value, id) => {
4006
4006
  const policy = checked(value, id, "integer");
4007
- return emit2(value, policy, boundedInteger(value.value, policy.output));
4007
+ return emit3(value, policy, boundedInteger(value.value, policy.output));
4008
4008
  },
4009
4009
  finiteNumber: async (value, id) => {
4010
4010
  const policy = checked(value, id, "finite_number");
4011
- return emit2(value, policy, boundedNumber(value.value, policy.output));
4011
+ return emit3(value, policy, boundedNumber(value.value, policy.output));
4012
4012
  },
4013
4013
  enum: async (value, id) => {
4014
4014
  const policy = checked(value, id, "enum");
4015
- return emit2(value, policy, enumMember(value.value, policy.output));
4015
+ return emit3(value, policy, enumMember(value.value, policy.output));
4016
4016
  },
4017
4017
  date: async (value, id) => {
4018
4018
  const policy = checked(value, id, "date");
4019
- return emit2(value, policy, canonicalDate(value.value, policy.output));
4019
+ return emit3(value, policy, canonicalDate(value.value, policy.output));
4020
4020
  },
4021
4021
  registeredId: async (value, id) => {
4022
4022
  const policy = checked(value, id, "registered_id");
4023
4023
  const registry = config.registeredIds?.[policy.output.registryId];
4024
4024
  const output = typeof value.value === "string" ? registry?.values[value.value] : void 0;
4025
4025
  if (!output) throw new CamelError("conversion_rejected", "Registered-ID conversion rejected the candidate.");
4026
- return emit2(value, policy, output);
4026
+ return emit3(value, policy, output);
4027
4027
  },
4028
4028
  digest: async (value, id) => {
4029
4029
  const policy = checked(value, id, "digest");
4030
4030
  if (!(value.value instanceof Uint8Array)) throw new CamelError("conversion_rejected", "Digest conversion requires bytes.");
4031
- return emit2(value, policy, await sha256Hex(value.value));
4031
+ return emit3(value, policy, await sha256Hex(value.value));
4032
4032
  },
4033
4033
  measure: async (value, metric, id) => {
4034
4034
  const policy = checked(value, id, "integer");
4035
4035
  const measured = measure(value.value, metric);
4036
- return emit2(value, policy, boundedInteger(measured, policy.output));
4036
+ return emit3(value, policy, boundedInteger(measured, policy.output));
4037
4037
  },
4038
4038
  test: async (value, predicateId, id) => {
4039
4039
  const policy = checked(value, id, "boolean");
4040
4040
  const predicate = config.predicates?.[predicateId];
4041
4041
  if (!predicate) throw new CamelError("conversion_rejected", "Predicate is not registered.");
4042
- return emit2(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
4042
+ return emit3(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
4043
4043
  }
4044
4044
  });
4045
4045
  return Object.freeze({ operations, policy: (id) => policies.get(id) ?? missingPolicy() });
@@ -4258,11 +4258,11 @@ async function digestStagedWorkspace(root, limits) {
4258
4258
  const hash = createHash2("sha256");
4259
4259
  let bytes = 0;
4260
4260
  for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
4261
- const content = await readFile2(file.target);
4262
- bytes += Buffer.byteLength(file.path) + content.byteLength;
4261
+ const content2 = await readFile2(file.target);
4262
+ bytes += Buffer.byteLength(file.path) + content2.byteLength;
4263
4263
  if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
4264
- hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content.byteLength}:`);
4265
- hash.update(content);
4264
+ hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
4265
+ hash.update(content2);
4266
4266
  }
4267
4267
  return `sha256:${hash.digest("hex")}`;
4268
4268
  }
@@ -4357,13 +4357,13 @@ function createCodeRuntimeControlClient(options) {
4357
4357
  if (!Number.isSafeInteger(modelRequestTimeoutMs) || modelRequestTimeoutMs < 3e4 || modelRequestTimeoutMs > 30 * 6e4) {
4358
4358
  throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
4359
4359
  }
4360
- const request = options.fetch ?? fetch;
4360
+ const request2 = options.fetch ?? fetch;
4361
4361
  const call2 = async (path, body, timeoutMs = requestTimeoutMs) => {
4362
4362
  const timeout = AbortSignal.timeout(timeoutMs);
4363
4363
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
4364
4364
  let response2;
4365
4365
  try {
4366
- response2 = await request(`${endpoint}${path}`, {
4366
+ response2 = await request2(`${endpoint}${path}`, {
4367
4367
  method: "POST",
4368
4368
  headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
4369
4369
  body: JSON.stringify(body),
@@ -5270,34 +5270,34 @@ function createCodeToolBroker(options) {
5270
5270
  const policy = createCodePolicyGate(options);
5271
5271
  let tail = Promise.resolve();
5272
5272
  return {
5273
- execute(context, request) {
5274
- const result = tail.then(() => route(context, request, options, recipes, policy));
5273
+ execute(context, request2) {
5274
+ const result = tail.then(() => route(context, request2, options, recipes, policy));
5275
5275
  tail = result.then(() => void 0, () => void 0);
5276
5276
  return result;
5277
5277
  }
5278
5278
  };
5279
5279
  }
5280
- async function route(context, request, options, recipes, policy) {
5280
+ async function route(context, request2, options, recipes, policy) {
5281
5281
  try {
5282
5282
  if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
5283
- if (request.tool === "sandbox.read") return await read(context, request, options, policy);
5284
- if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
5285
- return await recipe(context, request, options, recipes, policy);
5283
+ if (request2.tool === "sandbox.read") return await read(context, request2, options, policy);
5284
+ if (request2.tool === "sandbox.apply_patch") return await patch(context, request2, options, policy);
5285
+ return await recipe(context, request2, options, recipes, policy);
5286
5286
  } catch (reason) {
5287
- return response(request, false, reason instanceof TypeError ? reason.message : "tool failed closed");
5287
+ return response(request2, false, reason instanceof TypeError ? reason.message : "tool failed closed");
5288
5288
  }
5289
5289
  }
5290
- async function read(context, request, options, policy) {
5291
- exactKeys(request.input, ["path", "startLine", "endLine"]);
5292
- const path = stringField(request.input, "path");
5293
- const startLine = optionalInteger(request.input.startLine) ?? 1;
5294
- const endLine = optionalInteger(request.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
5290
+ async function read(context, request2, options, policy) {
5291
+ exactKeys(request2.input, ["path", "startLine", "endLine"]);
5292
+ const path = stringField(request2.input, "path");
5293
+ const startLine = optionalInteger(request2.input.startLine) ?? 1;
5294
+ const endLine = optionalInteger(request2.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
5295
5295
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
5296
5296
  throw new TypeError("requested line range exceeds its bound");
5297
5297
  }
5298
5298
  const paths = await registeredFiles(context.workspaceDir, 2e4);
5299
- const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
5300
- if (!allowed) return response(request, false, "tool denied by CaMeL policy");
5299
+ const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
5300
+ if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
5301
5301
  const target = resolveCodePath(context.workspaceDir, path);
5302
5302
  const info = await stat2(target);
5303
5303
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
@@ -5306,40 +5306,40 @@ async function read(context, request, options, policy) {
5306
5306
  const source = await readFile22(target);
5307
5307
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
5308
5308
  const lines = source.toString("utf8").split("\n");
5309
- const content = lines.slice(startLine - 1, endLine).join("\n");
5310
- if (Buffer.byteLength(content) > (options.maxReadBytes ?? 128 * 1024)) {
5309
+ const content2 = lines.slice(startLine - 1, endLine).join("\n");
5310
+ if (Buffer.byteLength(content2) > (options.maxReadBytes ?? 128 * 1024)) {
5311
5311
  throw new TypeError("read result exceeds its byte bound");
5312
5312
  }
5313
- return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
5313
+ return response(request2, true, content2, { path, startLine, endLine: Math.min(endLine, lines.length) });
5314
5314
  }
5315
- async function patch(context, request, options, policy) {
5316
- exactKeys(request.input, ["patch"]);
5317
- const value = stringField(request.input, "patch");
5315
+ async function patch(context, request2, options, policy) {
5316
+ exactKeys(request2.input, ["patch"]);
5317
+ const value = stringField(request2.input, "patch");
5318
5318
  const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
5319
5319
  if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
5320
5320
  throw new TypeError("patch targets a read-only reference source");
5321
5321
  }
5322
- const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
5323
- if (!allowed) return response(request, false, "tool denied by CaMeL policy");
5322
+ const allowed = await policy.patch(policyContext(context, request2, options, { patch: value }));
5323
+ if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
5324
5324
  await applyCodePatch(context.workspaceDir, value, paths);
5325
- return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
5325
+ return response(request2, true, `Applied patch to ${paths.length} file(s).`, { paths });
5326
5326
  }
5327
- async function recipe(context, request, options, recipes, policy) {
5328
- exactKeys(request.input, ["recipeId"]);
5329
- const recipeId = stringField(request.input, "recipeId");
5327
+ async function recipe(context, request2, options, recipes, policy) {
5328
+ exactKeys(request2.input, ["recipeId"]);
5329
+ const recipeId = stringField(request2.input, "recipeId");
5330
5330
  const digestLimits = {
5331
5331
  maxFiles: options.maxRecipeWorkspaceFiles ?? 2e4,
5332
5332
  maxBytes: options.maxRecipeWorkspaceBytes ?? 512 * 1024 * 1024
5333
5333
  };
5334
5334
  const sourceDigest = await digestStagedWorkspace(context.workspaceDir, digestLimits);
5335
- const allowed = await policy.recipe(policyContext(context, request, options, {
5335
+ const allowed = await policy.recipe(policyContext(context, request2, options, {
5336
5336
  recipeIds: [...recipes.keys()].sort(),
5337
5337
  recipeId,
5338
5338
  sourceDigest
5339
5339
  }));
5340
- if (!allowed) return response(request, false, "tool denied by CaMeL policy");
5340
+ if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
5341
5341
  const selected = recipes.get(recipeId);
5342
- if (!selected) return response(request, false, "build recipe is not registered");
5342
+ if (!selected) return response(request2, false, "build recipe is not registered");
5343
5343
  const staged = await stageWorkspace(context.workspaceDir, {
5344
5344
  maxFiles: digestLimits.maxFiles,
5345
5345
  maxBytes: digestLimits.maxBytes
@@ -5356,7 +5356,7 @@ async function recipe(context, request, options, recipes, policy) {
5356
5356
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
5357
5357
  const ok = result.exitCode === 0 && !result.outputLimitExceeded && !result.timedOut;
5358
5358
  const status = result.timedOut ? "timed out" : result.outputLimitExceeded ? "exceeded output limit" : ok ? "passed" : `failed with exit ${result.exitCode}`;
5359
- return response(request, ok, `Recipe ${recipeId} ${status}.${output ? `
5359
+ return response(request2, ok, `Recipe ${recipeId} ${status}.${output ? `
5360
5360
  ${output}` : ""}`, {
5361
5361
  recipeId,
5362
5362
  exitCode: result.exitCode,
@@ -5368,10 +5368,10 @@ ${output}` : ""}`, {
5368
5368
  await staged.cleanup();
5369
5369
  }
5370
5370
  }
5371
- function policyContext(context, request, options, extra) {
5371
+ function policyContext(context, request2, options, extra) {
5372
5372
  return {
5373
5373
  lease: context.lease,
5374
- request,
5374
+ request: request2,
5375
5375
  workspaceId: `workspace:${context.lease.task.attemptId}`,
5376
5376
  readers: { kind: "principals", principalIds: [options.readerId] },
5377
5377
  ...extra
@@ -5421,8 +5421,8 @@ function optionalInteger(value) {
5421
5421
  if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
5422
5422
  return value;
5423
5423
  }
5424
- function response(request, ok, content, details) {
5425
- return { requestId: request.requestId, ok, content, ...details ? { details } : {} };
5424
+ function response(request2, ok, content2, details) {
5425
+ return { requestId: request2.requestId, ok, content: content2, ...details ? { details } : {} };
5426
5426
  }
5427
5427
  function codeCommandMetadata(payload, resume) {
5428
5428
  const trusted = record22(payload.trustedBase);
@@ -5522,13 +5522,13 @@ function createCodeRuntimeToolBroker(input, lease, role) {
5522
5522
  readerId: `code-session:${lease.task.taskId}`,
5523
5523
  readOnlyPrefixes: [".odla-references"]
5524
5524
  });
5525
- 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" }) };
5525
+ 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" }) };
5526
5526
  }
5527
5527
  async function handleCodeRuntimeInference(input) {
5528
- const { command, metadata: metadata2, request, state } = input;
5529
- if (state.tokens >= metadata2.maxTokensPerInteraction) {
5530
- if (!state.noticeEmitted) {
5531
- state.noticeEmitted = true;
5528
+ const { command, metadata: metadata2, request: request2, state: state2 } = input;
5529
+ if (state2.tokens >= metadata2.maxTokensPerInteraction) {
5530
+ if (!state2.noticeEmitted) {
5531
+ state2.noticeEmitted = true;
5532
5532
  await input.event({
5533
5533
  type: "message",
5534
5534
  actor: "system",
@@ -5538,7 +5538,7 @@ async function handleCodeRuntimeInference(input) {
5538
5538
  return {
5539
5539
  protocolVersion: HARNESS_PROTOCOL_VERSION,
5540
5540
  type: "inference.response",
5541
- requestId: request.requestId,
5541
+ requestId: request2.requestId,
5542
5542
  response: {
5543
5543
  id: `budget:${command.commandId}`,
5544
5544
  provider: "openai",
@@ -5552,11 +5552,11 @@ async function handleCodeRuntimeInference(input) {
5552
5552
  }
5553
5553
  const startedAt = Date.now();
5554
5554
  const response2 = await input.control.infer(command.sessionId, {
5555
- requestId: request.requestId,
5555
+ requestId: request2.requestId,
5556
5556
  interactionId: command.commandId,
5557
- call: request.call
5557
+ call: request2.call
5558
5558
  });
5559
- state.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
5559
+ state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
5560
5560
  await input.event({
5561
5561
  type: "usage",
5562
5562
  provider: response2.receipt.provider,
@@ -5565,13 +5565,13 @@ async function handleCodeRuntimeInference(input) {
5565
5565
  outputTokens: response2.receipt.outputTokens,
5566
5566
  durationMs: Date.now() - startedAt,
5567
5567
  interactionId: command.commandId,
5568
- interactionTokens: state.tokens,
5568
+ interactionTokens: state2.tokens,
5569
5569
  interactionMaxTokens: metadata2.maxTokensPerInteraction
5570
5570
  }).catch(() => void 0);
5571
5571
  return {
5572
5572
  protocolVersion: HARNESS_PROTOCOL_VERSION,
5573
5573
  type: "inference.response",
5574
- requestId: request.requestId,
5574
+ requestId: request2.requestId,
5575
5575
  response: response2.response
5576
5576
  };
5577
5577
  }
@@ -5939,6 +5939,14 @@ Usage:
5939
5939
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
5940
5940
  odla-ai pm <goal|task|decision|bug> rm <id>
5941
5941
  odla-ai pm handoff --app <id> [--json]
5942
+ odla-ai discuss groups [--json]
5943
+ odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
5944
+ odla-ai discuss read <topic> [--json]
5945
+ odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"]
5946
+ odla-ai discuss reply <topic> --body "..." [--markup "..."]
5947
+ odla-ai discuss resolve <topic> [--reopen]
5948
+ odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
5949
+ odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
5942
5950
  odla-ai whoami [--json]
5943
5951
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
5944
5952
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -6043,6 +6051,13 @@ Commands:
6043
6051
  "app". Entities: goal (alias conformance), task (alias kanban),
6044
6052
  decision, bug. Status changes and comments post to each item's
6045
6053
  @odla-ai/chat discussion thread.
6054
+ discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
6055
+ one group per project, topics with replies, @-mentions of people,
6056
+ agents, PM items, and projects. Built for unattended use \u2014 post a
6057
+ question, then "watch" it until someone answers. "watch" exits 75
6058
+ (not 1) when it times out with nothing new, so a script can tell
6059
+ "no answer yet" from a failure and just rerun. Use "who" to look
6060
+ up the exact @[Label](kind/id) markup for a mention.
6046
6061
  provision Register services, compose integrations, persist credentials, optionally push secrets.
6047
6062
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6048
6063
  skill Same installer; --agent accepts all, claude, codex, cursor,
@@ -6782,6 +6797,18 @@ var COMMAND_SURFACE = {
6782
6797
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
6783
6798
  capabilities: {},
6784
6799
  code: { connect: {} },
6800
+ // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
6801
+ discuss: {
6802
+ groups: {},
6803
+ list: {},
6804
+ topics: {},
6805
+ read: {},
6806
+ post: {},
6807
+ reply: {},
6808
+ resolve: {},
6809
+ who: {},
6810
+ watch: {}
6811
+ },
6785
6812
  doctor: {},
6786
6813
  help: {},
6787
6814
  init: {},
@@ -6931,17 +6958,17 @@ async function runHostedSecurity(options) {
6931
6958
  allowNetwork: false
6932
6959
  }
6933
6960
  });
6934
- const report3 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
6935
- await writeSecurityArtifacts(output, report3);
6936
- const reportDigest = await securityFingerprint(report3);
6961
+ const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
6962
+ await writeSecurityArtifacts(output, report4);
6963
+ const reportDigest = await securityFingerprint(report4);
6937
6964
  await hosted.complete({
6938
6965
  reportDigest,
6939
- coverageStatus: report3.coverageStatus,
6940
- confirmed: report3.metrics.confirmed,
6941
- candidates: report3.metrics.candidates
6966
+ coverageStatus: report4.coverageStatus,
6967
+ confirmed: report4.metrics.confirmed,
6968
+ candidates: report4.metrics.candidates
6942
6969
  }, { signal: options.signal });
6943
- printSummary(options.stdout ?? console, appId, env, hosted.run, report3, output);
6944
- return Object.freeze({ report: report3, run: hosted.run, output });
6970
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
6971
+ return Object.freeze({ report: report4, run: hosted.run, output });
6945
6972
  }
6946
6973
  function selectEnv(requested, declared, configPath, rootDir) {
6947
6974
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -6951,11 +6978,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
6951
6978
  }
6952
6979
  return env;
6953
6980
  }
6954
- async function injectedToken(options, request) {
6955
- const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
6981
+ async function injectedToken(options, request2) {
6982
+ const value = options.token ?? await options.getToken?.(Object.freeze({ ...request2 }));
6956
6983
  if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
6957
6984
  throw new Error(
6958
- request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
6985
+ request2.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
6959
6986
  );
6960
6987
  }
6961
6988
  return value;
@@ -6967,14 +6994,14 @@ function profileFor(name, maxHuntTasks) {
6967
6994
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
6968
6995
  return { ...profile, maxHuntTasks };
6969
6996
  }
6970
- function printSummary(out, appId, env, run, report3, output) {
6971
- const complete = report3.coverage.filter((cell) => cell.state === "complete").length;
6997
+ function printSummary(out, appId, env, run, report4, output) {
6998
+ const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
6972
6999
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
6973
7000
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
6974
7001
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
6975
- 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}`);
6976
- if (report3.callBudget) out.log(` calls: discovery=${formatBudget(report3.callBudget.discovery)} validation=${formatBudget(report3.callBudget.validation)}`);
6977
- out.log(` findings: confirmed=${report3.metrics.confirmed} needs_reproduction=${report3.metrics.needsReproduction} candidates=${report3.metrics.candidates}`);
7002
+ 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}`);
7003
+ if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
7004
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
6978
7005
  out.log(` report: ${resolve9(output, "REPORT.md")}`);
6979
7006
  }
6980
7007
  function formatBudget(usage) {
@@ -7978,6 +8005,289 @@ async function codeCommand(parsed, dependencies) {
7978
8005
  });
7979
8006
  }
7980
8007
 
8008
+ // src/discuss-actions.ts
8009
+ async function request(ctx, method, path, body) {
8010
+ const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
8011
+ method,
8012
+ headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
8013
+ body: body === void 0 ? void 0 : JSON.stringify(body)
8014
+ });
8015
+ const data = await res.json().catch(() => ({}));
8016
+ if (!res.ok)
8017
+ throw new Error(`discuss ${method} ${path} failed: ${data.error ?? `registry returned ${res.status}`}`);
8018
+ return data;
8019
+ }
8020
+ function emit(ctx, value, human) {
8021
+ if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
8022
+ else human();
8023
+ }
8024
+ var state = (topic) => topic.resolved ? "resolved" : "open";
8025
+ function bodyWithRefs(post) {
8026
+ if (!post.refs || post.refs.length === 0) return post.body;
8027
+ let out = "";
8028
+ let cursor = 0;
8029
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8030
+ if (ref.start < cursor || ref.end > post.body.length) continue;
8031
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8032
+ cursor = ref.end;
8033
+ }
8034
+ return out + post.body.slice(cursor);
8035
+ }
8036
+ function content(parsed) {
8037
+ const markup = stringOpt(parsed.options.markup);
8038
+ if (markup) return { markup };
8039
+ const body = stringOpt(parsed.options.body);
8040
+ if (!body) throw new Error('needs --body "\u2026" (or --markup with @[Label](kind/id) mentions)');
8041
+ return { body };
8042
+ }
8043
+ async function discussGroups(ctx) {
8044
+ const result = await request(
8045
+ ctx,
8046
+ "GET",
8047
+ "/groups"
8048
+ );
8049
+ emit(ctx, result, () => {
8050
+ ctx.out.log("app name started");
8051
+ for (const group of result.groups) {
8052
+ ctx.out.log(`${group.appId} ${group.name} ${group.channelId ? "yes" : "no"}`);
8053
+ }
8054
+ });
8055
+ }
8056
+ async function discussList(ctx, parsed) {
8057
+ const query = new URLSearchParams();
8058
+ for (const [flag, param] of [
8059
+ ["app", "app"],
8060
+ ["q", "q"],
8061
+ ["state", "state"],
8062
+ ["limit", "limit"],
8063
+ ["offset", "offset"]
8064
+ ]) {
8065
+ const value = stringOpt(parsed.options[flag]);
8066
+ if (value) query.set(param, value);
8067
+ }
8068
+ const qs = query.toString();
8069
+ const page = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
8070
+ emit(ctx, page, () => {
8071
+ ctx.out.log(`topics \u2014 ${page.topics.length} of ${page.total}`);
8072
+ ctx.out.log("id state app replies subject");
8073
+ for (const topic of page.topics) {
8074
+ ctx.out.log(
8075
+ `${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
8076
+ );
8077
+ }
8078
+ });
8079
+ }
8080
+ async function discussRead(ctx, id) {
8081
+ const result = await request(
8082
+ ctx,
8083
+ "GET",
8084
+ `/topics/${encodeURIComponent(id)}`
8085
+ );
8086
+ emit(ctx, result, () => {
8087
+ ctx.out.log(`${result.topic.subject} [${state(result.topic)}] ${result.topic.appId ?? ""}`);
8088
+ for (const post of result.posts) {
8089
+ const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
8090
+ ctx.out.log(`
8091
+ \u2014 ${who}`);
8092
+ ctx.out.log(bodyWithRefs(post));
8093
+ for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8094
+ }
8095
+ });
8096
+ }
8097
+ async function discussPost(ctx, parsed) {
8098
+ const appId = stringOpt(parsed.options.app);
8099
+ if (!appId) throw new Error("discuss post needs --app <appId>");
8100
+ const subject = stringOpt(parsed.options.subject);
8101
+ if (!subject) throw new Error('discuss post needs --subject "\u2026"');
8102
+ const created = await request(ctx, "POST", "/topics", {
8103
+ appId,
8104
+ subject,
8105
+ ...content(parsed)
8106
+ });
8107
+ emit(ctx, created, () => ctx.out.log(`opened topic ${created.id}`));
8108
+ }
8109
+ async function discussReply(ctx, id, parsed) {
8110
+ const created = await request(
8111
+ ctx,
8112
+ "POST",
8113
+ `/topics/${encodeURIComponent(id)}/replies`,
8114
+ content(parsed)
8115
+ );
8116
+ emit(ctx, created, () => ctx.out.log(`replied ${created.id}`));
8117
+ }
8118
+ async function discussResolve(ctx, id, resolved) {
8119
+ const result = await request(
8120
+ ctx,
8121
+ "PATCH",
8122
+ `/topics/${encodeURIComponent(id)}`,
8123
+ { resolved }
8124
+ );
8125
+ emit(ctx, result, () => ctx.out.log(`${resolved ? "resolved" : "reopened"} ${result.id}`));
8126
+ }
8127
+ async function discussWho(ctx, parsed) {
8128
+ const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
8129
+ const app = stringOpt(parsed.options.app);
8130
+ if (app) query.set("app", app);
8131
+ const kinds = stringOpt(parsed.options.kinds);
8132
+ if (kinds) query.set("kinds", kinds);
8133
+ const found = await request(ctx, "GET", `/mentionables?${query.toString()}`);
8134
+ emit(ctx, found, () => {
8135
+ ctx.out.log("mention kind hint");
8136
+ for (const item of found.items) {
8137
+ ctx.out.log(`@[${item.label}](${item.kind}/${item.id}) ${item.kind} ${item.hint ?? ""}`);
8138
+ }
8139
+ if (found.failed.length > 0) ctx.out.error(`(no results from: ${found.failed.join(", ")})`);
8140
+ });
8141
+ }
8142
+
8143
+ // src/discuss-watch.ts
8144
+ var DEFAULT_INTERVAL_MS = 15e3;
8145
+ var DEFAULT_TIMEOUT_MS = 15 * 6e4;
8146
+ function numberOpt2(parsed, flag, fallback) {
8147
+ const raw = stringOpt(parsed.options[flag]);
8148
+ const value = raw == null ? NaN : Number(raw);
8149
+ return Number.isFinite(value) && value > 0 ? value : fallback;
8150
+ }
8151
+ async function get(ctx, path) {
8152
+ const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
8153
+ headers: { authorization: `Bearer ${ctx.token}` }
8154
+ });
8155
+ const data = await res.json().catch(() => ({}));
8156
+ if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
8157
+ return data;
8158
+ }
8159
+ async function discussWatch(ctx, topicId, parsed) {
8160
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
8161
+ const now = ctx.now ?? Date.now;
8162
+ const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
8163
+ const timeoutMs = numberOpt2(parsed, "timeout", DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
8164
+ const by = stringOpt(parsed.options.by);
8165
+ const self = stringOpt(parsed.options.self);
8166
+ const deadline = now() + timeoutMs;
8167
+ const fresh = (post) => (!by || post.authorId === by) && (!self || post.authorId !== self);
8168
+ let seen = /* @__PURE__ */ new Set();
8169
+ let since = now();
8170
+ if (topicId) {
8171
+ const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
8172
+ seen = new Set(first.posts.map((post) => post.id));
8173
+ }
8174
+ for (; ; ) {
8175
+ if (topicId) {
8176
+ const { posts } = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
8177
+ const added = posts.filter((post) => !seen.has(post.id) && fresh(post));
8178
+ if (added.length > 0) return report2(ctx, { found: true, posts: added });
8179
+ for (const post of posts) seen.add(post.id);
8180
+ } else {
8181
+ const app = stringOpt(parsed.options.app);
8182
+ const { topics } = await get(
8183
+ ctx,
8184
+ `/topics?state=all${app ? `&app=${encodeURIComponent(app)}` : ""}`
8185
+ );
8186
+ const active = topics.filter((topic) => topic.lastActivityAt > since);
8187
+ if (active.length > 0) return report2(ctx, { found: true, topics: active });
8188
+ since = now();
8189
+ }
8190
+ if (now() >= deadline) return report2(ctx, { found: false });
8191
+ await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
8192
+ }
8193
+ }
8194
+ function report2(ctx, result) {
8195
+ if (ctx.json) {
8196
+ ctx.out.log(JSON.stringify(result, null, 2));
8197
+ } else if (result.found) {
8198
+ for (const post of result.posts ?? []) {
8199
+ const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
8200
+ ctx.out.log(`\u2014 ${who}
8201
+ ${post.body}`);
8202
+ }
8203
+ for (const topic of result.topics ?? []) {
8204
+ ctx.out.log(`${topic.id} ${topic.appId ?? ""} ${topic.subject}`);
8205
+ }
8206
+ }
8207
+ return result;
8208
+ }
8209
+ var WatchTimeoutError = class extends Error {
8210
+ code = "watch_timeout";
8211
+ constructor() {
8212
+ super("nothing new before the timeout");
8213
+ this.name = "WatchTimeoutError";
8214
+ }
8215
+ };
8216
+
8217
+ // src/discuss-command.ts
8218
+ var ALLOWED = [
8219
+ "config",
8220
+ "token",
8221
+ "email",
8222
+ "json",
8223
+ "app",
8224
+ "subject",
8225
+ "body",
8226
+ "markup",
8227
+ "state",
8228
+ "q",
8229
+ "kinds",
8230
+ "limit",
8231
+ "offset",
8232
+ "reopen",
8233
+ "by",
8234
+ "self",
8235
+ "interval",
8236
+ "timeout"
8237
+ ];
8238
+ function requireId(id, action) {
8239
+ if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
8240
+ return id;
8241
+ }
8242
+ async function buildContext(parsed, deps) {
8243
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8244
+ const doFetch = deps.fetch ?? fetch;
8245
+ const out = deps.stdout ?? console;
8246
+ const token = await getDeveloperToken(
8247
+ cfg,
8248
+ {
8249
+ configPath: cfg.configPath,
8250
+ token: stringOpt(parsed.options.token),
8251
+ email: stringOpt(parsed.options.email),
8252
+ open: false
8253
+ },
8254
+ doFetch,
8255
+ out
8256
+ );
8257
+ return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
8258
+ }
8259
+ async function discussCommand(parsed, deps = {}) {
8260
+ assertArgs(parsed, ALLOWED, 3);
8261
+ const action = parsed.positionals[1];
8262
+ const id = parsed.positionals[2];
8263
+ if (!action) throw new Error('"discuss" needs an action. Run "odla-ai help".');
8264
+ const ctx = await buildContext(parsed, deps);
8265
+ switch (action) {
8266
+ case "groups":
8267
+ return discussGroups(ctx);
8268
+ case "list":
8269
+ case "topics":
8270
+ return discussList(ctx, parsed);
8271
+ case "read":
8272
+ return discussRead(ctx, requireId(id, "read"));
8273
+ case "post":
8274
+ return discussPost(ctx, parsed);
8275
+ case "reply":
8276
+ return discussReply(ctx, requireId(id, "reply"), parsed);
8277
+ case "resolve":
8278
+ return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true);
8279
+ case "who":
8280
+ return discussWho(ctx, parsed);
8281
+ case "watch": {
8282
+ const result = await discussWatch(ctx, id, parsed);
8283
+ if (!result.found) throw new WatchTimeoutError();
8284
+ return;
8285
+ }
8286
+ default:
8287
+ throw new Error(`unknown discuss action "${action}". Run "odla-ai help".`);
8288
+ }
8289
+ }
8290
+
7981
8291
  // src/pm-actions.ts
7982
8292
  var FIELD_MAP = {
7983
8293
  title: { key: "title" },
@@ -8041,7 +8351,7 @@ function statusCol(entity, r) {
8041
8351
  function printRecord(ctx, entity, r) {
8042
8352
  ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
8043
8353
  }
8044
- function emit(ctx, value, human) {
8354
+ function emit2(ctx, value, human) {
8045
8355
  if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
8046
8356
  else human();
8047
8357
  }
@@ -8067,7 +8377,7 @@ async function pmList(ctx, entity, parsed) {
8067
8377
  }
8068
8378
  const qs = q.toString();
8069
8379
  const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
8070
- emit(ctx, page, () => {
8380
+ emit2(ctx, page, () => {
8071
8381
  ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
8072
8382
  ctx.out.log("id state app title");
8073
8383
  for (const r of page.records) printRecord(ctx, entity, r);
@@ -8081,25 +8391,25 @@ async function pmAdd(ctx, entity, parsed) {
8081
8391
  if (entity === "bug" && !input.description)
8082
8392
  throw new Error("pm bug add needs --description <context> (or --body <context>)");
8083
8393
  const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
8084
- emit(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
8394
+ emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
8085
8395
  }
8086
8396
  async function pmGet(ctx, entity, id) {
8087
8397
  const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
8088
- emit(ctx, record5, () => printRecord(ctx, entity, record5));
8398
+ emit2(ctx, record5, () => printRecord(ctx, entity, record5));
8089
8399
  }
8090
8400
  async function pmSet(ctx, entity, id, parsed) {
8091
8401
  const patch2 = collectEntityFields(entity, parsed, true);
8092
8402
  if (Object.keys(patch2).length === 0)
8093
8403
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
8094
8404
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
8095
- emit(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
8405
+ emit2(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
8096
8406
  }
8097
8407
  async function pmDone(ctx, entity, id, parsed) {
8098
8408
  const decisionId = stringOpt(parsed.options.decision);
8099
8409
  if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
8100
8410
  const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
8101
8411
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
8102
- emit(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
8412
+ emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
8103
8413
  }
8104
8414
  async function allRecords(ctx, entity, appId) {
8105
8415
  const records = [];
@@ -8132,7 +8442,7 @@ async function pmHandoff(ctx, parsed) {
8132
8442
  ...handoff,
8133
8443
  clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
8134
8444
  };
8135
- emit(ctx, result, () => {
8445
+ emit2(ctx, result, () => {
8136
8446
  if (result.clean) {
8137
8447
  ctx.out.log(`${appId}: no unresolved PM work`);
8138
8448
  return;
@@ -8165,7 +8475,7 @@ async function pmComments(ctx, entity, id) {
8165
8475
  "GET",
8166
8476
  `/${entity}/${encodeURIComponent(id)}/comments`
8167
8477
  );
8168
- emit(ctx, messages, () => {
8478
+ emit2(ctx, messages, () => {
8169
8479
  if (messages.length === 0) ctx.out.log("(no comments)");
8170
8480
  else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
8171
8481
  });
@@ -8184,7 +8494,7 @@ var ALIASES = {
8184
8494
  decision: "decision",
8185
8495
  bug: "bug"
8186
8496
  };
8187
- var ALLOWED = [
8497
+ var ALLOWED2 = [
8188
8498
  "config",
8189
8499
  "token",
8190
8500
  "email",
@@ -8208,11 +8518,11 @@ var ALLOWED = [
8208
8518
  "offset",
8209
8519
  "q"
8210
8520
  ];
8211
- function requireId(id, action) {
8521
+ function requireId2(id, action) {
8212
8522
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
8213
8523
  return id;
8214
8524
  }
8215
- async function buildContext(parsed, deps) {
8525
+ async function buildContext2(parsed, deps) {
8216
8526
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8217
8527
  const doFetch = deps.fetch ?? fetch;
8218
8528
  const out = deps.stdout ?? console;
@@ -8227,14 +8537,14 @@ async function buildContext(parsed, deps) {
8227
8537
  async function pmCommand(parsed, deps = {}) {
8228
8538
  const word = parsed.positionals[1] ?? "";
8229
8539
  if (word === "handoff") {
8230
- assertArgs(parsed, ALLOWED, 2);
8231
- return pmHandoff(await buildContext(parsed, deps), parsed);
8540
+ assertArgs(parsed, ALLOWED2, 2);
8541
+ return pmHandoff(await buildContext2(parsed, deps), parsed);
8232
8542
  }
8233
8543
  const entity = ALIASES[word];
8234
8544
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
8235
8545
  const action = parsed.positionals[2] ?? "list";
8236
- assertArgs(parsed, ALLOWED, 4);
8237
- const ctx = await buildContext(parsed, deps);
8546
+ assertArgs(parsed, ALLOWED2, 4);
8547
+ const ctx = await buildContext2(parsed, deps);
8238
8548
  const id = parsed.positionals[3];
8239
8549
  switch (action) {
8240
8550
  case "list":
@@ -8243,21 +8553,21 @@ async function pmCommand(parsed, deps = {}) {
8243
8553
  case "create":
8244
8554
  return pmAdd(ctx, entity, parsed);
8245
8555
  case "get":
8246
- return pmGet(ctx, entity, requireId(id, action));
8556
+ return pmGet(ctx, entity, requireId2(id, action));
8247
8557
  case "set":
8248
8558
  case "update":
8249
8559
  case "status":
8250
8560
  case "move":
8251
- return pmSet(ctx, entity, requireId(id, action), parsed);
8561
+ return pmSet(ctx, entity, requireId2(id, action), parsed);
8252
8562
  case "done":
8253
- return pmDone(ctx, entity, requireId(id, action), parsed);
8563
+ return pmDone(ctx, entity, requireId2(id, action), parsed);
8254
8564
  case "comment":
8255
- return pmComment(ctx, entity, requireId(id, action), parsed);
8565
+ return pmComment(ctx, entity, requireId2(id, action), parsed);
8256
8566
  case "comments":
8257
- return pmComments(ctx, entity, requireId(id, action));
8567
+ return pmComments(ctx, entity, requireId2(id, action));
8258
8568
  case "rm":
8259
8569
  case "delete":
8260
- return pmRemove(ctx, entity, requireId(id, action));
8570
+ return pmRemove(ctx, entity, requireId2(id, action));
8261
8571
  default:
8262
8572
  throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
8263
8573
  }
@@ -8773,7 +9083,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
8773
9083
  return out;
8774
9084
  }
8775
9085
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
8776
- function report2(ctx, impacts) {
9086
+ function report3(ctx, impacts) {
8777
9087
  const covered = impacts.filter((i) => i.runbooks.length);
8778
9088
  ctx.out.log(
8779
9089
  `${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.`
@@ -8811,7 +9121,7 @@ async function runbookImpact(ctx, options, deps = {}) {
8811
9121
  }
8812
9122
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
8813
9123
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
8814
- report2(ctx, impacts);
9124
+ report3(ctx, impacts);
8815
9125
  }
8816
9126
 
8817
9127
  // src/runbook-lint.ts
@@ -9044,7 +9354,7 @@ async function whoamiCommand(parsed, deps = {}) {
9044
9354
  }
9045
9355
 
9046
9356
  // src/runbook-command.ts
9047
- var ALLOWED2 = [
9357
+ var ALLOWED3 = [
9048
9358
  "config",
9049
9359
  "token",
9050
9360
  "email",
@@ -9084,7 +9394,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
9084
9394
  }
9085
9395
  return loadProjectConfig(configPath);
9086
9396
  }
9087
- async function buildContext2(parsed, deps, action) {
9397
+ async function buildContext3(parsed, deps, action) {
9088
9398
  const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
9089
9399
  const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
9090
9400
  const doFetch = deps.fetch ?? fetch;
@@ -9140,8 +9450,8 @@ async function buildContext2(parsed, deps, action) {
9140
9450
  }
9141
9451
  async function runbookCommand(parsed, deps = {}) {
9142
9452
  const action = parsed.positionals[1] ?? "list";
9143
- assertArgs(parsed, ALLOWED2, action === "ask" || action === "search" ? 64 : 4);
9144
- const ctx = await buildContext2(parsed, deps, action);
9453
+ assertArgs(parsed, ALLOWED3, action === "ask" || action === "search" ? 64 : 4);
9454
+ const ctx = await buildContext3(parsed, deps, action);
9145
9455
  const slug = parsed.positionals[2];
9146
9456
  switch (action) {
9147
9457
  case "list":
@@ -9332,31 +9642,31 @@ function printHostedJob(out, job, platform, appId) {
9332
9642
  url.searchParams.set("job", job.jobId);
9333
9643
  out.log(` Studio: ${url.toString()}`);
9334
9644
  }
9335
- function printHostedReport(out, report3) {
9336
- out.log(`security report ${report3.jobId}: ${report3.repository}@${report3.revision}`);
9337
- 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}`);
9338
- out.log(` findings: confirmed=${report3.metrics.confirmed} needs_reproduction=${report3.metrics.needsReproduction} candidates=${report3.metrics.candidates} rejected=${report3.metrics.rejected}`);
9339
- out.log(` discovery: ${report3.provenance.discovery?.provider ?? "unknown"}/${report3.provenance.discovery?.model ?? "unknown"}`);
9340
- out.log(` validation: ${report3.provenance.validation?.provider ?? "unknown"}/${report3.provenance.validation?.model ?? "unknown"} independent=${String(report3.provenance.independentValidation)}`);
9341
- for (const finding of report3.findings) {
9645
+ function printHostedReport(out, report4) {
9646
+ out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
9647
+ 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}`);
9648
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
9649
+ out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
9650
+ out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
9651
+ for (const finding of report4.findings) {
9342
9652
  const location = finding.locations[0];
9343
9653
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
9344
9654
  }
9345
- for (const limitation of report3.limitations) out.log(` limitation: ${limitation}`);
9655
+ for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
9346
9656
  }
9347
- function enforceHostedReportGate(report3, parsed, out, emitSuccess) {
9657
+ function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
9348
9658
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9349
9659
  const candidateValue = parsed.options["fail-on-candidates"];
9350
9660
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9351
9661
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
9352
- const confirmed = report3.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
9353
- const leads = failOnCandidates ? report3.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
9354
- const incomplete = report3.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
9662
+ const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
9663
+ const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
9664
+ const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
9355
9665
  if (confirmed.length || leads.length || incomplete) {
9356
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report3.coverageStatus}` : ""}`);
9666
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
9357
9667
  }
9358
9668
  if (emitSuccess) {
9359
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report3.coverageStatus}. This is not proof that the application is secure.`);
9669
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
9360
9670
  }
9361
9671
  }
9362
9672
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -9460,13 +9770,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
9460
9770
  }
9461
9771
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
9462
9772
  }
9463
- const report3 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9773
+ const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9464
9774
  if (parsed.options.json === true) {
9465
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report3 }, null, 2));
9775
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
9466
9776
  } else {
9467
- printHostedReport(context.stdout, report3);
9777
+ printHostedReport(context.stdout, report4);
9468
9778
  }
9469
- enforceHostedReportGate(report3, parsed, context.stdout, parsed.options.json !== true);
9779
+ enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
9470
9780
  }
9471
9781
  async function runLocalSecurityCommand(parsed, dependencies) {
9472
9782
  if (parsed.options.source === true) {
@@ -9507,11 +9817,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9507
9817
  sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
9508
9818
  fetch: doFetch,
9509
9819
  stdout: out,
9510
- getToken: async (request) => {
9511
- if (request.scope === "platform:security:self") {
9820
+ getToken: async (request2) => {
9821
+ if (request2.scope === "platform:security:self") {
9512
9822
  return getScopedPlatformToken({
9513
- platform: request.platform,
9514
- scope: request.scope,
9823
+ platform: request2.platform,
9824
+ scope: request2.scope,
9515
9825
  email: stringOpt(parsed.options.email),
9516
9826
  open,
9517
9827
  fetch: doFetch,
@@ -9520,7 +9830,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9520
9830
  });
9521
9831
  }
9522
9832
  const cfg = await loadProjectConfig(configPath);
9523
- if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
9833
+ if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
9524
9834
  throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
9525
9835
  }
9526
9836
  return getDeveloperToken(
@@ -9533,13 +9843,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9533
9843
  });
9534
9844
  enforceLocalGate(result.report, parsed);
9535
9845
  }
9536
- function enforceLocalGate(report3, parsed) {
9846
+ function enforceLocalGate(report4, parsed) {
9537
9847
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9538
9848
  const candidateValue = parsed.options["fail-on-candidates"];
9539
9849
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9540
- const confirmed = findingsAtOrAbove(report3, failOn);
9541
- const leads = failOnCandidates ? findingsAtOrAbove(report3, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
9542
- const incomplete = report3.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
9850
+ const confirmed = findingsAtOrAbove(report4, failOn);
9851
+ const leads = failOnCandidates ? findingsAtOrAbove(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
9852
+ const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
9543
9853
  if (confirmed.length || leads.length || incomplete) {
9544
9854
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
9545
9855
  }
@@ -9578,9 +9888,9 @@ async function securityCommand(parsed, dependencies) {
9578
9888
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
9579
9889
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
9580
9890
  const context = await hostedSecurityContext(parsed, dependencies);
9581
- const report3 = await getHostedSecurityReport({ ...context, jobId });
9582
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report3, null, 2));
9583
- else printHostedReport(context.stdout, report3);
9891
+ const report4 = await getHostedSecurityReport({ ...context, jobId });
9892
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
9893
+ else printHostedReport(context.stdout, report4);
9584
9894
  return;
9585
9895
  }
9586
9896
  if (sub !== "run") {
@@ -9664,7 +9974,8 @@ async function securityStatus(parsed, dependencies) {
9664
9974
 
9665
9975
  // src/cli.ts
9666
9976
  function exitCodeFor(err) {
9667
- return err?.code === "handshake_pending" ? 75 : 1;
9977
+ const code = err?.code;
9978
+ return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
9668
9979
  }
9669
9980
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9670
9981
  const parsed = parseArgv(argv);
@@ -9712,6 +10023,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9712
10023
  await pmCommand(parsed, dependencies);
9713
10024
  return;
9714
10025
  }
10026
+ if (command === "discuss") {
10027
+ await discussCommand(parsed, dependencies);
10028
+ return;
10029
+ }
9715
10030
  if (command === "provision") {
9716
10031
  await provisionCommand(parsed, dependencies);
9717
10032
  return;
@@ -9831,4 +10146,4 @@ export {
9831
10146
  exitCodeFor,
9832
10147
  runCli
9833
10148
  };
9834
- //# sourceMappingURL=chunk-7P7MI4WX.js.map
10149
+ //# sourceMappingURL=chunk-M4XST3OX.js.map