@odla-ai/cli 0.23.0 → 0.24.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
@@ -43,12 +43,20 @@ app is registered, start each session by reading its doing tasks, open bugs and
43
43
  goals, and recent decisions:
44
44
 
45
45
  ```bash
46
+ npx odla-ai pm handoff --app <appId>
46
47
  npx odla-ai pm task list --app <appId> --column doing
47
48
  npx odla-ai pm bug list --app <appId> --status open
48
49
  npx odla-ai pm goal list --app <appId> --status open
49
50
  npx odla-ai pm decision list --app <appId> --limit 20
50
51
  ```
51
52
 
53
+ `pm handoff` is the authoritative unresolved-state view: unmet goals, every
54
+ non-done task, and every open or triaged bug. File bugs with actionable context
55
+ (`pm bug add ... --description "reproduction/evidence"`); a title-only bug is
56
+ rejected. When an accepted decision resolves or explicitly retains a bug, link
57
+ it with `pm bug done <id> --decision <decision-id>` or
58
+ `pm bug set <id> --status triaged --decision <decision-id>`.
59
+
52
60
  Read the active task, linked goal, and comments before changing code. Without
53
61
  `--app`, list commands span every project the account co-owns, which is the
54
62
  right first view when coordinating several efforts; scope every write to the
package/dist/bin.cjs CHANGED
@@ -6426,13 +6426,14 @@ Usage:
6426
6426
  odla-ai app owners add <email> [--email <odla-account>] [--json]
6427
6427
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
6428
6428
  odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
6429
- odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--body <text>|--proof <text>]
6429
+ odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
6430
6430
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
6431
- odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>]
6432
- odla-ai pm <goal|task|decision|bug> done <id>
6431
+ odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
6432
+ odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
6433
6433
  odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
6434
6434
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
6435
6435
  odla-ai pm <goal|task|decision|bug> rm <id>
6436
+ odla-ai pm handoff --app <id> [--json]
6436
6437
  odla-ai whoami [--json]
6437
6438
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6438
6439
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -7191,6 +7192,7 @@ var FIELD_MAP = {
7191
7192
  column: { key: "column" },
7192
7193
  rank: { key: "rank", num: true },
7193
7194
  goal: { key: "goalId" },
7195
+ decision: { key: "decisionId" },
7194
7196
  assignee: { key: "assigneeId" },
7195
7197
  due: { key: "dueAt", num: true },
7196
7198
  proof: { key: "proof" },
@@ -7229,6 +7231,14 @@ function collectFields(parsed, allowClear) {
7229
7231
  }
7230
7232
  return out;
7231
7233
  }
