@odla-ai/cli 0.42.2 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -174,7 +174,7 @@ function absoluteEntryPath(entryPath) {
174
174
 
175
175
  // src/bin.ts
176
176
  var argv = process.argv.slice(2);
177
- requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-4FCRBWED.js")).runCli()).catch((err) => {
177
+ requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-OITEPXW4.js")).runCli()).catch((err) => {
178
178
  console.error(redactSecrets(`odla-ai: ${err instanceof Error ? err.message : String(err)}`));
179
179
  process.exitCode = exitCodeFor(err);
180
180
  });
@@ -10697,7 +10697,8 @@ Usage:
10697
10697
  odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
10698
10698
  odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
10699
10699
  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]
10700
- odla-ai pm next --app <id> [--project <id>] [--json]
10700
+ odla-ai pm next --app <id> [--project <id>] [--verbose] [--json]
10701
+ odla-ai pm start --app <id> [<task-id>] [--goal <id>] [--verbose] [--mutation-id <id>] [--json] [claims the top Ready task in one call]
10701
10702
  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]
10702
10703
  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]
10703
10704
  odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
@@ -10718,7 +10719,7 @@ Usage:
10718
10719
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
10719
10720
  odla-ai pm <goal|task|decision|bug> history <id> [--limit <n>] [--json]
10720
10721
  odla-ai pm <goal|task|decision|bug> rm <id>
10721
- odla-ai pm handoff --app <id> [--project <id>] [--json]
10722
+ odla-ai pm handoff --app <id> [--project <id>] [--verbose] [--json]
10722
10723
  odla-ai discuss groups [--json]
10723
10724
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--mentions] [--json]
10724
10725
  odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
@@ -10883,6 +10884,16 @@ Commands:
10883
10884
  execution contract and a reviewed revision. Any caller with PM
10884
10885
  mutation access to that app/project may do it; claims coordinate
10885
10886
  execution but never own or lock the PM record.
10887
+ To pick up work: "pm next" reads open goals, Ready candidates,
10888
+ and work in progress; "pm start" claims the top Ready task in
10889
+ one call, compare-and-swapped on the revision it just read, and
10890
+ returns the task, its goal, and the acceptance criteria to work
10891
+ against. Finish with "pm task done <id>", or hand it back with
10892
+ "pm task release <id> --expected-revision <n>". "pm start" never
10893
+ marks work Ready: that contract is reviewed separately so an
10894
+ agent cannot authorize its own work.
10895
+ Intake output is projected to the fields an executing caller
10896
+ acts on; --verbose returns whole records with their audit trail.
10886
10897
  bug Intent-first alias for PM bugs. "bug report" writes to
10887
10898
  odla PM; odla product defects do not belong in GitHub Issues.
10888
10899
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
@@ -11633,6 +11644,15 @@ var FIELD_MAP = {
11633
11644
  execution: { key: "executionMode" },
11634
11645
  "expected-revision": { key: "expectedRevision", num: true }
11635
11646
  };
11647
+ var PmRequestError = class extends Error {
11648
+ constructor(status, message2) {
11649
+ super(message2);
11650
+ this.status = status;
11651
+ this.name = "PmRequestError";
11652
+ }
11653
+ status;
11654
+ };
11655
+ var isRevisionConflict = (error) => error instanceof PmRequestError && error.status === 409;
11636
11656
  async function pmRequest(ctx, method, path, body) {
11637
11657
  const response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
11638
11658
  method,
@@ -11652,7 +11672,8 @@ async function pmRequest(ctx, method, path, body) {
11652
11672
  const message2 = error.message;
11653
11673
  if (typeof message2 === "string" && message2.length > 0) detail = message2;
11654
11674
  }
11655
- throw new Error(
11675
+ throw new PmRequestError(
11676
+ response2.status,
11656
11677
  `pm ${method} ${path} failed: ${detail ?? `registry returned ${response2.status}`} (${response2.status})`
11657
11678
  );
11658
11679
  }
@@ -11841,103 +11862,196 @@ async function pmTaskLifecycle(ctx, id2, action2, parsed) {
11841
11862
  ctx.out.log(`task: ${label} \u2192 ${state2}`);
11842
11863
  });
11843
11864
  }
