@formigio/fazemos-cli 0.3.0 → 0.4.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/index.js CHANGED
@@ -224,6 +224,32 @@ orgs
224
224
  process.exit(1);
225
225
  }
226
226
  });
227
+ orgs
228
+ .command('show')
229
+ .description('Show organization details. Defaults to the active org if no ID is provided. Use "orgs list" to find org IDs.')
230
+ .argument('[id]', 'Organization ID (defaults to active org)')
231
+ .action(async (id) => {
232
+ try {
233
+ const orgId = id || getActiveOrgId();
234
+ if (!orgId) {
235
+ console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
236
+ process.exit(1);
237
+ }
238
+ const data = await api('GET', `/api/organizations/${orgId}`);
239
+ const org = data.organization;
240
+ console.log(chalk.cyan(org.name));
241
+ console.log(` ID: ${org.id}`);
242
+ console.log(` Slug: ${org.slug}`);
243
+ if (org.description)
244
+ console.log(` Desc: ${org.description}`);
245
+ if (org.member_count != null)
246
+ console.log(` Members: ${org.member_count}`);
247
+ }
248
+ catch (err) {
249
+ console.error(chalk.red(err.message));
250
+ process.exit(1);
251
+ }
252
+ });
227
253
  orgs
228
254
  .command('switch')
229
255
  .description('Switch active organization')
@@ -264,6 +290,82 @@ orgs
264
290
  process.exit(1);
265
291
  }
266
292
  });
293
+ orgs
294
+ .command('update')
295
+ .description('Update the active organization. Provide at least one of --name or --description. Requires an active org (use "orgs switch" first).')
296
+ .option('-n, --name <name>', 'New organization name')
297
+ .option('-d, --description <desc>', 'New description')
298
+ .action(async (opts) => {
299
+ try {
300
+ if (!opts.name && opts.description == null) {
301
+ console.error(chalk.red('Provide --name and/or --description'));
302
+ process.exit(1);
303
+ }
304
+ const orgId = getActiveOrgId();
305
+ if (!orgId) {
306
+ console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
307
+ process.exit(1);
308
+ }
309
+ const body = {};
310
+ if (opts.name)
311
+ body.name = opts.name;
312
+ if (opts.description != null)
313
+ body.description = opts.description;
314
+ const data = await api('PATCH', `/api/organizations/${orgId}`, body);
315
+ console.log(chalk.green(`Updated: ${data.organization.name}`));
316
+ }
317
+ catch (err) {
318
+ console.error(chalk.red(err.message));
319
+ process.exit(1);
320
+ }
321
+ });
322
+ orgs
323
+ .command('invite')
324
+ .description('Invite a human member to the organization by email. The invitee receives an email to join. Use "orgs add-agent" for AI agents instead.')
325
+ .requiredOption('-e, --email <email>', 'Email address to invite')
326
+ .option('-r, --role <role>', 'Organization role: admin (full access) or member (default)', 'member')
327
+ .action(async (opts) => {
328
+ try {
329
+ const orgId = getActiveOrgId();
330
+ if (!orgId) {
331
+ console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
332
+ process.exit(1);
333
+ }
334
+ const data = await api('POST', `/api/organizations/${orgId}/members/invite`, {
335
+ email: opts.email,
336
+ role: opts.role,
337
+ });
338
+ console.log(chalk.green(`Invited ${opts.email} as ${opts.role}`));
339
+ }
340
+ catch (err) {
341
+ console.error(chalk.red(err.message));
342
+ process.exit(1);
343
+ }
344
+ });
345
+ orgs
346
+ .command('add-agent')
347
+ .description('Create an AI agent member in the organization. The agent can then be assigned work via actions, commitments, or pipeline steps. Use "agents register" for bulk registration with roles and config.')
348
+ .requiredOption('-n, --name <name>', 'Agent display name (e.g., "kate", "marco")')
349
+ .option('-r, --role <role>', 'Organization role (admin, member)', 'member')
350
+ .action(async (opts) => {
351
+ try {
352
+ const orgId = getActiveOrgId();
353
+ if (!orgId) {
354
+ console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
355
+ process.exit(1);
356
+ }
357
+ const data = await api('POST', `/api/organizations/${orgId}/members`, {
358
+ displayName: opts.name,
359
+ role: opts.role,
360
+ });
361
+ console.log(chalk.green(`Created agent: ${data.member.display_name}`));
362
+ console.log(` ID: ${data.member.id}`);
363
+ }
364
+ catch (err) {
365
+ console.error(chalk.red(err.message));
366
+ process.exit(1);
367
+ }
368
+ });
267
369
  orgs
268
370
  .command('members')
269
371
  .description('List org members')
@@ -339,6 +441,91 @@ ws
339
441
  process.exit(1);
340
442
  }
341
443
  });
