@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.
@@ -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
  }
@@ -5931,13 +5931,22 @@ Usage:
5931
5931
  odla-ai app owners add <email> [--email <odla-account>] [--json]
5932
5932
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
5933
5933
  odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
5934
- odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--body <text>|--proof <text>]
5934
+ odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
5935
5935
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
5936
- odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>]
5937
- odla-ai pm <goal|task|decision|bug> done <id>
5936
+ odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
5937
+ odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
5938
5938
  odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
5939
5939
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
5940
5940
  odla-ai pm <goal|task|decision|bug> rm <id>
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]
5941
5950
  odla-ai whoami [--json]
5942
5951
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
5943
5952
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -6042,6 +6051,13 @@ Commands:
6042
6051
  "app". Entities: goal (alias conformance), task (alias kanban),
6043
6052
  decision, bug. Status changes and comments post to each item's
6044
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.
6045
6061
  provision Register services, compose integrations, persist credentials, optionally push secrets.
6046
6062
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6047
6063
  skill Same installer; --agent accepts all, claude, codex, cursor,
@@ -6781,10 +6797,25 @@ var COMMAND_SURFACE = {
6781
6797
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
6782
6798
  capabilities: {},
6783
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
+ },
6784
6812
  doctor: {},
6785
6813
  help: {},
6786
6814
  init: {},
6787
- pm: PM_ENTITIES,
6815
+ pm: {
6816
+ ...PM_ENTITIES,
6817
+ handoff: {}
6818
+ },
6788
6819
  provision: {},
6789
6820
  runbook: {
6790
6821
  ask: {},
@@ -6927,17 +6958,17 @@ async function runHostedSecurity(options) {
6927
6958
  allowNetwork: false
6928
6959
  }
6929
6960
  });
6930
- const report3 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
6931
- await writeSecurityArtifacts(output, report3);
6932
- 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);
6933
6964
  await hosted.complete({
6934
6965
  reportDigest,
6935
- coverageStatus: report3.coverageStatus,
6936
- confirmed: report3.metrics.confirmed,
6937
- candidates: report3.metrics.candidates
6966
+ coverageStatus: report4.coverageStatus,
6967
+ confirmed: report4.metrics.confirmed,
6968
+ candidates: report4.metrics.candidates
6938
6969
  }, { signal: options.signal });
6939
- printSummary(options.stdout ?? console, appId, env, hosted.run, report3, output);
6940
- 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 });
6941
6972
  }
6942
6973
  function selectEnv(requested, declared, configPath, rootDir) {
6943
6974
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -6947,11 +6978,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
6947
6978
  }
6948
6979
  return env;
6949
6980
  }
6950
- async function injectedToken(options, request) {
6951
- 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 }));
6952
6983
  if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
6953
6984
  throw new Error(
6954
- 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"
6955
6986
  );
6956
6987
  }
6957
6988
  return value;
@@ -6963,14 +6994,14 @@ function profileFor(name, maxHuntTasks) {
6963
6994
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
6964
6995
  return { ...profile, maxHuntTasks };
6965
6996
  }
6966
- function printSummary(out, appId, env, run, report3, output) {
6967
- 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;
6968
6999
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
6969
7000
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
6970
7001
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
6971
- 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}`);
6972
- if (report3.callBudget) out.log(` calls: discovery=${formatBudget(report3.callBudget.discovery)} validation=${formatBudget(report3.callBudget.validation)}`);
6973
- 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}`);
6974
7005
  out.log(` report: ${resolve9(output, "REPORT.md")}`);
6975
7006
  }
6976
7007
  function formatBudget(usage) {
@@ -7974,6 +8005,289 @@ async function codeCommand(parsed, dependencies) {
7974
8005
  });
7975
8006
  }
7976
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.resolvedAt ? "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
+
7977
8291
  // src/pm-actions.ts