11844
- async function allRecords(ctx, entity, appId, projectId) {
11865
+ async function pmRemove(ctx, entity, id2) {
11866
+ await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id2)}`);
11867
+ ctx.out.log(`deleted ${entity} ${id2}`);
11868
+ }
11869
+
11870
+ // src/pm-lean.ts
11871
+ var COMMON = ["id", "appId", "projectId", "title", "revision"];
11872
+ var PER_ENTITY = {
11873
+ goal: ["status", "proof", "targetPct", "currentPct"],
11874
+ task: [
11875
+ "column",
11876
+ "goalId",
11877
+ "alignmentDecisionId",
11878
+ "description",
11879
+ "acceptanceCriteria",
11880
+ "executionMode",
11881
+ "assigneeId",
11882
+ "claimedByPrincipalId",
11883
+ "dueAt"
11884
+ ],
11885
+ decision: ["status", "body", "supersedesId"],
11886
+ bug: ["status", "severity", "description", "goalId", "assigneeId", "decisionId"]
11887
+ };
11888
+ function leanRecord(entity, record11) {
11889
+ const out = {};
11890
+ for (const key of [...COMMON, ...PER_ENTITY[entity]]) {
11891
+ const value2 = record11[key];
11892
+ if (value2 !== void 0 && value2 !== null) out[key] = value2;
11893
+ }
11894
+ return out;
11895
+ }
11896
+ function leanRecords(entity, records, verbose) {
11897
+ return verbose ? records : records.map((record11) => leanRecord(entity, record11));
11898
+ }
11899
+
11900
+ // src/pm-intake.ts
11901
+ var READY_COLUMN = "todo";
11902
+ var UNMET_GOAL_STATUS = "open";
11903
+ var ACTIVE_TASK_COLUMNS = "backlog,todo,doing,review";
11904
+ var OPEN_BUG_STATUSES = "open,triaged";
11905
+ var wantsVerbose = (parsed) => parsed.options.verbose === true;
11906
+ async function listFiltered(ctx, entity, appId, projectId, filters) {
11845
11907
  const records = [];
11846
11908
  for (; ; ) {
11847
- const q = new URLSearchParams({
11848
- app: appId,
11849
- limit: "100",
11850
- offset: String(records.length)
11851
- });
11909
+ const q = new URLSearchParams({ app: appId, limit: "100", offset: String(records.length) });
11852
11910
  if (projectId) q.set("project", projectId);
11911
+ for (const [key, value2] of Object.entries(filters)) q.set(key, value2);
11853
11912
  const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
11854
11913
  records.push(...page2.records);
11855
11914
  if (records.length >= page2.total || page2.records.length === 0) return records;
11856
11915
  }
11857
11916
  }
11858
- async function pmNext(ctx, parsed) {
11917
+ function requireApp(ctx, parsed, command) {
11859
11918
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
11860
- const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
11861
- if (!appId) throw new Error("pm next needs --app <appId>");
11919
+ if (!appId) throw new Error(`pm ${command} needs --app <appId>`);
11920
+ return { appId, projectId: stringOpt(parsed.options.project) ?? ctx.projectId };
11921
+ }
11922
+ async function pmNext(ctx, parsed) {
11923
+ const { appId, projectId } = requireApp(ctx, parsed, "next");
11924
+ const verbose = wantsVerbose(parsed);
11862
11925
  const [goals, tasks] = await Promise.all([
11863
- allRecords(ctx, "goal", appId, projectId),
11864
- allRecords(ctx, "task", appId, projectId)
11926
+ listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
11927
+ // One request for both live columns; the split below is over open work only.
11928
+ listFiltered(ctx, "task", appId, projectId, { column: `${READY_COLUMN},doing` })
11865
11929
  ]);
11930
+ const ready = tasks.filter((record11) => record11.column === READY_COLUMN);
11931
+ const doing = tasks.filter((record11) => record11.column === "doing");
11932
+ const guidance = !goals.length ? "discuss alignment with the user before creating or claiming project work" : !ready.length ? 'refine a linked Backlog task and mark it Ready with "pm task ready <id>"' : `claim the top Ready task in one call: odla-ai pm start --app ${appId}`;
11866
11933
  const result = {
11867
11934
  appId,
11868
11935
  projectId,
11869
- openGoals: goals.filter((record11) => record11.status === "open"),
11870
- doing: tasks.filter((record11) => record11.column === "doing"),
11871
- ready: tasks.filter((record11) => record11.column === "todo")
11936
+ openGoals: leanRecords("goal", goals, verbose),
11937
+ doing: leanRecords("task", doing, verbose),
11938
+ ready: leanRecords("task", ready, verbose),
11939
+ next: guidance
11872
11940
  };
11873
11941
  emit2(ctx, result, () => {
11874
11942
  ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
11875
- for (const [label, records] of [
11876
- ["doing", result.doing],
11877
- ["ready", result.ready],
11878
- ["open goals", result.openGoals]
11943
+ for (const [label, entity, records] of [
11944
+ ["doing", "task", doing],
11945
+ ["ready", "task", ready],
11946
+ ["open goals", "goal", goals]
11879
11947
  ]) {
11880
11948
  ctx.out.log(`${label}:`);
11881
11949
  if (!records.length) ctx.out.log("- (none)");
11882
- else for (const record11 of records) printRecord(
11883
- ctx,
11884
- label === "open goals" ? "goal" : "task",
11885
- record11
11886
- );
11887
- }
11888
- if (!result.openGoals.length) {
11889
- ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
11890
- } else if (!result.ready.length) {
11891
- ctx.out.log("next: refine a linked Backlog task and mark it Ready");
11892
- } else {
11893
- ctx.out.log("next: review a Ready task, then claim it with its revision");
11950
+ else for (const record11 of records) printRecord(ctx, entity, record11);
11894
11951
  }
11952
+ ctx.out.log(`next: ${guidance}`);
11895
11953
  });
11896
11954
  }
11897
11955
  async function pmHandoff(ctx, parsed) {
11898
- const appId = stringOpt(parsed.options.app) ?? ctx.appId;
11899
- const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
11900
- if (!appId) throw new Error("pm handoff needs --app <appId>");
11956
+ const { appId, projectId } = requireApp(ctx, parsed, "handoff");
11957
+ const verbose = wantsVerbose(parsed);
11901
11958
  const [goals, tasks, bugs] = await Promise.all([
11902
- allRecords(ctx, "goal", appId, projectId),
11903
- allRecords(ctx, "task", appId, projectId),
11904
- allRecords(ctx, "bug", appId, projectId)
11959
+ listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
11960
+ listFiltered(ctx, "task", appId, projectId, { column: ACTIVE_TASK_COLUMNS }),
11961
+ listFiltered(ctx, "bug", appId, projectId, { status: OPEN_BUG_STATUSES })
11905
11962
  ]);
11906
- const handoff = {
11963
+ const clean4 = !goals.length && !tasks.length && !bugs.length;
11964
+ const result = {
11907
11965
  appId,
11908
11966
  projectId,
11909
- unmetGoals: goals.filter((record11) => record11.status !== "met"),
11910
- activeTasks: tasks.filter((record11) => record11.column !== "done"),
11911
- openBugs: bugs.filter((record11) => record11.status !== "fixed" && record11.status !== "wontfix")
11912
- };
11913
- const result = {
11914
- ...handoff,
11915
- clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
11967
+ unmetGoals: leanRecords("goal", goals, verbose),
11968
+ activeTasks: leanRecords("task", tasks, verbose),
11969
+ openBugs: leanRecords("bug", bugs, verbose),
11970
+ clean: clean4
11916
11971
  };
11917
11972
  emit2(ctx, result, () => {
11918
- if (result.clean) {
11973
+ if (clean4) {
11919
11974
  ctx.out.log(`${appId}: no unresolved PM work`);
11920
11975
  return;
11921
11976
  }
11922
11977
  ctx.out.log(`${appId}: authoritative PM handoff`);
11923
- for (const [label, records] of [
11924
- ["unmet goals", result.unmetGoals],
11925
- ["active tasks", result.activeTasks],
11926
- ["open bugs", result.openBugs]
11978
+ for (const [label, entity, records] of [
11979
+ ["unmet goals", "goal", goals],
11980
+ ["active tasks", "task", tasks],
11981
+ ["open bugs", "bug", bugs]
11927
11982
  ]) {
11928
11983
  ctx.out.log(`${label}:`);
11929
11984
  if (!records.length) ctx.out.log("- (none)");
11930
- else for (const record11 of records) printRecord(
11931
- ctx,
11932
- label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
11933
- record11
11934
- );
11985
+ else for (const record11 of records) printRecord(ctx, entity, record11);
11935
11986
  }
11936
11987
  });
11937
11988
  }
11938
- async function pmRemove(ctx, entity, id2) {
11939
- await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id2)}`);
11940
- ctx.out.log(`deleted ${entity} ${id2}`);
11989
+
11990
+ // src/pm-start.ts
11991
+ var getTask = async (ctx, id2) => (await pmRequest(ctx, "GET", `/task/${encodeURIComponent(id2)}`)).record;
11992
+ async function claimAt(ctx, task, parsed) {
11993
+ const res = await pmRequest(
11994
+ ctx,
11995
+ "POST",
11996
+ `/task/${encodeURIComponent(task.id)}/claim`,
11997
+ { expectedRevision: task.revision, mutationId: writeMutationId2(parsed) }
11998
+ );
11999
+ return res.record;
12000
+ }
12001
+ async function selectTask(ctx, parsed, appId, projectId) {
12002
+ const named = parsed.positionals[2];
12003
+ if (named) return getTask(ctx, named);
12004
+ const goalId = stringOpt(parsed.options.goal);
12005
+ const ready = await listFiltered(ctx, "task", appId, projectId, {
12006
+ column: READY_COLUMN,
12007
+ ...goalId ? { goalId } : {}
12008
+ });
12009
+ const candidate = ready[0];
12010
+ if (!candidate) {
12011
+ throw new Error(
12012
+ `no Ready task to claim in ${appId}${goalId ? ` for goal ${goalId}` : ""}. Run "odla-ai pm next --app ${appId}" to see open goals and Backlog work, then "odla-ai pm task ready <id>" once a task has a complete contract.`
12013
+ );
12014
+ }
12015
+ return candidate;
12016
+ }
12017
+ async function pmStart(ctx, parsed) {
12018
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
12019
+ if (!appId) throw new Error("pm start needs --app <appId>");
12020
+ const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
12021
+ const verbose = wantsVerbose(parsed);
12022
+ const selected = await selectTask(ctx, parsed, appId, projectId);
12023
+ if (selected.column !== READY_COLUMN) {
12024
+ throw new Error(
12025
+ `task ${selected.id} is ${selected.column === "doing" ? "already claimed" : `in ${selected.column}`}, not Ready. Only Ready work can be started; run "odla-ai pm task ready ${selected.id} --expected-revision ${selected.revision}" once its goal, description, and acceptance criteria are complete.`
12026
+ );
12027
+ }
12028
+ let claimed;
12029
+ try {
12030
+ claimed = await claimAt(ctx, selected, parsed);
12031
+ } catch (error) {
12032
+ if (!isRevisionConflict(error)) throw error;
12033
+ const reloaded = await getTask(ctx, selected.id);
12034
+ if (reloaded.column !== READY_COLUMN) {
12035
+ throw new Error(
12036
+ `task ${selected.id} was taken while starting it (now ${reloaded.column}). Run "odla-ai pm start --app ${appId}" again to take the next Ready task.`
12037
+ );
12038
+ }
12039
+ claimed = await claimAt(ctx, reloaded, parsed);
12040
+ }
12041
+ const task = claimed ?? await getTask(ctx, selected.id);
12042
+ const goalId = typeof task.goalId === "string" ? task.goalId : void 0;
12043
+ const goal = goalId ? await pmRequest(ctx, "GET", `/goal/${encodeURIComponent(goalId)}`).then((res) => res.record).catch(() => null) : null;
12044
+ const result = {
12045
+ claimed: verbose ? task : leanRecord("task", task),
12046
+ goal: goal ? verbose ? goal : leanRecord("goal", goal) : null,
12047
+ next: 'work the acceptance criteria, then "odla-ai pm task done <id>" (or "odla-ai pm task release <id> --expected-revision <n>" to hand it back)'
12048
+ };
12049
+ emit2(ctx, result, () => {
12050
+ ctx.out.log(`claimed: ${studioRecordLink(ctx, "task", task)}`);
12051
+ if (goal) ctx.out.log(`goal: ${studioRecordLink(ctx, "goal", goal)}`);
12052
+ if (typeof task.acceptanceCriteria === "string") ctx.out.log(`acceptance: ${task.acceptanceCriteria}`);
12053
+ ctx.out.log(`next: ${result.next}`);
12054
+ });
11941
12055
  }