7234
+ function collectEntityFields(entity, parsed, allowClear) {
7235
+ const fields = collectFields(parsed, allowClear);
7236
+ if ((entity === "task" || entity === "bug") && fields.body !== void 0) {
7237
+ if (fields.description === void 0) fields.description = fields.body;
7238
+ delete fields.body;
7239
+ }
7240
+ return fields;
7241
+ }
7232
7242
  function statusCol(entity, r) {
7233
7243
  if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
7234
7244
  if (entity === "task") return String(r.column ?? "");
@@ -7243,7 +7253,14 @@ function emit(ctx, value, human) {
7243
7253
  }
7244
7254
  async function pmList(ctx, entity, parsed) {
7245
7255
  const q = new URLSearchParams();
7246
- const filters = { status: "status", severity: "severity", column: "column", goal: "goalId", assignee: "assigneeId" };
7256
+ const filters = {
7257
+ status: "status",
7258
+ severity: "severity",
7259
+ column: "column",
7260
+ goal: "goalId",
7261
+ assignee: "assigneeId",
7262
+ decision: "decisionId"
7263
+ };
7247
7264
  const app = stringOpt(parsed.options.app);
7248
7265
  if (app) q.set("app", app);
7249
7266
  for (const [flag, param] of Object.entries(filters)) {
@@ -7265,8 +7282,10 @@ async function pmList(ctx, entity, parsed) {
7265
7282
  async function pmAdd(ctx, entity, parsed) {
7266
7283
  const appId = stringOpt(parsed.options.app);
7267
7284
  if (!appId) throw new Error("pm add needs --app <appId>");
7268
- const input = collectFields(parsed, false);
7285
+ const input = collectEntityFields(entity, parsed, false);
7269
7286
  if (!input.title) throw new Error("pm add needs --title <title>");
7287
+ if (entity === "bug" && !input.description)
7288
+ throw new Error("pm bug add needs --description <context> (or --body <context>)");
7270
7289
  const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
7271
7290
  emit(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7272
7291
  }
@@ -7275,16 +7294,71 @@ async function pmGet(ctx, entity, id) {
7275
7294
  emit(ctx, record5, () => printRecord(ctx, entity, record5));
7276
7295
  }
7277
7296
  async function pmSet(ctx, entity, id, parsed) {
7278
- const patch2 = collectFields(parsed, true);
7297
+ const patch2 = collectEntityFields(entity, parsed, true);
7279
7298
  if (Object.keys(patch2).length === 0)
7280
7299
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
7281
7300
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7282
7301
  emit(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
7283
7302
  }
7284
- async function pmDone(ctx, entity, id) {
7285
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: DONE[entity] });
7303
+ async function pmDone(ctx, entity, id, parsed) {
7304
+ const decisionId = stringOpt(parsed.options.decision);
7305
+ if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
7306
+ const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
7307
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7286
7308
  emit(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7287
7309
  }
7310
+ async function allRecords(ctx, entity, appId) {
7311
+ const records = [];
7312
+ for (; ; ) {
7313
+ const q = new URLSearchParams({
7314
+ app: appId,
7315
+ limit: "100",
7316
+ offset: String(records.length)
7317
+ });
7318
+ const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
7319
+ records.push(...page.records);
7320
+ if (records.length >= page.total || page.records.length === 0) return records;
7321
+ }
7322
+ }
7323
+ async function pmHandoff(ctx, parsed) {
7324
+ const appId = stringOpt(parsed.options.app);
7325
+ if (!appId) throw new Error("pm handoff needs --app <appId>");
7326
+ const [goals, tasks, bugs] = await Promise.all([
7327
+ allRecords(ctx, "goal", appId),
7328
+ allRecords(ctx, "task", appId),
7329
+ allRecords(ctx, "bug", appId)
7330
+ ]);
7331
+ const handoff = {
7332
+ appId,
7333
+ unmetGoals: goals.filter((record5) => record5.status !== "met"),
7334
+ activeTasks: tasks.filter((record5) => record5.column !== "done"),
7335
+ openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
7336
+ };
7337
+ const result = {
7338
+ ...handoff,
7339
+ clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
7340
+ };
7341
+ emit(ctx, result, () => {
7342
+ if (result.clean) {
7343
+ ctx.out.log(`${appId}: no unresolved PM work`);
7344
+ return;
7345
+ }
7346
+ ctx.out.log(`${appId}: authoritative PM handoff`);
7347
+ for (const [label, records] of [
7348
+ ["unmet goals", result.unmetGoals],
7349
+ ["active tasks", result.activeTasks],
7350
+ ["open bugs", result.openBugs]
7351
+ ]) {
7352
+ ctx.out.log(`${label}:`);
7353
+ if (!records.length) ctx.out.log("- (none)");
7354
+ else for (const record5 of records) printRecord(
7355
+ ctx,
7356
+ label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
7357
+ record5
7358
+ );
7359
+ }
7360
+ });
7361
+ }
7288
7362
  async function pmComment(ctx, entity, id, parsed) {
7289
7363
  const body = stringOpt(parsed.options.body);
7290
7364
  if (!body) throw new Error('pm comment needs --body "..."');
@@ -7335,6 +7409,7 @@ var ALLOWED = [
7335
7409
  "target",
7336
7410
  "description",
7337
7411
  "desc",
7412
+ "decision",
7338
7413
  "limit",
7339
7414
  "offset",
7340
7415
  "q"
@@ -7357,6 +7432,10 @@ async function buildContext(parsed, deps) {
7357
7432
  }
7358
7433
  async function pmCommand(parsed, deps = {}) {
7359
7434
  const word = parsed.positionals[1] ?? "";
7435
+ if (word === "handoff") {
7436
+ assertArgs(parsed, ALLOWED, 2);
7437
+ return pmHandoff(await buildContext(parsed, deps), parsed);
7438
+ }
7360
7439
  const entity = ALIASES[word];
7361
7440
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
7362
7441
  const action = parsed.positionals[2] ?? "list";
@@ -7377,7 +7456,7 @@ async function pmCommand(parsed, deps = {}) {
7377
7456
  case "move":
7378
7457
  return pmSet(ctx, entity, requireId(id, action), parsed);
7379
7458
  case "done":
7380
- return pmDone(ctx, entity, requireId(id, action));
7459
+ return pmDone(ctx, entity, requireId(id, action), parsed);
7381
7460
  case "comment":
7382
7461
  return pmComment(ctx, entity, requireId(id, action), parsed);
7383
7462
  case "comments":
@@ -7853,7 +7932,10 @@ var COMMAND_SURFACE = {
7853
7932
  doctor: {},
7854
7933
  help: {},
7855
7934
  init: {},
7856
- pm: PM_ENTITIES,
7935
+ pm: {
7936
+ ...PM_ENTITIES,
7937
+ handoff: {}
7938
+ },
7857
7939
  provision: {},
7858
7940
  runbook: {
7859
7941
  ask: {},