@odla-ai/cli 0.30.3 → 0.31.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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-3HFNKDZV.js";
4
+ } from "./chunk-Y3W7YKWI.js";
5
5
  import {
6
6
  exitCodeFor
7
7
  } from "./chunk-UKLSRQ5J.js";
@@ -9,4 +9,4 @@ export {
9
9
  exitCodeFor,
10
10
  runCli
11
11
  };
12
- //# sourceMappingURL=cli-BGSX5Z4C.js.map
12
+ //# sourceMappingURL=cli-Q2U7TCMV.js.map
package/dist/index.cjs CHANGED
@@ -149,7 +149,7 @@ var import_node_process = __toESM(require("process"), 1);
149
149
  async function openUrl(url, options = {}) {
150
150
  const command = openerFor(options.platform ?? import_node_process.default.platform);
151
151
  const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
152
- await new Promise((resolve13, reject) => {
152
+ await new Promise((resolve14, reject) => {
153
153
  const child = doSpawn(command.cmd, [...command.args, url], {
154
154
  stdio: "ignore",
155
155
  detached: true
@@ -157,7 +157,7 @@ async function openUrl(url, options = {}) {
157
157
  child.once("error", reject);
158
158
  child.once("spawn", () => {
159
159
  child.unref();
160
- resolve13();
160
+ resolve14();
161
161
  });
162
162
  });
163
163
  }
@@ -452,7 +452,7 @@ async function freshHandshake(ctx, waitMs) {
452
452
  } catch (err) {
453
453
  const code = err instanceof import_db.OdlaError ? err.code : void 0;
454
454
  if (code === "handshake_pending" && started) throw stillPending(started, ctx.email);
455
- if (code === "handshake_denied" || code === "handshake_expired" || code === "handshake_timeout") {
455
+ if (code === "handshake_duplicate" || code === "handshake_denied" || code === "handshake_expired" || code === "handshake_timeout") {
456
456
  clearPendingHandshake(ctx.pendingFile);
457
457
  }
458
458
  throw err;
@@ -2598,8 +2598,8 @@ function credential(value2) {
2598
2598
  // src/calendar-poll.ts
2599
2599
  async function waitForCalendarPoll(milliseconds, signal) {
2600
2600
  if (signal?.aborted) throw signal.reason ?? new Error("calendar connection aborted");
2601
- await new Promise((resolve13, reject) => {
2602
- const timer = setTimeout(resolve13, milliseconds);
2601
+ await new Promise((resolve14, reject) => {
2602
+ const timer = setTimeout(resolve14, milliseconds);
2603
2603
  signal?.addEventListener("abort", () => {
2604
2604
  clearTimeout(timer);
2605
2605
  reject(signal.reason ?? new Error("calendar connection aborted"));
@@ -3305,7 +3305,7 @@ async function configOperationWait(options) {
3305
3305
  assertOperationId(options.operationId);
3306
3306
  const cfg = await loadProjectConfig(options.configPath);
3307
3307
  const client = await operationClient(cfg, options, "wait");
3308
- const wait2 = options.pollWait ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms)));
3308
+ const wait2 = options.pollWait ?? ((ms) => new Promise((resolve14) => setTimeout(resolve14, ms)));
3309
3309
  const now = () => (options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
3310
3310
  const deadline = now() + (options.timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3;
3311
3311
  const interval = (options.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS) * 1e3;
@@ -8156,8 +8156,8 @@ function hostedPollTimeout(value2 = 10 * 6e4) {
8156
8156
  }
8157
8157
  async function waitForHostedPoll(milliseconds, signal) {
8158
8158
  if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
8159
- await new Promise((resolve13, reject) => {
8160
- const timer = setTimeout(resolve13, milliseconds);
8159
+ await new Promise((resolve14, reject) => {
8160
+ const timer = setTimeout(resolve14, milliseconds);
8161
8161
  signal?.addEventListener("abort", () => {
8162
8162
  clearTimeout(timer);
8163
8163
  reject(signal.reason ?? new DOMException("aborted", "AbortError"));
@@ -8904,13 +8904,16 @@ Usage:
8904
8904
  odla-ai app rename <name> [continue in Studio; human session required]
8905
8905
  odla-ai app owners <list|add|remove> [...] [continue in Studio; human session required]
8906
8906
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
8907
- odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8907
+ odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
8908
+ odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
8909
+ odla-ai pm project use <project-id> [--json] [saved locally in this worktree]
8910
+ odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8908
8911
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8909
8912
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8910
8913
  odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8911
8914
  odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
8912
8915
  odla-ai pm task add --app <id> --title <t> [--column <backlog|ready|doing|review|done>] [--goal <id>|--alignment-decision <id>] [--assignee <id>] [--description <text>|--body <text>] [--acceptance <text>] [--execution <human|agent|either>] [--due <epoch-ms>] [--mutation-id <id>] [--json]
8913
- odla-ai pm next --app <id> [--json]
8916
+ odla-ai pm next --app <id> [--project <id>] [--json]
8914
8917
  odla-ai pm watch --app <id> [--cursor <cursor>] [--entity goal|task|decision|bug] [--action created|updated|deleted|comment.created|comment.updated] [--state <state>] [--by <principalId>] [--self <principalId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
8915
8918
  odla-ai pm task ready <id> --expected-revision <n> [--goal <id>|--alignment-decision <id>] [--description <text>|--body <text>] [--acceptance <text>] [--execution <human|agent|either>] [--mutation-id <id>] [--json]
8916
8919
  odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
@@ -8930,7 +8933,7 @@ Usage:
8930
8933
  odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
8931
8934
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
8932
8935
  odla-ai pm <goal|task|decision|bug> rm <id>
8933
- odla-ai pm handoff --app <id> [--json]
8936
+ odla-ai pm handoff --app <id> [--project <id>] [--json]
8934
8937
  odla-ai discuss groups [--json]
8935
8938
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
8936
8939
  odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
@@ -9063,13 +9066,12 @@ Commands:
9063
9066
  code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
9064
9067
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
9065
9068
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
9066
- pm Project management (via @odla-ai/pm) shared across the apps you
9067
- co-own: conformance goals, kanban tasks, decisions, and bugs. With
9068
- no --app, "list" spans every co-owned project (the cross-project
9069
- view); with --app it scopes to one. Same device-grant auth as
9070
- "app". Entities: goal (alias conformance), task (alias kanban),
9071
- decision, bug. Status changes and comments post to each item's
9072
- @odla-ai/chat discussion thread.
9069
+ pm Project management (via @odla-ai/pm): Products contain Projects;
9070
+ projects contain goals, kanban tasks, decisions, and bugs. Use
9071
+ "pm project list|add|use" to select worktree-local context, or
9072
+ pass --app/--project explicitly. Same device-grant auth as "app".
9073
+ Status changes and comments post to each item's @odla-ai/chat
9074
+ discussion thread.
9073
9075
  bug Intent-first alias for PM bugs. "bug report" writes to
9074
9076
  odla PM; odla product defects do not belong in GitHub Issues.
9075
9077
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
@@ -9476,7 +9478,7 @@ function jsonl(ctx, parsed, value2) {
9476
9478
  }
9477
9479
  async function discussWatch(ctx, topicId, parsed) {
9478
9480
  if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
9479
- const sleep = ctx.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms)));
9481
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve14) => setTimeout(resolve14, ms)));
9480
9482
  const now = ctx.now ?? Date.now;
9481
9483
  const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
9482
9484
  const timeoutSeconds = numberOpt2(parsed, "timeout");
@@ -9824,6 +9826,8 @@ async function pmList(ctx, entity, parsed) {
9824
9826
  };
9825
9827
  const app = stringOpt(parsed.options.app) ?? ctx.appId;
9826
9828
  if (app) q.set("app", app);
9829
+ const project = stringOpt(parsed.options.project) ?? ctx.projectId;
9830
+ if (project) q.set("project", project);
9827
9831
  for (const [flag, param] of Object.entries(filters)) {
9828
9832
  const raw = stringOpt(parsed.options[flag]);
9829
9833
  const v = entity === "task" && flag === "column" && raw === "ready" ? "todo" : raw;
@@ -9843,6 +9847,7 @@ async function pmList(ctx, entity, parsed) {
9843
9847
  }
9844
9848
  async function pmAdd(ctx, entity, parsed) {
9845
9849
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9850
+ const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
9846
9851
  if (!appId) throw new Error("pm add needs --app <appId>");
9847
9852
  const input = collectEntityFields(entity, parsed, false);
9848
9853
  if (!input.title) throw new Error("pm add needs --title <title>");
@@ -9850,6 +9855,7 @@ async function pmAdd(ctx, entity, parsed) {
9850
9855
  throw new Error("pm bug add needs --description <context> (or --body <context>)");
9851
9856
  const res = await pmRequest(ctx, "POST", `/${entity}`, {
9852
9857
  appId,
9858
+ ...projectId ? { projectId } : {},
9853
9859
  input,
9854
9860
  mutationId: writeMutationId2(parsed)
9855
9861
  });
@@ -9929,7 +9935,7 @@ async function pmTaskLifecycle(ctx, id, action2, parsed) {
9929
9935
  ctx.out.log(`task: ${label} \u2192 ${state2}`);
9930
9936
  });
9931
9937
  }
9932
- async function allRecords(ctx, entity, appId) {
9938
+ async function allRecords(ctx, entity, appId, projectId) {
9933
9939
  const records = [];
9934
9940
  for (; ; ) {
9935
9941
  const q = new URLSearchParams({
@@ -9937,6 +9943,7 @@ async function allRecords(ctx, entity, appId) {
9937
9943
  limit: "100",
9938
9944
  offset: String(records.length)
9939
9945
  });
9946
+ if (projectId) q.set("project", projectId);
9940
9947
  const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
9941
9948
  records.push(...page2.records);
9942
9949
  if (records.length >= page2.total || page2.records.length === 0) return records;
@@ -9944,13 +9951,15 @@ async function allRecords(ctx, entity, appId) {
9944
9951
  }
9945
9952
  async function pmNext(ctx, parsed) {
9946
9953
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9954
+ const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
9947
9955
  if (!appId) throw new Error("pm next needs --app <appId>");
9948
9956
  const [goals, tasks] = await Promise.all([
9949
- allRecords(ctx, "goal", appId),
9950
- allRecords(ctx, "task", appId)
9957
+ allRecords(ctx, "goal", appId, projectId),
9958
+ allRecords(ctx, "task", appId, projectId)
9951
9959
  ]);
9952
9960
  const result = {
9953
9961
  appId,
9962
+ projectId,
9954
9963
  openGoals: goals.filter((record10) => record10.status === "open"),
9955
9964
  doing: tasks.filter((record10) => record10.column === "doing"),
9956
9965
  ready: tasks.filter((record10) => record10.column === "todo")
@@ -9981,14 +9990,16 @@ async function pmNext(ctx, parsed) {
9981
9990
  }
9982
9991
  async function pmHandoff(ctx, parsed) {
9983
9992
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9993
+ const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
9984
9994
  if (!appId) throw new Error("pm handoff needs --app <appId>");
9985
9995
  const [goals, tasks, bugs] = await Promise.all([
9986
- allRecords(ctx, "goal", appId),
9987
- allRecords(ctx, "task", appId),
9988
- allRecords(ctx, "bug", appId)
9996
+ allRecords(ctx, "goal", appId, projectId),
9997
+ allRecords(ctx, "task", appId, projectId),
9998
+ allRecords(ctx, "bug", appId, projectId)
9989
9999
  ]);
9990
10000
  const handoff = {
9991
10001
  appId,
10002
+ projectId,
9992
10003
  unmetGoals: goals.filter((record10) => record10.status !== "met"),
9993
10004
  activeTasks: tasks.filter((record10) => record10.column !== "done"),
9994
10005
  openBugs: bugs.filter((record10) => record10.status !== "fixed" && record10.status !== "wontfix")
@@ -10153,7 +10164,7 @@ async function pmWatch(ctx, parsed) {
10153
10164
  }
10154
10165
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
10155
10166
  if (!appId) throw new Error("pm watch needs --app <appId>");
10156
- const sleep = ctx.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms)));
10167
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve14) => setTimeout(resolve14, ms)));
10157
10168
  const now = ctx.now ?? Date.now;
10158
10169
  const intervalMs = (positiveNumber(parsed, "interval", DEFAULT_INTERVAL_MS2 / 1e3) ?? DEFAULT_INTERVAL_MS2 / 1e3) * 1e3;
10159
10170
  const timeoutSeconds = positiveNumber(parsed, "timeout");
@@ -10262,6 +10273,56 @@ async function pmWatch(ctx, parsed) {
10262
10273
  }
10263
10274
  }
10264
10275
 
10276
+ // src/pm-project-context.ts
10277
+ var import_node_path16 = require("path");
10278
+ var pmProjectContextFile = (rootDir) => (0, import_node_path16.resolve)(rootDir, ".odla", "pm-project.local.json");
10279
+ function readPmProjectContext(rootDir) {
10280
+ const value2 = readJsonFile(pmProjectContextFile(rootDir));
10281
+ return value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" ? value2 : null;
10282
+ }
10283
+ function writePmProjectContext(rootDir, value2) {
10284
+ writePrivateJson(pmProjectContextFile(rootDir), { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() });
10285
+ }
10286
+
10287
+ // src/pm-project-actions.ts
10288
+ async function pmProjectList(ctx, parsed) {
10289
+ const q = new URLSearchParams();
10290
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
10291
+ if (app) q.set("app", app);
10292
+ for (const key of ["status", "limit", "offset"]) {
10293
+ const value2 = stringOpt(parsed.options[key]);
10294
+ if (value2) q.set(key, value2);
10295
+ }
10296
+ const page2 = await pmRequest(ctx, "GET", `/project?${q}`);
10297
+ emit2(ctx, page2, () => {
10298
+ ctx.out.log(`projects \u2014 ${page2.records.length} of ${page2.total}`);
10299
+ ctx.out.log("id status product name");
10300
+ for (const project of page2.records) {
10301
+ ctx.out.log(`${project.id} ${project.status} ${project.appId} ${project.name}${project.isDefault ? " (default)" : ""}`);
10302
+ }
10303
+ });
10304
+ }
10305
+ async function pmProjectAdd(ctx, parsed) {
10306
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
10307
+ const name = stringOpt(parsed.options.name);
10308
+ if (!appId) throw new Error("pm project add needs --app <productId>");
10309
+ if (!name) throw new Error("pm project add needs --name <name>");
10310
+ const result = await pmRequest(ctx, "POST", "/project", {
10311
+ appId,
10312
+ name,
10313
+ description: stringOpt(parsed.options.description),
10314
+ mutationId: writeMutationId2(parsed)
10315
+ });
10316
+ emit2(ctx, result, () => ctx.out.log(`created project: ${result.project.name} (${result.project.id})`));
10317
+ }
10318
+ async function pmProjectUse(ctx, id) {
10319
+ if (!ctx.rootDir) throw new Error("pm project use needs a local project directory");
10320
+ const { project } = await pmRequest(ctx, "GET", `/project/${encodeURIComponent(id)}`);
10321
+ if (project.status !== "active") throw new Error(`project ${project.name} is ${project.status}, not active`);
10322
+ writePmProjectContext(ctx.rootDir, { appId: project.appId, projectId: project.id });
10323
+ emit2(ctx, project, () => ctx.out.log(`using ${project.appId} / ${project.name} (${project.id}) in this worktree`));
10324
+ }
10325
+
10265
10326
  // src/pm-command.ts
10266
10327
  var ALIASES = {
10267
10328
  goal: "goal",
@@ -10273,8 +10334,8 @@ var ALIASES = {
10273
10334
  };
10274
10335
  var COMMON_OPTIONS = ["config", "token", "email", "json", "platform", "context", "open"];
10275
10336
  var ACTION_OPTIONS = {
10276
- list: ["app", "q", "limit", "offset"],
10277
- add: ["app", "title", "mutation-id"],
10337
+ list: ["app", "project", "q", "limit", "offset"],
10338
+ add: ["app", "project", "title", "mutation-id"],
10278
10339
  get: [],
10279
10340
  set: ["mutation-id"],
10280
10341
  done: ["mutation-id"],
@@ -10346,6 +10407,9 @@ async function buildContext2(parsed, deps) {
10346
10407
  doFetch,
10347
10408
  out
10348
10409
  );
10410
+ const selectedProject = readPmProjectContext(cfg.rootDir);
10411
+ const requestedApp = stringOpt(parsed.options.app) ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0);
10412
+ const compatibleProject = selectedProject && (!requestedApp || selectedProject.appId === requestedApp) ? selectedProject : null;
10349
10413
  return {
10350
10414
  platformUrl: cfg.platformUrl,
10351
10415
  token,
@@ -10354,15 +10418,33 @@ async function buildContext2(parsed, deps) {
10354
10418
  json: parsed.options.json === true,
10355
10419
  // Preserve PM's existing cross-project default when a config is present;
10356
10420
  // only an explicit remote-agent environment or profile selects an app.
10357
- appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0,
10421
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : compatibleProject?.appId,
10422
+ projectId: compatibleProject?.projectId,
10423
+ rootDir: cfg.rootDir,
10358
10424
  ...deps.sleep ? { sleep: deps.sleep } : {},
10359
10425
  ...deps.now ? { now: deps.now } : {}
10360
10426
  };
10361
10427
  }
10362
10428
  async function pmCommand(parsed, deps = {}) {
10363
10429
  const word = parsed.positionals[1] ?? "";
10430
+ if (word === "project") {
10431
+ const action3 = parsed.positionals[2] ?? "list";
10432
+ if (action3 === "list") {
10433
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "status", "limit", "offset"], 3);
10434
+ return pmProjectList(await buildContext2(parsed, deps), parsed);
10435
+ }
10436
+ if (action3 === "add" || action3 === "create") {
10437
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "name", "description", "mutation-id"], 3);
10438
+ return pmProjectAdd(await buildContext2(parsed, deps), parsed);
10439
+ }
10440
+ if (action3 === "use") {
10441
+ assertArgs(parsed, COMMON_OPTIONS, 4);
10442
+ return pmProjectUse(await buildContext2(parsed, deps), requireId2(parsed.positionals[3], action3));
10443
+ }
10444
+ throw new Error(`unknown pm project action "${action3}". Try list|add|use.`);
10445
+ }
10364
10446
  if (word === "next") {
10365
- assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
10447
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
10366
10448
  return pmNext(await buildContext2(parsed, deps), parsed);
10367
10449
  }
10368
10450
  if (word === "watch") {
@@ -10382,7 +10464,7 @@ async function pmCommand(parsed, deps = {}) {
10382
10464
  return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
10383
10465
  }
10384
10466
  if (word === "handoff") {
10385
- assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
10467
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
10386
10468
  return pmHandoff(await buildContext2(parsed, deps), parsed);
10387
10469
  }
10388
10470
  const entity = ALIASES[word];
@@ -11514,6 +11596,7 @@ var COMMAND_SURFACE = {
11514
11596
  },
11515
11597
  pm: {
11516
11598
  ...PM_ENTITIES,
11599
+ project: { list: {}, add: {}, create: {}, use: {} },
11517
11600
  handoff: {},
11518
11601
  next: {},
11519
11602
  watch: {}
@@ -11794,7 +11877,7 @@ async function runbookRemove(ctx, slug) {
11794
11877
 
11795
11878
  // src/runbook-import.ts
11796
11879
  var import_node_fs17 = require("fs");
11797
- var import_node_path16 = require("path");
11880
+ var import_node_path17 = require("path");
11798
11881
  function parseRunbook(text2, slug) {
11799
11882
  let rest = text2;
11800
11883
  const meta = {};
@@ -11823,8 +11906,8 @@ function readRunbookDir(dir) {
11823
11906
  const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
11824
11907
  if (!files.length) throw new Error(`no .md files in ${dir}`);
11825
11908
  return files.map((file) => {
11826
- const slug = (0, import_node_path16.basename)(file, ".md");
11827
- const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
11909
+ const slug = (0, import_node_path17.basename)(file, ".md");
11910
+ const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path17.join)(dir, file), "utf8"), slug);
11828
11911
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
11829
11912
  });
11830
11913
  }
@@ -11898,7 +11981,7 @@ async function upsert(ctx, r, visibility) {
11898
11981
  // src/runbook-impact.ts
11899
11982
  var import_node_child_process7 = require("child_process");
11900
11983
  var import_node_fs18 = require("fs");
11901
- var import_node_path17 = require("path");
11984
+ var import_node_path18 = require("path");
11902
11985
 
11903
11986
  // src/runbook-impact-scan.ts
11904
11987
  var DECL = /^[+-]\s*export\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/;
@@ -12067,7 +12150,7 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
12067
12150
  }
12068
12151
  function manifestLabeller(root) {
12069
12152
  return (workspace) => {
12070
- const manifest = (0, import_node_path17.join)(root, workspace, "package.json");
12153
+ const manifest = (0, import_node_path18.join)(root, workspace, "package.json");
12071
12154
  if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
12072
12155
  try {
12073
12156
  const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
@@ -12137,7 +12220,7 @@ function report3(ctx, impacts) {
12137
12220
  async function runbookImpact(ctx, options, deps = {}) {
12138
12221
  const cwd = deps.cwd ?? process.cwd();
12139
12222
  const runGit = deps.runGit ?? gitRunner(cwd);
12140
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
12223
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path18.join)(cwd, path), "utf8"));
12141
12224
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
12142
12225
  if (!surfaces.length) {
12143
12226
  return ctx.out.log(
@@ -12272,7 +12355,7 @@ async function runbookComment(ctx, slug, body) {
12272
12355
  var import_node_child_process8 = require("child_process");
12273
12356
  var import_node_fs19 = require("fs");
12274
12357
  var import_node_os5 = require("os");
12275
- var import_node_path18 = require("path");
12358
+ var import_node_path19 = require("path");
12276
12359
  var import_node_process14 = __toESM(require("process"), 1);
12277
12360
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
12278
12361
  function resolveEditor(env = import_node_process14.default.env) {
@@ -12298,8 +12381,8 @@ function editText(initial, slug, deps = {}) {
12298
12381
  );
12299
12382
  if (!interactive())
12300
12383
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
12301
- const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
12302
- const file = (0, import_node_path18.join)(dir, `${slug}.md`);
12384
+ const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path19.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
12385
+ const file = (0, import_node_path19.join)(dir, `${slug}.md`);
12303
12386
  try {
12304
12387
  (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
12305
12388
  const code = defaultRunOrInjected(deps)(editor, file);
@@ -12652,7 +12735,7 @@ function hostedSeverity(value2, flag) {
12652
12735
  var import_security2 = require("@odla-ai/security");
12653
12736
 
12654
12737
  // src/security.ts
12655
- var import_node_path19 = require("path");
12738
+ var import_node_path20 = require("path");
12656
12739
  var import_security = require("@odla-ai/security");
12657
12740
  var import_node3 = require("@odla-ai/security/node");
12658
12741
  async function runHostedSecurity(options) {
@@ -12664,9 +12747,9 @@ async function runHostedSecurity(options) {
12664
12747
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
12665
12748
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
12666
12749
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
12667
- const target = (0, import_node_path19.resolve)(options.target ?? cfg?.rootDir ?? ".");
12668
- const output = (0, import_node_path19.resolve)(options.out ?? (0, import_node_path19.resolve)(target, ".odla/security/hosted"));
12669
- const outputRelative = (0, import_node_path19.relative)(target, output).split(import_node_path19.sep).join("/");
12750
+ const target = (0, import_node_path20.resolve)(options.target ?? cfg?.rootDir ?? ".");
12751
+ const output = (0, import_node_path20.resolve)(options.out ?? (0, import_node_path20.resolve)(target, ".odla/security/hosted"));
12752
+ const outputRelative = (0, import_node_path20.relative)(target, output).split(import_node_path20.sep).join("/");
12670
12753
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
12671
12754
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
12672
12755
  const tokenRequest = {
@@ -12678,7 +12761,7 @@ async function runHostedSecurity(options) {
12678
12761
  };
12679
12762
  const token = await injectedToken(options, tokenRequest);
12680
12763
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
12681
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path19.isAbsolute)(outputRelative) ? [outputRelative] : []
12764
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path20.isAbsolute)(outputRelative) ? [outputRelative] : []
12682
12765
  });
12683
12766
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
12684
12767
  platform,
@@ -12696,7 +12779,7 @@ async function runHostedSecurity(options) {
12696
12779
  });
12697
12780
  const harness = (0, import_security.createSecurityHarness)({
12698
12781
  profile,
12699
- store: new import_node3.FileRunStore((0, import_node_path19.resolve)(output, "state")),
12782
+ store: new import_node3.FileRunStore((0, import_node_path20.resolve)(output, "state")),
12700
12783
  discoveryReasoner: hosted.discoveryReasoner,
12701
12784
  validationReasoner: hosted.validationReasoner,
12702
12785
  policy: {
@@ -12720,7 +12803,7 @@ async function runHostedSecurity(options) {
12720
12803
  function selectEnv(requested, declared, configPath, rootDir) {
12721
12804
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
12722
12805
  if (!env || !declared.includes(env)) {
12723
- const shown = (0, import_node_path19.relative)(rootDir, configPath) || configPath;
12806
+ const shown = (0, import_node_path20.relative)(rootDir, configPath) || configPath;
12724
12807
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
12725
12808
  }
12726
12809
  return env;
@@ -12749,7 +12832,7 @@ function printSummary(out, appId, env, run, report4, output) {
12749
12832
  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}`);
12750
12833
  if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
12751
12834
  out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
12752
- out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
12835
+ out.log(` report: ${(0, import_node_path20.resolve)(output, "REPORT.md")}`);
12753
12836
  }
12754
12837
  function formatBudget(usage) {
12755
12838
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";