@chief-clancy/plan 0.2.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/README.md +37 -1
- package/bin/plan.js +5 -2
- package/dist/installer/install.d.ts.map +1 -1
- package/dist/installer/install.js +6 -2
- package/dist/installer/install.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/approve-plan.md +23 -0
- package/src/commands/commands.test.ts +1 -1
- package/src/workflows/approve-plan.md +1199 -0
- package/src/workflows/workflows.test.ts +300 -1
|
@@ -11,7 +11,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
11
11
|
|
|
12
12
|
const WORKFLOWS_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
13
13
|
|
|
14
|
-
const EXPECTED_WORKFLOWS = ['board-setup.md', 'plan.md'];
|
|
14
|
+
const EXPECTED_WORKFLOWS = ['approve-plan.md', 'board-setup.md', 'plan.md'];
|
|
15
15
|
|
|
16
16
|
describe('workflows directory structure', () => {
|
|
17
17
|
it('contains exactly the expected workflow files', () => {
|
|
@@ -640,3 +640,302 @@ describe('plan inventory step', () => {
|
|
|
640
640
|
);
|
|
641
641
|
});
|
|
642
642
|
});
|
|
643
|
+
|
|
644
|
+
// ---------------------------------------------------------------------------
|
|
645
|
+
// approve-plan.md content assertions (PR 7b — standalone adaptation)
|
|
646
|
+
// ---------------------------------------------------------------------------
|
|
647
|
+
|
|
648
|
+
describe('approve-plan three-state preflight', () => {
|
|
649
|
+
const content = readFileSync(
|
|
650
|
+
new URL('approve-plan.md', import.meta.url),
|
|
651
|
+
'utf8',
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
it('Step 1 detects all three installation states', () => {
|
|
655
|
+
expect(content).toContain('standalone mode');
|
|
656
|
+
expect(content).toContain('standalone+board mode');
|
|
657
|
+
expect(content).toContain('terminal mode');
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('Step 1 checks .clancy/.env presence for state detection', () => {
|
|
661
|
+
expect(content).toContain('.clancy/.env');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('Step 1 checks clancy-implement.js for terminal detection', () => {
|
|
665
|
+
expect(content).toContain('clancy-implement.js');
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('Step 1 does not hard-stop on missing .clancy/.env', () => {
|
|
669
|
+
expect(content).not.toContain(
|
|
670
|
+
'.clancy/ not found. Run /clancy:init to set up Clancy first.',
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('CLANCY_ROLES check only runs in terminal mode', () => {
|
|
675
|
+
expect(content).toContain('Terminal-mode preflight');
|
|
676
|
+
expect(content).toContain(
|
|
677
|
+
'skip in standalone mode and standalone+board mode',
|
|
678
|
+
);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('standalone mode requires .clancy/plans/ to exist', () => {
|
|
682
|
+
expect(content).toContain('.clancy/plans/');
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
describe('approve-plan dual-mode resolver (Step 2)', () => {
|
|
687
|
+
const content = readFileSync(
|
|
688
|
+
new URL('approve-plan.md', import.meta.url),
|
|
689
|
+
'utf8',
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
it('Step 2 routes between plan-file stem and ticket key based on mode', () => {
|
|
693
|
+
expect(content).toContain('plan-file stem');
|
|
694
|
+
expect(content).toContain('ticket key');
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('standalone mode only accepts plan-file stems', () => {
|
|
698
|
+
expect(content).toContain(
|
|
699
|
+
'In standalone mode, the argument must be a plan-file stem',
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('standalone+board and terminal modes try plan-file lookup first', () => {
|
|
704
|
+
expect(content).toContain(
|
|
705
|
+
'Try plan-file lookup first (does `.clancy/plans/{arg}.md` exist?)',
|
|
706
|
+
);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('plan stem wins over ticket key on collision', () => {
|
|
710
|
+
expect(content).toContain('plan stem wins over ticket key');
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('standalone no-arg auto-selects oldest unapproved plan', () => {
|
|
714
|
+
expect(content).toContain('auto-select the oldest unapproved local plan');
|
|
715
|
+
expect(content).toContain('no sibling marker');
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('standalone no-arg filters scan to actual plan files', () => {
|
|
719
|
+
expect(content).toContain('## Clancy Implementation Plan');
|
|
720
|
+
expect(content).toContain('Filter to plan files only');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('standalone no-arg sorts files with missing/unparseable date last', () => {
|
|
724
|
+
expect(content).toContain('missing or unparseable `**Planned:**` date');
|
|
725
|
+
expect(content).toContain('sort **last**');
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('preserves existing terminal-mode no-arg progress.txt scan', () => {
|
|
729
|
+
expect(content).toContain('.clancy/progress.txt');
|
|
730
|
+
expect(content).toContain('| PLAN |');
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('errors clearly if standalone arg is not a plan-file stem', () => {
|
|
734
|
+
expect(content).toContain('Plan file not found:');
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('errors if standalone has no plans and no arg', () => {
|
|
738
|
+
expect(content).toContain('No local plans awaiting approval');
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
describe('approve-plan local marker (Step 4a)', () => {
|
|
743
|
+
const content = readFileSync(
|
|
744
|
+
new URL('approve-plan.md', import.meta.url),
|
|
745
|
+
'utf8',
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
it('defines Step 4a — Write local marker', () => {
|
|
749
|
+
expect(content).toContain('## Step 4a — Write local marker');
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('Step 4a runs only when the resolved arg was a plan-file stem', () => {
|
|
753
|
+
expect(content).toContain(
|
|
754
|
+
'Run this step instead of Steps 5, 5b, 6 when the resolved argument was a plan-file stem',
|
|
755
|
+
);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('writes marker to .clancy/plans/{stem}.approved', () => {
|
|
759
|
+
expect(content).toContain('.clancy/plans/{stem}.approved');
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('uses race-safe exclusive create (O_EXCL / wx)', () => {
|
|
763
|
+
expect(content).toContain('exclusive create');
|
|
764
|
+
expect(content).toContain('O_EXCL');
|
|
765
|
+
expect(content).toContain('wx');
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('marker body contains sha256 and approved_at fields', () => {
|
|
769
|
+
expect(content).toContain('sha256=');
|
|
770
|
+
expect(content).toContain('approved_at=');
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('sha256 is computed over the plan file content at approval time', () => {
|
|
774
|
+
expect(content).toContain('SHA-256');
|
|
775
|
+
expect(content).toContain('Order of operations');
|
|
776
|
+
expect(content).toContain(
|
|
777
|
+
'Read the plan file at `.clancy/plans/{stem}.md` from disk',
|
|
778
|
+
);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('SHA hash is never computed over the .approved marker itself', () => {
|
|
782
|
+
expect(content).toContain('`.approved` file is **never** included');
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('approved_at is ISO 8601 UTC', () => {
|
|
786
|
+
expect(content).toContain('ISO 8601');
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('handles EEXIST as already-approved', () => {
|
|
790
|
+
expect(content).toContain('EEXIST');
|
|
791
|
+
expect(content).toContain('already approved');
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('explains the marker is the gate for /clancy:implement-from', () => {
|
|
795
|
+
expect(content).toContain('/clancy:implement-from');
|
|
796
|
+
expect(content).toContain('gate');
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('after writing the marker, Step 4a jumps to Step 7 (log) and skips board flow', () => {
|
|
800
|
+
expect(content).toContain('jump to Step 7');
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
describe('approve-plan brief-marker update (Step 4b)', () => {
|
|
805
|
+
const content = readFileSync(
|
|
806
|
+
new URL('approve-plan.md', import.meta.url),
|
|
807
|
+
'utf8',
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
it('defines Step 4b — Update brief marker', () => {
|
|
811
|
+
expect(content).toContain('## Step 4b — Update brief marker');
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('resolves brief filename from the plan **Brief:** header', () => {
|
|
815
|
+
expect(content).toContain('**Brief:**');
|
|
816
|
+
expect(content).toContain('extract the `**Brief:**` header line');
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('resolves row number from the plan **Row:** header', () => {
|
|
820
|
+
expect(content).toContain('**Row:**');
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('uses a tolerant line-anchored regex with optional approved prefix', () => {
|
|
824
|
+
expect(content).toContain(
|
|
825
|
+
'^<!--\\s*(?:approved:([\\d,]*)\\s+)?planned:([\\d,]+)\\s*-->\\s*$',
|
|
826
|
+
);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('canonical ordering puts approved before planned', () => {
|
|
830
|
+
expect(content).toContain('approved:` first, `planned:` second');
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('handles existing planned-only marker (PR 6b state)', () => {
|
|
834
|
+
expect(content).toContain('<!-- planned:1,2,3 -->');
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('handles existing approved+planned marker', () => {
|
|
838
|
+
expect(content).toContain('<!-- approved:1 planned:1,2,3 -->');
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('handles unspaced marker variant', () => {
|
|
842
|
+
expect(content).toContain('<!--planned:1,2,3-->');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('best-effort: failure does not roll back the .approved marker', () => {
|
|
846
|
+
expect(content).toContain('does NOT roll back');
|
|
847
|
+
expect(content).toContain('local marker is the source of truth');
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('warns and skips when **Brief:** header is absent', () => {
|
|
851
|
+
expect(content).toContain('cannot update brief marker');
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it('documents concurrency-not-safe nature of the read-modify-write', () => {
|
|
855
|
+
expect(content).toContain('not concurrency-safe');
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('documents reversed-order brief markers fall through to warn-and-skip', () => {
|
|
859
|
+
expect(content).toContain('Reversed-order markers');
|
|
860
|
+
expect(content).toContain('do NOT match this regex');
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it('documents code-fence false-positive risk', () => {
|
|
864
|
+
expect(content).toContain('Code-fence false positives');
|
|
865
|
+
expect(content).toContain('first match wins');
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
describe('approve-plan local-mode log + summary (Step 7)', () => {
|
|
870
|
+
const content = readFileSync(
|
|
871
|
+
new URL('approve-plan.md', import.meta.url),
|
|
872
|
+
'utf8',
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
it('uses LOCAL_APPROVE_PLAN log token for plan-file stem mode', () => {
|
|
876
|
+
expect(content).toContain('LOCAL_APPROVE_PLAN');
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it('local log entry includes the sha256 prefix for audit', () => {
|
|
880
|
+
expect(content).toContain('sha256={first 12 hex}');
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('local success summary points to /clancy:implement-from', () => {
|
|
884
|
+
expect(content).toContain(
|
|
885
|
+
'Next: /clancy:implement-from .clancy/plans/{stem}.md',
|
|
886
|
+
);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('preserves board-mode APPROVE_PLAN log entry', () => {
|
|
890
|
+
expect(content).toContain('| {KEY} | APPROVE_PLAN | —');
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it('Step 7 has an explicit mode gate so local and board branches do not double-render', () => {
|
|
894
|
+
expect(content).toContain('Mode gate (read first)');
|
|
895
|
+
expect(content).toContain(
|
|
896
|
+
'Do NOT render both — exactly one branch executes per approval',
|
|
897
|
+
);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('plan-file-not-found error hints at the row-number convention', () => {
|
|
901
|
+
expect(content).toContain('Plan stems include the row number');
|
|
902
|
+
expect(content).toContain('Run /clancy:plan --list');
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('EEXIST advice points at manual deletion, not a non-existent --fresh flag', () => {
|
|
906
|
+
expect(content).toContain('Delete .clancy/plans/{stem}.approved manually');
|
|
907
|
+
expect(content).not.toContain('/clancy:approve-plan --fresh');
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
describe('approve-plan board mode preserved unchanged', () => {
|
|
912
|
+
const content = readFileSync(
|
|
913
|
+
new URL('approve-plan.md', import.meta.url),
|
|
914
|
+
'utf8',
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
it('keeps Jira ADF construction', () => {
|
|
918
|
+
expect(content).toContain('ADF');
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('keeps GitHub PATCH /issues for description update', () => {
|
|
922
|
+
expect(content).toContain('PATCH');
|
|
923
|
+
expect(content).toContain('/issues/');
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('keeps Linear issueUpdate mutation', () => {
|
|
927
|
+
expect(content).toContain('issueUpdate');
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it('keeps Azure DevOps work item update', () => {
|
|
931
|
+
expect(content).toContain('Azure DevOps');
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('keeps Shortcut PUT /stories', () => {
|
|
935
|
+
expect(content).toContain('Shortcut');
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('keeps Notion description update', () => {
|
|
939
|
+
expect(content).toContain('Notion');
|
|
940
|
+
});
|
|
941
|
+
});
|