7978
8292
  var FIELD_MAP = {
7979
8293
  title: { key: "title" },
@@ -7982,6 +8296,7 @@ var FIELD_MAP = {
7982
8296
  column: { key: "column" },
7983
8297
  rank: { key: "rank", num: true },
7984
8298
  goal: { key: "goalId" },
8299
+ decision: { key: "decisionId" },
7985
8300
  assignee: { key: "assigneeId" },
7986
8301
  due: { key: "dueAt", num: true },
7987
8302
  proof: { key: "proof" },
@@ -8020,6 +8335,14 @@ function collectFields(parsed, allowClear) {
8020
8335
  }
8021
8336
  return out;
8022
8337
  }
8338
+ function collectEntityFields(entity, parsed, allowClear) {
8339
+ const fields = collectFields(parsed, allowClear);
8340
+ if ((entity === "task" || entity === "bug") && fields.body !== void 0) {
8341
+ if (fields.description === void 0) fields.description = fields.body;
8342
+ delete fields.body;
8343
+ }
8344
+ return fields;
8345
+ }
8023
8346
  function statusCol(entity, r) {
8024
8347
  if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
8025
8348
  if (entity === "task") return String(r.column ?? "");
@@ -8028,13 +8351,20 @@ function statusCol(entity, r) {
8028
8351
  function printRecord(ctx, entity, r) {
8029
8352
  ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
8030
8353
  }
8031
- function emit(ctx, value, human) {
8354
+ function emit2(ctx, value, human) {
8032
8355
  if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
8033
8356
  else human();
8034
8357
  }
8035
8358
  async function pmList(ctx, entity, parsed) {
8036
8359
  const q = new URLSearchParams();
8037
- const filters = { status: "status", severity: "severity", column: "column", goal: "goalId", assignee: "assigneeId" };
8360
+ const filters = {
8361
+ status: "status",
8362
+ severity: "severity",
8363
+ column: "column",
8364
+ goal: "goalId",
8365
+ assignee: "assigneeId",
8366
+ decision: "decisionId"
8367
+ };
8038
8368
  const app = stringOpt(parsed.options.app);
8039
8369
  if (app) q.set("app", app);
8040
8370
  for (const [flag, param] of Object.entries(filters)) {
@@ -8047,7 +8377,7 @@ async function pmList(ctx, entity, parsed) {
8047
8377
  }
8048
8378
  const qs = q.toString();
8049
8379
  const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
8050
- emit(ctx, page, () => {
8380
+ emit2(ctx, page, () => {
8051
8381
  ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
8052
8382
  ctx.out.log("id state app title");
8053
8383
  for (const r of page.records) printRecord(ctx, entity, r);
@@ -8056,25 +8386,82 @@ async function pmList(ctx, entity, parsed) {
8056
8386
  async function pmAdd(ctx, entity, parsed) {
8057
8387
  const appId = stringOpt(parsed.options.app);
8058
8388
  if (!appId) throw new Error("pm add needs --app <appId>");
8059
- const input = collectFields(parsed, false);
8389
+ const input = collectEntityFields(entity, parsed, false);
8060
8390
  if (!input.title) throw new Error("pm add needs --title <title>");
8391
+ if (entity === "bug" && !input.description)
8392
+ throw new Error("pm bug add needs --description <context> (or --body <context>)");
8061
8393
  const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
8062
- emit(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
8394
+ emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
8063
8395
  }
8064
8396
  async function pmGet(ctx, entity, id) {
8065
8397
  const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
8066
- emit(ctx, record5, () => printRecord(ctx, entity, record5));
8398
+ emit2(ctx, record5, () => printRecord(ctx, entity, record5));
8067
8399
  }
8068
8400
  async function pmSet(ctx, entity, id, parsed) {
8069
- const patch2 = collectFields(parsed, true);
8401
+ const patch2 = collectEntityFields(entity, parsed, true);
8070
8402
  if (Object.keys(patch2).length === 0)
8071
8403
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
8072
8404
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
8073
- 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}`));
8074
8406
  }
8075
- async function pmDone(ctx, entity, id) {
8076
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: DONE[entity] });
8077
- emit(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
8407
+ async function pmDone(ctx, entity, id, parsed) {
8408
+ const decisionId = stringOpt(parsed.options.decision);
8409
+ if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
8410
+ const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
8411
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
8412
+ emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
8413
+ }
8414
+ async function allRecords(ctx, entity, appId) {
8415
+ const records = [];
8416
+ for (; ; ) {
8417
+ const q = new URLSearchParams({
8418
+ app: appId,
8419
+ limit: "100",
8420
+ offset: String(records.length)
8421
+ });
8422
+ const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
8423
+ records.push(...page.records);
8424
+ if (records.length >= page.total || page.records.length === 0) return records;
8425
+ }
8426
+ }
8427
+ async function pmHandoff(ctx, parsed) {
8428
+ const appId = stringOpt(parsed.options.app);
8429
+ if (!appId) throw new Error("pm handoff needs --app <appId>");
8430
+ const [goals, tasks, bugs] = await Promise.all([
8431
+ allRecords(ctx, "goal", appId),
8432
+ allRecords(ctx, "task", appId),
8433
+ allRecords(ctx, "bug", appId)
8434
+ ]);
8435
+ const handoff = {
8436
+ appId,
8437
+ unmetGoals: goals.filter((record5) => record5.status !== "met"),
8438
+ activeTasks: tasks.filter((record5) => record5.column !== "done"),
8439
+ openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
8440
+ };
8441
+ const result = {
8442
+ ...handoff,
8443
+ clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
8444
+ };
8445
+ emit2(ctx, result, () => {
8446
+ if (result.clean) {
8447
+ ctx.out.log(`${appId}: no unresolved PM work`);
8448
+ return;
8449
+ }
8450
+ ctx.out.log(`${appId}: authoritative PM handoff`);
8451
+ for (const [label, records] of [
8452
+ ["unmet goals", result.unmetGoals],
8453
+ ["active tasks", result.activeTasks],
8454
+ ["open bugs", result.openBugs]
8455
+ ]) {
8456
+ ctx.out.log(`${label}:`);
8457
+ if (!records.length) ctx.out.log("- (none)");
8458
+ else for (const record5 of records) printRecord(
8459
+ ctx,
8460
+ label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
8461
+ record5
8462
+ );
8463
+ }
8464
+ });
8078
8465
  }
8079
8466
  async function pmComment(ctx, entity, id, parsed) {
8080
8467
  const body = stringOpt(parsed.options.body);
@@ -8088,7 +8475,7 @@ async function pmComments(ctx, entity, id) {
8088
8475
  "GET",
8089
8476
  `/${entity}/${encodeURIComponent(id)}/comments`
8090
8477
  );
8091
- emit(ctx, messages, () => {
8478
+ emit2(ctx, messages, () => {
8092
8479
  if (messages.length === 0) ctx.out.log("(no comments)");
8093
8480
  else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
8094
8481
  });
@@ -8107,7 +8494,7 @@ var ALIASES = {
8107
8494
  decision: "decision",
8108
8495
  bug: "bug"
8109
8496
  };
8110
- var ALLOWED = [
8497
+ var ALLOWED2 = [
8111
8498
  "config",
8112
8499
  "token",
8113
8500
  "email",
@@ -8126,15 +8513,16 @@ var ALLOWED = [
8126
8513
  "target",
8127
8514
  "description",
8128
8515
  "desc",
8516
+ "decision",
8129
8517
  "limit",
8130
8518
  "offset",
8131
8519
  "q"
8132
8520
  ];
8133
- function requireId(id, action) {
8521
+ function requireId2(id, action) {
8134
8522
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
8135
8523
  return id;
8136
8524
  }
8137
- async function buildContext(parsed, deps) {
8525
+ async function buildContext2(parsed, deps) {
8138
8526
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8139
8527
  const doFetch = deps.fetch ?? fetch;
8140
8528
  const out = deps.stdout ?? console;
@@ -8148,11 +8536,15 @@ async function buildContext(parsed, deps) {
8148
8536
  }
8149
8537
  async function pmCommand(parsed, deps = {}) {
8150
8538
  const word = parsed.positionals[1] ?? "";
8539
+ if (word === "handoff") {
8540
+ assertArgs(parsed, ALLOWED2, 2);
8541
+ return pmHandoff(await buildContext2(parsed, deps), parsed);
8542
+ }
8151
8543
  const entity = ALIASES[word];
8152
8544
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
8153
8545
  const action = parsed.positionals[2] ?? "list";
8154
- assertArgs(parsed, ALLOWED, 4);
8155
- const ctx = await buildContext(parsed, deps);
8546
+ assertArgs(parsed, ALLOWED2, 4);
8547
+ const ctx = await buildContext2(parsed, deps);
8156
8548
  const id = parsed.positionals[3];
8157
8549
  switch (action) {
8158
8550
  case "list":
@@ -8161,21 +8553,21 @@ async function pmCommand(parsed, deps = {}) {
8161
8553
  case "create":
8162
8554
  return pmAdd(ctx, entity, parsed);
8163
8555
  case "get":
8164
- return pmGet(ctx, entity, requireId(id, action));
8556
+ return pmGet(ctx, entity, requireId2(id, action));
8165
8557
  case "set":
8166
8558
  case "update":
8167
8559
  case "status":
8168
8560
  case "move":
8169
- return pmSet(ctx, entity, requireId(id, action), parsed);
8561
+ return pmSet(ctx, entity, requireId2(id, action), parsed);
8170
8562
  case "done":
8171
- return pmDone(ctx, entity, requireId(id, action));
8563
+ return pmDone(ctx, entity, requireId2(id, action), parsed);
8172
8564
  case "comment":
8173
- return pmComment(ctx, entity, requireId(id, action), parsed);
8565
+ return pmComment(ctx, entity, requireId2(id, action), parsed);
8174
8566
  case "comments":
8175
- return pmComments(ctx, entity, requireId(id, action));
8567
+ return pmComments(ctx, entity, requireId2(id, action));
8176
8568
  case "rm":
8177
8569
  case "delete":
8178
- return pmRemove(ctx, entity, requireId(id, action));
8570
+ return pmRemove(ctx, entity, requireId2(id, action));
8179
8571
  default:
8180
8572
  throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
8181
8573
  }
@@ -8691,7 +9083,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
8691
9083
  return out;
8692
9084
  }
8693
9085
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
8694
- function report2(ctx, impacts) {
9086
+ function report3(ctx, impacts) {
8695
9087
  const covered = impacts.filter((i) => i.runbooks.length);
8696
9088
  ctx.out.log(
8697
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.`
@@ -8729,7 +9121,7 @@ async function runbookImpact(ctx, options, deps = {}) {
8729
9121
  }
8730
9122
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
8731
9123
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
8732
- report2(ctx, impacts);
9124
+ report3(ctx, impacts);
8733
9125
  }
8734
9126
 
8735
9127
  // src/runbook-lint.ts
@@ -8962,7 +9354,7 @@ async function whoamiCommand(parsed, deps = {}) {
8962
9354
  }
8963
9355
 
8964
9356
  // src/runbook-command.ts
8965
- var ALLOWED2 = [
9357
+ var ALLOWED3 = [
8966
9358
  "config",
8967
9359
  "token",
8968
9360
  "email",
@@ -9002,7 +9394,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
9002
9394
  }
9003
9395
  return loadProjectConfig(configPath);
9004
9396
  }
9005
- async function buildContext2(parsed, deps, action) {
9397
+ async function buildContext3(parsed, deps, action) {
9006
9398
  const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
9007
9399
  const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
9008
9400
  const doFetch = deps.fetch ?? fetch;
@@ -9058,8 +9450,8 @@ async function buildContext2(parsed, deps, action) {
9058
9450
  }
9059
9451
  async function runbookCommand(parsed, deps = {}) {
9060
9452
  const action = parsed.positionals[1] ?? "list";
9061
- assertArgs(parsed, ALLOWED2, action === "ask" || action === "search" ? 64 : 4);
9062
- 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);
9063
9455
  const slug = parsed.positionals[2];
9064
9456
  switch (action) {
9065
9457
  case "list":
@@ -9250,31 +9642,31 @@ function printHostedJob(out, job, platform, appId) {
9250
9642
  url.searchParams.set("job", job.jobId);
9251
9643
  out.log(` Studio: ${url.toString()}`);
9252
9644
  }
9253
- function printHostedReport(out, report3) {
9254
- out.log(`security report ${report3.jobId}: ${report3.repository}@${report3.revision}`);
9255
- 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}`);
9256
- out.log(` findings: confirmed=${report3.metrics.confirmed} needs_reproduction=${report3.metrics.needsReproduction} candidates=${report3.metrics.candidates} rejected=${report3.metrics.rejected}`);
9257
- out.log(` discovery: ${report3.provenance.discovery?.provider ?? "unknown"}/${report3.provenance.discovery?.model ?? "unknown"}`);
9258
- out.log(` validation: ${report3.provenance.validation?.provider ?? "unknown"}/${report3.provenance.validation?.model ?? "unknown"} independent=${String(report3.provenance.independentValidation)}`);
9259
- 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) {
9260
9652
  const location = finding.locations[0];
9261
9653
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
9262
9654
  }
9263
- for (const limitation of report3.limitations) out.log(` limitation: ${limitation}`);
9655
+ for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
9264
9656
  }
9265
- function enforceHostedReportGate(report3, parsed, out, emitSuccess) {
9657
+ function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
9266
9658
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9267
9659
  const candidateValue = parsed.options["fail-on-candidates"];
9268
9660
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9269
9661
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
9270
- const confirmed = report3.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
9271
- const leads = failOnCandidates ? report3.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
9272
- 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;
9273
9665
  if (confirmed.length || leads.length || incomplete) {
9274
- 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}` : ""}`);
9275
9667
  }
9276
9668
  if (emitSuccess) {
9277
- 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.`);
9278
9670
  }
9279
9671
  }
9280
9672
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -9378,13 +9770,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
9378
9770
  }
9379
9771
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
9380
9772
  }
9381
- const report3 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9773
+ const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
9382
9774
  if (parsed.options.json === true) {
9383
- 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));
9384
9776
  } else {
9385
- printHostedReport(context.stdout, report3);
9777
+ printHostedReport(context.stdout, report4);
9386
9778
  }
9387
- enforceHostedReportGate(report3, parsed, context.stdout, parsed.options.json !== true);
9779
+ enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
9388
9780
  }
9389
9781
  async function runLocalSecurityCommand(parsed, dependencies) {
9390
9782
  if (parsed.options.source === true) {
@@ -9425,11 +9817,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9425
9817
  sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
9426
9818
  fetch: doFetch,
9427
9819
  stdout: out,
9428
- getToken: async (request) => {
9429
- if (request.scope === "platform:security:self") {
9820
+ getToken: async (request2) => {
9821
+ if (request2.scope === "platform:security:self") {
9430
9822
  return getScopedPlatformToken({
9431
- platform: request.platform,
9432
- scope: request.scope,
9823
+ platform: request2.platform,
9824
+ scope: request2.scope,
9433
9825
  email: stringOpt(parsed.options.email),
9434
9826
  open,
9435
9827
  fetch: doFetch,
@@ -9438,7 +9830,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9438
9830
  });
9439
9831
  }
9440
9832
  const cfg = await loadProjectConfig(configPath);
9441
- if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
9833
+ if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
9442
9834
  throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
9443
9835
  }
9444
9836
  return getDeveloperToken(
@@ -9451,13 +9843,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
9451
9843
  });
9452
9844
  enforceLocalGate(result.report, parsed);
9453
9845
  }
9454
- function enforceLocalGate(report3, parsed) {
9846
+ function enforceLocalGate(report4, parsed) {
9455
9847
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
9456
9848
  const candidateValue = parsed.options["fail-on-candidates"];
9457
9849
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
9458
- const confirmed = findingsAtOrAbove(report3, failOn);
9459
- const leads = failOnCandidates ? findingsAtOrAbove(report3, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
9460
- 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;
9461
9853
  if (confirmed.length || leads.length || incomplete) {
9462
9854
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
9463
9855
  }
@@ -9496,9 +9888,9 @@ async function securityCommand(parsed, dependencies) {
9496
9888
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
9497
9889
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
9498
9890
  const context = await hostedSecurityContext(parsed, dependencies);
9499
- const report3 = await getHostedSecurityReport({ ...context, jobId });
9500
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report3, null, 2));
9501
- 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);
9502
9894
  return;
9503
9895
  }
9504
9896
  if (sub !== "run") {
@@ -9582,7 +9974,8 @@ async function securityStatus(parsed, dependencies) {
9582
9974
 
9583
9975
  // src/cli.ts
9584
9976
  function exitCodeFor(err) {
9585
- return err?.code === "handshake_pending" ? 75 : 1;
9977
+ const code = err?.code;
9978
+ return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
9586
9979
  }
9587
9980
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9588
9981
  const parsed = parseArgv(argv);
@@ -9630,6 +10023,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9630
10023
  await pmCommand(parsed, dependencies);
9631
10024
  return;
9632
10025
  }
10026
+ if (command === "discuss") {
10027
+ await discussCommand(parsed, dependencies);
10028
+ return;
10029
+ }
9633
10030
  if (command === "provision") {
9634
10031
  await provisionCommand(parsed, dependencies);
9635
10032
  return;
@@ -9749,4 +10146,4 @@ export {
9749
10146
  exitCodeFor,
9750
10147
  runCli
9751
10148
  };
9752
- //# sourceMappingURL=chunk-RZDRUVBT.js.map
10149
+ //# sourceMappingURL=chunk-HG63GLGU.js.map