@odla-ai/cli 0.30.2 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -96,7 +96,10 @@ app is registered, start each session with the read-only aligned-work intake,
96
96
  then read open bugs and recent decisions:
97
97
 
98
98
  ```bash
99
- npx odla-ai pm next --app <appId>
99
+ npx odla-ai pm project list --app <productId>
100
+ npx odla-ai pm project add --app <productId> --name "My project"
101
+ npx odla-ai pm project use <projectId>
102
+ npx odla-ai pm next --app <appId> --project <projectId>
100
103
  npx odla-ai pm handoff --app <appId>
101
104
  npx odla-ai pm watch --app <appId> --entity task --jsonl
102
105
  npx odla-ai pm bug list --app <appId> --status open
package/bin/odla-ai.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { workspaceBuildOrder } from "./workspace-build.js";
7
+
8
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
9
+ const builtEntry = join(packageRoot, "dist", "bin.js");
10
+
11
+ if (!existsSync(builtEntry)) {
12
+ const workspaceRoot = join(packageRoot, "..", "..");
13
+ const rootManifestPath = join(workspaceRoot, "package.json");
14
+ const rootManifest = existsSync(rootManifestPath)
15
+ ? JSON.parse(readFileSync(rootManifestPath, "utf8"))
16
+ : null;
17
+ if (rootManifest?.name !== "odla-ai" || rootManifest.private !== true) {
18
+ throw new Error("odla-ai CLI build is missing; reinstall @odla-ai/cli from npm");
19
+ }
20
+
21
+ console.error("odla-ai: workspace CLI is not built; building the local monorepo first");
22
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
23
+ const workspaces = workspaceBuildOrder(workspaceRoot);
24
+ const child = spawnSync(
25
+ npm,
26
+ ["run", "build", ...workspaces.map((name) => `--workspace=${name}`)],
27
+ { cwd: workspaceRoot, stdio: "inherit" },
28
+ );
29
+ if (child.error) throw child.error;
30
+ if (child.status !== 0) process.exit(child.status ?? 1);
31
+ }
32
+
33
+ await import(pathToFileURL(builtEntry).href);
@@ -0,0 +1,2 @@
1
+ /** Return buildable internal dependencies in dependency-first order. */
2
+ export declare function workspaceBuildOrder(root: string): string[];
@@ -0,0 +1,36 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const manifestAt = (path) => JSON.parse(readFileSync(path, "utf8"));
5
+
6
+ /** Return buildable internal dependencies in dependency-first order. */
7
+ export function workspaceBuildOrder(root) {
8
+ const rootManifest = manifestAt(join(root, "package.json"));
9
+ const workspaces = new Map();
10
+ for (const pattern of rootManifest.workspaces ?? []) {
11
+ if (!pattern.endsWith("/*")) continue;
12
+ const parent = join(root, pattern.slice(0, -2));
13
+ if (!existsSync(parent)) continue;
14
+ for (const entry of readdirSync(parent, { withFileTypes: true })) {
15
+ if (!entry.isDirectory()) continue;
16
+ const manifestPath = join(parent, entry.name, "package.json");
17
+ if (!existsSync(manifestPath)) continue;
18
+ const manifest = manifestAt(manifestPath);
19
+ if (manifest.name) workspaces.set(manifest.name, manifest);
20
+ }
21
+ }
22
+
23
+ const visited = new Set();
24
+ const order = [];
25
+ const visit = (name) => {
26
+ if (visited.has(name)) return;
27
+ visited.add(name);
28
+ const manifest = workspaces.get(name);
29
+ if (!manifest) return;
30
+ const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
31
+ for (const dependency of Object.keys(dependencies)) visit(dependency);
32
+ if (manifest.scripts?.build) order.push(name);
33
+ };
34
+ visit("@odla-ai/cli");
35
+ return order;
36
+ }
package/dist/bin.cjs CHANGED
@@ -233,7 +233,7 @@ var init_approval_prompt = __esm({
233
233
  async function openUrl(url, options = {}) {
234
234
  const command = openerFor(options.platform ?? import_node_process.default.platform);
235
235
  const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
236
- await new Promise((resolve14, reject) => {
236
+ await new Promise((resolve15, reject) => {
237
237
  const child = doSpawn(command.cmd, [...command.args, url], {
238
238
  stdio: "ignore",
239
239
  detached: true
@@ -241,7 +241,7 @@ async function openUrl(url, options = {}) {
241
241
  child.once("error", reject);
242
242
  child.once("spawn", () => {
243
243
  child.unref();
244
- resolve14();
244
+ resolve15();
245
245
  });
246
246
  });
247
247
  }
@@ -2921,8 +2921,8 @@ var init_calendar_http = __esm({
2921
2921
  // src/calendar-poll.ts
2922
2922
  async function waitForCalendarPoll(milliseconds, signal) {
2923
2923
  if (signal?.aborted) throw signal.reason ?? new Error("calendar connection aborted");
2924
- await new Promise((resolve14, reject) => {
2925
- const timer = setTimeout(resolve14, milliseconds);
2924
+ await new Promise((resolve15, reject) => {
2925
+ const timer = setTimeout(resolve15, milliseconds);
2926
2926
  signal?.addEventListener("abort", () => {
2927
2927
  clearTimeout(timer);
2928
2928
  reject(signal.reason ?? new Error("calendar connection aborted"));
@@ -3694,7 +3694,7 @@ async function configOperationWait(options) {
3694
3694
  assertOperationId(options.operationId);
3695
3695
  const cfg = await loadProjectConfig(options.configPath);
3696
3696
  const client = await operationClient(cfg, options, "wait");
3697
- const wait2 = options.pollWait ?? ((ms) => new Promise((resolve14) => setTimeout(resolve14, ms)));
3697
+ const wait2 = options.pollWait ?? ((ms) => new Promise((resolve15) => setTimeout(resolve15, ms)));
3698
3698
  const now = () => (options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
3699
3699
  const deadline = now() + (options.timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3;
3700
3700
  const interval = (options.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS) * 1e3;
@@ -8802,8 +8802,8 @@ function hostedPollTimeout(value2 = 10 * 6e4) {
8802
8802
  }
8803
8803
  async function waitForHostedPoll(milliseconds, signal) {
8804
8804
  if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
8805
- await new Promise((resolve14, reject) => {
8806
- const timer = setTimeout(resolve14, milliseconds);
8805
+ await new Promise((resolve15, reject) => {
8806
+ const timer = setTimeout(resolve15, milliseconds);
8807
8807
  signal?.addEventListener("abort", () => {
8808
8808
  clearTimeout(timer);
8809
8809
  reject(signal.reason ?? new DOMException("aborted", "AbortError"));
@@ -9593,13 +9593,16 @@ Usage:
9593
9593
  odla-ai app rename <name> [continue in Studio; human session required]
9594
9594
  odla-ai app owners <list|add|remove> [...] [continue in Studio; human session required]
9595
9595
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
9596
- odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9596
+ odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
9597
+ odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
9598
+ odla-ai pm project use <project-id> [--json] [saved locally in this worktree]
9599
+ odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9597
9600
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9598
9601
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9599
9602
  odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9600
9603
  odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
9601
9604
  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]
9602
- odla-ai pm next --app <id> [--json]
9605
+ odla-ai pm next --app <id> [--project <id>] [--json]
9603
9606
  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]
9604
9607
  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]
9605
9608
  odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
@@ -9619,7 +9622,7 @@ Usage:
9619
9622
  odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
9620
9623
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
9621
9624
  odla-ai pm <goal|task|decision|bug> rm <id>
9622
- odla-ai pm handoff --app <id> [--json]
9625
+ odla-ai pm handoff --app <id> [--project <id>] [--json]
9623
9626
  odla-ai discuss groups [--json]
9624
9627
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
9625
9628
  odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
@@ -9754,13 +9757,12 @@ Commands:
9754
9757
  code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
9755
9758
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
9756
9759
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
9757
- pm Project management (via @odla-ai/pm) shared across the apps you
9758
- co-own: conformance goals, kanban tasks, decisions, and bugs. With
9759
- no --app, "list" spans every co-owned project (the cross-project
9760
- view); with --app it scopes to one. Same device-grant auth as
9761
- "app". Entities: goal (alias conformance), task (alias kanban),
9762
- decision, bug. Status changes and comments post to each item's
9763
- @odla-ai/chat discussion thread.
9760
+ pm Project management (via @odla-ai/pm): Products contain Projects;
9761
+ projects contain goals, kanban tasks, decisions, and bugs. Use
9762
+ "pm project list|add|use" to select worktree-local context, or
9763
+ pass --app/--project explicitly. Same device-grant auth as "app".
9764
+ Status changes and comments post to each item's @odla-ai/chat
9765
+ discussion thread.
9764
9766
  bug Intent-first alias for PM bugs. "bug report" writes to
9765
9767
  odla PM; odla product defects do not belong in GitHub Issues.
9766
9768
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
@@ -10202,7 +10204,7 @@ function jsonl(ctx, parsed, value2) {
10202
10204
  }
10203
10205
  async function discussWatch(ctx, topicId, parsed) {
10204
10206
  if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
10205
- const sleep = ctx.sleep ?? ((ms) => new Promise((resolve14) => setTimeout(resolve14, ms)));
10207
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve15) => setTimeout(resolve15, ms)));
10206
10208
  const now = ctx.now ?? Date.now;
10207
10209
  const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
10208
10210
  const timeoutSeconds = numberOpt2(parsed, "timeout");
@@ -10584,6 +10586,8 @@ async function pmList(ctx, entity, parsed) {
10584
10586
  };
10585
10587
  const app = stringOpt(parsed.options.app) ?? ctx.appId;
10586
10588
  if (app) q.set("app", app);
10589
+ const project = stringOpt(parsed.options.project) ?? ctx.projectId;
10590
+ if (project) q.set("project", project);
10587
10591
  for (const [flag, param] of Object.entries(filters)) {
10588
10592
  const raw = stringOpt(parsed.options[flag]);
10589
10593
  const v = entity === "task" && flag === "column" && raw === "ready" ? "todo" : raw;
@@ -10603,6 +10607,7 @@ async function pmList(ctx, entity, parsed) {
10603
10607
  }
10604
10608
  async function pmAdd(ctx, entity, parsed) {
10605
10609
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
10610
+ const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
10606
10611
  if (!appId) throw new Error("pm add needs --app <appId>");
10607
10612
  const input = collectEntityFields(entity, parsed, false);
10608
10613
  if (!input.title) throw new Error("pm add needs --title <title>");
@@ -10610,6 +10615,7 @@ async function pmAdd(ctx, entity, parsed) {
10610
10615
  throw new Error("pm bug add needs --description <context> (or --body <context>)");
10611
10616
  const res = await pmRequest(ctx, "POST", `/${entity}`, {
10612
10617
  appId,
10618
+ ...projectId ? { projectId } : {},
10613
10619
  input,
10614
10620
  mutationId: writeMutationId2(parsed)
10615
10621
  });
@@ -10689,7 +10695,7 @@ async function pmTaskLifecycle(ctx, id, action2, parsed) {
10689
10695
  ctx.out.log(`task: ${label} \u2192 ${state2}`);
10690
10696
  });
10691
10697
  }
10692
- async function allRecords(ctx, entity, appId) {
10698
+ async function allRecords(ctx, entity, appId, projectId) {
10693
10699
  const records = [];
10694
10700
  for (; ; ) {
10695
10701
  const q = new URLSearchParams({
@@ -10697,6 +10703,7 @@ async function allRecords(ctx, entity, appId) {
10697
10703
  limit: "100",
10698
10704
  offset: String(records.length)
10699
10705
  });
10706
+ if (projectId) q.set("project", projectId);
10700
10707
  const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
10701
10708
  records.push(...page2.records);
10702
10709
  if (records.length >= page2.total || page2.records.length === 0) return records;
@@ -10704,13 +10711,15 @@ async function allRecords(ctx, entity, appId) {
10704
10711
  }
10705
10712
  async function pmNext(ctx, parsed) {
10706
10713
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
10714
+ const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
10707
10715
  if (!appId) throw new Error("pm next needs --app <appId>");
10708
10716
  const [goals, tasks] = await Promise.all([
10709
- allRecords(ctx, "goal", appId),
10710
- allRecords(ctx, "task", appId)
10717
+ allRecords(ctx, "goal", appId, projectId),
10718
+ allRecords(ctx, "task", appId, projectId)
10711
10719
  ]);
10712
10720
  const result = {
10713
10721
  appId,
10722
+ projectId,
10714
10723
  openGoals: goals.filter((record10) => record10.status === "open"),
10715
10724
  doing: tasks.filter((record10) => record10.column === "doing"),
10716
10725
  ready: tasks.filter((record10) => record10.column === "todo")
@@ -10741,14 +10750,16 @@ async function pmNext(ctx, parsed) {
10741
10750
  }
10742
10751
  async function pmHandoff(ctx, parsed) {
10743
10752
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
10753
+ const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
10744
10754
  if (!appId) throw new Error("pm handoff needs --app <appId>");
10745
10755
  const [goals, tasks, bugs] = await Promise.all([
10746
- allRecords(ctx, "goal", appId),
10747
- allRecords(ctx, "task", appId),
10748
- allRecords(ctx, "bug", appId)
10756
+ allRecords(ctx, "goal", appId, projectId),
10757
+ allRecords(ctx, "task", appId, projectId),
10758
+ allRecords(ctx, "bug", appId, projectId)
10749
10759
  ]);
10750
10760
  const handoff = {
10751
10761
  appId,
10762
+ projectId,
10752
10763
  unmetGoals: goals.filter((record10) => record10.status !== "met"),
10753
10764
  activeTasks: tasks.filter((record10) => record10.column !== "done"),
10754
10765
  openBugs: bugs.filter((record10) => record10.status !== "fixed" && record10.status !== "wontfix")
@@ -10940,7 +10951,7 @@ async function pmWatch(ctx, parsed) {
10940
10951
  }
10941
10952
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
10942
10953
  if (!appId) throw new Error("pm watch needs --app <appId>");
10943
- const sleep = ctx.sleep ?? ((ms) => new Promise((resolve14) => setTimeout(resolve14, ms)));
10954
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve15) => setTimeout(resolve15, ms)));
10944
10955
  const now = ctx.now ?? Date.now;
10945
10956
  const intervalMs = (positiveNumber(parsed, "interval", DEFAULT_INTERVAL_MS2 / 1e3) ?? DEFAULT_INTERVAL_MS2 / 1e3) * 1e3;
10946
10957
  const timeoutSeconds = positiveNumber(parsed, "timeout");
@@ -11061,6 +11072,73 @@ var init_pm_watch = __esm({
11061
11072
  }
11062
11073
  });
11063
11074
 
11075
+ // src/pm-project-context.ts
11076
+ function readPmProjectContext(rootDir) {
11077
+ const value2 = readJsonFile(pmProjectContextFile(rootDir));
11078
+ return value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" ? value2 : null;
11079
+ }
11080
+ function writePmProjectContext(rootDir, value2) {
11081
+ writePrivateJson(pmProjectContextFile(rootDir), { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() });
11082
+ }
11083
+ var import_node_path17, pmProjectContextFile;
11084
+ var init_pm_project_context = __esm({
11085
+ "src/pm-project-context.ts"() {
11086
+ "use strict";
11087
+ init_cjs_shims();
11088
+ import_node_path17 = require("path");
11089
+ init_local();
11090
+ pmProjectContextFile = (rootDir) => (0, import_node_path17.resolve)(rootDir, ".odla", "pm-project.local.json");
11091
+ }
11092
+ });
11093
+
11094
+ // src/pm-project-actions.ts
11095
+ async function pmProjectList(ctx, parsed) {
11096
+ const q = new URLSearchParams();
11097
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
11098
+ if (app) q.set("app", app);
11099
+ for (const key of ["status", "limit", "offset"]) {
11100
+ const value2 = stringOpt(parsed.options[key]);
11101
+ if (value2) q.set(key, value2);
11102
+ }
11103
+ const page2 = await pmRequest(ctx, "GET", `/project?${q}`);
11104
+ emit2(ctx, page2, () => {
11105
+ ctx.out.log(`projects \u2014 ${page2.records.length} of ${page2.total}`);
11106
+ ctx.out.log("id status product name");
11107
+ for (const project of page2.records) {
11108
+ ctx.out.log(`${project.id} ${project.status} ${project.appId} ${project.name}${project.isDefault ? " (default)" : ""}`);
11109
+ }
11110
+ });
11111
+ }
11112
+ async function pmProjectAdd(ctx, parsed) {
11113
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
11114
+ const name = stringOpt(parsed.options.name);
11115
+ if (!appId) throw new Error("pm project add needs --app <productId>");
11116
+ if (!name) throw new Error("pm project add needs --name <name>");
11117
+ const result = await pmRequest(ctx, "POST", "/project", {
11118
+ appId,
11119
+ name,
11120
+ description: stringOpt(parsed.options.description),
11121
+ mutationId: writeMutationId2(parsed)
11122
+ });
11123
+ emit2(ctx, result, () => ctx.out.log(`created project: ${result.project.name} (${result.project.id})`));
11124
+ }
11125
+ async function pmProjectUse(ctx, id) {
11126
+ if (!ctx.rootDir) throw new Error("pm project use needs a local project directory");
11127
+ const { project } = await pmRequest(ctx, "GET", `/project/${encodeURIComponent(id)}`);
11128
+ if (project.status !== "active") throw new Error(`project ${project.name} is ${project.status}, not active`);
11129
+ writePmProjectContext(ctx.rootDir, { appId: project.appId, projectId: project.id });
11130
+ emit2(ctx, project, () => ctx.out.log(`using ${project.appId} / ${project.name} (${project.id}) in this worktree`));
11131
+ }
11132
+ var init_pm_project_actions = __esm({
11133
+ "src/pm-project-actions.ts"() {
11134
+ "use strict";
11135
+ init_cjs_shims();
11136
+ init_argv();
11137
+ init_pm_action_core();
11138
+ init_pm_project_context();
11139
+ }
11140
+ });
11141
+
11064
11142
  // src/pm-command.ts
11065
11143
  function canonicalAction(action2) {
11066
11144
  if (action2 === "create") return "add";
@@ -11095,6 +11173,9 @@ async function buildContext2(parsed, deps) {
11095
11173
  doFetch,
11096
11174
  out
11097
11175
  );
11176
+ const selectedProject = readPmProjectContext(cfg.rootDir);
11177
+ const requestedApp = stringOpt(parsed.options.app) ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0);
11178
+ const compatibleProject = selectedProject && (!requestedApp || selectedProject.appId === requestedApp) ? selectedProject : null;
11098
11179
  return {
11099
11180
  platformUrl: cfg.platformUrl,
11100
11181
  token,
@@ -11103,15 +11184,33 @@ async function buildContext2(parsed, deps) {
11103
11184
  json: parsed.options.json === true,
11104
11185
  // Preserve PM's existing cross-project default when a config is present;
11105
11186
  // only an explicit remote-agent environment or profile selects an app.
11106
- appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0,
11187
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : compatibleProject?.appId,
11188
+ projectId: compatibleProject?.projectId,
11189
+ rootDir: cfg.rootDir,
11107
11190
  ...deps.sleep ? { sleep: deps.sleep } : {},
11108
11191
  ...deps.now ? { now: deps.now } : {}
11109
11192
  };
11110
11193
  }
11111
11194
  async function pmCommand(parsed, deps = {}) {
11112
11195
  const word = parsed.positionals[1] ?? "";
11196
+ if (word === "project") {
11197
+ const action3 = parsed.positionals[2] ?? "list";
11198
+ if (action3 === "list") {
11199
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "status", "limit", "offset"], 3);
11200
+ return pmProjectList(await buildContext2(parsed, deps), parsed);
11201
+ }
11202
+ if (action3 === "add" || action3 === "create") {
11203
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "name", "description", "mutation-id"], 3);
11204
+ return pmProjectAdd(await buildContext2(parsed, deps), parsed);
11205
+ }
11206
+ if (action3 === "use") {
11207
+ assertArgs(parsed, COMMON_OPTIONS, 4);
11208
+ return pmProjectUse(await buildContext2(parsed, deps), requireId2(parsed.positionals[3], action3));
11209
+ }
11210
+ throw new Error(`unknown pm project action "${action3}". Try list|add|use.`);
11211
+ }
11113
11212
  if (word === "next") {
11114
- assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
11213
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
11115
11214
  return pmNext(await buildContext2(parsed, deps), parsed);
11116
11215
  }
11117
11216
  if (word === "watch") {
@@ -11131,7 +11230,7 @@ async function pmCommand(parsed, deps = {}) {
11131
11230
  return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
11132
11231
  }
11133
11232
  if (word === "handoff") {
11134
- assertArgs(parsed, [...COMMON_OPTIONS, "app"], 2);
11233
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
11135
11234
  return pmHandoff(await buildContext2(parsed, deps), parsed);
11136
11235
  }
11137
11236
  const entity = ALIASES[word];
@@ -11184,6 +11283,8 @@ var init_pm_command = __esm({
11184
11283
  init_pm_comments();
11185
11284
  init_token();
11186
11285
  init_pm_watch();
11286
+ init_pm_project_actions();
11287
+ init_pm_project_context();
11187
11288
  ALIASES = {
11188
11289
  goal: "goal",
11189
11290
  conformance: "goal",
@@ -11194,8 +11295,8 @@ var init_pm_command = __esm({
11194
11295
  };
11195
11296
  COMMON_OPTIONS = ["config", "token", "email", "json", "platform", "context", "open"];
11196
11297
  ACTION_OPTIONS = {
11197
- list: ["app", "q", "limit", "offset"],
11198
- add: ["app", "title", "mutation-id"],
11298
+ list: ["app", "project", "q", "limit", "offset"],
11299
+ add: ["app", "project", "title", "mutation-id"],
11199
11300
  get: [],
11200
11301
  set: ["mutation-id"],
11201
11302
  done: ["mutation-id"],
@@ -12453,6 +12554,7 @@ var init_surface = __esm({
12453
12554
  },
12454
12555
  pm: {
12455
12556
  ...PM_ENTITIES,
12557
+ project: { list: {}, add: {}, create: {}, use: {} },
12456
12558
  handoff: {},
12457
12559
  next: {},
12458
12560
  watch: {}
@@ -12687,8 +12789,8 @@ function readRunbookDir(dir) {
12687
12789
  const files = (0, import_node_fs19.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
12688
12790
  if (!files.length) throw new Error(`no .md files in ${dir}`);
12689
12791
  return files.map((file) => {
12690
- const slug = (0, import_node_path17.basename)(file, ".md");
12691
- const parsed = parseRunbook((0, import_node_fs19.readFileSync)((0, import_node_path17.join)(dir, file), "utf8"), slug);
12792
+ const slug = (0, import_node_path18.basename)(file, ".md");
12793
+ const parsed = parseRunbook((0, import_node_fs19.readFileSync)((0, import_node_path18.join)(dir, file), "utf8"), slug);
12692
12794
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
12693
12795
  });
12694
12796
  }
@@ -12758,13 +12860,13 @@ async function upsert(ctx, r, visibility) {
12758
12860
  );
12759
12861
  return "updated";
12760
12862
  }
12761
- var import_node_fs19, import_node_path17;
12863
+ var import_node_fs19, import_node_path18;
12762
12864
  var init_runbook_import = __esm({
12763
12865
  "src/runbook-import.ts"() {
12764
12866
  "use strict";
12765
12867
  init_cjs_shims();
12766
12868
  import_node_fs19 = require("fs");
12767
- import_node_path17 = require("path");
12869
+ import_node_path18 = require("path");
12768
12870
  init_runbook_actions();
12769
12871
  }
12770
12872
  });
@@ -12942,7 +13044,7 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
12942
13044
  }
12943
13045
  function manifestLabeller(root) {
12944
13046
  return (workspace) => {
12945
- const manifest = (0, import_node_path18.join)(root, workspace, "package.json");
13047
+ const manifest = (0, import_node_path19.join)(root, workspace, "package.json");
12946
13048
  if (!(0, import_node_fs20.existsSync)(manifest)) return void 0;
12947
13049
  try {
12948
13050
  const name = JSON.parse((0, import_node_fs20.readFileSync)(manifest, "utf8")).name;
@@ -13011,7 +13113,7 @@ function report3(ctx, impacts) {
13011
13113
  async function runbookImpact(ctx, options, deps = {}) {
13012
13114
  const cwd = deps.cwd ?? process.cwd();
13013
13115
  const runGit = deps.runGit ?? gitRunner(cwd);
13014
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs20.readFileSync)((0, import_node_path18.join)(cwd, path), "utf8"));
13116
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs20.readFileSync)((0, import_node_path19.join)(cwd, path), "utf8"));
13015
13117
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
13016
13118
  if (!surfaces.length) {
13017
13119
  return ctx.out.log(
@@ -13022,14 +13124,14 @@ async function runbookImpact(ctx, options, deps = {}) {
13022
13124
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
13023
13125
  report3(ctx, impacts);
13024
13126
  }
13025
- var import_node_child_process7, import_node_fs20, import_node_path18, SOURCE2, editHint;
13127
+ var import_node_child_process7, import_node_fs20, import_node_path19, SOURCE2, editHint;
13026
13128
  var init_runbook_impact = __esm({
13027
13129
  "src/runbook-impact.ts"() {
13028
13130
  "use strict";
13029
13131
  init_cjs_shims();
13030
13132
  import_node_child_process7 = require("child_process");
13031
13133
  import_node_fs20 = require("fs");
13032
- import_node_path18 = require("path");
13134
+ import_node_path19 = require("path");
13033
13135
  init_runbook_impact_scan();
13034
13136
  init_runbook_actions();
13035
13137
  SOURCE2 = /\.(ts|tsx|js|jsx|mts|cts)$/;
@@ -13198,8 +13300,8 @@ function editText(initial, slug, deps = {}) {
13198
13300
  );
13199
13301
  if (!interactive())
13200
13302
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
13201
- const dir = (0, import_node_fs21.mkdtempSync)((0, import_node_path19.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
13202
- const file = (0, import_node_path19.join)(dir, `${slug}.md`);
13303
+ const dir = (0, import_node_fs21.mkdtempSync)((0, import_node_path20.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
13304
+ const file = (0, import_node_path20.join)(dir, `${slug}.md`);
13203
13305
  try {
13204
13306
  (0, import_node_fs21.writeFileSync)(file, initial, { mode: 384 });
13205
13307
  const code = defaultRunOrInjected(deps)(editor, file);
@@ -13210,7 +13312,7 @@ function editText(initial, slug, deps = {}) {
13210
13312
  (0, import_node_fs21.rmSync)(dir, { recursive: true, force: true });
13211
13313
  }
13212
13314
  }
13213
- var import_node_child_process8, import_node_fs21, import_node_os5, import_node_path19, import_node_process14, EDITOR_ENV, defaultRunOrInjected;
13315
+ var import_node_child_process8, import_node_fs21, import_node_os5, import_node_path20, import_node_process14, EDITOR_ENV, defaultRunOrInjected;
13214
13316
  var init_runbook_editor = __esm({
13215
13317
  "src/runbook-editor.ts"() {
13216
13318
  "use strict";
@@ -13218,7 +13320,7 @@ var init_runbook_editor = __esm({
13218
13320
  import_node_child_process8 = require("child_process");
13219
13321
  import_node_fs21 = require("fs");
13220
13322
  import_node_os5 = require("os");
13221
- import_node_path19 = require("path");
13323
+ import_node_path20 = require("path");
13222
13324
  import_node_process14 = __toESM(require("process"), 1);
13223
13325
  EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
13224
13326
  defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -13615,9 +13717,9 @@ async function runHostedSecurity(options) {
13615
13717
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
13616
13718
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
13617
13719
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
13618
- const target = (0, import_node_path20.resolve)(options.target ?? cfg?.rootDir ?? ".");
13619
- const output = (0, import_node_path20.resolve)(options.out ?? (0, import_node_path20.resolve)(target, ".odla/security/hosted"));
13620
- const outputRelative = (0, import_node_path20.relative)(target, output).split(import_node_path20.sep).join("/");
13720
+ const target = (0, import_node_path21.resolve)(options.target ?? cfg?.rootDir ?? ".");
13721
+ const output = (0, import_node_path21.resolve)(options.out ?? (0, import_node_path21.resolve)(target, ".odla/security/hosted"));
13722
+ const outputRelative = (0, import_node_path21.relative)(target, output).split(import_node_path21.sep).join("/");
13621
13723
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
13622
13724
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
13623
13725
  const tokenRequest = {
@@ -13629,7 +13731,7 @@ async function runHostedSecurity(options) {
13629
13731
  };
13630
13732
  const token = await injectedToken(options, tokenRequest);
13631
13733
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
13632
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path20.isAbsolute)(outputRelative) ? [outputRelative] : []
13734
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path21.isAbsolute)(outputRelative) ? [outputRelative] : []
13633
13735
  });
13634
13736
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
13635
13737
  platform,
@@ -13647,7 +13749,7 @@ async function runHostedSecurity(options) {
13647
13749
  });
13648
13750
  const harness = (0, import_security.createSecurityHarness)({
13649
13751
  profile,
13650
- store: new import_node3.FileRunStore((0, import_node_path20.resolve)(output, "state")),
13752
+ store: new import_node3.FileRunStore((0, import_node_path21.resolve)(output, "state")),
13651
13753
  discoveryReasoner: hosted.discoveryReasoner,
13652
13754
  validationReasoner: hosted.validationReasoner,
13653
13755
  policy: {
@@ -13671,7 +13773,7 @@ async function runHostedSecurity(options) {
13671
13773
  function selectEnv(requested, declared, configPath, rootDir) {
13672
13774
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
13673
13775
  if (!env || !declared.includes(env)) {
13674
- const shown = (0, import_node_path20.relative)(rootDir, configPath) || configPath;
13776
+ const shown = (0, import_node_path21.relative)(rootDir, configPath) || configPath;
13675
13777
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
13676
13778
  }
13677
13779
  return env;
@@ -13700,17 +13802,17 @@ function printSummary(out, appId, env, run, report4, output) {
13700
13802
  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}`);
13701
13803
  if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
13702
13804
  out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
13703
- out.log(` report: ${(0, import_node_path20.resolve)(output, "REPORT.md")}`);
13805
+ out.log(` report: ${(0, import_node_path21.resolve)(output, "REPORT.md")}`);
13704
13806
  }
13705
13807
  function formatBudget(usage) {
13706
13808
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
13707
13809
  }
13708
- var import_node_path20, import_security, import_node3;
13810
+ var import_node_path21, import_security, import_node3;
13709
13811
  var init_security = __esm({
13710
13812
  "src/security.ts"() {
13711
13813
  "use strict";
13712
13814
  init_cjs_shims();
13713
- import_node_path20 = require("path");
13815
+ import_node_path21 = require("path");
13714
13816
  import_security = require("@odla-ai/security");
13715
13817
  import_node3 = require("@odla-ai/security/node");
13716
13818
  init_config();