444
+ ws
445
+ .command('update')
446
+ .description('Update worksheet properties. Provide at least one field. Use "ws list" to find IDs, "ws show <id>" to see current values.')
447
+ .argument('<id>', 'Worksheet ID')
448
+ .option('-n, --name <name>', 'New worksheet name')
449
+ .option('-p, --purpose <purpose>', 'New purpose (e.g., "From X to Y by When")')
450
+ .option('--cadence <cadence>', 'Check-in cadence: weekly, biweekly, or monthly')
451
+ .action(async (id, opts) => {
452
+ try {
453
+ if (!opts.name && !opts.purpose && !opts.cadence) {
454
+ console.error(chalk.red('Provide --name, --purpose, and/or --cadence'));
455
+ process.exit(1);
456
+ }
457
+ const body = {};
458
+ if (opts.name)
459
+ body.name = opts.name;
460
+ if (opts.purpose)
461
+ body.purpose = opts.purpose;
462
+ if (opts.cadence)
463
+ body.checkInCadence = opts.cadence;
464
+ const data = await api('PATCH', `/api/worksheets/${id}`, body);
465
+ console.log(chalk.green(`Updated: ${data.worksheet.name}`));
466
+ }
467
+ catch (err) {
468
+ console.error(chalk.red(err.message));
469
+ process.exit(1);
470
+ }
471
+ });
472
+ ws
473
+ .command('archive')
474
+ .description('Archive a worksheet. Archived worksheets are hidden from "ws list" by default but can be found with "ws list -s archived". This is not reversible from the CLI.')
475
+ .argument('<id>', 'Worksheet ID')
476
+ .action(async (id) => {
477
+ try {
478
+ await api('POST', `/api/worksheets/${id}/archive`);
479
+ console.log(chalk.green('Worksheet archived'));
480
+ }
481
+ catch (err) {
482
+ console.error(chalk.red(err.message));
483
+ process.exit(1);
484
+ }
485
+ });
486
+ ws
487
+ .command('progress')
488
+ .description('Show the aggregated progress board for a worksheet. Combines outcomes, milestones, commitments, and actions into a single view. Use "ws show <id>" for the raw detail view instead.')
489
+ .argument('<id>', 'Worksheet ID')
490
+ .action(async (id) => {
491
+ try {
492
+ const data = await api('GET', `/api/worksheets/${id}/progress-board`);
493
+ const board = data.progressBoard || data;
494
+ console.log(chalk.cyan('Progress Board'));
495
+ if (board.outcomes?.length) {
496
+ console.log(chalk.cyan('\n Outcomes:'));
497
+ for (const o of board.outcomes) {
498
+ const progress = o.target_value ? ` ${o.current_value ?? 0}/${o.target_value}` : '';
499
+ console.log(` ${o.status === 'achieved' ? '✓' : '○'} ${o.name}${progress}`);
500
+ }
501
+ }
502
+ if (board.milestones?.length) {
503
+ console.log(chalk.cyan('\n Milestones:'));
504
+ for (const m of board.milestones) {
505
+ const icon = m.status === 'reached' ? '✓' : m.status === 'missed' ? '✗' : '○';
506
+ console.log(` ${icon} ${m.name}${m.target_date ? ` (${m.target_date})` : ''}`);
507
+ }
508
+ }
509
+ if (board.commitments?.length) {
510
+ console.log(chalk.cyan('\n Commitments:'));
511
+ for (const c of board.commitments) {
512
+ const icon = c.status === 'completed' ? chalk.green('✓') : c.status === 'missed' ? chalk.red('✗') : '○';
513
+ console.log(` ${icon} ${c.description} — ${c.status}`);
514
+ }
515
+ }
516
+ if (board.actions?.length) {
517
+ console.log(chalk.cyan('\n Actions:'));
518
+ for (const a of board.actions) {
519
+ const progress = a.target_value ? ` ${a.current_value ?? 0}/${a.target_value}` : '';
520
+ console.log(` ${a.description}${progress}`);
521
+ }
522
+ }
523
+ }
524
+ catch (err) {
525
+ console.error(chalk.red(err.message));
526
+ process.exit(1);
527
+ }
528
+ });
342
529
  ws
343
530
  .command('show')
344
531
  .description('Show worksheet detail')
@@ -483,6 +670,41 @@ outcomes
483
670
  process.exit(1);
484
671
  }
485
672
  });
673
+ outcomes
674
+ .command('update')
675
+ .description('Update an outcome. Provide at least one field to change. Use "oc update-value" to change just the current value, or "oc link/unlink" to manage parent linkage. Use "ws show <id>" to find outcome IDs.')
676
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
677
+ .requiredOption('-o, --outcome <id>', 'Outcome ID (from "ws show" output)')
678
+ .option('-n, --name <name>', 'New outcome name')
679
+ .option('-d, --description <desc>', 'New description')
680
+ .option('-m, --measurement <method>', 'How it is measured (e.g., "Count of active users")')
681
+ .option('-t, --target <value>', 'Target value (numeric)', parseNumber)
682
+ .option('-s, --status <status>', 'Status: active, achieved, or dropped')
683
+ .action(async (opts) => {
684
+ try {
685
+ const body = {};
686
+ if (opts.name)
687
+ body.name = opts.name;
688
+ if (opts.description != null)
689
+ body.description = opts.description;
690
+ if (opts.measurement)
691
+ body.measurement = opts.measurement;
692
+ if (opts.target !== undefined)
693
+ body.targetValue = opts.target;
694
+ if (opts.status)
695
+ body.status = opts.status;
696
+ if (Object.keys(body).length === 0) {
697
+ console.error(chalk.red('Provide at least one field to update'));
698
+ process.exit(1);
699
+ }
700
+ await api('PATCH', `/api/worksheets/${opts.worksheet}/outcomes/${opts.outcome}`, body);
701
+ console.log(chalk.green('Outcome updated'));
702
+ }
703
+ catch (err) {
704
+ console.error(chalk.red(err.message));
705
+ process.exit(1);
706
+ }
707
+ });
486
708
  outcomes
487
709
  .command('update-value')
488
710
  .description('Update an outcome current value')
@@ -499,6 +721,101 @@ outcomes
499
721
  process.exit(1);
500
722
  }
501
723
  });
