@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.cjs CHANGED
@@ -11649,7 +11649,8 @@ Usage:
11649
11649
  odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
11650
11650
  odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
11651
11651
  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]
11652
- odla-ai pm next --app <id> [--project <id>] [--json]
11652
+ odla-ai pm next --app <id> [--project <id>] [--verbose] [--json]
11653
+ odla-ai pm start --app <id> [<task-id>] [--goal <id>] [--verbose] [--mutation-id <id>] [--json] [claims the top Ready task in one call]
11653
11654
  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]
11654
11655
  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]
11655
11656
  odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
@@ -11670,7 +11671,7 @@ Usage:
11670
11671
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
11671
11672
  odla-ai pm <goal|task|decision|bug> history <id> [--limit <n>] [--json]
11672
11673
  odla-ai pm <goal|task|decision|bug> rm <id>
11673
- odla-ai pm handoff --app <id> [--project <id>] [--json]
11674
+ odla-ai pm handoff --app <id> [--project <id>] [--verbose] [--json]
11674
11675
  odla-ai discuss groups [--json]
11675
11676
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--mentions] [--json]
11676
11677
  odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
@@ -11837,6 +11838,16 @@ Commands:
11837
11838
  execution contract and a reviewed revision. Any caller with PM
11838
11839
  mutation access to that app/project may do it; claims coordinate
11839
11840
  execution but never own or lock the PM record.
11841
+ To pick up work: "pm next" reads open goals, Ready candidates,
11842
+ and work in progress; "pm start" claims the top Ready task in
11843
+ one call, compare-and-swapped on the revision it just read, and
11844
+ returns the task, its goal, and the acceptance criteria to work
11845
+ against. Finish with "pm task done <id>", or hand it back with
11846
+ "pm task release <id> --expected-revision <n>". "pm start" never
11847
+ marks work Ready: that contract is reviewed separately so an
11848
+ agent cannot authorize its own work.
11849
+ Intake output is projected to the fields an executing caller
11850
+ acts on; --verbose returns whole records with their audit trail.
11840
11851
  bug Intent-first alias for PM bugs. "bug report" writes to
11841
11852
  odla PM; odla product defects do not belong in GitHub Issues.
11842
11853
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
@@ -12648,7 +12659,8 @@ async function pmRequest(ctx, method, path, body) {
12648
12659
  const message2 = error.message;
12649
12660
  if (typeof message2 === "string" && message2.length > 0) detail = message2;
12650
12661
  }
12651
- throw new Error(
12662
+ throw new PmRequestError(
12663
+ response2.status,
12652
12664
  `pm ${method} ${path} failed: ${detail ?? `registry returned ${response2.status}`} (${response2.status})`
12653
12665
  );
12654
12666
  }
@@ -12708,7 +12720,7 @@ function emit2(ctx, value2, human) {
12708
12720
  if (ctx.json) ctx.out.log(JSON.stringify(value2, null, 2));
12709
12721
  else human();
12710
12722
  }
