Files
resolutionflow/docs/plans/archive/2026-03-06-procedural-flow-assist.md
Michael Chihlas cbb4b25671
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
CI / frontend (pull_request) Successful in 6m42s
CI / e2e (pull_request) Successful in 10m11s
CI / backend (pull_request) Successful in 10m43s
fix(ui): drop setState-in-effect in useAuthSessionExpiry
CI surfaced react-hooks/set-state-in-effect on the synchronous
setState(computeState(token)) inside the useEffect body. The earlier
shape mirrored token -> state via an effect, which is exactly the
"you might not need an effect" pattern React 19's eslint rule now
flags.

Switch to derived state: compute during render, use a useReducer
tick to force re-render on the 30s cadence (so relative timestamps
stay current even when token props don't change). Same observable
behavior, no cascading renders.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 20:15:11 -04:00

31 KiB

Procedural Flow Assist — AI Chat Builder Support

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Make the Flow Assist AI chat builder correctly generate procedural/maintenance flows using the flat steps-array schema instead of the troubleshooting decision-tree schema.

Architecture: The AI chat service (ai_chat_service.py) currently has hardcoded troubleshooting-specific prompts (schema, interview protocol, response format). We add parallel procedural versions and dispatch based on flow_type. The AI validator gets a procedural counterpart. The frontend gets a procedural steps preview component and the store/page handle the different data shape.

Tech Stack: Python/FastAPI (backend), React/TypeScript (frontend), Zustand (state), Tailwind CSS (styling)


Context: Procedural vs Troubleshooting Structure

Troubleshooting flows use a recursive tree: { id, type: "decision", question, options, children: [...] } — branching paths ending in solution nodes.

Procedural flows use a flat ordered array: { steps: [{ id, type, title, ... }, ...] } — sequential steps with a procedure_end as the final step.

Procedural Step Schema

{
  "id": "unique-slug",
  "type": "procedure_step | procedure_end | section_header",
  "title": "Step title",
  "description": "Detailed instructions (supports [VAR:variable_name] interpolation)",
  "content_type": "action | informational | verification | warning",
  "estimated_minutes": 5,
  "commands": [{ "code": "Get-Service ...", "label": "Check service", "language": "powershell" }],
  "expected_outcome": "What success looks like",
  "verification_prompt": "Confirm the service is running",
  "verification_type": "checkbox | text_input",
  "warning_text": "Caution text for warning content_type",
  "notes_enabled": true,
  "reference_url": "https://docs.microsoft.com/...",
  "section_header": "Optional section label"
}

Structural Rules

  • steps array must be non-empty
  • Each step needs id, type, title
  • Valid types: procedure_step, procedure_end, section_header
  • Exactly ONE procedure_end as the LAST step
  • No duplicate step IDs
  • content_type if present must be: action, informational, verification, warning
  • Commands can be a string or array of { code, label?, language? }

Intake Form (Optional)

Procedural flows can have an intake form that captures variables before execution. Fields use variable_name (e.g., server_name) referenced in step descriptions as [VAR:server_name].


Task 1: Add Procedural System Prompts to AI Chat Service

Files:

  • Modify: backend/app/core/ai_chat_service.py

Step 1: Add procedural schema context constant

After the existing SCHEMA_CONTEXT constant (~line 78), add:

PROCEDURAL_SCHEMA_CONTEXT = """
PROCEDURAL STEP SCHEMA — This is what you are building:

Procedural flows are a FLAT ORDERED ARRAY of steps (NOT a branching tree). The structure is:
{"steps": [step1, step2, ..., end_step]}

Each step has a "type" field:

1. procedure_step — A task the engineer performs
   Required: id (string), type ("procedure_step"), title (string)
   Optional: description (string — detailed instructions, supports [VAR:variable_name] interpolation),
             content_type ("action" | "informational" | "verification" | "warning"),
             estimated_minutes (integer),
             commands (array of {code: string, label?: string, language?: string}),
             expected_outcome (string),
             verification_prompt (string — question to confirm step completion),
             verification_type ("checkbox" | "text_input"),
             warning_text (string — caution text, used with content_type "warning"),
             notes_enabled (boolean, default true),
             reference_url (string — documentation link)

2. section_header — A visual divider to organize steps into phases
   Required: id (string), type ("section_header"), title (string)
   Optional: description (string)

3. procedure_end — The final completion marker (exactly ONE, always LAST)
   Required: id (string), type ("procedure_end"), title (string)
   Optional: description (string — completion summary text)

CONTENT TYPES for procedure_step:
- "action" (default): Executable task with commands — shows terminal icon
- "informational": Read-only context or reference info — shows info icon
- "verification": Requires engineer confirmation before proceeding — shows checkmark icon
- "warning": Highlighted caution/danger step — shows alert icon

STRUCTURAL RULES:
- Steps are executed in array order — position determines sequence
- All IDs must be unique strings (use descriptive slugs like "install-ad-ds-role")
- The LAST step MUST be type "procedure_end"
- Section headers group related steps visually but don't affect execution order
- Use [VAR:variable_name] in descriptions to reference intake form variables (e.g., "Configure IP on [VAR:server_name]")

COMMAND FORMAT:
Commands are arrays of objects, each with:
- code (required): The exact command syntax (PowerShell, CMD, bash, etc.)
- label (optional): Short description of what the command does
- language (optional): "powershell", "cmd", "bash", etc.
"""

PROCEDURAL_INTERVIEW_PROTOCOL = """
INTERVIEW PHASES — Follow this progression:

PHASE 1 - SCOPING (current_phase: scoping):
Understand what procedure this flow covers:
- What process is this flow for? (e.g., "new domain controller build", "Exchange migration", "firewall replacement")
- What is the target environment? (on-prem, hybrid, cloud, specific vendors?)
- Who will execute this? (Tier level, experience assumptions)
- What information will the engineer need before starting? (This becomes the intake form — server name, IP, domain, credentials, etc.)
Demonstrate expertise: "For a DC build, we'd typically need server name, IP, subnet, gateway, domain name, DSRM password, and whether this is the first DC or joining an existing domain."
DO NOT emit [STEPS_UPDATE] during scoping.

PHASE 2 - DISCOVERY (current_phase: discovery):
Build out the procedure step by step:
- Establish the major phases (these become section_headers)
- For each phase, work through the steps in execution order
- Capture specific commands with exact syntax
- Add verification steps where the engineer should confirm something before proceeding
- Add warning steps for anything destructive or irreversible
EMIT [STEPS_UPDATE] when you and the user have agreed on concrete steps. Include ALL steps discussed so far.

PHASE 3 - ENRICHMENT (current_phase: enrichment):
Circle back to improve existing steps:
- Add exact PowerShell/CLI commands with syntax
- Add expected_outcome for action steps
- Add verification prompts for critical checkpoints
- Add estimated_minutes for time-sensitive procedures
- Add reference_url links to relevant documentation
- Add warning_text for dangerous operations
- Suggest intake form variables for values that change per execution
EMIT [STEPS_UPDATE] when enriching steps.

PHASE 4 - REVIEW (current_phase: review):
Present a summary:
- Total step count by content_type
- Section-by-section outline
- Estimated total time
- List of intake form variables suggested
- Flag any gaps or areas needing more detail
EMIT [STEPS_UPDATE] only if the user requests changes.

TRANSITION between phases by emitting [PHASE:phase_name] when the conversation naturally moves to the next stage.
"""

PROCEDURAL_RESPONSE_FORMAT = """
RESPONSE FORMAT:

Your response is natural conversational text. When the step structure changes, include structured markers that will be parsed by the system (the user will NOT see these markers):

1. Steps update (only when structure changes — see phase rules above):
[STEPS_UPDATE]
{"steps": [...valid steps array...]}
[/STEPS_UPDATE]

2. Phase transition (when moving to next phase):
[PHASE:discovery]

3. Metadata capture (when you learn the flow's name, description, or tags):
[METADATA]
{"name": "...", "description": "...", "tags": ["..."]}
[/METADATA]

4. Intake form suggestion (when you identify variables the engineer will need):
[INTAKE_FORM]
[{"variable_name": "server_name", "label": "Server Name", "field_type": "text", "required": true, "placeholder": "e.g., DC01", "group_name": "Server Details", "display_order": 1}]
[/INTAKE_FORM]

IMPORTANT:
- Include [STEPS_UPDATE] sparingly. Only when concrete steps are established or modified.
- The steps update should be the COMPLETE working steps array, not a diff.
- Always include conversational text OUTSIDE the markers — never respond with only markers.
- The last step in the array MUST always be type "procedure_end".
"""

Step 2: Update _build_system_prompt to dispatch by flow_type

Replace the existing _build_system_prompt function:

def _build_system_prompt(flow_type: str) -> str:
    """Assemble the full system prompt for the chat builder."""
    if flow_type in ("procedural", "maintenance"):
        flow_context = (
            f"The user wants to build a {'MAINTENANCE' if flow_type == 'maintenance' else 'PROCEDURAL'} flow — "
            "a step-by-step process guide that walks engineers through a procedure in sequence. "
            "Steps are executed in order, not branching paths."
        )
        return f"{ROLE_PERSONA}\n\n{flow_context}\n\n{PROCEDURAL_SCHEMA_CONTEXT}\n\n{PROCEDURAL_INTERVIEW_PROTOCOL}\n\n{PROCEDURAL_RESPONSE_FORMAT}"
    else:
        flow_context = (
            "The user wants to build a TROUBLESHOOTING flow — a diagnostic decision tree "
            "that guides engineers through symptom identification, diagnostic checks, and "
            "resolution steps."
        )
        return f"{ROLE_PERSONA}\n\n{flow_context}\n\n{SCHEMA_CONTEXT}\n\n{INTERVIEW_PROTOCOL}\n\n{RESPONSE_FORMAT}"

Step 3: Update _parse_ai_response to handle [STEPS_UPDATE] and [INTAKE_FORM]

Add extraction for the new markers. After the [METADATA] extraction block:

    # Extract [STEPS_UPDATE]...[/STEPS_UPDATE]
    steps_match = re.search(
        r"\[STEPS_UPDATE\]\s*([\s\S]*?)\s*\[/STEPS_UPDATE\]", result["content"]
    )
    if steps_match:
        try:
            raw_json = _strip_markdown_fences(steps_match.group(1))
            result["tree_update"] = json.loads(raw_json)
        except (json.JSONDecodeError, ValueError) as e:
            logger.warning("Failed to parse steps update JSON: %s", e)
        result["content"] = result["content"][: steps_match.start()] + result["content"][steps_match.end() :]
    else:
        truncated_steps = re.search(r"\[STEPS_UPDATE\][\s\S]*$", result["content"])
        if truncated_steps:
            logger.warning("Truncated [STEPS_UPDATE] block detected — stripping from display")
            result["content"] = result["content"][: truncated_steps.start()]

    # Extract [INTAKE_FORM]...[/INTAKE_FORM]
    intake_match = re.search(
        r"\[INTAKE_FORM\]\s*([\s\S]*?)\s*\[/INTAKE_FORM\]", result["content"]
    )
    if intake_match:
        try:
            raw_json = _strip_markdown_fences(intake_match.group(1))
            result["intake_form"] = json.loads(raw_json)
        except (json.JSONDecodeError, ValueError) as e:
            logger.warning("Failed to parse intake form JSON: %s", e)
        result["content"] = result["content"][: intake_match.start()] + result["content"][intake_match.end() :]
    else:
        truncated_intake = re.search(r"\[INTAKE_FORM\][\s\S]*$", result["content"])
        if truncated_intake:
            logger.warning("Truncated [INTAKE_FORM] block detected — stripping from display")
            result["content"] = result["content"][: truncated_intake.start()]

Also add "intake_form": None to the initial result dict.

Step 4: Update send_message to validate procedural structure

In send_message(), replace the tree_update validation block (~line 320-326):

    # Validate tree update if present
    tree_update = parsed["tree_update"]
    if tree_update:
        if session.flow_type in ("procedural", "maintenance"):
            # Procedural: must have a steps array
            if not isinstance(tree_update, dict) or not isinstance(tree_update.get("steps"), list):
                logger.warning("AI steps update rejected: must have a steps array")
                tree_update = None
        else:
            # Troubleshooting: root must be a decision node
            if not isinstance(tree_update, dict) or tree_update.get("type") != "decision":
                logger.warning("AI tree update rejected: root must be a decision node")
                tree_update = None
            elif not tree_update.get("id"):
                logger.warning("AI tree update rejected: root node missing id")
                tree_update = None

Also handle intake_form persistence after the metadata block:

    if parsed.get("intake_form"):
        session.intake_form_draft = parsed["intake_form"]

Wait — AIChatSession may not have an intake_form_draft field. We'll store it in tree_metadata instead:

    if parsed.get("intake_form"):
        merged = dict(session.tree_metadata) if session.tree_metadata else {}
        merged["intake_form"] = parsed["intake_form"]
        session.tree_metadata = merged

Step 5: Update generate_final_tree for procedural flows

Replace the generation_instruction string with flow-type-aware instructions:

    if session.flow_type in ("procedural", "maintenance"):
        generation_instruction = """Based on our entire conversation, generate the COMPLETE and FINAL procedural steps JSON for this flow.

Requirements:
- Include ALL steps we discussed, organized into sections
- Use descriptive step IDs (slugs, not UUIDs)
- Each step needs: id, type, title, description
- Include commands with exact syntax where discussed
- Include content_type for each step (action, informational, verification, warning)
- Include estimated_minutes where discussed
- Include verification_prompt for verification steps
- Include warning_text for warning steps
- The LAST step MUST be type "procedure_end"
- Respond with ONLY the JSON — no conversational text, no markdown fences

Format: {"steps": [step1, step2, ..., end_step]}

Also provide metadata as a separate JSON object after the steps:
[METADATA]
{"name": "...", "description": "...", "tags": ["..."]}
[/METADATA]

If we discussed intake form variables, include them:
[INTAKE_FORM]
[{"variable_name": "...", "label": "...", "field_type": "text", "required": true, "display_order": 1}]
[/INTAKE_FORM]"""
    else:
        generation_instruction = """Based on our entire conversation, generate the COMPLETE and FINAL TreeStructure JSON for this flow.
...existing troubleshooting instruction..."""

Update the validation inside the generation loop to handle procedural:

        if not tree:
            # ... existing retry logic ...

        if session.flow_type in ("procedural", "maintenance"):
            # Validate procedural structure
            p_errors = validate_generated_procedural_steps(tree)
            if p_errors:
                if attempt == 0:
                    # ... retry with correction ...
                    continue
                raise ValueError(f"Generated steps failed validation: {'; '.join(p_errors)}")
        else:
            errors = validate_generated_tree(tree)
            if errors:
                # ... existing retry logic ...

Step 6: Run backend tests

Run: cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/test_ai_chat.py -v --override-ini="addopts=" Expected: All existing tests pass (they test troubleshooting flow).

Step 7: Commit

git add backend/app/core/ai_chat_service.py
git commit -m "feat: add procedural flow prompts to AI chat builder"

Task 2: Add Procedural Validation to AI Tree Validator

Files:

  • Modify: backend/app/core/ai_tree_validator.py

Step 1: Add validate_generated_procedural_steps function

Add after the existing count_tree_stats function:

VALID_PROCEDURAL_STEP_TYPES = {"procedure_step", "procedure_end", "section_header"}
VALID_CONTENT_TYPES = {"action", "informational", "verification", "warning"}


def validate_generated_procedural_steps(tree: dict[str, Any]) -> list[str]:
    """Validate an AI-generated procedural steps structure.

    Returns a list of error strings. Empty list means valid.
    """
    errors: list[str] = []

    if not isinstance(tree, dict):
        return ["Steps structure must be a JSON object"]

    steps = tree.get("steps")
    if not isinstance(steps, list) or len(steps) == 0:
        return ["Must have a non-empty 'steps' array"]

    seen_ids: set[str] = set()
    end_count = 0
    step_count = 0

    for i, step in enumerate(steps):
        if not isinstance(step, dict):
            errors.append(f"Step at index {i} is not an object")
            continue

        step_id = step.get("id")
        step_type = step.get("type")

        # Check ID
        if not step_id:
            errors.append(f"Step at index {i} missing 'id'")
        elif step_id in seen_ids:
            errors.append(f"Duplicate step ID: '{step_id}'")
        else:
            seen_ids.add(step_id)

        # Check type
        if step_type not in VALID_PROCEDURAL_STEP_TYPES:
            errors.append(
                f"Step '{step_id or i}' has invalid type '{step_type}'. "
                f"Must be one of: {', '.join(sorted(VALID_PROCEDURAL_STEP_TYPES))}"
            )
            continue

        # Check title
        if not step.get("title"):
            errors.append(f"Step '{step_id}' missing 'title'")

        # Content type validation
        content_type = step.get("content_type")
        if content_type and content_type not in VALID_CONTENT_TYPES:
            errors.append(
                f"Step '{step_id}' has invalid content_type '{content_type}'. "
                f"Must be one of: {', '.join(sorted(VALID_CONTENT_TYPES))}"
            )

        if step_type == "procedure_step":
            step_count += 1
        elif step_type == "procedure_end":
            end_count += 1

    # Structural checks
    if end_count == 0:
        errors.append("Must have exactly one 'procedure_end' step as the last step")
    elif end_count > 1:
        errors.append(f"Found {end_count} procedure_end steps, must have exactly 1")

    if end_count == 1 and steps[-1].get("type") != "procedure_end":
        errors.append("The procedure_end step must be the last step in the array")

    if step_count < 2:
        errors.append(
            f"Flow has only {step_count} procedure steps. "
            "Need at least 2 for a useful procedure."
        )

    if len(steps) > 100:
        errors.append(f"Flow has {len(steps)} steps. Maximum 100 allowed.")

    return errors

Step 2: Run tests

Run: cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/ -k "procedural" -v --override-ini="addopts=" Expected: Existing procedural tests pass.

Step 3: Commit

git add backend/app/core/ai_tree_validator.py
git commit -m "feat: add procedural steps validator for AI-generated flows"

Task 3: Handle Intake Form + Procedural Import in AI Chat Endpoint

Files:

  • Modify: backend/app/api/endpoints/ai_chat.py

Step 1: Update the import_tree endpoint to handle intake form from metadata

In the import_tree function (~line 393), after building the Tree object, check for intake form:

    # Extract intake form from metadata if present
    intake_form = None
    if metadata.get("intake_form"):
        intake_form = metadata.pop("intake_form")

    tree = Tree(
        name=data.name or metadata.get("name", "AI-Generated Flow"),
        description=data.description or metadata.get("description", ""),
        tree_type=session.flow_type,
        tree_structure=session.working_tree,
        intake_form=intake_form,
        author_id=current_user.id,
        account_id=current_user.account_id,
        category_id=data.category_id,
        is_public=False,
    )

Step 2: Run tests

Run: cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/test_ai_chat.py -v --override-ini="addopts=" Expected: All tests pass.

Step 3: Commit

git add backend/app/api/endpoints/ai_chat.py
git commit -m "feat: handle intake form in AI chat procedural import"

Task 4: Add Procedural Steps Preview Component (Frontend)

Files:

  • Create: frontend/src/components/ai-chat/StaticStepsPreview.tsx

Step 1: Create the procedural steps preview component

import type { ProceduralStep } from '@/types'
import { Terminal, Info, CheckSquare, AlertTriangle, LayoutList } from 'lucide-react'
import { cn } from '@/lib/utils'

interface StaticStepsPreviewProps {
  steps: ProceduralStep[]
  name?: string
}

const CONTENT_TYPE_ICONS: Record<string, typeof Terminal> = {
  action: Terminal,
  informational: Info,
  verification: CheckSquare,
  warning: AlertTriangle,
}

export function StaticStepsPreview({ steps, name }: StaticStepsPreviewProps) {
  let stepNumber = 0

  return (
    <div className="flex h-full flex-col">
      <div className="border-b border-border px-4 py-2">
        <h3 className="text-sm font-semibold text-foreground">
          Preview: {name || 'Untitled Flow'}
        </h3>
        <p className="text-xs text-muted-foreground">
          {steps.filter((s) => s.type === 'procedure_step').length} steps
        </p>
      </div>
      <div className="flex-1 overflow-auto p-4">
        <div className="space-y-1.5">
          {steps.map((step) => {
            if (step.type === 'section_header') {
              return (
                <div key={step.id} className="pt-3 pb-1 first:pt-0">
                  <div className="flex items-center gap-2">
                    <LayoutList className="h-3.5 w-3.5 text-primary" />
                    <span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-primary">
                      {step.title}
                    </span>
                  </div>
                </div>
              )
            }

            if (step.type === 'procedure_end') {
              return (
                <div
                  key={step.id}
                  className="mt-2 rounded-lg border border-emerald-500/20 bg-emerald-500/5 px-3 py-2"
                >
                  <span className="text-xs font-medium text-emerald-400">
                    {step.title || 'Procedure Complete'}
                  </span>
                </div>
              )
            }

            stepNumber++
            const contentType = step.content_type || 'action'
            const Icon = CONTENT_TYPE_ICONS[contentType] || Terminal

            return (
              <div
                key={step.id}
                className={cn(
                  'rounded-lg border px-3 py-2 text-xs',
                  contentType === 'warning'
                    ? 'border-amber-500/20 bg-amber-500/5'
                    : 'border-border bg-card'
                )}
              >
                <div className="flex items-start gap-2">
                  <span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded bg-primary/10 font-label text-[0.5rem] text-primary">
                    {stepNumber}
                  </span>
                  <div className="min-w-0 flex-1">
                    <div className="flex items-center gap-1.5">
                      <Icon className={cn(
                        'h-3 w-3 shrink-0',
                        contentType === 'warning' ? 'text-amber-400' : 'text-muted-foreground'
                      )} />
                      <span className={cn(
                        'font-medium truncate',
                        contentType === 'warning' ? 'text-amber-400' : 'text-foreground'
                      )}>
                        {step.title}
                      </span>
                    </div>
                    {step.commands && (
                      <div className="mt-1 flex items-center gap-1 text-muted-foreground">
                        <Terminal className="h-2.5 w-2.5" />
                        <span className="font-label text-[0.5rem]">
                          {Array.isArray(step.commands) ? step.commands.length : 1} command{(Array.isArray(step.commands) ? step.commands.length : 1) !== 1 ? 's' : ''}
                        </span>
                      </div>
                    )}
                  </div>
                  {step.estimated_minutes && (
                    <span className="shrink-0 font-label text-[0.5rem] text-muted-foreground">
                      ~{step.estimated_minutes}m
                    </span>
                  )}
                </div>
              </div>
            )
          })}
        </div>
      </div>
    </div>
  )
}

Step 2: Run build

Run: cd /home/michaelchihlas/dev/patherly/frontend && npm run build Expected: Build passes.

Step 3: Commit

git add frontend/src/components/ai-chat/StaticStepsPreview.tsx
git commit -m "feat: add procedural steps preview component for AI chat builder"

Task 5: Update AI Chat Store + Page for Procedural Flows

Files:

  • Modify: frontend/src/store/aiChatStore.ts
  • Modify: frontend/src/pages/AIChatBuilderPage.tsx

Step 1: Update AIChatState interface and sendMessage handler in store

In aiChatStore.ts, update the workingTree type to also accept procedural structure:

// Change line 29:
workingTree: TreeStructure | { steps: ProceduralStep[] } | null
// Change line 33:
generatedTree: TreeStructure | { steps: ProceduralStep[] } | null

Add ProceduralStep to the imports:

import type {
  ChatMessage,
  InterviewPhase,
  TreeStructure,
  ProceduralStep,
} from '@/types'

Update sendMessage (~line 121-127) — the response handling already works because working_tree is stored as-is from the API. The cast just needs updating:

workingTree: (response.working_tree as TreeStructure | { steps: ProceduralStep[] } | null) ?? state.workingTree,

And in generateTree (~line 142-143):

generatedTree: response.tree_structure as unknown as TreeStructure | { steps: ProceduralStep[] },
workingTree: response.tree_structure as unknown as TreeStructure | { steps: ProceduralStep[] },

And in resumeSession (~line 185-187):

workingTree: session.working_tree as TreeStructure | { steps: ProceduralStep[] } | null,
generatedTree: session.generated_tree as TreeStructure | { steps: ProceduralStep[] } | null,

Step 2: Update AIChatBuilderPage.tsx to render correct preview

Add import for StaticStepsPreview and ProceduralStep:

import { StaticStepsPreview } from '@/components/ai-chat/StaticStepsPreview'
import type { ProceduralStep } from '@/types'

Replace the preview tree logic (~line 116) and preview render (~line 143-151):

  const previewData = generatedTree || workingTree

  // Determine if this is a procedural preview
  const isProceduralPreview = previewData && 'steps' in previewData

  // ... in the JSX:
  {previewData ? (
    isProceduralPreview ? (
      <StaticStepsPreview
        steps={(previewData as { steps: ProceduralStep[] }).steps}
        name={treeMetadata?.name}
      />
    ) : (
      <StaticTreePreview
        tree={previewData as TreeStructure}
        name={treeMetadata?.name}
      />
    )
  ) : (
    <EmptyPreview />
  )}

Remove the now-unused const previewTree = (generatedTree || workingTree) as TreeStructure | null line.

Step 3: Run build

Run: cd /home/michaelchihlas/dev/patherly/frontend && npm run build Expected: Build passes.

Step 4: Commit

git add frontend/src/store/aiChatStore.ts frontend/src/pages/AIChatBuilderPage.tsx
git commit -m "feat: wire procedural steps preview into AI chat builder page"

Task 6: Update generate_final_tree Generation + Validation Wiring

Files:

  • Modify: backend/app/core/ai_chat_service.py

This task ensures the full generate_final_tree function properly handles the procedural path end-to-end, including the retry loop and validation import.

Step 1: Add import for the new validator

from app.core.ai_tree_validator import validate_generated_tree, validate_generated_procedural_steps

Step 2: Update the validation block in generate_final_tree

Inside the for attempt in range(2) loop, after tree is extracted, replace the validation block:

        if session.flow_type in ("procedural", "maintenance"):
            val_errors = validate_generated_procedural_steps(tree)
        else:
            val_errors = validate_generated_tree(tree)

        if val_errors:
            if attempt == 0:
                provider_messages.append({"role": "assistant", "content": response_text})
                correction = (
                    f"The generated structure has validation errors: {'; '.join(val_errors)}. "
                    "Please fix these issues and respond with the corrected JSON only."
                )
                provider_messages.append({"role": "user", "content": correction})
                continue
            raise ValueError(f"Generated structure failed validation: {'; '.join(val_errors)}")

Step 3: Handle intake form from final generation

After the # Success comment, before returning:

        # Extract intake form from metadata if present
        if parsed.get("intake_form") and isinstance(parsed["intake_form"], list):
            metadata["intake_form"] = parsed["intake_form"]

Step 4: Run backend tests

Run: cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/test_ai_chat.py -v --override-ini="addopts=" Expected: All tests pass.

Step 5: Commit

git add backend/app/core/ai_chat_service.py
git commit -m "feat: wire procedural validation into AI chat generate flow"

Task 7: Final Integration Test + Build Verification

Step 1: Run full backend test suite

Run: cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/ --override-ini="addopts=" -v Expected: All tests pass.

Step 2: Run frontend build

Run: cd /home/michaelchihlas/dev/patherly/frontend && npm run build Expected: Build passes with zero errors.

Step 3: Final commit (if any remaining changes)

git add -A
git commit -m "chore: final cleanup for procedural Flow Assist support"

Summary of Changes

File Change
backend/app/core/ai_chat_service.py Add procedural schema/protocol/format prompts, dispatch by flow_type, parse [STEPS_UPDATE] + [INTAKE_FORM], validate procedural structure, procedural generation instruction
backend/app/core/ai_tree_validator.py Add validate_generated_procedural_steps() function
backend/app/api/endpoints/ai_chat.py Handle intake form in import endpoint
frontend/src/components/ai-chat/StaticStepsPreview.tsx New procedural steps preview component
frontend/src/store/aiChatStore.ts Widen workingTree/generatedTree types for procedural
frontend/src/pages/AIChatBuilderPage.tsx Render StaticStepsPreview for procedural flows