@formigio/fazemos-cli 0.2.4 → 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')
@@ -568,32 +948,124 @@ members
568
948
  process.exit(1);
569
949
  }
570
950
  });
571
- // ── Actions ─────────────────────────────────────────────────
572
- const actions = program.command('actions').alias('ac').description('Action (lead measure) commands');
573
- actions
574
- .command('add')
575
- .description('Add an action to a worksheet')
951
+ members
952
+ .command('update')
953
+ .description('Change a worksheet member\'s role. Use "members list -w <id>" to find worksheet member IDs.')
576
954
  .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
577
- .requiredOption('-n, --name <name>', 'Action description')
578
- .option('-o, --outcome <id>', 'Linked outcome ID')
579
- .option('-m, --measurement <method>', 'How it is measured')
580
- .option('-t, --target <value>', 'Target value', parseNumber)
581
- .option('-c, --current <value>', 'Current value', parseNumber)
955
+ .requiredOption('-m, --member <id>', 'Worksheet member ID (from "members list" output)')
956
+ .requiredOption('-r, --role <role>', 'New role: lead or contributor')
582
957
  .action(async (opts) => {
583
958
  try {
584
- const body = { description: opts.name };
585
- if (opts.outcome)
586
- body.outcomeId = opts.outcome;
587
- if (opts.measurement)
588
- body.measurement = opts.measurement;
589
- if (opts.target !== undefined)
590
- body.targetValue = opts.target;
591
- if (opts.current !== undefined)
592
- body.currentValue = opts.current;
593
- const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions`, body);
594
- const a = data.action;
595
- console.log(chalk.green(`Added action: ${a.description}`));
596
- console.log(` ID: ${a.id}`);
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) => {
1034
+ try {
1035
+ await api('DELETE', `/api/members/${id}`);
1036
+ console.log(chalk.green('Member removed from organization'));
1037
+ }
1038
+ catch (err) {
1039
+ console.error(chalk.red(err.message));
1040
+ process.exit(1);
1041
+ }
1042
+ });
1043
+ // ── Actions ─────────────────────────────────────────────────
1044
+ const actions = program.command('actions').alias('ac').description('Action (lead measure) commands');
1045
+ actions
1046
+ .command('add')
1047
+ .description('Add an action to a worksheet')
1048
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
1049
+ .requiredOption('-n, --name <name>', 'Action description')
1050
+ .option('-o, --outcome <id>', 'Linked outcome ID')
1051
+ .option('-m, --measurement <method>', 'How it is measured')
1052
+ .option('-t, --target <value>', 'Target value', parseNumber)
1053
+ .option('-c, --current <value>', 'Current value', parseNumber)
1054
+ .action(async (opts) => {
1055
+ try {
1056
+ const body = { description: opts.name };
1057
+ if (opts.outcome)
1058
+ body.outcomeId = opts.outcome;
1059
+ if (opts.measurement)
1060
+ body.measurement = opts.measurement;
1061
+ if (opts.target !== undefined)
1062
+ body.targetValue = opts.target;
1063
+ if (opts.current !== undefined)
1064
+ body.currentValue = opts.current;
1065
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions`, body);
1066
+ const a = data.action;
1067
+ console.log(chalk.green(`Added action: ${a.description}`));
1068
+ console.log(` ID: ${a.id}`);
597
1069
  }
598
1070
  catch (err) {
599
1071
  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 = [];
@@ -717,8 +1475,24 @@ function requireDraftStatus(template) {
717
1475
  process.exit(1);
718
1476
  }
719
1477
  }
1478
+ function findPhaseById(definition, phaseId) {
1479
+ return (definition?.phases || []).find((p) => p.id === phaseId);
1480
+ }
1481
+ const VALID_STEP_TYPES = ['human', 'agent', 'script', 'gate'];
720
1482
  // ── Templates ──────────────────────────────────────────────
721
- const templates = program.command('templates').alias('tpl').description('Pipeline template commands');
1483
+ const templates = program.command('templates').alias('tpl').description('Pipeline template commands.\n\n' +
1484
+ ' Templates define multi-step workflows: template → phases → steps → I/O.\n' +
1485
+ ' Lifecycle: draft → active → archived (archived → draft to unarchive).\n' +
1486
+ ' Structural edits (add/remove/edit phases, steps, I/O) require draft status.\n\n' +
1487
+ ' Typical workflow:\n' +
1488
+ ' 1. tpl create -n "Name" Create empty draft template\n' +
1489
+ ' 2. tpl add-phase <id> --name ... Add phases\n' +
1490
+ ' 3. tpl add-step <id> --phase ... Add steps to phases\n' +
1491
+ ' 4. tpl add-output / add-input Wire I/O between steps\n' +
1492
+ ' 5. tpl validate <id> Check for errors\n' +
1493
+ ' 6. tpl activate <id> Make available for instances\n\n' +
1494
+ ' Use "tpl steps <id>" to list step IDs needed by --step options.\n' +
1495
+ ' Use "tpl show <id>" to see full structure with I/O declarations.');
722
1496
  templates
723
1497
  .command('list')
724
1498
  .description('List pipeline templates')
@@ -744,8 +1518,8 @@ templates
744
1518
  });