11942
12056
 
11943
12057
  // src/pm-links.ts
@@ -12411,9 +12525,13 @@ async function pmCommand(parsed, deps = {}) {
12411
12525
  throw new Error(`unknown pm project action "${action3}". Try list|add|use.`);
12412
12526
  }
12413
12527
  if (word === "next") {
12414
- assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
12528
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
12415
12529
  return pmNext(await buildContext2(parsed, deps), parsed);
12416
12530
  }
12531
+ if (word === "start") {
12532
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "goal", "verbose", "mutation-id"], 3);
12533
+ return pmStart(await buildContext2(parsed, deps), parsed);
12534
+ }
12417
12535
  if (word === "watch") {
12418
12536
  assertArgs(parsed, [
12419
12537
  ...COMMON_OPTIONS,
@@ -12431,11 +12549,11 @@ async function pmCommand(parsed, deps = {}) {
12431
12549
  return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
12432
12550
  }
12433
12551
  if (word === "handoff") {
12434
- assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
12552
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
12435
12553
  return pmHandoff(await buildContext2(parsed, deps), parsed);
12436
12554
  }
12437
12555
  const entity = ALIASES[word];
12438
- if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
12556
+ if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm start" to claim Ready work, or "odla-ai pm bug list" (goal|task|decision|bug).`);
12439
12557
  const requestedAction = parsed.positionals[2] ?? "list";
12440
12558
  const action2 = canonicalAction(requestedAction);
12441
12559
  if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|link|ref|comment|comments|history|rm.`);
@@ -15958,4 +16076,4 @@ export {
15958
16076
  isTerminalHostedSecurityStatus,
15959
16077
  runCli
15960
16078
  };
15961
- //# sourceMappingURL=chunk-K6ATAKGR.js.map
16079
+ //# sourceMappingURL=chunk-WDWJ7HC7.js.map