724
+ outcomes
725
+ .command('remove')
726
+ .description('Permanently delete an outcome from a worksheet. If the outcome has linked children, those links will be broken. This cannot be undone.')
727
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
728
+ .requiredOption('-o, --outcome <id>', 'Outcome ID (from "ws show" output)')
729
+ .action(async (opts) => {
730
+ try {
731
+ await api('DELETE', `/api/worksheets/${opts.worksheet}/outcomes/${opts.outcome}`);
732
+ console.log(chalk.green('Outcome deleted'));
733
+ }
734
+ catch (err) {
735
+ console.error(chalk.red(err.message));
736
+ process.exit(1);
737
+ }
738
+ });
739
+ outcomes
740
+ .command('reorder')
741
+ .description('Reorder outcomes on a worksheet. Pass all outcome IDs in the desired display order. IDs not included will be appended at the end.')
742
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
743
+ .requiredOption('--ids <ids>', 'Comma-separated outcome IDs in desired order (e.g., --ids id1,id2,id3)', (v) => v.split(','))
744
+ .action(async (opts) => {
745
+ try {
746
+ await api('PATCH', `/api/worksheets/${opts.worksheet}/outcomes/reorder`, { outcomeIds: opts.ids });
747
+ console.log(chalk.green('Outcomes reordered'));
748
+ }
749
+ catch (err) {
750
+ console.error(chalk.red(err.message));
751
+ process.exit(1);
752
+ }
753
+ });
754
+ outcomes
755
+ .command('linkable')
756
+ .description('List outcomes from other worksheets that can be linked to as parents. Use this to discover valid --parent IDs for "oc link". Only shows outcomes that would not create a circular reference.')
757
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID whose outcomes you want to link from')
758
+ .action(async (opts) => {
759
+ try {
760
+ const data = await api('GET', `/api/outcomes/linkable?worksheetId=${opts.worksheet}`);
761
+ if (!data.outcomes?.length) {
762
+ console.log(chalk.yellow('No linkable outcomes'));
763
+ return;
764
+ }
765
+ for (const o of data.outcomes) {
766
+ const ws = o.worksheet_name ? ` on ${o.worksheet_name}` : '';
767
+ console.log(` ${o.name}${ws} — ${o.id}`);
768
+ }
769
+ }
770
+ catch (err) {
771
+ console.error(chalk.red(err.message));
772
+ process.exit(1);
773
+ }
774
+ });
775
+ outcomes
776
+ .command('children')
777
+ .description('Show outcomes from other worksheets that are linked to this outcome as children. These are outcomes where "oc link --parent <this-id>" was used.')
778
+ .argument('<id>', 'Parent outcome ID')
779
+ .action(async (id) => {
780
+ try {
781
+ const data = await api('GET', `/api/outcomes/${id}/children`);
782
+ if (!data.children?.length) {
783
+ console.log(chalk.yellow('No child outcomes'));
784
+ return;
785
+ }
786
+ for (const c of data.children) {
787
+ const progress = c.target_value ? ` ${c.current_value ?? 0}/${c.target_value}` : '';
788
+ const ws = c.worksheet_name ? ` on ${c.worksheet_name}` : '';
789
+ console.log(` ${c.status === 'achieved' ? '✓' : '○'} ${c.name}${progress}${ws} — ${c.id}`);
790
+ }
791
+ }
792
+ catch (err) {
793
+ console.error(chalk.red(err.message));
794
+ process.exit(1);
795
+ }
796
+ });
797
+ outcomes
798
+ .command('tree')
799
+ .description('Show the full hierarchy tree for an outcome, including all descendant outcomes across worksheets. Useful for seeing how team-level outcomes roll up to org-level goals.')
800
+ .argument('<id>', 'Root outcome ID')
801
+ .action(async (id) => {
802
+ try {
803
+ const data = await api('GET', `/api/outcomes/${id}/tree`);
804
+ const printNode = (node, indent) => {
805
+ const progress = node.target_value ? ` ${node.current_value ?? 0}/${node.target_value}` : '';
806
+ const ws = node.worksheet_name ? ` on ${node.worksheet_name}` : '';
807
+ console.log(`${indent}${node.status === 'achieved' ? '✓' : '○'} ${node.name}${progress}${ws}`);
808
+ for (const child of node.children || []) {
809
+ printNode(child, indent + ' ');
810
+ }
811
+ };
812
+ printNode(data.tree || data, ' ');
813
+ }
814
+ catch (err) {
815
+ console.error(chalk.red(err.message));
816
+ process.exit(1);
817
+ }
818
+ });
502
819
  // ── Milestones ──────────────────────────────────────────────
503
820
  const milestones = program.command('milestones').alias('ms').description('Milestone commands');
504
821
  milestones
@@ -527,8 +844,71 @@ milestones
527
844
  process.exit(1);
528
845
  }
529
846
  });
