@pennyfarthing/core 7.8.2 → 7.9.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.
Files changed (210) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/init.js +8 -7
  5. package/packages/core/dist/cli/commands/init.js.map +1 -1
  6. package/packages/core/dist/cli/cyclist-migration.test.js +16 -13
  7. package/packages/core/dist/cli/cyclist-migration.test.js.map +1 -1
  8. package/packages/core/dist/cli/utils/files.d.ts +5 -4
  9. package/packages/core/dist/cli/utils/files.d.ts.map +1 -1
  10. package/packages/core/dist/cli/utils/files.js +8 -6
  11. package/packages/core/dist/cli/utils/files.js.map +1 -1
  12. package/packages/core/dist/cli/utils/symlinks.d.ts +7 -0
  13. package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
  14. package/packages/core/dist/cli/utils/symlinks.js +25 -0
  15. package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
  16. package/packages/core/dist/cli/utils/themes.d.ts +1 -1
  17. package/packages/core/dist/cli/utils/themes.d.ts.map +1 -1
  18. package/packages/core/dist/scripts/run-ci.test.js +1 -1
  19. package/packages/core/dist/scripts/run-ci.test.js.map +1 -1
  20. package/pennyfarthing-dist/agents/README.md +25 -17
  21. package/pennyfarthing-dist/agents/architect.md +3 -11
  22. package/pennyfarthing-dist/agents/dev.md +2 -2
  23. package/pennyfarthing-dist/agents/devops.md +3 -11
  24. package/pennyfarthing-dist/agents/handoff.md +4 -4
  25. package/pennyfarthing-dist/agents/orchestrator.md +2 -4
  26. package/pennyfarthing-dist/agents/pm.md +4 -11
  27. package/pennyfarthing-dist/agents/reviewer-preflight.md +4 -3
  28. package/pennyfarthing-dist/agents/reviewer.md +2 -8
  29. package/pennyfarthing-dist/agents/sm-handoff.md +3 -3
  30. package/pennyfarthing-dist/agents/sm-setup.md +1 -1
  31. package/pennyfarthing-dist/agents/sm.md +5 -29
  32. package/pennyfarthing-dist/agents/tea.md +2 -2
  33. package/pennyfarthing-dist/agents/tech-writer.md +3 -12
  34. package/pennyfarthing-dist/agents/testing-runner.md +8 -8
  35. package/pennyfarthing-dist/agents/ux-designer.md +3 -12
  36. package/pennyfarthing-dist/commands/git-cleanup.md +29 -53
  37. package/pennyfarthing-dist/commands/party-mode.md +20 -10
  38. package/pennyfarthing-dist/commands/work.md +6 -105
  39. package/pennyfarthing-dist/guides/agent-behavior.md +19 -7
  40. package/pennyfarthing-dist/personas/themes/1984.yaml +0 -12
  41. package/pennyfarthing-dist/personas/themes/a-team.yaml +0 -10
  42. package/pennyfarthing-dist/personas/themes/agatha-christie.yaml +0 -10
  43. package/pennyfarthing-dist/personas/themes/alice-in-wonderland.yaml +0 -10
  44. package/pennyfarthing-dist/personas/themes/all-stars.yaml +0 -10
  45. package/pennyfarthing-dist/personas/themes/ancient-philosophers.yaml +0 -12
  46. package/pennyfarthing-dist/personas/themes/ancient-strategists.yaml +0 -12
  47. package/pennyfarthing-dist/personas/themes/arcane.yaml +0 -10
  48. package/pennyfarthing-dist/personas/themes/arthurian-mythos.yaml +0 -13
  49. package/pennyfarthing-dist/personas/themes/avatar-the-last-airbender.yaml +0 -10
  50. package/pennyfarthing-dist/personas/themes/babylon-5.yaml +0 -10
  51. package/pennyfarthing-dist/personas/themes/battlestar-galactica.yaml +0 -10
  52. package/pennyfarthing-dist/personas/themes/better-call-saul.yaml +0 -10
  53. package/pennyfarthing-dist/personas/themes/big-lebowski.yaml +0 -10
  54. package/pennyfarthing-dist/personas/themes/black-sails.yaml +0 -10
  55. package/pennyfarthing-dist/personas/themes/blade-runner.yaml +0 -10
  56. package/pennyfarthing-dist/personas/themes/bobiverse.yaml +0 -10
  57. package/pennyfarthing-dist/personas/themes/breaking-bad.yaml +0 -12
  58. package/pennyfarthing-dist/personas/themes/catch-22.yaml +0 -12
  59. package/pennyfarthing-dist/personas/themes/classical-composers.yaml +0 -12
  60. package/pennyfarthing-dist/personas/themes/count-of-monte-cristo.yaml +0 -12
  61. package/pennyfarthing-dist/personas/themes/cowboy-bebop.yaml +0 -12
  62. package/pennyfarthing-dist/personas/themes/deadwood.yaml +0 -10
  63. package/pennyfarthing-dist/personas/themes/dickens.yaml +0 -12
  64. package/pennyfarthing-dist/personas/themes/discworld.yaml +0 -10
  65. package/pennyfarthing-dist/personas/themes/doctor-who.yaml +0 -10
  66. package/pennyfarthing-dist/personas/themes/don-quixote.yaml +0 -12
  67. package/pennyfarthing-dist/personas/themes/dune.yaml +0 -10
  68. package/pennyfarthing-dist/personas/themes/enlightenment-thinkers.yaml +0 -12
  69. package/pennyfarthing-dist/personas/themes/expeditionary-force.yaml +0 -10
  70. package/pennyfarthing-dist/personas/themes/fargo.yaml +0 -12
  71. package/pennyfarthing-dist/personas/themes/film-auteurs.yaml +0 -12
  72. package/pennyfarthing-dist/personas/themes/firefly.yaml +0 -12
  73. package/pennyfarthing-dist/personas/themes/foundation.yaml +0 -10
  74. package/pennyfarthing-dist/personas/themes/futurama.yaml +0 -12
  75. package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +0 -10
  76. package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +0 -12
  77. package/pennyfarthing-dist/personas/themes/gothic-literature.yaml +0 -12
  78. package/pennyfarthing-dist/personas/themes/great-gatsby.yaml +0 -12
  79. package/pennyfarthing-dist/personas/themes/greek-mythology.yaml +0 -13
  80. package/pennyfarthing-dist/personas/themes/hannibal.yaml +0 -10
  81. package/pennyfarthing-dist/personas/themes/harry-potter.yaml +0 -12
  82. package/pennyfarthing-dist/personas/themes/his-dark-materials.yaml +0 -10
  83. package/pennyfarthing-dist/personas/themes/historical-figures.yaml +0 -10
  84. package/pennyfarthing-dist/personas/themes/hitchhikers-guide.yaml +0 -12
  85. package/pennyfarthing-dist/personas/themes/house-md.yaml +0 -12
  86. package/pennyfarthing-dist/personas/themes/imperial-radch.yaml +0 -10
  87. package/pennyfarthing-dist/personas/themes/inspector-morse.yaml +0 -10
  88. package/pennyfarthing-dist/personas/themes/jane-austen.yaml +0 -10
  89. package/pennyfarthing-dist/personas/themes/jazz-legends.yaml +0 -12
  90. package/pennyfarthing-dist/personas/themes/justified.yaml +0 -10
  91. package/pennyfarthing-dist/personas/themes/legion-of-doom.yaml +0 -10
  92. package/pennyfarthing-dist/personas/themes/les-miserables.yaml +0 -10
  93. package/pennyfarthing-dist/personas/themes/lord-of-the-rings.yaml +0 -12
  94. package/pennyfarthing-dist/personas/themes/lovecraft-mythos.yaml +0 -13
  95. package/pennyfarthing-dist/personas/themes/mad-max.yaml +0 -10
  96. package/pennyfarthing-dist/personas/themes/mad-men.yaml +0 -10
  97. package/pennyfarthing-dist/personas/themes/marvel-mcu.yaml +0 -10
  98. package/pennyfarthing-dist/personas/themes/mash.yaml +0 -12
  99. package/pennyfarthing-dist/personas/themes/mass-effect.yaml +0 -10
  100. package/pennyfarthing-dist/personas/themes/military-commanders.yaml +0 -12
  101. package/pennyfarthing-dist/personas/themes/moby-dick.yaml +0 -12
  102. package/pennyfarthing-dist/personas/themes/monty-python.yaml +0 -10
  103. package/pennyfarthing-dist/personas/themes/neuromancer.yaml +0 -10
  104. package/pennyfarthing-dist/personas/themes/norse-mythology.yaml +0 -12
  105. package/pennyfarthing-dist/personas/themes/parks-and-rec.yaml +0 -12
  106. package/pennyfarthing-dist/personas/themes/peaky-blinders.yaml +0 -10
  107. package/pennyfarthing-dist/personas/themes/princess-bride.yaml +0 -10
  108. package/pennyfarthing-dist/personas/themes/renaissance-masters.yaml +0 -12
  109. package/pennyfarthing-dist/personas/themes/rome.yaml +0 -10
  110. package/pennyfarthing-dist/personas/themes/russian-masters.yaml +0 -12
  111. package/pennyfarthing-dist/personas/themes/sandman.yaml +0 -10
  112. package/pennyfarthing-dist/personas/themes/scientific-revolutionaries.yaml +0 -12
  113. package/pennyfarthing-dist/personas/themes/shakespeare.yaml +0 -10
  114. package/pennyfarthing-dist/personas/themes/sherlock-holmes.yaml +0 -10
  115. package/pennyfarthing-dist/personas/themes/snow-crash.yaml +0 -10
  116. package/pennyfarthing-dist/personas/themes/software-pioneers.yaml +0 -10
  117. package/pennyfarthing-dist/personas/themes/star-trek-tng.yaml +0 -11
  118. package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +0 -10
  119. package/pennyfarthing-dist/personas/themes/star-wars.yaml +0 -10
  120. package/pennyfarthing-dist/personas/themes/succession.yaml +0 -10
  121. package/pennyfarthing-dist/personas/themes/superfriends.yaml +0 -10
  122. package/pennyfarthing-dist/personas/themes/ted-lasso.yaml +0 -11
  123. package/pennyfarthing-dist/personas/themes/the-americans.yaml +0 -10
  124. package/pennyfarthing-dist/personas/themes/the-crown.yaml +0 -10
  125. package/pennyfarthing-dist/personas/themes/the-expanse.yaml +0 -10
  126. package/pennyfarthing-dist/personas/themes/the-good-place.yaml +0 -11
  127. package/pennyfarthing-dist/personas/themes/the-matrix.yaml +0 -15
  128. package/pennyfarthing-dist/personas/themes/the-odyssey.yaml +0 -10
  129. package/pennyfarthing-dist/personas/themes/the-office.yaml +0 -11
  130. package/pennyfarthing-dist/personas/themes/the-simpsons.yaml +0 -12
  131. package/pennyfarthing-dist/personas/themes/the-sopranos.yaml +0 -10
  132. package/pennyfarthing-dist/personas/themes/the-wire.yaml +0 -12
  133. package/pennyfarthing-dist/personas/themes/the-witcher.yaml +0 -10
  134. package/pennyfarthing-dist/personas/themes/twin-peaks.yaml +0 -10
  135. package/pennyfarthing-dist/personas/themes/vorkosigan-saga.yaml +0 -10
  136. package/pennyfarthing-dist/personas/themes/watchmen.yaml +0 -10
  137. package/pennyfarthing-dist/personas/themes/west-wing.yaml +0 -10
  138. package/pennyfarthing-dist/personas/themes/world-explorers.yaml +0 -12
  139. package/pennyfarthing-dist/personas/themes/wwii-leaders.yaml +0 -12
  140. package/pennyfarthing-dist/personas/themes/x-files.yaml +0 -10
  141. package/pennyfarthing-dist/scripts/core/agent-session.sh +13 -14
  142. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
  143. package/pennyfarthing-dist/scripts/core/prime.sh +17 -2
  144. package/pennyfarthing-dist/scripts/core/run.sh +5 -5
  145. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -2
  146. package/pennyfarthing-dist/scripts/git/release.sh +2 -2
  147. package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -1
  148. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -2
  149. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +2 -2
  150. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +1 -1
  151. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
  152. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +1 -1
  153. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +1 -1
  154. package/pennyfarthing-dist/scripts/lib/common.sh +1 -1
  155. package/pennyfarthing-dist/scripts/lib/find-root.sh +4 -4
  156. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +102 -0
  157. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -1
  158. package/pennyfarthing-dist/scripts/misc/add_short_names.py +2 -2
  159. package/pennyfarthing-dist/scripts/misc/backlog.sh +2 -2
  160. package/pennyfarthing-dist/scripts/misc/deploy.sh +2 -2
  161. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +4 -4
  162. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +2 -2
  163. package/pennyfarthing-dist/scripts/misc/run-ci.sh +1 -1
  164. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +2 -2
  165. package/pennyfarthing-dist/scripts/sprint/archive-story.sh +6 -2
  166. package/pennyfarthing-dist/scripts/sprint/available-stories.sh +1 -1
  167. package/pennyfarthing-dist/scripts/sprint/check-story.sh +1 -1
  168. package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +1 -1
  169. package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +1 -1
  170. package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +2 -2
  171. package/pennyfarthing-dist/scripts/sprint/list-future.sh +1 -1
  172. package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +1 -1
  173. package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +1 -1
  174. package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +3 -3
  175. package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +1 -1
  176. package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +2 -2
  177. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +2 -2
  178. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +3 -2
  179. package/pennyfarthing-dist/scripts/workflow/check.py +2 -2
  180. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +1 -1
  181. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +1 -1
  182. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +2 -2
  183. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +1 -1
  184. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -1
  185. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +1 -1
  186. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +1 -1
  187. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +1 -1
  188. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +1 -1
  189. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +18 -0
  190. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-03-execute.md +18 -4
  191. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +13 -5
  192. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  193. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  194. package/pennyfarthing_scripts/jira/client.py +1 -1
  195. package/pennyfarthing_scripts/prime/__init__.py +98 -11
  196. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  197. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  198. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/prime/cli.py +208 -53
  203. package/pennyfarthing_scripts/prime/models.py +169 -0
  204. package/pennyfarthing_scripts/prime/persona.py +288 -0
  205. package/pennyfarthing_scripts/prime/session.py +183 -0
  206. package/pennyfarthing_scripts/prime/workflow.py +275 -0
  207. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  209. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  210. package/pennyfarthing_scripts/tests/test_prime.py +653 -0
