@pennyfarthing/core 11.2.2 → 11.3.2

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.
Files changed (168) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/packages/core/dist/cli/commands/doctor-legacy.test.js +2 -2
  4. package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
  5. package/packages/core/dist/cli/commands/doctor.d.ts +63 -0
  6. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/packages/core/dist/cli/commands/doctor.js +280 -43
  8. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  9. package/packages/core/dist/cli/commands/init.d.ts +12 -0
  10. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  11. package/packages/core/dist/cli/commands/init.js +45 -0
  12. package/packages/core/dist/cli/commands/init.js.map +1 -1
  13. package/packages/core/dist/cli/commands/pyproject-install.test.d.ts +19 -0
  14. package/packages/core/dist/cli/commands/pyproject-install.test.d.ts.map +1 -0
  15. package/packages/core/dist/cli/commands/pyproject-install.test.js +261 -0
  16. package/packages/core/dist/cli/commands/pyproject-install.test.js.map +1 -0
  17. package/packages/core/dist/cli/commands/update-consolidation.test.js +14 -6
  18. package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
  19. package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
  20. package/packages/core/dist/cli/commands/update.js +5 -1
  21. package/packages/core/dist/cli/commands/update.js.map +1 -1
  22. package/packages/core/dist/cli/index.js +2 -0
  23. package/packages/core/dist/cli/index.js.map +1 -1
  24. package/packages/core/dist/cli/utils/python.d.ts +1 -0
  25. package/packages/core/dist/cli/utils/python.d.ts.map +1 -1
  26. package/packages/core/dist/cli/utils/python.js +22 -1
  27. package/packages/core/dist/cli/utils/python.js.map +1 -1
  28. package/packages/core/dist/cli/utils/settings-hook-migration.test.d.ts +17 -0
  29. package/packages/core/dist/cli/utils/settings-hook-migration.test.d.ts.map +1 -0
  30. package/packages/core/dist/cli/utils/settings-hook-migration.test.js +382 -0
  31. package/packages/core/dist/cli/utils/settings-hook-migration.test.js.map +1 -0
  32. package/packages/core/dist/cli/utils/settings-pf-wrapper.test.d.ts +16 -0
  33. package/packages/core/dist/cli/utils/settings-pf-wrapper.test.d.ts.map +1 -0
  34. package/packages/core/dist/cli/utils/settings-pf-wrapper.test.js +377 -0
  35. package/packages/core/dist/cli/utils/settings-pf-wrapper.test.js.map +1 -0
  36. package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
  37. package/packages/core/dist/cli/utils/settings.js +15 -2
  38. package/packages/core/dist/cli/utils/settings.js.map +1 -1
  39. package/packages/core/dist/server/paths.d.ts.map +1 -1
  40. package/packages/core/dist/server/paths.js +6 -0
  41. package/packages/core/dist/server/paths.js.map +1 -1
  42. package/packages/core/dist/server/settings.d.ts.map +1 -1
  43. package/packages/core/dist/server/settings.js +5 -0
  44. package/packages/core/dist/server/settings.js.map +1 -1
  45. package/packages/core/dist/workflow/tandem-workflow-templates.test.js +7 -5
  46. package/packages/core/dist/workflow/tandem-workflow-templates.test.js.map +1 -1
  47. package/packages/core/dist/workflow/workflow-graph-validation.d.ts +65 -0
  48. package/packages/core/dist/workflow/workflow-graph-validation.d.ts.map +1 -0
  49. package/packages/core/dist/workflow/workflow-graph-validation.js +190 -0
  50. package/packages/core/dist/workflow/workflow-graph-validation.js.map +1 -0
  51. package/packages/core/dist/workflow/workflow-graph-validation.test.d.ts +18 -0
  52. package/packages/core/dist/workflow/workflow-graph-validation.test.d.ts.map +1 -0
  53. package/packages/core/dist/workflow/workflow-graph-validation.test.js +706 -0
  54. package/packages/core/dist/workflow/workflow-graph-validation.test.js.map +1 -0
  55. package/packages/core/dist/workflow/workflow-migration.test.js +6 -5
  56. package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
  57. package/pennyfarthing-dist/agents/dev.md +4 -2
  58. package/pennyfarthing-dist/agents/devops.md +2 -10
  59. package/pennyfarthing-dist/agents/reviewer-preflight.md +4 -5
  60. package/pennyfarthing-dist/agents/sm.md +4 -17
  61. package/pennyfarthing-dist/commands/pf-health-check.md +30 -11
  62. package/pennyfarthing-dist/gates/{confidence-sm.md → confidence.md} +16 -17
  63. package/pennyfarthing-dist/gates/dev-exit.md +75 -0
  64. package/pennyfarthing-dist/gates/merge-ready.md +49 -0
  65. package/pennyfarthing-dist/gates/release-ready.md +95 -0
  66. package/pennyfarthing-dist/gates/reviewer-preflight-check.md +90 -0
  67. package/pennyfarthing-dist/gates/sm-setup-exit.md +82 -0
  68. package/pennyfarthing-dist/guides/agent-behavior.md +88 -30
  69. package/pennyfarthing-dist/guides/gates.md +7 -2
  70. package/pennyfarthing-dist/scripts/lib/find-root.sh +5 -0
  71. package/pennyfarthing-dist/scripts/lib/run-pf.sh +7 -0
  72. package/pennyfarthing-dist/skills/pf-settings/skill.md +42 -0
  73. package/pennyfarthing-dist/skills/skill-registry.yaml +15 -0
  74. package/pennyfarthing-dist/templates/pyproject.toml +27 -0
  75. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +7 -3
  76. package/pennyfarthing-dist/workflows/bdd.yaml +7 -3
  77. package/pennyfarthing-dist/workflows/installation-check/steps/step-01-foundation.md +77 -0
  78. package/pennyfarthing-dist/workflows/installation-check/steps/step-02-commands.md +82 -0
  79. package/pennyfarthing-dist/workflows/installation-check/steps/step-03-hooks.md +121 -0
  80. package/pennyfarthing-dist/workflows/installation-check/steps/step-04-scripts.md +83 -0
  81. package/pennyfarthing-dist/workflows/installation-check/steps/step-05-layout.md +81 -0
  82. package/pennyfarthing-dist/workflows/installation-check/steps/step-06-legacy.md +94 -0
  83. package/pennyfarthing-dist/workflows/installation-check/steps/step-07-tools.md +80 -0
  84. package/pennyfarthing-dist/workflows/installation-check/steps/step-08-summary.md +99 -0
  85. package/pennyfarthing-dist/workflows/installation-check/workflow.yaml +47 -0
  86. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +7 -3
  87. package/pennyfarthing-dist/workflows/tdd.yaml +7 -3
  88. package/pennyfarthing-dist/workflows/trivial.yaml +7 -3
  89. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  91. package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
  92. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  93. package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
  94. package/pennyfarthing_scripts/bc/__pycache__/split.cpython-314.pyc +0 -0
  95. package/pennyfarthing_scripts/bc/cli.py +21 -0
  96. package/pennyfarthing_scripts/bc/focus.py +1 -0
  97. package/pennyfarthing_scripts/bc/split.py +52 -0
  98. package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_panel.cpython-314.pyc +0 -0
  99. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  100. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  101. package/pennyfarthing_scripts/bikerack/__pycache__/context_meter_footer.cpython-314.pyc +0 -0
  102. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  103. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  104. package/pennyfarthing_scripts/bikerack/__pycache__/events.cpython-314.pyc +0 -0
  105. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  106. package/pennyfarthing_scripts/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
  107. package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
  108. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  109. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  110. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  111. package/pennyfarthing_scripts/bikerack/context_meter_footer.py +53 -3
  112. package/pennyfarthing_scripts/bikerack/tui.py +202 -8
  113. package/pennyfarthing_scripts/bmad/__init__.py +1 -0
  114. package/pennyfarthing_scripts/bmad/__pycache__/__init__.cpython-314.pyc +0 -0
  115. package/pennyfarthing_scripts/bmad/__pycache__/cli.cpython-314.pyc +0 -0
  116. package/pennyfarthing_scripts/bmad/__pycache__/parser.cpython-314.pyc +0 -0
  117. package/pennyfarthing_scripts/bmad/__pycache__/sync.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/bmad/__pycache__/test_parser.cpython-314-pytest-9.0.2.pyc +0 -0
  119. package/pennyfarthing_scripts/bmad/__pycache__/test_sync.cpython-314-pytest-9.0.2.pyc +0 -0
  120. package/pennyfarthing_scripts/bmad/cli.py +197 -0
  121. package/pennyfarthing_scripts/bmad/importer.py +200 -0
  122. package/pennyfarthing_scripts/bmad/parser.py +233 -0
  123. package/pennyfarthing_scripts/bmad/sync.py +464 -0
  124. package/pennyfarthing_scripts/bmad/test_parser.py +253 -0
  125. package/pennyfarthing_scripts/bmad/test_sync.py +223 -0
  126. package/pennyfarthing_scripts/cli.py +10 -0
  127. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  128. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  130. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
  133. package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  135. package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  136. package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
  137. package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
  138. package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
  141. package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
  142. package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
  143. package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  145. package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
  146. package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
  147. package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
  148. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  149. package/pennyfarthing_scripts/settings/__init__.py +0 -0
  150. package/pennyfarthing_scripts/settings/__pycache__/__init__.cpython-314.pyc +0 -0
  151. package/pennyfarthing_scripts/settings/__pycache__/cli.cpython-314.pyc +0 -0
  152. package/pennyfarthing_scripts/settings/__pycache__/settings.cpython-314.pyc +0 -0
  153. package/pennyfarthing_scripts/settings/cli.py +55 -0
  154. package/pennyfarthing_scripts/settings/settings.py +98 -0
  155. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  156. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  157. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  158. package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_evaluation.cpython-314-pytest-9.0.2.pyc +0 -0
  159. package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_gate.cpython-314-pytest-9.0.2.pyc +0 -0
  160. package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
  161. package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
  162. package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
  163. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_list_team.cpython-314.pyc +0 -0
  164. package/pennyfarthing_scripts/tests/test_confidence_sm_gate.py +17 -16
  165. package/pennyfarthing_scripts/tests/test_resolve_gate_file_field.py +45 -47
  166. package/pennyfarthing_scripts/tests/test_workflow_list_team.py +0 -4
  167. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