847
+ milestones
848
+ .command('update')
849
+ .description('Update a milestone. Provide at least one field to change. Use "ws show <id>" to find milestone IDs and see current values.')
850
+ .argument('<id>', 'Milestone ID (from "ws show" output)')
851
+ .option('-n, --name <name>', 'New milestone name')
852
+ .option('-d, --description <desc>', 'New description')
853
+ .option('-t, --target-date <date>', 'New target date (YYYY-MM-DD)')
854
+ .option('-s, --status <status>', 'Status: pending (default), reached, or missed')
855
+ .action(async (id, opts) => {
856
+ try {
857
+ const body = {};
858
+ if (opts.name)
859
+ body.name = opts.name;
860
+ if (opts.description != null)
861
+ body.description = opts.description;
862
+ if (opts.targetDate)
863
+ body.targetDate = opts.targetDate;
864
+ if (opts.status)
865
+ body.status = opts.status;
866
+ if (Object.keys(body).length === 0) {
867
+ console.error(chalk.red('Provide at least one field to update'));
868
+ process.exit(1);
869
+ }
870
+ await api('PATCH', `/api/milestones/${id}`, body);
871
+ console.log(chalk.green('Milestone updated'));
872
+ }
873
+ catch (err) {
874
+ console.error(chalk.red(err.message));
875
+ process.exit(1);
876
+ }
877
+ });
878
+ milestones
879
+ .command('remove')
880
+ .description('Permanently delete a milestone. This cannot be undone.')
881
+ .argument('<id>', 'Milestone ID (from "ws show" output)')
882
+ .action(async (id) => {
883
+ try {
884
+ await api('DELETE', `/api/milestones/${id}`);
885
+ console.log(chalk.green('Milestone deleted'));
886
+ }
887
+ catch (err) {
888
+ console.error(chalk.red(err.message));
889
+ process.exit(1);
890
+ }
891
+ });
892
+ milestones
893
+ .command('reorder')
894
+ .description('Reorder milestones on a worksheet. Pass all milestone IDs in the desired display order.')
895
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
896
+ .requiredOption('--ids <ids>', 'Comma-separated milestone IDs in desired order (e.g., --ids id1,id2,id3)', (v) => v.split(','))
897
+ .action(async (opts) => {
898
+ try {
899
+ await api('PATCH', `/api/worksheets/${opts.worksheet}/milestones/reorder`, { milestoneIds: opts.ids });
900
+ console.log(chalk.green('Milestones reordered'));
901
+ }
902
+ catch (err) {
903
+ console.error(chalk.red(err.message));
904
+ process.exit(1);
905
+ }
906
+ });
530
907
  // ── Members ─────────────────────────────────────────────────
531
- const members = program.command('members').description('Worksheet member commands');
908
+ const members = program.command('members').description('Member commands (worksheet and org level).\n\n' +
909
+ ' Worksheet members: add, list, update, remove — manage who works on a worksheet.\n' +
910
+ ' Org members: show, update-org, delete — manage org-level member records.\n' +
911
+ ' Use "orgs members" to list all org members and find member IDs.');
532
912
  members
533
913
  .command('add')
534
914
  .description('Add a member to a worksheet')
@@ -549,19 +929,111 @@ members
549
929
  }
550
930
  });
551
931
  members