745
1519
  templates
746
1520
  .command('show')
747
- .description('Show template detail')
748
- .argument('<id>', 'Template ID')
1521
+ .description('Show template detail including phases, steps, I/O declarations, and pipeline inputs. Use this to inspect the full structure of a template and discover phase/step IDs needed by other commands.')
1522
+ .argument('<id>', 'Template ID (use "tpl list" to find IDs)')
749
1523
  .action(async (id) => {
750
1524
  try {
751
1525
  const data = await api('GET', `/api/pipeline-templates/${id}`);
@@ -803,19 +1577,243 @@ templates
803
1577
  }
804
1578
  });
805
1579
  templates
806
- .command('create')
807
- .description('Create a pipeline template')
808
- .requiredOption('-n, --name <name>', 'Template name')
809
- .option('-d, --description <desc>', 'Description')
810
- .action(async (opts) => {
1580
+ .command('create')
1581
+ .description('Create an empty pipeline template in draft status. After creation, use add-phase, add-step, and I/O commands to build the structure, then activate.')
1582
+ .requiredOption('-n, --name <name>', 'Template name')
1583
+ .option('-d, --description <desc>', 'Description')
1584
+ .action(async (opts) => {
1585
+ try {
1586
+ const body = { name: opts.name, definition: { phases: [] } };
1587
+ if (opts.description)
1588
+ body.description = opts.description;
1589
+ const data = await api('POST', '/api/pipeline-templates', body);
1590
+ const t = data.template;
1591
+ console.log(chalk.green(`Created: ${t.name}`));
1592
+ console.log(` ID: ${t.id}`);
1593
+ }
1594
+ catch (err) {
1595
+ console.error(chalk.red(err.message));
1596
+ process.exit(1);
1597
+ }
1598
+ });
1599
+ templates
1600
+ .command('import')
1601
+ .description('Import a JOE template JSON file as a Fazemos pipeline template. Imports steps, outputs, inputs, and pipeline-level inputs. Step IDs are remapped to new UUIDs and input references are updated accordingly.')
1602
+ .argument('<file>', 'Path to JOE template JSON file')
1603
+ .action(async (file) => {
1604
+ try {
1605
+ const raw = JSON.parse(readFileSync(resolve(file), 'utf-8'));
1606
+ // Map JOE template to Fazemos format
1607
+ const joeSteps = raw.steps || [];
1608
+ // Build ID mapping so input source_step_id references survive UUID reassignment
1609
+ const idMap = new Map();
1610
+ for (const s of joeSteps) {
1611
+ if (s.id)
1612
+ idMap.set(s.id, crypto.randomUUID());
1613
+ }
1614
+ const definition = {
1615
+ inputs: (raw.inputs || []).map((i) => ({
1616
+ name: i.name,
1617
+ type: i.type || 'text',
1618
+ required: i.required !== false,
1619
+ ...(i.description ? { description: i.description } : {}),
1620
+ ...(i.default_value != null ? { default_value: i.default_value } : {}),
1621
+ })),
1622
+ phases: [{
1623
+ id: crypto.randomUUID(),
1624
+ name: raw.name || 'Main',
1625
+ description: raw.description || '',
1626
+ deliverables: [],
1627
+ steps: joeSteps.map((s, i) => ({
1628
+ id: idMap.get(s.id) || crypto.randomUUID(),
1629
+ name: s.name,
1630
+ description: s.description || '',
1631
+ step_type: s.executionMode === 'script' ? 'script' : (s.agent ? 'agent' : 'human'),
1632
+ role: s.role || s.agent || 'unassigned',
1633
+ inputs: (s.inputs || []).map((inp) => ({
1634
+ name: inp.name,
1635
+ ...(inp.source_step_id ? { source_step_id: idMap.get(inp.source_step_id) || inp.source_step_id, source_output_name: inp.source_output_name } : {}),
1636
+ ...(inp.pipeline_input ? { pipeline_input: inp.pipeline_input } : {}),
1637
+ ...(inp.description ? { description: inp.description } : {}),
1638
+ required: inp.required !== false,
1639
+ })),
1640
+ outputs: (s.outputs || []).map((o) => ({
1641
+ name: o.name,
1642
+ type: o.type || 'text',
1643
+ ...(o.description ? { description: o.description } : {}),
1644
+ required: o.required !== false,
1645
+ ...(o.format ? { format: o.format } : {}),
1646
+ })),
1647
+ sections: s.sections || '',
1648
+ reviewer: s.reviewer || null,
1649
+ max_review_cycles: s.maxReviewCycles || 0,
1650
+ execution_config: s.executionMode === 'script' ? {
1651
+ image: s.image || '',
1652
+ command: s.command || '',
1653
+ } : null,
1654
+ parallel_group: null,
1655
+ sort_order: i,
1656
+ })),
1657
+ }],
1658
+ };
1659
+ const body = {
1660
+ name: raw.name || file,
1661
+ description: raw.description || '',
1662
+ definition,
1663
+ };
1664
+ const data = await api('POST', '/api/pipeline-templates', body);
1665
+ const t = data.template;
1666
+ console.log(chalk.green(`Imported: ${t.name} (${joeSteps.length} steps)`));
1667
+ console.log(` ID: ${t.id}`);
1668
+ }
1669
+ catch (err) {
1670
+ console.error(chalk.red(err.message));
1671
+ process.exit(1);
1672
+ }
1673
+ });
1674
+ templates
1675
+ .command('update-definition')
1676
+ .description('Update a template definition from a JSON file')
1677
+ .argument('<id>', 'Template ID')
1678
+ .argument('<file>', 'Path to definition JSON file')
1679
+ .action(async (id, file) => {
1680
+ try {
1681
+ const definition = JSON.parse(readFileSync(resolve(file), 'utf-8'));
1682
+ await api('PUT', `/api/pipeline-templates/${id}`, { definition });
1683
+ console.log(chalk.green('Template definition updated'));
1684
+ }
1685
+ catch (err) {
1686
+ console.error(chalk.red(err.message));
1687
+ process.exit(1);
1688
+ }
1689
+ });
1690
+ templates
1691
+ .command('activate')
1692
+ .description('Activate a draft template (required before creating pipeline instances). Template must have at least one phase with at least one step. Status changes from draft → active.')
1693
+ .argument('<id>', 'Template ID')
1694
+ .action(async (id) => {
1695
+ try {
1696
+ await api('PATCH', `/api/pipeline-templates/${id}/status`, { status: 'active' });
1697
+ console.log(chalk.green('Template activated'));
1698
+ }
1699
+ catch (err) {
1700
+ console.error(chalk.red(err.message));
1701
+ process.exit(1);
1702
+ }
1703
+ });
1704
+ // ── Template structure commands ─────────────────────────────
1705
+ templates
1706
+ .command('update')
1707
+ .description('Update template name or description. Works on any status (draft, active, or archived). Does not modify the definition or bump version.')
1708
+ .argument('<id>', 'Template ID')
1709
+ .option('-n, --name <name>', 'New name')
1710
+ .option('-d, --description <desc>', 'New description')
1711
+ .action(async (id, opts) => {
1712
+ try {
1713
+ if (!opts.name && opts.description == null) {
1714
+ console.error(chalk.red('Provide --name and/or --description'));
1715
+ process.exit(1);
1716
+ }
1717
+ const body = {};
1718
+ if (opts.name)
1719
+ body.name = opts.name;
1720
+ if (opts.description != null)
1721
+ body.description = opts.description;
1722
+ const data = await api('PUT', `/api/pipeline-templates/${id}`, body);
1723
+ console.log(chalk.green(`Updated: ${data.template.name}`));
1724
+ }
1725
+ catch (err) {
1726
+ console.error(chalk.red(err.message));
1727
+ process.exit(1);
1728
+ }
1729
+ });
1730
+ templates
1731
+ .command('archive')
1732
+ .description('Archive an active template. Archived templates cannot be used to create new instances. Use "tpl unarchive" to move back to draft for editing.')
1733
+ .argument('<id>', 'Template ID')
1734
+ .action(async (id) => {
1735
+ try {
1736
+ await api('PATCH', `/api/pipeline-templates/${id}/status`, { status: 'archived' });
1737
+ console.log(chalk.green('Template archived'));
1738
+ }
1739
+ catch (err) {
1740
+ console.error(chalk.red(err.message));
1741
+ process.exit(1);
1742
+ }
1743
+ });
1744
+ templates
1745
+ .command('unarchive')
1746
+ .description('Move an archived template back to draft status for editing. After edits, use "tpl activate" to make it available again.')
1747
+ .argument('<id>', 'Template ID')
1748
+ .action(async (id) => {
1749
+ try {
1750
+ await api('PATCH', `/api/pipeline-templates/${id}/status`, { status: 'draft' });
1751
+ console.log(chalk.green('Template moved to draft'));
1752
+ }
1753
+ catch (err) {
1754
+ console.error(chalk.red(err.message));
1755
+ process.exit(1);
1756
+ }
1757
+ });
1758
+ templates
1759
+ .command('add-phase')
1760
+ .description('Add a phase to a template. Phases group steps into logical stages (e.g., "Design", "Build", "Test"). Template must be in draft status. Phase names must be unique within the template. Returns the generated phase ID needed by add-step.')
1761
+ .argument('<templateId>', 'Template ID')
1762
+ .requiredOption('--name <name>', 'Phase name (must be unique within the template)')
1763
+ .option('--description <desc>', 'Phase description')
1764
+ .action(async (templateId, opts) => {
1765
+ try {
1766
+ const data = await api('GET', `/api/pipeline-templates/${templateId}`);
1767
+ const t = data.template;
1768
+ requireDraftStatus(t);
1769
+ if (!t.definition.phases)
1770
+ t.definition.phases = [];
1771
+ if (t.definition.phases.find((p) => p.name === opts.name)) {
1772
+ console.error(chalk.red(`Phase "${opts.name}" already exists`));
1773
+ process.exit(1);
1774
+ }
1775
+ const phase = {
1776
+ id: crypto.randomUUID(),
1777
+ name: opts.name,
1778
+ description: opts.description || '',
1779
+ deliverables: [],
1780
+ steps: [],
1781
+ };
1782
+ t.definition.phases.push(phase);
1783
+ await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
1784
+ console.log(chalk.green(`Added phase: ${opts.name}`));
1785
+ console.log(` ID: ${phase.id}`);
1786
+ }
1787
+ catch (err) {
1788
+ console.error(chalk.red(err.message));
1789
+ process.exit(1);
1790
+ }
1791
+ });
1792
+ templates
1793
+ .command('remove-phase')
1794
+ .description('Remove a phase from a template. Blocked if the phase contains steps unless --force is used. Template must be in draft status. Use "tpl show" to find phase IDs.')
1795
+ .argument('<templateId>', 'Template ID')
1796
+ .requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" output)')
1797
+ .option('--force', 'Remove even if phase contains steps')
1798
+ .action(async (templateId, opts) => {
811
1799
  try {
812
- const body = { name: opts.name, definition: { phases: [] } };
813
- if (opts.description)
814
- body.description = opts.description;
815
- const data = await api('POST', '/api/pipeline-templates', body);
1800
+ const data = await api('GET', `/api/pipeline-templates/${templateId}`);
816
1801
  const t = data.template;
817
- console.log(chalk.green(`Created: ${t.name}`));
818
- console.log(` ID: ${t.id}`);
1802
+ requireDraftStatus(t);
1803
+ const idx = (t.definition.phases || []).findIndex((p) => p.id === opts.phase);
1804
+ if (idx === -1) {
1805
+ console.error(chalk.red(`Phase "${opts.phase}" not found`));
1806
+ process.exit(1);
1807
+ }
1808
+ const phase = t.definition.phases[idx];
1809
+ if (phase.steps?.length && !opts.force) {
1810
+ console.error(chalk.yellow(`Phase "${phase.name}" has ${phase.steps.length} steps`));
1811
+ console.error(chalk.yellow('Use --force to remove anyway'));
1812
+ process.exit(1);
1813
+ }
1814
+ t.definition.phases.splice(idx, 1);
1815
+ await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
1816
+ console.log(chalk.green(`Removed phase: ${phase.name}`));
819
1817
  }
820
1818
  catch (err) {
821
1819
  console.error(chalk.red(err.message));
@@ -823,74 +1821,137 @@ templates
823
1821
  }
824
1822
  });
825
1823
  templates
826
- .command('import')
827
- .description('Import a JOE template JSON file as a Fazemos pipeline template')
828
- .argument('<file>', 'Path to JOE template JSON file')
829
- .action(async (file) => {
1824
+ .command('edit-phase')
1825
+ .description('Edit a phase name or description. Template must be in draft status. Provide at least one of --name or --description.')
1826
+ .argument('<templateId>', 'Template ID')
1827
+ .requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" output)')
1828
+ .option('--name <name>', 'New phase name (must be unique within the template)')
1829
+ .option('--description <desc>', 'New phase description')
1830
+ .action(async (templateId, opts) => {
830
1831
  try {
831
- const raw = JSON.parse(readFileSync(resolve(file), 'utf-8'));
832
- // Map JOE template to Fazemos format
833
- const joeSteps = raw.steps || [];
834
- // Build ID mapping so input source_step_id references survive UUID reassignment
835
- const idMap = new Map();
836
- for (const s of joeSteps) {
837
- if (s.id)
838
- idMap.set(s.id, crypto.randomUUID());
1832
+ if (!opts.name && opts.description == null) {
1833
+ console.error(chalk.red('Provide --name and/or --description'));
1834
+ process.exit(1);
839
1835
  }
840
- const definition = {
841
- inputs: (raw.inputs || []).map((i) => ({
842
- name: i.name,
843
- type: i.type || 'text',
844
- required: i.required !== false,
845
- ...(i.description ? { description: i.description } : {}),
846
- ...(i.default_value != null ? { default_value: i.default_value } : {}),
847
- })),
848
- phases: [{
849
- id: crypto.randomUUID(),
850
- name: raw.name || 'Main',
851
- description: raw.description || '',
852
- deliverables: [],
853
- steps: joeSteps.map((s, i) => ({
854
- id: idMap.get(s.id) || crypto.randomUUID(),
855
- name: s.name,
856
- description: s.description || '',
857
- step_type: s.executionMode === 'script' ? 'script' : (s.agent ? 'agent' : 'human'),
858
- role: s.role || s.agent || 'unassigned',
859
- inputs: (s.inputs || []).map((inp) => ({
860
- name: inp.name,
861
- ...(inp.source_step_id ? { source_step_id: idMap.get(inp.source_step_id) || inp.source_step_id, source_output_name: inp.source_output_name } : {}),
862
- ...(inp.pipeline_input ? { pipeline_input: inp.pipeline_input } : {}),
863
- ...(inp.description ? { description: inp.description } : {}),
864
- required: inp.required !== false,
865
- })),
866
- outputs: (s.outputs || []).map((o) => ({
867
- name: o.name,
868
- type: o.type || 'text',
869
- ...(o.description ? { description: o.description } : {}),
870
- required: o.required !== false,
871
- ...(o.format ? { format: o.format } : {}),
872
- })),
873
- sections: s.sections || '',
874
- reviewer: s.reviewer || null,
875
- max_review_cycles: s.maxReviewCycles || 0,
876
- execution_config: s.executionMode === 'script' ? {
877
- image: s.image || '',
878
- command: s.command || '',
879
- } : null,
880
- parallel_group: null,
881
- sort_order: i,
882
- })),
883
- }],
884
- };
885
- const body = {
886
- name: raw.name || file,
887
- description: raw.description || '',
888
- definition,
1836
+ const data = await api('GET', `/api/pipeline-templates/${templateId}`);
1837
+ const t = data.template;
1838
+ requireDraftStatus(t);
1839
+ const phase = findPhaseById(t.definition, opts.phase);
1840
+ if (!phase) {
1841
+ console.error(chalk.red(`Phase "${opts.phase}" not found`));
1842
+ process.exit(1);
1843
+ }
1844
+ if (opts.name) {
1845
+ if (t.definition.phases.find((p) => p.id !== opts.phase && p.name === opts.name)) {
1846
+ console.error(chalk.red(`Phase name "${opts.name}" already exists`));
1847
+ process.exit(1);
1848
+ }
1849
+ phase.name = opts.name;
1850
+ }
1851
+ if (opts.description != null)
1852
+ phase.description = opts.description;
1853
+ await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
1854
+ console.log(chalk.green(`Updated phase: ${phase.name}`));
1855
+ }
1856
+ catch (err) {
1857
+ console.error(chalk.red(err.message));
1858
+ process.exit(1);
1859
+ }
1860
+ });
1861
+ templates
1862
+ .command('add-step')
1863
+ .description('Add a step to a phase. Template must be in draft status. Step names must be unique within their phase. Returns the generated step ID needed by I/O commands (add-output, add-input, etc.). Use "tpl show" or "tpl steps" to find existing step IDs.')
1864
+ .argument('<templateId>', 'Template ID')
1865
+ .requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" or "tpl add-phase" output)')
1866
+ .requiredOption('--name <name>', 'Step name (must be unique within the phase)')
1867
+ .option('--type <type>', 'Step type: human (manual task), agent (AI agent), script (automated), gate (approval checkpoint)', 'human')
1868
+ .option('--role <role>', 'Role or agent name (e.g., "kate", "marco", "dev-team")')
1869
+ .option('--description <desc>', 'Step description')
1870
+ .option('--reviewer <reviewer>', 'Reviewer role for review steps')
1871
+ .option('--max-review-cycles <n>', 'Max review cycles before auto-approval', '0')
1872
+ .option('--parallel-group <group>', 'Group name for parallel execution (steps in the same group run concurrently)')
1873
+ .action(async (templateId, opts) => {
1874
+ try {
1875
+ if (!VALID_STEP_TYPES.includes(opts.type)) {
1876
+ console.error(chalk.red(`Invalid step type "${opts.type}". Valid: ${VALID_STEP_TYPES.join(', ')}`));
1877
+ process.exit(1);
1878
+ }
1879
+ const data = await api('GET', `/api/pipeline-templates/${templateId}`);
1880
+ const t = data.template;
1881
+ requireDraftStatus(t);
1882
+ const phase = findPhaseById(t.definition, opts.phase);
1883
+ if (!phase) {
1884
+ console.error(chalk.red(`Phase "${opts.phase}" not found`));
1885
+ process.exit(1);
1886
+ }
1887
+ if (!phase.steps)
1888
+ phase.steps = [];
1889
+ if (phase.steps.find((s) => s.name === opts.name)) {
1890
+ console.error(chalk.red(`Step "${opts.name}" already exists in phase "${phase.name}"`));
1891
+ process.exit(1);
1892
+ }
1893
+ const step = {
1894
+ id: crypto.randomUUID(),
1895
+ name: opts.name,
1896
+ description: opts.description || '',
1897
+ step_type: opts.type,
1898
+ role: opts.role || 'unassigned',
1899
+ inputs: [],
1900
+ outputs: [],
1901
+ sections: '',
1902
+ reviewer: opts.reviewer || null,
1903
+ max_review_cycles: parseInt(opts.maxReviewCycles) || 0,
1904
+ execution_config: null,
1905
+ parallel_group: opts.parallelGroup || null,
1906
+ sort_order: phase.steps.length,
889
1907
  };
890
- const data = await api('POST', '/api/pipeline-templates', body);
1908
+ phase.steps.push(step);
1909
+ await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
1910
+ console.log(chalk.green(`Added step: ${opts.name} (${opts.type}) to phase ${phase.name}`));
1911
+ console.log(` ID: ${step.id}`);
1912
+ }
1913
+ catch (err) {
1914
+ console.error(chalk.red(err.message));
1915
+ process.exit(1);
1916
+ }
1917
+ });
1918
+ templates
1919
+ .command('remove-step')
1920
+ .description('Remove a step from a template. Blocked if other steps reference this step\'s outputs as inputs unless --force is used. Template must be in draft status. Use "tpl steps" to find step IDs.')
1921
+ .argument('<templateId>', 'Template ID')
1922
+ .requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
1923
+ .option('--force', 'Remove even if other steps reference this step\'s outputs')
1924
+ .action(async (templateId, opts) => {
1925
+ try {
1926
+ const data = await api('GET', `/api/pipeline-templates/${templateId}`);
891
1927
  const t = data.template;
892
- console.log(chalk.green(`Imported: ${t.name} (${joeSteps.length} steps)`));
893
- console.log(` ID: ${t.id}`);
1928
+ requireDraftStatus(t);
1929
+ // Find the step and its phase
1930
+ let targetPhase = null;
1931
+ let stepIdx = -1;
1932
+ for (const phase of t.definition.phases || []) {
1933
+ const idx = (phase.steps || []).findIndex((s) => s.id === opts.step);
1934
+ if (idx !== -1) {
1935
+ targetPhase = phase;
1936
+ stepIdx = idx;
1937
+ break;
1938
+ }
1939
+ }
1940
+ if (!targetPhase) {
1941
+ console.error(chalk.red(`Step "${opts.step}" not found`));
1942
+ process.exit(1);
1943
+ }
1944
+ const step = targetPhase.steps[stepIdx];
1945
+ // Check for references from other steps
1946
+ const refs = allSteps(t.definition).filter((s) => s.id !== step.id && (s.inputs || []).some((inp) => inp.source_step_id === step.id));
1947
+ if (refs.length && !opts.force) {
1948
+ console.error(chalk.yellow(`Step "${step.name}" is referenced by: ${refs.map((r) => r.name).join(', ')}`));
1949
+ console.error(chalk.yellow('Use --force to remove anyway'));
1950
+ process.exit(1);
1951
+ }
1952
+ targetPhase.steps.splice(stepIdx, 1);
1953
+ await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
1954
+ console.log(chalk.green(`Removed step: ${step.name}`));
894
1955
  }
895
1956
  catch (err) {
896
1957
  console.error(chalk.red(err.message));
@@ -898,13 +1959,64 @@ templates
898
1959
  }
899
1960
  });
900
1961
  templates
901
- .command('activate')
902
- .description('Activate a template (required before creating instances)')
903
- .argument('<id>', 'Template ID')
904
- .action(async (id) => {
1962
+ .command('edit-step')
1963
+ .description('Edit step properties. Template must be in draft status. Provide at least one field to update. Use "tpl steps" to find step IDs.')
1964
+ .argument('<templateId>', 'Template ID')
1965
+ .requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
1966
+ .option('--name <name>', 'New step name (must be unique within the phase)')
1967
+ .option('--type <type>', 'Step type: human, agent, script, gate')
1968
+ .option('--role <role>', 'Role or agent name')
1969
+ .option('--description <desc>', 'Description')
1970
+ .option('--reviewer <reviewer>', 'Reviewer role (empty string to clear)')
1971
+ .option('--max-review-cycles <n>', 'Max review cycles')
1972
+ .option('--parallel-group <group>', 'Parallel execution group (empty string to clear)')
1973
+ .action(async (templateId, opts) => {
905
1974
  try {
906
- await api('PATCH', `/api/pipeline-templates/${id}/status`, { status: 'active' });
907
- console.log(chalk.green('Template activated'));
1975
+ const hasUpdate = opts.name || opts.type || opts.role || opts.description != null
1976
+ || opts.reviewer != null || opts.maxReviewCycles != null || opts.parallelGroup != null;
1977
+ if (!hasUpdate) {
1978
+ console.error(chalk.red('Provide at least one field to update'));
1979
+ process.exit(1);
1980
+ }
1981
+ if (opts.type && !VALID_STEP_TYPES.includes(opts.type)) {
1982
+ console.error(chalk.red(`Invalid step type "${opts.type}". Valid: ${VALID_STEP_TYPES.join(', ')}`));
1983
+ process.exit(1);
1984
+ }
1985
+ const data = await api('GET', `/api/pipeline-templates/${templateId}`);
1986
+ const t = data.template;
1987
+ requireDraftStatus(t);
1988
+ const step = findStepById(t.definition, opts.step);
1989
+ if (!step) {
1990
+ console.error(chalk.red(`Step "${opts.step}" not found`));
1991
+ process.exit(1);
1992
+ }
1993
+ if (opts.name) {
1994
+ // Check uniqueness within the step's phase
1995
+ for (const phase of t.definition.phases || []) {
1996
+ if (phase.steps?.find((s) => s.id === step.id)) {
1997
+ if (phase.steps.find((s) => s.id !== step.id && s.name === opts.name)) {
1998
+ console.error(chalk.red(`Step name "${opts.name}" already exists in phase "${phase.name}"`));
1999
+ process.exit(1);
2000
+ }
2001
+ break;
2002
+ }
2003
+ }
2004
+ step.name = opts.name;
2005
+ }
2006
+ if (opts.type)
2007
+ step.step_type = opts.type;
2008
+ if (opts.role)
2009
+ step.role = opts.role;
2010
+ if (opts.description != null)
2011
+ step.description = opts.description;
2012
+ if (opts.reviewer != null)
2013
+ step.reviewer = opts.reviewer || null;
2014
+ if (opts.maxReviewCycles != null)
2015
+ step.max_review_cycles = parseInt(opts.maxReviewCycles) || 0;
2016
+ if (opts.parallelGroup != null)
2017
+ step.parallel_group = opts.parallelGroup || null;
2018
+ await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
2019
+ console.log(chalk.green(`Updated step: ${step.name}`));
908
2020
  }
909
2021
  catch (err) {
910
2022
  console.error(chalk.red(err.message));
@@ -914,7 +2026,7 @@ templates
914
2026
  // ── Template I/O commands ──────────────────────────────────
915
2027
  templates
916
2028
  .command('steps')
917
- .description('List step IDs and names in a template')
2029
+ .description('List step IDs and names in a template. Use this to discover step IDs needed by --step options in add-output, add-input, remove-step, edit-step, etc.')
918
2030
  .argument('<id>', 'Template ID')
919
2031
  .action(async (id) => {
920
2032
  try {
@@ -935,14 +2047,14 @@ templates
935
2047
  });
936
2048
  templates
937
2049
  .command('add-output')
938
- .description('Add an output declaration to a step')
2050
+ .description('Declare what a step produces. Outputs can be consumed by downstream steps via add-input. Template must be in draft status. Output names must be unique within the step.')
939
2051
  .argument('<templateId>', 'Template ID')
940
- .requiredOption('--step <stepId>', 'Step ID')
941
- .requiredOption('--name <name>', 'Output name')
942
- .requiredOption('--type <type>', `Output type: ${VALID_IO_TYPES.join(', ')}`)
943
- .option('--description <desc>', 'Description')
944
- .option('--optional', 'Mark as not required')
945
- .option('--format <format>', 'Format hint (file_path, commit_sha, semver, date, etc.)')
2052
+ .requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
2053
+ .requiredOption('--name <name>', 'Output name (unique within the step, e.g., "requirements", "metrics_data")')
2054
+ .requiredOption('--type <type>', 'Data type: text, markdown, number, boolean, url, json, object, array')
2055
+ .option('--description <desc>', 'Human-readable description of what this output contains')
2056
+ .option('--optional', 'Mark as not required (default: required)')
2057
+ .option('--format <format>', 'Format hint: file_path, commit_sha, semver, date, datetime, email, integer, percentage')
946
2058
  .action(async (templateId, opts) => {
947
2059
  try {
948
2060
  if (!VALID_IO_TYPES.includes(opts.type)) {
@@ -979,10 +2091,10 @@ templates
979
2091
  });
980
2092
  templates
981
2093
  .command('remove-output')
982
- .description('Remove an output declaration from a step')
2094
+ .description('Remove an output declaration from a step. Blocked if downstream steps consume this output via add-input unless --force is used. Template must be in draft status.')
983
2095
  .argument('<templateId>', 'Template ID')
984
- .requiredOption('--step <stepId>', 'Step ID')
985
- .requiredOption('--name <name>', 'Output name')
2096
+ .requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
2097
+ .requiredOption('--name <name>', 'Output name to remove')
986
2098
  .option('--force', 'Remove even if downstream steps reference this output')
987
2099
  .action(async (templateId, opts) => {
988
2100
  try {
@@ -1017,15 +2129,18 @@ templates
1017
2129
  });
1018
2130
  templates
1019
2131
  .command('add-input')
1020
- .description('Add an input declaration to a step')
2132
+ .description('Declare what a step consumes. Two modes (mutually exclusive):\n' +
2133
+ ' Step-sourced: --source-step + --source-output (consumes output from another step)\n' +
2134
+ ' Pipeline-sourced: --pipeline-input (consumes a template-level input)\n' +
2135
+ 'Validates that the source step/output/pipeline-input exists. Template must be in draft status.')
1021
2136
  .argument('<templateId>', 'Template ID')
1022
- .requiredOption('--step <stepId>', 'Step ID')
1023
- .requiredOption('--name <name>', 'Input name')
1024
- .option('--source-step <stepId>', 'Source step ID (for step-sourced inputs)')
1025
- .option('--source-output <name>', 'Source output name (for step-sourced inputs)')
1026
- .option('--pipeline-input <name>', 'Pipeline-level input name (for pipeline-sourced inputs)')
1027
- .option('--description <desc>', 'Description')
1028
- .option('--optional', 'Mark as not required')
2137
+ .requiredOption('--step <stepId>', 'Step ID receiving the input (from "tpl steps" output)')
2138
+ .requiredOption('--name <name>', 'Local input name (unique within the step)')
2139
+ .option('--source-step <stepId>', 'Upstream step ID that produces the data (use with --source-output)')
2140
+ .option('--source-output <name>', 'Output name on the source step (use with --source-step)')
2141
+ .option('--pipeline-input <name>', 'Pipeline-level input name (use instead of --source-step/--source-output)')
2142
+ .option('--description <desc>', 'What this data is used for')
2143
+ .option('--optional', 'Mark as not required (default: required)')
1029
2144
  .action(async (templateId, opts) => {
1030
2145
  try {
1031
2146
  const hasSrc = opts.sourceStep || opts.sourceOutput;
@@ -1106,10 +2221,10 @@ templates
1106
2221
  });
1107
2222
  templates
1108
2223
  .command('remove-input')
1109
- .description('Remove an input declaration from a step')
2224
+ .description('Remove an input declaration from a step. Template must be in draft status.')
1110
2225
  .argument('<templateId>', 'Template ID')
1111
- .requiredOption('--step <stepId>', 'Step ID')
1112
- .requiredOption('--name <name>', 'Input name')
2226
+ .requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
2227
+ .requiredOption('--name <name>', 'Input name to remove')
1113
2228
  .action(async (templateId, opts) => {
1114
2229
  try {
1115
2230
  const data = await api('GET', `/api/pipeline-templates/${templateId}`);
@@ -1136,12 +2251,12 @@ templates
1136
2251
  });
1137
2252
  templates
1138
2253
  .command('add-pipeline-input')
1139
- .description('Add a pipeline-level input to the template')
2254
+ .description('Add a template-level input that is provided when creating a pipeline instance. Steps can consume these via "tpl add-input --pipeline-input <name>". Template must be in draft status.')
1140
2255
  .argument('<templateId>', 'Template ID')
1141
- .requiredOption('--name <name>', 'Input name')
1142
- .requiredOption('--type <type>', `Input type: ${VALID_IO_TYPES.join(', ')}`)
1143
- .option('--description <desc>', 'Description')
1144
- .option('--optional', 'Not required at instance creation')
2256
+ .requiredOption('--name <name>', 'Input name (unique within the template)')
2257
+ .requiredOption('--type <type>', 'Data type: text, markdown, number, boolean, url, json, object, array')
2258
+ .option('--description <desc>', 'Human-readable description')
2259
+ .option('--optional', 'Not required at instance creation (default: required)')
1145
2260
  .option('--default <value>', 'Default value')
1146
2261
  .action(async (templateId, opts) => {
1147
2262
  try {
@@ -1174,9 +2289,9 @@ templates
1174
2289
  });
1175
2290
  templates
1176
2291
  .command('remove-pipeline-input')
1177
- .description('Remove a pipeline-level input from the template')
2292
+ .description('Remove a template-level input. Blocked if steps reference this input via --pipeline-input unless --force is used. Template must be in draft status.')
1178
2293
  .argument('<templateId>', 'Template ID')
1179
- .requiredOption('--name <name>', 'Input name')
2294
+ .requiredOption('--name <name>', 'Pipeline input name to remove')
1180
2295
  .option('--force', 'Remove even if steps reference this input')
1181
2296
  .action(async (templateId, opts) => {
1182
2297
  try {
@@ -1207,7 +2322,7 @@ templates
1207
2322
  });
1208
2323
  templates
1209
2324
  .command('validate')
1210
- .description('Validate template I/O contracts')
2325
+ .description('Validate template I/O contracts. Checks for: circular dependencies between steps, missing source step/output references, self-references, duplicate input names per step. Also reports warnings for unreferenced outputs and info for steps with no outputs declared. Run before activating to catch errors early.')
1211
2326
  .argument('<id>', 'Template ID')
1212
2327
  .action(async (id) => {
1213
2328
  try {
@@ -1639,6 +2754,50 @@ step
1639
2754
  process.exit(1);
1640
2755
  }
1641
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
+ });
1642
2801
  step
1643
2802
  .command('skip')
1644
2803
  .description('Skip a step')
@@ -2043,6 +3202,82 @@ executions
2043
3202
  process.exit(1);
2044
3203
  }
2045
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
+ });
2046
3281
  executions
2047
3282
  .command('cloudwatch-logs')
2048
3283
  .alias('cw')