@@ -395,3 +395,656 @@ class TestMainCLI:
395
395
  assert result == 1
396
396
  captured = capsys.readouterr()
397
397
  assert "Error" in captured.err
398
+
399
+
400
+ # =============================================================================
401
+ # Prime v2 Tests - Workflow State Detection
402
+ # =============================================================================
403
+
404
+
405
+ class TestWorkflowStateDetection:
406
+ """Tests for workflow state detection (Prime v2)."""
407
+
408
+ def test_detect_finish_state(self, tmp_path: Path) -> None:
409
+ """Test detecting FINISH_STATE when phase is approved."""
410
+ from pennyfarthing_scripts.prime.workflow import detect_workflow_state
411
+ from pennyfarthing_scripts.prime.models import WorkflowState
412
+
413
+ # Setup
414
+ pf_dir = tmp_path / ".pennyfarthing"
415
+ pf_dir.mkdir()
416
+ session_dir = tmp_path / ".session"
417
+ session_dir.mkdir()
418
+ session_file = session_dir / "MSSCI-12345-session.md"
419
+ session_file.write_text("""# MSSCI-12345: Test Story
420
+
421
+ ## Story Context
422
+ - **ID:** MSSCI-12345
423
+ - **Workflow:** tdd
424
+
425
+ ## Workflow Phase
426
+ - **Current Phase:** REVIEW (APPROVED)
427
+ """)
428
+
429
+ # Test
430
+ result = detect_workflow_state(tmp_path)
431
+
432
+ # Verify
433
+ assert result.state == WorkflowState.FINISH_STATE
434
+ assert result.story_id == "MSSCI-12345"
435
+ assert result.phase_owner == "sm"
436
+
437
+ def test_detect_in_progress_state(self, tmp_path: Path) -> None:
438
+ """Test detecting IN_PROGRESS_STATE with active phase."""
439
+ from pennyfarthing_scripts.prime.workflow import detect_workflow_state
440
+ from pennyfarthing_scripts.prime.models import WorkflowState
441
+ import yaml
442
+
443
+ # Setup
444
+ pf_dir = tmp_path / ".pennyfarthing"
445
+ pf_dir.mkdir()
446
+
447
+ # Create workflow YAML
448
+ workflows_dir = tmp_path / "pennyfarthing-dist" / "workflows"
449
+ workflows_dir.mkdir(parents=True)
450
+ (workflows_dir / "tdd.yaml").write_text(yaml.dump({
451
+ "workflow": {
452
+ "phases": [
453
+ {"name": "setup", "agent": "sm"},
454
+ {"name": "red", "agent": "tea"},
455
+ {"name": "green", "agent": "dev"},
456
+ {"name": "review", "agent": "reviewer"},
457
+ ]
458
+ }
459
+ }))
460
+
461
+ session_dir = tmp_path / ".session"
462
+ session_dir.mkdir()
463
+ session_file = session_dir / "63-5-session.md"
464
+ session_file.write_text("""# 63-5: Test Story
465
+
466
+ ## Story Context
467
+ - **ID:** 63-5
468
+ - **Workflow:** tdd
469
+
470
+ ## Workflow Phase
471
+ - **Current Phase:** green
472
+ """)
473
+
474
+ # Test
475
+ result = detect_workflow_state(tmp_path)
476
+
477
+ # Verify
478
+ assert result.state == WorkflowState.IN_PROGRESS_STATE
479
+ assert result.story_id == "63-5"
480
+ assert result.phase == "green"
481
+ assert result.phase_owner == "dev"
482
+ assert result.workflow == "tdd"
483
+
484
+ def test_detect_new_work_state(self, tmp_path: Path) -> None:
485
+ """Test detecting NEW_WORK_STATE with backlog stories."""
486
+ from pennyfarthing_scripts.prime.workflow import detect_workflow_state
487
+ from pennyfarthing_scripts.prime.models import WorkflowState
488
+ import yaml
489
+
490
+ # Setup
491
+ pf_dir = tmp_path / ".pennyfarthing"
492
+ pf_dir.mkdir()
493
+ sprint_dir = tmp_path / "sprint"
494
+ sprint_dir.mkdir()
495
+ (sprint_dir / "current-sprint.yaml").write_text(yaml.dump({
496
+ "sprint": {"number": 12},
497
+ "epics": [
498
+ {
499
+ "id": "epic-1",
500
+ "stories": [
501
+ {"id": "1-1", "status": "backlog", "points": 3},
502
+ {"id": "1-2", "status": "ready", "points": 5},
503
+ ]
504
+ }
505
+ ]
506
+ }))
507
+
508
+ # Test
509
+ result = detect_workflow_state(tmp_path)
510
+
511
+ # Verify
512
+ assert result.state == WorkflowState.NEW_WORK_STATE
513
+ assert result.backlog_count == 2
514
+
515
+ def test_detect_empty_backlog_state(self, tmp_path: Path) -> None:
516
+ """Test detecting EMPTY_BACKLOG_STATE with no backlog."""
517
+ from pennyfarthing_scripts.prime.workflow import detect_workflow_state
518
+ from pennyfarthing_scripts.prime.models import WorkflowState
519
+ import yaml
520
+
521
+ # Setup
522
+ pf_dir = tmp_path / ".pennyfarthing"
523
+ pf_dir.mkdir()
524
+ sprint_dir = tmp_path / "sprint"
525
+ sprint_dir.mkdir()
526
+ (sprint_dir / "current-sprint.yaml").write_text(yaml.dump({
527
+ "sprint": {"number": 12},
528
+ "epics": [
529
+ {
530
+ "id": "epic-1",
531
+ "stories": [
532
+ {"id": "1-1", "status": "done", "points": 3},
533
+ ]
534
+ }
535
+ ]
536
+ }))
537
+
538
+ # Test
539
+ result = detect_workflow_state(tmp_path)
540
+
541
+ # Verify
542
+ assert result.state == WorkflowState.EMPTY_BACKLOG_STATE
543
+
544
+
545
+ class TestParseSessionHeader:
546
+ """Tests for session header parsing."""
547
+
548
+ def test_parse_standard_header(self, tmp_path: Path) -> None:
549
+ """Test parsing a standard session header."""
550
+ from pennyfarthing_scripts.prime.workflow import parse_session_header
551
+
552
+ session_file = tmp_path / "MSSCI-12345-session.md"
553
+ session_file.write_text("""# MSSCI-12345: Test Story
554
+
555
+ ## Story Context
556
+ - **ID:** MSSCI-12345
557
+ - **Workflow:** tdd
558
+
559
+ ## Workflow Phase
560
+ - **Current Phase:** green
561
+ """)
562
+
563
+ result = parse_session_header(session_file)
564
+
565
+ assert result["story_id"] == "MSSCI-12345"
566
+ assert result["workflow"] == "tdd"
567
+ assert result["phase"] == "green"
568
+
569
+ def test_parse_approved_phase(self, tmp_path: Path) -> None:
570
+ """Test parsing phase with APPROVED status."""
571
+ from pennyfarthing_scripts.prime.workflow import parse_session_header
572
+
573
+ session_file = tmp_path / "63-1-session.md"
574
+ session_file.write_text("""# Session
575
+
576
+ - **Current Phase:** REVIEW (APPROVED)
577
+ """)
578
+
579
+ result = parse_session_header(session_file)
580
+
581
+ assert result["phase"] == "review"
582
+ assert result["phase_status"] == "approved"
583
+
584
+
585
+ class TestCheckRedirect:
586
+ """Tests for redirect detection."""
587
+
588
+ def test_redirect_when_wrong_agent(self) -> None:
589
+ """Test redirect is detected when wrong agent is activated."""
590
+ from pennyfarthing_scripts.prime.workflow import check_redirect
591
+ from pennyfarthing_scripts.prime.models import WorkflowState, WorkflowStatus
592
+
593
+ status = WorkflowStatus(
594
+ state=WorkflowState.IN_PROGRESS_STATE,
595
+ phase="green",
596
+ phase_owner="dev",
597
+ )
598
+
599
+ result = check_redirect(status, "tea")
600
+
601
+ assert result is not None
602
+ target, reason = result
603
+ assert target == "dev"
604
+ assert "green" in reason
605
+ assert "dev" in reason
606
+
607
+ def test_no_redirect_when_correct_agent(self) -> None:
608
+ """Test no redirect when correct agent is activated."""
609
+ from pennyfarthing_scripts.prime.workflow import check_redirect
610
+ from pennyfarthing_scripts.prime.models import WorkflowState, WorkflowStatus
611
+
612
+ status = WorkflowStatus(
613
+ state=WorkflowState.IN_PROGRESS_STATE,
614
+ phase="green",
615
+ phase_owner="dev",
616
+ )
617
+
618
+ result = check_redirect(status, "dev")
619
+
620
+ assert result is None
621
+
622
+ def test_no_redirect_for_new_work(self) -> None:
623
+ """Test no redirect for NEW_WORK_STATE."""
624
+ from pennyfarthing_scripts.prime.workflow import check_redirect
625
+ from pennyfarthing_scripts.prime.models import WorkflowState, WorkflowStatus
626
+
627
+ status = WorkflowStatus(
628
+ state=WorkflowState.NEW_WORK_STATE,
629
+ backlog_count=5,
630
+ )
631
+
632
+ result = check_redirect(status, "dev")
633
+
634
+ assert result is None
635
+
636
+
637
+ # =============================================================================
638
+ # Prime v2 Tests - Persona Loading
639
+ # =============================================================================
640
+
641
+
642
+ class TestPersonaLoading:
643
+ """Tests for persona loading (Prime v2)."""
644
+
645
+ def test_load_persona_from_theme(self, tmp_path: Path) -> None:
646
+ """Test loading persona from theme YAML."""
647
+ from pennyfarthing_scripts.prime.persona import load_persona
648
+ import yaml
649
+
650
+ # Setup
651
+ pf_dir = tmp_path / ".pennyfarthing"
652
+ pf_dir.mkdir()
653
+
654
+ # Create config
655
+ (pf_dir / "config.local.yaml").write_text(yaml.dump({"theme": "test-theme"}))
656
+
657
+ # Create theme
658
+ themes_dir = pf_dir / "personas" / "themes"
659
+ themes_dir.mkdir(parents=True)
660
+ (themes_dir / "test-theme.yaml").write_text(yaml.dump({
661
+ "theme": {"name": "Test Theme"},
662
+ "agents": {
663
+ "dev": {
664
+ "character": "Test Developer",
665
+ "style": "Test style",
666
+ "role": "Test role",
667
+ "quote": "Test quote",
668
+ }
669
+ }
670
+ }))
671
+
672
+ # Test
673
+ persona, theme = load_persona("dev", tmp_path)
674
+
675
+ # Verify
676
+ assert persona is not None
677
+ assert persona.character == "Test Developer"
678
+ assert persona.style == "Test style"
679
+ assert persona.role == "Test role"
680
+ assert persona.quote == "Test quote"
681
+ assert theme == "test-theme"
682
+
683
+ def test_load_persona_no_theme(self, tmp_path: Path) -> None:
684
+ """Test load_persona returns None when no theme configured."""
685
+ from pennyfarthing_scripts.prime.persona import load_persona
686
+
687
+ # Setup - no config
688
+ pf_dir = tmp_path / ".pennyfarthing"
689
+ pf_dir.mkdir()
690
+
691
+ # Test
692
+ persona, theme = load_persona("dev", tmp_path)
693
+
694
+ # Verify
695
+ assert persona is None
696
+ assert theme is None
697
+
698
+ def test_get_crew_manifest(self, tmp_path: Path) -> None:
699
+ """Test getting crew manifest for handoff reference."""
700
+ from pennyfarthing_scripts.prime.persona import get_crew_manifest
701
+ import yaml
702
+
703
+ # Setup
704
+ pf_dir = tmp_path / ".pennyfarthing"
705
+ pf_dir.mkdir()
706
+
707
+ # Create config
708
+ (pf_dir / "config.local.yaml").write_text(yaml.dump({"theme": "test-theme"}))
709
+
710
+ # Create theme with multiple agents
711
+ themes_dir = pf_dir / "personas" / "themes"
712
+ themes_dir.mkdir(parents=True)
713
+ (themes_dir / "test-theme.yaml").write_text(yaml.dump({
714
+ "agents": {
715
+ "sm": {"character": "Scrum Master"},
716
+ "tea": {"character": "Test Engineer"},
717
+ "dev": {"character": "Developer"},
718
+ }
719
+ }))
720
+
721
+ # Test
722
+ crew = get_crew_manifest(tmp_path)
723
+
724
+ # Verify
725
+ assert len(crew) == 3
726
+ roles = {c.role for c in crew}
727
+ assert "sm" in roles
728
+ assert "tea" in roles
729
+ assert "dev" in roles
730
+
731
+ def test_format_persona_output(self) -> None:
732
+ """Test formatting persona as XML."""
733
+ from pennyfarthing_scripts.prime.persona import format_persona_output
734
+ from pennyfarthing_scripts.prime.models import Persona, CrewMember
735
+
736
+ persona = Persona(
737
+ character="Naomi Nagata",
738
+ style="Precise, systematic",
739
+ role="The XO and engineer",
740
+ quote="I can fix this.",
741
+ )
742
+ crew = [
743
+ CrewMember(role="sm", character="Drummer"),
744
+ CrewMember(role="dev", character="Naomi"),
745
+ ]
746
+
747
+ result = format_persona_output(persona, "the-expanse", "dev", crew, "Bossmang")
748
+
749
+ assert '<persona agent="dev" theme="the-expanse">' in result
750
+ assert "Character: Naomi Nagata" in result
751
+ assert "Quote: I can fix this." in result
752
+ assert "<user-title>Address the user as: Bossmang</user-title>" in result
753
+ assert '<crew theme="the-expanse">' in result
754
+
755
+
756
+ # =============================================================================
757
+ # Prime v2 Tests - Session Registration
758
+ # =============================================================================
759
+
760
+
761
+ class TestSessionRegistration:
762
+ """Tests for session registration (Prime v2)."""
763
+
764
+ def test_register_session(self, tmp_path: Path) -> None:
765
+ """Test registering a new session."""
766
+ from pennyfarthing_scripts.prime.session import register_session
767
+
768
+ # Setup
769
+ pf_dir = tmp_path / ".pennyfarthing"
770
+ pf_dir.mkdir()
771
+
772
+ # Test
773
+ result = register_session("dev", session_id="test-123", project_root=tmp_path)
774
+
775
+ # Verify
776
+ assert result.session_id == "test-123"
777
+ assert result.agent_name == "dev"
778
+
779
+ # Check file was created
780
+ session_file = tmp_path / ".session" / "agents" / "test-123"
781
+ assert session_file.exists()
782
+ assert session_file.read_text() == "dev"
783
+
784
+ def test_register_session_generates_id(self, tmp_path: Path) -> None:
785
+ """Test that session ID is generated if not provided."""
786
+ from pennyfarthing_scripts.prime.session import register_session
787
+
788
+ # Setup
789
+ pf_dir = tmp_path / ".pennyfarthing"
790
+ pf_dir.mkdir()
791
+
792
+ # Test
793
+ result = register_session("tea", project_root=tmp_path)
794
+
795
+ # Verify - should have a UUID-like session ID
796
+ assert result.session_id is not None
797
+ assert len(result.session_id) > 0
798
+
799
+ def test_cleanup_old_sessions(self, tmp_path: Path) -> None:
800
+ """Test cleanup of old session files."""
801
+ from pennyfarthing_scripts.prime.session import cleanup_old_sessions
802
+ import time
803
+
804
+ # Setup
805
+ pf_dir = tmp_path / ".pennyfarthing"
806
+ pf_dir.mkdir()
807
+ agents_dir = tmp_path / ".session" / "agents"
808
+ agents_dir.mkdir(parents=True)
809
+
810
+ # Create an old session file
811
+ old_session = agents_dir / "old-session"
812
+ old_session.write_text("sm")
813
+ # Set mtime to 10 days ago
814
+ old_time = time.time() - (10 * 86400)
815
+ import os
816
+ os.utime(old_session, (old_time, old_time))
817
+
818
+ # Create a new session file
819
+ new_session = agents_dir / "new-session"
820
+ new_session.write_text("dev")
821
+
822
+ # Test
823
+ removed = cleanup_old_sessions(tmp_path, max_age_days=7)
824
+
825
+ # Verify
826
+ assert removed == 1
827
+ assert not old_session.exists()
828
+ assert new_session.exists()
829
+
830
+ def test_get_session_agent(self, tmp_path: Path) -> None:
831
+ """Test getting agent name for a session."""
832
+ from pennyfarthing_scripts.prime.session import get_session_agent
833
+
834
+ # Setup
835
+ pf_dir = tmp_path / ".pennyfarthing"
836
+ pf_dir.mkdir()
837
+ agents_dir = tmp_path / ".session" / "agents"
838
+ agents_dir.mkdir(parents=True)
839
+ (agents_dir / "test-session").write_text("reviewer")
840
+
841
+ # Test
842
+ result = get_session_agent("test-session", tmp_path)
843
+
844
+ # Verify
845
+ assert result == "reviewer"
846
+
847
+ def test_unregister_session(self, tmp_path: Path) -> None:
848
+ """Test unregistering a session."""
849
+ from pennyfarthing_scripts.prime.session import unregister_session
850
+
851
+ # Setup
852
+ pf_dir = tmp_path / ".pennyfarthing"
853
+ pf_dir.mkdir()
854
+ agents_dir = tmp_path / ".session" / "agents"
855
+ agents_dir.mkdir(parents=True)
856
+ session_file = agents_dir / "to-remove"
857
+ session_file.write_text("dev")
858
+
859
+ # Test
860
+ result = unregister_session("to-remove", tmp_path)
861
+
862
+ # Verify
863
+ assert result is True
864
+ assert not session_file.exists()
865
+
866
+
867
+ # =============================================================================
868
+ # Prime v2 Tests - JSON Output
869
+ # =============================================================================
870
+
871
+
872
+ class TestJSONOutput:
873
+ """Tests for JSON output (Prime v2)."""
874
+
875
+ def test_json_output_minimal(self, tmp_path: Path, capsys) -> None:
876
+ """Test JSON output in minimal mode."""
877
+ import json
878
+
879
+ result = prime(minimal=True, json_output=True, project_root=tmp_path)
880
+
881
+ assert result == 0
882
+ captured = capsys.readouterr()
883
+ data = json.loads(captured.out)
884
+ assert data["minimal"] is True
885
+
886
+ def test_json_output_with_workflow(self, tmp_path: Path, capsys) -> None:
887
+ """Test JSON output includes workflow status."""
888
+ import json
889
+ import yaml
890
+
891
+ # Setup
892
+ pf_dir = tmp_path / ".pennyfarthing"
893
+ pf_dir.mkdir()
894
+ sprint_dir = tmp_path / "sprint"
895
+ sprint_dir.mkdir()
896
+ (sprint_dir / "current-sprint.yaml").write_text(yaml.dump({
897
+ "sprint": {"number": 12},
898
+ "epics": [
899
+ {
900
+ "id": "epic-1",
901
+ "stories": [
902
+ {"id": "1-1", "status": "backlog", "points": 3},
903
+ ]
904
+ }
905
+ ]
906
+ }))
907
+
908
+ # Test
909
+ result = prime(
910
+ agent_name="sm",
911
+ json_output=True,
912
+ no_persona=True,
913
+ no_register=True,
914
+ project_root=tmp_path,
915
+ )
916
+
917
+ assert result == 0
918
+ captured = capsys.readouterr()
919
+ data = json.loads(captured.out)
920
+
921
+ assert "workflow_status" in data
922
+ assert data["workflow_status"]["state"] == "NEW_WORK_STATE"
923
+ assert data["workflow_status"]["backlog_count"] == 1
924
+
925
+ def test_json_output_with_redirect(self, tmp_path: Path, capsys) -> None:
926
+ """Test JSON output includes redirect info."""
927
+ import json
928
+ import yaml
929
+
930
+ # Setup
931
+ pf_dir = tmp_path / ".pennyfarthing"
932
+ pf_dir.mkdir()
933
+
934
+ # Create workflow YAML
935
+ workflows_dir = tmp_path / "pennyfarthing-dist" / "workflows"
936
+ workflows_dir.mkdir(parents=True)
937
+ (workflows_dir / "tdd.yaml").write_text(yaml.dump({
938
+ "workflow": {
939
+ "phases": [
940
+ {"name": "green", "agent": "dev"},
941
+ ]
942
+ }
943
+ }))
944
+
945
+ session_dir = tmp_path / ".session"
946
+ session_dir.mkdir()
947
+ (session_dir / "test-session.md").write_text("""# Test
948
+
949
+ - **Workflow:** tdd
950
+ - **Current Phase:** green
951
+ """)
952
+
953
+ # Test - activating TEA when DEV owns the phase
954
+ result = prime(
955
+ agent_name="tea",
956
+ json_output=True,
957
+ no_persona=True,
958
+ no_register=True,
959
+ project_root=tmp_path,
960
+ )
961
+
962
+ assert result == 0
963
+ captured = capsys.readouterr()
964
+ data = json.loads(captured.out)
965
+
966
+ assert data["redirect_to"] == "dev"
967
+ assert "green" in data["redirect_reason"]
968
+
969
+
970
+ # =============================================================================
971
+ # Prime v2 Tests - CLI Flags
972
+ # =============================================================================
973
+
974
+
975
+ class TestCLIFlagsV2:
976
+ """Tests for new CLI flags in Prime v2."""
977
+
978
+ def test_json_flag(self, tmp_path: Path, capsys) -> None:
979
+ """Test --json flag."""
980
+ import json
981
+
982
+ pf_dir = tmp_path / ".pennyfarthing"
983
+ pf_dir.mkdir()
984
+
985
+ with patch("pennyfarthing_scripts.prime.cli.get_project_root", return_value=tmp_path):
986
+ result = main(["--json", "--no-workflow", "--no-register"])
987
+
988
+ assert result == 0
989
+ captured = capsys.readouterr()
990
+ # Should be valid JSON
991
+ data = json.loads(captured.out)
992
+ assert "agent_name" in data
993
+
994
+ def test_no_persona_flag(self, tmp_path: Path, capsys) -> None:
995
+ """Test --no-persona flag skips persona loading."""
996
+ import yaml
997
+
998
+ # Setup
999
+ pf_dir = tmp_path / ".pennyfarthing"
1000
+ pf_dir.mkdir()
1001
+ agents_dir = pf_dir / "agents"
1002
+ agents_dir.mkdir()
1003
+ (agents_dir / "dev.md").write_text("# Dev Agent")
1004
+
1005
+ # Create config and theme that would normally load
1006
+ (pf_dir / "config.local.yaml").write_text(yaml.dump({"theme": "test"}))
1007
+ themes_dir = pf_dir / "personas" / "themes"
1008
+ themes_dir.mkdir(parents=True)
1009
+ (themes_dir / "test.yaml").write_text(yaml.dump({
1010
+ "agents": {"dev": {"character": "Test", "style": "s", "role": "r"}}
1011
+ }))
1012
+
1013
+ with patch("pennyfarthing_scripts.prime.cli.get_project_root", return_value=tmp_path):
1014
+ result = main(["--agent", "dev", "--no-persona", "--no-workflow", "--no-register"])
1015
+
1016
+ assert result == 0
1017
+ captured = capsys.readouterr()
1018
+ # Should NOT contain persona XML
1019
+ assert "<persona" not in captured.out
1020
+ # Should contain agent definition
1021
+ assert "# Dev Agent" in captured.out
1022
+
1023
+ def test_no_workflow_flag(self, tmp_path: Path, capsys) -> None:
1024
+ """Test --no-workflow flag skips workflow detection."""
1025
+ pf_dir = tmp_path / ".pennyfarthing"
1026
+ pf_dir.mkdir()
1027
+
1028
+ with patch("pennyfarthing_scripts.prime.cli.get_project_root", return_value=tmp_path):
1029
+ result = main(["--no-workflow", "--no-register"])
1030
+
1031
+ assert result == 0
1032
+ captured = capsys.readouterr()
1033
+ # Should NOT contain workflow state header
1034
+ assert "# Workflow State" not in captured.out
1035
+
1036
+ def test_session_id_flag(self, tmp_path: Path) -> None:
1037
+ """Test --session-id flag uses explicit ID."""
1038
+ from pennyfarthing_scripts.prime.session import get_session_agent
1039
+
1040
+ pf_dir = tmp_path / ".pennyfarthing"
1041
+ pf_dir.mkdir()
1042
+
1043
+ with patch("pennyfarthing_scripts.prime.cli.get_project_root", return_value=tmp_path):
1044
+ result = main(["--agent", "sm", "--session-id", "explicit-123", "--no-workflow"])
1045
+
1046
+ assert result == 0
1047
+
1048
+ # Verify session was created with explicit ID
1049
+ agent = get_session_agent("explicit-123", tmp_path)
1050
+ assert agent == "sm"