552
- .command('list')
553
- .description('List worksheet members')
554
- .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
555
- .action(async (opts) => {
932
+ .command('list')
933
+ .description('List worksheet members')
934
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
935
+ .action(async (opts) => {
936
+ try {
937
+ const data = await api('GET', `/api/worksheets/${opts.worksheet}/members`);
938
+ if (!data.members?.length) {
939
+ console.log(chalk.yellow('No members'));
940
+ return;
941
+ }
942
+ for (const m of data.members) {
943
+ console.log(` ${chalk.cyan(m.display_name)} (${m.worksheet_role}) — ${m.member_id}`);
944
+ }
945
+ }
946
+ catch (err) {
947
+ console.error(chalk.red(err.message));
948
+ process.exit(1);
949
+ }
950
+ });
951
+ members
952
+ .command('update')
953
+ .description('Change a worksheet member\'s role. Use "members list -w <id>" to find worksheet member IDs.')
954
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
955
+ .requiredOption('-m, --member <id>', 'Worksheet member ID (from "members list" output)')
956
+ .requiredOption('-r, --role <role>', 'New role: lead or contributor')
957
+ .action(async (opts) => {
958
+ try {
959
+ await api('PATCH', `/api/worksheets/${opts.worksheet}/members/${opts.member}`, { role: opts.role });
960
+ console.log(chalk.green(`Member role updated to ${opts.role}`));
961
+ }
962
+ catch (err) {
963
+ console.error(chalk.red(err.message));
964
+ process.exit(1);
965
+ }
966
+ });
967
+ members
968
+ .command('remove')
969
+ .description('Remove a member from a worksheet. The member remains in the organization but loses access to this worksheet.')
970
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
971
+ .requiredOption('-m, --member <id>', 'Worksheet member ID (from "members list" output)')
972
+ .action(async (opts) => {
973
+ try {
974
+ await api('DELETE', `/api/worksheets/${opts.worksheet}/members/${opts.member}`);
975
+ console.log(chalk.green('Member removed from worksheet'));
976
+ }
977
+ catch (err) {
978
+ console.error(chalk.red(err.message));
979
+ process.exit(1);
980
+ }
981
+ });
982
+ members
983
+ .command('show')
984
+ .description('Show an organization member\'s detail, including role, type, and agent status. Use "orgs members" to find member IDs.')
985
+ .argument('<id>', 'Member ID (from "orgs members" output)')
986
+ .action(async (id) => {
987
+ try {
988
+ const data = await api('GET', `/api/members/${id}`);
989
+ const m = data.member;
990
+ console.log(chalk.cyan(m.display_name));
991
+ console.log(` ID: ${m.id}`);
992
+ console.log(` Role: ${m.role}`);
993
+ console.log(` Type: ${m.member_type}`);
994
+ if (m.email)
995
+ console.log(` Email: ${m.email}`);
996
+ if (m.agent_status)
997
+ console.log(` Status: ${m.agent_status}`);
998
+ }
999
+ catch (err) {
1000
+ console.error(chalk.red(err.message));
1001
+ process.exit(1);
1002
+ }
1003
+ });
1004
+ members
1005
+ .command('update-org')
1006
+ .description('Update an organization member\'s role or display name. This is the org-level role, not the worksheet role. Use "members update" for worksheet roles instead.')
1007
+ .argument('<id>', 'Member ID (from "orgs members" output)')
1008
+ .option('-r, --role <role>', 'New org role: admin or member')
1009
+ .option('-n, --name <name>', 'New display name')
1010
+ .action(async (id, opts) => {
1011
+ try {
1012
+ const body = {};
1013
+ if (opts.role)
1014
+ body.role = opts.role;
1015
+ if (opts.name)
1016
+ body.displayName = opts.name;
1017
+ if (Object.keys(body).length === 0) {
1018
+ console.error(chalk.red('Provide --role and/or --name'));
1019
+ process.exit(1);
1020
+ }
1021
+ await api('PATCH', `/api/members/${id}`, body);
1022
+ console.log(chalk.green('Member updated'));
1023
+ }
1024
+ catch (err) {
1025
+ console.error(chalk.red(err.message));
1026
+ process.exit(1);
1027
+ }
1028
+ });
1029
+ members
1030
+ .command('delete')
1031
+ .description('Permanently remove a member from the organization. This removes them from all worksheets and revokes access. Cannot be undone.')
1032
+ .argument('<id>', 'Member ID (from "orgs members" output)')
1033
+ .action(async (id) => {
556
1034
  try {
557
- const data = await api('GET', `/api/worksheets/${opts.worksheet}/members`);
558
- if (!data.members?.length) {
559
- console.log(chalk.yellow('No members'));
560
- return;
561
- }
562
- for (const m of data.members) {
563
- console.log(` ${chalk.cyan(m.display_name)} (${m.worksheet_role}) — ${m.member_id}`);
564
- }
1035
+ await api('DELETE', `/api/members/${id}`);
1036
+ console.log(chalk.green('Member removed from organization'));
565
1037
  }
566
1038
  catch (err) {
567
1039
  console.error(chalk.red(err.message));
@@ -637,6 +1109,55 @@ actions
637
1109
  process.exit(1);
638
1110
  }
639
1111
  });
1112
+ actions
1113
+ .command('update')
1114
+ .description('Update an action. Provide at least one field to change. Use "ac update-value" to change just the current value. Use "ac list -w <id>" to find action IDs.')
1115
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
1116
+ .requiredOption('-a, --action <id>', 'Action ID (from "ac list" or "ws show" output)')
1117
+ .option('-n, --name <name>', 'New action description')
1118
+ .option('-m, --measurement <method>', 'How it is measured (e.g., "PRs merged per week")')
1119
+ .option('-t, --target <value>', 'Target value (numeric)', parseNumber)
1120
+ .option('-o, --outcome <id>', 'Link to a different outcome ID')
1121
+ .action(async (opts) => {
1122
+ try {
1123
+ const body = {};
1124
+ if (opts.name)
1125
+ body.description = opts.name;
1126
+ if (opts.measurement)
1127
+ body.measurement = opts.measurement;
1128
+ if (opts.target !== undefined)
1129
+ body.targetValue = opts.target;
1130
+ if (opts.outcome)
1131
+ body.outcomeId = opts.outcome;
1132
+ if (Object.keys(body).length === 0) {
1133
+ console.error(chalk.red('Provide at least one field to update'));
1134
+ process.exit(1);
1135
+ }
1136
+ await api('PATCH', `/api/worksheets/${opts.worksheet}/actions/${opts.action}`, body);
1137
+ console.log(chalk.green('Action updated'));
1138
+ }
1139
+ catch (err) {
1140
+ console.error(chalk.red(err.message));
1141
+ process.exit(1);
1142
+ }
1143
+ });
1144
+ actions
1145
+ .command('execute')
1146
+ .description('Trigger an agent execution for an action. The API determines which agent to use based on the action\'s configuration. For more control over agent selection and parameters, use the top-level "execute" command instead.')
1147
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
1148
+ .requiredOption('-a, --action <id>', 'Action ID (from "ac list" or "ws show" output)')
1149
+ .action(async (opts) => {
1150
+ try {
1151
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions/${opts.action}/execute`);
1152
+ console.log(chalk.green('Execution triggered'));
1153
+ if (data.execution?.id)
1154
+ console.log(` Execution ID: ${data.execution.id}`);
1155
+ }
1156
+ catch (err) {
1157
+ console.error(chalk.red(err.message));
1158
+ process.exit(1);
1159
+ }
1160
+ });
640
1161
  // ── Commitments ─────────────────────────────────────────────
641
1162
  const commitments = program.command('commitments').alias('cm').description('Commitment commands');
642
1163
  commitments
@@ -697,6 +1218,243 @@ commitments
697
1218
  process.exit(1);
698
1219
  }
699
1220
  });
1221
+ commitments
1222
+ .command('execute')
1223
+ .description('Trigger an agent execution for a commitment. The API determines which agent to use based on the commitment\'s configuration. For more control, use the top-level "execute" command instead.')
1224
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
1225
+ .requiredOption('-c, --commitment <id>', 'Commitment ID (from "cm list" or "ws show" output)')
1226
+ .action(async (opts) => {
1227
+ try {
1228
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/commitments/${opts.commitment}/execute`);
1229
+ console.log(chalk.green('Execution triggered'));
1230
+ if (data.execution?.id)
1231
+ console.log(` Execution ID: ${data.execution.id}`);
1232
+ }
1233
+ catch (err) {
1234
+ console.error(chalk.red(err.message));
1235
+ process.exit(1);
1236
+ }
1237
+ });
1238
+ // ── Notes ───────────────────────────────────────────────────
1239
+ const notes = program.command('notes').alias('nt').description('Worksheet note commands. Notes are freeform text attached to a worksheet for capturing context, decisions, or observations.');
1240
+ notes
1241
+ .command('add')
1242
+ .description('Add a note to a worksheet. Notes are timestamped and attributed to the current user. Good for recording decisions, context, or observations that don\'t fit into outcomes or actions.')
1243
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
1244
+ .requiredOption('-t, --text <text>', 'Note text (e.g., "Decided to defer mobile until Q3")')
1245
+ .action(async (opts) => {
1246
+ try {
1247
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/notes`, { text: opts.text });
1248
+ const n = data.note;
1249
+ console.log(chalk.green('Note added'));
1250
+ console.log(` ID: ${n.id}`);
1251
+ }
1252
+ catch (err) {
1253
+ console.error(chalk.red(err.message));
1254
+ process.exit(1);
1255
+ }
1256
+ });
1257
+ notes
1258
+ .command('list')
1259
+ .description('List notes on a worksheet')
1260
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
1261
+ .action(async (opts) => {
1262
+ try {
1263
+ const data = await api('GET', `/api/worksheets/${opts.worksheet}/notes`);
1264
+ if (!data.notes?.length) {
1265
+ console.log(chalk.yellow('No notes'));
1266
+ return;
1267
+ }
1268
+ for (const n of data.notes) {
1269
+ const time = n.created_at ? new Date(n.created_at).toLocaleString() : '';
1270
+ const author = n.author_name || '';
1271
+ console.log(` ${chalk.gray(time)} ${author ? chalk.cyan(author) + ' — ' : ''}${n.text}`);
1272
+ console.log(` ID: ${n.id}`);
1273
+ }
1274
+ }
1275
+ catch (err) {
1276
+ console.error(chalk.red(err.message));
1277
+ process.exit(1);
1278
+ }
1279
+ });
1280
+ notes
1281
+ .command('remove')
1282
+ .description('Permanently delete a note. Use "nt list -w <id>" to find note IDs.')
1283
+ .argument('<id>', 'Note ID (from "nt list" output)')
1284
+ .action(async (id) => {
1285
+ try {
1286
+ await api('DELETE', `/api/notes/${id}`);
1287
+ console.log(chalk.green('Note deleted'));
1288
+ }
1289
+ catch (err) {
1290
+ console.error(chalk.red(err.message));
1291
+ process.exit(1);
1292
+ }
1293
+ });
1294
+ // ── Check-ins ───────────────────────────────────────────────
1295
+ const checkins = program.command('check-ins').alias('ci').description('Check-in commands.\n\n' +
1296
+ ' Check-ins are periodic reviews of worksheet progress (weekly, biweekly, etc.).\n' +
1297
+ ' During a check-in, members report on commitments and make new ones.\n\n' +
1298
+ ' Typical flow:\n' +
1299
+ ' 1. ci create -w <id> Start a new check-in\n' +
1300
+ ' 2. ci report <id> --reports [...] Report on past commitments\n' +
1301
+ ' 3. ci add-commitment <id> -d "..." Make new commitments\n' +
1302
+ ' 4. ci complete <id> Close the check-in');
1303
+ checkins
1304
+ .command('create')
1305
+ .description('Start a new check-in for a worksheet. This creates a check-in record where members can report on past commitments and make new ones. Only one check-in is typically open at a time.')
1306
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
1307
+ .option('-n, --name <name>', 'Check-in name (e.g., "Week of 2026-03-23")')
1308
+ .action(async (opts) => {
1309
+ try {
1310
+ const body = {};
1311
+ if (opts.name)
1312
+ body.name = opts.name;
1313
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/check-ins`, body);
1314
+ const ci = data.checkIn;
1315
+ console.log(chalk.green('Check-in created'));
1316
+ console.log(` ID: ${ci.id}`);
1317
+ if (ci.status)
1318
+ console.log(` Status: ${ci.status}`);
1319
+ }
1320
+ catch (err) {
1321
+ console.error(chalk.red(err.message));
1322
+ process.exit(1);
1323
+ }
1324
+ });
1325
+ checkins
1326
+ .command('list')
1327
+ .description('List check-ins for a worksheet')
1328
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
1329
+ .action(async (opts) => {
1330
+ try {
1331
+ const data = await api('GET', `/api/worksheets/${opts.worksheet}/check-ins`);
1332
+ if (!data.checkIns?.length) {
1333
+ console.log(chalk.yellow('No check-ins'));
1334
+ return;
1335
+ }
1336
+ for (const ci of data.checkIns) {
1337
+ const icon = ci.status === 'completed' ? chalk.green('✓') : '○';
1338
+ const date = ci.created_at ? new Date(ci.created_at).toLocaleDateString() : '';
1339
+ console.log(` ${icon} ${ci.name || date} (${ci.status}) — ${ci.id}`);
1340
+ }
1341
+ }
1342
+ catch (err) {
1343
+ console.error(chalk.red(err.message));
1344
+ process.exit(1);
1345
+ }
1346
+ });
1347
+ checkins
1348
+ .command('show')
1349
+ .description('Show check-in detail including commitment reports and new commitments made during the check-in.')
1350
+ .argument('<id>', 'Check-in ID (from "ci list" output)')
1351
+ .action(async (id) => {
1352
+ try {
1353
+ const data = await api('GET', `/api/check-ins/${id}`);
1354
+ const ci = data.checkIn;
1355
+ console.log(chalk.cyan(ci.name || `Check-in ${ci.id}`));
1356
+ console.log(` ID: ${ci.id}`);
1357
+ console.log(` Status: ${ci.status}`);
1358
+ if (ci.created_at)
1359
+ console.log(` Created: ${ci.created_at}`);
1360
+ if (ci.reports?.length) {
1361
+ console.log(chalk.cyan('\n Reports:'));
1362
+ for (const r of ci.reports) {
1363
+ const icon = r.status === 'completed' ? chalk.green('✓') : r.status === 'missed' ? chalk.red('✗') : '○';
1364
+ console.log(` ${icon} ${r.commitment_description || r.commitment_id} — ${r.status}`);
1365
+ if (r.notes)
1366
+ console.log(` ${r.notes}`);
1367
+ }
1368
+ }
1369
+ if (ci.commitments?.length) {
1370
+ console.log(chalk.cyan('\n Commitments:'));
1371
+ for (const c of ci.commitments) {
1372
+ const icon = c.status === 'completed' ? chalk.green('✓') : '○';
1373
+ console.log(` ${icon} ${c.description} — due ${c.due_date} — ${c.id}`);
1374
+ }
1375
+ }
1376
+ }
1377
+ catch (err) {
1378
+ console.error(chalk.red(err.message));
1379
+ process.exit(1);
1380
+ }
1381
+ });
1382
+ checkins
1383
+ .command('update')
1384
+ .description('Update a check-in\'s name or notes. The check-in must still be open (not completed).')
1385
+ .argument('<id>', 'Check-in ID (from "ci list" output)')
1386
+ .option('-n, --name <name>', 'New check-in name')
1387
+ .option('--notes <text>', 'General notes for this check-in')
1388
+ .action(async (id, opts) => {
1389
+ try {
1390
+ const body = {};
1391
+ if (opts.name)
1392
+ body.name = opts.name;
1393
+ if (opts.notes != null)
1394
+ body.notes = opts.notes;
1395
+ if (Object.keys(body).length === 0) {
1396
+ console.error(chalk.red('Provide at least one field to update'));
1397
+ process.exit(1);
1398
+ }
1399
+ await api('PATCH', `/api/check-ins/${id}`, body);
1400
+ console.log(chalk.green('Check-in updated'));
1401
+ }
1402
+ catch (err) {
1403
+ console.error(chalk.red(err.message));
1404
+ process.exit(1);
1405
+ }
1406
+ });
1407
+ checkins
1408
+ .command('report')
1409
+ .description('Submit commitment reports for a check-in. Each report records whether a past commitment was completed, missed, or is still in progress.\n\n Example:\n ci report <id> --reports \'[{"commitmentId":"abc","status":"completed","notes":"Shipped it"}]\'')
1410
+ .argument('<id>', 'Check-in ID (from "ci list" output)')
1411
+ .requiredOption('--reports <json>', 'JSON array of reports. Each: {commitmentId, status (completed|missed|in_progress), notes?}')
1412
+ .action(async (id, opts) => {
1413
+ try {
1414
+ const reports = JSON.parse(opts.reports);
1415
+ await api('POST', `/api/check-ins/${id}/reports`, { reports });
1416
+ console.log(chalk.green(`Submitted ${reports.length} report(s)`));
1417
+ }
1418
+ catch (err) {
1419
+ console.error(chalk.red(err.message));
1420
+ process.exit(1);
1421
+ }
1422
+ });
1423
+ checkins
1424
+ .command('add-commitment')
1425
+ .description('Make a new commitment during a check-in. This is how team members pledge specific deliverables for the next period. The commitment will appear in "cm list" and be reportable in the next check-in.')
1426
+ .argument('<id>', 'Check-in ID (from "ci list" output)')
1427
+ .requiredOption('-d, --description <desc>', 'What you commit to do (e.g., "Ship the new API endpoint")')
1428
+ .requiredOption('--due <date>', 'Due date (YYYY-MM-DD)')
1429
+ .option('-a, --action <id>', 'Link to an action ID for tracking')
1430
+ .action(async (id, opts) => {
1431
+ try {
1432
+ const body = { description: opts.description, dueDate: opts.due };
1433
+ if (opts.action)
1434
+ body.actionId = opts.action;
1435
+ const data = await api('POST', `/api/check-ins/${id}/commitments`, body);
1436
+ console.log(chalk.green(`Commitment added: ${data.commitment.description}`));
1437
+ console.log(` ID: ${data.commitment.id}`);
1438
+ }
1439
+ catch (err) {
1440
+ console.error(chalk.red(err.message));
1441
+ process.exit(1);
1442
+ }
1443
+ });
1444
+ checkins
1445
+ .command('complete')
1446
+ .description('Close a check-in. Once completed, no more reports or commitments can be added. This finalizes the check-in record.')
1447
+ .argument('<id>', 'Check-in ID (from "ci list" output)')
1448
+ .action(async (id) => {
1449
+ try {
1450
+ await api('POST', `/api/check-ins/${id}/complete`);
1451
+ console.log(chalk.green('Check-in completed'));
1452
+ }
1453
+ catch (err) {
1454
+ console.error(chalk.red(err.message));
1455
+ process.exit(1);
1456
+ }
1457
+ });
700
1458
  // ── Template I/O helpers ───────────────────────────────────