12711
- var DONE, writeMutationId2, FIELD_MAP, STUDIO_SECTION;
12723
+ var DONE, writeMutationId2, FIELD_MAP, PmRequestError, isRevisionConflict, STUDIO_SECTION;
12712
12724
  var init_pm_action_core = __esm({
12713
12725
  "src/pm-action-core.ts"() {
12714
12726
  "use strict";
@@ -12741,6 +12753,15 @@ var init_pm_action_core = __esm({
12741
12753
  execution: { key: "executionMode" },
12742
12754
  "expected-revision": { key: "expectedRevision", num: true }
12743
12755
  };
12756
+ PmRequestError = class extends Error {
12757
+ constructor(status, message2) {
12758
+ super(message2);
12759
+ this.status = status;
12760
+ this.name = "PmRequestError";
12761
+ }
12762
+ status;
12763
+ };
12764
+ isRevisionConflict = (error) => error instanceof PmRequestError && error.status === 409;
12744
12765
  STUDIO_SECTION = {
12745
12766
  goal: "goals",
12746
12767
  task: "board",
@@ -12872,110 +12893,231 @@ async function pmTaskLifecycle(ctx, id2, action2, parsed) {
12872
12893
  ctx.out.log(`task: ${label} \u2192 ${state2}`);
12873
12894
  });
12874
12895
  }
12875
- async function allRecords(ctx, entity, appId, projectId) {
12896
+ async function pmRemove(ctx, entity, id2) {
12897
+ await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id2)}`);
12898
+ ctx.out.log(`deleted ${entity} ${id2}`);
12899
+ }
12900
+ var init_pm_actions = __esm({
12901
+ "src/pm-actions.ts"() {
12902
+ "use strict";
12903
+ init_cjs_shims();
12904
+ init_argv();
12905
+ init_pm_action_core();
12906
+ }
12907
+ });
12908
+
12909
+ // src/pm-lean.ts
12910
+ function leanRecord(entity, record11) {
12911
+ const out = {};
12912
+ for (const key of [...COMMON, ...PER_ENTITY[entity]]) {
12913
+ const value2 = record11[key];
12914
+ if (value2 !== void 0 && value2 !== null) out[key] = value2;
12915
+ }
12916
+ return out;
12917
+ }
12918
+ function leanRecords(entity, records, verbose) {
12919
+ return verbose ? records : records.map((record11) => leanRecord(entity, record11));
12920
+ }
12921
+ var COMMON, PER_ENTITY;
12922
+ var init_pm_lean = __esm({
12923
+ "src/pm-lean.ts"() {
12924
+ "use strict";
12925
+ init_cjs_shims();
12926
+ COMMON = ["id", "appId", "projectId", "title", "revision"];
12927
+ PER_ENTITY = {
12928
+ goal: ["status", "proof", "targetPct", "currentPct"],
12929
+ task: [
12930
+ "column",
12931
+ "goalId",
12932
+ "alignmentDecisionId",
12933
+ "description",
12934
+ "acceptanceCriteria",
12935
+ "executionMode",
12936
+ "assigneeId",
12937
+ "claimedByPrincipalId",
12938
+ "dueAt"
12939
+ ],
12940
+ decision: ["status", "body", "supersedesId"],
12941
+ bug: ["status", "severity", "description", "goalId", "assigneeId", "decisionId"]
12942
+ };
12943
+ }
12944
+ });
12945
+
12946
+ // src/pm-intake.ts
12947
+ async function listFiltered(ctx, entity, appId, projectId, filters) {
12876
12948
  const records = [];
12877
12949
  for (; ; ) {
12878
- const q = new URLSearchParams({
12879
- app: appId,
12880
- limit: "100",
12881
- offset: String(records.length)
12882
- });
12950
+ const q = new URLSearchParams({ app: appId, limit: "100", offset: String(records.length) });
12883
12951
  if (projectId) q.set("project", projectId);
12952
+ for (const [key, value2] of Object.entries(filters)) q.set(key, value2);
12884
12953
  const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
12885
12954
  records.push(...page2.records);
12886
12955
  if (records.length >= page2.total || page2.records.length === 0) return records;
12887
12956
  }
12888
12957
  }
12889
- async function pmNext(ctx, parsed) {
12958
+ function requireApp(ctx, parsed, command) {
12890
12959
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
12891
- const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
12892
- if (!appId) throw new Error("pm next needs --app <appId>");
12960
+ if (!appId) throw new Error(`pm ${command} needs --app <appId>`);
12961
+ return { appId, projectId: stringOpt(parsed.options.project) ?? ctx.projectId };
12962
+ }
12963
+ async function pmNext(ctx, parsed) {
12964
+ const { appId, projectId } = requireApp(ctx, parsed, "next");
12965
+ const verbose = wantsVerbose(parsed);
12893
12966
  const [goals, tasks] = await Promise.all([
12894
- allRecords(ctx, "goal", appId, projectId),
12895
- allRecords(ctx, "task", appId, projectId)
12967
+ listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
12968
+ // One request for both live columns; the split below is over open work only.
12969
+ listFiltered(ctx, "task", appId, projectId, { column: `${READY_COLUMN},doing` })
12896
12970
  ]);
12971
+ const ready = tasks.filter((record11) => record11.column === READY_COLUMN);
12972
+ const doing = tasks.filter((record11) => record11.column === "doing");
12973
+ 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}`;
12897
12974
  const result = {
12898
12975
  appId,
12899
12976
  projectId,
12900
- openGoals: goals.filter((record11) => record11.status === "open"),
12901
- doing: tasks.filter((record11) => record11.column === "doing"),
12902
- ready: tasks.filter((record11) => record11.column === "todo")
12977
+ openGoals: leanRecords("goal", goals, verbose),
12978
+ doing: leanRecords("task", doing, verbose),
12979
+ ready: leanRecords("task", ready, verbose),
12980
+ next: guidance
12903
12981
  };
12904
12982
  emit2(ctx, result, () => {
12905
12983
  ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
12906
- for (const [label, records] of [
12907
- ["doing", result.doing],
12908
- ["ready", result.ready],
12909
- ["open goals", result.openGoals]
12984
+ for (const [label, entity, records] of [
12985
+ ["doing", "task", doing],
12986
+ ["ready", "task", ready],
12987
+ ["open goals", "goal", goals]
12910
12988
  ]) {
12911
12989
  ctx.out.log(`${label}:`);
12912
12990
  if (!records.length) ctx.out.log("- (none)");
12913
- else for (const record11 of records) printRecord(
12914
- ctx,
12915
- label === "open goals" ? "goal" : "task",
12916
- record11
12917
- );
12918
- }
12919
- if (!result.openGoals.length) {
12920
- ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
12921
- } else if (!result.ready.length) {
12922
- ctx.out.log("next: refine a linked Backlog task and mark it Ready");
12923
- } else {
12924
- ctx.out.log("next: review a Ready task, then claim it with its revision");
12991
+ else for (const record11 of records) printRecord(ctx, entity, record11);
12925
12992
  }
12993
+ ctx.out.log(`next: ${guidance}`);
12926
12994
  });
12927
12995
  }
12928
12996
  async function pmHandoff(ctx, parsed) {
12929
- const appId = stringOpt(parsed.options.app) ?? ctx.appId;
12930
- const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
12931
- if (!appId) throw new Error("pm handoff needs --app <appId>");
12997
+ const { appId, projectId } = requireApp(ctx, parsed, "handoff");
12998
+ const verbose = wantsVerbose(parsed);
12932
12999
  const [goals, tasks, bugs] = await Promise.all([
12933
- allRecords(ctx, "goal", appId, projectId),
12934
- allRecords(ctx, "task", appId, projectId),
12935
- allRecords(ctx, "bug", appId, projectId)
13000
+ listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
13001
+ listFiltered(ctx, "task", appId, projectId, { column: ACTIVE_TASK_COLUMNS }),
13002
+ listFiltered(ctx, "bug", appId, projectId, { status: OPEN_BUG_STATUSES })
12936
13003
  ]);
12937
- const handoff = {
13004
+ const clean4 = !goals.length && !tasks.length && !bugs.length;
13005
+ const result = {
12938
13006
  appId,
12939
13007
  projectId,
12940
- unmetGoals: goals.filter((record11) => record11.status !== "met"),
12941
- activeTasks: tasks.filter((record11) => record11.column !== "done"),
12942
- openBugs: bugs.filter((record11) => record11.status !== "fixed" && record11.status !== "wontfix")
12943
- };
12944
- const result = {
12945
- ...handoff,
12946
- clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
13008
+ unmetGoals: leanRecords("goal", goals, verbose),
13009
+ activeTasks: leanRecords("task", tasks, verbose),
13010
+ openBugs: leanRecords("bug", bugs, verbose),
13011
+ clean: clean4
12947
13012
  };
12948
13013
  emit2(ctx, result, () => {
12949
- if (result.clean) {
13014
+ if (clean4) {
12950
13015
  ctx.out.log(`${appId}: no unresolved PM work`);
12951
13016
  return;
12952
13017
  }
12953
13018
  ctx.out.log(`${appId}: authoritative PM handoff`);
12954
- for (const [label, records] of [
12955
- ["unmet goals", result.unmetGoals],
12956
- ["active tasks", result.activeTasks],
12957
- ["open bugs", result.openBugs]
13019
+ for (const [label, entity, records] of [
13020
+ ["unmet goals", "goal", goals],
13021
+ ["active tasks", "task", tasks],
13022
+ ["open bugs", "bug", bugs]
12958
13023
  ]) {
12959
13024
  ctx.out.log(`${label}:`);
12960
13025
  if (!records.length) ctx.out.log("- (none)");
12961
- else for (const record11 of records) printRecord(
12962
- ctx,
12963
- label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
12964
- record11
12965
- );
13026
+ else for (const record11 of records) printRecord(ctx, entity, record11);
12966
13027
  }
12967
13028
  });
12968
13029
  }
12969
- async function pmRemove(ctx, entity, id2) {
12970
- await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id2)}`);
12971
- ctx.out.log(`deleted ${entity} ${id2}`);
13030
+ var READY_COLUMN, UNMET_GOAL_STATUS, ACTIVE_TASK_COLUMNS, OPEN_BUG_STATUSES, wantsVerbose;
13031
+ var init_pm_intake = __esm({
13032
+ "src/pm-intake.ts"() {
13033
+ "use strict";
13034
+ init_cjs_shims();
13035
+ init_argv();
13036
+ init_pm_action_core();
13037
+ init_pm_lean();
13038
+ READY_COLUMN = "todo";
13039
+ UNMET_GOAL_STATUS = "open";
13040
+ ACTIVE_TASK_COLUMNS = "backlog,todo,doing,review";
13041
+ OPEN_BUG_STATUSES = "open,triaged";
13042
+ wantsVerbose = (parsed) => parsed.options.verbose === true;
13043
+ }
13044
+ });
13045
+
13046
+ // src/pm-start.ts
13047
+ async function claimAt(ctx, task, parsed) {
13048
+ const res = await pmRequest(
13049
+ ctx,
13050
+ "POST",
13051
+ `/task/${encodeURIComponent(task.id)}/claim`,
13052
+ { expectedRevision: task.revision, mutationId: writeMutationId2(parsed) }
13053
+ );
13054
+ return res.record;
13055
+ }
13056
+ async function selectTask(ctx, parsed, appId, projectId) {
13057
+ const named = parsed.positionals[2];
13058
+ if (named) return getTask(ctx, named);
13059
+ const goalId = stringOpt(parsed.options.goal);
13060
+ const ready = await listFiltered(ctx, "task", appId, projectId, {
13061
+ column: READY_COLUMN,
13062
+ ...goalId ? { goalId } : {}
13063
+ });
13064
+ const candidate = ready[0];
13065
+ if (!candidate) {
13066
+ throw new Error(
13067
+ `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.`
13068
+ );
13069
+ }
13070
+ return candidate;
12972
13071
  }
12973
- var init_pm_actions = __esm({
12974
- "src/pm-actions.ts"() {
13072
+ async function pmStart(ctx, parsed) {
13073
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
13074
+ if (!appId) throw new Error("pm start needs --app <appId>");
13075
+ const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
13076
+ const verbose = wantsVerbose(parsed);
13077
+ const selected = await selectTask(ctx, parsed, appId, projectId);
13078
+ if (selected.column !== READY_COLUMN) {
13079
+ throw new Error(
13080
+ `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.`
13081
+ );
13082
+ }
13083
+ let claimed;
13084
+ try {
13085
+ claimed = await claimAt(ctx, selected, parsed);
13086
+ } catch (error) {
13087
+ if (!isRevisionConflict(error)) throw error;
13088
+ const reloaded = await getTask(ctx, selected.id);
13089
+ if (reloaded.column !== READY_COLUMN) {
13090
+ throw new Error(
13091
+ `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.`
13092
+ );
13093
+ }
13094
+ claimed = await claimAt(ctx, reloaded, parsed);
13095
+ }
13096
+ const task = claimed ?? await getTask(ctx, selected.id);
13097
+ const goalId = typeof task.goalId === "string" ? task.goalId : void 0;
13098
+ const goal = goalId ? await pmRequest(ctx, "GET", `/goal/${encodeURIComponent(goalId)}`).then((res) => res.record).catch(() => null) : null;
13099
+ const result = {
13100
+ claimed: verbose ? task : leanRecord("task", task),
13101
+ goal: goal ? verbose ? goal : leanRecord("goal", goal) : null,
13102
+ 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)'
13103
+ };
13104
+ emit2(ctx, result, () => {
13105
+ ctx.out.log(`claimed: ${studioRecordLink(ctx, "task", task)}`);
13106
+ if (goal) ctx.out.log(`goal: ${studioRecordLink(ctx, "goal", goal)}`);
13107
+ if (typeof task.acceptanceCriteria === "string") ctx.out.log(`acceptance: ${task.acceptanceCriteria}`);
13108
+ ctx.out.log(`next: ${result.next}`);
13109
+ });
13110
+ }
13111
+ var getTask;
13112
+ var init_pm_start = __esm({
13113
+ "src/pm-start.ts"() {
12975
13114
  "use strict";
12976
13115
  init_cjs_shims();
12977
13116
  init_argv();
12978
13117
  init_pm_action_core();
13118
+ init_pm_lean();
13119
+ init_pm_intake();
13120
+ getTask = async (ctx, id2) => (await pmRequest(ctx, "GET", `/task/${encodeURIComponent(id2)}`)).record;
12979
13121
  }
12980
13122
  });
12981
13123
 
@@ -13457,9 +13599,13 @@ async function pmCommand(parsed, deps = {}) {
13457
13599
  throw new Error(`unknown pm project action "${action3}". Try list|add|use.`);
13458
13600
  }
13459
13601
  if (word === "next") {
13460
- assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
13602
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
13461
13603
  return pmNext(await buildContext2(parsed, deps), parsed);
13462
13604
  }
13605
+ if (word === "start") {
13606
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "goal", "verbose", "mutation-id"], 3);
13607
+ return pmStart(await buildContext2(parsed, deps), parsed);
13608
+ }
13463
13609
  if (word === "watch") {
13464
13610
  assertArgs(parsed, [
13465
13611
  ...COMMON_OPTIONS,
@@ -13477,11 +13623,11 @@ async function pmCommand(parsed, deps = {}) {
13477
13623
  return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
13478
13624
  }
13479
13625
  if (word === "handoff") {
13480
- assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
13626
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
13481
13627
  return pmHandoff(await buildContext2(parsed, deps), parsed);
13482
13628
  }
13483
13629
  const entity = ALIASES[word];
13484
- if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
13630
+ 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).`);
13485
13631
  const requestedAction = parsed.positionals[2] ?? "list";
13486
13632
  const action2 = canonicalAction(requestedAction);
13487
13633
  if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|link|ref|comment|comments|history|rm.`);
@@ -13528,6 +13674,8 @@ var init_pm_command = __esm({
13528
13674
  init_argv();
13529
13675
  init_operator_context();
13530
13676
  init_pm_actions();
13677
+ init_pm_intake();
13678
+ init_pm_start();
13531
13679
  init_pm_links();
13532
13680
  init_pm_comments();
13533
13681
  init_pm_history();