@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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-K6ATAKGR.js";
4
+ } from "./chunk-WDWJ7HC7.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-4FCRBWED.js.map
12
+ //# sourceMappingURL=cli-OITEPXW4.js.map
package/dist/index.cjs CHANGED
@@ -10824,7 +10824,8 @@ Usage:
10824
10824
  odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
10825
10825
  odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
10826
10826
  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]
10827
- odla-ai pm next --app <id> [--project <id>] [--json]
10827
+ odla-ai pm next --app <id> [--project <id>] [--verbose] [--json]
10828
+ odla-ai pm start --app <id> [<task-id>] [--goal <id>] [--verbose] [--mutation-id <id>] [--json] [claims the top Ready task in one call]
10828
10829
  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]
10829
10830
  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]
10830
10831
  odla-ai pm task claim <id> --expected-revision <n> [--mutation-id <id>] [--json]
@@ -10845,7 +10846,7 @@ Usage:
10845
10846
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
10846
10847
  odla-ai pm <goal|task|decision|bug> history <id> [--limit <n>] [--json]
10847
10848
  odla-ai pm <goal|task|decision|bug> rm <id>
10848
- odla-ai pm handoff --app <id> [--project <id>] [--json]
10849
+ odla-ai pm handoff --app <id> [--project <id>] [--verbose] [--json]
10849
10850
  odla-ai discuss groups [--json]
10850
10851
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--mentions] [--json]
10851
10852
  odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
@@ -11010,6 +11011,16 @@ Commands:
11010
11011
  execution contract and a reviewed revision. Any caller with PM
11011
11012
  mutation access to that app/project may do it; claims coordinate
11012
11013
  execution but never own or lock the PM record.
11014
+ To pick up work: "pm next" reads open goals, Ready candidates,
11015
+ and work in progress; "pm start" claims the top Ready task in
11016
+ one call, compare-and-swapped on the revision it just read, and
11017
+ returns the task, its goal, and the acceptance criteria to work
11018
+ against. Finish with "pm task done <id>", or hand it back with
11019
+ "pm task release <id> --expected-revision <n>". "pm start" never
11020
+ marks work Ready: that contract is reviewed separately so an
11021
+ agent cannot authorize its own work.
11022
+ Intake output is projected to the fields an executing caller
11023
+ acts on; --verbose returns whole records with their audit trail.
11013
11024
  bug Intent-first alias for PM bugs. "bug report" writes to
11014
11025
  odla PM; odla product defects do not belong in GitHub Issues.
11015
11026
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
@@ -11760,6 +11771,15 @@ var FIELD_MAP = {
11760
11771
  execution: { key: "executionMode" },
11761
11772
  "expected-revision": { key: "expectedRevision", num: true }
11762
11773
  };