701
1459
  function allSteps(definition) {
702
1460
  const steps = [];
@@ -1996,6 +2754,50 @@ step
1996
2754
  process.exit(1);
1997
2755
  }
1998
2756
  });
2757
+ step
2758
+ .command('approve')
2759
+ .description('Approve a submitted step. The step must be in "submitted" status (awaiting review). Approval marks the step as completed and may trigger downstream steps to be queued. Use "pl show <id>" to see step statuses.')
2760
+ .argument('<instanceId>', 'Pipeline instance ID')
2761
+ .argument('<stepId>', 'Step instance ID')
2762
+ .option('--notes <text>', 'Approval notes (visible to the step assignee)')
2763
+ .action(async (instanceId, stepId, opts) => {
2764
+ try {
2765
+ const body = {};
2766
+ if (opts.notes)
2767
+ body.notes = opts.notes;
2768
+ const data = await api('POST', `/api/pipeline-instances/${instanceId}/steps/${stepId}/approve`, body);
2769
+ console.log(chalk.green(`Step approved: ${data.step?.step_name || stepId}`));
2770
+ if (data.queued_steps?.length) {
2771
+ console.log(chalk.cyan(`\n Next steps queued: ${data.queued_steps.length}`));
2772
+ for (const qs of data.queued_steps) {
2773
+ console.log(` ○ ${qs.step_name} [${qs.role || '-'}]`);
2774
+ }
2775
+ }
2776
+ }
2777
+ catch (err) {
2778
+ console.error(chalk.red(err.message));
2779
+ process.exit(1);
2780
+ }
2781
+ });
2782
+ step
2783
+ .command('revise')
2784
+ .description('Request revision on a submitted step. Sends the step back to "in_progress" with feedback. The assignee can then resubmit. The step\'s review cycle counter increments; if max cycles are reached, the step auto-approves.')
2785
+ .argument('<instanceId>', 'Pipeline instance ID')
2786
+ .argument('<stepId>', 'Step instance ID')
2787
+ .requiredOption('--reason <text>', 'Revision feedback explaining what needs to change')
2788
+ .action(async (instanceId, stepId, opts) => {
2789
+ try {
2790
+ const data = await api('POST', `/api/pipeline-instances/${instanceId}/steps/${stepId}/revise`, {
2791
+ reason: opts.reason,
2792
+ });
2793
+ console.log(chalk.green(`Revision requested: ${data.step?.step_name || stepId}`));
2794
+ console.log(` Status: ${data.step?.status}`);
2795
+ }
2796
+ catch (err) {
2797
+ console.error(chalk.red(err.message));
2798
+ process.exit(1);
2799
+ }
2800
+ });
1999
2801
  step