@@ -0,0 +1,706 @@
1
+ /**
2
+ * Tests for Story 91-14: Workflow Graph Validation
3
+ *
4
+ * These tests validate the semantic/graph-level checks for workflow definitions,
5
+ * going beyond the structural schema validation in workflow-schema.ts.
6
+ *
7
+ * Graph validation covers:
8
+ * - Phase reachability (no orphan/unreachable phases)
9
+ * - Duplicate phase name detection
10
+ * - Agent reference validation (phase agents, tandem partners against known agents)
11
+ * - Gate file existence (gate.file references resolve to known gate files)
12
+ * - Data flow validation (input references match prior output declarations)
13
+ * - Collaboration validation (tandem/team agent consistency)
14
+ *
15
+ * Run with: node --test dist/workflow/workflow-graph-validation.test.js
16
+ */
17
+ import { describe, it } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import { validateWorkflowGraph, } from './workflow-graph-validation.js';
20
+ import { VALID_AGENT_NAMES } from './workflow-schema.js';
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+ /** Minimal valid context with all known gate files */
25
+ function defaultContext(overrides) {
26
+ return {
27
+ knownGateFiles: [
28
+ 'gates/sm-setup-exit',
29
+ 'gates/tests-fail',
30
+ 'gates/tests-pass',
31
+ 'gates/dev-exit',
32
+ 'gates/quality-pass',
33
+ 'gates/approval',
34
+ 'gates/design-review',
35
+ 'gates/merge-ready',
36
+ 'gates/release-ready',
37
+ 'gates/reviewer-preflight-check',
38
+ 'gates/confidence',
39
+ 'gates/context-ok',
40
+ ],
41
+ ...overrides,
42
+ };
43
+ }
44
+ /** Build a minimal valid phased workflow for graph testing */
45
+ function minimalWorkflow(overrides) {
46
+ return {
47
+ name: 'test-workflow',
48
+ phases: [
49
+ { name: 'setup', agent: 'sm' },
50
+ { name: 'implement', agent: 'dev' },
51
+ { name: 'review', agent: 'reviewer' },
52
+ ],
53
+ ...overrides,
54
+ };
55
+ }
56
+ /** TDD workflow matching the real tdd.yaml */
57
+ function tddWorkflow() {
58
+ return {
59
+ name: 'tdd',
60
+ phases: [
61
+ {
62
+ name: 'setup',
63
+ agent: 'sm',
64
+ output: ['session_file', 'branches', 'story_context'],
65
+ gate: { file: 'gates/sm-setup-exit', type: 'sm_setup_exit' },
66
+ },
67
+ {
68
+ name: 'red',
69
+ agent: 'tea',
70
+ input: ['session_file', 'story_context'],
71
+ output: ['failing_tests'],
72
+ gate: { file: 'gates/tests-fail', type: 'tests_fail' },
73
+ },
74
+ {
75
+ name: 'green',
76
+ agent: 'dev',
77
+ input: ['failing_tests', 'story_context'],
78
+ output: ['implementation', 'passing_tests'],
79
+ gate: { file: 'gates/dev-exit', type: 'dev_exit' },
80
+ },
81
+ {
82
+ name: 'verify',
83
+ agent: 'tea',
84
+ input: ['implementation', 'passing_tests'],
85
+ output: ['quality_verified'],
86
+ gate: { file: 'gates/quality-pass', type: 'quality_pass' },
87
+ },
88
+ {
89
+ name: 'review',
90
+ agent: 'reviewer',
91
+ input: ['implementation', 'passing_tests', 'quality_verified'],
92
+ output: ['approval'],
93
+ gate: { file: 'gates/approval', type: 'approval' },
94
+ },
95
+ {
96
+ name: 'finish',
97
+ agent: 'sm',
98
+ input: ['approval'],
99
+ output: ['archived_session', 'story_summary'],
100
+ },
101
+ ],
102
+ };
103
+ }
104
+ /** Helper to collect error messages from result */
105
+ function errorMessages(result) {
106
+ return (result.errors ?? []).map(e => e.message);
107
+ }
108
+ /** Helper to collect warning messages from result */
109
+ function warningMessages(result) {
110
+ return (result.warnings ?? []).map(w => w.message);
111
+ }
112
+ // ===========================================================================
113
+ // Tests
114
+ // ===========================================================================
115
+ describe('Workflow Graph Validation (91-14)', () => {
116
+ // -------------------------------------------------------------------------
117
+ // AC1: Phase reachability — no orphans, no unreachable states
118
+ // -------------------------------------------------------------------------
119
+ describe('Phase reachability', () => {
120
+ it('should accept a linear workflow where all phases are reachable', () => {
121
+ const wf = minimalWorkflow();
122
+ const result = validateWorkflowGraph(wf, defaultContext());
123
+ assert.strictEqual(result.valid, true, 'Linear workflow should be fully reachable');
124
+ assert.strictEqual((result.errors ?? []).length, 0);
125
+ });
126
+ it('should accept the full TDD workflow as reachable', () => {
127
+ const wf = tddWorkflow();
128
+ const result = validateWorkflowGraph(wf, defaultContext());
129
+ assert.strictEqual(result.valid, true, 'TDD workflow should be fully reachable');
130
+ });
131
+ it('should accept a single-phase workflow', () => {
132
+ const wf = minimalWorkflow({
133
+ phases: [{ name: 'work', agent: 'dev' }],
134
+ });
135
+ const result = validateWorkflowGraph(wf, defaultContext());
136
+ assert.strictEqual(result.valid, true, 'Single phase is trivially reachable');
137
+ });
138
+ it('should detect an unreachable phase when next: creates a bypass', () => {
139
+ // setup -> implement (skipped via next:review) -> review
140
+ const wf = minimalWorkflow({
141
+ phases: [
142
+ { name: 'setup', agent: 'sm', next: 'review' },
143
+ { name: 'implement', agent: 'dev' },
144
+ { name: 'review', agent: 'reviewer' },
145
+ ],
146
+ });
147
+ const result = validateWorkflowGraph(wf, defaultContext());
148
+ assert.strictEqual(result.valid, false, 'Unreachable phase should fail');
149
+ const msgs = errorMessages(result);
150
+ assert.ok(msgs.some(m => m.includes('implement') && m.toLowerCase().includes('unreachable')), `Expected unreachable error for 'implement', got: ${msgs.join('; ')}`);
151
+ });
152
+ it('should accept non-linear routing via next: when all phases remain reachable', () => {
153
+ // setup -> red, red -> green, green -> review (next:review skips verify), verify also reached via review -> verify
154
+ // Actually let's use a simpler case: a -> b -> c, a also has next:c but b is still reachable
155
+ // because the default sequential flow reaches b before next: kicks in
156
+ // Wait — need to think about this. If a has next:c, does b get visited?
157
+ //
158
+ // The convention: `next:` overrides the default sequential progression.
159
+ // If phase[0].next = 'c', then the flow goes 0 -> c, skipping b.
160
+ // So b IS unreachable.
161
+ //
162
+ // For all phases to remain reachable with next:, we need explicit next: chains:
163
+ // a (next:c) -> c (next:b) -> b
164
+ const wf = minimalWorkflow({
165
+ phases: [
166
+ { name: 'setup', agent: 'sm', next: 'review' },
167
+ { name: 'implement', agent: 'dev' },
168
+ { name: 'review', agent: 'reviewer', next: 'implement' },
169
+ ],
170
+ });
171
+ const result = validateWorkflowGraph(wf, defaultContext());
172
+ assert.strictEqual(result.valid, true, 'All phases reachable via next: chain');
173
+ });
174
+ it('should detect cycle that leaves trailing phases unreachable', () => {
175
+ // a -> b -> a (cycle), c is unreachable
176
+ const wf = minimalWorkflow({
177
+ phases: [
178
+ { name: 'a', agent: 'sm', next: 'b' },
179
+ { name: 'b', agent: 'dev', next: 'a' },
180
+ { name: 'c', agent: 'reviewer' },
181
+ ],
182
+ });
183
+ const result = validateWorkflowGraph(wf, defaultContext());
184
+ assert.strictEqual(result.valid, false);
185
+ const msgs = errorMessages(result);
186
+ assert.ok(msgs.some(m => m.includes('c') && m.toLowerCase().includes('unreachable')), `Expected unreachable error for 'c', got: ${msgs.join('; ')}`);
187
+ });
188
+ it('should handle workflows without phases (stepped) gracefully', () => {
189
+ const wf = {
190
+ name: 'stepped-wf',
191
+ type: 'stepped',
192
+ steps: { path: 'workflows/steps', pattern: 'step-{nn}-*.md' },
193
+ };
194
+ const result = validateWorkflowGraph(wf, defaultContext());
195
+ // Stepped workflows skip phase reachability (no phases to check)
196
+ assert.strictEqual(result.valid, true);
197
+ });
198
+ });
199
+ // -------------------------------------------------------------------------
200
+ // AC1 continued: Duplicate phase names
201
+ // -------------------------------------------------------------------------
202
+ describe('Duplicate phase names', () => {
203
+ it('should accept a workflow with unique phase names', () => {
204
+ const wf = tddWorkflow();
205
+ const result = validateWorkflowGraph(wf, defaultContext());
206
+ assert.strictEqual(result.valid, true);
207
+ });
208
+ it('should detect duplicate phase names', () => {
209
+ const wf = minimalWorkflow({
210
+ phases: [
211
+ { name: 'setup', agent: 'sm' },
212
+ { name: 'work', agent: 'dev' },
213
+ { name: 'setup', agent: 'reviewer' }, // duplicate
214
+ ],
215
+ });
216
+ const result = validateWorkflowGraph(wf, defaultContext());
217
+ assert.strictEqual(result.valid, false);
218
+ const msgs = errorMessages(result);
219
+ assert.ok(msgs.some(m => m.includes('setup') && m.toLowerCase().includes('duplicate')), `Expected duplicate phase name error, got: ${msgs.join('; ')}`);
220
+ });
221
+ it('should report all duplicates, not just the first', () => {
222
+ const wf = minimalWorkflow({
223
+ phases: [
224
+ { name: 'a', agent: 'sm' },
225
+ { name: 'b', agent: 'dev' },
226
+ { name: 'a', agent: 'tea' }, // dup of a
227
+ { name: 'b', agent: 'reviewer' }, // dup of b
228
+ ],
229
+ });
230
+ const result = validateWorkflowGraph(wf, defaultContext());
231
+ assert.strictEqual(result.valid, false);
232
+ const msgs = errorMessages(result);
233
+ assert.ok(msgs.some(m => m.includes("'a'")), 'Should report duplicate a');
234
+ assert.ok(msgs.some(m => m.includes("'b'")), 'Should report duplicate b');
235
+ });
236
+ });
237
+ // -------------------------------------------------------------------------
238
+ // AC2: Dangling agent references
239
+ // -------------------------------------------------------------------------
240
+ describe('Agent reference validation', () => {
241
+ it('should accept a workflow where all phase agents are known', () => {
242
+ const wf = tddWorkflow();
243
+ const result = validateWorkflowGraph(wf, defaultContext());
244
+ assert.strictEqual(result.valid, true);
245
+ });
246
+ it('should detect an unknown phase agent', () => {
247
+ const wf = minimalWorkflow({
248
+ phases: [
249
+ { name: 'setup', agent: 'sm' },
250
+ { name: 'work', agent: 'nonexistent-agent' },
251
+ ],
252
+ });
253
+ const result = validateWorkflowGraph(wf, defaultContext());
254
+ assert.strictEqual(result.valid, false);
255
+ const msgs = errorMessages(result);
256
+ assert.ok(msgs.some(m => m.includes('nonexistent-agent')), `Expected unknown agent error, got: ${msgs.join('; ')}`);
257
+ });
258
+ it('should detect an unknown tandem partner', () => {
259
+ const wf = minimalWorkflow({
260
+ phases: [
261
+ { name: 'setup', agent: 'sm' },
262
+ {
263
+ name: 'implement',
264
+ agent: 'dev',
265
+ tandem: { partner: 'ghost-agent', scope: 'file-watch' },
266
+ },
267
+ ],
268
+ });
269
+ const result = validateWorkflowGraph(wf, defaultContext());
270
+ assert.strictEqual(result.valid, false);
271
+ const msgs = errorMessages(result);
272
+ assert.ok(msgs.some(m => m.includes('ghost-agent') && m.toLowerCase().includes('tandem')), `Expected unknown tandem partner error, got: ${msgs.join('; ')}`);
273
+ });
274
+ it('should accept all valid agent names as phase agents', () => {
275
+ // Each VALID_AGENT_NAME should be accepted
276
+ for (const agent of VALID_AGENT_NAMES) {
277
+ const wf = minimalWorkflow({
278
+ phases: [{ name: 'work', agent }],
279
+ });
280
+ const result = validateWorkflowGraph(wf, defaultContext());
281
+ assert.strictEqual(result.valid, true, `Agent '${agent}' should be accepted`);
282
+ }
283
+ });
284
+ it('should accept a custom knownAgents list', () => {
285
+ const wf = minimalWorkflow({
286
+ phases: [{ name: 'work', agent: 'custom-agent' }],
287
+ });
288
+ const ctx = defaultContext({ knownAgents: ['custom-agent'] });
289
+ const result = validateWorkflowGraph(wf, ctx);
290
+ assert.strictEqual(result.valid, true, 'Custom agent should be accepted via context');
291
+ });
292
+ it('should detect unknown team teammate agents', () => {
293
+ const wf = minimalWorkflow({
294
+ phases: [
295
+ { name: 'setup', agent: 'sm' },
296
+ {
297
+ name: 'work',
298
+ agent: 'dev',
299
+ team: {
300
+ teammates: [
301
+ { agent: 'fake-agent', task: 'Help implement' },
302
+ ],
303
+ },
304
+ },
305
+ ],
306
+ });
307
+ const result = validateWorkflowGraph(wf, defaultContext());
308
+ assert.strictEqual(result.valid, false);
309
+ const msgs = errorMessages(result);
310
+ assert.ok(msgs.some(m => m.includes('fake-agent') && m.toLowerCase().includes('team')), `Expected unknown team agent error, got: ${msgs.join('; ')}`);
311
+ });
312
+ });
313
+ // -------------------------------------------------------------------------
314
+ // AC3: Gate file existence
315
+ // -------------------------------------------------------------------------
316
+ describe('Gate file validation', () => {
317
+ it('should accept a workflow where all gate files are known', () => {
318
+ const wf = tddWorkflow();
319
+ const result = validateWorkflowGraph(wf, defaultContext());
320
+ assert.strictEqual(result.valid, true);
321
+ });
322
+ it('should detect a gate file that does not exist', () => {
323
+ const wf = minimalWorkflow({
324
+ phases: [
325
+ {
326
+ name: 'setup',
327
+ agent: 'sm',
328
+ gate: { file: 'gates/nonexistent-gate', type: 'manual' },
329
+ },
330
+ ],
331
+ });
332
+ const result = validateWorkflowGraph(wf, defaultContext());
333
+ assert.strictEqual(result.valid, false);
334
+ const msgs = errorMessages(result);
335
+ assert.ok(msgs.some(m => m.includes('nonexistent-gate') && m.toLowerCase().includes('gate')), `Expected unknown gate file error, got: ${msgs.join('; ')}`);
336
+ });
337
+ it('should skip gate file check for phases without gates', () => {
338
+ const wf = minimalWorkflow({
339
+ phases: [
340
+ { name: 'work', agent: 'dev' }, // no gate
341
+ ],
342
+ });
343
+ const result = validateWorkflowGraph(wf, defaultContext());
344
+ assert.strictEqual(result.valid, true);
345
+ });
346
+ it('should skip gate file check for gates with type only (no file)', () => {
347
+ const wf = minimalWorkflow({
348
+ phases: [
349
+ {
350
+ name: 'work',
351
+ agent: 'dev',
352
+ gate: { type: 'manual' }, // type only, no file
353
+ },
354
+ ],
355
+ });
356
+ const result = validateWorkflowGraph(wf, defaultContext());
357
+ assert.strictEqual(result.valid, true);
358
+ });
359
+ it('should detect multiple missing gate files', () => {
360
+ const wf = minimalWorkflow({
361
+ phases: [
362
+ {
363
+ name: 'a',
364
+ agent: 'sm',
365
+ gate: { file: 'gates/missing-one' },
366
+ },
367
+ {
368
+ name: 'b',
369
+ agent: 'dev',
370
+ gate: { file: 'gates/missing-two' },
371
+ },
372
+ ],
373
+ });
374
+ const result = validateWorkflowGraph(wf, defaultContext());
375
+ assert.strictEqual(result.valid, false);
376
+ const msgs = errorMessages(result);
377
+ assert.ok(msgs.some(m => m.includes('missing-one')), 'Should report first missing gate');
378
+ assert.ok(msgs.some(m => m.includes('missing-two')), 'Should report second missing gate');
379
+ });
380
+ });
381
+ // -------------------------------------------------------------------------
382
+ // AC4: Collaboration validation (tandem + team)
383
+ // -------------------------------------------------------------------------
384
+ describe('Collaboration validation', () => {
385
+ it('should accept valid tandem configuration', () => {
386
+ const wf = minimalWorkflow({
387
+ phases: [
388
+ { name: 'setup', agent: 'sm' },
389
+ {
390
+ name: 'red',
391
+ agent: 'tea',
392
+ tandem: { partner: 'architect', scope: 'file-watch' },
393
+ },
394
+ ],
395
+ });
396
+ const result = validateWorkflowGraph(wf, defaultContext());
397
+ assert.strictEqual(result.valid, true);
398
+ });
399
+ it('should accept valid team configuration', () => {
400
+ const wf = minimalWorkflow({
401
+ phases: [
402
+ { name: 'setup', agent: 'sm' },
403
+ {
404
+ name: 'work',
405
+ agent: 'dev',
406
+ team: {
407
+ teammates: [
408
+ { agent: 'tea', task: 'Verify tests' },
409
+ ],
410
+ },
411
+ },
412
+ ],
413
+ });
414
+ const result = validateWorkflowGraph(wf, defaultContext());
415
+ assert.strictEqual(result.valid, true);
416
+ });
417
+ it('should warn when tandem partner is the same as phase agent', () => {
418
+ const wf = minimalWorkflow({
419
+ phases: [
420
+ {
421
+ name: 'work',
422
+ agent: 'dev',
423
+ tandem: { partner: 'dev' }, // same as phase agent
424
+ },
425
+ ],
426
+ });
427
+ const result = validateWorkflowGraph(wf, defaultContext());
428
+ // This is a warning, not an error — unusual but maybe intentional
429
+ const warns = warningMessages(result);
430
+ assert.ok(warns.some(m => m.toLowerCase().includes('same') || m.toLowerCase().includes('self')), `Expected warning about self-tandem, got: ${warns.join('; ')}`);
431
+ });
432
+ it('should error when team teammate is the same as phase agent (lead)', () => {
433
+ const wf = minimalWorkflow({
434
+ phases: [
435
+ {
436
+ name: 'work',
437
+ agent: 'dev',
438
+ team: {
439
+ teammates: [
440
+ { agent: 'dev', task: 'Help myself' }, // same as lead
441
+ ],
442
+ },
443
+ },
444
+ ],
445
+ });
446
+ const result = validateWorkflowGraph(wf, defaultContext());
447
+ assert.strictEqual(result.valid, false);
448
+ const msgs = errorMessages(result);
449
+ assert.ok(msgs.some(m => m.toLowerCase().includes('lead') || m.toLowerCase().includes('same')), `Expected error about teammate same as lead, got: ${msgs.join('; ')}`);
450
+ });
451
+ it('should detect duplicate teammates in a team', () => {
452
+ const wf = minimalWorkflow({
453
+ phases: [
454
+ {
455
+ name: 'work',
456
+ agent: 'dev',
457
+ team: {
458
+ teammates: [
459
+ { agent: 'tea', task: 'Run tests' },
460
+ { agent: 'tea', task: 'Also run tests' }, // duplicate
461
+ ],
462
+ },
463
+ },
464
+ ],
465
+ });
466
+ const result = validateWorkflowGraph(wf, defaultContext());
467
+ assert.strictEqual(result.valid, false);
468
+ const msgs = errorMessages(result);
469
+ assert.ok(msgs.some(m => m.includes('tea') && m.toLowerCase().includes('duplicate')), `Expected duplicate teammate error, got: ${msgs.join('; ')}`);
470
+ });
471
+ });
472
+ // -------------------------------------------------------------------------
473
+ // AC5: Data flow validation (input/output wiring)
474
+ // -------------------------------------------------------------------------
475
+ describe('Data flow validation', () => {
476
+ it('should accept TDD workflow with correct data flow', () => {
477
+ const wf = tddWorkflow();
478
+ const result = validateWorkflowGraph(wf, defaultContext());
479
+ assert.strictEqual(result.valid, true);
480
+ assert.strictEqual((result.warnings ?? []).length, 0, 'No warnings for correct data flow');
481
+ });
482
+ it('should warn when a phase input is not produced by any prior phase output', () => {
483
+ const wf = minimalWorkflow({
484
+ phases: [
485
+ { name: 'setup', agent: 'sm', output: ['session_file'] },
486
+ {
487
+ name: 'work',
488
+ agent: 'dev',
489
+ input: ['session_file', 'magic_data'], // magic_data not produced
490
+ },
491
+ ],
492
+ });
493
+ const result = validateWorkflowGraph(wf, defaultContext());
494
+ // Unresolved inputs are warnings (advisory), not errors
495
+ const warns = warningMessages(result);
496
+ assert.ok(warns.some(m => m.includes('magic_data')), `Expected warning about unresolved input 'magic_data', got: ${warns.join('; ')}`);
497
+ });
498
+ it('should not warn when inputs come from earlier phases', () => {
499
+ const wf = minimalWorkflow({
500
+ phases: [
501
+ { name: 'setup', agent: 'sm', output: ['session_file', 'branches'] },
502
+ { name: 'work', agent: 'dev', input: ['session_file'] },
503
+ ],
504
+ });
505
+ const result = validateWorkflowGraph(wf, defaultContext());
506
+ assert.strictEqual((result.warnings ?? []).length, 0, 'All inputs resolved');
507
+ });
508
+ it('should not warn for phases without inputs or outputs', () => {
509
+ const wf = minimalWorkflow({
510
+ phases: [
511
+ { name: 'a', agent: 'sm' },
512
+ { name: 'b', agent: 'dev' },
513
+ ],
514
+ });
515
+ const result = validateWorkflowGraph(wf, defaultContext());
516
+ assert.strictEqual(result.valid, true);
517
+ assert.strictEqual((result.warnings ?? []).length, 0);
518
+ });
519
+ it('should warn for multiple unresolved inputs across phases', () => {
520
+ const wf = minimalWorkflow({
521
+ phases: [
522
+ { name: 'a', agent: 'sm', output: ['x'] },
523
+ { name: 'b', agent: 'dev', input: ['x', 'y'] }, // y unresolved
524
+ { name: 'c', agent: 'reviewer', input: ['x', 'z'] }, // z unresolved
525
+ ],
526
+ });
527
+ const result = validateWorkflowGraph(wf, defaultContext());
528
+ const warns = warningMessages(result);
529
+ assert.ok(warns.some(m => m.includes('y')), 'Should warn about unresolved y');
530
+ assert.ok(warns.some(m => m.includes('z')), 'Should warn about unresolved z');
531
+ });
532
+ });
533
+ // -------------------------------------------------------------------------
534
+ // AC6: Integration — validate real workflow files
535
+ // -------------------------------------------------------------------------
536
+ describe('Integration with real workflow structures', () => {
537
+ it('should validate the tdd-tandem workflow graph', () => {
538
+ const wf = {
539
+ name: 'tdd-tandem',
540
+ phases: [
541
+ {
542
+ name: 'setup',
543
+ agent: 'sm',
544
+ output: ['session_file', 'branches', 'story_context'],
545
+ gate: { file: 'gates/sm-setup-exit', type: 'sm_setup_exit' },
546
+ },
547
+ {
548
+ name: 'red',
549
+ agent: 'tea',
550
+ input: ['session_file', 'story_context'],
551
+ output: ['failing_tests'],
552
+ gate: { file: 'gates/tests-fail', type: 'tests_fail' },
553
+ tandem: { partner: 'architect', scope: 'file-watch' },
554
+ },
555
+ {
556
+ name: 'green',
557
+ agent: 'dev',
558
+ input: ['failing_tests', 'story_context'],
559
+ output: ['implementation', 'passing_tests'],
560
+ gate: { file: 'gates/dev-exit', type: 'dev_exit' },
561
+ tandem: { partner: 'architect', scope: 'file-watch' },
562
+ },
563
+ {
564
+ name: 'review',
565
+ agent: 'reviewer',
566
+ input: ['implementation', 'passing_tests'],
567
+ output: ['approval'],
568
+ gate: { file: 'gates/approval', type: 'approval' },
569
+ tandem: { partner: 'pm', scope: 'file-watch' },
570
+ },
571
+ {
572
+ name: 'finish',
573
+ agent: 'sm',
574
+ input: ['approval'],
575
+ output: ['archived_session', 'story_summary'],
576
+ },
577
+ ],
578
+ };
579
+ const result = validateWorkflowGraph(wf, defaultContext());
580
+ assert.strictEqual(result.valid, true, 'tdd-tandem should have valid graph');
581
+ });
582
+ it('should validate the bdd-team workflow graph', () => {
583
+ const wf = {
584
+ name: 'bdd-team',
585
+ phases: [
586
+ {
587
+ name: 'setup',
588
+ agent: 'sm',
589
+ output: ['session_file', 'branches', 'story_context'],
590
+ },
591
+ {
592
+ name: 'design',
593
+ agent: 'ux-designer',
594
+ input: ['session_file', 'story_context'],
595
+ output: ['design_spec', 'user_flows', 'wireframes', 'behavior_scenarios'],
596
+ gate: { file: 'gates/design-review', type: 'design_review' },
597
+ team: {
598
+ teammates: [
599
+ { agent: 'architect', task: 'Validate technical feasibility' },
600
+ ],
601
+ },
602
+ },
603
+ {
604
+ name: 'red',
605
+ agent: 'tea',
606
+ input: ['design_spec', 'behavior_scenarios', 'story_context'],
607
+ output: ['failing_tests'],
608
+ gate: { file: 'gates/tests-fail', type: 'tests_fail' },
609
+ },
610
+ {
611
+ name: 'green',
612
+ agent: 'dev',
613
+ input: ['failing_tests', 'design_spec', 'story_context'],
614
+ output: ['implementation', 'passing_tests'],
615
+ gate: { file: 'gates/tests-pass', type: 'tests_pass' },
616
+ team: {
617
+ teammates: [{ agent: 'tea', task: 'Verify tests stay green' }],
618
+ },
619
+ },
620
+ {
621
+ name: 'review',
622
+ agent: 'reviewer',
623
+ input: ['implementation', 'passing_tests', 'design_spec'],
624
+ output: ['approval'],
625
+ gate: { file: 'gates/approval', type: 'approval' },
626
+ },
627
+ {
628
+ name: 'finish',
629
+ agent: 'sm',
630
+ input: ['approval'],
631
+ output: ['archived_session', 'story_summary'],
632
+ },
633
+ ],
634
+ };
635
+ const result = validateWorkflowGraph(wf, defaultContext());
636
+ assert.strictEqual(result.valid, true, 'bdd-team should have valid graph');
637
+ });
638
+ it('should detect a malformed workflow with multiple graph issues', () => {
639
+ const wf = {
640
+ name: 'broken',
641
+ phases: [
642
+ {
643
+ name: 'setup',
644
+ agent: 'sm',
645
+ next: 'review', // skips implement → implement unreachable
646
+ },
647
+ {
648
+ name: 'implement',
649
+ agent: 'invalid-agent', // unknown agent
650
+ gate: { file: 'gates/does-not-exist' }, // missing gate file
651
+ },
652
+ {
653
+ name: 'review',
654
+ agent: 'reviewer',
655
+ tandem: { partner: 'nobody' }, // unknown tandem partner
656
+ input: ['phantom_data'], // unresolved input
657
+ },
658
+ ],
659
+ };
660
+ const result = validateWorkflowGraph(wf, defaultContext());
661
+ assert.strictEqual(result.valid, false, 'Workflow with multiple issues should fail');
662
+ const msgs = errorMessages(result);
663
+ // Should catch: unreachable phase, unknown agent, missing gate, unknown tandem
664
+ assert.ok(msgs.length >= 3, `Expected at least 3 errors, got ${msgs.length}: ${msgs.join('; ')}`);
665
+ assert.ok(msgs.some(m => m.includes('implement') && m.toLowerCase().includes('unreachable')));
666
+ assert.ok(msgs.some(m => m.includes('invalid-agent')));
667
+ assert.ok(msgs.some(m => m.includes('nobody')));
668
+ });
669
+ });
670
+ // -------------------------------------------------------------------------
671
+ // Edge cases
672
+ // -------------------------------------------------------------------------
673
+ describe('Edge cases', () => {
674
+ it('should handle undefined phases gracefully', () => {
675
+ const wf = { name: 'no-phases' };
676
+ const result = validateWorkflowGraph(wf, defaultContext());
677
+ // No phases = nothing to validate graph-wise
678
+ assert.strictEqual(result.valid, true);
679
+ });
680
+ it('should handle empty phases array', () => {
681
+ const wf = { name: 'empty', phases: [] };
682
+ const result = validateWorkflowGraph(wf, defaultContext());
683
+ assert.strictEqual(result.valid, true);
684
+ });
685
+ it('should handle empty knownGateFiles list', () => {
686
+ const wf = minimalWorkflow({
687
+ phases: [
688
+ { name: 'work', agent: 'dev', gate: { file: 'gates/approval' } },
689
+ ],
690
+ });
691
+ const ctx = defaultContext({ knownGateFiles: [] });
692
+ const result = validateWorkflowGraph(wf, ctx);
693
+ assert.strictEqual(result.valid, false, 'Gate file should fail against empty known list');
694
+ });
695
+ it('should validate gate file paths case-sensitively', () => {
696
+ const wf = minimalWorkflow({
697
+ phases: [
698
+ { name: 'work', agent: 'dev', gate: { file: 'gates/Approval' } }, // wrong case
699
+ ],
700
+ });
701
+ const result = validateWorkflowGraph(wf, defaultContext());
702
+ assert.strictEqual(result.valid, false, 'Gate file check should be case-sensitive');
703
+ });
704
+ });
705
+ });
706
+ //# sourceMappingURL=workflow-graph-validation.test.js.map