11774
+ var PmRequestError = class extends Error {
11775
+ constructor(status, message2) {
11776
+ super(message2);
11777
+ this.status = status;
11778
+ this.name = "PmRequestError";
11779
+ }
11780
+ status;
11781
+ };
11782
+ var isRevisionConflict = (error) => error instanceof PmRequestError && error.status === 409;
11763
11783
  async function pmRequest(ctx, method, path, body) {
11764
11784
  const response2 = await ctx.doFetch(`${ctx.platformUrl}/registry/pm${path}`, {
11765
11785
  method,
@@ -11779,7 +11799,8 @@ async function pmRequest(ctx, method, path, body) {
11779
11799
  const message2 = error.message;
11780
11800
  if (typeof message2 === "string" && message2.length > 0) detail = message2;
11781
11801
  }
11782
- throw new Error(
11802
+ throw new PmRequestError(
11803
+ response2.status,
11783
11804
  `pm ${method} ${path} failed: ${detail ?? `registry returned ${response2.status}`} (${response2.status})`
11784
11805
  );
11785
11806
  }
@@ -11968,103 +11989,196 @@ async function pmTaskLifecycle(ctx, id2, action2, parsed) {
11968
11989
  ctx.out.log(`task: ${label} \u2192 ${state2}`);
11969
11990
  });
11970
11991
  }
11971
- async function allRecords(ctx, entity, appId, projectId) {
11992
+ async function pmRemove(ctx, entity, id2) {
11993
+ await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id2)}`);
11994
+ ctx.out.log(`deleted ${entity} ${id2}`);
11995
+ }
11996
+
11997
+ // src/pm-lean.ts
11998
+ var COMMON = ["id", "appId", "projectId", "title", "revision"];
11999
+ var PER_ENTITY = {
12000
+ goal: ["status", "proof", "targetPct", "currentPct"],
12001
+ task: [
12002
+ "column",
12003
+ "goalId",
12004
+ "alignmentDecisionId",
12005
+ "description",
12006
+ "acceptanceCriteria",
12007
+ "executionMode",
12008
+ "assigneeId",
12009
+ "claimedByPrincipalId",
12010
+ "dueAt"
12011
+ ],
12012
+ decision: ["status", "body", "supersedesId"],
12013
+ bug: ["status", "severity", "description", "goalId", "assigneeId", "decisionId"]
12014
+ };
12015
+ function leanRecord(entity, record11) {
12016
+ const out = {};
12017
+ for (const key of [...COMMON, ...PER_ENTITY[entity]]) {
12018
+ const value2 = record11[key];
12019
+ if (value2 !== void 0 && value2 !== null) out[key] = value2;
12020
+ }
12021
+ return out;
12022
+ }
12023
+ function leanRecords(entity, records, verbose) {
12024
+ return verbose ? records : records.map((record11) => leanRecord(entity, record11));
12025
+ }
12026
+
12027
+ // src/pm-intake.ts
12028
+ var READY_COLUMN = "todo";
12029
+ var UNMET_GOAL_STATUS = "open";
12030
+ var ACTIVE_TASK_COLUMNS = "backlog,todo,doing,review";
12031
+ var OPEN_BUG_STATUSES = "open,triaged";
12032
+ var wantsVerbose = (parsed) => parsed.options.verbose === true;
12033
+ async function listFiltered(ctx, entity, appId, projectId, filters) {
11972
12034
  const records = [];
11973
12035
  for (; ; ) {
11974
- const q = new URLSearchParams({
11975
- app: appId,
11976
- limit: "100",
11977
- offset: String(records.length)
11978
- });
12036
+ const q = new URLSearchParams({ app: appId, limit: "100", offset: String(records.length) });
11979
12037
  if (projectId) q.set("project", projectId);
12038
+ for (const [key, value2] of Object.entries(filters)) q.set(key, value2);
11980
12039
  const page2 = await pmRequest(ctx, "GET", `/${entity}?${q}`);
11981
12040
  records.push(...page2.records);
11982
12041
  if (records.length >= page2.total || page2.records.length === 0) return records;
11983
12042
  }
11984
12043
  }
11985
- async function pmNext(ctx, parsed) {
12044
+ function requireApp(ctx, parsed, command) {
11986
12045
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
11987
- const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
11988
- if (!appId) throw new Error("pm next needs --app <appId>");
12046
+ if (!appId) throw new Error(`pm ${command} needs --app <appId>`);
12047
+ return { appId, projectId: stringOpt(parsed.options.project) ?? ctx.projectId };
12048
+ }
12049
+ async function pmNext(ctx, parsed) {
12050
+ const { appId, projectId } = requireApp(ctx, parsed, "next");
12051
+ const verbose = wantsVerbose(parsed);
11989
12052
  const [goals, tasks] = await Promise.all([
11990
- allRecords(ctx, "goal", appId, projectId),
11991
- allRecords(ctx, "task", appId, projectId)
12053
+ listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
12054
+ // One request for both live columns; the split below is over open work only.
12055
+ listFiltered(ctx, "task", appId, projectId, { column: `${READY_COLUMN},doing` })
11992
12056
  ]);
12057
+ const ready = tasks.filter((record11) => record11.column === READY_COLUMN);
12058
+ const doing = tasks.filter((record11) => record11.column === "doing");
12059
+ 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}`;
11993
12060
  const result = {
11994
12061
  appId,
11995
12062
  projectId,
11996
- openGoals: goals.filter((record11) => record11.status === "open"),
11997
- doing: tasks.filter((record11) => record11.column === "doing"),
11998
- ready: tasks.filter((record11) => record11.column === "todo")
12063
+ openGoals: leanRecords("goal", goals, verbose),
12064
+ doing: leanRecords("task", doing, verbose),
12065
+ ready: leanRecords("task", ready, verbose),
12066
+ next: guidance
11999
12067
  };
12000
12068
  emit2(ctx, result, () => {
12001
12069
  ctx.out.log(`${appId}: goal-aligned work intake (read only)`);
12002
- for (const [label, records] of [
12003
- ["doing", result.doing],
12004
- ["ready", result.ready],
12005
- ["open goals", result.openGoals]
12070
+ for (const [label, entity, records] of [
12071
+ ["doing", "task", doing],
12072
+ ["ready", "task", ready],
12073
+ ["open goals", "goal", goals]
12006
12074
  ]) {
12007
12075
  ctx.out.log(`${label}:`);
12008
12076
  if (!records.length) ctx.out.log("- (none)");
12009
- else for (const record11 of records) printRecord(
12010
- ctx,
12011
- label === "open goals" ? "goal" : "task",
12012
- record11
12013
- );
12014
- }
12015
- if (!result.openGoals.length) {
12016
- ctx.out.log("next: discuss alignment with the user before creating or claiming project work");
12017
- } else if (!result.ready.length) {
12018
- ctx.out.log("next: refine a linked Backlog task and mark it Ready");
12019
- } else {
12020
- ctx.out.log("next: review a Ready task, then claim it with its revision");
12077
+ else for (const record11 of records) printRecord(ctx, entity, record11);
12021
12078
  }
12079
+ ctx.out.log(`next: ${guidance}`);
12022
12080
  });
12023
12081
  }
12024
12082
  async function pmHandoff(ctx, parsed) {
12025
- const appId = stringOpt(parsed.options.app) ?? ctx.appId;
12026
- const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
12027
- if (!appId) throw new Error("pm handoff needs --app <appId>");
12083
+ const { appId, projectId } = requireApp(ctx, parsed, "handoff");
12084
+ const verbose = wantsVerbose(parsed);
12028
12085
  const [goals, tasks, bugs] = await Promise.all([
12029
- allRecords(ctx, "goal", appId, projectId),
12030
- allRecords(ctx, "task", appId, projectId),
12031
- allRecords(ctx, "bug", appId, projectId)
12086
+ listFiltered(ctx, "goal", appId, projectId, { status: UNMET_GOAL_STATUS }),
12087
+ listFiltered(ctx, "task", appId, projectId, { column: ACTIVE_TASK_COLUMNS }),
12088
+ listFiltered(ctx, "bug", appId, projectId, { status: OPEN_BUG_STATUSES })
12032
12089
  ]);
12033
- const handoff = {
12090
+ const clean4 = !goals.length && !tasks.length && !bugs.length;
12091
+ const result = {
12034
12092
  appId,
12035
12093
  projectId,
12036
- unmetGoals: goals.filter((record11) => record11.status !== "met"),
12037
- activeTasks: tasks.filter((record11) => record11.column !== "done"),
12038
- openBugs: bugs.filter((record11) => record11.status !== "fixed" && record11.status !== "wontfix")
12039
- };
12040
- const result = {
12041
- ...handoff,
12042
- clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
12094
+ unmetGoals: leanRecords("goal", goals, verbose),
12095
+ activeTasks: leanRecords("task", tasks, verbose),
12096
+ openBugs: leanRecords("bug", bugs, verbose),
12097
+ clean: clean4
12043
12098
  };
12044
12099
  emit2(ctx, result, () => {
12045
- if (result.clean) {
12100
+ if (clean4) {
12046
12101
  ctx.out.log(`${appId}: no unresolved PM work`);
12047
12102
  return;
12048
12103
  }
12049
12104
  ctx.out.log(`${appId}: authoritative PM handoff`);
12050
- for (const [label, records] of [
12051
- ["unmet goals", result.unmetGoals],
12052
- ["active tasks", result.activeTasks],
12053
- ["open bugs", result.openBugs]
12105
+ for (const [label, entity, records] of [
12106
+ ["unmet goals", "goal", goals],
12107
+ ["active tasks", "task", tasks],
12108
+ ["open bugs", "bug", bugs]
12054
12109
  ]) {
12055
12110
  ctx.out.log(`${label}:`);
12056
12111
  if (!records.length) ctx.out.log("- (none)");
12057
- else for (const record11 of records) printRecord(
12058
- ctx,
12059
- label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
12060
- record11
12061
- );
12112
+ else for (const record11 of records) printRecord(ctx, entity, record11);
12062
12113
  }
12063
12114
  });
12064
12115
  }
12065
- async function pmRemove(ctx, entity, id2) {
12066
- await pmRequest(ctx, "DELETE", `/${entity}/${encodeURIComponent(id2)}`);
12067
- ctx.out.log(`deleted ${entity} ${id2}`);
12116
+
12117
+ // src/pm-start.ts
12118
+ var getTask = async (ctx, id2) => (await pmRequest(ctx, "GET", `/task/${encodeURIComponent(id2)}`)).record;
12119
+ async function claimAt(ctx, task, parsed) {
12120
+ const res = await pmRequest(
12121
+ ctx,
12122
+ "POST",
12123
+ `/task/${encodeURIComponent(task.id)}/claim`,
12124
+ { expectedRevision: task.revision, mutationId: writeMutationId2(parsed) }
12125
+ );
12126
+ return res.record;
12127
+ }
12128
+ async function selectTask(ctx, parsed, appId, projectId) {
12129
+ const named = parsed.positionals[2];
12130
+ if (named) return getTask(ctx, named);
12131
+ const goalId = stringOpt(parsed.options.goal);
12132
+ const ready = await listFiltered(ctx, "task", appId, projectId, {
12133
+ column: READY_COLUMN,
12134
+ ...goalId ? { goalId } : {}
12135
+ });
12136
+ const candidate = ready[0];
12137
+ if (!candidate) {
12138
+ throw new Error(
12139
+ `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.`
12140
+ );
12141
+ }
12142
+ return candidate;
12143
+ }
12144
+ async function pmStart(ctx, parsed) {
12145
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
12146
+ if (!appId) throw new Error("pm start needs --app <appId>");
12147
+ const projectId = stringOpt(parsed.options.project) ?? ctx.projectId;
12148
+ const verbose = wantsVerbose(parsed);
12149
+ const selected = await selectTask(ctx, parsed, appId, projectId);
12150
+ if (selected.column !== READY_COLUMN) {
12151
+ throw new Error(
12152
+ `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.`
12153
+ );
12154
+ }
12155
+ let claimed;
12156
+ try {
12157
+ claimed = await claimAt(ctx, selected, parsed);
12158
+ } catch (error) {
12159
+ if (!isRevisionConflict(error)) throw error;
12160
+ const reloaded = await getTask(ctx, selected.id);
12161
+ if (reloaded.column !== READY_COLUMN) {
12162
+ throw new Error(
12163
+ `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.`
12164
+ );
12165
+ }
12166
+ claimed = await claimAt(ctx, reloaded, parsed);
12167
+ }
12168
+ const task = claimed ?? await getTask(ctx, selected.id);
12169
+ const goalId = typeof task.goalId === "string" ? task.goalId : void 0;
12170
+ const goal = goalId ? await pmRequest(ctx, "GET", `/goal/${encodeURIComponent(goalId)}`).then((res) => res.record).catch(() => null) : null;
12171
+ const result = {
12172
+ claimed: verbose ? task : leanRecord("task", task),
12173
+ goal: goal ? verbose ? goal : leanRecord("goal", goal) : null,
12174
+ 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)'
12175
+ };
12176
+ emit2(ctx, result, () => {
12177
+ ctx.out.log(`claimed: ${studioRecordLink(ctx, "task", task)}`);
12178
+ if (goal) ctx.out.log(`goal: ${studioRecordLink(ctx, "goal", goal)}`);
12179
+ if (typeof task.acceptanceCriteria === "string") ctx.out.log(`acceptance: ${task.acceptanceCriteria}`);
12180
+ ctx.out.log(`next: ${result.next}`);
12181
+ });
12068
12182
  }
12069
12183
 
12070
12184
  // src/pm-links.ts
@@ -12538,9 +12652,13 @@ async function pmCommand(parsed, deps = {}) {
12538
12652
  throw new Error(`unknown pm project action "${action3}". Try list|add|use.`);
12539
12653
  }
12540
12654
  if (word === "next") {
12541
- assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
12655
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
12542
12656
  return pmNext(await buildContext2(parsed, deps), parsed);
12543
12657
  }
12658
+ if (word === "start") {
12659
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "goal", "verbose", "mutation-id"], 3);
12660
+ return pmStart(await buildContext2(parsed, deps), parsed);
12661
+ }
12544
12662
  if (word === "watch") {
12545
12663
  assertArgs(parsed, [
12546
12664
  ...COMMON_OPTIONS,
@@ -12558,11 +12676,11 @@ async function pmCommand(parsed, deps = {}) {
12558
12676
  return pmWatch(await buildContext2(parsed, deps), parsed).then(() => void 0);
12559
12677
  }
12560
12678
  if (word === "handoff") {
12561
- assertArgs(parsed, [...COMMON_OPTIONS, "app", "project"], 2);
12679
+ assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
12562
12680
  return pmHandoff(await buildContext2(parsed, deps), parsed);
12563
12681
  }
12564
12682
  const entity = ALIASES[word];
12565
- if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
12683
+ 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).`);
12566
12684
  const requestedAction = parsed.positionals[2] ?? "list";
12567
12685
  const action2 = canonicalAction(requestedAction);
12568
12686
  if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|link|ref|comment|comments|history|rm.`);