2000
2802
  .command('skip')
2001
2803
  .description('Skip a step')
@@ -2400,6 +3202,82 @@ executions
2400
3202
  process.exit(1);
2401
3203
  }
2402
3204
  });
3205
+ executions
3206
+ .command('dispatch')
3207
+ .description('Dispatch a pending execution, claiming it for container launch. Moves the execution from "pending" to "dispatched". Typically called by the execution dispatcher, not manually.')
3208
+ .argument('<id>', 'Execution ID')
3209
+ .action(async (id) => {
3210
+ try {
3211
+ const data = await api('POST', `/api/executions/${id}/dispatch`);
3212
+ console.log(chalk.green(`Execution dispatched: ${data.execution?.id || id}`));
3213
+ console.log(` Status: ${data.execution?.status}`);
3214
+ }
3215
+ catch (err) {
3216
+ console.error(chalk.red(err.message));
3217
+ process.exit(1);
3218
+ }
3219
+ });
3220
+ executions
3221
+ .command('complete')
3222
+ .description('Mark an execution as completed with optional results. Typically called by the agent container when work is done. Use "ex update" for partial updates during execution.')
3223
+ .argument('<id>', 'Execution ID')
3224
+ .option('-o, --output <json>', 'Final output data as JSON')
3225
+ .option('--cost <usd>', 'Total cost in USD', parseNumber)
3226
+ .option('--input-tokens <n>', 'Total input token count', parseNumber)
3227
+ .option('--output-tokens <n>', 'Total output token count', parseNumber)
3228
+ .option('--duration <ms>', 'Total duration in milliseconds', parseNumber)
3229
+ .action(async (id, opts) => {
3230
+ try {
3231
+ const body = {};
3232
+ if (opts.output)
3233
+ body.outputData = JSON.parse(opts.output);
3234
+ if (opts.cost !== undefined)
3235
+ body.costUsd = opts.cost;
3236
+ if (opts.inputTokens !== undefined)
3237
+ body.inputTokens = opts.inputTokens;
3238
+ if (opts.outputTokens !== undefined)
3239
+ body.outputTokens = opts.outputTokens;
3240
+ if (opts.duration !== undefined)
3241
+ body.durationMs = opts.duration;
3242
+ await api('POST', `/api/executions/${id}/complete`, body);
3243
+ console.log(chalk.green('Execution completed'));
3244
+ }
3245
+ catch (err) {
3246
+ console.error(chalk.red(err.message));
3247
+ process.exit(1);
3248
+ }
3249
+ });
3250
+ executions
3251
+ .command('fail')
3252
+ .description('Mark an execution as failed with an error message. Typically called by the agent container on unrecoverable errors. Partial output and cost data can still be recorded.')
3253
+ .argument('<id>', 'Execution ID')
3254
+ .requiredOption('--error <text>', 'Error message describing the failure')
3255
+ .option('-o, --output <json>', 'Partial output data as JSON (any work completed before failure)')
3256
+ .option('--cost <usd>', 'Cost incurred in USD', parseNumber)
3257
+ .option('--input-tokens <n>', 'Input token count', parseNumber)
3258
+ .option('--output-tokens <n>', 'Output token count', parseNumber)
3259
+ .option('--duration <ms>', 'Duration in milliseconds before failure', parseNumber)
3260
+ .action(async (id, opts) => {
3261
+ try {
3262
+ const body = { error: opts.error };
3263
+ if (opts.output)
3264
+ body.outputData = JSON.parse(opts.output);
3265
+ if (opts.cost !== undefined)
3266
+ body.costUsd = opts.cost;
3267
+ if (opts.inputTokens !== undefined)
3268
+ body.inputTokens = opts.inputTokens;
3269
+ if (opts.outputTokens !== undefined)
3270
+ body.outputTokens = opts.outputTokens;
3271
+ if (opts.duration !== undefined)
3272
+ body.durationMs = opts.duration;
3273
+ await api('POST', `/api/executions/${id}/fail`, body);
3274
+ console.log(chalk.green('Execution marked as failed'));
3275
+ }
3276
+ catch (err) {
3277
+ console.error(chalk.red(err.message));
3278
+ process.exit(1);
3279
+ }
3280
+ });
2403
3281
  executions
2404
3282
  .command('cloudwatch-logs')
2405
3283
  .alias('cw')