powershell woes
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"project": "GP Clinical Record — Depth Enhancement",
|
"project": "GP Clinical Record — Depth Enhancement",
|
||||||
"branchName": "ralph/depth-enhancement",
|
"branchName": "ralph/depth-enhancement",
|
||||||
"description": "Add depth, interactivity, and rich content to the GP clinical record dashboard: slide-in detail panels, sub-navigation, expanded skills/KPI data, career constellation D3 visualization, and login refresh. Full spec in Ralph/depth-design.md, requirements in Ralph/depth-requirements.md, workflow in Ralph/workflow_depth.md.",
|
"description": "Add depth, interactivity, and rich content to the GP clinical record dashboard: slide-in detail panels, sub-navigation, expanded skills/KPI data, career constellation D3 visualization, and login refresh. Full spec in Ralph/depth-design.md, requirements in Ralph/depth-requirements.md, workflow in Ralph/workflow_depth.md.",
|
||||||
"userStories": [
|
"userStories": [
|
||||||
@@ -23,18 +23,18 @@
|
|||||||
"Typecheck passes"
|
"Typecheck passes"
|
||||||
],
|
],
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 1 at 2026-02-13 22:57. Model: opus."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-002",
|
"id": "US-002",
|
||||||
"title": "Add new TypeScript types and CSS custom properties for depth features",
|
"title": "Add new TypeScript types and CSS custom properties for depth features",
|
||||||
"description": "As a developer, I need new types and CSS foundations that subsequent stories will use. Add types to src/types/pmr.ts and CSS variables + keyframes to src/index.css. See Ralph/depth-design.md Section 4 for type definitions and Section 9 for CSS.",
|
"description": "As a developer, I need new types and CSS foundations that subsequent stories will use. Add types to src/types/pmr.ts and CSS variables + keyframes to src/index.css. See Ralph/depth-design.md Section 4 for type definitions and Section 9 for CSS.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Add SkillCategory type: 'Technical' | 'Domain' | 'Leadership' to src/types/pmr.ts",
|
"Add SkillCategory type: \u0027Technical\u0027 | \u0027Domain\u0027 | \u0027Leadership\u0027 to src/types/pmr.ts",
|
||||||
"Add KPIStory interface with fields: context (string), role (string), outcomes (string[]), period (string optional) to src/types/pmr.ts",
|
"Add KPIStory interface with fields: context (string), role (string), outcomes (string[]), period (string optional) to src/types/pmr.ts",
|
||||||
"Add optional story?: KPIStory field to existing KPI interface in src/types/pmr.ts",
|
"Add optional story?: KPIStory field to existing KPI interface in src/types/pmr.ts",
|
||||||
"Add ConstellationNode interface (id, type: 'role'|'skill', label, shortLabel?, organization?, startYear?, endYear?, orgColor?, domain?) to src/types/pmr.ts",
|
"Add ConstellationNode interface (id, type: \u0027role\u0027|\u0027skill\u0027, label, shortLabel?, organization?, startYear?, endYear?, orgColor?, domain?) to src/types/pmr.ts",
|
||||||
"Add ConstellationLink interface (source, target, strength) to src/types/pmr.ts",
|
"Add ConstellationLink interface (source, target, strength) to src/types/pmr.ts",
|
||||||
"Add DetailPanelContent discriminated union type (kpi | skill | skills-all | consultation | project | education | career-role) to src/types/pmr.ts",
|
"Add DetailPanelContent discriminated union type (kpi | skill | skills-all | consultation | project | education | career-role) to src/types/pmr.ts",
|
||||||
"Add EducationExtra interface (documentId, extracurriculars?, researchDescription?, programmeDetail?) to src/types/pmr.ts",
|
"Add EducationExtra interface (documentId, extracurriculars?, researchDescription?, programmeDetail?) to src/types/pmr.ts",
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
"Typecheck passes"
|
"Typecheck passes"
|
||||||
],
|
],
|
||||||
"priority": 2,
|
"priority": 2,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 2 at 2026-02-13 22:59. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-003",
|
"id": "US-003",
|
||||||
@@ -53,14 +53,14 @@
|
|||||||
"description": "As a developer, I need the core detail panel infrastructure: a context for managing panel state, the slide-in panel component, and a focus trap hook. Create 3 new files. The panel renders placeholder content for now (real renderers come later). See Ralph/depth-design.md Sections 2.1, 2.2 for full interface specs.",
|
"description": "As a developer, I need the core detail panel infrastructure: a context for managing panel state, the slide-in panel component, and a focus trap hook. Create 3 new files. The panel renders placeholder content for now (real renderers come later). See Ralph/depth-design.md Sections 2.1, 2.2 for full interface specs.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Create src/contexts/DetailPanelContext.tsx with DetailPanelProvider that manages: content (DetailPanelContent | null), openPanel, closePanel, isOpen",
|
"Create src/contexts/DetailPanelContext.tsx with DetailPanelProvider that manages: content (DetailPanelContent | null), openPanel, closePanel, isOpen",
|
||||||
"Width mapping is deterministic from content.type: kpi/skill/skills-all/education → 'narrow' (var(--panel-narrow)), consultation/project/career-role → 'wide' (var(--panel-wide))",
|
"Width mapping is deterministic from content.type: kpi/skill/skills-all/education → \u0027narrow\u0027 (var(--panel-narrow)), consultation/project/career-role → \u0027wide\u0027 (var(--panel-wide))",
|
||||||
"Title mapping derives from content data (e.g., kpi → kpi.label, skill → skill.name, consultation → consultation.role)",
|
"Title mapping derives from content data (e.g., kpi → kpi.label, skill → skill.name, consultation → consultation.role)",
|
||||||
"Create src/components/DetailPanel.tsx: full-screen backdrop (var(--backdrop-bg) + backdrop-filter: blur(var(--backdrop-blur))) with panel sliding from right",
|
"Create src/components/DetailPanel.tsx: full-screen backdrop (var(--backdrop-bg) + backdrop-filter: blur(var(--backdrop-blur))) with panel sliding from right",
|
||||||
"Panel has header with X close button (lucide X icon), colored dot matching tile, and title text",
|
"Panel has header with X close button (lucide X icon), colored dot matching tile, and title text",
|
||||||
"Panel body is scrollable and renders placeholder text showing content type",
|
"Panel body is scrollable and renders placeholder text showing content type",
|
||||||
"Close triggers: backdrop click, Escape key, X button",
|
"Close triggers: backdrop click, Escape key, X button",
|
||||||
"ARIA: aria-modal=true, role=dialog, aria-labelledby pointing to title",
|
"ARIA: aria-modal=true, role=dialog, aria-labelledby pointing to title",
|
||||||
"Mobile (<768px): both narrow and wide become 100vw",
|
"Mobile (\u003c768px): both narrow and wide become 100vw",
|
||||||
"prefers-reduced-motion: instant appear, no slide animation",
|
"prefers-reduced-motion: instant appear, no slide animation",
|
||||||
"Create src/hooks/useFocusTrap.ts: useFocusTrap(containerRef, isActive) traps Tab/Shift+Tab within container when active, returns focus to previous element when deactivated",
|
"Create src/hooks/useFocusTrap.ts: useFocusTrap(containerRef, isActive) traps Tab/Shift+Tab within container when active, returns focus to previous element when deactivated",
|
||||||
"DetailPanel uses useFocusTrap when open",
|
"DetailPanel uses useFocusTrap when open",
|
||||||
@@ -68,8 +68,8 @@
|
|||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 3,
|
"priority": 3,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 3 at 2026-02-13 23:03. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-004",
|
"id": "US-004",
|
||||||
@@ -84,14 +84,14 @@
|
|||||||
"Inactive tabs: var(--text-secondary)",
|
"Inactive tabs: var(--text-secondary)",
|
||||||
"Click scrolls smoothly to [data-tile-id=tileId] element",
|
"Click scrolls smoothly to [data-tile-id=tileId] element",
|
||||||
"Create src/hooks/useActiveSection.ts using IntersectionObserver to track visible tile by data-tile-id attribute",
|
"Create src/hooks/useActiveSection.ts using IntersectionObserver to track visible tile by data-tile-id attribute",
|
||||||
"Maps tile IDs to section IDs: patient-summary→overview, core-skills→skills, career-activity→experience, projects→projects, education→education",
|
"Maps tile IDs to section IDs: patient-summary→overview, core-skills→skills, career-activity→experience, projects→projects, education→education",
|
||||||
"SubNav accepts activeSection and onSectionClick props",
|
"SubNav accepts activeSection and onSectionClick props",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 4,
|
"priority": 4,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 4 at 2026-02-13 23:06. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-005",
|
"id": "US-005",
|
||||||
@@ -101,32 +101,32 @@
|
|||||||
"src/data/skills.ts has ~21 SkillMedication entries",
|
"src/data/skills.ts has ~21 SkillMedication entries",
|
||||||
"Technical category (8): Data Analysis, Python, SQL, Power BI, JavaScript/TypeScript, Excel, Algorithm Design, Data Pipelines",
|
"Technical category (8): Data Analysis, Python, SQL, Power BI, JavaScript/TypeScript, Excel, Algorithm Design, Data Pipelines",
|
||||||
"Healthcare Domain category (6): Medicines Optimisation, Population Health, NICE TA Implementation, Health Economics, Clinical Pathways, Controlled Drugs",
|
"Healthcare Domain category (6): Medicines Optimisation, Population Health, NICE TA Implementation, Health Economics, Clinical Pathways, Controlled Drugs",
|
||||||
"Strategic & Leadership category (7): Budget Management, Stakeholder Engagement, Pharmaceutical Negotiation, Team Development, Change Management, Financial Modelling, Executive Communication",
|
"Strategic \u0026 Leadership category (7): Budget Management, Stakeholder Engagement, Pharmaceutical Negotiation, Team Development, Change Management, Financial Modelling, Executive Communication",
|
||||||
"Each skill has: id (kebab-case), name, frequency (medication-style: Daily, Twice daily, Once weekly, When required, etc.), startYear, yearsOfExperience, proficiency (0-100), category, status (Active/Historical), icon (lucide icon name)",
|
"Each skill has: id (kebab-case), name, frequency (medication-style: Daily, Twice daily, Once weekly, When required, etc.), startYear, yearsOfExperience, proficiency (0-100), category, status (Active/Historical), icon (lucide icon name)",
|
||||||
"Frequency and proficiency values are realistic based on CV_v4.md role descriptions",
|
"Frequency and proficiency values are realistic based on CV_v4.md role descriptions",
|
||||||
"Typecheck passes"
|
"Typecheck passes"
|
||||||
],
|
],
|
||||||
"priority": 5,
|
"priority": 5,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 5 at 2026-02-13 23:08. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-006",
|
"id": "US-006",
|
||||||
"title": "Add KPI story data and update 4th KPI",
|
"title": "Add KPI story data and update 4th KPI",
|
||||||
"description": "As a developer, I need to add rich story content to each KPI in src/data/kpis.ts for the detail panel, and change the 4th KPI from '12 Team Size Led' to '1.2M Population served'. Source from References/CV_v4.md. See Ralph/depth-design.md Section 5.2.",
|
"description": "As a developer, I need to add rich story content to each KPI in src/data/kpis.ts for the detail panel, and change the 4th KPI from \u002712 Team Size Led\u0027 to \u00271.2M Population served\u0027. Source from References/CV_v4.md. See Ralph/depth-design.md Section 5.2.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Change 4th KPI from {id:'team', value:'12', label:'Team Size Led'} to {id:'population', value:'1.2M', label:'Population Served', sub:'Norfolk & Waveney ICS', colorVariant:'teal'}",
|
"Change 4th KPI from {id:\u0027team\u0027, value:\u002712\u0027, label:\u0027Team Size Led\u0027} to {id:\u0027population\u0027, value:\u00271.2M\u0027, label:\u0027Population Served\u0027, sub:\u0027Norfolk \u0026 Waveney ICS\u0027, colorVariant:\u0027teal\u0027}",
|
||||||
"Add story field (KPIStory) to all 4 KPIs with: context, role, outcomes[], period",
|
"Add story field (KPIStory) to all 4 KPIs with: context, role, outcomes[], period",
|
||||||
"£220M story: context about ICB prescribing budget for 1.2M population, role about forecasting models and ICB board accountability, outcomes about proactive financial planning",
|
"£220M story: context about ICB prescribing budget for 1.2M population, role about forecasting models and ICB board accountability, outcomes about proactive financial planning",
|
||||||
"£14.6M story: context about efficiency programme, role about data analysis identification, outcomes about over-target performance",
|
"£14.6M story: context about efficiency programme, role about data analysis identification, outcomes about over-target performance",
|
||||||
"9+ Years story: context about career span Aug 2016-present, role about progression from community pharmacy to system-level leadership",
|
"9+ Years story: context about career span Aug 2016-present, role about progression from community pharmacy to system-level leadership",
|
||||||
"1.2M story: context about Norfolk & Waveney ICS population, role about population health analytics and data-driven decision making",
|
"1.2M story: context about Norfolk \u0026 Waveney ICS population, role about population health analytics and data-driven decision making",
|
||||||
"Add explanation field to 4th KPI matching the story context",
|
"Add explanation field to 4th KPI matching the story context",
|
||||||
"Typecheck passes"
|
"Typecheck passes"
|
||||||
],
|
],
|
||||||
"priority": 6,
|
"priority": 6,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 6 at 2026-02-13 23:10. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-007",
|
"id": "US-007",
|
||||||
@@ -134,14 +134,14 @@
|
|||||||
"description": "As a developer, I need src/data/educationExtras.ts with expanded detail for the education detail panel. Source from References/CV_v4.md Education section. See Ralph/depth-design.md Section 5.4.",
|
"description": "As a developer, I need src/data/educationExtras.ts with expanded detail for the education detail panel. Source from References/CV_v4.md Education section. See Ralph/depth-design.md Section 5.4.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Create src/data/educationExtras.ts exporting educationExtras array of EducationExtra objects",
|
"Create src/data/educationExtras.ts exporting educationExtras array of EducationExtra objects",
|
||||||
"MPharm entry (documentId matching doc-mpharm or equivalent from documents.ts): extracurriculars ['President of UEA Pharmacy Society', 'Secretary & Vice-President of UEA Ultimate Frisbee', 'Publicity Officer for UEA Alzheimer\\'s Society'], researchDescription about cocrystal formation for drug delivery",
|
"MPharm entry (documentId matching doc-mpharm or equivalent from documents.ts): extracurriculars [\u0027President of UEA Pharmacy Society\u0027, \u0027Secretary \u0026 Vice-President of UEA Ultimate Frisbee\u0027, \u0027Publicity Officer for UEA Alzheimer\\\u0027s Society\u0027], researchDescription about cocrystal formation for drug delivery",
|
||||||
"Mary Seacole entry: programmeDetail about NHS leadership qualification, change management, healthcare leadership, system-level thinking",
|
"Mary Seacole entry: programmeDetail about NHS leadership qualification, change management, healthcare leadership, system-level thinking",
|
||||||
"Document IDs match those used in src/data/documents.ts",
|
"Document IDs match those used in src/data/documents.ts",
|
||||||
"Typecheck passes"
|
"Typecheck passes"
|
||||||
],
|
],
|
||||||
"priority": 7,
|
"priority": 7,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 7 at 2026-02-13 23:11. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-008",
|
"id": "US-008",
|
||||||
@@ -159,8 +159,8 @@
|
|||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 8,
|
"priority": 8,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 8 at 2026-02-13 23:15. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-009",
|
"id": "US-009",
|
||||||
@@ -176,8 +176,8 @@
|
|||||||
"Typecheck passes"
|
"Typecheck passes"
|
||||||
],
|
],
|
||||||
"priority": 9,
|
"priority": 9,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 9 at 2026-02-13 23:17. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-010",
|
"id": "US-010",
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
"Each KPI renders as a clickable button/card with: value at 28-32px font-size, weight 700, colored by kpi.colorVariant",
|
"Each KPI renders as a clickable button/card with: value at 28-32px font-size, weight 700, colored by kpi.colorVariant",
|
||||||
"Label at 12px, weight 500, color var(--text-primary), marginTop 4px",
|
"Label at 12px, weight 500, color var(--text-primary), marginTop 4px",
|
||||||
"Sub-text at 10px, font-family var(--font-geist-mono), color var(--text-tertiary), marginTop 2px",
|
"Sub-text at 10px, font-family var(--font-geist-mono), color var(--text-tertiary), marginTop 2px",
|
||||||
"Click calls openPanel({ type: 'kpi', kpi }) from DetailPanelContext",
|
"Click calls openPanel({ type: \u0027kpi\u0027, kpi }) from DetailPanelContext",
|
||||||
"Hover: border color shift + shadow deepens (transition 150ms)",
|
"Hover: border color shift + shadow deepens (transition 150ms)",
|
||||||
"Keyboard: Enter/Space triggers panel open",
|
"Keyboard: Enter/Space triggers panel open",
|
||||||
"Card styling: padding 16px, background var(--surface), border 1px solid var(--border-light), border-radius var(--radius-sm)",
|
"Card styling: padding 16px, background var(--surface), border 1px solid var(--border-light), border-radius var(--radius-sm)",
|
||||||
@@ -196,28 +196,28 @@
|
|||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 10 at 2026-02-13. Model: opus. Manually marked passed (script hung after story-complete signal)."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-011",
|
"id": "US-011",
|
||||||
"title": "Modify CoreSkillsTile: full width, categorised groups, panel triggers",
|
"title": "Modify CoreSkillsTile: full width, categorised groups, panel triggers",
|
||||||
"description": "As a developer, I need to redesign CoreSkillsTile.tsx as full-width with skills grouped by 3 categories, showing top 3-4 per category with 'view all' buttons. Individual skills and 'view all' trigger the detail panel. See Ralph/depth-design.md Section 3.4.",
|
"description": "As a developer, I need to redesign CoreSkillsTile.tsx as full-width with skills grouped by 3 categories, showing top 3-4 per category with \u0027view all\u0027 buttons. Individual skills and \u0027view all\u0027 trigger the detail panel. See Ralph/depth-design.md Section 3.4.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Card uses full prop (spans both grid columns)",
|
"Card uses full prop (spans both grid columns)",
|
||||||
"Skills grouped by category: Technical, Healthcare Domain (Domain), Strategic & Leadership (Leadership)",
|
"Skills grouped by category: Technical, Healthcare Domain (Domain), Strategic \u0026 Leadership (Leadership)",
|
||||||
"Each category has a header: thin divider line with category label (styled like sidebar section dividers: 10px, uppercase, var(--text-tertiary))",
|
"Each category has a header: thin divider line with category label (styled like sidebar section dividers: 10px, uppercase, var(--text-tertiary))",
|
||||||
"Show top 3-4 skills per category on the dashboard tile (sorted by proficiency or relevance)",
|
"Show top 3-4 skills per category on the dashboard tile (sorted by proficiency or relevance)",
|
||||||
"Each skill row is clickable → openPanel({ type: 'skill', skill }) from DetailPanelContext",
|
"Each skill row is clickable → openPanel({ type: \u0027skill\u0027, skill }) from DetailPanelContext",
|
||||||
"Each category with >4 skills shows a 'View all (N)' button → openPanel({ type: 'skills-all', category })",
|
"Each category with \u003e4 skills shows a \u0027View all (N)\u0027 button → openPanel({ type: \u0027skills-all\u0027, category })",
|
||||||
"Retain medication metaphor display (frequency, status badge)",
|
"Retain medication metaphor display (frequency, status badge)",
|
||||||
"Remove old single-expand accordion for skills (replaced by panel)",
|
"Remove old single-expand accordion for skills (replaced by panel)",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 11,
|
"priority": 11,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 1 at 2026-02-13 23:50. Model: opus."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-012",
|
"id": "US-012",
|
||||||
@@ -227,23 +227,23 @@
|
|||||||
"Remove full prop from Card (half-width, single grid column)",
|
"Remove full prop from Card (half-width, single grid column)",
|
||||||
"Compact project cards: status dot + name + year (right-aligned) per row",
|
"Compact project cards: status dot + name + year (right-aligned) per row",
|
||||||
"Tech stack shown as small inline tags",
|
"Tech stack shown as small inline tags",
|
||||||
"Each project card clickable → openPanel({ type: 'project', investigation }) from DetailPanelContext",
|
"Each project card clickable → openPanel({ type: \u0027project\u0027, investigation }) from DetailPanelContext",
|
||||||
"Remove old in-place expansion (replaced by panel)",
|
"Remove old in-place expansion (replaced by panel)",
|
||||||
"Hover: border color shift, shadow deepens",
|
"Hover: border color shift, shadow deepens",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 12,
|
"priority": 12,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 2 at 2026-02-13 23:52. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-013",
|
"id": "US-013",
|
||||||
"title": "Modify LastConsultationTile: add panel trigger",
|
"title": "Modify LastConsultationTile: add panel trigger",
|
||||||
"description": "As a developer, I need to add a 'View full record' button to LastConsultationTile.tsx that opens the detail panel with full role details. See Ralph/depth-design.md Section 3.9.",
|
"description": "As a developer, I need to add a \u0027View full record\u0027 button to LastConsultationTile.tsx that opens the detail panel with full role details. See Ralph/depth-design.md Section 3.9.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Add 'View full record' link/button at the bottom of the tile",
|
"Add \u0027View full record\u0027 link/button at the bottom of the tile",
|
||||||
"Click → openPanel({ type: 'consultation', consultation }) from DetailPanelContext, passing the first consultation entry",
|
"Click → openPanel({ type: \u0027consultation\u0027, consultation }) from DetailPanelContext, passing the first consultation entry",
|
||||||
"Make the tile header area also clickable (opens same panel)",
|
"Make the tile header area also clickable (opens same panel)",
|
||||||
"Keep existing inline content (header info row, achievement bullets)",
|
"Keep existing inline content (header info row, achievement bullets)",
|
||||||
"Hover state on clickable areas",
|
"Hover state on clickable areas",
|
||||||
@@ -251,15 +251,15 @@
|
|||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 13,
|
"priority": 13,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 3 at 2026-02-13 23:55. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-014",
|
"id": "US-014",
|
||||||
"title": "Modify CareerActivityTile: panel triggers and hover preview",
|
"title": "Modify CareerActivityTile: panel triggers and hover preview",
|
||||||
"description": "As a developer, I need to change CareerActivityTile.tsx so timeline items click to open the detail panel instead of expanding in-place, and add hover previews. See Ralph/depth-design.md Section 3.7.",
|
"description": "As a developer, I need to change CareerActivityTile.tsx so timeline items click to open the detail panel instead of expanding in-place, and add hover previews. See Ralph/depth-design.md Section 3.7.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Role timeline items click → openPanel({ type: 'career-role', consultation }) from DetailPanelContext",
|
"Role timeline items click → openPanel({ type: \u0027career-role\u0027, consultation }) from DetailPanelContext",
|
||||||
"Remove in-place accordion expansion for career items (replaced by panel)",
|
"Remove in-place accordion expansion for career items (replaced by panel)",
|
||||||
"Hover preview: items lift slightly on hover with shadow deepens, show 1-2 lines of preview text",
|
"Hover preview: items lift slightly on hover with shadow deepens, show 1-2 lines of preview text",
|
||||||
"Keep color-coded dots and entry type styling (teal roles, amber projects, green certs, purple education)",
|
"Keep color-coded dots and entry type styling (teal roles, amber projects, green certs, purple education)",
|
||||||
@@ -268,8 +268,8 @@
|
|||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 14,
|
"priority": 14,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 4 at 2026-02-13 23:58. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-015",
|
"id": "US-015",
|
||||||
@@ -277,15 +277,15 @@
|
|||||||
"description": "As a developer, I need to enhance EducationTile.tsx with richer inline content and click-to-panel interaction. See Ralph/depth-design.md Section 3.8.",
|
"description": "As a developer, I need to enhance EducationTile.tsx with richer inline content and click-to-panel interaction. See Ralph/depth-design.md Section 3.8.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Show richer inline content: MPharm research project score (75.1%), OSCE score (80%), A-level grades (A* Maths, B Chemistry, C Politics)",
|
"Show richer inline content: MPharm research project score (75.1%), OSCE score (80%), A-level grades (A* Maths, B Chemistry, C Politics)",
|
||||||
"Each education entry is clickable → openPanel({ type: 'education', document }) from DetailPanelContext",
|
"Each education entry is clickable → openPanel({ type: \u0027education\u0027, document }) from DetailPanelContext",
|
||||||
"Hover: border color shift on clickable entries",
|
"Hover: border color shift on clickable entries",
|
||||||
"Use education extras data from src/data/educationExtras.ts for inline detail where appropriate",
|
"Use education extras data from src/data/educationExtras.ts for inline detail where appropriate",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 15,
|
"priority": 15,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed iteration 4 at 2026-02-14 00:33. Model: sonnet."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-016",
|
"id": "US-016",
|
||||||
@@ -293,40 +293,40 @@
|
|||||||
"description": "As a developer, I need to improve PatientSummaryTile.tsx with the full CV_v4.md profile text and a visual highlight strip. See Ralph/depth-design.md Section 3.10 and Ralph/depth-requirements.md Section 4.1.",
|
"description": "As a developer, I need to improve PatientSummaryTile.tsx with the full CV_v4.md profile text and a visual highlight strip. See Ralph/depth-design.md Section 3.10 and Ralph/depth-requirements.md Section 4.1.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Verify src/data/profile.ts has the complete profile text from References/CV_v4.md (update if needed)",
|
"Verify src/data/profile.ts has the complete profile text from References/CV_v4.md (update if needed)",
|
||||||
"Add a visual highlight strip showing key stats: e.g. '9+ Years Experience', '1.2M Population', '£220M Budget' as small styled badges or pills",
|
"Add a visual highlight strip showing key stats: e.g. \u00279+ Years Experience\u0027, \u00271.2M Population\u0027, \u0027£220M Budget\u0027 as small styled badges or pills",
|
||||||
"Profile text is not a wall of text — use hierarchy: bold key phrases, structured paragraphs if needed",
|
"Profile text is not a wall of text — use hierarchy: bold key phrases, structured paragraphs if needed",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 16,
|
"priority": 16,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-017",
|
"id": "US-017",
|
||||||
"title": "Create KPIDetail renderer for detail panel",
|
"title": "Create KPIDetail renderer for detail panel",
|
||||||
"description": "As a developer, I need src/components/detail/KPIDetail.tsx that renders rich KPI story content inside the detail panel. Wire it into DetailPanel so content.type === 'kpi' renders this component. See Ralph/depth-design.md Section 6.1.",
|
"description": "As a developer, I need src/components/detail/KPIDetail.tsx that renders rich KPI story content inside the detail panel. Wire it into DetailPanel so content.type === \u0027kpi\u0027 renders this component. See Ralph/depth-design.md Section 6.1.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Create src/components/detail/KPIDetail.tsx accepting a KPI prop",
|
"Create src/components/detail/KPIDetail.tsx accepting a KPI prop",
|
||||||
"Renders: headline number (large, colored by kpi.colorVariant), context paragraph (story.context), 'Your role' paragraph (story.role), outcome bullets (story.outcomes), period badge (story.period)",
|
"Renders: headline number (large, colored by kpi.colorVariant), context paragraph (story.context), \u0027Your role\u0027 paragraph (story.role), outcome bullets (story.outcomes), period badge (story.period)",
|
||||||
"Graceful fallback if story is undefined (show kpi.explanation instead)",
|
"Graceful fallback if story is undefined (show kpi.explanation instead)",
|
||||||
"Wire into DetailPanel: when content.type === 'kpi', render <KPIDetail kpi={content.kpi} />",
|
"Wire into DetailPanel: when content.type === \u0027kpi\u0027, render \u003cKPIDetail kpi={content.kpi} /\u003e",
|
||||||
"Styling matches dashboard design system (fonts, colors, spacing)",
|
"Styling matches dashboard design system (fonts, colors, spacing)",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 17,
|
"priority": 17,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-018",
|
"id": "US-018",
|
||||||
"title": "Create ConsultationDetail renderer for detail panel",
|
"title": "Create ConsultationDetail renderer for detail panel",
|
||||||
"description": "As a developer, I need src/components/detail/ConsultationDetail.tsx for displaying full role details in the detail panel. Used for both 'consultation' and 'career-role' content types. See Ralph/depth-design.md Section 6.4.",
|
"description": "As a developer, I need src/components/detail/ConsultationDetail.tsx for displaying full role details in the detail panel. Used for both \u0027consultation\u0027 and \u0027career-role\u0027 content types. See Ralph/depth-design.md Section 6.4.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Create src/components/detail/ConsultationDetail.tsx accepting a Consultation prop",
|
"Create src/components/detail/ConsultationDetail.tsx accepting a Consultation prop",
|
||||||
"Renders: role title + organization + dates, history paragraph (consultation.history), achievement bullets (consultation.examination), plan/outcomes (consultation.plan), coded entries as badges (consultation.codedEntries)",
|
"Renders: role title + organization + dates, history paragraph (consultation.history), achievement bullets (consultation.examination), plan/outcomes (consultation.plan), coded entries as badges (consultation.codedEntries)",
|
||||||
"Wire into DetailPanel: content.type === 'consultation' or 'career-role' renders this component",
|
"Wire into DetailPanel: content.type === \u0027consultation\u0027 or \u0027career-role\u0027 renders this component",
|
||||||
"Styled consistently with dashboard design system",
|
"Styled consistently with dashboard design system",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
@@ -342,13 +342,13 @@
|
|||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Create src/components/detail/ProjectDetail.tsx accepting an Investigation prop",
|
"Create src/components/detail/ProjectDetail.tsx accepting an Investigation prop",
|
||||||
"Renders: project name + year + status badge, methodology description, tech stack as tags, results bullets, external link button (if investigation.externalUrl exists, opens in new tab)",
|
"Renders: project name + year + status badge, methodology description, tech stack as tags, results bullets, external link button (if investigation.externalUrl exists, opens in new tab)",
|
||||||
"Wire into DetailPanel: content.type === 'project' renders this component",
|
"Wire into DetailPanel: content.type === \u0027project\u0027 renders this component",
|
||||||
"External link uses rel='noopener noreferrer'",
|
"External link uses rel=\u0027noopener noreferrer\u0027",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 19,
|
"priority": 19,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -358,8 +358,8 @@
|
|||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Create src/components/detail/SkillDetail.tsx accepting a SkillMedication prop",
|
"Create src/components/detail/SkillDetail.tsx accepting a SkillMedication prop",
|
||||||
"Renders: skill name + frequency + status badge, visual proficiency bar (0-100%), years of experience, category label",
|
"Renders: skill name + frequency + status badge, visual proficiency bar (0-100%), years of experience, category label",
|
||||||
"If constellation data is available, show 'Used in' section listing roles that used this skill (import from src/data/constellation.ts)",
|
"If constellation data is available, show \u0027Used in\u0027 section listing roles that used this skill (import from src/data/constellation.ts)",
|
||||||
"Wire into DetailPanel: content.type === 'skill' renders this component",
|
"Wire into DetailPanel: content.type === \u0027skill\u0027 renders this component",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
@@ -373,11 +373,11 @@
|
|||||||
"description": "As a developer, I need src/components/detail/SkillsAllDetail.tsx showing the full categorised list of all skills. Clicking an individual skill switches the panel to SkillDetail. See Ralph/depth-design.md Section 6.3.",
|
"description": "As a developer, I need src/components/detail/SkillsAllDetail.tsx showing the full categorised list of all skills. Clicking an individual skill switches the panel to SkillDetail. See Ralph/depth-design.md Section 6.3.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Create src/components/detail/SkillsAllDetail.tsx",
|
"Create src/components/detail/SkillsAllDetail.tsx",
|
||||||
"Shows full list grouped by Technical / Healthcare Domain / Strategic & Leadership",
|
"Shows full list grouped by Technical / Healthcare Domain / Strategic \u0026 Leadership",
|
||||||
"Category headers styled consistently with CoreSkillsTile category headers",
|
"Category headers styled consistently with CoreSkillsTile category headers",
|
||||||
"Each skill row is clickable → calls openPanel({ type: 'skill', skill }) to switch panel content",
|
"Each skill row is clickable → calls openPanel({ type: \u0027skill\u0027, skill }) to switch panel content",
|
||||||
"If opened with a category filter (content.category), scroll to or highlight that category",
|
"If opened with a category filter (content.category), scroll to or highlight that category",
|
||||||
"Wire into DetailPanel: content.type === 'skills-all' renders this component",
|
"Wire into DetailPanel: content.type === \u0027skills-all\u0027 renders this component",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
@@ -396,7 +396,7 @@
|
|||||||
"If MPharm: shows research project description, extracurricular activities list",
|
"If MPharm: shows research project description, extracurricular activities list",
|
||||||
"If Mary Seacole: shows programme detail",
|
"If Mary Seacole: shows programme detail",
|
||||||
"Shows notes from document data if present",
|
"Shows notes from document data if present",
|
||||||
"Wire into DetailPanel: content.type === 'education' renders this component",
|
"Wire into DetailPanel: content.type === \u0027education\u0027 renders this component",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
@@ -411,7 +411,7 @@
|
|||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Run npm install d3 @types/d3",
|
"Run npm install d3 @types/d3",
|
||||||
"Create src/components/CareerConstellation.tsx with props: onRoleClick(id: string), onSkillClick(id: string)",
|
"Create src/components/CareerConstellation.tsx with props: onRoleClick(id: string), onSkillClick(id: string)",
|
||||||
"Component renders a responsive SVG container using useRef<SVGSVGElement>",
|
"Component renders a responsive SVG container using useRef\u003cSVGSVGElement\u003e",
|
||||||
"Container: full width, height 400px desktop / 300px tablet / 250px mobile (use CSS or media queries)",
|
"Container: full width, height 400px desktop / 300px tablet / 250px mobile (use CSS or media queries)",
|
||||||
"SVG has viewBox for responsive scaling",
|
"SVG has viewBox for responsive scaling",
|
||||||
"Import constellation data from src/data/constellation.ts",
|
"Import constellation data from src/data/constellation.ts",
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
"title": "Add accessibility to CareerConstellation",
|
"title": "Add accessibility to CareerConstellation",
|
||||||
"description": "As a developer, I need the CareerConstellation to be accessible: keyboard navigable, screen-reader friendly, and respecting reduced motion. See Ralph/depth-design.md Section 2.4 accessibility notes.",
|
"description": "As a developer, I need the CareerConstellation to be accessible: keyboard navigable, screen-reader friendly, and respecting reduced motion. See Ralph/depth-design.md Section 2.4 accessibility notes.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"SVG has role=img and aria-label describing the graph ('Career constellation showing roles and skills across career timeline')",
|
"SVG has role=img and aria-label describing the graph (\u0027Career constellation showing roles and skills across career timeline\u0027)",
|
||||||
"Screen-reader-only text description of graph structure (hidden visually, available to assistive tech)",
|
"Screen-reader-only text description of graph structure (hidden visually, available to assistive tech)",
|
||||||
"Keyboard navigation: Tab through role nodes, Enter/Space opens detail panel for focused node",
|
"Keyboard navigation: Tab through role nodes, Enter/Space opens detail panel for focused node",
|
||||||
"Focus indicators visible on keyboard-focused nodes",
|
"Focus indicators visible on keyboard-focused nodes",
|
||||||
@@ -496,12 +496,12 @@
|
|||||||
"title": "Change login username to a.recruiter and add connection status indicator",
|
"title": "Change login username to a.recruiter and add connection status indicator",
|
||||||
"description": "As a developer, I need to change the typed username from a.charlwood to a.recruiter and add a connection status indicator below the login button. See Ralph/depth-design.md Section 3.3.",
|
"description": "As a developer, I need to change the typed username from a.charlwood to a.recruiter and add a connection status indicator below the login button. See Ralph/depth-design.md Section 3.3.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"Username typed in login animation is 'a.recruiter' (not 'A.CHARLWOOD' or similar)",
|
"Username typed in login animation is \u0027a.recruiter\u0027 (not \u0027A.CHARLWOOD\u0027 or similar)",
|
||||||
"Connection status indicator appears below the login button: 6px dot + text",
|
"Connection status indicator appears below the login button: 6px dot + text",
|
||||||
"Initial state: red/alert dot + 'Awaiting secure connection...' (var(--alert) color)",
|
"Initial state: red/alert dot + \u0027Awaiting secure connection...\u0027 (var(--alert) color)",
|
||||||
"After ~2000ms: dot transitions to green + 'Secure connection established' (var(--success) color, 300ms transition)",
|
"After ~2000ms: dot transitions to green + \u0027Secure connection established\u0027 (var(--success) color, 300ms transition)",
|
||||||
"Text: 10px, font-family var(--font-geist-mono), color var(--text-tertiary)",
|
"Text: 10px, font-family var(--font-geist-mono), color var(--text-tertiary)",
|
||||||
"Login button disabled until BOTH typing is complete AND connectionState === 'connected'",
|
"Login button disabled until BOTH typing is complete AND connectionState === \u0027connected\u0027",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
@@ -514,11 +514,11 @@
|
|||||||
"title": "Add post-login loading state and update TopBar session name",
|
"title": "Add post-login loading state and update TopBar session name",
|
||||||
"description": "As a developer, I need a brief loading state after clicking the login button before the dashboard appears, and the TopBar should show A.RECRUITER as the session user. See Ralph/depth-design.md Sections 3.3 and 3.2.",
|
"description": "As a developer, I need a brief loading state after clicking the login button before the dashboard appears, and the TopBar should show A.RECRUITER as the session user. See Ralph/depth-design.md Sections 3.3 and 3.2.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"On login button click: isLoading=true, card content replaced with spinner + 'Loading clinical records...' text",
|
"On login button click: isLoading=true, card content replaced with spinner + \u0027Loading clinical records...\u0027 text",
|
||||||
"Loading state lasts ~600ms, then calls onComplete() to transition to dashboard",
|
"Loading state lasts ~600ms, then calls onComplete() to transition to dashboard",
|
||||||
"Spinner is a CSS-animated spinner (not a GIF), styled with var(--accent) or similar",
|
"Spinner is a CSS-animated spinner (not a GIF), styled with var(--accent) or similar",
|
||||||
"Loading text: 12px, color var(--text-secondary)",
|
"Loading text: 12px, color var(--text-secondary)",
|
||||||
"In TopBar.tsx: change session display name from 'Dr. A.CHARLWOOD' (or current value) to 'A.RECRUITER'",
|
"In TopBar.tsx: change session display name from \u0027Dr. A.CHARLWOOD\u0027 (or current value) to \u0027A.RECRUITER\u0027",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
@@ -535,7 +535,7 @@
|
|||||||
"Selecting a skill result opens the detail panel for that skill (openPanel call or dispatch event)",
|
"Selecting a skill result opens the detail panel for that skill (openPanel call or dispatch event)",
|
||||||
"Selecting a KPI result opens the KPI detail panel",
|
"Selecting a KPI result opens the KPI detail panel",
|
||||||
"Selecting a project result opens the project detail panel",
|
"Selecting a project result opens the project detail panel",
|
||||||
"Ensure DashboardLayout handlePaletteAction supports a new 'panel' action type or adapts existing types to trigger detail panel",
|
"Ensure DashboardLayout handlePaletteAction supports a new \u0027panel\u0027 action type or adapts existing types to trigger detail panel",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
@@ -548,12 +548,12 @@
|
|||||||
"title": "Responsive testing and fixes for all new components",
|
"title": "Responsive testing and fixes for all new components",
|
||||||
"description": "As a developer, I need to verify and fix responsive behavior for the detail panel, sub-nav, constellation, and restructured layout at all breakpoints.",
|
"description": "As a developer, I need to verify and fix responsive behavior for the detail panel, sub-nav, constellation, and restructured layout at all breakpoints.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"DetailPanel: both narrow and wide render as 100vw on mobile (<768px)",
|
"DetailPanel: both narrow and wide render as 100vw on mobile (\u003c768px)",
|
||||||
"SubNav: works on tablet/mobile (horizontal scroll if needed, no overflow)",
|
"SubNav: works on tablet/mobile (horizontal scroll if needed, no overflow)",
|
||||||
"CareerConstellation: renders at 300px height on tablet, 250px on mobile",
|
"CareerConstellation: renders at 300px height on tablet, 250px on mobile",
|
||||||
"Projects + KPIs: stack vertically on mobile when grid falls to single column",
|
"Projects + KPIs: stack vertically on mobile when grid falls to single column",
|
||||||
"CoreSkillsTile: full-width layout works on all breakpoints",
|
"CoreSkillsTile: full-width layout works on all breakpoints",
|
||||||
"All interactive elements have touch targets >= 44px on mobile",
|
"All interactive elements have touch targets \u003e= 44px on mobile",
|
||||||
"No horizontal overflow at 375px viewport width",
|
"No horizontal overflow at 375px viewport width",
|
||||||
"Typecheck passes",
|
"Typecheck passes",
|
||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
|
|||||||
@@ -8,3 +8,16 @@ Stories: 32 (US-001 through US-032)
|
|||||||
## Status
|
## Status
|
||||||
|
|
||||||
No iterations completed yet.
|
No iterations completed yet.
|
||||||
|
2026-02-13 22:57 | PASS | US-001: Clean up unused legacy components and hooks | model=opus elapsed=01:58 tools=18
|
||||||
|
2026-02-13 22:59 | PASS | US-002: Add new TypeScript types and CSS custom properties for depth features | model=sonnet elapsed=01:54 tools=11
|
||||||
|
2026-02-13 23:03 | PASS | US-003: Create DetailPanelContext, DetailPanel component, and useFocusTrap hook | model=sonnet elapsed=03:39 tools=22
|
||||||
|
2026-02-13 23:06 | PASS | US-004: Create SubNav component and useActiveSection hook | model=sonnet elapsed=02:54 tools=18
|
||||||
|
2026-02-13 23:08 | PASS | US-005: Expand skills data from 5 to ~20 with three categories | model=sonnet elapsed=01:58 tools=11
|
||||||
|
2026-02-13 23:10 | PASS | US-006: Add KPI story data and update 4th KPI | model=sonnet elapsed=01:59 tools=9
|
||||||
|
2026-02-13 23:11 | PASS | US-007: Create education extras data file | model=sonnet elapsed=01:25 tools=10
|
||||||
|
2026-02-13 23:15 | PASS | US-008: Restructure DashboardLayout with SubNav, new tile order, and DetailPanel | model=sonnet elapsed=03:10 tools=27
|
||||||
|
2026-02-13 23:17 | PASS | US-009: Create constellation data mapping file | model=sonnet elapsed=02:20 tools=10
|
||||||
|
2026-02-13 23:50 | PASS | US-011: Modify CoreSkillsTile: full width, categorised groups, panel triggers | model=opus elapsed=02:54 tools=22
|
||||||
|
2026-02-13 23:52 | PASS | US-012: Modify ProjectsTile: half width, compact card grid, panel trigger | model=sonnet elapsed=02:16 tools=11
|
||||||
|
2026-02-13 23:55 | PASS | US-013: Modify LastConsultationTile: add panel trigger | model=sonnet elapsed=02:20 tools=15
|
||||||
|
2026-02-13 23:58 | PASS | US-014: Modify CareerActivityTile: panel triggers and hover preview | model=sonnet elapsed=02:49 tools=14
|
||||||
|
|||||||
@@ -0,0 +1,568 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Ralph Wiggum Loop - PRD-driven variant.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Iterates through user stories in prd.json, spawning a fresh `claude --print`
|
||||||
|
invocation for each story. Memory persists via filesystem only: git commits,
|
||||||
|
prd.json (passes field), and progress.txt.
|
||||||
|
|
||||||
|
Each iteration works on ONE user story (in priority order).
|
||||||
|
When all stories pass, the loop completes.
|
||||||
|
|
||||||
|
Circuit breakers prevent runaway costs:
|
||||||
|
- No git changes for N consecutive iterations (stalled)
|
||||||
|
- Same error repeated N consecutive iterations (stuck)
|
||||||
|
|
||||||
|
.PARAMETER Model
|
||||||
|
Initial Claude model to use. Default: "opus". The agent can dynamically switch
|
||||||
|
models between iterations via <next-model>opus|sonnet</next-model> signals.
|
||||||
|
|
||||||
|
.PARAMETER MaxNoProgress
|
||||||
|
Number of consecutive iterations with no git changes before circuit breaker trips. Default: 3.
|
||||||
|
|
||||||
|
.PARAMETER MaxSameError
|
||||||
|
Number of consecutive iterations with the same error before circuit breaker trips. Default: 3.
|
||||||
|
|
||||||
|
.PARAMETER StartFrom
|
||||||
|
Story ID to start from (e.g., "US-005"). Treats all earlier stories as already passed.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\.claude\skills\ralph\ralph.ps1 -Model "opus"
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\.claude\skills\ralph\ralph.ps1 -StartFrom "US-010" -Model "sonnet"
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Model = "opus",
|
||||||
|
[int]$MaxNoProgress = 3,
|
||||||
|
[int]$MaxSameError = 3,
|
||||||
|
[string]$StartFrom = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$prdFile = Join-Path $scriptDir "prd.json"
|
||||||
|
$progressFile = Join-Path $scriptDir "progress.txt"
|
||||||
|
$logDir = Join-Path $scriptDir "logs"
|
||||||
|
|
||||||
|
# --- Find project root (git repo root) ---
|
||||||
|
|
||||||
|
$projectRoot = git rev-parse --show-toplevel 2>$null
|
||||||
|
if (-not $projectRoot) {
|
||||||
|
Write-Error "Not inside a git repository. Run from the project directory."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$projectRoot = (Resolve-Path $projectRoot).Path
|
||||||
|
|
||||||
|
# --- Validation ---
|
||||||
|
|
||||||
|
if (-not (Test-Path $prdFile)) {
|
||||||
|
Write-Error "prd.json not found at $prdFile"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure logs directory exists
|
||||||
|
if (-not (Test-Path $logDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $logDir | Out-Null
|
||||||
|
Write-Host "Created logs directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- PRD Read/Write ---
|
||||||
|
|
||||||
|
function Read-Prd {
|
||||||
|
Get-Content -Path $prdFile -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Save-Prd {
|
||||||
|
param($prdObj)
|
||||||
|
$prdObj | ConvertTo-Json -Depth 10 | Set-Content -Path $prdFile -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
$prd = Read-Prd
|
||||||
|
|
||||||
|
# --- Git Setup ---
|
||||||
|
|
||||||
|
$BranchName = $prd.branchName
|
||||||
|
|
||||||
|
if ($BranchName) {
|
||||||
|
$currentBranch = git branch --show-current
|
||||||
|
if ($currentBranch -ne $BranchName) {
|
||||||
|
$branchExists = git branch --list $BranchName
|
||||||
|
if ($branchExists) {
|
||||||
|
Write-Host "Switching to existing branch: $BranchName"
|
||||||
|
git checkout $BranchName
|
||||||
|
} else {
|
||||||
|
Write-Host "Creating branch: $BranchName"
|
||||||
|
git checkout -b $BranchName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Handle StartFrom: mark earlier stories as passed ---
|
||||||
|
|
||||||
|
if ($StartFrom) {
|
||||||
|
$startPriority = [int]($StartFrom -replace 'US-0*', '')
|
||||||
|
$skippedCount = 0
|
||||||
|
foreach ($story in $prd.userStories) {
|
||||||
|
$storyPriority = [int]($story.id -replace 'US-0*', '')
|
||||||
|
if ($storyPriority -lt $startPriority -and $story.passes -ne $true) {
|
||||||
|
$story.passes = $true
|
||||||
|
$story.notes = "Skipped (--StartFrom $StartFrom)"
|
||||||
|
$skippedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($skippedCount -gt 0) {
|
||||||
|
Save-Prd $prd
|
||||||
|
Write-Host "Marked $skippedCount stories before $StartFrom as skipped." -ForegroundColor DarkYellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Circuit Breaker State ---
|
||||||
|
|
||||||
|
$noProgressCount = 0
|
||||||
|
$lastErrorSignature = ""
|
||||||
|
$sameErrorCount = 0
|
||||||
|
|
||||||
|
# --- Prompt Generation ---
|
||||||
|
|
||||||
|
function Build-StoryPrompt {
|
||||||
|
param(
|
||||||
|
$story,
|
||||||
|
$prdObj,
|
||||||
|
[array]$completedStories
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build completed list
|
||||||
|
$completedSection = ""
|
||||||
|
if ($completedStories.Count -gt 0) {
|
||||||
|
$completedLines = ($completedStories | ForEach-Object {
|
||||||
|
"- $($_.id): $($_.title)"
|
||||||
|
}) -join "`n"
|
||||||
|
$completedSection = "`n## Previously Completed Stories (do not redo)`n$completedLines`n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build criteria list
|
||||||
|
$criteriaLines = ($story.acceptanceCriteria | ForEach-Object { "- [ ] $_" }) -join "`n"
|
||||||
|
|
||||||
|
# Build prompt using array-join (avoids PS 5.1 here-string indentation issues)
|
||||||
|
$sid = $story.id
|
||||||
|
$stitle = $story.title
|
||||||
|
$sdesc = $story.description
|
||||||
|
$pdesc = $prdObj.description
|
||||||
|
|
||||||
|
$prompt = @(
|
||||||
|
"# Ralph Iteration: $sid - $stitle"
|
||||||
|
""
|
||||||
|
"## Project"
|
||||||
|
"$pdesc"
|
||||||
|
""
|
||||||
|
"Read CLAUDE.md for full project conventions, architecture, and design system. This is mandatory before starting work."
|
||||||
|
""
|
||||||
|
"## Your Task"
|
||||||
|
""
|
||||||
|
"**${sid}: $stitle**"
|
||||||
|
""
|
||||||
|
"$sdesc"
|
||||||
|
""
|
||||||
|
"## Acceptance Criteria"
|
||||||
|
""
|
||||||
|
"$criteriaLines"
|
||||||
|
""
|
||||||
|
"## Reference Documents"
|
||||||
|
""
|
||||||
|
"Read these as needed for implementation detail:"
|
||||||
|
""
|
||||||
|
"- **CLAUDE.md** - Project conventions, architecture, design tokens, guardrails (READ FIRST)"
|
||||||
|
"- **Ralph/depth-design.md** - Component architecture, props interfaces, CSS specs, data models"
|
||||||
|
"- **Ralph/depth-requirements.md** - Full requirements with content sources and UX patterns"
|
||||||
|
"- **References/CV_v4.md** - Source of truth for all CV content (roles, dates, achievements, numbers)"
|
||||||
|
"$completedSection"
|
||||||
|
"## Workflow"
|
||||||
|
""
|
||||||
|
"1. Read CLAUDE.md to understand project conventions"
|
||||||
|
"2. Read Ralph/depth-design.md sections relevant to this story"
|
||||||
|
"3. Read existing source files you will modify to understand current patterns"
|
||||||
|
"4. Implement ALL acceptance criteria"
|
||||||
|
"5. Run npm run typecheck - fix any type errors"
|
||||||
|
"6. Run npm run build - fix any build errors"
|
||||||
|
"7. Stage and commit your changes:"
|
||||||
|
" git add [specific files] && git commit -m `"${sid}: [descriptive message]`""
|
||||||
|
"8. When ALL criteria are met, output: <story-complete>$sid</story-complete>"
|
||||||
|
""
|
||||||
|
"## Rules"
|
||||||
|
""
|
||||||
|
"- Work ONLY on $sid. Do not modify code for other stories."
|
||||||
|
"- Read files before modifying them."
|
||||||
|
"- Follow existing patterns and conventions in the codebase."
|
||||||
|
"- Use lucide-react for icons, never unicode symbols."
|
||||||
|
"- Use the project's CSS custom properties and Tailwind tokens."
|
||||||
|
"- Commit specific files, not git add -A."
|
||||||
|
"- Do NOT start a dev server (npm run dev). One is already running on port $devServerPort. Do NOT run any background tasks."
|
||||||
|
"- If genuinely blocked, output <story-blocked>$sid</story-blocked> with explanation."
|
||||||
|
"- To recommend a different model for the NEXT iteration, output <next-model>opus</next-model> or <next-model>sonnet</next-model>."
|
||||||
|
) -join "`n"
|
||||||
|
|
||||||
|
return $prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Banner ---
|
||||||
|
|
||||||
|
$completedCount = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count
|
||||||
|
$totalCount = $prd.userStories.Count
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "===== Ralph Wiggum Loop (PRD-driven) =====" -ForegroundColor Cyan
|
||||||
|
Write-Host "Project: $($prd.project)" -ForegroundColor Cyan
|
||||||
|
Write-Host "Branch: $BranchName | Model: $Model (dynamic switching enabled)" -ForegroundColor Cyan
|
||||||
|
Write-Host "Stories: $completedCount/$totalCount complete" -ForegroundColor Cyan
|
||||||
|
Write-Host "Circuit breakers: no-progress=$MaxNoProgress, same-error=$MaxSameError" -ForegroundColor Cyan
|
||||||
|
Write-Host "===========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Dev server port (assumed to be running externally)
|
||||||
|
$devServerPort = 5173
|
||||||
|
Write-Host "Dev server assumed running on port $devServerPort" -ForegroundColor DarkGray
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# --- Story Loop ---
|
||||||
|
|
||||||
|
$iterationCount = 0
|
||||||
|
$originalDir = Get-Location
|
||||||
|
Set-Location $projectRoot
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
# Re-read PRD each iteration (in case previous iteration updated it)
|
||||||
|
$prd = Read-Prd
|
||||||
|
|
||||||
|
# Partition stories
|
||||||
|
$completedStories = @($prd.userStories | Where-Object { $_.passes -eq $true })
|
||||||
|
$pendingStories = @($prd.userStories | Where-Object { $_.passes -ne $true } | Sort-Object { $_.priority })
|
||||||
|
|
||||||
|
# Check if all done
|
||||||
|
if ($pendingStories.Count -eq 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "===== ALL STORIES COMPLETE =====" -ForegroundColor Green
|
||||||
|
Write-Host "$($completedStories.Count)/$($prd.userStories.Count) stories passed." -ForegroundColor Green
|
||||||
|
Write-Host "Branch: $BranchName" -ForegroundColor Green
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentStory = $pendingStories[0]
|
||||||
|
$iterationCount++
|
||||||
|
$pctComplete = [math]::Round(($completedStories.Count / $prd.userStories.Count) * 100)
|
||||||
|
|
||||||
|
$storyLabel = "$($currentStory.id): $($currentStory.title)"
|
||||||
|
$pctStr = "${pctComplete}%"
|
||||||
|
$progressMsg = " Progress: $($completedStories.Count)/$($prd.userStories.Count) ($pctStr) - Remaining: $($pendingStories.Count)"
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "--- Iteration $iterationCount - $storyLabel ---" -ForegroundColor Yellow
|
||||||
|
Write-Host $progressMsg -ForegroundColor DarkGray
|
||||||
|
|
||||||
|
# Record HEAD before this iteration
|
||||||
|
$headBefore = git rev-parse HEAD 2>$null
|
||||||
|
|
||||||
|
$iterStart = Get-Date
|
||||||
|
Write-Host " Started: $($iterStart.ToString('HH:mm:ss')) | Model: $Model" -ForegroundColor DarkGray
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Generate prompt for this story
|
||||||
|
$promptContent = Build-StoryPrompt -story $currentStory -prdObj $prd -completedStories $completedStories
|
||||||
|
|
||||||
|
# --- Spawn Claude ---
|
||||||
|
|
||||||
|
$logFile = Join-Path $logDir "$($currentStory.id).log"
|
||||||
|
$rawLogFile = Join-Path $logDir "$($currentStory.id).raw.jsonl"
|
||||||
|
$maxRetries = 10
|
||||||
|
$retryCount = 0
|
||||||
|
$outputString = ""
|
||||||
|
$apiOverloaded = $false
|
||||||
|
|
||||||
|
do {
|
||||||
|
$apiOverloaded = $false
|
||||||
|
$textBuilder = [System.Text.StringBuilder]::new()
|
||||||
|
$toolCount = 0
|
||||||
|
|
||||||
|
# Clear raw log file for this attempt
|
||||||
|
if (Test-Path $rawLogFile) { Remove-Item $rawLogFile -Force }
|
||||||
|
|
||||||
|
if ($retryCount -gt 0) {
|
||||||
|
$backoffSeconds = [Math]::Pow(2, $retryCount - 1)
|
||||||
|
Write-Host " [Retry $retryCount/$maxRetries] API overloaded, waiting $backoffSeconds seconds..." -ForegroundColor DarkYellow
|
||||||
|
Start-Sleep -Seconds $backoffSeconds
|
||||||
|
Write-Host " Retrying Claude invocation..." -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Spawn Claude via Process.Start for clean shutdown control ---
|
||||||
|
# Using Process.Start instead of pipeline so we can break on the result
|
||||||
|
# event and force-kill the process tree. The pipeline approach hangs when
|
||||||
|
# Claude spawns background tasks (e.g. npm run dev) that keep stdout open.
|
||||||
|
|
||||||
|
$promptTempFile = Join-Path $logDir "$($currentStory.id).prompt.tmp"
|
||||||
|
$promptContent | Set-Content -Path $promptTempFile -Encoding UTF8
|
||||||
|
|
||||||
|
$claudeArgs = "--print --verbose --dangerously-skip-permissions --model $Model --output-format stream-json"
|
||||||
|
$psi = [System.Diagnostics.ProcessStartInfo]::new()
|
||||||
|
$psi.FileName = "cmd.exe"
|
||||||
|
$psi.Arguments = "/c type `"$promptTempFile`" | claude $claudeArgs"
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.WorkingDirectory = $projectRoot
|
||||||
|
|
||||||
|
$claudeProc = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
|
||||||
|
# Drain stderr async to prevent buffer deadlock
|
||||||
|
$claudeProc.add_ErrorDataReceived({ param($s,$e) })
|
||||||
|
$claudeProc.BeginErrorReadLine()
|
||||||
|
|
||||||
|
try {
|
||||||
|
while ($null -ne ($line = $claudeProc.StandardOutput.ReadLine())) {
|
||||||
|
$line = $line.Trim()
|
||||||
|
if (-not $line) { continue }
|
||||||
|
|
||||||
|
# Save raw event for debugging
|
||||||
|
try {
|
||||||
|
Add-Content -Path $rawLogFile -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
$isResultEvent = $false
|
||||||
|
try {
|
||||||
|
$evt = $line | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
|
||||||
|
# --- Tool use start ---
|
||||||
|
if ($evt.type -eq 'content_block_start' -and $evt.content_block.type -eq 'tool_use') {
|
||||||
|
$toolCount++
|
||||||
|
$toolName = $evt.content_block.name
|
||||||
|
Write-Host " [$toolName]" -ForegroundColor DarkCyan
|
||||||
|
}
|
||||||
|
# --- Streaming text ---
|
||||||
|
elseif ($evt.type -eq 'content_block_delta' -and $evt.delta.type -eq 'text_delta' -and $evt.delta.text) {
|
||||||
|
Write-Host -NoNewline $evt.delta.text
|
||||||
|
[void]$textBuilder.Append($evt.delta.text)
|
||||||
|
}
|
||||||
|
# --- Result event (terminal — stop reading after this) ---
|
||||||
|
elseif ($evt.type -eq 'result') {
|
||||||
|
if ($evt.subtype -eq 'error_result' -and $evt.error) {
|
||||||
|
Write-Host " [ERROR] $($evt.error)" -ForegroundColor Red
|
||||||
|
[void]$textBuilder.AppendLine("ERROR: $($evt.error)")
|
||||||
|
}
|
||||||
|
elseif ($evt.result) {
|
||||||
|
[void]$textBuilder.AppendLine($evt.result)
|
||||||
|
}
|
||||||
|
$isResultEvent = $true
|
||||||
|
}
|
||||||
|
# --- Message-level content ---
|
||||||
|
elseif ($evt.message -and $evt.message.content) {
|
||||||
|
foreach ($block in $evt.message.content) {
|
||||||
|
if ($block.type -eq 'text' -and $block.text) {
|
||||||
|
Write-Host $block.text
|
||||||
|
[void]$textBuilder.AppendLine($block.text)
|
||||||
|
}
|
||||||
|
elseif ($block.type -eq 'tool_use') {
|
||||||
|
$toolCount++
|
||||||
|
Write-Host " [$($block.name)]" -ForegroundColor DarkCyan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if ($line -and $line -notmatch '^\s*["\{\[\}\]]') {
|
||||||
|
Write-Host $line -ForegroundColor DarkYellow
|
||||||
|
[void]$textBuilder.AppendLine($line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Result is always the final stream event — stop reading
|
||||||
|
if ($isResultEvent) { break }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
# Kill the Claude process tree to prevent orphaned cmd.exe/node processes
|
||||||
|
if ($claudeProc -and -not $claudeProc.HasExited) {
|
||||||
|
try {
|
||||||
|
taskkill /T /F /PID $claudeProc.Id 2>$null | Out-Null
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
Remove-Item -Path $promptTempFile -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputString = $textBuilder.ToString()
|
||||||
|
|
||||||
|
# Check for 529 overloaded error
|
||||||
|
if ($outputString -match "529.*overloaded|overloaded_error") {
|
||||||
|
$apiOverloaded = $true
|
||||||
|
$retryCount++
|
||||||
|
if ($retryCount -ge $maxRetries) {
|
||||||
|
Write-Host " [ERROR] API overloaded after $maxRetries retries, giving up." -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Check for usage limit with cooldown
|
||||||
|
elseif ($outputString -match "(?i)usage limit reached.*reset at (\d{1,2})(?::(\d{2}))?\s*(am|pm)") {
|
||||||
|
$resetHour = [int]$Matches[1]
|
||||||
|
$resetMinute = if ($Matches[2]) { [int]$Matches[2] } else { 0 }
|
||||||
|
$resetAmPm = $Matches[3]
|
||||||
|
|
||||||
|
if ($resetAmPm -ieq "pm" -and $resetHour -ne 12) { $resetHour += 12 }
|
||||||
|
elseif ($resetAmPm -ieq "am" -and $resetHour -eq 12) { $resetHour = 0 }
|
||||||
|
|
||||||
|
$now = Get-Date
|
||||||
|
$resetTime = Get-Date -Hour $resetHour -Minute $resetMinute -Second 0
|
||||||
|
if ($resetTime -le $now) { $resetTime = $resetTime.AddDays(1) }
|
||||||
|
$resetTime = $resetTime.AddMinutes(2)
|
||||||
|
|
||||||
|
$waitSeconds = [Math]::Ceiling(($resetTime - $now).TotalSeconds)
|
||||||
|
$waitMinutes = [Math]::Ceiling($waitSeconds / 60)
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " [USAGE LIMIT] Reset at $($Matches[1]) $resetAmPm. Cooling down ~$waitMinutes minutes (until $($resetTime.ToString('HH:mm')))..." -ForegroundColor Yellow
|
||||||
|
Start-Sleep -Seconds $waitSeconds
|
||||||
|
Write-Host " [USAGE LIMIT] Cooldown complete. Retrying..." -ForegroundColor Green
|
||||||
|
|
||||||
|
$apiOverloaded = $true
|
||||||
|
}
|
||||||
|
} while ($apiOverloaded -and $retryCount -lt $maxRetries)
|
||||||
|
|
||||||
|
# Save log
|
||||||
|
$outputString | Set-Content -Path $logFile -Encoding UTF8
|
||||||
|
|
||||||
|
# Show elapsed time
|
||||||
|
$elapsed = (Get-Date) - $iterStart
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Finished: $(Get-Date -Format 'HH:mm:ss') (elapsed: $($elapsed.ToString('mm\:ss')), tools: $toolCount)" -ForegroundColor DarkGray
|
||||||
|
|
||||||
|
# --- Detect signals ---
|
||||||
|
|
||||||
|
$storyComplete = $outputString -match "<story-complete>$([regex]::Escape($currentStory.id))</story-complete>"
|
||||||
|
$storyBlocked = $outputString -match "<story-blocked>$([regex]::Escape($currentStory.id))</story-blocked>"
|
||||||
|
$headAfter = git rev-parse HEAD 2>$null
|
||||||
|
$hasGitChanges = $headAfter -ne $headBefore
|
||||||
|
|
||||||
|
# --- Update story status ---
|
||||||
|
|
||||||
|
if ($storyComplete) {
|
||||||
|
# Mark story as passed in prd.json
|
||||||
|
$prd = Read-Prd
|
||||||
|
$storyToUpdate = $prd.userStories | Where-Object { $_.id -eq $currentStory.id }
|
||||||
|
if ($storyToUpdate) {
|
||||||
|
$alreadyDone = if (-not $hasGitChanges) { " (already committed)" } else { "" }
|
||||||
|
$storyToUpdate.passes = $true
|
||||||
|
$storyToUpdate.notes = "Completed iteration $iterationCount at $(Get-Date -Format 'yyyy-MM-dd HH:mm'). Model: $Model.$alreadyDone"
|
||||||
|
}
|
||||||
|
Save-Prd $prd
|
||||||
|
|
||||||
|
# Append to progress.txt
|
||||||
|
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
||||||
|
$el = $elapsed.ToString('mm\:ss')
|
||||||
|
$tag = if ($hasGitChanges) { "PASS" } else { "PASS (no new commits)" }
|
||||||
|
$progressEntry = "$ts | $tag | $($currentStory.id): $($currentStory.title) | model=$Model elapsed=$el tools=$toolCount"
|
||||||
|
Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8
|
||||||
|
|
||||||
|
Write-Host " [PASSED] $storyLabel" -ForegroundColor Green
|
||||||
|
if (-not $hasGitChanges) {
|
||||||
|
Write-Host " (Work was already committed)" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
$noProgressCount = 0
|
||||||
|
$sameErrorCount = 0
|
||||||
|
$lastErrorSignature = ""
|
||||||
|
}
|
||||||
|
elseif ($storyBlocked) {
|
||||||
|
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
||||||
|
$progressEntry = "$ts | BLOCKED | $storyLabel"
|
||||||
|
Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8
|
||||||
|
Write-Host " [BLOCKED] $storyLabel - check $logFile for details." -ForegroundColor Red
|
||||||
|
# Blocked counts as no progress
|
||||||
|
$noProgressCount++
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# No completion signal
|
||||||
|
if ($hasGitChanges) {
|
||||||
|
Write-Host " [PARTIAL] Git changes but no completion signal. Retrying story." -ForegroundColor DarkYellow
|
||||||
|
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
||||||
|
$progressEntry = "$ts | PARTIAL | $storyLabel"
|
||||||
|
Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8
|
||||||
|
$noProgressCount = 0
|
||||||
|
} else {
|
||||||
|
Write-Host " [NO PROGRESS] No changes and no signal." -ForegroundColor DarkYellow
|
||||||
|
$noProgressCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Circuit Breaker: No Progress ---
|
||||||
|
|
||||||
|
if ($noProgressCount -ge $MaxNoProgress) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "===== CIRCUIT BREAKER: NO PROGRESS =====" -ForegroundColor Red
|
||||||
|
Write-Host "No meaningful progress for $MaxNoProgress consecutive iterations." -ForegroundColor Red
|
||||||
|
Write-Host "Stuck on: $($currentStory.id) - $($currentStory.title)" -ForegroundColor Red
|
||||||
|
Write-Host "Check $logFile for details." -ForegroundColor Red
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Circuit Breaker: Repeated Error ---
|
||||||
|
|
||||||
|
$errorLines = $outputString | Select-String -Pattern "(?i)(error|exception|failed|fatal)[:.].*" -AllMatches
|
||||||
|
if ($errorLines) {
|
||||||
|
$filteredErrors = $errorLines.Matches | Where-Object { $_.Value -notmatch "529|overloaded" } | Select-Object -First 3
|
||||||
|
$currentErrorSignature = ($filteredErrors | ForEach-Object { $_.Value }) -join "|"
|
||||||
|
if ($currentErrorSignature -and $currentErrorSignature -eq $lastErrorSignature) {
|
||||||
|
$sameErrorCount++
|
||||||
|
Write-Host " [Circuit Breaker] Same error pattern repeated ($sameErrorCount/$MaxSameError)" -ForegroundColor DarkYellow
|
||||||
|
if ($sameErrorCount -ge $MaxSameError) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "===== CIRCUIT BREAKER: REPEATED ERROR =====" -ForegroundColor Red
|
||||||
|
Write-Host "Same error for $MaxSameError consecutive iterations:" -ForegroundColor Red
|
||||||
|
Write-Host " $currentErrorSignature" -ForegroundColor Red
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} elseif ($currentErrorSignature) {
|
||||||
|
$sameErrorCount = 0
|
||||||
|
}
|
||||||
|
$lastErrorSignature = $currentErrorSignature
|
||||||
|
} else {
|
||||||
|
$sameErrorCount = 0
|
||||||
|
$lastErrorSignature = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Dynamic Model Selection ---
|
||||||
|
|
||||||
|
if ($outputString -match "<next-model>(opus|sonnet)</next-model>") {
|
||||||
|
$nextModel = $Matches[1]
|
||||||
|
if ($nextModel -ne $Model) {
|
||||||
|
Write-Host " [Model Switch] $Model -> $nextModel (agent recommendation)" -ForegroundColor Magenta
|
||||||
|
$Model = $nextModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Brief pause between iterations
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
Set-Location $originalDir
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Final Summary ---
|
||||||
|
|
||||||
|
$prd = Read-Prd
|
||||||
|
$finalPassed = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count
|
||||||
|
$finalTotal = $prd.userStories.Count
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "===========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Ralph Loop finished after $iterationCount iteration(s)" -ForegroundColor Cyan
|
||||||
|
Write-Host " Stories: $finalPassed/$finalTotal passed" -ForegroundColor Cyan
|
||||||
|
Write-Host " Branch: $BranchName" -ForegroundColor Cyan
|
||||||
|
Write-Host " Logs: $logDir" -ForegroundColor Cyan
|
||||||
|
Write-Host "===========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
if ($finalPassed -eq $finalTotal) {
|
||||||
|
exit 0
|
||||||
|
} else {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,582 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Ralph Wiggum Loop — PRD-driven variant.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Iterates through user stories in prd.json, spawning a fresh `claude --print`
|
||||||
|
invocation for each story. Memory persists via filesystem only: git commits,
|
||||||
|
prd.json (passes field), and progress.txt.
|
||||||
|
|
||||||
|
Each iteration works on ONE user story (in priority order).
|
||||||
|
When all stories pass, the loop completes.
|
||||||
|
|
||||||
|
Circuit breakers prevent runaway costs:
|
||||||
|
- No git changes for N consecutive iterations (stalled)
|
||||||
|
- Same error repeated N consecutive iterations (stuck)
|
||||||
|
|
||||||
|
.PARAMETER Model
|
||||||
|
Initial Claude model to use. Default: "opus". The agent can dynamically switch
|
||||||
|
models between iterations via <next-model>opus|sonnet</next-model> signals.
|
||||||
|
|
||||||
|
.PARAMETER MaxNoProgress
|
||||||
|
Number of consecutive iterations with no git changes before circuit breaker trips. Default: 3.
|
||||||
|
|
||||||
|
.PARAMETER MaxSameError
|
||||||
|
Number of consecutive iterations with the same error before circuit breaker trips. Default: 3.
|
||||||
|
|
||||||
|
.PARAMETER StartFrom
|
||||||
|
Story ID to start from (e.g., "US-005"). Treats all earlier stories as already passed.
|
||||||
|
|
||||||
|
.PARAMETER SkipVerify
|
||||||
|
Skip post-iteration typecheck verification. Faster but less safe.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\.claude\skills\ralph\ralph.ps1 -Model "opus"
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\.claude\skills\ralph\ralph.ps1 -StartFrom "US-010" -Model "sonnet"
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Model = "opus",
|
||||||
|
[int]$MaxNoProgress = 3,
|
||||||
|
[int]$MaxSameError = 3,
|
||||||
|
[string]$StartFrom = "",
|
||||||
|
[switch]$SkipVerify
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$prdFile = Join-Path $scriptDir "prd.json"
|
||||||
|
$progressFile = Join-Path $scriptDir "progress.txt"
|
||||||
|
$logDir = Join-Path $scriptDir "logs"
|
||||||
|
|
||||||
|
# --- Find project root (git repo root) ---
|
||||||
|
|
||||||
|
$projectRoot = git rev-parse --show-toplevel 2>$null
|
||||||
|
if (-not $projectRoot) {
|
||||||
|
Write-Error "Not inside a git repository. Run from the project directory."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$projectRoot = (Resolve-Path $projectRoot).Path
|
||||||
|
|
||||||
|
# --- Validation ---
|
||||||
|
|
||||||
|
if (-not (Test-Path $prdFile)) {
|
||||||
|
Write-Error "prd.json not found at $prdFile"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure logs directory exists
|
||||||
|
if (-not (Test-Path $logDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $logDir | Out-Null
|
||||||
|
Write-Host "Created logs directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- PRD Read/Write ---
|
||||||
|
|
||||||
|
function Read-Prd {
|
||||||
|
Get-Content -Path $prdFile -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Save-Prd {
|
||||||
|
param($prdObj)
|
||||||
|
$prdObj | ConvertTo-Json -Depth 10 | Set-Content -Path $prdFile -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
$prd = Read-Prd
|
||||||
|
|
||||||
|
# --- Git Setup ---
|
||||||
|
|
||||||
|
$BranchName = $prd.branchName
|
||||||
|
|
||||||
|
if ($BranchName) {
|
||||||
|
$currentBranch = git branch --show-current
|
||||||
|
if ($currentBranch -ne $BranchName) {
|
||||||
|
$branchExists = git branch --list $BranchName
|
||||||
|
if ($branchExists) {
|
||||||
|
Write-Host "Switching to existing branch: $BranchName"
|
||||||
|
git checkout $BranchName
|
||||||
|
} else {
|
||||||
|
Write-Host "Creating branch: $BranchName"
|
||||||
|
git checkout -b $BranchName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Handle StartFrom: mark earlier stories as passed ---
|
||||||
|
|
||||||
|
if ($StartFrom) {
|
||||||
|
$startPriority = [int]($StartFrom -replace 'US-0*', '')
|
||||||
|
$skippedCount = 0
|
||||||
|
foreach ($story in $prd.userStories) {
|
||||||
|
$storyPriority = [int]($story.id -replace 'US-0*', '')
|
||||||
|
if ($storyPriority -lt $startPriority -and $story.passes -ne $true) {
|
||||||
|
$story.passes = $true
|
||||||
|
$story.notes = "Skipped (--StartFrom $StartFrom)"
|
||||||
|
$skippedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($skippedCount -gt 0) {
|
||||||
|
Save-Prd $prd
|
||||||
|
Write-Host "Marked $skippedCount stories before $StartFrom as skipped." -ForegroundColor DarkYellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Circuit Breaker State ---
|
||||||
|
|
||||||
|
$noProgressCount = 0
|
||||||
|
$lastErrorSignature = ""
|
||||||
|
$sameErrorCount = 0
|
||||||
|
|
||||||
|
# --- Prompt Generation ---
|
||||||
|
|
||||||
|
function Build-StoryPrompt {
|
||||||
|
param(
|
||||||
|
$story,
|
||||||
|
$prdObj,
|
||||||
|
[array]$completedStories
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build completed list
|
||||||
|
$completedSection = ""
|
||||||
|
if ($completedStories.Count -gt 0) {
|
||||||
|
$completedLines = ($completedStories | ForEach-Object {
|
||||||
|
"- $($_.id): $($_.title)"
|
||||||
|
}) -join "`n"
|
||||||
|
$completedSection = "`n## Previously Completed Stories (do not redo)`n$completedLines`n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build criteria list
|
||||||
|
$criteriaLines = ($story.acceptanceCriteria | ForEach-Object { "- [ ] $_" }) -join "`n"
|
||||||
|
|
||||||
|
# Build prompt using array-join (avoids PS 5.1 here-string indentation issues)
|
||||||
|
$sid = $story.id
|
||||||
|
$stitle = $story.title
|
||||||
|
$sdesc = $story.description
|
||||||
|
$pdesc = $prdObj.description
|
||||||
|
|
||||||
|
$prompt = @(
|
||||||
|
"# Ralph Iteration: $sid - $stitle"
|
||||||
|
""
|
||||||
|
"## Project"
|
||||||
|
"$pdesc"
|
||||||
|
""
|
||||||
|
"Read CLAUDE.md for full project conventions, architecture, and design system. This is mandatory before starting work."
|
||||||
|
""
|
||||||
|
"## Your Task"
|
||||||
|
""
|
||||||
|
"**${sid}: $stitle**"
|
||||||
|
""
|
||||||
|
"$sdesc"
|
||||||
|
""
|
||||||
|
"## Acceptance Criteria"
|
||||||
|
""
|
||||||
|
"$criteriaLines"
|
||||||
|
""
|
||||||
|
"## Reference Documents"
|
||||||
|
""
|
||||||
|
"Read these as needed for implementation detail:"
|
||||||
|
""
|
||||||
|
"- **CLAUDE.md** - Project conventions, architecture, design tokens, guardrails (READ FIRST)"
|
||||||
|
"- **Ralph/depth-design.md** - Component architecture, props interfaces, CSS specs, data models"
|
||||||
|
"- **Ralph/depth-requirements.md** - Full requirements with content sources and UX patterns"
|
||||||
|
"- **References/CV_v4.md** - Source of truth for all CV content (roles, dates, achievements, numbers)"
|
||||||
|
"$completedSection"
|
||||||
|
"## Workflow"
|
||||||
|
""
|
||||||
|
"1. Read CLAUDE.md to understand project conventions"
|
||||||
|
"2. Read Ralph/depth-design.md sections relevant to this story"
|
||||||
|
"3. Read existing source files you will modify to understand current patterns"
|
||||||
|
"4. Implement ALL acceptance criteria"
|
||||||
|
"5. Run npm run typecheck - fix any type errors"
|
||||||
|
"6. Run npm run build - fix any build errors"
|
||||||
|
"7. Stage and commit your changes:"
|
||||||
|
" git add [specific files] && git commit -m `"${sid}: [descriptive message]`""
|
||||||
|
"8. When ALL criteria are met, output: <story-complete>$sid</story-complete>"
|
||||||
|
""
|
||||||
|
"## Rules"
|
||||||
|
""
|
||||||
|
"- Work ONLY on $sid. Do not modify code for other stories."
|
||||||
|
"- Read files before modifying them."
|
||||||
|
"- Follow existing patterns and conventions in the codebase."
|
||||||
|
"- Use lucide-react for icons, never unicode symbols."
|
||||||
|
"- Use the project's CSS custom properties and Tailwind tokens."
|
||||||
|
"- Commit specific files, not git add -A."
|
||||||
|
"- If genuinely blocked, output <story-blocked>$sid</story-blocked> with explanation."
|
||||||
|
"- To recommend a different model for the NEXT iteration, output <next-model>opus</next-model> or <next-model>sonnet</next-model>."
|
||||||
|
) -join "`n"
|
||||||
|
|
||||||
|
return $prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Banner ---
|
||||||
|
|
||||||
|
$completedCount = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count
|
||||||
|
$totalCount = $prd.userStories.Count
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "===== Ralph Wiggum Loop (PRD-driven) =====" -ForegroundColor Cyan
|
||||||
|
Write-Host "Project: $($prd.project)" -ForegroundColor Cyan
|
||||||
|
Write-Host "Branch: $BranchName | Model: $Model (dynamic switching enabled)" -ForegroundColor Cyan
|
||||||
|
Write-Host "Stories: $completedCount/$totalCount complete" -ForegroundColor Cyan
|
||||||
|
Write-Host "Circuit breakers: no-progress=$MaxNoProgress, same-error=$MaxSameError" -ForegroundColor Cyan
|
||||||
|
if (-not $SkipVerify) { Write-Host "Post-iteration typecheck verification: ON" -ForegroundColor Cyan }
|
||||||
|
Write-Host "===========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# --- Dev Server ---
|
||||||
|
|
||||||
|
$devServerPort = 5173
|
||||||
|
$devServerPid = $null
|
||||||
|
|
||||||
|
try {
|
||||||
|
$null = Invoke-WebRequest -Uri "http://localhost:$devServerPort" -TimeoutSec 2 -ErrorAction Stop
|
||||||
|
Write-Host "Dev server detected on port $devServerPort" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "Starting dev server (port $devServerPort)..." -ForegroundColor Cyan
|
||||||
|
$devProc = Start-Process -FilePath "npm.cmd" -ArgumentList "run", "dev" -WorkingDirectory $projectRoot -PassThru -WindowStyle Minimized
|
||||||
|
$devServerPid = $devProc.Id
|
||||||
|
|
||||||
|
for ($w = 1; $w -le 20; $w++) {
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
try {
|
||||||
|
$null = Invoke-WebRequest -Uri "http://localhost:$devServerPort" -TimeoutSec 2 -ErrorAction Stop
|
||||||
|
Write-Host "Dev server ready on port $devServerPort" -ForegroundColor Green
|
||||||
|
break
|
||||||
|
} catch {
|
||||||
|
if ($w -eq 20) {
|
||||||
|
Write-Warning "Dev server may not be ready — visual review steps may fail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# --- Story Loop ---
|
||||||
|
|
||||||
|
$iterationCount = 0
|
||||||
|
$originalDir = Get-Location
|
||||||
|
Set-Location $projectRoot
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
# Re-read PRD each iteration (in case previous iteration updated it)
|
||||||
|
$prd = Read-Prd
|
||||||
|
|
||||||
|
# Partition stories
|
||||||
|
$completedStories = @($prd.userStories | Where-Object { $_.passes -eq $true })
|
||||||
|
$pendingStories = @($prd.userStories | Where-Object { $_.passes -ne $true } | Sort-Object { $_.priority })
|
||||||
|
|
||||||
|
# Check if all done
|
||||||
|
if ($pendingStories.Count -eq 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "===== ALL STORIES COMPLETE =====" -ForegroundColor Green
|
||||||
|
Write-Host "$($completedStories.Count)/$($prd.userStories.Count) stories passed." -ForegroundColor Green
|
||||||
|
Write-Host "Branch: $BranchName" -ForegroundColor Green
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentStory = $pendingStories[0]
|
||||||
|
$iterationCount++
|
||||||
|
$pctComplete = [math]::Round(($completedStories.Count / $prd.userStories.Count) * 100)
|
||||||
|
|
||||||
|
$storyLabel = "$($currentStory.id): $($currentStory.title)"
|
||||||
|
$pctStr = "${pctComplete}%"
|
||||||
|
$progressMsg = " Progress: $($completedStories.Count)/$($prd.userStories.Count) ($pctStr) - Remaining: $($pendingStories.Count)"
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "--- Iteration $iterationCount - $storyLabel ---" -ForegroundColor Yellow
|
||||||
|
Write-Host $progressMsg -ForegroundColor DarkGray
|
||||||
|
|
||||||
|
# Record HEAD before this iteration
|
||||||
|
$headBefore = git rev-parse HEAD 2>$null
|
||||||
|
|
||||||
|
$iterStart = Get-Date
|
||||||
|
Write-Host " Started: $($iterStart.ToString('HH:mm:ss')) | Model: $Model" -ForegroundColor DarkGray
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Generate prompt for this story
|
||||||
|
$promptContent = Build-StoryPrompt -story $currentStory -prdObj $prd -completedStories $completedStories
|
||||||
|
|
||||||
|
# --- Spawn Claude ---
|
||||||
|
|
||||||
|
$logFile = Join-Path $logDir "$($currentStory.id).log"
|
||||||
|
$rawLogFile = Join-Path $logDir "$($currentStory.id).raw.jsonl"
|
||||||
|
$maxRetries = 10
|
||||||
|
$retryCount = 0
|
||||||
|
$outputString = ""
|
||||||
|
$apiOverloaded = $false
|
||||||
|
|
||||||
|
do {
|
||||||
|
$apiOverloaded = $false
|
||||||
|
$textBuilder = [System.Text.StringBuilder]::new()
|
||||||
|
$toolCount = 0
|
||||||
|
|
||||||
|
# Clear raw log file for this attempt
|
||||||
|
if (Test-Path $rawLogFile) { Remove-Item $rawLogFile -Force }
|
||||||
|
|
||||||
|
if ($retryCount -gt 0) {
|
||||||
|
$backoffSeconds = [Math]::Pow(2, $retryCount - 1)
|
||||||
|
Write-Host " [Retry $retryCount/$maxRetries] API overloaded, waiting $backoffSeconds seconds..." -ForegroundColor DarkYellow
|
||||||
|
Start-Sleep -Seconds $backoffSeconds
|
||||||
|
Write-Host " Retrying Claude invocation..." -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
|
||||||
|
$promptContent | claude --print --verbose --dangerously-skip-permissions --model $Model --output-format stream-json 2>&1 | ForEach-Object {
|
||||||
|
$line = $_.ToString().Trim()
|
||||||
|
if (-not $line) { return }
|
||||||
|
|
||||||
|
# Save raw event for debugging
|
||||||
|
try {
|
||||||
|
Add-Content -Path $rawLogFile -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$evt = $line | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
|
||||||
|
# --- Tool use start ---
|
||||||
|
if ($evt.type -eq 'content_block_start' -and $evt.content_block.type -eq 'tool_use') {
|
||||||
|
$toolCount++
|
||||||
|
$toolName = $evt.content_block.name
|
||||||
|
Write-Host " [$toolName]" -ForegroundColor DarkCyan
|
||||||
|
}
|
||||||
|
# --- Streaming text ---
|
||||||
|
elseif ($evt.type -eq 'content_block_delta' -and $evt.delta.type -eq 'text_delta' -and $evt.delta.text) {
|
||||||
|
Write-Host -NoNewline $evt.delta.text
|
||||||
|
[void]$textBuilder.Append($evt.delta.text)
|
||||||
|
}
|
||||||
|
# --- Result event ---
|
||||||
|
elseif ($evt.type -eq 'result') {
|
||||||
|
if ($evt.subtype -eq 'error_result' -and $evt.error) {
|
||||||
|
Write-Host " [ERROR] $($evt.error)" -ForegroundColor Red
|
||||||
|
[void]$textBuilder.AppendLine("ERROR: $($evt.error)")
|
||||||
|
}
|
||||||
|
elseif ($evt.result) {
|
||||||
|
[void]$textBuilder.AppendLine($evt.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# --- Message-level content ---
|
||||||
|
elseif ($evt.message -and $evt.message.content) {
|
||||||
|
foreach ($block in $evt.message.content) {
|
||||||
|
if ($block.type -eq 'text' -and $block.text) {
|
||||||
|
Write-Host $block.text
|
||||||
|
[void]$textBuilder.AppendLine($block.text)
|
||||||
|
}
|
||||||
|
elseif ($block.type -eq 'tool_use') {
|
||||||
|
$toolCount++
|
||||||
|
Write-Host " [$($block.name)]" -ForegroundColor DarkCyan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if ($line -and $line -notmatch '^\s*[\{\[\}\]"]') {
|
||||||
|
Write-Host $line -ForegroundColor DarkYellow
|
||||||
|
[void]$textBuilder.AppendLine($line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputString = $textBuilder.ToString()
|
||||||
|
|
||||||
|
# Check for 529 overloaded error
|
||||||
|
if ($outputString -match "529.*overloaded|overloaded_error") {
|
||||||
|
$apiOverloaded = $true
|
||||||
|
$retryCount++
|
||||||
|
if ($retryCount -ge $maxRetries) {
|
||||||
|
Write-Host " [ERROR] API overloaded after $maxRetries retries, giving up." -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Check for usage limit with cooldown
|
||||||
|
elseif ($outputString -match "(?i)usage limit reached.*reset at (\d{1,2})(?::(\d{2}))?\s*(am|pm)") {
|
||||||
|
$resetHour = [int]$Matches[1]
|
||||||
|
$resetMinute = if ($Matches[2]) { [int]$Matches[2] } else { 0 }
|
||||||
|
$resetAmPm = $Matches[3]
|
||||||
|
|
||||||
|
if ($resetAmPm -ieq "pm" -and $resetHour -ne 12) { $resetHour += 12 }
|
||||||
|
elseif ($resetAmPm -ieq "am" -and $resetHour -eq 12) { $resetHour = 0 }
|
||||||
|
|
||||||
|
$now = Get-Date
|
||||||
|
$resetTime = Get-Date -Hour $resetHour -Minute $resetMinute -Second 0
|
||||||
|
if ($resetTime -le $now) { $resetTime = $resetTime.AddDays(1) }
|
||||||
|
$resetTime = $resetTime.AddMinutes(2)
|
||||||
|
|
||||||
|
$waitSeconds = [Math]::Ceiling(($resetTime - $now).TotalSeconds)
|
||||||
|
$waitMinutes = [Math]::Ceiling($waitSeconds / 60)
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " [USAGE LIMIT] Reset at $($Matches[1]) $resetAmPm. Cooling down ~$waitMinutes minutes (until $($resetTime.ToString('HH:mm')))..." -ForegroundColor Yellow
|
||||||
|
Start-Sleep -Seconds $waitSeconds
|
||||||
|
Write-Host " [USAGE LIMIT] Cooldown complete. Retrying..." -ForegroundColor Green
|
||||||
|
|
||||||
|
$apiOverloaded = $true
|
||||||
|
}
|
||||||
|
} while ($apiOverloaded -and $retryCount -lt $maxRetries)
|
||||||
|
|
||||||
|
# Save log
|
||||||
|
$outputString | Set-Content -Path $logFile -Encoding UTF8
|
||||||
|
|
||||||
|
# Show elapsed time
|
||||||
|
$elapsed = (Get-Date) - $iterStart
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Finished: $(Get-Date -Format 'HH:mm:ss') (elapsed: $($elapsed.ToString('mm\:ss')), tools: $toolCount)" -ForegroundColor DarkGray
|
||||||
|
|
||||||
|
# --- Detect signals ---
|
||||||
|
|
||||||
|
$storyComplete = $outputString -match "<story-complete>$([regex]::Escape($currentStory.id))</story-complete>"
|
||||||
|
$storyBlocked = $outputString -match "<story-blocked>$([regex]::Escape($currentStory.id))</story-blocked>"
|
||||||
|
$headAfter = git rev-parse HEAD 2>$null
|
||||||
|
$hasGitChanges = $headAfter -ne $headBefore
|
||||||
|
|
||||||
|
# --- Post-iteration typecheck verification ---
|
||||||
|
|
||||||
|
$typecheckPassed = $true
|
||||||
|
if ($storyComplete -and $hasGitChanges -and -not $SkipVerify) {
|
||||||
|
Write-Host " Verifying typecheck..." -ForegroundColor DarkGray
|
||||||
|
$typecheckOutput = npm run typecheck 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host " [VERIFY FAIL] Typecheck failed after completion signal. Not marking as passed." -ForegroundColor Red
|
||||||
|
$typecheckPassed = $false
|
||||||
|
} else {
|
||||||
|
Write-Host " [VERIFY OK] Typecheck passed." -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Update story status ---
|
||||||
|
|
||||||
|
if ($storyComplete -and $hasGitChanges -and $typecheckPassed) {
|
||||||
|
# Mark story as passed in prd.json
|
||||||
|
$prd = Read-Prd
|
||||||
|
$storyToUpdate = $prd.userStories | Where-Object { $_.id -eq $currentStory.id }
|
||||||
|
if ($storyToUpdate) {
|
||||||
|
$storyToUpdate.passes = $true
|
||||||
|
$storyToUpdate.notes = "Completed iteration $iterationCount at $(Get-Date -Format 'yyyy-MM-dd HH:mm'). Model: $Model."
|
||||||
|
}
|
||||||
|
Save-Prd $prd
|
||||||
|
|
||||||
|
# Append to progress.txt
|
||||||
|
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
||||||
|
$el = $elapsed.ToString('mm\:ss')
|
||||||
|
$progressEntry = "$ts | PASS | $($currentStory.id): $($currentStory.title) | model=$Model elapsed=$el tools=$toolCount"
|
||||||
|
Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8
|
||||||
|
|
||||||
|
Write-Host " [PASSED] $storyLabel" -ForegroundColor Green
|
||||||
|
$noProgressCount = 0
|
||||||
|
$sameErrorCount = 0
|
||||||
|
$lastErrorSignature = ""
|
||||||
|
}
|
||||||
|
elseif ($storyBlocked) {
|
||||||
|
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
||||||
|
$progressEntry = "$ts | BLOCKED | $storyLabel"
|
||||||
|
Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8
|
||||||
|
Write-Host " [BLOCKED] $storyLabel - check $logFile for details." -ForegroundColor Red
|
||||||
|
# Blocked counts as no progress
|
||||||
|
$noProgressCount++
|
||||||
|
}
|
||||||
|
elseif ($storyComplete -and -not $hasGitChanges) {
|
||||||
|
Write-Host " [WARNING] Completion signaled but no git commits. Retrying story." -ForegroundColor DarkYellow
|
||||||
|
$noProgressCount++
|
||||||
|
}
|
||||||
|
elseif ($storyComplete -and -not $typecheckPassed) {
|
||||||
|
Write-Host " [WARNING] Completion signaled but typecheck failed. Retrying story." -ForegroundColor DarkYellow
|
||||||
|
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
||||||
|
$progressEntry = "$ts | TYPECHECK_FAIL | $storyLabel"
|
||||||
|
Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8
|
||||||
|
# Has git changes, so not stalled — but not passed either
|
||||||
|
$noProgressCount = 0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# No completion signal
|
||||||
|
if ($hasGitChanges) {
|
||||||
|
Write-Host " [PARTIAL] Git changes but no completion signal. Retrying story." -ForegroundColor DarkYellow
|
||||||
|
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
||||||
|
$progressEntry = "$ts | PARTIAL | $storyLabel"
|
||||||
|
Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8
|
||||||
|
$noProgressCount = 0
|
||||||
|
} else {
|
||||||
|
Write-Host " [NO PROGRESS] No changes and no signal." -ForegroundColor DarkYellow
|
||||||
|
$noProgressCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Circuit Breaker: No Progress ---
|
||||||
|
|
||||||
|
if ($noProgressCount -ge $MaxNoProgress) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "===== CIRCUIT BREAKER: NO PROGRESS =====" -ForegroundColor Red
|
||||||
|
Write-Host "No meaningful progress for $MaxNoProgress consecutive iterations." -ForegroundColor Red
|
||||||
|
Write-Host "Stuck on: $($currentStory.id) — $($currentStory.title)" -ForegroundColor Red
|
||||||
|
Write-Host "Check $logFile for details." -ForegroundColor Red
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Circuit Breaker: Repeated Error ---
|
||||||
|
|
||||||
|
$errorLines = $outputString | Select-String -Pattern "(?i)(error|exception|failed|fatal)[:.].*" -AllMatches
|
||||||
|
if ($errorLines) {
|
||||||
|
$filteredErrors = $errorLines.Matches | Where-Object { $_.Value -notmatch "529|overloaded" } | Select-Object -First 3
|
||||||
|
$currentErrorSignature = ($filteredErrors | ForEach-Object { $_.Value }) -join "|"
|
||||||
|
if ($currentErrorSignature -and $currentErrorSignature -eq $lastErrorSignature) {
|
||||||
|
$sameErrorCount++
|
||||||
|
Write-Host " [Circuit Breaker] Same error pattern repeated ($sameErrorCount/$MaxSameError)" -ForegroundColor DarkYellow
|
||||||
|
if ($sameErrorCount -ge $MaxSameError) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "===== CIRCUIT BREAKER: REPEATED ERROR =====" -ForegroundColor Red
|
||||||
|
Write-Host "Same error for $MaxSameError consecutive iterations:" -ForegroundColor Red
|
||||||
|
Write-Host " $currentErrorSignature" -ForegroundColor Red
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} elseif ($currentErrorSignature) {
|
||||||
|
$sameErrorCount = 0
|
||||||
|
}
|
||||||
|
$lastErrorSignature = $currentErrorSignature
|
||||||
|
} else {
|
||||||
|
$sameErrorCount = 0
|
||||||
|
$lastErrorSignature = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Dynamic Model Selection ---
|
||||||
|
|
||||||
|
if ($outputString -match "<next-model>(opus|sonnet)</next-model>") {
|
||||||
|
$nextModel = $Matches[1]
|
||||||
|
if ($nextModel -ne $Model) {
|
||||||
|
Write-Host " [Model Switch] $Model -> $nextModel (agent recommendation)" -ForegroundColor Magenta
|
||||||
|
$Model = $nextModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Brief pause between iterations
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
# Cleanup: restore directory, kill dev server
|
||||||
|
Set-Location $originalDir
|
||||||
|
if ($devServerPid) {
|
||||||
|
Write-Host "Stopping dev server (PID $devServerPid)..." -ForegroundColor DarkGray
|
||||||
|
taskkill /T /F /PID $devServerPid 2>$null | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Final Summary ---
|
||||||
|
|
||||||
|
$prd = Read-Prd
|
||||||
|
$finalPassed = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count
|
||||||
|
$finalTotal = $prd.userStories.Count
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "===========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Ralph Loop finished after $iterationCount iteration(s)" -ForegroundColor Cyan
|
||||||
|
Write-Host " Stories: $finalPassed/$finalTotal passed" -ForegroundColor Cyan
|
||||||
|
Write-Host " Branch: $BranchName" -ForegroundColor Cyan
|
||||||
|
Write-Host " Logs: $logDir" -ForegroundColor Cyan
|
||||||
|
Write-Host "===========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
if ($finalPassed -eq $finalTotal) {
|
||||||
|
exit 0
|
||||||
|
} else {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user